1use denise::{ElementState, KeyCode, Modifiers};
48
49#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
51pub enum Output {
52 #[default]
54 None,
55 Char(char),
57 Dead(char),
63}
64
65impl Output {
66 #[inline]
67 const fn is_none(self) -> bool {
68 matches!(self, Output::None)
69 }
70}
71
72#[derive(Clone, Copy, Debug)]
74pub struct Entry {
75 pub code: KeyCode,
77 pub base: Output,
79 pub shift: Output,
81 pub altgr: Output,
83 pub shift_altgr: Output,
85}
86
87impl Entry {
88 const fn pair(code: KeyCode, base: char, shift: char) -> Self {
90 Self {
91 code,
92 base: Output::Char(base),
93 shift: Output::Char(shift),
94 altgr: Output::None,
95 shift_altgr: Output::None,
96 }
97 }
98
99 const fn triple(code: KeyCode, base: char, shift: char, altgr: char) -> Self {
101 Self {
102 code,
103 base: Output::Char(base),
104 shift: Output::Char(shift),
105 altgr: Output::Char(altgr),
106 shift_altgr: Output::None,
107 }
108 }
109
110 const fn letter(code: KeyCode, lower: char, upper: char) -> Self {
112 Self::pair(code, lower, upper)
113 }
114
115 #[inline]
116 const fn at(&self, shift: bool, level3: bool) -> Output {
117 match (shift, level3) {
118 (false, false) => self.base,
119 (true, false) => self.shift,
120 (false, true) => self.altgr,
121 (true, true) => {
122 if self.shift_altgr.is_none() {
125 self.altgr
126 } else {
127 self.shift_altgr
128 }
129 }
130 }
131 }
132}
133
134#[derive(Clone, Copy, Debug)]
136pub struct Layout {
137 pub name: &'static str,
139 pub entries: &'static [Entry],
143 pub decimal_separator: char,
146}
147
148impl Layout {
149 fn entry(&self, code: KeyCode) -> Option<&'static Entry> {
150 self.entries
153 .iter()
154 .find(|entry| entry.code == code)
155 .or_else(|| LETTERS.iter().find(|entry| entry.code == code))
156 }
157}
158
159#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
165pub struct Composed {
166 chars: [char; 2],
167 len: u8,
168}
169
170impl Composed {
171 pub const NONE: Self = Self {
173 chars: ['\0', '\0'],
174 len: 0,
175 };
176
177 const fn one(ch: char) -> Self {
178 Self {
179 chars: [ch, '\0'],
180 len: 1,
181 }
182 }
183
184 const fn two(first: char, second: char) -> Self {
185 Self {
186 chars: [first, second],
187 len: 2,
188 }
189 }
190
191 #[inline]
193 pub fn as_slice(&self) -> &[char] {
194 &self.chars[..self.len as usize]
195 }
196
197 #[inline]
199 pub const fn is_empty(&self) -> bool {
200 self.len == 0
201 }
202}
203
204#[derive(Clone, Debug)]
210pub struct Composer {
211 layout: &'static Layout,
212 pending_dead: Option<char>,
213 caps_lock: bool,
214 num_lock: bool,
215 level3: bool,
219}
220
221impl Composer {
222 pub fn new(layout: &'static Layout) -> Self {
225 Self {
226 layout,
227 pending_dead: None,
228 caps_lock: false,
229 num_lock: true,
230 level3: false,
231 }
232 }
233
234 #[inline]
236 pub const fn layout(&self) -> &'static Layout {
237 self.layout
238 }
239
240 pub fn set_layout(&mut self, layout: &'static Layout) {
242 self.layout = layout;
243 self.pending_dead = None;
244 }
245
246 #[inline]
248 pub const fn pending_dead(&self) -> Option<char> {
249 self.pending_dead
250 }
251
252 #[inline]
254 pub const fn caps_lock(&self) -> bool {
255 self.caps_lock
256 }
257
258 pub fn feed(&mut self, code: KeyCode, state: ElementState, modifiers: Modifiers) -> Composed {
262 if code == KeyCode::AltRight {
263 self.level3 = state.is_down();
264 return Composed::NONE;
265 }
266 if state != ElementState::Down {
267 return Composed::NONE;
268 }
269 match code {
270 KeyCode::CapsLock => {
271 self.caps_lock = !self.caps_lock;
272 return Composed::NONE;
273 }
274 KeyCode::NumLock => {
275 self.num_lock = !self.num_lock;
276 return Composed::NONE;
277 }
278 _ => {}
279 }
280
281 let chord = if self.level3 {
287 modifiers.contains(Modifiers::SUPER)
288 } else {
289 modifiers.contains(Modifiers::CTRL)
290 || modifiers.contains(Modifiers::SUPER)
291 || modifiers.contains(Modifiers::ALT)
292 };
293 if chord {
294 self.pending_dead = None;
295 return Composed::NONE;
296 }
297
298 let shift = modifiers.contains(Modifiers::SHIFT);
299 let output = self.output_for(code, shift);
300 match output {
301 Output::None => {
302 self.pending_dead = None;
306 Composed::NONE
307 }
308 Output::Dead(mark) => match self.pending_dead.replace(mark) {
309 Some(previous) if previous == mark => {
311 self.pending_dead = None;
312 Composed::one(mark)
313 }
314 Some(previous) => Composed::one(previous),
315 None => Composed::NONE,
316 },
317 Output::Char(ch) => match self.pending_dead.take() {
318 None => Composed::one(ch),
319 Some(mark) if ch == ' ' => Composed::one(mark),
321 Some(mark) => match compose(mark, ch) {
322 Some(combined) => Composed::one(combined),
323 None => Composed::two(mark, ch),
324 },
325 },
326 }
327 }
328
329 fn output_for(&self, code: KeyCode, shift: bool) -> Output {
330 if let Some(output) = self.numpad(code) {
331 return output;
332 }
333 if code == KeyCode::Space {
334 return Output::Char(' ');
335 }
336 let Some(entry) = self.layout.entry(code) else {
337 return Output::None;
338 };
339 let shift = shift != (self.caps_lock && is_letter(entry));
342 entry.at(shift, self.level3)
343 }
344
345 fn numpad(&self, code: KeyCode) -> Option<Output> {
346 let digit = match code {
347 KeyCode::Numpad0 => '0',
348 KeyCode::Numpad1 => '1',
349 KeyCode::Numpad2 => '2',
350 KeyCode::Numpad3 => '3',
351 KeyCode::Numpad4 => '4',
352 KeyCode::Numpad5 => '5',
353 KeyCode::Numpad6 => '6',
354 KeyCode::Numpad7 => '7',
355 KeyCode::Numpad8 => '8',
356 KeyCode::Numpad9 => '9',
357 KeyCode::NumpadDecimal => self.layout.decimal_separator,
358 KeyCode::NumpadAdd => return Some(Output::Char('+')),
359 KeyCode::NumpadSubtract => return Some(Output::Char('-')),
360 KeyCode::NumpadMultiply => return Some(Output::Char('*')),
361 KeyCode::NumpadDivide => return Some(Output::Char('/')),
362 _ => return None,
363 };
364 Some(if self.num_lock {
367 Output::Char(digit)
368 } else {
369 Output::None
370 })
371 }
372}
373
374fn is_letter(entry: &Entry) -> bool {
376 matches!(
377 (entry.base, entry.shift),
378 (Output::Char(lower), Output::Char(upper))
379 if lower.is_alphabetic() && upper.is_alphabetic()
380 )
381}
382
383fn compose(mark: char, base: char) -> Option<char> {
385 COMPOSE
386 .binary_search_by(|&(m, b, _)| (m, b).cmp(&(mark, base)))
387 .ok()
388 .map(|index| COMPOSE[index].2)
389}
390
391const LETTERS: [Entry; 26] = {
398 use KeyCode as K;
399 [
400 Entry::letter(K::A, 'a', 'A'),
401 Entry::letter(K::B, 'b', 'B'),
402 Entry::letter(K::C, 'c', 'C'),
403 Entry::letter(K::D, 'd', 'D'),
404 Entry::letter(K::E, 'e', 'E'),
405 Entry::letter(K::F, 'f', 'F'),
406 Entry::letter(K::G, 'g', 'G'),
407 Entry::letter(K::H, 'h', 'H'),
408 Entry::letter(K::I, 'i', 'I'),
409 Entry::letter(K::J, 'j', 'J'),
410 Entry::letter(K::K, 'k', 'K'),
411 Entry::letter(K::L, 'l', 'L'),
412 Entry::letter(K::M, 'm', 'M'),
413 Entry::letter(K::N, 'n', 'N'),
414 Entry::letter(K::O, 'o', 'O'),
415 Entry::letter(K::P, 'p', 'P'),
416 Entry::letter(K::Q, 'q', 'Q'),
417 Entry::letter(K::R, 'r', 'R'),
418 Entry::letter(K::S, 's', 'S'),
419 Entry::letter(K::T, 't', 'T'),
420 Entry::letter(K::U, 'u', 'U'),
421 Entry::letter(K::V, 'v', 'V'),
422 Entry::letter(K::W, 'w', 'W'),
423 Entry::letter(K::X, 'x', 'X'),
424 Entry::letter(K::Y, 'y', 'Y'),
425 Entry::letter(K::Z, 'z', 'Z'),
426 ]
427};
428
429const US_ENTRIES: [Entry; 22] = {
430 use KeyCode as K;
431 [
432 Entry::pair(K::Digit1, '1', '!'),
433 Entry::pair(K::Digit2, '2', '@'),
434 Entry::pair(K::Digit3, '3', '#'),
435 Entry::pair(K::Digit4, '4', '$'),
436 Entry::pair(K::Digit5, '5', '%'),
437 Entry::pair(K::Digit6, '6', '^'),
438 Entry::pair(K::Digit7, '7', '&'),
439 Entry::pair(K::Digit8, '8', '*'),
440 Entry::pair(K::Digit9, '9', '('),
441 Entry::pair(K::Digit0, '0', ')'),
442 Entry::pair(K::Minus, '-', '_'),
443 Entry::pair(K::Equal, '=', '+'),
444 Entry::pair(K::BracketLeft, '[', '{'),
445 Entry::pair(K::BracketRight, ']', '}'),
446 Entry::pair(K::Backslash, '\\', '|'),
447 Entry::pair(K::Semicolon, ';', ':'),
448 Entry::pair(K::Quote, '\'', '"'),
449 Entry::pair(K::Backquote, '`', '~'),
450 Entry::pair(K::Comma, ',', '<'),
451 Entry::pair(K::Period, '.', '>'),
452 Entry::pair(K::Slash, '/', '?'),
453 Entry::pair(K::IntlBackslash, '\\', '|'),
456 ]
457};
458
459pub static US: Layout = Layout {
461 name: "us",
462 entries: &US_ENTRIES,
463 decimal_separator: '.',
464};
465
466const NORWEGIAN_ENTRIES: [Entry; 24] = {
467 use KeyCode as K;
468 [
469 Entry::pair(K::Backquote, '|', '\u{00a7}'),
470 Entry::pair(K::Digit1, '1', '!'),
471 Entry::triple(K::Digit2, '2', '"', '@'),
472 Entry::triple(K::Digit3, '3', '#', '\u{00a3}'),
473 Entry::triple(K::Digit4, '4', '\u{00a4}', '$'),
474 Entry::triple(K::Digit5, '5', '%', '\u{20ac}'),
475 Entry::pair(K::Digit6, '6', '&'),
476 Entry::triple(K::Digit7, '7', '/', '{'),
477 Entry::triple(K::Digit8, '8', '(', '['),
478 Entry::triple(K::Digit9, '9', ')', ']'),
479 Entry::triple(K::Digit0, '0', '=', '}'),
480 Entry::triple(K::Minus, '+', '?', '\\'),
481 Entry {
484 code: K::Equal,
485 base: Output::Dead('\u{00b4}'),
486 shift: Output::Dead('`'),
487 altgr: Output::Char('|'),
488 shift_altgr: Output::None,
489 },
490 Entry::letter(K::BracketLeft, '\u{00e5}', '\u{00c5}'),
491 Entry {
494 code: K::BracketRight,
495 base: Output::Dead('\u{00a8}'),
496 shift: Output::Dead('^'),
497 altgr: Output::Dead('~'),
498 shift_altgr: Output::None,
499 },
500 Entry::letter(K::Semicolon, '\u{00f8}', '\u{00d8}'),
501 Entry::letter(K::Quote, '\u{00e6}', '\u{00c6}'),
502 Entry::pair(K::Backslash, '\'', '*'),
503 Entry::triple(K::IntlBackslash, '<', '>', '\\'),
504 Entry::pair(K::Comma, ',', ';'),
505 Entry::pair(K::Period, '.', ':'),
506 Entry::pair(K::Slash, '-', '_'),
507 Entry::triple(K::E, 'e', 'E', '\u{20ac}'),
509 Entry::triple(K::M, 'm', 'M', '\u{00b5}'),
510 ]
511};
512
513pub static NORWEGIAN: Layout = Layout {
518 name: "no",
519 entries: &NORWEGIAN_ENTRIES,
520 decimal_separator: ',',
521};
522
523pub static BUILT_IN: [&Layout; 2] = [&US, &NORWEGIAN];
525
526pub fn by_name(name: &str) -> Option<&'static Layout> {
528 BUILT_IN
529 .iter()
530 .copied()
531 .find(|layout| layout.name.eq_ignore_ascii_case(name))
532}
533const COMPOSE: [(char, char, char); 118] = [
539 ('^', '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}'), ];
666
667#[derive(Clone, Copy, Debug, PartialEq, Eq)]
669pub enum LayoutSource {
670 Denise,
672 Xkb,
674 File(&'static str),
676 Default,
678}
679
680impl core::fmt::Display for LayoutSource {
681 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
682 match self {
683 LayoutSource::Denise => f.write_str("DENISE_KEYMAP"),
684 LayoutSource::Xkb => f.write_str("XKB_DEFAULT_LAYOUT"),
685 LayoutSource::File(path) => write!(f, "{path}"),
686 LayoutSource::Default => f.write_str("default"),
687 }
688 }
689}
690
691const SYSTEM_FILES: [(&str, &str); 4] = [
694 ("/etc/vconsole.conf", "KEYMAP"),
696 ("/etc/default/keyboard", "XKBLAYOUT"),
698 ("/etc/conf.d/loadkmap", "KEYMAP"),
700 ("/etc/rc.conf", "KEYMAP"),
702];
703
704pub fn normalise_name(raw: &str) -> &str {
710 let raw = raw.trim().trim_matches(['"', '\'']);
711 let base = raw.rsplit('/').next().unwrap_or(raw);
712 let stem = base.split('.').next().unwrap_or(base);
713 if by_name(stem).is_some() {
714 return stem;
715 }
716 stem.split('-').next().unwrap_or(stem)
717}
718
719pub fn from_system() -> (&'static Layout, LayoutSource) {
737 for (variable, source) in [
738 ("DENISE_KEYMAP", LayoutSource::Denise),
739 ("XKB_DEFAULT_LAYOUT", LayoutSource::Xkb),
740 ] {
741 if let Ok(value) = std::env::var(variable)
742 && let Some(layout) = by_name(normalise_name(&value))
743 {
744 return (layout, source);
745 }
746 }
747
748 for (path, key) in SYSTEM_FILES {
749 let Ok(contents) = std::fs::read_to_string(path) else {
750 continue;
751 };
752 if let Some(value) = value_of(&contents, key)
753 && let Some(layout) = by_name(normalise_name(value))
754 {
755 return (layout, LayoutSource::File(path));
756 }
757 }
758
759 (&US, LayoutSource::Default)
760}
761
762fn value_of<'a>(contents: &'a str, key: &str) -> Option<&'a str> {
764 contents.lines().find_map(|line| {
765 let line = line.trim();
766 if line.starts_with('#') {
767 return None;
768 }
769 let (name, value) = line.split_once('=')?;
770 (name.trim() == key).then(|| value.trim())
771 })
772}
773
774#[cfg(test)]
775mod tests {
776 use super::*;
777
778 fn type_keys(composer: &mut Composer, keys: &[(KeyCode, Modifiers)]) -> String {
780 let mut out = String::new();
781 for &(code, modifiers) in keys {
782 if modifiers.contains(Modifiers::ALT) {
785 composer.feed(KeyCode::AltRight, ElementState::Down, Modifiers::ALT);
786 }
787 let composed = composer.feed(code, ElementState::Down, modifiers);
788 out.extend(composed.as_slice());
789 composer.feed(code, ElementState::Up, modifiers);
790 if modifiers.contains(Modifiers::ALT) {
791 composer.feed(KeyCode::AltRight, ElementState::Up, Modifiers::NONE);
792 }
793 }
794 out
795 }
796
797 fn plain(keys: &[KeyCode]) -> Vec<(KeyCode, Modifiers)> {
798 keys.iter().map(|&k| (k, Modifiers::NONE)).collect()
799 }
800
801 #[test]
802 fn us_types_ascii() {
803 let mut c = Composer::new(&US);
804 assert_eq!(
805 type_keys(&mut c, &plain(&[KeyCode::H, KeyCode::I, KeyCode::Digit1])),
806 "hi1"
807 );
808 assert_eq!(
809 type_keys(
810 &mut c,
811 &[
812 (KeyCode::H, Modifiers::SHIFT),
813 (KeyCode::Digit1, Modifiers::SHIFT),
814 ]
815 ),
816 "H!"
817 );
818 }
819
820 #[test]
821 fn norwegian_types_the_three_letters_it_exists_for() {
822 let mut c = Composer::new(&NORWEGIAN);
823 assert_eq!(
826 type_keys(
827 &mut c,
828 &plain(&[KeyCode::Quote, KeyCode::Semicolon, KeyCode::BracketLeft])
829 ),
830 "æøå"
831 );
832 assert_eq!(
833 type_keys(
834 &mut c,
835 &[
836 (KeyCode::Quote, Modifiers::SHIFT),
837 (KeyCode::Semicolon, Modifiers::SHIFT),
838 (KeyCode::BracketLeft, Modifiers::SHIFT),
839 ]
840 ),
841 "ÆØÅ"
842 );
843 }
844
845 #[test]
846 fn the_same_positions_type_ascii_on_a_us_layout() {
847 let mut c = Composer::new(&US);
848 assert_eq!(
849 type_keys(
850 &mut c,
851 &plain(&[KeyCode::Quote, KeyCode::Semicolon, KeyCode::BracketLeft])
852 ),
853 "';["
854 );
855 }
856
857 #[test]
858 fn dead_keys_compose() {
859 let mut c = Composer::new(&NORWEGIAN);
860 assert_eq!(
862 type_keys(&mut c, &plain(&[KeyCode::BracketRight, KeyCode::O])),
863 "ö"
864 );
865 assert_eq!(
867 type_keys(&mut c, &plain(&[KeyCode::Equal, KeyCode::E])),
868 "é"
869 );
870 assert_eq!(
871 type_keys(
872 &mut c,
873 &[
874 (KeyCode::Equal, Modifiers::SHIFT),
875 (KeyCode::A, Modifiers::NONE)
876 ]
877 ),
878 "à"
879 );
880 assert_eq!(
882 type_keys(
883 &mut c,
884 &[
885 (KeyCode::BracketRight, Modifiers::SHIFT),
886 (KeyCode::O, Modifiers::NONE)
887 ]
888 ),
889 "ô"
890 );
891 assert_eq!(
892 type_keys(
893 &mut c,
894 &[
895 (KeyCode::BracketRight, Modifiers::ALT),
896 (KeyCode::N, Modifiers::NONE)
897 ]
898 ),
899 "ñ"
900 );
901 }
902
903 #[test]
904 fn a_dead_key_produces_nothing_until_it_is_resolved() {
905 let mut c = Composer::new(&NORWEGIAN);
906 let composed = c.feed(KeyCode::BracketRight, ElementState::Down, Modifiers::NONE);
907 assert!(composed.is_empty(), "a dead key must not type anything yet");
908 assert_eq!(c.pending_dead(), Some('¨'));
909 }
910
911 #[test]
912 fn a_dead_key_twice_types_the_mark_itself() {
913 let mut c = Composer::new(&NORWEGIAN);
914 assert_eq!(
915 type_keys(
916 &mut c,
917 &plain(&[KeyCode::BracketRight, KeyCode::BracketRight])
918 ),
919 "¨"
920 );
921 assert_eq!(c.pending_dead(), None);
922 }
923
924 #[test]
925 fn space_after_a_dead_key_types_the_bare_mark() {
926 let mut c = Composer::new(&NORWEGIAN);
927 assert_eq!(
928 type_keys(&mut c, &plain(&[KeyCode::BracketRight, KeyCode::Space])),
929 "¨"
930 );
931 }
932
933 #[test]
934 fn a_dead_key_that_cannot_combine_emits_both() {
935 let mut c = Composer::new(&NORWEGIAN);
936 assert_eq!(
939 type_keys(&mut c, &plain(&[KeyCode::BracketRight, KeyCode::Q])),
940 "¨q"
941 );
942 }
943
944 #[test]
945 fn a_key_that_types_nothing_cancels_a_pending_mark() {
946 let mut c = Composer::new(&NORWEGIAN);
947 c.feed(KeyCode::BracketRight, ElementState::Down, Modifiers::NONE);
948 assert_eq!(c.pending_dead(), Some('¨'));
949 c.feed(KeyCode::Escape, ElementState::Down, Modifiers::NONE);
950 assert_eq!(
951 c.pending_dead(),
952 None,
953 "Escape must not leave a latch behind"
954 );
955 assert_eq!(type_keys(&mut c, &plain(&[KeyCode::O])), "o");
956 }
957
958 #[test]
959 fn switching_layouts_abandons_a_half_typed_composition() {
960 let mut c = Composer::new(&NORWEGIAN);
961 c.feed(KeyCode::BracketRight, ElementState::Down, Modifiers::NONE);
962 c.set_layout(&US);
963 assert_eq!(c.pending_dead(), None);
964 }
965
966 #[test]
967 fn the_third_level_needs_the_right_alt_key() {
968 let mut c = Composer::new(&NORWEGIAN);
969 assert_eq!(type_keys(&mut c, &[(KeyCode::Digit2, Modifiers::ALT)]), "@");
970 assert_eq!(type_keys(&mut c, &[(KeyCode::Digit7, Modifiers::ALT)]), "{");
971 assert_eq!(type_keys(&mut c, &[(KeyCode::E, Modifiers::ALT)]), "€");
972
973 let mut c = Composer::new(&NORWEGIAN);
975 let composed = c.feed(KeyCode::Digit2, ElementState::Down, Modifiers::ALT);
976 assert!(
977 composed.is_empty(),
978 "left Alt must not reach the third level"
979 );
980 }
981
982 #[test]
983 fn altgr_reaches_the_third_level_even_reported_as_ctrl_plus_alt() {
984 let mut c = Composer::new(&NORWEGIAN);
988 c.feed(KeyCode::ControlLeft, ElementState::Down, Modifiers::CTRL);
989 c.feed(
990 KeyCode::AltRight,
991 ElementState::Down,
992 Modifiers::CTRL | Modifiers::ALT,
993 );
994 let composed = c.feed(
995 KeyCode::Digit2,
996 ElementState::Down,
997 Modifiers::CTRL | Modifiers::ALT,
998 );
999 assert_eq!(composed.as_slice(), ['@']);
1000
1001 c.feed(KeyCode::AltRight, ElementState::Up, Modifiers::CTRL);
1003 let composed = c.feed(KeyCode::Digit2, ElementState::Down, Modifiers::CTRL);
1004 assert!(composed.is_empty(), "Ctrl+2 is a binding, not an at sign");
1005 }
1006
1007 #[test]
1008 fn control_chords_type_nothing() {
1009 let mut c = Composer::new(&US);
1010 for modifier in [Modifiers::CTRL, Modifiers::SUPER] {
1011 let composed = c.feed(KeyCode::C, ElementState::Down, modifier);
1012 assert!(composed.is_empty(), "{modifier:?} + C must not type a c");
1013 }
1014 }
1015
1016 #[test]
1017 fn control_and_enter_and_backspace_are_never_text() {
1018 let mut c = Composer::new(&NORWEGIAN);
1019 for code in [
1020 KeyCode::Enter,
1021 KeyCode::Tab,
1022 KeyCode::Backspace,
1023 KeyCode::Delete,
1024 KeyCode::ArrowLeft,
1025 KeyCode::F1,
1026 ] {
1027 let composed = c.feed(code, ElementState::Down, Modifiers::NONE);
1028 assert!(composed.is_empty(), "{code:?} must not produce text");
1029 }
1030 }
1031
1032 #[test]
1033 fn caps_lock_shifts_letters_and_leaves_the_digit_row_alone() {
1034 let mut c = Composer::new(&NORWEGIAN);
1035 c.feed(KeyCode::CapsLock, ElementState::Down, Modifiers::NONE);
1036 assert!(c.caps_lock());
1037 assert_eq!(
1038 type_keys(
1039 &mut c,
1040 &plain(&[KeyCode::A, KeyCode::Quote, KeyCode::Digit1])
1041 ),
1042 "AÆ1",
1043 "caps lock must reach æøå but not turn 1 into !"
1044 );
1045 assert_eq!(type_keys(&mut c, &[(KeyCode::A, Modifiers::SHIFT)]), "a");
1047 c.feed(KeyCode::CapsLock, ElementState::Down, Modifiers::NONE);
1048 assert!(!c.caps_lock());
1049 }
1050
1051 #[test]
1052 fn the_numpad_follows_num_lock_and_the_layout() {
1053 let mut us = Composer::new(&US);
1054 assert_eq!(
1055 type_keys(&mut us, &plain(&[KeyCode::Numpad4, KeyCode::NumpadDecimal])),
1056 "4."
1057 );
1058 let mut no = Composer::new(&NORWEGIAN);
1059 assert_eq!(
1060 type_keys(&mut no, &plain(&[KeyCode::Numpad4, KeyCode::NumpadDecimal])),
1061 "4,",
1062 "a European numpad types a decimal comma"
1063 );
1064
1065 no.feed(KeyCode::NumLock, ElementState::Down, Modifiers::NONE);
1066 let composed = no.feed(KeyCode::Numpad4, ElementState::Down, Modifiers::NONE);
1067 assert!(
1068 composed.is_empty(),
1069 "with num lock off the numpad is arrows, not digits"
1070 );
1071 }
1072
1073 #[test]
1074 fn key_release_types_nothing() {
1075 let mut c = Composer::new(&US);
1076 let composed = c.feed(KeyCode::A, ElementState::Up, Modifiers::NONE);
1077 assert!(composed.is_empty(), "a key types on the way down, once");
1078 }
1079
1080 #[test]
1081 fn the_compose_table_is_sorted_and_free_of_duplicates() {
1082 assert!(
1083 COMPOSE
1084 .windows(2)
1085 .all(|w| (w[0].0, w[0].1) < (w[1].0, w[1].1)),
1086 "lookup bisects, so an unsorted table would silently miss entries"
1087 );
1088 }
1089
1090 #[test]
1091 fn composition_matches_unicode() {
1092 for (mark, base, expected) in [
1096 ('´', 'e', 'é'),
1097 ('`', 'a', 'à'),
1098 ('¨', 'u', 'ü'),
1099 ('^', 'i', 'î'),
1100 ('~', 'n', 'ñ'),
1101 ('\u{02da}', 'a', 'å'),
1102 ('¸', 'c', 'ç'),
1103 ('\u{02c7}', 's', 'š'),
1104 ] {
1105 assert_eq!(compose(mark, base), Some(expected), "{mark}{base}");
1106 }
1107 assert_eq!(compose('¨', 'q'), None);
1108 assert_eq!(compose('!', 'a'), None);
1109 }
1110
1111 #[test]
1112 fn no_layout_lists_a_position_twice() {
1113 for layout in BUILT_IN {
1114 for (i, entry) in layout.entries.iter().enumerate() {
1115 assert!(
1116 !layout.entries[..i].iter().any(|e| e.code == entry.code),
1117 "{} lists {:?} twice; the first would silently win",
1118 layout.name,
1119 entry.code
1120 );
1121 }
1122 }
1123 }
1124
1125 #[test]
1126 fn every_layout_can_type_the_whole_alphabet_and_the_digits() {
1127 for layout in BUILT_IN {
1128 let mut c = Composer::new(layout);
1129 let letters = type_keys(
1130 &mut c,
1131 &plain(&[
1132 KeyCode::A,
1133 KeyCode::B,
1134 KeyCode::C,
1135 KeyCode::X,
1136 KeyCode::Y,
1137 KeyCode::Z,
1138 ]),
1139 );
1140 assert_eq!(letters, "abcxyz", "{}", layout.name);
1141 let digits = type_keys(
1142 &mut c,
1143 &plain(&[KeyCode::Digit0, KeyCode::Digit5, KeyCode::Digit9]),
1144 );
1145 assert_eq!(digits, "059", "{}", layout.name);
1146 }
1147 }
1148
1149 #[test]
1150 fn keymap_names_are_reduced_to_something_findable() {
1151 assert_eq!(normalise_name("/etc/keymap/no.bmap.gz"), "no");
1154 assert_eq!(normalise_name("\"no\""), "no");
1155 assert_eq!(normalise_name("no-latin1"), "no");
1156 assert_eq!(normalise_name("us"), "us");
1157 assert_eq!(normalise_name("/usr/share/keymaps/xkb/us.map.gz"), "us");
1158 assert!(by_name(normalise_name("fr-bepo")).is_none());
1161 }
1162
1163 #[test]
1164 fn a_configuration_file_is_parsed_the_way_a_shell_would() {
1165 let alpine = "# Absolut path to the keymap.\n #KEYMAP=\"/usr/share/keymaps/xkb/us.map.gz\"\n KEYMAP=/etc/keymap/no.bmap.gz\n";
1166 let value = value_of(alpine, "KEYMAP").expect("a value");
1167 assert_eq!(
1168 normalise_name(value),
1169 "no",
1170 "the commented-out line must not win"
1171 );
1172
1173 assert_eq!(value_of("XKBLAYOUT=\"gb\"\n", "XKBLAYOUT"), Some("\"gb\""));
1174 assert_eq!(value_of("# nothing here\n", "KEYMAP"), None);
1175 }
1176
1177 #[test]
1178 fn layouts_are_findable_by_name() {
1179 assert!(core::ptr::eq(by_name("no").expect("no"), &NORWEGIAN));
1180 assert!(core::ptr::eq(by_name("US").expect("us"), &US));
1181 assert_eq!(by_name("dvorak").map(|l| l.name), None);
1182 }
1183}