Skip to main content

zellij_utils/input/
web_client.rs

1use kdl::{KdlDocument, KdlNode, KdlValue};
2use serde::{Deserialize, Serialize};
3
4use crate::{
5    data::PaletteColor, kdl_children_or_error, kdl_first_entry_as_string, kdl_get_child,
6    kdl_get_child_entry_bool_value, kdl_get_child_entry_string_value,
7};
8
9use super::config::ConfigError;
10
11#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
12pub struct WebClientTheme {
13    pub background: Option<String>,
14    pub foreground: Option<String>,
15    pub black: Option<String>,
16    pub blue: Option<String>,
17    pub bright_black: Option<String>,
18    pub bright_blue: Option<String>,
19    pub bright_cyan: Option<String>,
20    pub bright_green: Option<String>,
21    pub bright_magenta: Option<String>,
22    pub bright_red: Option<String>,
23    pub bright_white: Option<String>,
24    pub bright_yellow: Option<String>,
25    pub cursor: Option<String>,
26    pub cursor_accent: Option<String>,
27    pub cyan: Option<String>,
28    pub green: Option<String>,
29    pub magenta: Option<String>,
30    pub red: Option<String>,
31    pub selection_background: Option<String>,
32    pub selection_foreground: Option<String>,
33    pub selection_inactive_background: Option<String>,
34    pub white: Option<String>,
35    pub yellow: Option<String>,
36}
37
38impl WebClientTheme {
39    pub fn from_kdl(kdl: &KdlNode) -> Result<Self, ConfigError> {
40        let mut theme = WebClientTheme::default();
41        let colors = kdl_children_or_error!(kdl, "empty theme");
42
43        // Helper function to extract colors
44        let extract_color = |name: &str| -> Result<Option<String>, ConfigError> {
45            if colors.get(name).is_some() {
46                let color = PaletteColor::try_from((name, colors))?;
47                Ok(Some(color.as_rgb_str()))
48            } else {
49                Ok(None)
50            }
51        };
52
53        theme.background = extract_color("background")?;
54        theme.foreground = extract_color("foreground")?;
55        theme.black = extract_color("black")?;
56        theme.blue = extract_color("blue")?;
57        theme.bright_black = extract_color("bright_black")?;
58        theme.bright_blue = extract_color("bright_blue")?;
59        theme.bright_cyan = extract_color("bright_cyan")?;
60        theme.bright_green = extract_color("bright_green")?;
61        theme.bright_magenta = extract_color("bright_magenta")?;
62        theme.bright_red = extract_color("bright_red")?;
63        theme.bright_white = extract_color("bright_white")?;
64        theme.bright_yellow = extract_color("bright_yellow")?;
65        theme.cursor = extract_color("cursor")?;
66        theme.cursor_accent = extract_color("cursor_accent")?;
67        theme.cyan = extract_color("cyan")?;
68        theme.green = extract_color("green")?;
69        theme.magenta = extract_color("magenta")?;
70        theme.red = extract_color("red")?;
71        theme.selection_background = extract_color("selection_background")?;
72        theme.selection_foreground = extract_color("selection_foreground")?;
73        theme.selection_inactive_background = extract_color("selection_inactive_background")?;
74        theme.white = extract_color("white")?;
75        theme.yellow = extract_color("yellow")?;
76
77        Ok(theme)
78    }
79
80    pub fn to_kdl(&self) -> KdlNode {
81        macro_rules! add_color_nodes {
82            ($theme_children:expr, $self:expr, $($field:ident),+ $(,)?) => {
83                $(
84                    if let Some(color) = &$self.$field {
85                        let node = PaletteColor::from_rgb_str(color).to_kdl(stringify!($field));
86                        $theme_children.nodes_mut().push(node);
87                    }
88                )+
89            };
90        }
91        let mut theme_node = KdlNode::new("theme");
92        let mut theme_children = KdlDocument::new();
93
94        add_color_nodes!(
95            theme_children,
96            self,
97            background,
98            foreground,
99            black,
100            blue,
101            bright_black,
102            bright_blue,
103            bright_cyan,
104            bright_green,
105            bright_magenta,
106            bright_red,
107            bright_white,
108            bright_yellow,
109            cursor,
110            cursor_accent,
111            cyan,
112            green,
113            magenta,
114            red,
115            selection_background,
116            selection_foreground,
117            selection_inactive_background,
118            white,
119            yellow,
120        );
121
122        theme_node.set_children(theme_children);
123        theme_node
124    }
125}
126
127#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
128pub enum CursorInactiveStyle {
129    Outline,
130    Block,
131    Bar,
132    Underline,
133    NoStyle,
134}
135
136impl std::fmt::Display for CursorInactiveStyle {
137    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
138        match self {
139            CursorInactiveStyle::Block => write!(f, "block"),
140            CursorInactiveStyle::Bar => write!(f, "bar"),
141            CursorInactiveStyle::Underline => write!(f, "underline"),
142            CursorInactiveStyle::Outline => write!(f, "outline"),
143            CursorInactiveStyle::NoStyle => write!(f, "none"),
144        }
145    }
146}
147
148impl CursorInactiveStyle {
149    pub fn from_kdl(kdl: &KdlNode) -> Result<Self, ConfigError> {
150        match kdl_first_entry_as_string!(kdl) {
151            Some("block") => Ok(CursorInactiveStyle::Block),
152            Some("bar") => Ok(CursorInactiveStyle::Bar),
153            Some("underline") => Ok(CursorInactiveStyle::Underline),
154            Some("outline") => Ok(CursorInactiveStyle::Outline),
155            Some("no_style") => Ok(CursorInactiveStyle::NoStyle),
156            _ => Err(ConfigError::new_kdl_error(
157                format!("Must be 'block', 'bar', 'underline', 'outline' or 'no_style'"),
158                kdl.span().offset(),
159                kdl.span().len(),
160            )),
161        }
162    }
163    pub fn to_kdl(&self) -> KdlNode {
164        let mut cursor_inactive_style_node = KdlNode::new("cursor_inactive_style");
165        match self {
166            CursorInactiveStyle::Block => {
167                cursor_inactive_style_node.push(KdlValue::String("block".to_owned()));
168            },
169            CursorInactiveStyle::Bar => {
170                cursor_inactive_style_node.push(KdlValue::String("bar".to_owned()));
171            },
172            CursorInactiveStyle::Underline => {
173                cursor_inactive_style_node.push(KdlValue::String("underline".to_owned()));
174            },
175            CursorInactiveStyle::Outline => {
176                cursor_inactive_style_node.push(KdlValue::String("outline".to_owned()));
177            },
178            CursorInactiveStyle::NoStyle => {
179                cursor_inactive_style_node.push(KdlValue::String("no_style".to_owned()));
180            },
181        }
182        cursor_inactive_style_node
183    }
184}
185
186#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
187pub enum CursorStyle {
188    Block,
189    Bar,
190    Underline,
191}
192
193impl std::fmt::Display for CursorStyle {
194    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
195        match self {
196            CursorStyle::Block => write!(f, "block"),
197            CursorStyle::Bar => write!(f, "bar"),
198            CursorStyle::Underline => write!(f, "underline"),
199        }
200    }
201}
202
203impl CursorStyle {
204    pub fn from_kdl(kdl: &KdlNode) -> Result<Self, ConfigError> {
205        match kdl_first_entry_as_string!(kdl) {
206            Some("block") => Ok(CursorStyle::Block),
207            Some("bar") => Ok(CursorStyle::Bar),
208            Some("underline") => Ok(CursorStyle::Underline),
209            _ => Err(ConfigError::new_kdl_error(
210                format!("Must be 'block', 'bar' or 'underline'"),
211                kdl.span().offset(),
212                kdl.span().len(),
213            )),
214        }
215    }
216    pub fn to_kdl(&self) -> KdlNode {
217        let mut cursor_style_node = KdlNode::new("cursor_style");
218        match self {
219            CursorStyle::Block => {
220                cursor_style_node.push(KdlValue::String("block".to_owned()));
221            },
222            CursorStyle::Bar => {
223                cursor_style_node.push(KdlValue::String("bar".to_owned()));
224            },
225            CursorStyle::Underline => {
226                cursor_style_node.push(KdlValue::String("underline".to_owned()));
227            },
228        }
229        cursor_style_node
230    }
231}
232
233#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
234pub struct WebClientConfig {
235    pub font: String,
236    pub theme: Option<WebClientTheme>,
237    pub cursor_blink: bool,
238    pub cursor_inactive_style: Option<CursorInactiveStyle>,
239    pub cursor_style: Option<CursorStyle>,
240    pub mac_option_is_meta: bool,
241    pub base_url: Option<String>,
242    pub font_size: Option<u16>,
243}
244
245impl Default for WebClientConfig {
246    fn default() -> Self {
247        WebClientConfig {
248            font: "monospace".to_string(),
249            theme: None,
250            cursor_blink: false,
251            cursor_inactive_style: None,
252            cursor_style: None,
253            mac_option_is_meta: true, // TODO: yes? no?
254            base_url: None,
255            font_size: None,
256        }
257    }
258}
259
260impl WebClientConfig {
261    pub fn from_kdl(kdl: &KdlNode) -> Result<Self, ConfigError> {
262        let mut web_client_config = WebClientConfig::default();
263
264        if let Some(font) = kdl_get_child_entry_string_value!(kdl, "font") {
265            web_client_config.font = font.to_owned();
266        }
267
268        if let Some(theme_node) = kdl_get_child!(kdl, "theme") {
269            web_client_config.theme = Some(WebClientTheme::from_kdl(theme_node)?);
270        }
271
272        if let Some(cursor_blink) = kdl_get_child_entry_bool_value!(kdl, "cursor_blink") {
273            web_client_config.cursor_blink = cursor_blink;
274        }
275
276        if let Some(cursor_inactive_style_node) = kdl_get_child!(kdl, "cursor_inactive_style") {
277            web_client_config.cursor_inactive_style =
278                Some(CursorInactiveStyle::from_kdl(cursor_inactive_style_node)?);
279        }
280
281        if let Some(cursor_style_node) = kdl_get_child!(kdl, "cursor_style") {
282            web_client_config.cursor_style = Some(CursorStyle::from_kdl(cursor_style_node)?);
283        }
284
285        if let Some(mac_option_is_meta) = kdl_get_child_entry_bool_value!(kdl, "mac_option_is_meta")
286        {
287            web_client_config.mac_option_is_meta = mac_option_is_meta;
288        }
289
290        if let Some(base_url) = kdl_get_child_entry_string_value!(kdl, "base_url") {
291            web_client_config.base_url = Some(base_url.to_owned());
292        }
293
294        if let Some(font_size_node) = kdl_get_child!(kdl, "font_size") {
295            let raw = font_size_node
296                .entries()
297                .iter()
298                .find_map(|e| e.value().as_i64());
299            if let Some(value) = raw {
300                if value > 0 && value <= u16::MAX as i64 {
301                    web_client_config.font_size = Some(value as u16);
302                } else {
303                    return Err(ConfigError::new_kdl_error(
304                        format!("font_size must be between 1 and {}", u16::MAX),
305                        font_size_node.span().offset(),
306                        font_size_node.span().len(),
307                    ));
308                }
309            }
310        }
311
312        Ok(web_client_config)
313    }
314
315    pub fn to_kdl(&self) -> KdlNode {
316        let mut web_client_node = KdlNode::new("web_client");
317        let mut web_client_children = KdlDocument::new();
318
319        let mut font_node = KdlNode::new("font");
320        font_node.push(KdlValue::String(self.font.clone()));
321        web_client_children.nodes_mut().push(font_node);
322
323        if let Some(theme_node) = self.theme.as_ref().map(|t| t.to_kdl()) {
324            web_client_children.nodes_mut().push(theme_node);
325        }
326
327        if self.cursor_blink {
328            // this defaults to false, so we only need to add it if it's true
329            let mut cursor_blink_node = KdlNode::new("cursor_blink");
330            cursor_blink_node.push(KdlValue::Bool(true));
331            web_client_children.nodes_mut().push(cursor_blink_node);
332        }
333
334        if let Some(cursor_inactive_style_node) =
335            self.cursor_inactive_style.as_ref().map(|c| c.to_kdl())
336        {
337            web_client_children
338                .nodes_mut()
339                .push(cursor_inactive_style_node);
340        }
341
342        if let Some(cursor_style_node) = self.cursor_style.as_ref().map(|c| c.to_kdl()) {
343            web_client_children.nodes_mut().push(cursor_style_node);
344        }
345
346        if !self.mac_option_is_meta {
347            // this defaults to true, so we only need to add it if it's false
348            let mut mac_option_is_meta_node = KdlNode::new("mac_option_is_meta");
349            mac_option_is_meta_node.push(KdlValue::Bool(false));
350            web_client_children
351                .nodes_mut()
352                .push(mac_option_is_meta_node);
353        }
354
355        if let Some(base_url) = &self.base_url {
356            let mut base_url_node = KdlNode::new("base_url");
357            base_url_node.push(KdlValue::String(base_url.clone()));
358            web_client_children.nodes_mut().push(base_url_node);
359        }
360
361        if let Some(font_size) = self.font_size {
362            let mut font_size_node = KdlNode::new("font_size");
363            font_size_node.push(KdlValue::Base10(font_size as i64));
364            web_client_children.nodes_mut().push(font_size_node);
365        }
366
367        web_client_node.set_children(web_client_children);
368        web_client_node
369    }
370
371    pub fn merge(&self, other: WebClientConfig) -> Self {
372        let mut merged = self.clone();
373        merged.font = other.font;
374        merged.theme = other.theme;
375        merged.cursor_blink = other.cursor_blink;
376        merged.cursor_inactive_style = other.cursor_inactive_style;
377        merged.cursor_style = other.cursor_style;
378        merged.mac_option_is_meta = other.mac_option_is_meta;
379        merged.base_url = other.base_url;
380        merged.font_size = other.font_size;
381        merged
382    }
383}