libdof/
combos.rs

1//! A way to define combos for a keyboard layout.
2
3use crate::{
4    interaction::Pos, keyboard_conv, DofError, DofErrorInner as DErr, Key, Keyboard, Layer, Result,
5};
6use serde::{Deserialize, Serialize};
7use serde_with::{serde_as, DisplayFromStr};
8use std::{collections::BTreeMap, iter, str::FromStr};
9
10/// Represents a combo by way of specifying a `Key`, and if there are multiple on the keyboard,
11/// the nth index. If there are 2 `e` keys for example, you can specify `e-2`.
12#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
13pub struct ComboKey {
14    key: Key,
15    nth: usize,
16}
17
18impl ComboKey {
19    fn new(s: &str) -> Self {
20        let key = s.parse().unwrap();
21
22        Self { key, nth: 0 }
23    }
24
25    fn new_nth(s: &str, nth: usize) -> Self {
26        let key = s.parse().unwrap();
27
28        Self { key, nth }
29    }
30}
31
32impl FromStr for ComboKey {
33    type Err = DofError;
34
35    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
36        let ck = match s.len() {
37            0 => return Err(DErr::EmptyComboKey.into()),
38            1 | 2 => Self::new(s),
39            _ => match s.chars().rev().position(|c| c == '-') {
40                Some(p) => {
41                    let (key, num) = s.split_at(s.len() - p - 1);
42                    let num = &num[1..];
43
44                    match num.parse::<usize>() {
45                        Ok(nth) => Self::new_nth(key, nth.saturating_sub(1)),
46                        Err(_) => Self::new(s),
47                    }
48                }
49                None => Self::new(s),
50            },
51        };
52
53        Ok(ck)
54    }
55}
56
57impl std::fmt::Display for ComboKey {
58    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59        match self.nth {
60            0 => write!(f, "{}", self.key),
61            nth => write!(f, "{}-{}", self.key, nth),
62        }
63    }
64}
65
66keyboard_conv!(ComboKey, ComboKeyStrAsRow);
67
68/// Structure to store combos for a layout. Contains a map with layer names, where each layer
69/// contains a map from a `Vec` of [`ComboKey`](crate::ComboKey)s to a single `Key`.
70#[serde_as]
71#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
72pub struct ParseCombos(
73    #[serde_as(as = "BTreeMap<_, BTreeMap<ComboKeyStrAsRow, DisplayFromStr>>")]
74    pub  BTreeMap<String, BTreeMap<Vec<ComboKey>, Key>>,
75);
76
77impl ParseCombos {
78    /// Convert layers to a `Key` + row/column map.
79    pub(crate) fn into_pos_layers(self, layers: &BTreeMap<String, Layer>) -> Result<Combos> {
80        let layers = layers
81            .iter()
82            .map(|(name, layer)| {
83                let layer = layer
84                    .rows()
85                    .enumerate()
86                    .flat_map(|(i, row)| {
87                        row.iter()
88                            .enumerate()
89                            .map(move |(j, key)| (Pos::new(i, j), key))
90                    })
91                    .collect::<Vec<_>>();
92                (name.as_str(), layer)
93            })
94            .collect::<BTreeMap<_, _>>();
95
96        self.0
97            .into_iter()
98            .flat_map(|(layer_name, combos)| {
99                let layer = layers.get(layer_name.as_str()).map(|l| l.as_slice());
100                iter::repeat((layer_name, layer)).zip(combos)
101            })
102            .map(|((layer_name, layer), (combo, output))| {
103                let l = layer.ok_or_else(|| {
104                    DErr::UnknownComboLayer(layer_name.clone(), combo_to_str(&combo))
105                })?;
106
107                combo
108                    .iter()
109                    .map(|ck| {
110                        l.iter()
111                            .filter_map(|(pos, key)| (**key == ck.key).then_some(*pos))
112                            .nth(ck.nth)
113                            .ok_or_else(|| {
114                                DErr::InvalidKeyIndex(
115                                    combo_to_str(&combo),
116                                    ck.key.to_string(),
117                                    ck.nth,
118                                )
119                                .into()
120                            })
121                    })
122                    .collect::<Result<Vec<_>>>()
123                    .map(|combo| (layer_name, (combo, output)))
124            })
125            .try_fold(
126                BTreeMap::new(),
127                |mut acc: BTreeMap<_, Vec<_>>, layer_combo| match layer_combo {
128                    Ok((layer_name, combo)) => {
129                        acc.entry(layer_name).or_default().push(combo);
130                        Ok(acc)
131                    }
132                    Err(e) => Err(e),
133                },
134            )
135            .map(Combos)
136    }
137}
138
139/// Fully parsed `Dof` representation of combos on a layout. In here is a BTreeMap mapping layer
140/// names by `String` to a vector of `(Vec<Pos>, Key)` which are all combos on a keyboard, mapped
141/// by their row/column index.
142#[derive(Clone, Debug, Default, PartialEq)]
143pub struct Combos(pub BTreeMap<String, Vec<(Vec<Pos>, Key)>>);
144
145impl Combos {
146    pub(crate) fn into_parse_combos(self, layers: &BTreeMap<String, Layer>) -> Option<ParseCombos> {
147        if self.0.is_empty() {
148            return None;
149        }
150
151        let parse_combos = self
152            .0
153            .into_iter()
154            .map(|(layer_label, combos)| {
155                let layer = &layers.get(&layer_label).unwrap().0;
156
157                let layer_combos = combos
158                    .into_iter()
159                    .map(move |(combo, key)| {
160                        let combo = combo
161                            .into_iter()
162                            .map(|pos| {
163                                let key = layer[pos.row()][pos.col()].clone();
164                                let nth = layer[..(pos.row() + 1)]
165                                    .iter()
166                                    .flat_map(move |row| &row[..(pos.col() + 1)])
167                                    .filter(|k| k == &&key)
168                                    .count();
169                                let nth = match nth {
170                                    0 | 1 => 0,
171                                    n => n,
172                                };
173                                ComboKey::new_nth(&key.to_string(), nth)
174                            })
175                            .collect::<Vec<_>>();
176                        (combo, key)
177                    })
178                    .collect();
179                (layer_label, layer_combos)
180            })
181            .collect();
182
183        Some(ParseCombos(parse_combos))
184    }
185}
186
187fn combo_to_str(combos: &[ComboKey]) -> String {
188    if combos.is_empty() {
189        String::new()
190    } else {
191        combos
192            .iter()
193            .take(combos.len() - 1)
194            .map(|c| format!("{c} "))
195            .chain([combos.last().unwrap().to_string()])
196            .collect::<String>()
197    }
198}
199
200#[cfg(test)]
201pub(crate) fn ck(key: Key, nth: usize) -> ComboKey {
202    ComboKey { key, nth }
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208    use crate::{Key::*, SpecialKey::*};
209
210    #[test]
211    fn parse_combos() {
212        let json = r#"
213            {
214                "main": {
215                    "a b": "x"
216                },
217                "edge-cases": {
218                    "-1 1-": "6",
219                    "--1": "d",
220                    "---": "X",
221                    "🦀-12": "rpt",
222                    "a-1 b-2 c-3 ~-4 rpt-5": "*"
223                }
224            }
225        "#;
226
227        let parse =
228            serde_json::from_str::<ParseCombos>(json).expect("couldn't parse combos json: ");
229
230        let reference = ParseCombos(BTreeMap::from([
231            (
232                "main".to_string(),
233                BTreeMap::from([(vec![ck(Char('a'), 0), ck(Char('b'), 0)], Char('x'))]),
234            ),
235            (
236                "edge-cases".to_string(),
237                BTreeMap::from([
238                    (
239                        vec![ck(Word("-1".into()), 0), ck(Word("1-".into()), 0)],
240                        Char('6'),
241                    ),
242                    (vec![ck(Char('-'), 0)], Char('d')),
243                    (vec![ck(Word("---".into()), 0)], Char('X')),
244                    (vec![ck(Char('🦀'), 11)], Special(Repeat)),
245                    (
246                        vec![
247                            ck(Char('a'), 0),
248                            ck(Char('b'), 1),
249                            ck(Char('c'), 2),
250                            ck(Empty, 3),
251                            ck(Special(Repeat), 4),
252                        ],
253                        Transparent,
254                    ),
255                ]),
256            ),
257        ]));
258
259        assert_eq!(parse, reference);
260    }
261
262    #[test]
263    fn to_combos_simple() {
264        let json = r#"
265            {
266                "main": {
267                    "a b": "x",
268                    "e-2 b e": "rpt"
269                }
270            }
271        "#;
272
273        let parse =
274            serde_json::from_str::<ParseCombos>(json).expect("couldn't parse combos json: ");
275
276        let layers = BTreeMap::from_iter([(
277            "main".to_owned(),
278            vec![vec![Char('a'), Char('e'), Char('b'), Char('c'), Char('e')]].into(),
279        )]);
280
281        let combos = parse.into_pos_layers(&layers);
282
283        assert_eq!(
284            combos,
285            Ok(Combos(BTreeMap::from_iter([(
286                "main".to_owned(),
287                vec![
288                    (vec![Pos::new(0, 0), Pos::new(0, 2)], Char('x')),
289                    (
290                        vec![Pos::new(0, 4), Pos::new(0, 2), Pos::new(0, 1)],
291                        Special(Repeat)
292                    )
293                ]
294            )])))
295        );
296
297        let parse_combos = combos.unwrap().into_parse_combos(&layers);
298
299        let s = serde_json::to_string(&parse_combos).unwrap();
300
301        println!("{s}")
302    }
303}