use serde_json::json;
use crate::layout;
const PHYSICAL: &str = "qwertyuiopasdfghjkl;zxcvbnm,./";
fn remaps(stage: usize) -> Vec<(char, char)> {
PHYSICAL
.chars()
.zip(layout::keys(stage))
.filter(|(from, to)| from != to)
.collect()
}
fn header(stage: usize) -> String {
format!(
"{}, stage {stage} of {}: {}",
layout::plan().name,
layout::last(),
layout::name(stage)
)
}
pub fn kanata(stage: usize) -> String {
let spaced = |s: &str| s.chars().map(String::from).collect::<Vec<_>>().join(" ");
let keys: String = layout::keys(stage).into_iter().collect();
format!(
";; very-normal: {}\n\
;; This remaps every keyboard kanata grabs. Leave out any keyboard whose\n\
;; firmware already has the stage (a remapped Voyager), or its keys move twice.\n\
;; On macOS, uncomment the line below to grab only the built-in keyboard.\n\
;; On Linux use linux-dev to list devices; on Windows, kanata's interception build.\n\
(defcfg\n \
process-unmapped-keys yes\n \
;; macos-dev-names-include (\"Apple Internal Keyboard / Trackpad\")\n\
)\n\
(defsrc {})\n\
(deflayer stage{stage} {})\n",
header(stage),
spaced(PHYSICAL),
spaced(&keys),
)
}
fn key_code(c: char) -> String {
match c {
';' => "semicolon".into(),
',' => "comma".into(),
'.' => "period".into(),
'/' => "slash".into(),
'\'' => "quote".into(),
c => c.to_string(),
}
}
pub fn karabiner(stage: usize, all_keyboards: bool) -> String {
let built_in = json!([{
"type": "device_if",
"identifiers": [{ "is_built_in_keyboard": true }],
}]);
let manipulators: Vec<_> = remaps(stage)
.into_iter()
.map(|(from, to)| {
let mut m = json!({
"type": "basic",
"from": { "key_code": key_code(from), "modifiers": { "optional": ["any"] } },
"to": [{ "key_code": key_code(to) }],
});
if !all_keyboards {
m["conditions"] = built_in.clone();
}
m
})
.collect();
let which = if all_keyboards {
"every keyboard; each must send QWERTY"
} else {
"built-in keyboard only"
};
let doc = json!({
"title": format!("very-normal: {}", layout::plan().name),
"rules": [{
"description": format!("{} ({which})", header(stage)),
"manipulators": manipulators,
}],
});
serde_json::to_string_pretty(&doc).expect("json") + "\n"
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn kanata_maps_every_key_to_the_stage() {
let k = kanata(1);
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 , . /)"));
assert!(k.contains(
"(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 , . /)"
));
}
#[test]
fn karabiner_only_remaps_what_moved() {
let v: serde_json::Value = serde_json::from_str(&karabiner(1, false)).unwrap();
let m = v["rules"][0]["manipulators"].as_array().unwrap();
assert_eq!(m.len(), 4);
assert_eq!(
m[0]["conditions"][0]["identifiers"][0]["is_built_in_keyboard"],
true
);
assert_eq!(m[0]["from"]["key_code"], "e");
assert_eq!(m[0]["to"][0]["key_code"], "l");
}
#[test]
fn karabiner_names_punctuation() {
let v: serde_json::Value = serde_json::from_str(&karabiner(layout::last(), true)).unwrap();
assert!(v["rules"][0]["manipulators"][0].get("conditions").is_none());
let codes: Vec<&str> = v["rules"][0]["manipulators"]
.as_array()
.unwrap()
.iter()
.filter_map(|m| m["to"][0]["key_code"].as_str())
.collect();
assert!(codes.contains(&"semicolon"));
assert!(remaps(0).is_empty());
}
}