kbvm 0.1.5

An implementation of the XKB specification
Documentation
use crate::{
    GroupIndex, Keycode, Keysym, ModifierMask,
    builder::Builder,
    evdev,
    lookup::{Lookup, LookupTable},
    syms,
    xkb::{Context, diagnostic::WriteToStderr},
};

fn lookup_table(map: &str) -> LookupTable {
    let mut context = Context::builder();
    context.clear();
    context.enable_environment(false);
    context.append_path(&format!(
        "{}/../type-tests/include",
        env!("CARGO_MANIFEST_DIR")
    ));
    let map = context
        .build()
        .keymap_from_bytes(WriteToStderr, None, map)
        .unwrap();
    map.to_builder().build_lookup_table()
}

#[test]
fn clamp_negative() {
    const MAP: &str = r#"
        xkb_keymap {
            xkb_keycodes {
                <a> = 1;
                <b> = 2;
                <c> = 3;
            };
            xkb_symbols {
                key <a> {
                    groupsClamp,
                    [ a ], [ b ],
                };
                key <b> {
                    [ a ], [ b ],
                };
                key <c> {
                    groupsRedirect = Group3,
                    [ a ], [ b ],
                };
            };
        };
    "#;
    let lookup = lookup_table(MAP);
    let keysym = |kc| {
        lookup
            .lookup(GroupIndex(!0), ModifierMask::NONE, Keycode::from_x11(kc))
            .into_iter()
            .next()
            .unwrap()
            .keysym()
    };
    assert_eq!(keysym(1), syms::a);
    assert_eq!(keysym(2), syms::b);
    assert_eq!(keysym(3), syms::a);
}

fn create_custom_lookup<A, F>(
    map: &str,
    transform: F,
) -> impl Fn(Keycode, ModifierMask, A) -> (Keysym, char)
where
    F: for<'a> Fn(Lookup<'a>, A) -> Lookup<'a>,
{
    let lookup = lookup_table(map);
    move |kc, mask, a| {
        let lookup = lookup.lookup(GroupIndex::ZERO, mask, kc);
        let lookup = transform(lookup, a);
        lookup
            .into_iter()
            .map(|p| (p.keysym(), p.char().unwrap()))
            .next()
            .unwrap()
    }
}

#[test]
fn no_ctrl_fallback() {
    const MAP: &str = r#"
        xkb_keymap {
            xkb_keycodes {
                include "generated"
            };
            xkb_symbols {
                key <a> { [ kana_A ], [ a ] };
            };
        };
    "#;
    let prod = create_custom_lookup(MAP, |lookup, fallback| lookup.with_ctrl_fallback(fallback));
    assert_eq!(
        prod(evdev::A, ModifierMask::NONE, false),
        (syms::kana_A, 'ã‚¢'),
    );
    assert_eq!(
        prod(evdev::A, ModifierMask::NONE, true),
        (syms::kana_A, 'ã‚¢'),
    );
    assert_eq!(
        prod(evdev::A, ModifierMask::CONTROL, false),
        (syms::kana_A, 'ã‚¢'),
    );
    assert_eq!(
        prod(evdev::A, ModifierMask::CONTROL, true),
        (syms::a, '\u{1}'),
    );
}

#[test]
fn no_ctrl_transform() {
    const MAP: &str = r#"
        xkb_keymap {
            xkb_keycodes {
                include "generated"
            };
            xkb_symbols {
                key <a> { [ kana_A ], [ a ] };
                key <b> { [ b ] };
            };
        };
    "#;
    let prod = create_custom_lookup(MAP, |lookup, transform| {
        lookup.with_ctrl_transform(transform)
    });
    assert_eq!(
        prod(evdev::A, ModifierMask::NONE, false),
        (syms::kana_A, 'ã‚¢'),
    );
    assert_eq!(
        prod(evdev::A, ModifierMask::NONE, true),
        (syms::kana_A, 'ã‚¢'),
    );
    assert_eq!(
        prod(evdev::A, ModifierMask::CONTROL, false),
        (syms::kana_A, 'ã‚¢'),
    );
    assert_eq!(
        prod(evdev::A, ModifierMask::CONTROL, true),
        (syms::a, '\u{1}'),
    );
    assert_eq!(prod(evdev::B, ModifierMask::NONE, false), (syms::b, 'b'),);
    assert_eq!(prod(evdev::B, ModifierMask::NONE, true), (syms::b, 'b'),);
    assert_eq!(prod(evdev::B, ModifierMask::CONTROL, false), (syms::b, 'b'),);
    assert_eq!(
        prod(evdev::B, ModifierMask::CONTROL, true),
        (syms::b, '\u{2}'),
    );
}

