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!(
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    /// All possible chords.
74    #[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    /// Tracks current input to check against possible chords.
83    /// This does not store by the input order;
84    /// instead it is by some consistent ordering for
85    /// hashing into the possible chord map.
86    #[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    /// Sorted consistently by some arbitrary key order;
123    /// as opposed to, for example, simply the user press order.
124    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                // Q: what is the meaning of Ok vs. Err?
131                // A: Ok means the element already in vector @ `pos`. Normally this wouldn't be
132                // expected to happen but it turns out that key repeat might get in the way of this
133                // assumption. Err means element does not exist and returns the correct insert position.
134                Ok(_pos) => {}
135                Err(pos) => self.zch_keys.insert(pos, key),
136            }
137        }
138    }
139
140    /// A chord.
141    ///
142    /// If any followups exist it will be Some.
143    /// E.g. with:
144    /// - dy   -> day
145    /// - dy 1 -> Monday
146    /// - dy 2 -> Tuesday
147    ///
148    /// the output will be "day" and the Monday+Tuesday chords will be in `followups`.
149    #[derive(Debug, Clone)]
150    pub struct ZchChordOutput {
151        pub zch_output: Box<[ZchOutput]>,
152        pub zch_followups: Option<Arc<Mutex<ZchPossibleChords>>>,
153    }
154
155    /// Zch output can be uppercase, lowercase, altgr, and shift-altgr characters.
156    /// The parser should ensure all `OsCode`s in variants containing them
157    /// are visible characters that are backspacable.
158    #[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    /// User configuration for smart space.
210    ///
211    /// - `Full`         = add spaces after words, remove these spaces after typing punctuation.
212    /// - `AddSpaceOnly` = add spaces after words
213    /// - `Disabled`     = do nothing
214    #[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        /// When, during typing, chord fails to activate, zippychord functionality becomes temporarily
224        /// disabled. This is to avoid accidental chord activations when typing normally, as opposed to
225        /// intentionally trying to activate a chord. The duration of temporary disabling is determined
226        /// by this configuration item. Re-enabling also happens when word-splitting characters are
227        /// typed, for example typing  a space or a comma, but a pause of all typing activity lasting a
228        /// number of milliseconds equal to this configuration will also re-enable chording even if
229        /// typing within a single word.
230        pub zch_cfg_ticks_wait_enable: u16,
231
232        /// Assuming zippychording is enabled, when the first press happens this deadline will begin
233        /// and if no chords are completed within the deadline, zippychording will be disabled
234        /// temporarily (see `zch_cfg_ticks_wait_enable`). You may want a long or short deadline
235        /// depending on your use case. If you are primarily typing normally, with chords being used
236        /// occasionally being used, you may want a short deadline so that regular typing will be
237        /// unlikely to activate any chord. However, if you primarily type with chords, you may want a
238        /// longer deadline to give you more time to complete the intended chord (e.g. in case of
239        /// overlaps). With a long deadline you should be very intentional about pressing and releasing
240        /// an individual key to begin a sequence of regular typing to trigger the disabling of
241        /// zippychord. If, after the first press, a chord activates, this deadline will reset to
242        /// enable further chord activations.
243        pub zch_cfg_ticks_chord_deadline: u16,
244
245        /// User configuration for smart space. See `pub enum ZchSmartSpaceCfg`.
246        pub zch_cfg_smart_space: ZchSmartSpaceCfg,
247
248        /// Define keys for punctuation, which is relevant to smart space auto-erasure of added spaces.
249        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                    // Need to save and parse this later since it makes use of KEY_NAME_MAPPINGS.
403                    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                                // note for unwrap below: must be list if not atom
447                                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(); // non-empty, checked length already
496                                        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(); // checked count above
543                        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        // process zippy file
568        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                        // Check for a starting space.
636                        (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                        // Parse chord until next space.
646                        (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}