use std::collections::BTreeMap;
use rgb::RGB8;
use serde::{
Deserialize,
de::{self, Deserializer},
};
use crate::Error;
pub type Env = BTreeMap<String, String>;
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum ColorError {
#[error("colour {input:?} must start with '#'")]
MissingHashPrefix {
input: String,
},
#[error("colour {input:?} must have six hex digits, found {found}")]
WrongLength {
input: String,
found: usize,
},
#[error("colour {input:?} contains a non-hexadecimal digit")]
NonHexDigit {
input: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum PaletteError {
#[error("palette colour at index {index}: {source}")]
Color {
index: usize,
#[source]
source: ColorError,
},
#[error("palette must contain 8 or 16 colours, found {found}")]
WrongLength {
found: usize,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Rgb(RGB8);
impl Rgb {
#[must_use]
pub fn new(r: u8, g: u8, b: u8) -> Self {
Self(RGB8::new(r, g, b))
}
#[must_use]
pub fn r(&self) -> u8 {
self.0.r
}
#[must_use]
pub fn g(&self) -> u8 {
self.0.g
}
#[must_use]
pub fn b(&self) -> u8 {
self.0.b
}
pub(crate) fn parse(s: &str) -> Result<Self, ColorError> {
let hex = s
.strip_prefix('#')
.ok_or_else(|| ColorError::MissingHashPrefix {
input: s.to_owned(),
})?;
if hex.len() != 6 {
return Err(ColorError::WrongLength {
input: s.to_owned(),
found: hex.len(),
});
}
if !hex.bytes().all(|b| b.is_ascii_hexdigit()) {
return Err(ColorError::NonHexDigit {
input: s.to_owned(),
});
}
let value = u32::from_str_radix(hex, 16).map_err(|_| ColorError::NonHexDigit {
input: s.to_owned(),
})?;
let [_, r, g, b] = value.to_be_bytes();
Ok(Self(RGB8::new(r, g, b)))
}
}
impl<'de> Deserialize<'de> for Rgb {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s = String::deserialize(deserializer)?;
Self::parse(&s).map_err(de::Error::custom)
}
}
pub(crate) fn deserialize_palette<'de, D>(deserializer: D) -> Result<Vec<Rgb>, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
let palette = s
.split(':')
.enumerate()
.map(|(index, colour)| {
Rgb::parse(colour).map_err(|source| PaletteError::Color { index, source })
})
.collect::<Result<Vec<_>, _>>()
.map_err(de::Error::custom)?;
if palette.len() != 8 && palette.len() != 16 {
return Err(de::Error::custom(PaletteError::WrongLength {
found: palette.len(),
}));
}
Ok(palette)
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct Theme {
pub fg: Rgb,
pub bg: Rgb,
#[serde(deserialize_with = "deserialize_palette")]
pub palette: Vec<Rgb>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Resize {
pub cols: u16,
pub rows: u16,
}
impl Resize {
pub(crate) fn parse(s: &str) -> Result<Self, Error> {
let (cols, rows) = s
.split_once('x')
.ok_or_else(|| Error::MalformedEvent(format!("invalid resize payload: {s:?}")))?;
Ok(Self {
cols: cols
.parse()
.map_err(|_| Error::MalformedEvent(format!("invalid resize columns: {s:?}")))?,
rows: rows
.parse()
.map_err(|_| Error::MalformedEvent(format!("invalid resize rows: {s:?}")))?,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ExitStatus(i32);
impl ExitStatus {
pub(crate) fn parse(s: &str) -> Result<Self, Error> {
s.trim()
.parse()
.map(Self)
.map_err(|_| Error::MalformedEvent(format!("invalid exit status: {s:?}")))
}
#[must_use]
pub fn code(&self) -> i32 {
self.0
}
}
#[cfg(test)]
mod tests {
use super::{ColorError, Rgb};
#[test]
fn rgb_parse_returns_typed_errors() {
assert_eq!(
Rgb::parse("ffffff"),
Err(ColorError::MissingHashPrefix {
input: "ffffff".to_owned()
})
);
assert_eq!(
Rgb::parse("#fff"),
Err(ColorError::WrongLength {
input: "#fff".to_owned(),
found: 3
})
);
assert_eq!(
Rgb::parse("#zzzzzz"),
Err(ColorError::NonHexDigit {
input: "#zzzzzz".to_owned()
})
);
assert_eq!(Rgb::parse("#1a2b3c"), Ok(Rgb::new(0x1a, 0x2b, 0x3c)));
}
}