use std::str::FromStr; use serde::{Deserialize, Serialize}; use thiserror::Error; use crate::hlwm::{command::HlwmCommand, hex::ParseHex}; use super::{ color::{Color, X11Color}, command::CommandError, hex::HexError, hlwmbool, octal::{OctalError, ParseOctal}, parser::{FromStringsHint, ParseError, ToOption}, }; #[derive(Debug, Clone, Error)] pub enum AttributeError { #[error("error parsing value: {0}")] ParseError(#[from] ParseError), #[error("unknown attribute type [{0}]")] UnknownType(String), #[error("not a valid rectangle: [{0}]")] NotRectangle(String), #[error("hex parsing error: [{0}]")] HexError(#[from] HexError), #[error("octal parsing error: [{0}]")] OctalError(#[from] OctalError), } impl From for ParseError { fn from(value: AttributeError) -> Self { match value { AttributeError::UnknownType(t) => ParseError::InvalidCommand(t), _ => ParseError::PrimitiveError(value.to_string()), } } } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub enum AttributeOption { Bool(Option), Color(Option), Int(Option), String(Option), Uint(Option), Rectangle(Option<(u32, u32)>), WindowID(Option), } impl FromStringsHint<&str> for AttributeOption { fn from_strings_hint>(s: I, hint: &str) -> Result { let s = s.collect::>().to_option().map(|t| t.join(" ")); Ok(Self::new(AttributeType::from_str(hint)?, s.as_ref())?) } } impl Default for AttributeOption { fn default() -> Self { Self::Int(None) } } impl AttributeOption { pub fn new( attr_type: AttributeType, value_string: Option<&String>, ) -> Result { if let Some(val) = value_string { return Ok(Attribute::new(attr_type, val)?.into()); } match attr_type { AttributeType::Int => Ok(Self::Int(None)), AttributeType::Bool => Ok(Self::Bool(None)), AttributeType::Uint => Ok(Self::Uint(None)), AttributeType::Color => Ok(Self::Color(None)), AttributeType::WindowID => Ok(Self::WindowID(None)), AttributeType::Rectangle => Ok(Self::Rectangle(None)), AttributeType::String => Ok(Self::String(None)), } } fn to_default_attr(&self) -> Attribute { match self { AttributeOption::Int(_) => Attribute::Int(Default::default()), AttributeOption::Uint(_) => Attribute::Uint(Default::default()), AttributeOption::Bool(_) => Attribute::Bool(Default::default()), AttributeOption::Color(_) => Attribute::Color(Default::default()), AttributeOption::String(_) => Attribute::String(Default::default()), AttributeOption::WindowID(_) => Attribute::WindowID(Default::default()), AttributeOption::Rectangle(_) => Attribute::Rectangle { x: Default::default(), y: Default::default(), }, } } pub fn type_string(&self) -> &'static str { self.to_default_attr().type_string() } pub fn value_string(&self) -> Option { match self { AttributeOption::Int(val) => val.map(|val| val.to_string()), AttributeOption::Bool(val) => val.map(|val| val.to_string()), AttributeOption::Uint(val) => val.map(|val| val.to_string()), AttributeOption::Color(val) => val.clone().map(|val| val.to_string()), AttributeOption::String(val) => val.clone().map(|val| val.to_string()), AttributeOption::WindowID(val) => val.map(|w| format!("{w:#x}")), AttributeOption::Rectangle(val) => val.map(|(x, y)| format!("{x}x{y}")), } } } impl From for AttributeOption { fn from(value: Attribute) -> Self { match value { Attribute::Bool(val) => Self::Bool(Some(val)), Attribute::Color(val) => Self::Color(Some(val)), Attribute::Int(val) => Self::Int(Some(val)), Attribute::String(val) => Self::String(Some(val)), Attribute::Uint(val) => Self::Uint(Some(val)), Attribute::Rectangle { x, y } => Self::Rectangle(Some((x, y))), Attribute::WindowID(win) => Self::WindowID(Some(win)), } } } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, strum::EnumDiscriminants)] #[strum_discriminants( name(AttributeType), derive(strum::Display, strum::EnumIter), strum(serialize_all = "lowercase") )] pub enum Attribute { Bool(bool), Color(Color), Int(i32), String(String), Uint(u32), Rectangle { x: u32, y: u32 }, WindowID(u32), } impl From for Attribute { fn from(value: i32) -> Self { Self::Int(value) } } impl From for Attribute { fn from(value: u32) -> Self { Self::Uint(value) } } impl From for Attribute { fn from(value: String) -> Self { Self::String(value) } } impl From<&str> for Attribute { fn from(value: &str) -> Self { Self::String(value.to_string()) } } impl From for Attribute { fn from(value: Color) -> Self { Self::Color(value) } } impl From for Attribute { fn from(value: bool) -> Self { Self::Bool(value) } } impl From<(u32, u32)> for Attribute { fn from((x, y): (u32, u32)) -> Self { Self::Rectangle { x, y } } } impl FromStringsHint<&str> for Attribute { fn from_strings_hint>(s: I, hint: &str) -> Result { let value = s.collect::>().join(" "); Ok(Self::new( AttributeType::get_type_or_guess(hint, &value), &value, )?) } } impl Attribute { pub fn new(attr_type: AttributeType, value_string: &str) -> Result { match attr_type { AttributeType::Bool => Ok(Self::Bool(hlwmbool::from_hlwm_string(value_string)?)), AttributeType::Color => Ok(Attribute::Color(Color::from_str(value_string)?)), AttributeType::Int => Ok(Attribute::Int( value_string.parse().map_err(|err| ParseError::from(err))?, )), AttributeType::String => Ok(Attribute::String(value_string.to_string())), AttributeType::Uint => Ok(Attribute::Uint( value_string.parse().map_err(|err| ParseError::from(err))?, )), AttributeType::Rectangle => { let parts = value_string.split('x').collect::>(); if parts.len() != 2 { return Err(AttributeError::NotRectangle(value_string.to_string())); } Ok(Attribute::Rectangle { x: parts .get(0) .unwrap() .parse() .map_err(|err| ParseError::from(err))?, y: parts .get(1) .unwrap() .parse() .map_err(|err| ParseError::from(err))?, }) } AttributeType::WindowID => { if let Some(hex) = value_string.strip_prefix("0x") { Ok(Attribute::WindowID(hex.parse_hex()?)) } else if let Some(octal) = value_string.strip_prefix("0") { Ok(Attribute::WindowID(octal.parse_octal()?)) } else { Ok(Attribute::WindowID( value_string.parse().map_err(|err| ParseError::from(err))?, )) } } } } pub fn type_string(&self) -> &'static str { match self { Attribute::Bool(_) => "bool", Attribute::Color(_) => "color", Attribute::Int(_) => "int", Attribute::String(_) => "string", Attribute::Uint(_) => "uint", Attribute::Rectangle { x: _, y: _ } => "rectangle", Attribute::WindowID(_) => "windowid", } } } impl std::fmt::Display for Attribute { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Attribute::Bool(b) => write!(f, "{b}"), Attribute::Color(c) => write!(f, "{c}"), Attribute::Int(i) => write!(f, "{i}"), Attribute::String(s) => f.write_str(s), Attribute::Uint(u) => write!(f, "{u}"), Attribute::Rectangle { x, y } => write!(f, "{x}x{y}"), Attribute::WindowID(win) => write!(f, "{win:#x}"), } } } impl Default for Attribute { fn default() -> Self { Self::Int(0) } } impl Default for AttributeType { fn default() -> Self { Self::String } } impl FromStr for AttributeType { type Err = ParseError; fn from_str(s: &str) -> Result { match s { "bool" => Ok(Self::Bool), "color" => Ok(Self::Color), "int" => Ok(Self::Int), "string" | "names" | "regex" | "font" => Ok(Self::String), "uint" => Ok(Self::Uint), "rectangle" => Ok(Self::Rectangle), "windowid" => Ok(Self::WindowID), _ => Err(ParseError::InvalidCommand(s.to_string())), } } } impl AttributeType { /// Gets the type for the given `path` from herbstluftwm pub fn get_type(path: &str) -> Result { Ok(Self::from_str( &String::from_utf8(HlwmCommand::AttrType(path.to_string()).execute()?.stdout)? .split('\n') .map(|l| l.trim()) .filter(|l| !l.is_empty()) .next() .ok_or(CommandError::Empty)?, )?) } /// Tries to get the type for the given `path`, but defaults to [AttributeType::String] /// if that fails in any way pub fn get_type_or_string(path: &str) -> AttributeType { Self::get_type(path).unwrap_or(AttributeType::String) } /// Tries to get the type for the given `path`, and if that fails /// tries to guess the type using [Self::guess] pub fn get_type_or_guess(path: &str, value: &str) -> Self { Self::get_type(path).unwrap_or(Self::guess(value)) } pub fn guess(value: &str) -> Self { if value.is_empty() { return Self::String; } match value { "on" | "true" | "off" | "false" => Self::Bool, _ => { // Match for all colors first if X11Color::iter().into_iter().any(|c| c.to_string() == value) { return Self::Color; } // Is it a valid color string? if Color::from_hex(value).is_ok() { return Self::Color; } // Match for primitive types or color string let mut chars = value.chars(); match chars.next().unwrap() { '+' => Self::guess(&chars.collect::()), '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '-' => Self::Int, _ => Self::String, } } } } }