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