1use serde_json::json;
10
11use crate::layout;
12
13const PHYSICAL: &str = "qwertyuiopasdfghjkl;zxcvbnm,./";
15
16fn 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
66pub 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}