#[test]
fn no_caps_transform() {
    const MAP: &str = r#"
        xkb_keymap {
            xkb_keycodes {
                include "generated"
            };
            xkb_symbols {
                key <a> { type = "ALPHABETIC", [ a, u ] };
                key <b> { [ b ] };
            };
        };
    "#;
    let prod = create_custom_lookup(MAP, |lookup, transform| {
        lookup.with_caps_transform(transform)
    });
    assert_eq!(prod(evdev::A, ModifierMask::NONE, false), (syms::a, 'a'),);
    assert_eq!(prod(evdev::A, ModifierMask::NONE, true), (syms::a, 'a'),);
    assert_eq!(prod(evdev::A, ModifierMask::LOCK, false), (syms::u, 'u'),);
    assert_eq!(prod(evdev::A, ModifierMask::LOCK, true), (syms::u, 'u'),);
    assert_eq!(prod(evdev::B, ModifierMask::NONE, false), (syms::b, 'b'),);
    assert_eq!(prod(evdev::B, ModifierMask::NONE, true), (syms::b, 'b'),);
    assert_eq!(prod(evdev::B, ModifierMask::LOCK, false), (syms::b, 'b'),);
    assert_eq!(prod(evdev::B, ModifierMask::LOCK, true), (syms::B, 'B'),);
}

#[test]
fn setters() {
    let table = Builder::default().build_lookup_table();
    let lookup = table.lookup(GroupIndex::ZERO, ModifierMask::NONE, evdev::A);
    assert_eq!(lookup.use_ctrl_fallback, true);
    assert_eq!(lookup.do_ctrl_transform, true);
    assert_eq!(lookup.do_caps_transform, true);
    {
        let mut lookup = lookup;
        lookup.set_ctrl_fallback(false);
        assert_eq!(lookup.use_ctrl_fallback, false);
    }
    {
        let mut lookup = lookup;
        lookup.set_ctrl_transform(false);
        assert_eq!(lookup.do_ctrl_transform, false);
    }
    {
        let mut lookup = lookup;
        lookup.set_caps_transform(false);
        assert_eq!(lookup.do_caps_transform, false);
    }
    {
        let lookup = lookup.with_ctrl_fallback(false);
        assert_eq!(lookup.use_ctrl_fallback, false);
    }
    {
        let lookup = lookup.with_ctrl_transform(false);
        assert_eq!(lookup.do_ctrl_transform, false);
    }
    {
        let lookup = lookup.with_caps_transform(false);
        assert_eq!(lookup.do_caps_transform, false);
    }
}

#[test]
fn remaining_mods() {
    const MAP: &str = r#"
        xkb_keymap {
            xkb_keycodes {
                include "generated"
            };
            xkb_types {
                type "KEYPAD" {
                    modifiers = Shift + Mod2;
                    map[Mod2] = Level2;
                    preserve[Shift] = Shift;
                };
            };
            xkb_symbols {
                key <a> { [ a, A ] };
                key <kp4> {
                    type = "KEYPAD",
                    [ leftarrow, 4 ],
                };
            };
        };
    "#;
    let lookup = lookup_table(MAP);
    let prod = |kc, mask| {
        let lookup = lookup.lookup(GroupIndex::ZERO, mask, kc);
        lookup
            .into_iter()
            .map(|p| (p.keysym(), lookup.remaining_mods()))
            .next()
            .unwrap()
    };
    assert_eq!(
        prod(evdev::A, ModifierMask::NONE),
        (syms::a, ModifierMask::NONE),
    );
    assert_eq!(
        prod(evdev::A, ModifierMask::CONTROL),
        (syms::a, ModifierMask::CONTROL),
    );
    assert_eq!(
        prod(evdev::A, ModifierMask::SHIFT),
        (syms::A, ModifierMask::NONE),
    );
    assert_eq!(
        prod(evdev::A, ModifierMask::SHIFT | ModifierMask::CONTROL),
        (syms::A, ModifierMask::CONTROL),
    );
    assert_eq!(
        prod(evdev::KP4, ModifierMask::NONE),
        (syms::leftarrow, ModifierMask::NONE),
    );
    assert_eq!(
        prod(evdev::KP4, ModifierMask::MOD2),
        (syms::_4, ModifierMask::NONE),
    );
    assert_eq!(
        prod(evdev::KP4, ModifierMask::SHIFT),
        (syms::leftarrow, ModifierMask::SHIFT),
    );
    assert_eq!(
        prod(evdev::KP4, ModifierMask::SHIFT | ModifierMask::MOD2),
        (syms::leftarrow, ModifierMask::NONE),
    );
    assert_eq!(
        prod(evdev::KP4, ModifierMask::CONTROL),
        (syms::leftarrow, ModifierMask::CONTROL),
    );
    assert_eq!(
        prod(evdev::KP4, ModifierMask::CONTROL | ModifierMask::MOD2),
        (syms::_4, ModifierMask::CONTROL),
    );
    assert_eq!(
        prod(evdev::KP4, ModifierMask::CONTROL | ModifierMask::SHIFT),
        (syms::leftarrow, ModifierMask::CONTROL | ModifierMask::SHIFT),
    );
    assert_eq!(
        prod(
            evdev::KP4,
            ModifierMask::CONTROL | ModifierMask::SHIFT | ModifierMask::MOD2
        ),
        (syms::leftarrow, ModifierMask::CONTROL),
    );
}

