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. A keyboard whose
5//! firmware already has the stage must be left out, or its keys move twice:
6//! the Karabiner rule only touches the built-in keyboard unless asked
7//! otherwise, and the kanata config shows how to pick devices.
8
9use serde_json::json;
10
11use crate::layout;
12
13/// The physical key at each position, as the OS sees it.
14const PHYSICAL: &str = "qwertyuiopasdfghjkl;zxcvbnm,./";
15
16/// Positions where this stage types something other than the physical key.
17fn remaps(stage: usize) -> Vec<(char, char)> {
18    PHYSICAL
19        .chars()
20        .zip(layout::keys(stage))
21        .filter(|(from, to)| from != to)
22        .collect()
23}
24
25fn header(stage: usize) -> String {
26    format!(
27        "{}, stage {stage} of {}: {}",
28        layout::plan().name,
29        layout::last(),
30        layout::name(stage)
31    )
32}
33
34pub fn kanata(stage: usize) -> String {
35    let spaced = |s: &str| s.chars().map(String::from).collect::<Vec<_>>().join(" ");
36    let keys: String = layout::keys(stage).into_iter().collect();
37    format!(
38        ";; very-normal: {}\n\
39         ;; This remaps every keyboard kanata grabs. Leave out any keyboard whose\n\
40         ;; firmware already has the stage (a remapped Voyager), or its keys move twice.\n\
41         ;; On macOS, uncomment the line below to grab only the built-in keyboard.\n\
42         ;; On Linux use linux-dev to list devices; on Windows, kanata's interception build.\n\
43         (defcfg\n  \
44           process-unmapped-keys yes\n  \
45           ;; macos-dev-names-include (\"Apple Internal Keyboard / Trackpad\")\n\
46         )\n\
47         (defsrc {})\n\
48         (deflayer stage{stage} {})\n",
49        header(stage),
50        spaced(PHYSICAL),
51        spaced(&keys),
52    )
53}
54
55fn key_code(c: char) -> String {
56    match c {
57        ';' => "semicolon".into(),
58        ',' => "comma".into(),
59        '.' => "period".into(),
60        '/' => "slash".into(),
61        '\'' => "quote".into(),
62        c => c.to_string(),
63    }
64}
65
66/// A complex modification for `~/.config/karabiner/assets/complex_modifications/`.
67/// Unless `all_keyboards`, it only applies to the Mac's built-in keyboard, so
68/// an external keyboard that already has the stage in firmware is left alone.
69pub fn karabiner(stage: usize, all_keyboards: bool) -> String {
70    let built_in = json!([{
71        "type": "device_if",
72        "identifiers": [{ "is_built_in_keyboard": true }],
73    }]);
74    let manipulators: Vec<_> = remaps(stage)
75        .into_iter()
76        .map(|(from, to)| {
77            let mut m = json!({
78                "type": "basic",
79                "from": { "key_code": key_code(from), "modifiers": { "optional": ["any"] } },
80                "to": [{ "key_code": key_code(to) }],
81            });
82            if !all_keyboards {
83                m["conditions"] = built_in.clone();
84            }
85            m
86        })
87        .collect();
88    let which = if all_keyboards {
89        "every keyboard; each must send QWERTY"
90    } else {
91        "built-in keyboard only"
92    };
93    let doc = json!({
94        "title": format!("very-normal: {}", layout::plan().name),
95        "rules": [{
96            "description": format!("{} ({which})", header(stage)),
97            "manipulators": manipulators,
98        }],
99    });
100    serde_json::to_string_pretty(&doc).expect("json") + "\n"
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    #[test]
108    fn kanata_maps_every_key_to_the_stage() {
109        let k = kanata(1);
110        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 , . /)"));
111        assert!(k.contains(
112            "(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 , . /)"
113        ));
114    }
115
116    #[test]
117    fn karabiner_only_remaps_what_moved() {
118        let v: serde_json::Value = serde_json::from_str(&karabiner(1, false)).unwrap();
119        let m = v["rules"][0]["manipulators"].as_array().unwrap();
120        assert_eq!(m.len(), 4);
121        assert_eq!(
122            m[0]["conditions"][0]["identifiers"][0]["is_built_in_keyboard"],
123            true
124        );
125        assert_eq!(m[0]["from"]["key_code"], "e");
126        assert_eq!(m[0]["to"][0]["key_code"], "l");
127    }
128
129    #[test]
130    fn karabiner_names_punctuation() {
131        let v: serde_json::Value = serde_json::from_str(&karabiner(layout::last(), true)).unwrap();
132        assert!(v["rules"][0]["manipulators"][0].get("conditions").is_none());
133        let codes: Vec<&str> = v["rules"][0]["manipulators"]
134            .as_array()
135            .unwrap()
136            .iter()
137            .filter_map(|m| m["to"][0]["key_code"].as_str())
138            .collect();
139        assert!(codes.contains(&"semicolon"));
140        assert!(remaps(0).is_empty());
141    }
142}