kanata_parser/cfg/
zippychord.rs

1//! Zipchord-like parsing. Probably not 100% compatible.
2//!
3//! Example lines in input file.
4//! The " => " string represents a tab character.
5//!
6//! "dy => day"
7//!   -> chord: (d y)
8//!   -> output: "day"
9//!
10//! "dy => day"
11//! "dy 1 => Monday"
12//!   -> chord: (d y)
13//!   -> output: "day"
14//!   -> chord: (d y)
15//!   -> output: "Monday"; "day" gets erased
16//!
17//! " abc => Alphabet"
18//!   -> chord: (space a b c)
19//!   -> output: "Alphabet"
20//!
21//! "r df => recipient"
22//!   -> chord: (r)
23//!   -> output: nothing yet, just type r
24//!   -> chord: (d f)
25//!   -> output: "recipient"
26//!
27//! " w  a => Washington"
28//!   -> chord: (space w)
29//!   -> output: nothing yet, type spacebar+w in whatever true order they were pressed
30//!   -> chord: (space a)
31//!   -> output: "Washington"
32//!   -> note: do observe the two spaces between 'w' and 'a'
33use 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    /// All possible chords.
71    #[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    /// Tracks current input to check against possible chords.
80    /// This does not store by the input order;
81    /// instead it is by some consistent ordering for
82    /// hashing into the possible chord map.
83    #[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    /// Sorted consistently by some arbitrary key order;
120    /// as opposed to, for example, simply the user press order.
121    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                // Q: what is the meaning of Ok vs. Err?
128                // A: Ok means the element already in vector @ `pos`. Normally this wouldn't be
129                // expected to happen but it turns out that key repeat might get in the way of this
130                // assumption. Err means element does not exist and returns the correct insert position.
131                Ok(_pos) => {}
132                Err(pos) => self.zch_keys.insert(pos, key),
133            }
134        }
135    }
136
137    /// A chord.
138    ///
139    /// If any followups exist it will be Some.
140    /// E.g. with:
141    /// - dy   -> day
142    /// - dy 1 -> Monday
143    /// - dy 2 -> Tuesday
144    ///
145    /// the output will be "day" and the Monday+Tuesday chords will be in `followups`.
146    #[derive(Debug, Clone)]
147    pub struct ZchChordOutput {
148        pub zch_output: Box<[ZchOutput]>,
149        pub zch_followups: Option<Arc<Mutex<ZchPossibleChords>>>,
150    }
151
152    /// Zch output can be uppercase, lowercase, altgr, and shift-altgr characters.
153    /// The parser should ensure all `OsCode`s in variants containing them
154    /// are visible characters that are backspacable.
155    #[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    /// User configuration for smart space.
207    ///
208    /// - `Full`         = add spaces after words, remove these spaces after typing punctuation.
209    /// - `AddSpaceOnly` = add spaces after words
210    /// - `Disabled`     = do nothing
211    #[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        /// When, during typing, chord fails to activate, zippychord functionality becomes temporarily
221        /// disabled. This is to avoid accidental chord activations when typing normally, as opposed to
222        /// intentionally trying to activate a chord. The duration of temporary disabling is determined
223        /// by this configuration item. Re-enabling also happens when word-splitting characters are
224        /// typed, for example typing  a space or a comma, but a pause of all typing activity lasting a
225        /// number of milliseconds equal to this configuration will also re-enable chording even if
226        /// typing within a single word.
227        pub zch_cfg_ticks_wait_enable: u16,
228
229        /// Assuming zippychording is enabled, when the first press happens this deadline will begin
230        /// and if no chords are completed within the deadline, zippychording will be disabled
231        /// temporarily (see `zch_cfg_ticks_wait_enable`). You may want a long or short deadline
232        /// depending on your use case. If you are primarily typing normally, with chords being used
233        /// occasionally being used, you may want a short deadline so that regular typing will be
234        /// unlikely to activate any chord. However, if you primarily type with chords, you may want a
235        /// longer deadline to give you more time to complete the intended chord (e.g. in case of
236        /// overlaps). With a long deadline you should be very intentional about pressing and releasing
237        /// an individual key to begin a sequence of regular typing to trigger the disabling of
238        /// zippychord. If, after the first press, a chord activates, this deadline will reset to
239        /// enable further chord activations.
240        pub zch_cfg_ticks_chord_deadline: u16,
241
242        /// User configuration for smart space. See `pub enum ZchSmartSpaceCfg`.
243        pub zch_cfg_smart_space: ZchSmartSpaceCfg,
244
245        /// Define keys for punctuation, which is relevant to smart space auto-erasure of added spaces.
246        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                    // Need to save and parse this later since it makes use of KEY_NAME_MAPPINGS.
400                    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                                // note for unwrap below: must be list if not atom
444                                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(); // non-empty, checked length already
490                                        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(); // checked count above
537                        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        // process zippy file
562        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                        // Check for a starting space.
630                        (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                        // Parse chord until next space.
640                        (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}