Skip to main content

very_normal/
keymap.rs

1//! Software keymaps for a stage, for keyboards without Oryx: kanata (macOS,
2//! Linux, Windows) and Karabiner-Elements (macOS).
3//!
4//! Both assume the keyboard itself still sends QWERTY. Don't use them on a
5//! board whose firmware already has the stage, or the keys move twice.
6
7use serde_json::json;
8
9use crate::layout;
10
11/// The physical key at each position, as the OS sees it.
12const PHYSICAL: &str = "qwertyuiopasdfghjkl;zxcvbnm,./";
13
14/// Positions where this stage types something other than the physical key.
15fn remaps(stage: usize) -> Vec<(char, char)> {
16    PHYSICAL
17        .chars()
18        .zip(layout::keys(stage))
19        .filter(|(from, to)| from != to)
20        .collect()
21}
22
23fn header(stage: usize) -> String {
24    format!(
25        "{}, stage {stage} of {}: {}",
26        layout::plan().name,
27        layout::last(),
28        layout::name(stage)
29    )
30}
31
32pub fn kanata(stage: usize) -> String {
33    let spaced = |s: &str| s.chars().map(String::from).collect::<Vec<_>>().join(" ");
34    let keys: String = layout::keys(stage).into_iter().collect();
35    format!(
36        ";; very-normal: {}\n\
37         ;; The keyboard must send QWERTY; don't also remap it in firmware.\n\
38         (defcfg process-unmapped-keys yes)\n\
39         (defsrc {})\n\
40         (deflayer stage{stage} {})\n",
41        header(stage),
42        spaced(PHYSICAL),
43        spaced(&keys),
44    )
45}
46
47fn key_code(c: char) -> String {
48    match c {
49        ';' => "semicolon".into(),
50        ',' => "comma".into(),
51        '.' => "period".into(),
52        '/' => "slash".into(),
53        '\'' => "quote".into(),
54        c => c.to_string(),
55    }
56}
57
58/// A complex modification for `~/.config/karabiner/assets/complex_modifications/`.
59pub fn karabiner(stage: usize) -> String {
60    let manipulators: Vec<_> = remaps(stage)
61        .into_iter()
62        .map(|(from, to)| {
63            json!({
64                "type": "basic",
65                "from": { "key_code": key_code(from), "modifiers": { "optional": ["any"] } },
66                "to": [{ "key_code": key_code(to) }],
67            })
68        })
69        .collect();
70    let doc = json!({
71        "title": format!("very-normal: {}", layout::plan().name),
72        "rules": [{
73            "description": format!("{} (the keyboard must send QWERTY)", header(stage)),
74            "manipulators": manipulators,
75        }],
76    });
77    serde_json::to_string_pretty(&doc).expect("json") + "\n"
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83
84    #[test]
85    fn kanata_maps_every_key_to_the_stage() {
86        let k = kanata(1);
87        assert!(k.contains("(defsrc q w e r t y u i o p a s d f g h j k l ; z x c v b n m , . /)"));
88        assert!(k.contains(
89            "(deflayer stage1 q w l r f y u i o p a s d t g h j k e ; z x c v b n m , . /)"
90        ));
91    }
92
93    #[test]
94    fn karabiner_only_remaps_what_moved() {
95        let v: serde_json::Value = serde_json::from_str(&karabiner(1)).unwrap();
96        let m = v["rules"][0]["manipulators"].as_array().unwrap();
97        assert_eq!(m.len(), 4);
98        assert_eq!(m[0]["from"]["key_code"], "e");
99        assert_eq!(m[0]["to"][0]["key_code"], "l");
100    }
101
102    #[test]
103    fn karabiner_names_punctuation() {
104        let v: serde_json::Value = serde_json::from_str(&karabiner(layout::last())).unwrap();
105        let codes: Vec<&str> = v["rules"][0]["manipulators"]
106            .as_array()
107            .unwrap()
108            .iter()
109            .filter_map(|m| m["to"][0]["key_code"].as_str())
110            .collect();
111        assert!(codes.contains(&"semicolon"));
112        assert!(remaps(0).is_empty());
113    }
114}