perky 0.1.5

An application to permute and score keyboard layouts.
Documentation
use core::{
    error::Error,
    fmt::{self, Display},
};

use std::{fs::File, path::Path};

use serde_json::Value;

use crate::{json::read_enveloped_data, tables::Table};

#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum Laterality {
    Left,
    Right,
}

impl Laterality {
    pub fn as_char(&self) -> char {
        use Laterality::*;
        match self {
            Left => 'l',
            Right => 'r',
        }
    }
}

impl Display for Laterality {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.as_char())
    }
}

impl TryFrom<char> for Laterality {
    type Error = String;

    fn try_from(value: char) -> Result<Self, Self::Error> {
        use Laterality::*;
        match value {
            'l' => Ok(Left),
            'r' => Ok(Right),
            _ => Err(format!("Invalid laterality character '{}'", value)),
        }
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum Position {
    Thumb,
    Index,
    Middle,
    Ring,
    Pinky,
}

impl Position {
    pub fn as_char(&self) -> char {
        use Position::*;
        match self {
            Thumb => 't',
            Index => 'i',
            Middle => 'm',
            Ring => 'r',
            Pinky => 'p',
        }
    }
}

impl Display for Position {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.as_char())
    }
}

impl TryFrom<char> for Position {
    type Error = String;

    fn try_from(value: char) -> Result<Self, Self::Error> {
        use Position::*;
        match value {
            't' => Ok(Thumb),
            'i' => Ok(Index),
            'm' => Ok(Middle),
            'r' => Ok(Ring),
            'p' => Ok(Pinky),
            _ => Err(format!("Invalid position character '{}'", value)),
        }
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct Digit(pub Laterality, pub Position);

impl Display for Digit {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}{}", self.0, self.1)
    }
}

impl From<Digit> for Value {
    fn from(value: Digit) -> Value {
        Value::String(value.to_string())
    }
}

impl TryFrom<&str> for Digit {
    type Error = String;

    fn try_from(value: &str) -> Result<Self, Self::Error> {
        let mut chars = value.chars();
        match (chars.next(), chars.next(), chars.next()) {
            (Some(c1), Some(c2), None) => {
                let laterality = Laterality::try_from(c1)?;
                let position = Position::try_from(c2)?;
                Ok(Digit(laterality, position))
            }
            _ => Err(format!(
                "Invalid digit string '{}': expected two ASCII characters",
                value
            )),
        }
    }
}

impl TryFrom<&Value> for Digit {
    type Error = String;

    fn try_from(value: &Value) -> Result<Self, Self::Error> {
        match value {
            Value::String(s) => Digit::try_from(s.as_str()),
            _ => Err("Invalid type: expected a string of two ASCII characters".into()),
        }
    }
}

pub struct LayoutTable<const C: usize, const R: usize>(pub Table<C, R, Digit>);

impl<const C: usize, const R: usize> LayoutTable<C, R> {
    pub fn mask<F: FnMut(usize, usize, &Digit) -> bool>(&mut self, mut predicate: F) {
        (0..R).for_each(|r| {
            (0..C).for_each(|c| {
                if let Some(digit) = &self.0[r][c] {
                    if !predicate(r, c, digit) {
                        self.0[r][c] = None;
                    }
                }
            })
        })
    }

    pub fn read_from_path(path: &Path) -> Result<Self, Box<dyn Error>> {
        const EXPECTED_VERSION: u64 = 1;
        let file = File::open(path)?;
        let value = read_enveloped_data::<_, Value>(file, EXPECTED_VERSION)?;
        Ok(LayoutTable::try_from(&value)?)
    }
}

impl<const C: usize, const R: usize> Default for LayoutTable<C, R> {
    fn default() -> Self {
        Self(Default::default())
    }
}

impl<const C: usize, const R: usize> From<&LayoutTable<C, R>> for Value {
    fn from(value: &LayoutTable<C, R>) -> Self {
        Value::from(&value.0)
    }
}

impl<const C: usize, const R: usize> TryFrom<&Value> for LayoutTable<C, R> {
    type Error = String;

    fn try_from(value: &Value) -> Result<Self, Self::Error> {
        Table::<C, R, Digit>::try_from(value).map(LayoutTable)
    }
}