diff --git a/cfg_samples/chords.tsv b/cfg_samples/chords.tsv new file mode 100644 index 000000000..8805591bd --- /dev/null +++ b/cfg_samples/chords.tsv @@ -0,0 +1,192 @@ +rus rust +col cool +nice nice +you you +th the + a a + an an +man man +name name +an and +as as +or or +bu but +if if +so so +dn then +bc because + +to to +of of +in in + f for + w with +on on +at at +fm from +by by +abt about +up up +io into +ov over +af after +wo without + i I + me me + my my +ou you +ur your +he he +hm him +his his +sh she +hr her +it it +ts its +we we +us us +our our +dz they +dr their +dm them +wc which +wn when +wt what +wr where +ho who +hw how +wz why +is is +ar are +wa was +er were +be be +hv have +hs has +hd had +nt not +cn can +do do +wl will +cd could +wd would +sd should +li like +bn been +ge get +maz may +mad made +mk make +ai said +wk work +uz use +sz say + g go +kn know +tk take + se see +lk look +cm come +thk think +wnt want +gi give +ct cannot +de does +di did +sem seem +cl call +tha thank + + im I'm + id I'd +dt that +dis this +des these +tes test +al all + o one +mo more +the there +out out +ao also +tm time +sm some +js just +ne new +odr other +pl people + n no +dan than +oz only + m most +ay any +may many +el well +fs first +vy very +much much +now now +ev even +go good +grt great +way way + t two +yr year +bk back +day day +qn question +sc second +dg thing + y yes +cn' can't +dif different +dgh though +tru through +sr sorry +mv move +dir dir +stop stop +tye type +nx next +sam same +tp top +cod code +git git + to TODO +cls class +clus cluster +sure sure +lets let's +sup super +such such +thig thing +yet yet +don done +sem seem +ran ran +job job +bot bot +fx effect +nce once +rad read +ltr later +lot lot +brw brew +unst uninstall +rmv remove + ad add +poe problem +buld build + tol tool +got got +les less + 0 zero + 1 one + 2 two + 3 three + 4 four + 5 five + 6 six + 7 seven + 8 eight + 9 nine diff --git a/parser/src/cfg/chord.rs b/parser/src/cfg/chord.rs index b32215fe9..e1dae4995 100644 --- a/parser/src/cfg/chord.rs +++ b/parser/src/cfg/chord.rs @@ -1,6 +1,9 @@ +use itertools::Itertools; use kanata_keyberon::chord::{ChordV2, ChordsForKey, ChordsForKeys, ReleaseBehaviour}; use rustc_hash::{FxHashMap, FxHashSet}; +use std::fs; + use crate::{anyhow_expr, bail_expr}; use super::*; @@ -10,96 +13,71 @@ pub(crate) fn parse_defchordv2( s: &ParserState, ) -> Result> { let mut chunks = exprs[1..].chunks_exact(5); - let mut all_chords = FxHashSet::default(); let mut chords_container = ChordsForKeys::<'static, KanataCustom> { mapping: FxHashMap::default(), }; - for chunk in chunks.by_ref() { - let keys = &chunk[0]; - - let mut participants = keys - .list(s.vars()) - .map(|l| { - l.iter() - .try_fold(vec![], |mut keys, key| -> Result> { - let k = key.atom(s.vars()).and_then(str_to_oscode).ok_or_else(|| { - anyhow_expr!( - key, - "The first chord item must be a list of keys.\nInvalid key name." - ) - })?; - keys.push(k.into()); - Ok(keys) - }) - }) - .ok_or_else(|| anyhow_expr!(keys, "The first chord item must be a list of keys."))??; - if participants.len() < 2 { - bail_expr!(keys, "The minimum number of participating chord keys is 2"); - } - participants.sort(); - if !all_chords.insert(participants.clone()) { - bail_expr!( - keys, + let all_chords = chunks + .by_ref() + .flat_map(|chunk| match chunk[0] { + // Match a line like + // (include filename.txt) () 100 all-released (layer1 layer2) + SExpr::List(Spanned { + t: ref exprs, + span: _, + }) if matches!(exprs.first(), Some(SExpr::Atom(a)) if a.t == "include") => { + let file_name = exprs[1].atom(s.vars()).unwrap(); + let chord_translation = ChordTranslation::create( + file_name, + &chunk[2], + &chunk[3], + &chunk[4], + &s.layers[0][0], + ); + let chord_definitions = parse_chord_file(file_name).unwrap(); + let processed = chord_definitions.iter().map(|chord_def| { + let chunk = chord_translation.translate_chord(chord_def); + parse_single_chord(&chunk, s) + }); + Ok::<_, ParseError>(processed.collect_vec()) + } + _ => Ok(vec![parse_single_chord(chunk, s)]), + }) + .flat_map(|vec_result| vec_result.into_iter()) + .collect::>>(); + let unsuccessful = all_chords + .iter() + .filter_map(|r| r.as_ref().err()) + .collect::>(); + if !unsuccessful.is_empty() { + bail_expr!( + &exprs[0], + "Error parsing chord definition:\n{}", + unsuccessful + .iter() + .map(|e| e.msg.clone()) + .collect::>() + .join("\n") + ); + } + let successful = all_chords.into_iter().filter_map(Result::ok).collect_vec(); + + let mut all_participating_key_sets = FxHashSet::default(); + for chord in successful { + if !all_participating_key_sets.insert(chord.participating_keys) { + ParseError::new_without_span( "This chord has previously been defined.\n\ - Only one set of chords must exist for one key combination." + Only one set of chords must exist for one key combination.", ); - } - - let action = parse_action(&chunk[1], s)?; - let timeout = parse_non_zero_u16(&chunk[2], s, "chord timeout")?; - let release_behaviour = chunk[3] - .atom(s.vars()) - .and_then(|r| { - Some(match r { - "first-release" => ReleaseBehaviour::OnFirstRelease, - "all-released" => ReleaseBehaviour::OnLastRelease, - _ => return None, - }) - }) - .ok_or_else(|| { - anyhow_expr!( - &chunk[3], - "Chord release behaviour must be one of:\n\ - first-release | all-released" - ) - })?; - - let disabled_layers = &chunk[4]; - let disabled_layers = disabled_layers - .list(s.vars()) - .map(|dl| { - dl.iter() - .try_fold(vec![], |mut layers, layer| -> Result> { - let l_idx = layer - .atom(s.vars()) - .and_then(|l| s.layer_idxs.get(l)) - .ok_or_else(|| anyhow_expr!(layer, "Not a known layer name."))?; - layers.push((*l_idx) as u16); - Ok(layers) - }) - }) - .ok_or_else(|| { - anyhow_expr!( - disabled_layers, - "Disabled layers must be a list of layer names" - ) - })??; - let chord = ChordV2 { - action, - participating_keys: s.a.sref_vec(participants.clone()), - pending_duration: timeout, - disabled_layers: s.a.sref_vec(disabled_layers), - release_behaviour, - }; - let chord = s.a.sref(chord); - for pkey in participants.iter().copied() { - log::trace!("chord for key:{pkey:?} > {chord:?}"); - chords_container - .mapping - .entry(pkey) - .or_insert(ChordsForKey { chords: vec![] }) - .chords - .push(chord); + } else { + for pkey in chord.participating_keys.iter().copied() { + //log::trace!("chord for key:{pkey:?} > {chord:?}"); + chords_container + .mapping + .entry(pkey) + .or_insert(ChordsForKey { chords: vec![] }) + .chords + .push(s.a.sref(chord.clone())); + } } } let rem = chunks.remainder(); @@ -112,3 +90,244 @@ pub(crate) fn parse_defchordv2( } Ok(chords_container) } + +fn parse_single_chord(chunk: &[SExpr], s: &ParserState) -> Result> { + let participants = parse_participating_keys(&chunk[0], s)?; + let action = parse_action(&chunk[1], s)?; + let timeout = parse_timeout(&chunk[2], s)?; + let release_behaviour = parse_release_behaviour(&chunk[3], s)?; + let disabled_layers = parse_disabled_layers(&chunk[4], s)?; + let chord: ChordV2<'static, KanataCustom> = ChordV2 { + action, + participating_keys: s.a.sref_vec(participants.clone()), + pending_duration: timeout, + disabled_layers: s.a.sref_vec(disabled_layers), + release_behaviour, + }; + Ok(s.a.sref(chord).clone()) +} + +fn parse_participating_keys(keys: &SExpr, s: &ParserState) -> Result> { + let mut participants = keys + .list(s.vars()) + .map(|l| { + l.iter() + .try_fold(vec![], |mut keys, key| -> Result> { + let k = key.atom(s.vars()).and_then(str_to_oscode).ok_or_else(|| { + anyhow_expr!( + key, + "The first chord item must be a list of keys.\nInvalid key name." + ) + })?; + keys.push(k.into()); + Ok(keys) + }) + }) + .ok_or_else(|| anyhow_expr!(keys, "The first chord item must be a list of keys."))??; + if participants.len() < 2 { + bail_expr!(keys, "The minimum number of participating chord keys is 2"); + } + participants.sort(); + Ok(participants) +} + +fn parse_timeout(chunk: &SExpr, s: &ParserState) -> Result { + let timeout = parse_non_zero_u16(chunk, s, "chord timeout")?; + Ok(timeout) +} + +fn parse_release_behaviour( + release_behaviour_string: &SExpr, + s: &ParserState, +) -> Result { + let release_behaviour = release_behaviour_string + .atom(s.vars()) + .and_then(|r| { + Some(match r { + "first-release" => ReleaseBehaviour::OnFirstRelease, + "all-released" => ReleaseBehaviour::OnLastRelease, + _ => return None, + }) + }) + .ok_or_else(|| { + anyhow_expr!( + release_behaviour_string, + "Chord release behaviour must be one of:\n\ + first-release | all-released" + ) + })?; + Ok(release_behaviour) +} + +fn parse_disabled_layers(disabled_layers: &SExpr, s: &ParserState) -> Result> { + let disabled_layers = disabled_layers + .list(s.vars()) + .map(|dl| { + dl.iter() + .try_fold(vec![], |mut layers, layer| -> Result> { + let l_idx = layer + .atom(s.vars()) + .and_then(|l| s.layer_idxs.get(l)) + .ok_or_else(|| anyhow_expr!(layer, "Not a known layer name."))?; + layers.push((*l_idx) as u16); + Ok(layers) + }) + }) + .ok_or_else(|| { + anyhow_expr!( + disabled_layers, + "Disabled layers must be a list of layer names" + ) + })??; + Ok(disabled_layers) +} + +fn parse_chord_file(file_name: &str) -> Result> { + let input_data = fs::read_to_string(file_name) + .unwrap_or_else(|_| panic!("Unable to read file {}", file_name)); + let parsed_chords = parse_input(&input_data).unwrap(); + Ok(parsed_chords) +} + +fn parse_input(input: &str) -> Result> { + input + .lines() + .filter(|line| !line.trim().is_empty() && !line.trim().starts_with("//")) + .map(|line| { + let mut caps = line.split('\t'); + let error_message = format!( + "Each line needs to have an action separated by a tab character, got '{}'", + line + ); + let keys = caps.next().expect(&error_message); + let action = caps.next().expect(&error_message); + Ok(ChordDefinition { + keys: keys.to_string(), + action: action.to_string(), + }) + }) + .collect() +} + +#[derive(Debug)] +struct ChordDefinition { + keys: String, + action: String, +} + +struct ChordTranslation<'a> { + file_name: &'a str, + target_map: FxHashMap, + postprocess_map: FxHashMap, + timeout: &'a SExpr, + release_behaviour: &'a SExpr, + disabled_layers: &'a SExpr, +} + +impl<'a> ChordTranslation<'a> { + fn create( + file_name: &'a str, + timeout: &'a SExpr, + release_behaviour: &'a SExpr, + disabled_layers: &'a SExpr, + first_layer: &[Action<'static, &&[&CustomAction]>], + ) -> Self { + let postprocess_map: FxHashMap = [ + ("semicolon", ";"), + ("colon", "S-."), + ("slash", "/"), + ("apostrophe", "'"), + ("dot", "."), + (" ", "spc"), + ] + .iter() + .cloned() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect(); + let target_map = first_layer + .iter() + .enumerate() + .filter_map(|(idx, layout)| { + layout + .key_codes() + .next() + .map(|kc| kc.to_string().to_lowercase()) + .zip( + idx.try_into() + .ok() + .and_then(OsCode::from_u16) + .map(|osc| osc.to_string().to_lowercase()), + ) + }) + .collect::>() + .into_iter() + .chain(vec![(" ".to_string(), "spc".to_string())]) + .collect::>(); + ChordTranslation { + file_name, + target_map, + postprocess_map, + timeout, + release_behaviour, + disabled_layers, + } + } + + fn post_process(&self, converted: &str) -> String { + self.postprocess_map + .get(converted) + .map(|c| c.to_string()) + .unwrap_or_else(|| { + if converted.chars().all(|c| c.is_uppercase()) { + format!("S-{}", converted.to_lowercase()) + } else { + converted.to_string() + } + }) + } + + fn participant_keys(&self, keys: &str) -> Vec { + keys.chars() + .map(|key| { + self.target_map + .get(key.to_string().to_lowercase().as_str()) + .map(|c| self.postprocess_map.get(c).unwrap_or(c).to_string()) + .unwrap_or_else(|| key.to_string()) + }) + .collect::>() + } + + fn action(&self, action: &str) -> Vec { + let mut action_strings = action + .chars() + .map(|c| self.post_process(&c.to_string())) + .collect_vec(); + // Wait 50ms for one-shot Shift to release + // TODO: This would be better handled by a (multi (release-key lsft)(release-key rsft)) + // but I haven't gotten that to work yet. + action_strings.insert(1, "50".to_string()); + action_strings.extend_from_slice(&[ + "sldr".to_string(), + "spc".to_string(), + "nop0".to_string(), + ]); + action_strings + } + + fn translate_chord(&self, chord_def: &ChordDefinition) -> Vec { + let sexpr_string = format!( + "(({}) (macro {}))", + self.participant_keys(&chord_def.keys).join(" "), + self.action(&chord_def.action).join(" ") + ); + let mut participant_action = sexpr::parse(&sexpr_string, self.file_name).unwrap()[0] + .t + .clone(); + participant_action.extend_from_slice(&[ + self.timeout.clone(), + self.release_behaviour.clone(), + self.disabled_layers.clone(), + ]); + participant_action + } +} diff --git a/parser/src/cfg/mod.rs b/parser/src/cfg/mod.rs index 042ee9b5e..78a5b9a99 100755 --- a/parser/src/cfg/mod.rs +++ b/parser/src/cfg/mod.rs @@ -789,7 +789,8 @@ pub fn parse_cfg_raw_string( let mut klayers = parse_layers(s, &mut mapped_keys, &cfg)?; resolve_chord_groups(&mut klayers, s)?; - + let layers = s.a.bref_slice(klayers); + s.layers = layers; let override_exprs = root_exprs .iter() .filter(gen_first_atom_filter("defoverrides")) @@ -841,7 +842,7 @@ pub fn parse_cfg_raw_string( ) .into()); } - let layers = s.a.bref_slice(klayers); + let klayers = unsafe { KanataLayers::new(layers, s.a.clone()) }; Ok(IntermediateCfg { options: cfg, @@ -1176,6 +1177,7 @@ enum SpannedLayerExprs { #[derive(Debug)] pub struct ParserState { + layers: KLayers, layer_exprs: Vec, aliases: Aliases, layer_idxs: LayerIndexes, @@ -1205,6 +1207,7 @@ impl Default for ParserState { fn default() -> Self { let default_cfg = CfgOptions::default(); Self { + layers: Default::default(), layer_exprs: Default::default(), aliases: Default::default(), layer_idxs: Default::default(), @@ -1439,6 +1442,7 @@ fn parse_action_atom(ac_span: &Spanned, s: &ParserState) -> Result<&'sta "This is a list action and must be in parentheses: ({ac} ...)" ); } + match ac { "_" | "‗" | "≝" => { if let Some(trans_forbidden_reason) = s.trans_forbidden_reason {