#[test]
fn did_transform() {
    const MAP: &str = r#"
        xkb_keymap {
            xkb_keycodes {
                include "generated"
            };
            xkb_symbols {
                key <a> { [ a ] };
                key <b> { [ kana_A ], [ a ] };
            };
        };
    "#;
    let lookup = lookup_table(MAP);
    let prod = |kc, mask| {
        let lookup = lookup.lookup(GroupIndex::ZERO, mask, kc);
        lookup.into_iter().next().unwrap()
    };
    assert!(!prod(evdev::A, ModifierMask::NONE).did_ctrl_fallback());
    assert!(!prod(evdev::A, ModifierMask::NONE).did_ctrl_transform());
    assert!(!prod(evdev::A, ModifierMask::NONE).did_caps_transform());
    assert!(!prod(evdev::A, ModifierMask::LOCK).did_ctrl_fallback());
    assert!(!prod(evdev::A, ModifierMask::LOCK).did_ctrl_transform());
    assert!(prod(evdev::A, ModifierMask::LOCK).did_caps_transform());
    assert!(!prod(evdev::A, ModifierMask::CONTROL).did_ctrl_fallback());
    assert!(prod(evdev::A, ModifierMask::CONTROL).did_ctrl_transform());
    assert!(!prod(evdev::A, ModifierMask::CONTROL).did_caps_transform());
    assert!(prod(evdev::B, ModifierMask::CONTROL).did_ctrl_fallback());
    assert!(prod(evdev::B, ModifierMask::CONTROL).did_ctrl_transform());
    assert!(!prod(evdev::B, ModifierMask::CONTROL).did_caps_transform());
    assert!(prod(evdev::B, ModifierMask::CONTROL | ModifierMask::LOCK).did_ctrl_fallback());
    assert!(prod(evdev::B, ModifierMask::CONTROL | ModifierMask::LOCK).did_ctrl_transform());
    assert!(prod(evdev::B, ModifierMask::CONTROL | ModifierMask::LOCK).did_caps_transform());
}

#[test]
fn effective_group() {
    const MAP: &str = r#"
        xkb_keymap {
            xkb_keycodes {
                include "generated"
            };
            xkb_symbols {
                key <a> { [ a ] };
                key <b> { [ a ], [ b ] };
                key <c> { groupsClamp, [ a ], [ b ] };
                key <d> { groupsRedirect = Group2, [ a ], [ b ] };
                key <e> { };
            };
        };
    "#;
    let lookup = lookup_table(MAP);
    let prod = |kc, idx: u32| lookup.effective_group(GroupIndex(idx), kc).map(|g| g.0);
    assert_eq!(prod(evdev::A, 0), Some(0));
    assert_eq!(prod(evdev::A, 1), Some(0));
    assert_eq!(prod(evdev::A, !0), Some(0));
    assert_eq!(prod(evdev::B, 0), Some(0));
    assert_eq!(prod(evdev::B, 1), Some(1));
    assert_eq!(prod(evdev::B, 2), Some(0));
    assert_eq!(prod(evdev::B, 3), Some(1));
    assert_eq!(prod(evdev::B, !0), Some(1));
    assert_eq!(prod(evdev::C, 0), Some(0));
    assert_eq!(prod(evdev::C, 1), Some(1));
    assert_eq!(prod(evdev::C, 2), Some(1));
    assert_eq!(prod(evdev::C, 3), Some(1));
    assert_eq!(prod(evdev::C, !0), Some(0));
    assert_eq!(prod(evdev::C, !0 - 1), Some(0));
    assert_eq!(prod(evdev::D, 0), Some(0));
    assert_eq!(prod(evdev::D, 1), Some(1));
    assert_eq!(prod(evdev::D, 2), Some(1));
    assert_eq!(prod(evdev::D, 3), Some(1));
    assert_eq!(prod(evdev::D, !0), Some(1));
    assert_eq!(prod(evdev::D, !0 - 1), Some(1));
    assert_eq!(prod(evdev::E, 0), None);
    assert_eq!(prod(evdev::E, 1), None);
    assert_eq!(prod(evdev::E, 2), None);
    assert_eq!(prod(evdev::E, 3), None);
    assert_eq!(prod(evdev::E, !0), None);
    assert_eq!(prod(evdev::E, !0 - 1), None);
}

#[test]
fn repeats() {
    const MAP: &str = r#"
        xkb_keymap {
            xkb_keycodes {
                include "generated"
            };
            xkb_symbols {
                key <a> { [ a ] };
                key <b> { repeats = false, [ a ] };
            };
        };
    "#;
    let lookup = lookup_table(MAP);
    assert_eq!(lookup.repeats(evdev::A), true);
    assert_eq!(lookup.repeats(evdev::B), false);
    assert_eq!(lookup.repeats(evdev::C), true);
}