use kdl::{KdlDocument, KdlNode, KdlValue}; use serde::{Deserialize, Serialize}; use crate::{ data::PaletteColor, kdl_children_or_error, kdl_first_entry_as_string, kdl_get_child, kdl_get_child_entry_bool_value, kdl_get_child_entry_string_value, }; use super::config::ConfigError; #[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] pub struct WebClientTheme { pub background: Option, pub foreground: Option, pub black: Option, pub blue: Option, pub bright_black: Option, pub bright_blue: Option, pub bright_cyan: Option, pub bright_green: Option, pub bright_magenta: Option, pub bright_red: Option, pub bright_white: Option, pub bright_yellow: Option, pub cursor: Option, pub cursor_accent: Option, pub cyan: Option, pub green: Option, pub magenta: Option, pub red: Option, pub selection_background: Option, pub selection_foreground: Option, pub selection_inactive_background: Option, pub white: Option, pub yellow: Option, } impl WebClientTheme { pub fn from_kdl(kdl: &KdlNode) -> Result { let mut theme = WebClientTheme::default(); let colors = kdl_children_or_error!(kdl, "empty theme"); // Helper function to extract colors let extract_color = |name: &str| -> Result, ConfigError> { if colors.get(name).is_some() { let color = PaletteColor::try_from((name, colors))?; Ok(Some(color.as_rgb_str())) } else { Ok(None) } }; theme.background = extract_color("background")?; theme.foreground = extract_color("foreground")?; theme.black = extract_color("black")?; theme.blue = extract_color("blue")?; theme.bright_black = extract_color("bright_black")?; theme.bright_blue = extract_color("bright_blue")?; theme.bright_cyan = extract_color("bright_cyan")?; theme.bright_green = extract_color("bright_green")?; theme.bright_magenta = extract_color("bright_magenta")?; theme.bright_red = extract_color("bright_red")?; theme.bright_white = extract_color("bright_white")?; theme.bright_yellow = extract_color("bright_yellow")?; theme.cursor = extract_color("cursor")?; theme.cursor_accent = extract_color("cursor_accent")?; theme.cyan = extract_color("cyan")?; theme.green = extract_color("green")?; theme.magenta = extract_color("magenta")?; theme.red = extract_color("red")?; theme.selection_background = extract_color("selection_background")?; theme.selection_foreground = extract_color("selection_foreground")?; theme.selection_inactive_background = extract_color("selection_inactive_background")?; theme.white = extract_color("white")?; theme.yellow = extract_color("yellow")?; Ok(theme) } pub fn to_kdl(&self) -> KdlNode { macro_rules! add_color_nodes { ($theme_children:expr, $self:expr, $($field:ident),+ $(,)?) => { $( if let Some(color) = &$self.$field { let node = PaletteColor::from_rgb_str(color).to_kdl(stringify!($field)); $theme_children.nodes_mut().push(node); } )+ }; } let mut theme_node = KdlNode::new("theme"); let mut theme_children = KdlDocument::new(); add_color_nodes!( theme_children, self, background, foreground, black, blue, bright_black, bright_blue, bright_cyan, bright_green, bright_magenta, bright_red, bright_white, bright_yellow, cursor, cursor_accent, cyan, green, magenta, red, selection_background, selection_foreground, selection_inactive_background, white, yellow, ); theme_node.set_children(theme_children); theme_node } } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum CursorInactiveStyle { Outline, Block, Bar, Underline, NoStyle, } impl std::fmt::Display for CursorInactiveStyle { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { CursorInactiveStyle::Block => write!(f, "block"), CursorInactiveStyle::Bar => write!(f, "bar"), CursorInactiveStyle::Underline => write!(f, "underline"), CursorInactiveStyle::Outline => write!(f, "outline"), CursorInactiveStyle::NoStyle => write!(f, "none"), } } } impl CursorInactiveStyle { pub fn from_kdl(kdl: &KdlNode) -> Result { match kdl_first_entry_as_string!(kdl) { Some("block") => Ok(CursorInactiveStyle::Block), Some("bar") => Ok(CursorInactiveStyle::Bar), Some("underline") => Ok(CursorInactiveStyle::Underline), Some("outline") => Ok(CursorInactiveStyle::Outline), Some("no_style") => Ok(CursorInactiveStyle::NoStyle), _ => Err(ConfigError::new_kdl_error( format!("Must be 'block', 'bar', 'underline', 'outline' or 'no_style'"), kdl.span().offset(), kdl.span().len(), )), } } pub fn to_kdl(&self) -> KdlNode { let mut cursor_inactive_style_node = KdlNode::new("cursor_inactive_style"); match self { CursorInactiveStyle::Block => { cursor_inactive_style_node.push(KdlValue::String("block".to_owned())); }, CursorInactiveStyle::Bar => { cursor_inactive_style_node.push(KdlValue::String("bar".to_owned())); }, CursorInactiveStyle::Underline => { cursor_inactive_style_node.push(KdlValue::String("underline".to_owned())); }, CursorInactiveStyle::Outline => { cursor_inactive_style_node.push(KdlValue::String("outline".to_owned())); }, CursorInactiveStyle::NoStyle => { cursor_inactive_style_node.push(KdlValue::String("no_style".to_owned())); }, } cursor_inactive_style_node } } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum CursorStyle { Block, Bar, Underline, } impl std::fmt::Display for CursorStyle { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { CursorStyle::Block => write!(f, "block"), CursorStyle::Bar => write!(f, "bar"), CursorStyle::Underline => write!(f, "underline"), } } } impl CursorStyle { pub fn from_kdl(kdl: &KdlNode) -> Result { match kdl_first_entry_as_string!(kdl) { Some("block") => Ok(CursorStyle::Block), Some("bar") => Ok(CursorStyle::Bar), Some("underline") => Ok(CursorStyle::Underline), _ => Err(ConfigError::new_kdl_error( format!("Must be 'block', 'bar' or 'underline'"), kdl.span().offset(), kdl.span().len(), )), } } pub fn to_kdl(&self) -> KdlNode { let mut cursor_style_node = KdlNode::new("cursor_style"); match self { CursorStyle::Block => { cursor_style_node.push(KdlValue::String("block".to_owned())); }, CursorStyle::Bar => { cursor_style_node.push(KdlValue::String("bar".to_owned())); }, CursorStyle::Underline => { cursor_style_node.push(KdlValue::String("underline".to_owned())); }, } cursor_style_node } } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct WebClientConfig { pub font: String, pub theme: Option, pub cursor_blink: bool, pub cursor_inactive_style: Option, pub cursor_style: Option, pub mac_option_is_meta: bool, pub base_url: Option, pub font_size: Option, } impl Default for WebClientConfig { fn default() -> Self { WebClientConfig { font: "monospace".to_string(), theme: None, cursor_blink: false, cursor_inactive_style: None, cursor_style: None, mac_option_is_meta: true, // TODO: yes? no? base_url: None, font_size: None, } } } impl WebClientConfig { pub fn from_kdl(kdl: &KdlNode) -> Result { let mut web_client_config = WebClientConfig::default(); if let Some(font) = kdl_get_child_entry_string_value!(kdl, "font") { web_client_config.font = font.to_owned(); } if let Some(theme_node) = kdl_get_child!(kdl, "theme") { web_client_config.theme = Some(WebClientTheme::from_kdl(theme_node)?); } if let Some(cursor_blink) = kdl_get_child_entry_bool_value!(kdl, "cursor_blink") { web_client_config.cursor_blink = cursor_blink; } if let Some(cursor_inactive_style_node) = kdl_get_child!(kdl, "cursor_inactive_style") { web_client_config.cursor_inactive_style = Some(CursorInactiveStyle::from_kdl(cursor_inactive_style_node)?); } if let Some(cursor_style_node) = kdl_get_child!(kdl, "cursor_style") { web_client_config.cursor_style = Some(CursorStyle::from_kdl(cursor_style_node)?); } if let Some(mac_option_is_meta) = kdl_get_child_entry_bool_value!(kdl, "mac_option_is_meta") { web_client_config.mac_option_is_meta = mac_option_is_meta; } if let Some(base_url) = kdl_get_child_entry_string_value!(kdl, "base_url") { web_client_config.base_url = Some(base_url.to_owned()); } if let Some(font_size_node) = kdl_get_child!(kdl, "font_size") { let raw = font_size_node .entries() .iter() .find_map(|e| e.value().as_i64()); if let Some(value) = raw { if value > 0 && value <= u16::MAX as i64 { web_client_config.font_size = Some(value as u16); } else { return Err(ConfigError::new_kdl_error( format!("font_size must be between 1 and {}", u16::MAX), font_size_node.span().offset(), font_size_node.span().len(), )); } } } Ok(web_client_config) } pub fn to_kdl(&self) -> KdlNode { let mut web_client_node = KdlNode::new("web_client"); let mut web_client_children = KdlDocument::new(); let mut font_node = KdlNode::new("font"); font_node.push(KdlValue::String(self.font.clone())); web_client_children.nodes_mut().push(font_node); if let Some(theme_node) = self.theme.as_ref().map(|t| t.to_kdl()) { web_client_children.nodes_mut().push(theme_node); } if self.cursor_blink { // this defaults to false, so we only need to add it if it's true let mut cursor_blink_node = KdlNode::new("cursor_blink"); cursor_blink_node.push(KdlValue::Bool(true)); web_client_children.nodes_mut().push(cursor_blink_node); } if let Some(cursor_inactive_style_node) = self.cursor_inactive_style.as_ref().map(|c| c.to_kdl()) { web_client_children .nodes_mut() .push(cursor_inactive_style_node); } if let Some(cursor_style_node) = self.cursor_style.as_ref().map(|c| c.to_kdl()) { web_client_children.nodes_mut().push(cursor_style_node); } if !self.mac_option_is_meta { // this defaults to true, so we only need to add it if it's false let mut mac_option_is_meta_node = KdlNode::new("mac_option_is_meta"); mac_option_is_meta_node.push(KdlValue::Bool(false)); web_client_children .nodes_mut() .push(mac_option_is_meta_node); } if let Some(base_url) = &self.base_url { let mut base_url_node = KdlNode::new("base_url"); base_url_node.push(KdlValue::String(base_url.clone())); web_client_children.nodes_mut().push(base_url_node); } if let Some(font_size) = self.font_size { let mut font_size_node = KdlNode::new("font_size"); font_size_node.push(KdlValue::Base10(font_size as i64)); web_client_children.nodes_mut().push(font_size_node); } web_client_node.set_children(web_client_children); web_client_node } pub fn merge(&self, other: WebClientConfig) -> Self { let mut merged = self.clone(); merged.font = other.font; merged.theme = other.theme; merged.cursor_blink = other.cursor_blink; merged.cursor_inactive_style = other.cursor_inactive_style; merged.cursor_style = other.cursor_style; merged.mac_option_is_meta = other.mac_option_is_meta; merged.base_url = other.base_url; merged.font_size = other.font_size; merged } }