1use super::*;
34use crate::bail_expr;
35
36#[cfg(not(feature = "zippychord"))]
37#[derive(Debug, Clone, Default)]
38pub struct ZchPossibleChords();
39#[cfg(not(feature = "zippychord"))]
40#[derive(Debug, Clone, Default)]
41pub struct ZchConfig();
42#[cfg(not(feature = "zippychord"))]
43fn parse_zippy_inner(
44 exprs: &[SExpr],
45 _s: &ParserState,
46 _f: &mut FileContentProvider,
47) -> Result<(ZchPossibleChords, ZchConfig)> {
48 bail_expr!(&exprs[0], "Kanata was not compiled with the \"zippychord\" feature. This configuration is unsupported")
49}
50
51pub(crate) fn parse_zippy(
52 exprs: &[SExpr],
53 s: &ParserState,
54 f: &mut FileContentProvider,
55) -> Result<(ZchPossibleChords, ZchConfig)> {
56 parse_zippy_inner(exprs, s, f)
57}
58
59#[cfg(feature = "zippychord")]
60pub use inner::*;
61#[cfg(feature = "zippychord")]
62mod inner {
63 use super::*;
64
65 use crate::anyhow_expr;
66 use crate::subset::*;
67
68 use parking_lot::Mutex;
69
70 #[derive(Debug, Clone, Default)]
72 pub struct ZchPossibleChords(pub SubsetMap<u16, Arc<ZchChordOutput>>);
73 impl ZchPossibleChords {
74 pub fn is_empty(&self) -> bool {
75 self.0.is_empty()
76 }
77 }
78
79 #[derive(Debug, Clone, Default, PartialEq, Eq)]
84 pub struct ZchInputKeys {
85 zch_inputs: ZchSortedChord,
86 }
87 impl ZchInputKeys {
88 pub fn zchik_new() -> Self {
89 Self {
90 zch_inputs: ZchSortedChord {
91 zch_keys: Vec::new(),
92 },
93 }
94 }
95 pub fn zchik_contains(&self, osc: OsCode) -> bool {
96 self.zch_inputs.zch_keys.contains(&osc.into())
97 }
98 pub fn zchik_insert(&mut self, osc: OsCode) {
99 self.zch_inputs.zch_insert(osc.into());
100 }
101 pub fn zchik_remove(&mut self, osc: OsCode) {
102 self.zch_inputs.zch_keys.retain(|k| *k != osc.into());
103 }
104 pub fn zchik_len(&self) -> usize {
105 self.zch_inputs.zch_keys.len()
106 }
107 pub fn zchik_clear(&mut self) {
108 self.zch_inputs.zch_keys.clear()
109 }
110 pub fn zchik_keys(&self) -> &[u16] {
111 &self.zch_inputs.zch_keys
112 }
113 pub fn zchik_is_empty(&self) -> bool {
114 self.zch_inputs.zch_keys.is_empty()
115 }
116 }
117
118 #[derive(Debug, Default, Clone, Hash, PartialEq, Eq)]
119 pub struct ZchSortedChord {
122 zch_keys: Vec<u16>,
123 }
124 impl ZchSortedChord {
125 pub fn zch_insert(&mut self, key: u16) {
126 match self.zch_keys.binary_search(&key) {
127 Ok(_pos) => {}
132 Err(pos) => self.zch_keys.insert(pos, key),
133 }
134 }
135 }
136
137 #[derive(Debug, Clone)]
147 pub struct ZchChordOutput {
148 pub zch_output: Box<[ZchOutput]>,
149 pub zch_followups: Option<Arc<Mutex<ZchPossibleChords>>>,
150 }
151
152 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
156 pub enum ZchOutput {
157 Lowercase(OsCode),
158 Uppercase(OsCode),
159 AltGr(OsCode),
160 ShiftAltGr(OsCode),
161 NoEraseLowercase(OsCode),
162 NoEraseUppercase(OsCode),
163 NoEraseAltGr(OsCode),
164 NoEraseShiftAltGr(OsCode),
165 }
166
167 impl ZchOutput {
168 pub fn osc(self) -> OsCode {
169 use ZchOutput::*;
170 match self {
171 Lowercase(osc)
172 | Uppercase(osc)
173 | AltGr(osc)
174 | ShiftAltGr(osc)
175 | NoEraseLowercase(osc)
176 | NoEraseUppercase(osc)
177 | NoEraseAltGr(osc)
178 | NoEraseShiftAltGr(osc) => osc,
179 }
180 }
181 pub fn osc_and_is_noerase(self) -> (OsCode, bool) {
182 use ZchOutput::*;
183 match self {
184 Lowercase(osc) | Uppercase(osc) | AltGr(osc) | ShiftAltGr(osc) => (osc, false),
185 NoEraseLowercase(osc)
186 | NoEraseUppercase(osc)
187 | NoEraseAltGr(osc)
188 | NoEraseShiftAltGr(osc) => (osc, true),
189 }
190 }
191 pub fn display_len(outs: impl AsRef<[Self]>) -> i16 {
192 outs.as_ref().iter().copied().fold(0i16, |mut len, out| {
193 len += out.output_char_count();
194 len
195 })
196 }
197 pub fn output_char_count(self) -> i16 {
198 match self.osc_and_is_noerase() {
199 (OsCode::KEY_BACKSPACE, _) => -1,
200 (_, false) => 1,
201 (_, true) => 0,
202 }
203 }
204 }
205
206 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
212 pub enum ZchSmartSpaceCfg {
213 Full,
214 AddSpaceOnly,
215 Disabled,
216 }
217
218 #[derive(Debug)]
219 pub struct ZchConfig {
220 pub zch_cfg_ticks_wait_enable: u16,
228
229 pub zch_cfg_ticks_chord_deadline: u16,
241
242 pub zch_cfg_smart_space: ZchSmartSpaceCfg,
244
245 pub zch_cfg_smart_space_punctuation: HashSet<ZchOutput>,
247 }
248
249 impl Default for ZchConfig {
250 fn default() -> Self {
251 Self {
252 zch_cfg_ticks_wait_enable: 500,
253 zch_cfg_ticks_chord_deadline: 500,
254 zch_cfg_smart_space: ZchSmartSpaceCfg::Disabled,
255 zch_cfg_smart_space_punctuation: {
256 let mut puncs = HashSet::default();
257 puncs.insert(ZchOutput::Lowercase(OsCode::KEY_DOT));
258 puncs.insert(ZchOutput::Lowercase(OsCode::KEY_COMMA));
259 puncs.insert(ZchOutput::Lowercase(OsCode::KEY_SEMICOLON));
260 puncs.shrink_to_fit();
261 puncs
262 },
263 }
264 }
265 }
266
267 const NO_ERASE: &str = "no-erase";
268 const SINGLE_OUTPUT_MULTI_KEY: &str = "single-output";
269
270 enum ZchIoMappingType {
271 NoErase,
272 SingleOutput,
273 }
274 impl ZchIoMappingType {
275 fn try_parse(expr: &SExpr, vars: Option<&HashMap<String, SExpr>>) -> Result<Self> {
276 use ZchIoMappingType::*;
277 expr.atom(vars)
278 .and_then(|name| match name {
279 NO_ERASE => Some(NoErase),
280 SINGLE_OUTPUT_MULTI_KEY => Some(SingleOutput),
281 _ => None,
282 })
283 .ok_or_else(|| {
284 anyhow_expr!(
285 &expr,
286 "Unknown output type. Must be one of:\nno-erase | single-output"
287 )
288 })
289 }
290 }
291
292 #[cfg(feature = "zippychord")]
293 pub(super) fn parse_zippy_inner(
294 exprs: &[SExpr],
295 s: &ParserState,
296 f: &mut FileContentProvider,
297 ) -> Result<(ZchPossibleChords, ZchConfig)> {
298 use crate::subset::GetOrIsSubsetOfKnownKey::*;
299
300 if exprs[0].atom(None).expect("should be atom") == "defzippy-experimental" {
301 log::warn!(
302 "You should replace defzippy-experimental with defzippy.\n\
303 Using -experimental will be invalid in the future."
304 );
305 }
306
307 if exprs.len() < 2 {
308 bail_expr!(
309 &exprs[0],
310 "There must be a filename following the zippy definition.\nFound {}",
311 exprs.len() - 1
312 );
313 }
314
315 let Some(file_name) = exprs[1].atom(s.vars()) else {
316 bail_expr!(&exprs[1], "Filename must be a string, not a list.");
317 };
318
319 let mut config = ZchConfig::default();
320
321 const KEY_NAME_MAPPINGS: &str = "output-character-mappings";
322 const IDLE_REACTIVATE_TIME: &str = "idle-reactivate-time";
323 const CHORD_DEADLINE: &str = "on-first-press-chord-deadline";
324 const SMART_SPACE: &str = "smart-space";
325 const SMART_SPACE_PUNCTUATION: &str = "smart-space-punctuation";
326
327 let mut idle_reactivate_time_seen = false;
328 let mut key_name_mappings_seen = false;
329 let mut chord_deadline_seen = false;
330 let mut smart_space_seen = false;
331 let mut smart_space_punctuation_seen = false;
332 let mut smart_space_punctuation_val_expr = None;
333
334 let mut user_cfg_char_to_output: HashMap<char, Vec<ZchOutput>> = HashMap::default();
335 let mut pairs = exprs[2..].chunks_exact(2);
336 for pair in pairs.by_ref() {
337 let config_name = &pair[0];
338 let config_value = &pair[1];
339
340 match config_name.atom(s.vars()).ok_or_else(|| {
341 anyhow_expr!(
342 config_name,
343 "A configuration name must be a string, not a list"
344 )
345 })? {
346 IDLE_REACTIVATE_TIME => {
347 if idle_reactivate_time_seen {
348 bail_expr!(
349 config_name,
350 "This is the 2nd instance; it can only be defined once"
351 );
352 }
353 idle_reactivate_time_seen = true;
354 config.zch_cfg_ticks_wait_enable =
355 parse_u16(config_value, s, IDLE_REACTIVATE_TIME)?;
356 }
357
358 CHORD_DEADLINE => {
359 if chord_deadline_seen {
360 bail_expr!(
361 config_name,
362 "This is the 2nd instance; it can only be defined once"
363 );
364 }
365 chord_deadline_seen = true;
366 config.zch_cfg_ticks_chord_deadline =
367 parse_u16(config_value, s, CHORD_DEADLINE)?;
368 }
369
370 SMART_SPACE => {
371 if smart_space_seen {
372 bail_expr!(
373 config_name,
374 "This is the 2nd instance; it can only be defined once"
375 );
376 }
377 smart_space_seen = true;
378 config.zch_cfg_smart_space = config_value
379 .atom(s.vars())
380 .and_then(|val| match val {
381 "none" => Some(ZchSmartSpaceCfg::Disabled),
382 "full" => Some(ZchSmartSpaceCfg::Full),
383 "add-space-only" => Some(ZchSmartSpaceCfg::AddSpaceOnly),
384 _ => None,
385 })
386 .ok_or_else(|| {
387 anyhow_expr!(&config_value, "Must be: none | full | add-space-only")
388 })?;
389 }
390
391 SMART_SPACE_PUNCTUATION => {
392 if smart_space_punctuation_seen {
393 bail_expr!(
394 config_name,
395 "This is the 2nd instance; it can only be defined once"
396 );
397 }
398 smart_space_punctuation_seen = true;
399 smart_space_punctuation_val_expr = Some(config_value);
401 }
402
403 KEY_NAME_MAPPINGS => {
404 if key_name_mappings_seen {
405 bail_expr!(
406 config_name,
407 "This is the 2nd instance; it can only be defined once"
408 );
409 }
410 key_name_mappings_seen = true;
411 let mut mappings = config_value
412 .list(s.vars())
413 .ok_or_else(|| {
414 anyhow_expr!(
415 config_value,
416 "{KEY_NAME_MAPPINGS} must be followed by a list"
417 )
418 })?
419 .chunks_exact(2);
420
421 for mapping_pair in mappings.by_ref() {
422 let input = mapping_pair[0]
423 .atom(None)
424 .ok_or_else(|| {
425 anyhow_expr!(
426 &mapping_pair[0],
427 "key mapping input does not use lists"
428 )
429 })?
430 .trim_atom_quotes();
431 if input.chars().count() != 1 {
432 bail_expr!(&mapping_pair[0], "Inputs should be exactly one character");
433 }
434 let input_char = input.chars().next().expect("count is 1");
435
436 let output = match mapping_pair[1].atom(s.vars()) {
437 Some(o) => vec![parse_single_zippy_output_mapping(
438 o,
439 &mapping_pair[1],
440 false,
441 )?],
442 None => {
443 let output_list = mapping_pair[1].list(s.vars()).unwrap();
445 if output_list.is_empty() {
446 bail_expr!(
447 &mapping_pair[1],
448 "Empty list is invalid for zippy output mapping."
449 );
450 }
451 let output_type =
452 ZchIoMappingType::try_parse(&output_list[0], s.vars())?;
453 match output_type {
454 ZchIoMappingType::NoErase => {
455 const ERR: &str = "expects a single key or output chord.";
456 if output_list.len() != 2 {
457 anyhow_expr!(&output_list[1], "{NO_ERASE} {ERR}");
458 }
459 let output =
460 output_list[1].atom(s.vars()).ok_or_else(|| {
461 anyhow_expr!(&output_list[1], "{NO_ERASE} {ERR}")
462 })?;
463 vec![parse_single_zippy_output_mapping(
464 output,
465 &output_list[1],
466 true,
467 )?]
468 }
469 ZchIoMappingType::SingleOutput => {
470 if output_list.len() < 2 {
471 anyhow_expr!(&output_list[1], "{SINGLE_OUTPUT_MULTI_KEY} expects one or more keys or output chords.");
472 }
473 let all_params_except_last =
474 &output_list[1..output_list.len() - 1];
475 let mut outs = vec![];
476 for expr in all_params_except_last {
477 let output = expr
478 .atom(s.vars())
479 .ok_or_else(|| {
480 anyhow_expr!(&output_list[1], "{SINGLE_OUTPUT_MULTI_KEY} does not allow list parameters.")
481 })?;
482 let out = parse_single_zippy_output_mapping(
483 output,
484 &output_list[1],
485 true,
486 )?;
487 outs.push(out);
488 }
489 let last_expr = &output_list.last().unwrap(); let last_out = last_expr
491 .atom(s.vars())
492 .ok_or_else(|| {
493 anyhow_expr!(last_expr, "{SINGLE_OUTPUT_MULTI_KEY} does not allow list parameters.")
494 })?;
495 outs.push(parse_single_zippy_output_mapping(
496 last_out, last_expr, false,
497 )?);
498 outs
499 }
500 }
501 }
502 };
503
504 if user_cfg_char_to_output.insert(input_char, output).is_some() {
505 bail_expr!(&mapping_pair[0], "Duplicate character, not allowed");
506 }
507 }
508
509 let rem = mappings.remainder();
510 if !rem.is_empty() {
511 bail_expr!(&rem[0], "zippy input is missing its output mapping");
512 }
513 }
514 _ => bail_expr!(config_name, "Unknown zippy configuration name"),
515 }
516 }
517
518 let rem = pairs.remainder();
519 if !rem.is_empty() {
520 bail_expr!(&rem[0], "zippy config name is missing its value");
521 }
522
523 if let Some(val) = smart_space_punctuation_val_expr {
524 config.zch_cfg_smart_space_punctuation = val
525 .list(s.vars())
526 .ok_or_else(|| {
527 anyhow_expr!(val, "{SMART_SPACE_PUNCTUATION} must be followed by a list")
528 })?
529 .iter()
530 .try_fold(vec![], |mut puncs, punc_expr| -> Result<Vec<ZchOutput>> {
531 let punc = punc_expr
532 .atom(s.vars())
533 .ok_or_else(|| anyhow_expr!(&punc_expr, "Lists are not allowed"))?;
534
535 if punc.chars().count() == 1 {
536 let c = punc.chars().next().unwrap(); if let Some(out) = user_cfg_char_to_output.get(&c) {
538 if out.len() > 1 {
539 bail_expr!(
540 punc_expr,
541 "This character is a single-output with multiple keys\n
542 and is not yet supported as use for punctuation."
543 );
544 }
545 puncs.push(out[0]);
546 return Ok(puncs);
547 }
548 }
549
550 let osc = str_to_oscode(punc)
551 .ok_or_else(|| anyhow_expr!(&punc_expr, "Unknown key name"))?;
552 puncs.push(ZchOutput::Lowercase(osc));
553
554 Ok(puncs)
555 })?
556 .into_iter()
557 .collect();
558 config.zch_cfg_smart_space_punctuation.shrink_to_fit();
559 }
560
561 let input_data = f
563 .get_file_content(file_name.as_ref())
564 .map_err(|e| anyhow_expr!(&exprs[1], "Failed to read file:\n{e}"))?;
565 let res = input_data
566 .lines()
567 .enumerate()
568 .filter(|(_, line)| !line.trim().is_empty() && !line.trim().starts_with("//"))
569 .try_fold(
570 Arc::new(Mutex::new(ZchPossibleChords(SubsetMap::ssm_new()))),
571 |zch, (line_number, line)| {
572 let Some((input, output)) = line.split_once('\t') else {
573 bail_expr!(
574 &exprs[1],
575 "Input and output are separated by a tab, but found no tab:\n{}: {line}",
576 line_number + 1
577 );
578 };
579 if input.is_empty() {
580 bail_expr!(
581 &exprs[1],
582 "No input defined; line must not begin with a tab:\n{}: {line}",
583 line_number + 1
584 );
585 }
586
587 let mut char_buf: [u8; 4] = [0; 4];
588 let output = {
589 output
590 .chars()
591 .try_fold(vec![], |mut zch_output, out_char| -> Result<_> {
592 if let Some(out) = user_cfg_char_to_output.get(&out_char) {
593 zch_output.extend(out.iter());
594 return Ok(zch_output);
595 }
596
597 let out_key = out_char.to_lowercase().next().unwrap();
598 let key_name = out_key.encode_utf8(&mut char_buf);
599 let osc = match key_name as &str {
600 " " => OsCode::KEY_SPACE,
601 _ => str_to_oscode(key_name).ok_or_else(|| {
602 anyhow_expr!(
603 &exprs[1],
604 "Unknown output key name '{}':\n{}: {line}",
605 out_char,
606 line_number + 1,
607 )
608 })?,
609 };
610 let out = match out_char.is_uppercase() {
611 true => ZchOutput::Uppercase(osc),
612 false => ZchOutput::Lowercase(osc),
613 };
614 zch_output.push(out);
615 Ok(zch_output)
616 })?
617 .into_boxed_slice()
618 };
619 let mut input_left_to_parse = input;
620 let mut chord_chars;
621 let mut input_chord = ZchInputKeys::zchik_new();
622 let mut is_space_included;
623 let mut possible_chords_map = zch.clone();
624 let mut next_map: Option<Arc<Mutex<_>>>;
625
626 while !input_left_to_parse.is_empty() {
627 input_chord.zchik_clear();
628
629 (is_space_included, input_left_to_parse) =
631 match input_left_to_parse.strip_prefix(' ') {
632 None => (false, input_left_to_parse),
633 Some(i) => (true, i),
634 };
635 if is_space_included {
636 input_chord.zchik_insert(OsCode::KEY_SPACE);
637 }
638
639 (chord_chars, input_left_to_parse) =
641 match input_left_to_parse.split_once(' ') {
642 Some(split) => split,
643 None => (input_left_to_parse, ""),
644 };
645
646 chord_chars
647 .chars()
648 .try_fold((), |_, chord_char| -> Result<()> {
649 let key_name = chord_char.encode_utf8(&mut char_buf);
650 let osc = str_to_oscode(key_name).ok_or_else(|| {
651 anyhow_expr!(
652 &exprs[1],
653 "Unknown input key name: '{key_name}':\n{}: {line}",
654 line_number + 1
655 )
656 })?;
657 input_chord.zchik_insert(osc);
658 Ok(())
659 })?;
660
661 let output_for_input_chord = possible_chords_map
662 .lock()
663 .0
664 .ssm_get_or_is_subset_ksorted(input_chord.zchik_keys());
665 match (input_left_to_parse.is_empty(), output_for_input_chord) {
666 (true, HasValue(_)) => {
667 bail_expr!(
668 &exprs[1],
669 "Found duplicate input chord, which is disallowed {input}:\n{}: {line}",
670 line_number + 1
671 );
672 }
673 (true, _) => {
674 possible_chords_map.lock().0.ssm_insert_ksorted(
675 input_chord.zchik_keys(),
676 Arc::new(ZchChordOutput {
677 zch_output: output,
678 zch_followups: None,
679 }),
680 );
681 break;
682 }
683 (false, HasValue(next_nested_map)) => {
684 match &next_nested_map.zch_followups {
685 None => {
686 let map = Arc::new(Mutex::new(ZchPossibleChords(
687 SubsetMap::ssm_new(),
688 )));
689 next_map = Some(map.clone());
690 possible_chords_map.lock().0.ssm_insert_ksorted(
691 input_chord.zchik_keys(),
692 ZchChordOutput {
693 zch_output: next_nested_map.zch_output.clone(),
694 zch_followups: Some(map),
695 }
696 .into(),
697 );
698 }
699 Some(followup) => {
700 next_map = Some(followup.clone());
701 }
702 }
703 }
704 (false, _) => {
705 let map =
706 Arc::new(Mutex::new(ZchPossibleChords(SubsetMap::ssm_new())));
707 next_map = Some(map.clone());
708 possible_chords_map.lock().0.ssm_insert_ksorted(
709 input_chord.zchik_keys(),
710 Arc::new(ZchChordOutput {
711 zch_output: Box::new([]),
712 zch_followups: Some(map),
713 }),
714 );
715 }
716 };
717 if let Some(map) = next_map.take() {
718 possible_chords_map = map;
719 }
720 }
721 Ok(zch)
722 },
723 )?;
724 Ok((
725 Arc::into_inner(res).expect("no other refs").into_inner(),
726 config,
727 ))
728 }
729
730 fn parse_single_zippy_output_mapping(
731 output: &str,
732 output_expr: &SExpr,
733 is_noerase: bool,
734 ) -> Result<ZchOutput> {
735 let (output_mods, output_key) = parse_mod_prefix(output)?;
736 if output_mods.contains(&KeyCode::LShift) && output_mods.contains(&KeyCode::RShift) {
737 bail_expr!(
738 output_expr,
739 "Both shifts are used which is redundant, use only one."
740 );
741 }
742 if output_mods
743 .iter()
744 .any(|m| !matches!(m, KeyCode::LShift | KeyCode::RShift | KeyCode::RAlt))
745 {
746 bail_expr!(output_expr, "Only S- and AG- are supported.");
747 }
748 let output_osc = str_to_oscode(output_key)
749 .ok_or_else(|| anyhow_expr!(output_expr, "unknown key name"))?;
750 let output = match output_mods.len() {
751 0 => match is_noerase {
752 false => ZchOutput::Lowercase(output_osc),
753 true => ZchOutput::NoEraseLowercase(output_osc),
754 },
755 1 => match output_mods[0] {
756 KeyCode::LShift | KeyCode::RShift => match is_noerase {
757 false => ZchOutput::Uppercase(output_osc),
758 true => ZchOutput::NoEraseUppercase(output_osc),
759 },
760 KeyCode::RAlt => match is_noerase {
761 false => ZchOutput::AltGr(output_osc),
762 true => ZchOutput::NoEraseAltGr(output_osc),
763 },
764 _ => unreachable!("forbidden by earlier parsing"),
765 },
766 2 => match is_noerase {
767 false => ZchOutput::ShiftAltGr(output_osc),
768 true => ZchOutput::NoEraseShiftAltGr(output_osc),
769 },
770 _ => {
771 unreachable!("contains at most: altgr and one of the shifts")
772 }
773 };
774 Ok(output)
775 }
776}