use denise::{ElementState, KeyCode, Modifiers};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Output {
#[default]
None,
Char(char),
Dead(char),
}
impl Output {
#[inline]
const fn is_none(self) -> bool {
matches!(self, Output::None)
}
}
#[derive(Clone, Copy, Debug)]
pub struct Entry {
pub code: KeyCode,
pub base: Output,
pub shift: Output,
pub altgr: Output,
pub shift_altgr: Output,
}
impl Entry {
const fn pair(code: KeyCode, base: char, shift: char) -> Self {
Self {
code,
base: Output::Char(base),
shift: Output::Char(shift),
altgr: Output::None,
shift_altgr: Output::None,
}
}
const fn triple(code: KeyCode, base: char, shift: char, altgr: char) -> Self {
Self {
code,
base: Output::Char(base),
shift: Output::Char(shift),
altgr: Output::Char(altgr),
shift_altgr: Output::None,
}
}
const fn letter(code: KeyCode, lower: char, upper: char) -> Self {
Self::pair(code, lower, upper)
}
#[inline]
const fn at(&self, shift: bool, level3: bool) -> Output {
match (shift, level3) {
(false, false) => self.base,
(true, false) => self.shift,
(false, true) => self.altgr,
(true, true) => {
if self.shift_altgr.is_none() {
self.altgr
} else {
self.shift_altgr
}
}
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct Layout {
pub name: &'static str,
pub entries: &'static [Entry],
pub decimal_separator: char,
}
impl Layout {
fn entry(&self, code: KeyCode) -> Option<&'static Entry> {
self.entries
.iter()
.find(|entry| entry.code == code)
.or_else(|| LETTERS.iter().find(|entry| entry.code == code))
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Composed {
chars: [char; 2],
len: u8,
}
impl Composed {
pub const NONE: Self = Self {
chars: ['\0', '\0'],
len: 0,
};
const fn one(ch: char) -> Self {
Self {
chars: [ch, '\0'],
len: 1,
}
}
const fn two(first: char, second: char) -> Self {
Self {
chars: [first, second],
len: 2,
}
}
#[inline]
pub fn as_slice(&self) -> &[char] {
&self.chars[..self.len as usize]
}
#[inline]
pub const fn is_empty(&self) -> bool {
self.len == 0
}
}
#[derive(Clone, Debug)]
pub struct Composer {
layout: &'static Layout,
pending_dead: Option<char>,
caps_lock: bool,
num_lock: bool,
level3: bool,
}
impl Composer {
pub fn new(layout: &'static Layout) -> Self {
Self {
layout,
pending_dead: None,
caps_lock: false,
num_lock: true,
level3: false,
}
}
#[inline]
pub const fn layout(&self) -> &'static Layout {
self.layout
}
pub fn set_layout(&mut self, layout: &'static Layout) {
self.layout = layout;
self.pending_dead = None;
}
#[inline]
pub const fn pending_dead(&self) -> Option<char> {
self.pending_dead
}
#[inline]
pub const fn caps_lock(&self) -> bool {
self.caps_lock
}
pub fn feed(&mut self, code: KeyCode, state: ElementState, modifiers: Modifiers) -> Composed {
if code == KeyCode::AltRight {
self.level3 = state.is_down();
return Composed::NONE;
}
if state != ElementState::Down {
return Composed::NONE;
}
match code {
KeyCode::CapsLock => {
self.caps_lock = !self.caps_lock;
return Composed::NONE;
}
KeyCode::NumLock => {
self.num_lock = !self.num_lock;
return Composed::NONE;
}
_ => {}
}
let chord = if self.level3 {
modifiers.contains(Modifiers::SUPER)
} else {
modifiers.contains(Modifiers::CTRL)
|| modifiers.contains(Modifiers::SUPER)
|| modifiers.contains(Modifiers::ALT)
};
if chord {
self.pending_dead = None;
return Composed::NONE;
}
let shift = modifiers.contains(Modifiers::SHIFT);
let output = self.output_for(code, shift);
match output {
Output::None => {
self.pending_dead = None;
Composed::NONE
}
Output::Dead(mark) => match self.pending_dead.replace(mark) {
Some(previous) if previous == mark => {
self.pending_dead = None;
Composed::one(mark)
}
Some(previous) => Composed::one(previous),
None => Composed::NONE,
},
Output::Char(ch) => match self.pending_dead.take() {
None => Composed::one(ch),
Some(mark) if ch == ' ' => Composed::one(mark),
Some(mark) => match compose(mark, ch) {
Some(combined) => Composed::one(combined),
None => Composed::two(mark, ch),
},
},
}
}
fn output_for(&self, code: KeyCode, shift: bool) -> Output {
if let Some(output) = self.numpad(code) {
return output;
}
if code == KeyCode::Space {
return Output::Char(' ');
}
let Some(entry) = self.layout.entry(code) else {
return Output::None;
};
let shift = shift != (self.caps_lock && is_letter(entry));
entry.at(shift, self.level3)
}
fn numpad(&self, code: KeyCode) -> Option<Output> {
let digit = match code {
KeyCode::Numpad0 => '0',
KeyCode::Numpad1 => '1',
KeyCode::Numpad2 => '2',
KeyCode::Numpad3 => '3',
KeyCode::Numpad4 => '4',
KeyCode::Numpad5 => '5',
KeyCode::Numpad6 => '6',
KeyCode::Numpad7 => '7',
KeyCode::Numpad8 => '8',
KeyCode::Numpad9 => '9',
KeyCode::NumpadDecimal => self.layout.decimal_separator,
KeyCode::NumpadAdd => return Some(Output::Char('+')),
KeyCode::NumpadSubtract => return Some(Output::Char('-')),
KeyCode::NumpadMultiply => return Some(Output::Char('*')),
KeyCode::NumpadDivide => return Some(Output::Char('/')),
_ => return None,
};
Some(if self.num_lock {
Output::Char(digit)
} else {
Output::None
})
}
}
fn is_letter(entry: &Entry) -> bool {
matches!(
(entry.base, entry.shift),
(Output::Char(lower), Output::Char(upper))
if lower.is_alphabetic() && upper.is_alphabetic()
)
}
fn compose(mark: char, base: char) -> Option<char> {
COMPOSE
.binary_search_by(|&(m, b, _)| (m, b).cmp(&(mark, base)))
.ok()
.map(|index| COMPOSE[index].2)
}
const LETTERS: [Entry; 26] = {
use KeyCode as K;
[
Entry::letter(K::A, 'a', 'A'),
Entry::letter(K::B, 'b', 'B'),
Entry::letter(K::C, 'c', 'C'),
Entry::letter(K::D, 'd', 'D'),
Entry::letter(K::E, 'e', 'E'),
Entry::letter(K::F, 'f', 'F'),
Entry::letter(K::G, 'g', 'G'),
Entry::letter(K::H, 'h', 'H'),
Entry::letter(K::I, 'i', 'I'),
Entry::letter(K::J, 'j', 'J'),
Entry::letter(K::K, 'k', 'K'),
Entry::letter(K::L, 'l', 'L'),
Entry::letter(K::M, 'm', 'M'),
Entry::letter(K::N, 'n', 'N'),
Entry::letter(K::O, 'o', 'O'),
Entry::letter(K::P, 'p', 'P'),
Entry::letter(K::Q, 'q', 'Q'),
Entry::letter(K::R, 'r', 'R'),
Entry::letter(K::S, 's', 'S'),
Entry::letter(K::T, 't', 'T'),
Entry::letter(K::U, 'u', 'U'),
Entry::letter(K::V, 'v', 'V'),
Entry::letter(K::W, 'w', 'W'),
Entry::letter(K::X, 'x', 'X'),
Entry::letter(K::Y, 'y', 'Y'),
Entry::letter(K::Z, 'z', 'Z'),
]
};
const US_ENTRIES: [Entry; 22] = {
use KeyCode as K;
[
Entry::pair(K::Digit1, '1', '!'),
Entry::pair(K::Digit2, '2', '@'),
Entry::pair(K::Digit3, '3', '#'),
Entry::pair(K::Digit4, '4', '$'),
Entry::pair(K::Digit5, '5', '%'),
Entry::pair(K::Digit6, '6', '^'),
Entry::pair(K::Digit7, '7', '&'),
Entry::pair(K::Digit8, '8', '*'),
Entry::pair(K::Digit9, '9', '('),
Entry::pair(K::Digit0, '0', ')'),
Entry::pair(K::Minus, '-', '_'),
Entry::pair(K::Equal, '=', '+'),
Entry::pair(K::BracketLeft, '[', '{'),
Entry::pair(K::BracketRight, ']', '}'),
Entry::pair(K::Backslash, '\\', '|'),
Entry::pair(K::Semicolon, ';', ':'),
Entry::pair(K::Quote, '\'', '"'),
Entry::pair(K::Backquote, '`', '~'),
Entry::pair(K::Comma, ',', '<'),
Entry::pair(K::Period, '.', '>'),
Entry::pair(K::Slash, '/', '?'),
Entry::pair(K::IntlBackslash, '\\', '|'),
]
};
pub static US: Layout = Layout {
name: "us",
entries: &US_ENTRIES,
decimal_separator: '.',
};
const NORWEGIAN_ENTRIES: [Entry; 24] = {
use KeyCode as K;
[
Entry::pair(K::Backquote, '|', '\u{00a7}'),
Entry::pair(K::Digit1, '1', '!'),
Entry::triple(K::Digit2, '2', '"', '@'),
Entry::triple(K::Digit3, '3', '#', '\u{00a3}'),
Entry::triple(K::Digit4, '4', '\u{00a4}', '$'),
Entry::triple(K::Digit5, '5', '%', '\u{20ac}'),
Entry::pair(K::Digit6, '6', '&'),
Entry::triple(K::Digit7, '7', '/', '{'),
Entry::triple(K::Digit8, '8', '(', '['),
Entry::triple(K::Digit9, '9', ')', ']'),
Entry::triple(K::Digit0, '0', '=', '}'),
Entry::triple(K::Minus, '+', '?', '\\'),
Entry {
code: K::Equal,
base: Output::Dead('\u{00b4}'),
shift: Output::Dead('`'),
altgr: Output::Char('|'),
shift_altgr: Output::None,
},
Entry::letter(K::BracketLeft, '\u{00e5}', '\u{00c5}'),
Entry {
code: K::BracketRight,
base: Output::Dead('\u{00a8}'),
shift: Output::Dead('^'),
altgr: Output::Dead('~'),
shift_altgr: Output::None,
},
Entry::letter(K::Semicolon, '\u{00f8}', '\u{00d8}'),
Entry::letter(K::Quote, '\u{00e6}', '\u{00c6}'),
Entry::pair(K::Backslash, '\'', '*'),
Entry::triple(K::IntlBackslash, '<', '>', '\\'),
Entry::pair(K::Comma, ',', ';'),
Entry::pair(K::Period, '.', ':'),
Entry::pair(K::Slash, '-', '_'),
Entry::triple(K::E, 'e', 'E', '\u{20ac}'),
Entry::triple(K::M, 'm', 'M', '\u{00b5}'),
]
};
pub static NORWEGIAN: Layout = Layout {
name: "no",
entries: &NORWEGIAN_ENTRIES,
decimal_separator: ',',
};
pub static BUILT_IN: [&Layout; 2] = [&US, &NORWEGIAN];
pub fn by_name(name: &str) -> Option<&'static Layout> {
BUILT_IN
.iter()
.copied()
.find(|layout| layout.name.eq_ignore_ascii_case(name))
}
const COMPOSE: [(char, char, char); 118] = [
('^', '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}'), ];
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LayoutSource {
Denise,
Xkb,
File(&'static str),
Default,
}
impl core::fmt::Display for LayoutSource {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
LayoutSource::Denise => f.write_str("DENISE_KEYMAP"),
LayoutSource::Xkb => f.write_str("XKB_DEFAULT_LAYOUT"),
LayoutSource::File(path) => write!(f, "{path}"),
LayoutSource::Default => f.write_str("default"),
}
}
}
const SYSTEM_FILES: [(&str, &str); 4] = [
("/etc/vconsole.conf", "KEYMAP"),
("/etc/default/keyboard", "XKBLAYOUT"),
("/etc/conf.d/loadkmap", "KEYMAP"),
("/etc/rc.conf", "KEYMAP"),
];
pub fn normalise_name(raw: &str) -> &str {
let raw = raw.trim().trim_matches(['"', '\'']);
let base = raw.rsplit('/').next().unwrap_or(raw);
let stem = base.split('.').next().unwrap_or(base);
if by_name(stem).is_some() {
return stem;
}
stem.split('-').next().unwrap_or(stem)
}
pub fn from_system() -> (&'static Layout, LayoutSource) {
for (variable, source) in [
("DENISE_KEYMAP", LayoutSource::Denise),
("XKB_DEFAULT_LAYOUT", LayoutSource::Xkb),
] {
if let Ok(value) = std::env::var(variable)
&& let Some(layout) = by_name(normalise_name(&value))
{
return (layout, source);
}
}
for (path, key) in SYSTEM_FILES {
let Ok(contents) = std::fs::read_to_string(path) else {
continue;
};
if let Some(value) = value_of(&contents, key)
&& let Some(layout) = by_name(normalise_name(value))
{
return (layout, LayoutSource::File(path));
}
}
(&US, LayoutSource::Default)
}
fn value_of<'a>(contents: &'a str, key: &str) -> Option<&'a str> {
contents.lines().find_map(|line| {
let line = line.trim();
if line.starts_with('#') {
return None;
}
let (name, value) = line.split_once('=')?;
(name.trim() == key).then(|| value.trim())
})
}
#[cfg(test)]
mod tests {
use super::*;
fn type_keys(composer: &mut Composer, keys: &[(KeyCode, Modifiers)]) -> String {
let mut out = String::new();
for &(code, modifiers) in keys {
if modifiers.contains(Modifiers::ALT) {
composer.feed(KeyCode::AltRight, ElementState::Down, Modifiers::ALT);
}
let composed = composer.feed(code, ElementState::Down, modifiers);
out.extend(composed.as_slice());
composer.feed(code, ElementState::Up, modifiers);
if modifiers.contains(Modifiers::ALT) {
composer.feed(KeyCode::AltRight, ElementState::Up, Modifiers::NONE);
}
}
out
}
fn plain(keys: &[KeyCode]) -> Vec<(KeyCode, Modifiers)> {
keys.iter().map(|&k| (k, Modifiers::NONE)).collect()
}
#[test]
fn us_types_ascii() {
let mut c = Composer::new(&US);
assert_eq!(
type_keys(&mut c, &plain(&[KeyCode::H, KeyCode::I, KeyCode::Digit1])),
"hi1"
);
assert_eq!(
type_keys(
&mut c,
&[
(KeyCode::H, Modifiers::SHIFT),
(KeyCode::Digit1, Modifiers::SHIFT),
]
),
"H!"
);
}
#[test]
fn norwegian_types_the_three_letters_it_exists_for() {
let mut c = Composer::new(&NORWEGIAN);
assert_eq!(
type_keys(
&mut c,
&plain(&[KeyCode::Quote, KeyCode::Semicolon, KeyCode::BracketLeft])
),
"æøå"
);
assert_eq!(
type_keys(
&mut c,
&[
(KeyCode::Quote, Modifiers::SHIFT),
(KeyCode::Semicolon, Modifiers::SHIFT),
(KeyCode::BracketLeft, Modifiers::SHIFT),
]
),
"ÆØÅ"
);
}
#[test]
fn the_same_positions_type_ascii_on_a_us_layout() {
let mut c = Composer::new(&US);
assert_eq!(
type_keys(
&mut c,
&plain(&[KeyCode::Quote, KeyCode::Semicolon, KeyCode::BracketLeft])
),
"';["
);
}
#[test]
fn dead_keys_compose() {
let mut c = Composer::new(&NORWEGIAN);
assert_eq!(
type_keys(&mut c, &plain(&[KeyCode::BracketRight, KeyCode::O])),
"ö"
);
assert_eq!(
type_keys(&mut c, &plain(&[KeyCode::Equal, KeyCode::E])),
"é"
);
assert_eq!(
type_keys(
&mut c,
&[
(KeyCode::Equal, Modifiers::SHIFT),
(KeyCode::A, Modifiers::NONE)
]
),
"à"
);
assert_eq!(
type_keys(
&mut c,
&[
(KeyCode::BracketRight, Modifiers::SHIFT),
(KeyCode::O, Modifiers::NONE)
]
),
"ô"
);
assert_eq!(
type_keys(
&mut c,
&[
(KeyCode::BracketRight, Modifiers::ALT),
(KeyCode::N, Modifiers::NONE)
]
),
"ñ"
);
}
#[test]
fn a_dead_key_produces_nothing_until_it_is_resolved() {
let mut c = Composer::new(&NORWEGIAN);
let composed = c.feed(KeyCode::BracketRight, ElementState::Down, Modifiers::NONE);
assert!(composed.is_empty(), "a dead key must not type anything yet");
assert_eq!(c.pending_dead(), Some('¨'));
}
#[test]
fn a_dead_key_twice_types_the_mark_itself() {
let mut c = Composer::new(&NORWEGIAN);
assert_eq!(
type_keys(
&mut c,
&plain(&[KeyCode::BracketRight, KeyCode::BracketRight])
),
"¨"
);
assert_eq!(c.pending_dead(), None);
}
#[test]
fn space_after_a_dead_key_types_the_bare_mark() {
let mut c = Composer::new(&NORWEGIAN);
assert_eq!(
type_keys(&mut c, &plain(&[KeyCode::BracketRight, KeyCode::Space])),
"¨"
);
}
#[test]
fn a_dead_key_that_cannot_combine_emits_both() {
let mut c = Composer::new(&NORWEGIAN);
assert_eq!(
type_keys(&mut c, &plain(&[KeyCode::BracketRight, KeyCode::Q])),
"¨q"
);
}
#[test]
fn a_key_that_types_nothing_cancels_a_pending_mark() {
let mut c = Composer::new(&NORWEGIAN);
c.feed(KeyCode::BracketRight, ElementState::Down, Modifiers::NONE);
assert_eq!(c.pending_dead(), Some('¨'));
c.feed(KeyCode::Escape, ElementState::Down, Modifiers::NONE);
assert_eq!(
c.pending_dead(),
None,
"Escape must not leave a latch behind"
);
assert_eq!(type_keys(&mut c, &plain(&[KeyCode::O])), "o");
}
#[test]
fn switching_layouts_abandons_a_half_typed_composition() {
let mut c = Composer::new(&NORWEGIAN);
c.feed(KeyCode::BracketRight, ElementState::Down, Modifiers::NONE);
c.set_layout(&US);
assert_eq!(c.pending_dead(), None);
}
#[test]
fn the_third_level_needs_the_right_alt_key() {
let mut c = Composer::new(&NORWEGIAN);
assert_eq!(type_keys(&mut c, &[(KeyCode::Digit2, Modifiers::ALT)]), "@");
assert_eq!(type_keys(&mut c, &[(KeyCode::Digit7, Modifiers::ALT)]), "{");
assert_eq!(type_keys(&mut c, &[(KeyCode::E, Modifiers::ALT)]), "€");
let mut c = Composer::new(&NORWEGIAN);
let composed = c.feed(KeyCode::Digit2, ElementState::Down, Modifiers::ALT);
assert!(
composed.is_empty(),
"left Alt must not reach the third level"
);
}
#[test]
fn altgr_reaches_the_third_level_even_reported_as_ctrl_plus_alt() {
let mut c = Composer::new(&NORWEGIAN);
c.feed(KeyCode::ControlLeft, ElementState::Down, Modifiers::CTRL);
c.feed(
KeyCode::AltRight,
ElementState::Down,
Modifiers::CTRL | Modifiers::ALT,
);
let composed = c.feed(
KeyCode::Digit2,
ElementState::Down,
Modifiers::CTRL | Modifiers::ALT,
);
assert_eq!(composed.as_slice(), ['@']);
c.feed(KeyCode::AltRight, ElementState::Up, Modifiers::CTRL);
let composed = c.feed(KeyCode::Digit2, ElementState::Down, Modifiers::CTRL);
assert!(composed.is_empty(), "Ctrl+2 is a binding, not an at sign");
}
#[test]
fn control_chords_type_nothing() {
let mut c = Composer::new(&US);
for modifier in [Modifiers::CTRL, Modifiers::SUPER] {
let composed = c.feed(KeyCode::C, ElementState::Down, modifier);
assert!(composed.is_empty(), "{modifier:?} + C must not type a c");
}
}
#[test]
fn control_and_enter_and_backspace_are_never_text() {
let mut c = Composer::new(&NORWEGIAN);
for code in [
KeyCode::Enter,
KeyCode::Tab,
KeyCode::Backspace,
KeyCode::Delete,
KeyCode::ArrowLeft,
KeyCode::F1,
] {
let composed = c.feed(code, ElementState::Down, Modifiers::NONE);
assert!(composed.is_empty(), "{code:?} must not produce text");
}
}
#[test]
fn caps_lock_shifts_letters_and_leaves_the_digit_row_alone() {
let mut c = Composer::new(&NORWEGIAN);
c.feed(KeyCode::CapsLock, ElementState::Down, Modifiers::NONE);
assert!(c.caps_lock());
assert_eq!(
type_keys(
&mut c,
&plain(&[KeyCode::A, KeyCode::Quote, KeyCode::Digit1])
),
"AÆ1",
"caps lock must reach æøå but not turn 1 into !"
);
assert_eq!(type_keys(&mut c, &[(KeyCode::A, Modifiers::SHIFT)]), "a");
c.feed(KeyCode::CapsLock, ElementState::Down, Modifiers::NONE);
assert!(!c.caps_lock());
}
#[test]
fn the_numpad_follows_num_lock_and_the_layout() {
let mut us = Composer::new(&US);
assert_eq!(
type_keys(&mut us, &plain(&[KeyCode::Numpad4, KeyCode::NumpadDecimal])),
"4."
);
let mut no = Composer::new(&NORWEGIAN);
assert_eq!(
type_keys(&mut no, &plain(&[KeyCode::Numpad4, KeyCode::NumpadDecimal])),
"4,",
"a European numpad types a decimal comma"
);
no.feed(KeyCode::NumLock, ElementState::Down, Modifiers::NONE);
let composed = no.feed(KeyCode::Numpad4, ElementState::Down, Modifiers::NONE);
assert!(
composed.is_empty(),
"with num lock off the numpad is arrows, not digits"
);
}
#[test]
fn key_release_types_nothing() {
let mut c = Composer::new(&US);
let composed = c.feed(KeyCode::A, ElementState::Up, Modifiers::NONE);
assert!(composed.is_empty(), "a key types on the way down, once");
}
#[test]
fn the_compose_table_is_sorted_and_free_of_duplicates() {
assert!(
COMPOSE
.windows(2)
.all(|w| (w[0].0, w[0].1) < (w[1].0, w[1].1)),
"lookup bisects, so an unsorted table would silently miss entries"
);
}
#[test]
fn composition_matches_unicode() {
for (mark, base, expected) in [
('´', 'e', 'é'),
('`', 'a', 'à'),
('¨', 'u', 'ü'),
('^', 'i', 'î'),
('~', 'n', 'ñ'),
('\u{02da}', 'a', 'å'),
('¸', 'c', 'ç'),
('\u{02c7}', 's', 'š'),
] {
assert_eq!(compose(mark, base), Some(expected), "{mark}{base}");
}
assert_eq!(compose('¨', 'q'), None);
assert_eq!(compose('!', 'a'), None);
}
#[test]
fn no_layout_lists_a_position_twice() {
for layout in BUILT_IN {
for (i, entry) in layout.entries.iter().enumerate() {
assert!(
!layout.entries[..i].iter().any(|e| e.code == entry.code),
"{} lists {:?} twice; the first would silently win",
layout.name,
entry.code
);
}
}
}
#[test]
fn every_layout_can_type_the_whole_alphabet_and_the_digits() {
for layout in BUILT_IN {
let mut c = Composer::new(layout);
let letters = type_keys(
&mut c,
&plain(&[
KeyCode::A,
KeyCode::B,
KeyCode::C,
KeyCode::X,
KeyCode::Y,
KeyCode::Z,
]),
);
assert_eq!(letters, "abcxyz", "{}", layout.name);
let digits = type_keys(
&mut c,
&plain(&[KeyCode::Digit0, KeyCode::Digit5, KeyCode::Digit9]),
);
assert_eq!(digits, "059", "{}", layout.name);
}
}
#[test]
fn keymap_names_are_reduced_to_something_findable() {
assert_eq!(normalise_name("/etc/keymap/no.bmap.gz"), "no");
assert_eq!(normalise_name("\"no\""), "no");
assert_eq!(normalise_name("no-latin1"), "no");
assert_eq!(normalise_name("us"), "us");
assert_eq!(normalise_name("/usr/share/keymaps/xkb/us.map.gz"), "us");
assert!(by_name(normalise_name("fr-bepo")).is_none());
}
#[test]
fn a_configuration_file_is_parsed_the_way_a_shell_would() {
let alpine = "# Absolut path to the keymap.\n #KEYMAP=\"/usr/share/keymaps/xkb/us.map.gz\"\n KEYMAP=/etc/keymap/no.bmap.gz\n";
let value = value_of(alpine, "KEYMAP").expect("a value");
assert_eq!(
normalise_name(value),
"no",
"the commented-out line must not win"
);
assert_eq!(value_of("XKBLAYOUT=\"gb\"\n", "XKBLAYOUT"), Some("\"gb\""));
assert_eq!(value_of("# nothing here\n", "KEYMAP"), None);
}
#[test]
fn layouts_are_findable_by_name() {
assert!(core::ptr::eq(by_name("no").expect("no"), &NORWEGIAN));
assert!(core::ptr::eq(by_name("US").expect("us"), &US));
assert_eq!(by_name("dvorak").map(|l| l.name), None);
}
}