use crate::{
DofError, DofErrorInner as DErr, Key, Keyboard, Layer, Result, interaction::Pos, keyboard_conv,
};
use serde::{Deserialize, Serialize};
use serde_with::{DisplayFromStr, serde_as};
use std::{collections::BTreeMap, iter, str::FromStr};
#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ComboKey {
key: Key,
nth: usize,
}
impl ComboKey {
fn new(s: &str) -> Self {
let key = s.parse().unwrap();
Self { key, nth: 0 }
}
fn new_nth(s: &str, nth: usize) -> Self {
let key = s.parse().unwrap();
Self { key, nth }
}
}
impl FromStr for ComboKey {
type Err = DofError;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
let ck = match s.len() {
0 => return Err(DErr::EmptyComboKey.into()),
1 | 2 => Self::new(s),
_ => match s.chars().rev().position(|c| c == '-') {
Some(p) => {
let (key, num) = s.split_at(s.len() - p - 1);
let num = &num[1..];
match num.parse::<usize>() {
Ok(nth) => Self::new_nth(key, nth.saturating_sub(1)),
Err(_) => Self::new(s),
}
}
None => Self::new(s),
},
};
Ok(ck)
}
}
impl std::fmt::Display for ComboKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.nth {
0 => write!(f, "{}", self.key),
nth => write!(f, "{}-{}", self.key, nth),
}
}
}
keyboard_conv!(ComboKey, ComboKeyStrAsRow);
#[serde_as]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ParseCombos(
#[serde_as(as = "BTreeMap<_, BTreeMap<ComboKeyStrAsRow, DisplayFromStr>>")]
pub BTreeMap<String, BTreeMap<Vec<ComboKey>, Key>>,
);
impl ParseCombos {
pub(crate) fn into_pos_layers(self, layers: &BTreeMap<String, Layer>) -> Result<Combos> {
let layers = layers
.iter()
.map(|(name, layer)| {
let layer = layer
.rows()
.enumerate()
.flat_map(|(i, row)| {
row.iter()
.enumerate()
.map(move |(j, key)| (Pos::new(i, j), key))
})
.collect::<Vec<_>>();
(name.as_str(), layer)
})
.collect::<BTreeMap<_, _>>();
self.0
.into_iter()
.flat_map(|(layer_name, combos)| {
let layer = layers.get(layer_name.as_str()).map(|l| l.as_slice());
iter::repeat((layer_name, layer)).zip(combos)
})
.map(|((layer_name, layer), (combo, output))| {
let l = layer.ok_or_else(|| {
DErr::UnknownComboLayer(layer_name.clone(), combo_to_str(&combo))
})?;
combo
.iter()
.map(|ck| {
l.iter()
.filter_map(|(pos, key)| (**key == ck.key).then_some(*pos))
.nth(ck.nth)
.ok_or_else(|| {
DErr::InvalidKeyIndex(
combo_to_str(&combo),
ck.key.to_string(),
ck.nth,
)
.into()
})
})
.collect::<Result<Vec<_>>>()
.map(|combo| (layer_name, (combo, output)))
})
.try_fold(
BTreeMap::new(),
|mut acc: BTreeMap<_, Vec<_>>, layer_combo| match layer_combo {
Ok((layer_name, combo)) => {
acc.entry(layer_name).or_default().push(combo);
Ok(acc)
}
Err(e) => Err(e),
},
)
.map(Combos)
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Combos(pub BTreeMap<String, Vec<(Vec<Pos>, Key)>>);
impl Combos {
pub(crate) fn into_parse_combos(self, layers: &BTreeMap<String, Layer>) -> Option<ParseCombos> {
if self.0.is_empty() {
return None;
}
let parse_combos = self
.0
.into_iter()
.map(|(layer_label, combos)| {
let layer = &layers.get(&layer_label).unwrap().0;
let layer_combos = combos
.into_iter()
.map(move |(combo, key)| {
let combo = combo
.into_iter()
.map(|pos| {
let key = layer[pos.row()][pos.col()].clone();
let nth = layer[..(pos.row() + 1)]
.iter()
.flat_map(move |row| &row[..(pos.col() + 1)])
.filter(|k| k == &&key)
.count();
let nth = match nth {
0 | 1 => 0,
n => n,
};
ComboKey::new_nth(&key.to_string(), nth)
})
.collect::<Vec<_>>();
(combo, key)
})
.collect();
(layer_label, layer_combos)
})
.collect();
Some(ParseCombos(parse_combos))
}
}
fn combo_to_str(combos: &[ComboKey]) -> String {
if combos.is_empty() {
String::new()
} else {
combos
.iter()
.take(combos.len() - 1)
.map(|c| format!("{c} "))
.chain([combos.last().unwrap().to_string()])
.collect::<String>()
}
}
#[cfg(test)]
pub(crate) fn ck(key: Key, nth: usize) -> ComboKey {
ComboKey { key, nth }
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Key::*, SpecialKey::*};
#[test]
fn parse_combos() {
let json = r#"
{
"main": {
"a b": "x"
},
"edge-cases": {
"-1 1-": "6",
"--1": "d",
"---": "X",
"🦀-12": "rpt",
"a-1 b-2 c-3 ~-4 rpt-5": "*"
}
}
"#;
let parse =
serde_json::from_str::<ParseCombos>(json).expect("couldn't parse combos json: ");
let reference = ParseCombos(BTreeMap::from([
(
"main".to_string(),
BTreeMap::from([(vec![ck(Char('a'), 0), ck(Char('b'), 0)], Char('x'))]),
),
(
"edge-cases".to_string(),
BTreeMap::from([
(
vec![ck(Word("-1".into()), 0), ck(Word("1-".into()), 0)],
Char('6'),
),
(vec![ck(Char('-'), 0)], Char('d')),
(vec![ck(Word("---".into()), 0)], Char('X')),
(vec![ck(Char('🦀'), 11)], Special(Repeat)),
(
vec![
ck(Char('a'), 0),
ck(Char('b'), 1),
ck(Char('c'), 2),
ck(Empty, 3),
ck(Special(Repeat), 4),
],
Transparent,
),
]),
),
]));
assert_eq!(parse, reference);
}
#[test]
fn to_combos_simple() {
let json = r#"
{
"main": {
"a b": "x",
"e-2 b e": "rpt"
}
}
"#;
let parse =
serde_json::from_str::<ParseCombos>(json).expect("couldn't parse combos json: ");
let layers = BTreeMap::from_iter([(
"main".to_owned(),
vec![vec![Char('a'), Char('e'), Char('b'), Char('c'), Char('e')]].into(),
)]);
let combos = parse.into_pos_layers(&layers);
assert_eq!(
combos,
Ok(Combos(BTreeMap::from_iter([(
"main".to_owned(),
vec![
(vec![Pos::new(0, 0), Pos::new(0, 2)], Char('x')),
(
vec![Pos::new(0, 4), Pos::new(0, 2), Pos::new(0, 1)],
Special(Repeat)
)
]
)])))
);
let parse_combos = combos.unwrap().into_parse_combos(&layers);
let s = serde_json::to_string(&parse_combos).unwrap();
println!("{s}")
}
}