zellij_utils/kdl/
mod.rs

1mod kdl_layout_parser;
2use crate::data::{
3    BareKey, Direction, FloatingPaneCoordinates, InputMode, KeyWithModifier, LayoutInfo,
4    MultiplayerColors, Palette, PaletteColor, PaneInfo, PaneManifest, PermissionType, Resize,
5    SessionInfo, StyleDeclaration, Styling, TabInfo, WebSharing, DEFAULT_STYLES,
6};
7use crate::envs::EnvironmentVariables;
8use crate::home::{find_default_config_dir, get_layout_dir};
9use crate::input::config::{Config, ConfigError, KdlError};
10use crate::input::keybinds::Keybinds;
11use crate::input::layout::{
12    Layout, PluginUserConfiguration, RunPlugin, RunPluginOrAlias, SplitSize,
13};
14use crate::input::options::{Clipboard, OnForceClose, Options};
15use crate::input::permission::{GrantedPermission, PermissionCache};
16use crate::input::plugins::PluginAliases;
17use crate::input::theme::{FrameConfig, Theme, Themes, UiConfig};
18use crate::input::web_client::WebClientConfig;
19use kdl_layout_parser::KdlLayoutParser;
20use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
21use std::net::{IpAddr, Ipv4Addr};
22use strum::IntoEnumIterator;
23use uuid::Uuid;
24
25use miette::NamedSource;
26
27use kdl::{KdlDocument, KdlEntry, KdlNode, KdlValue};
28
29use std::path::PathBuf;
30use std::str::FromStr;
31
32use crate::input::actions::{Action, SearchDirection, SearchOption};
33use crate::input::command::RunCommandAction;
34
35#[macro_export]
36macro_rules! parse_kdl_action_arguments {
37    ( $action_name:expr, $action_arguments:expr, $action_node:expr ) => {{
38        if !$action_arguments.is_empty() {
39            Err(ConfigError::new_kdl_error(
40                format!("Action '{}' must have arguments", $action_name),
41                $action_node.span().offset(),
42                $action_node.span().len(),
43            ))
44        } else {
45            match $action_name {
46                "Quit" => Ok(Action::Quit),
47                "FocusNextPane" => Ok(Action::FocusNextPane),
48                "FocusPreviousPane" => Ok(Action::FocusPreviousPane),
49                "SwitchFocus" => Ok(Action::SwitchFocus),
50                "EditScrollback" => Ok(Action::EditScrollback),
51                "ScrollUp" => Ok(Action::ScrollUp),
52                "ScrollDown" => Ok(Action::ScrollDown),
53                "ScrollToBottom" => Ok(Action::ScrollToBottom),
54                "ScrollToTop" => Ok(Action::ScrollToTop),
55                "PageScrollUp" => Ok(Action::PageScrollUp),
56                "PageScrollDown" => Ok(Action::PageScrollDown),
57                "HalfPageScrollUp" => Ok(Action::HalfPageScrollUp),
58                "HalfPageScrollDown" => Ok(Action::HalfPageScrollDown),
59                "ToggleFocusFullscreen" => Ok(Action::ToggleFocusFullscreen),
60                "TogglePaneFrames" => Ok(Action::TogglePaneFrames),
61                "ToggleActiveSyncTab" => Ok(Action::ToggleActiveSyncTab),
62                "TogglePaneEmbedOrFloating" => Ok(Action::TogglePaneEmbedOrFloating),
63                "ToggleFloatingPanes" => Ok(Action::ToggleFloatingPanes),
64                "CloseFocus" => Ok(Action::CloseFocus),
65                "UndoRenamePane" => Ok(Action::UndoRenamePane),
66                "NoOp" => Ok(Action::NoOp),
67                "GoToNextTab" => Ok(Action::GoToNextTab),
68                "GoToPreviousTab" => Ok(Action::GoToPreviousTab),
69                "CloseTab" => Ok(Action::CloseTab),
70                "ToggleTab" => Ok(Action::ToggleTab),
71                "UndoRenameTab" => Ok(Action::UndoRenameTab),
72                "Detach" => Ok(Action::Detach),
73                "Copy" => Ok(Action::Copy),
74                "Confirm" => Ok(Action::Confirm),
75                "Deny" => Ok(Action::Deny),
76                "ToggleMouseMode" => Ok(Action::ToggleMouseMode),
77                "PreviousSwapLayout" => Ok(Action::PreviousSwapLayout),
78                "NextSwapLayout" => Ok(Action::NextSwapLayout),
79                "Clear" => Ok(Action::ClearScreen),
80                _ => Err(ConfigError::new_kdl_error(
81                    format!("Unsupported action: {:?}", $action_name),
82                    $action_node.span().offset(),
83                    $action_node.span().len(),
84                )),
85            }
86        }
87    }};
88}
89
90#[macro_export]
91macro_rules! parse_kdl_action_u8_arguments {
92    ( $action_name:expr, $action_arguments:expr, $action_node:expr ) => {{
93        let mut bytes = vec![];
94        for kdl_entry in $action_arguments.iter() {
95            match kdl_entry.value().as_i64() {
96                Some(int_value) => bytes.push(int_value as u8),
97                None => {
98                    return Err(ConfigError::new_kdl_error(
99                        format!("Arguments for '{}' must be integers", $action_name),
100                        kdl_entry.span().offset(),
101                        kdl_entry.span().len(),
102                    ));
103                },
104            }
105        }
106        Action::new_from_bytes($action_name, bytes, $action_node)
107    }};
108}
109
110#[macro_export]
111macro_rules! kdl_parsing_error {
112    ( $message:expr, $entry:expr ) => {
113        ConfigError::new_kdl_error($message, $entry.span().offset(), $entry.span().len())
114    };
115}
116
117#[macro_export]
118macro_rules! kdl_entries_as_i64 {
119    ( $node:expr ) => {
120        $node
121            .entries()
122            .iter()
123            .map(|kdl_node| kdl_node.value().as_i64())
124    };
125}
126
127#[macro_export]
128macro_rules! kdl_first_entry_as_string {
129    ( $node:expr ) => {
130        $node
131            .entries()
132            .iter()
133            .next()
134            .and_then(|s| s.value().as_string())
135    };
136}
137
138#[macro_export]
139macro_rules! kdl_first_entry_as_i64 {
140    ( $node:expr ) => {
141        $node
142            .entries()
143            .iter()
144            .next()
145            .and_then(|i| i.value().as_i64())
146    };
147}
148
149#[macro_export]
150macro_rules! kdl_first_entry_as_bool {
151    ( $node:expr ) => {
152        $node
153            .entries()
154            .iter()
155            .next()
156            .and_then(|i| i.value().as_bool())
157    };
158}
159
160#[macro_export]
161macro_rules! entry_count {
162    ( $node:expr ) => {{
163        $node.entries().iter().len()
164    }};
165}
166
167#[macro_export]
168macro_rules! parse_kdl_action_char_or_string_arguments {
169    ( $action_name:expr, $action_arguments:expr, $action_node:expr ) => {{
170        let mut chars_to_write = String::new();
171        for kdl_entry in $action_arguments.iter() {
172            match kdl_entry.value().as_string() {
173                Some(string_value) => chars_to_write.push_str(string_value),
174                None => {
175                    return Err(ConfigError::new_kdl_error(
176                        format!("All entries for action '{}' must be strings", $action_name),
177                        kdl_entry.span().offset(),
178                        kdl_entry.span().len(),
179                    ))
180                },
181            }
182        }
183        Action::new_from_string($action_name, chars_to_write, $action_node)
184    }};
185}
186
187#[macro_export]
188macro_rules! kdl_arg_is_truthy {
189    ( $kdl_node:expr, $arg_name:expr ) => {
190        match $kdl_node.get($arg_name) {
191            Some(arg) => match arg.value().as_bool() {
192                Some(value) => value,
193                None => {
194                    return Err(ConfigError::new_kdl_error(
195                        format!("Argument must be true or false, found: {}", arg.value()),
196                        arg.span().offset(),
197                        arg.span().len(),
198                    ))
199                },
200            },
201            None => false,
202        }
203    };
204}
205
206#[macro_export]
207macro_rules! kdl_children_nodes_or_error {
208    ( $kdl_node:expr, $error:expr ) => {
209        $kdl_node
210            .children()
211            .ok_or(ConfigError::new_kdl_error(
212                $error.into(),
213                $kdl_node.span().offset(),
214                $kdl_node.span().len(),
215            ))?
216            .nodes()
217    };
218}
219
220#[macro_export]
221macro_rules! kdl_children_nodes {
222    ( $kdl_node:expr ) => {
223        $kdl_node.children().map(|c| c.nodes())
224    };
225}
226
227#[macro_export]
228macro_rules! kdl_property_nodes {
229    ( $kdl_node:expr ) => {{
230        $kdl_node
231            .entries()
232            .iter()
233            .filter_map(|e| e.name())
234            .map(|e| e.value())
235    }};
236}
237
238#[macro_export]
239macro_rules! kdl_children_or_error {
240    ( $kdl_node:expr, $error:expr ) => {
241        $kdl_node.children().ok_or(ConfigError::new_kdl_error(
242            $error.into(),
243            $kdl_node.span().offset(),
244            $kdl_node.span().len(),
245        ))?
246    };
247}
248
249#[macro_export]
250macro_rules! kdl_children {
251    ( $kdl_node:expr ) => {
252        $kdl_node.children().iter().copied().collect()
253    };
254}
255
256#[macro_export]
257macro_rules! kdl_get_string_property_or_child_value {
258    ( $kdl_node:expr, $name:expr ) => {
259        $kdl_node
260            .get($name)
261            .and_then(|e| e.value().as_string())
262            .or_else(|| {
263                $kdl_node
264                    .children()
265                    .and_then(|c| c.get($name))
266                    .and_then(|c| c.get(0))
267                    .and_then(|c| c.value().as_string())
268            })
269    };
270}
271
272#[macro_export]
273macro_rules! kdl_string_arguments {
274    ( $kdl_node:expr ) => {{
275        let res: Result<Vec<_>, _> = $kdl_node
276            .entries()
277            .iter()
278            .map(|e| {
279                e.value().as_string().ok_or(ConfigError::new_kdl_error(
280                    "Not a string".into(),
281                    e.span().offset(),
282                    e.span().len(),
283                ))
284            })
285            .collect();
286        res?
287    }};
288}
289
290#[macro_export]
291macro_rules! kdl_property_names {
292    ( $kdl_node:expr ) => {{
293        $kdl_node
294            .entries()
295            .iter()
296            .filter_map(|e| e.name())
297            .map(|e| e.value())
298    }};
299}
300
301#[macro_export]
302macro_rules! kdl_argument_values {
303    ( $kdl_node:expr ) => {
304        $kdl_node.entries().iter().collect()
305    };
306}
307
308#[macro_export]
309macro_rules! kdl_name {
310    ( $kdl_node:expr ) => {
311        $kdl_node.name().value()
312    };
313}
314
315#[macro_export]
316macro_rules! kdl_document_name {
317    ( $kdl_node:expr ) => {
318        $kdl_node.node().name().value()
319    };
320}
321
322#[macro_export]
323macro_rules! keys_from_kdl {
324    ( $kdl_node:expr ) => {
325        kdl_string_arguments!($kdl_node)
326            .iter()
327            .map(|k| {
328                KeyWithModifier::from_str(k).map_err(|_| {
329                    ConfigError::new_kdl_error(
330                        format!("Invalid key: '{}'", k),
331                        $kdl_node.span().offset(),
332                        $kdl_node.span().len(),
333                    )
334                })
335            })
336            .collect::<Result<_, _>>()?
337    };
338}
339
340#[macro_export]
341macro_rules! actions_from_kdl {
342    ( $kdl_node:expr, $config_options:expr ) => {
343        kdl_children_nodes_or_error!($kdl_node, "no actions found for key_block")
344            .iter()
345            .map(|kdl_action| Action::try_from((kdl_action, $config_options)))
346            .collect::<Result<_, _>>()?
347    };
348}
349
350pub fn kdl_arguments_that_are_strings<'a>(
351    arguments: impl Iterator<Item = &'a KdlEntry>,
352) -> Result<Vec<String>, ConfigError> {
353    let mut args: Vec<String> = vec![];
354    for kdl_entry in arguments {
355        match kdl_entry.value().as_string() {
356            Some(string_value) => args.push(string_value.to_string()),
357            None => {
358                return Err(ConfigError::new_kdl_error(
359                    format!("Argument must be a string"),
360                    kdl_entry.span().offset(),
361                    kdl_entry.span().len(),
362                ));
363            },
364        }
365    }
366    Ok(args)
367}
368
369pub fn kdl_arguments_that_are_digits<'a>(
370    arguments: impl Iterator<Item = &'a KdlEntry>,
371) -> Result<Vec<i64>, ConfigError> {
372    let mut args: Vec<i64> = vec![];
373    for kdl_entry in arguments {
374        match kdl_entry.value().as_i64() {
375            Some(digit_value) => {
376                args.push(digit_value);
377            },
378            None => {
379                return Err(ConfigError::new_kdl_error(
380                    format!("Argument must be a digit"),
381                    kdl_entry.span().offset(),
382                    kdl_entry.span().len(),
383                ));
384            },
385        }
386    }
387    Ok(args)
388}
389
390pub fn kdl_child_string_value_for_entry<'a>(
391    command_metadata: &'a KdlDocument,
392    entry_name: &'a str,
393) -> Option<&'a str> {
394    command_metadata
395        .get(entry_name)
396        .and_then(|cwd| cwd.entries().iter().next())
397        .and_then(|cwd_value| cwd_value.value().as_string())
398}
399
400pub fn kdl_child_bool_value_for_entry<'a>(
401    command_metadata: &'a KdlDocument,
402    entry_name: &'a str,
403) -> Option<bool> {
404    command_metadata
405        .get(entry_name)
406        .and_then(|cwd| cwd.entries().iter().next())
407        .and_then(|cwd_value| cwd_value.value().as_bool())
408}
409
410impl Action {
411    pub fn new_from_bytes(
412        action_name: &str,
413        bytes: Vec<u8>,
414        action_node: &KdlNode,
415    ) -> Result<Self, ConfigError> {
416        match action_name {
417            "Write" => Ok(Action::Write(None, bytes, false)),
418            "PaneNameInput" => Ok(Action::PaneNameInput(bytes)),
419            "TabNameInput" => Ok(Action::TabNameInput(bytes)),
420            "SearchInput" => Ok(Action::SearchInput(bytes)),
421            "GoToTab" => {
422                let tab_index = *bytes.get(0).ok_or_else(|| {
423                    ConfigError::new_kdl_error(
424                        format!("Missing tab index"),
425                        action_node.span().offset(),
426                        action_node.span().len(),
427                    )
428                })? as u32;
429                Ok(Action::GoToTab(tab_index))
430            },
431            _ => Err(ConfigError::new_kdl_error(
432                "Failed to parse action".into(),
433                action_node.span().offset(),
434                action_node.span().len(),
435            )),
436        }
437    }
438    pub fn new_from_string(
439        action_name: &str,
440        string: String,
441        action_node: &KdlNode,
442    ) -> Result<Self, ConfigError> {
443        match action_name {
444            "WriteChars" => Ok(Action::WriteChars(string)),
445            "SwitchToMode" => match InputMode::from_str(string.as_str()) {
446                Ok(input_mode) => Ok(Action::SwitchToMode(input_mode)),
447                Err(_e) => {
448                    return Err(ConfigError::new_kdl_error(
449                        format!("Unknown InputMode '{}'", string),
450                        action_node.span().offset(),
451                        action_node.span().len(),
452                    ))
453                },
454            },
455            "Resize" => {
456                let mut resize: Option<Resize> = None;
457                let mut direction: Option<Direction> = None;
458                for word in string.to_ascii_lowercase().split_whitespace() {
459                    match Resize::from_str(word) {
460                        Ok(value) => resize = Some(value),
461                        Err(_) => match Direction::from_str(word) {
462                            Ok(value) => direction = Some(value),
463                            Err(_) => {
464                                return Err(ConfigError::new_kdl_error(
465                                    format!(
466                                    "failed to read either of resize type or direction from '{}'",
467                                    word
468                                ),
469                                    action_node.span().offset(),
470                                    action_node.span().len(),
471                                ))
472                            },
473                        },
474                    }
475                }
476                let resize = resize.unwrap_or(Resize::Increase);
477                Ok(Action::Resize(resize, direction))
478            },
479            "MoveFocus" => {
480                let direction = Direction::from_str(string.as_str()).map_err(|_| {
481                    ConfigError::new_kdl_error(
482                        format!("Invalid direction: '{}'", string),
483                        action_node.span().offset(),
484                        action_node.span().len(),
485                    )
486                })?;
487                Ok(Action::MoveFocus(direction))
488            },
489            "MoveFocusOrTab" => {
490                let direction = Direction::from_str(string.as_str()).map_err(|_| {
491                    ConfigError::new_kdl_error(
492                        format!("Invalid direction: '{}'", string),
493                        action_node.span().offset(),
494                        action_node.span().len(),
495                    )
496                })?;
497                Ok(Action::MoveFocusOrTab(direction))
498            },
499            "MoveTab" => {
500                let direction = Direction::from_str(string.as_str()).map_err(|_| {
501                    ConfigError::new_kdl_error(
502                        format!("Invalid direction: '{}'", string),
503                        action_node.span().offset(),
504                        action_node.span().len(),
505                    )
506                })?;
507                if direction.is_vertical() {
508                    Err(ConfigError::new_kdl_error(
509                        format!("Invalid horizontal direction: '{}'", string),
510                        action_node.span().offset(),
511                        action_node.span().len(),
512                    ))
513                } else {
514                    Ok(Action::MoveTab(direction))
515                }
516            },
517            "MovePane" => {
518                if string.is_empty() {
519                    return Ok(Action::MovePane(None));
520                } else {
521                    let direction = Direction::from_str(string.as_str()).map_err(|_| {
522                        ConfigError::new_kdl_error(
523                            format!("Invalid direction: '{}'", string),
524                            action_node.span().offset(),
525                            action_node.span().len(),
526                        )
527                    })?;
528                    Ok(Action::MovePane(Some(direction)))
529                }
530            },
531            "MovePaneBackwards" => Ok(Action::MovePaneBackwards),
532            "DumpScreen" => Ok(Action::DumpScreen(string, false)),
533            "DumpLayout" => Ok(Action::DumpLayout),
534            "NewPane" => {
535                if string.is_empty() {
536                    return Ok(Action::NewPane(None, None, false));
537                } else if string == "stacked" {
538                    return Ok(Action::NewStackedPane(None, None));
539                } else {
540                    let direction = Direction::from_str(string.as_str()).map_err(|_| {
541                        ConfigError::new_kdl_error(
542                            format!("Invalid direction: '{}'", string),
543                            action_node.span().offset(),
544                            action_node.span().len(),
545                        )
546                    })?;
547                    Ok(Action::NewPane(Some(direction), None, false))
548                }
549            },
550            "SearchToggleOption" => {
551                let toggle_option = SearchOption::from_str(string.as_str()).map_err(|_| {
552                    ConfigError::new_kdl_error(
553                        format!("Invalid direction: '{}'", string),
554                        action_node.span().offset(),
555                        action_node.span().len(),
556                    )
557                })?;
558                Ok(Action::SearchToggleOption(toggle_option))
559            },
560            "Search" => {
561                let search_direction =
562                    SearchDirection::from_str(string.as_str()).map_err(|_| {
563                        ConfigError::new_kdl_error(
564                            format!("Invalid direction: '{}'", string),
565                            action_node.span().offset(),
566                            action_node.span().len(),
567                        )
568                    })?;
569                Ok(Action::Search(search_direction))
570            },
571            "RenameSession" => Ok(Action::RenameSession(string)),
572            _ => Err(ConfigError::new_kdl_error(
573                format!("Unsupported action: {}", action_name),
574                action_node.span().offset(),
575                action_node.span().len(),
576            )),
577        }
578    }
579    pub fn to_kdl(&self) -> Option<KdlNode> {
580        match self {
581            Action::Quit => Some(KdlNode::new("Quit")),
582            Action::Write(_key, bytes, _is_kitty) => {
583                let mut node = KdlNode::new("Write");
584                for byte in bytes {
585                    node.push(KdlValue::Base10(*byte as i64));
586                }
587                Some(node)
588            },
589            Action::WriteChars(string) => {
590                let mut node = KdlNode::new("WriteChars");
591                node.push(string.clone());
592                Some(node)
593            },
594            Action::SwitchToMode(input_mode) => {
595                let mut node = KdlNode::new("SwitchToMode");
596                node.push(format!("{:?}", input_mode).to_lowercase());
597                Some(node)
598            },
599            Action::Resize(resize, resize_direction) => {
600                let mut node = KdlNode::new("Resize");
601                let resize = match resize {
602                    Resize::Increase => "Increase",
603                    Resize::Decrease => "Decrease",
604                };
605                if let Some(resize_direction) = resize_direction {
606                    let resize_direction = match resize_direction {
607                        Direction::Left => "left",
608                        Direction::Right => "right",
609                        Direction::Up => "up",
610                        Direction::Down => "down",
611                    };
612                    node.push(format!("{} {}", resize, resize_direction));
613                } else {
614                    node.push(format!("{}", resize));
615                }
616                Some(node)
617            },
618            Action::FocusNextPane => Some(KdlNode::new("FocusNextPane")),
619            Action::FocusPreviousPane => Some(KdlNode::new("FocusPreviousPane")),
620            Action::SwitchFocus => Some(KdlNode::new("SwitchFocus")),
621            Action::MoveFocus(direction) => {
622                let mut node = KdlNode::new("MoveFocus");
623                let direction = match direction {
624                    Direction::Left => "left",
625                    Direction::Right => "right",
626                    Direction::Up => "up",
627                    Direction::Down => "down",
628                };
629                node.push(direction);
630                Some(node)
631            },
632            Action::MoveFocusOrTab(direction) => {
633                let mut node = KdlNode::new("MoveFocusOrTab");
634                let direction = match direction {
635                    Direction::Left => "left",
636                    Direction::Right => "right",
637                    Direction::Up => "up",
638                    Direction::Down => "down",
639                };
640                node.push(direction);
641                Some(node)
642            },
643            Action::MovePane(direction) => {
644                let mut node = KdlNode::new("MovePane");
645                if let Some(direction) = direction {
646                    let direction = match direction {
647                        Direction::Left => "left",
648                        Direction::Right => "right",
649                        Direction::Up => "up",
650                        Direction::Down => "down",
651                    };
652                    node.push(direction);
653                }
654                Some(node)
655            },
656            Action::MovePaneBackwards => Some(KdlNode::new("MovePaneBackwards")),
657            Action::DumpScreen(file, _) => {
658                let mut node = KdlNode::new("DumpScreen");
659                node.push(file.clone());
660                Some(node)
661            },
662            Action::DumpLayout => Some(KdlNode::new("DumpLayout")),
663            Action::EditScrollback => Some(KdlNode::new("EditScrollback")),
664            Action::ScrollUp => Some(KdlNode::new("ScrollUp")),
665            Action::ScrollDown => Some(KdlNode::new("ScrollDown")),
666            Action::ScrollToBottom => Some(KdlNode::new("ScrollToBottom")),
667            Action::ScrollToTop => Some(KdlNode::new("ScrollToTop")),
668            Action::PageScrollUp => Some(KdlNode::new("PageScrollUp")),
669            Action::PageScrollDown => Some(KdlNode::new("PageScrollDown")),
670            Action::HalfPageScrollUp => Some(KdlNode::new("HalfPageScrollUp")),
671            Action::HalfPageScrollDown => Some(KdlNode::new("HalfPageScrollDown")),
672            Action::ToggleFocusFullscreen => Some(KdlNode::new("ToggleFocusFullscreen")),
673            Action::TogglePaneFrames => Some(KdlNode::new("TogglePaneFrames")),
674            Action::ToggleActiveSyncTab => Some(KdlNode::new("ToggleActiveSyncTab")),
675            Action::NewPane(direction, _, _) => {
676                let mut node = KdlNode::new("NewPane");
677                if let Some(direction) = direction {
678                    let direction = match direction {
679                        Direction::Left => "left",
680                        Direction::Right => "right",
681                        Direction::Up => "up",
682                        Direction::Down => "down",
683                    };
684                    node.push(direction);
685                }
686                Some(node)
687            },
688            Action::TogglePaneEmbedOrFloating => Some(KdlNode::new("TogglePaneEmbedOrFloating")),
689            Action::ToggleFloatingPanes => Some(KdlNode::new("ToggleFloatingPanes")),
690            Action::CloseFocus => Some(KdlNode::new("CloseFocus")),
691            Action::PaneNameInput(bytes) => {
692                let mut node = KdlNode::new("PaneNameInput");
693                for byte in bytes {
694                    node.push(KdlValue::Base10(*byte as i64));
695                }
696                Some(node)
697            },
698            Action::UndoRenamePane => Some(KdlNode::new("UndoRenamePane")),
699            Action::NewTab(_, _, _, _, name, should_change_focus_to_new_tab, cwd) => {
700                let mut node = KdlNode::new("NewTab");
701                let mut children = KdlDocument::new();
702                if let Some(name) = name {
703                    let mut name_node = KdlNode::new("name");
704                    if !should_change_focus_to_new_tab {
705                        let mut should_change_focus_to_new_tab_node =
706                            KdlNode::new("should_change_focus_to_new_tab");
707                        should_change_focus_to_new_tab_node.push(KdlValue::Bool(false));
708                        children
709                            .nodes_mut()
710                            .push(should_change_focus_to_new_tab_node);
711                    }
712                    name_node.push(name.clone());
713                    children.nodes_mut().push(name_node);
714                }
715                if let Some(cwd) = cwd {
716                    let mut cwd_node = KdlNode::new("cwd");
717                    cwd_node.push(cwd.display().to_string());
718                    children.nodes_mut().push(cwd_node);
719                }
720                if name.is_some() || cwd.is_some() {
721                    node.set_children(children);
722                }
723                Some(node)
724            },
725            Action::GoToNextTab => Some(KdlNode::new("GoToNextTab")),
726            Action::GoToPreviousTab => Some(KdlNode::new("GoToPreviousTab")),
727            Action::CloseTab => Some(KdlNode::new("CloseTab")),
728            Action::GoToTab(index) => {
729                let mut node = KdlNode::new("GoToTab");
730                node.push(KdlValue::Base10(*index as i64));
731                Some(node)
732            },
733            Action::ToggleTab => Some(KdlNode::new("ToggleTab")),
734            Action::TabNameInput(bytes) => {
735                let mut node = KdlNode::new("TabNameInput");
736                for byte in bytes {
737                    node.push(KdlValue::Base10(*byte as i64));
738                }
739                Some(node)
740            },
741            Action::UndoRenameTab => Some(KdlNode::new("UndoRenameTab")),
742            Action::MoveTab(direction) => {
743                let mut node = KdlNode::new("MoveTab");
744                let direction = match direction {
745                    Direction::Left => "left",
746                    Direction::Right => "right",
747                    Direction::Up => "up",
748                    Direction::Down => "down",
749                };
750                node.push(direction);
751                Some(node)
752            },
753            Action::NewTiledPane(direction, run_command_action, name) => {
754                let mut node = KdlNode::new("Run");
755                let mut node_children = KdlDocument::new();
756                if let Some(run_command_action) = run_command_action {
757                    node.push(run_command_action.command.display().to_string());
758                    for arg in &run_command_action.args {
759                        node.push(arg.clone());
760                    }
761                    if let Some(cwd) = &run_command_action.cwd {
762                        let mut cwd_node = KdlNode::new("cwd");
763                        cwd_node.push(cwd.display().to_string());
764                        node_children.nodes_mut().push(cwd_node);
765                    }
766                    if run_command_action.hold_on_start {
767                        let mut hos_node = KdlNode::new("hold_on_start");
768                        hos_node.push(KdlValue::Bool(true));
769                        node_children.nodes_mut().push(hos_node);
770                    }
771                    if !run_command_action.hold_on_close {
772                        let mut hoc_node = KdlNode::new("hold_on_close");
773                        hoc_node.push(KdlValue::Bool(false));
774                        node_children.nodes_mut().push(hoc_node);
775                    }
776                }
777                if let Some(name) = name {
778                    let mut name_node = KdlNode::new("name");
779                    name_node.push(name.clone());
780                    node_children.nodes_mut().push(name_node);
781                }
782                if let Some(direction) = direction {
783                    let mut direction_node = KdlNode::new("direction");
784                    let direction = match direction {
785                        Direction::Left => "left",
786                        Direction::Right => "right",
787                        Direction::Up => "up",
788                        Direction::Down => "down",
789                    };
790                    direction_node.push(direction);
791                    node_children.nodes_mut().push(direction_node);
792                }
793                if !node_children.nodes().is_empty() {
794                    node.set_children(node_children);
795                }
796                Some(node)
797            },
798            Action::NewFloatingPane(run_command_action, name, floating_pane_coordinates) => {
799                let mut node = KdlNode::new("Run");
800                let mut node_children = KdlDocument::new();
801                let mut floating_pane = KdlNode::new("floating");
802                floating_pane.push(KdlValue::Bool(true));
803                node_children.nodes_mut().push(floating_pane);
804                if let Some(run_command_action) = run_command_action {
805                    node.push(run_command_action.command.display().to_string());
806                    for arg in &run_command_action.args {
807                        node.push(arg.clone());
808                    }
809                    if let Some(cwd) = &run_command_action.cwd {
810                        let mut cwd_node = KdlNode::new("cwd");
811                        cwd_node.push(cwd.display().to_string());
812                        node_children.nodes_mut().push(cwd_node);
813                    }
814                    if run_command_action.hold_on_start {
815                        let mut hos_node = KdlNode::new("hold_on_start");
816                        hos_node.push(KdlValue::Bool(true));
817                        node_children.nodes_mut().push(hos_node);
818                    }
819                    if !run_command_action.hold_on_close {
820                        let mut hoc_node = KdlNode::new("hold_on_close");
821                        hoc_node.push(KdlValue::Bool(false));
822                        node_children.nodes_mut().push(hoc_node);
823                    }
824                }
825                if let Some(floating_pane_coordinates) = floating_pane_coordinates {
826                    if let Some(x) = floating_pane_coordinates.x {
827                        let mut x_node = KdlNode::new("x");
828                        match x {
829                            SplitSize::Percent(x) => {
830                                x_node.push(format!("{}%", x));
831                            },
832                            SplitSize::Fixed(x) => {
833                                x_node.push(KdlValue::Base10(x as i64));
834                            },
835                        };
836                        node_children.nodes_mut().push(x_node);
837                    }
838                    if let Some(y) = floating_pane_coordinates.y {
839                        let mut y_node = KdlNode::new("y");
840                        match y {
841                            SplitSize::Percent(y) => {
842                                y_node.push(format!("{}%", y));
843                            },
844                            SplitSize::Fixed(y) => {
845                                y_node.push(KdlValue::Base10(y as i64));
846                            },
847                        };
848                        node_children.nodes_mut().push(y_node);
849                    }
850                    if let Some(width) = floating_pane_coordinates.width {
851                        let mut width_node = KdlNode::new("width");
852                        match width {
853                            SplitSize::Percent(width) => {
854                                width_node.push(format!("{}%", width));
855                            },
856                            SplitSize::Fixed(width) => {
857                                width_node.push(KdlValue::Base10(width as i64));
858                            },
859                        };
860                        node_children.nodes_mut().push(width_node);
861                    }
862                    if let Some(height) = floating_pane_coordinates.height {
863                        let mut height_node = KdlNode::new("height");
864                        match height {
865                            SplitSize::Percent(height) => {
866                                height_node.push(format!("{}%", height));
867                            },
868                            SplitSize::Fixed(height) => {
869                                height_node.push(KdlValue::Base10(height as i64));
870                            },
871                        };
872                        node_children.nodes_mut().push(height_node);
873                    }
874                }
875                if let Some(name) = name {
876                    let mut name_node = KdlNode::new("name");
877                    name_node.push(name.clone());
878                    node_children.nodes_mut().push(name_node);
879                }
880                if !node_children.nodes().is_empty() {
881                    node.set_children(node_children);
882                }
883                Some(node)
884            },
885            Action::NewInPlacePane(run_command_action, name) => {
886                let mut node = KdlNode::new("Run");
887                let mut node_children = KdlDocument::new();
888                if let Some(run_command_action) = run_command_action {
889                    node.push(run_command_action.command.display().to_string());
890                    for arg in &run_command_action.args {
891                        node.push(arg.clone());
892                    }
893                    let mut in_place_node = KdlNode::new("in_place");
894                    in_place_node.push(KdlValue::Bool(true));
895                    node_children.nodes_mut().push(in_place_node);
896                    if let Some(cwd) = &run_command_action.cwd {
897                        let mut cwd_node = KdlNode::new("cwd");
898                        cwd_node.push(cwd.display().to_string());
899                        node_children.nodes_mut().push(cwd_node);
900                    }
901                    if run_command_action.hold_on_start {
902                        let mut hos_node = KdlNode::new("hold_on_start");
903                        hos_node.push(KdlValue::Bool(true));
904                        node_children.nodes_mut().push(hos_node);
905                    }
906                    if !run_command_action.hold_on_close {
907                        let mut hoc_node = KdlNode::new("hold_on_close");
908                        hoc_node.push(KdlValue::Bool(false));
909                        node_children.nodes_mut().push(hoc_node);
910                    }
911                }
912                if let Some(name) = name {
913                    let mut name_node = KdlNode::new("name");
914                    name_node.push(name.clone());
915                    node_children.nodes_mut().push(name_node);
916                }
917                if !node_children.nodes().is_empty() {
918                    node.set_children(node_children);
919                }
920                Some(node)
921            },
922            Action::NewStackedPane(run_command_action, name) => match run_command_action {
923                Some(run_command_action) => {
924                    let mut node = KdlNode::new("Run");
925                    let mut node_children = KdlDocument::new();
926                    node.push(run_command_action.command.display().to_string());
927                    for arg in &run_command_action.args {
928                        node.push(arg.clone());
929                    }
930                    let mut stacked_node = KdlNode::new("stacked");
931                    stacked_node.push(KdlValue::Bool(true));
932                    node_children.nodes_mut().push(stacked_node);
933                    if let Some(cwd) = &run_command_action.cwd {
934                        let mut cwd_node = KdlNode::new("cwd");
935                        cwd_node.push(cwd.display().to_string());
936                        node_children.nodes_mut().push(cwd_node);
937                    }
938                    if run_command_action.hold_on_start {
939                        let mut hos_node = KdlNode::new("hold_on_start");
940                        hos_node.push(KdlValue::Bool(true));
941                        node_children.nodes_mut().push(hos_node);
942                    }
943                    if !run_command_action.hold_on_close {
944                        let mut hoc_node = KdlNode::new("hold_on_close");
945                        hoc_node.push(KdlValue::Bool(false));
946                        node_children.nodes_mut().push(hoc_node);
947                    }
948                    if let Some(name) = name {
949                        let mut name_node = KdlNode::new("name");
950                        name_node.push(name.clone());
951                        node_children.nodes_mut().push(name_node);
952                    }
953                    if !node_children.nodes().is_empty() {
954                        node.set_children(node_children);
955                    }
956                    Some(node)
957                },
958                None => {
959                    let mut node = KdlNode::new("NewPane");
960                    node.push("stacked");
961                    Some(node)
962                },
963            },
964            Action::Detach => Some(KdlNode::new("Detach")),
965            Action::LaunchOrFocusPlugin(
966                run_plugin_or_alias,
967                should_float,
968                move_to_focused_tab,
969                should_open_in_place,
970                skip_plugin_cache,
971            ) => {
972                let mut node = KdlNode::new("LaunchOrFocusPlugin");
973                let mut node_children = KdlDocument::new();
974                let location = run_plugin_or_alias.location_string();
975                node.push(location);
976                if *should_float {
977                    let mut should_float_node = KdlNode::new("floating");
978                    should_float_node.push(KdlValue::Bool(true));
979                    node_children.nodes_mut().push(should_float_node);
980                }
981                if *move_to_focused_tab {
982                    let mut move_to_focused_tab_node = KdlNode::new("move_to_focused_tab");
983                    move_to_focused_tab_node.push(KdlValue::Bool(true));
984                    node_children.nodes_mut().push(move_to_focused_tab_node);
985                }
986                if *should_open_in_place {
987                    let mut should_open_in_place_node = KdlNode::new("in_place");
988                    should_open_in_place_node.push(KdlValue::Bool(true));
989                    node_children.nodes_mut().push(should_open_in_place_node);
990                }
991                if *skip_plugin_cache {
992                    let mut skip_plugin_cache_node = KdlNode::new("skip_plugin_cache");
993                    skip_plugin_cache_node.push(KdlValue::Bool(true));
994                    node_children.nodes_mut().push(skip_plugin_cache_node);
995                }
996                if let Some(configuration) = run_plugin_or_alias.get_configuration() {
997                    for (config_key, config_value) in configuration.inner().iter() {
998                        let mut node = KdlNode::new(config_key.clone());
999                        node.push(config_value.clone());
1000                        node_children.nodes_mut().push(node);
1001                    }
1002                }
1003                if !node_children.nodes().is_empty() {
1004                    node.set_children(node_children);
1005                }
1006                Some(node)
1007            },
1008            Action::LaunchPlugin(
1009                run_plugin_or_alias,
1010                should_float,
1011                should_open_in_place,
1012                skip_plugin_cache,
1013                cwd,
1014            ) => {
1015                let mut node = KdlNode::new("LaunchPlugin");
1016                let mut node_children = KdlDocument::new();
1017                let location = run_plugin_or_alias.location_string();
1018                node.push(location);
1019                if *should_float {
1020                    let mut should_float_node = KdlNode::new("floating");
1021                    should_float_node.push(KdlValue::Bool(true));
1022                    node_children.nodes_mut().push(should_float_node);
1023                }
1024                if *should_open_in_place {
1025                    let mut should_open_in_place_node = KdlNode::new("in_place");
1026                    should_open_in_place_node.push(KdlValue::Bool(true));
1027                    node_children.nodes_mut().push(should_open_in_place_node);
1028                }
1029                if *skip_plugin_cache {
1030                    let mut skip_plugin_cache_node = KdlNode::new("skip_plugin_cache");
1031                    skip_plugin_cache_node.push(KdlValue::Bool(true));
1032                    node_children.nodes_mut().push(skip_plugin_cache_node);
1033                }
1034                if let Some(cwd) = &cwd {
1035                    let mut cwd_node = KdlNode::new("cwd");
1036                    cwd_node.push(cwd.display().to_string());
1037                    node_children.nodes_mut().push(cwd_node);
1038                } else if let Some(cwd) = run_plugin_or_alias.get_initial_cwd() {
1039                    let mut cwd_node = KdlNode::new("cwd");
1040                    cwd_node.push(cwd.display().to_string());
1041                    node_children.nodes_mut().push(cwd_node);
1042                }
1043                if let Some(configuration) = run_plugin_or_alias.get_configuration() {
1044                    for (config_key, config_value) in configuration.inner().iter() {
1045                        let mut node = KdlNode::new(config_key.clone());
1046                        node.push(config_value.clone());
1047                        node_children.nodes_mut().push(node);
1048                    }
1049                }
1050                if !node_children.nodes().is_empty() {
1051                    node.set_children(node_children);
1052                }
1053                Some(node)
1054            },
1055            Action::Copy => Some(KdlNode::new("Copy")),
1056            Action::SearchInput(bytes) => {
1057                let mut node = KdlNode::new("SearchInput");
1058                for byte in bytes {
1059                    node.push(KdlValue::Base10(*byte as i64));
1060                }
1061                Some(node)
1062            },
1063            Action::Search(search_direction) => {
1064                let mut node = KdlNode::new("Search");
1065                let direction = match search_direction {
1066                    SearchDirection::Down => "down",
1067                    SearchDirection::Up => "up",
1068                };
1069                node.push(direction);
1070                Some(node)
1071            },
1072            Action::SearchToggleOption(search_toggle_option) => {
1073                let mut node = KdlNode::new("SearchToggleOption");
1074                node.push(format!("{:?}", search_toggle_option));
1075                Some(node)
1076            },
1077            Action::ToggleMouseMode => Some(KdlNode::new("ToggleMouseMode")),
1078            Action::PreviousSwapLayout => Some(KdlNode::new("PreviousSwapLayout")),
1079            Action::NextSwapLayout => Some(KdlNode::new("NextSwapLayout")),
1080            Action::BreakPane => Some(KdlNode::new("BreakPane")),
1081            Action::BreakPaneRight => Some(KdlNode::new("BreakPaneRight")),
1082            Action::BreakPaneLeft => Some(KdlNode::new("BreakPaneLeft")),
1083            Action::KeybindPipe {
1084                name,
1085                payload,
1086                args: _, // currently unsupported
1087                plugin,
1088                configuration,
1089                launch_new,
1090                skip_cache,
1091                floating,
1092                in_place: _, // currently unsupported
1093                cwd,
1094                pane_title,
1095                plugin_id,
1096            } => {
1097                if plugin_id.is_some() {
1098                    log::warn!("Not serializing temporary keybinding MessagePluginId");
1099                    return None;
1100                }
1101                let mut node = KdlNode::new("MessagePlugin");
1102                let mut node_children = KdlDocument::new();
1103                if let Some(plugin) = plugin {
1104                    node.push(plugin.clone());
1105                }
1106                if let Some(name) = name {
1107                    let mut name_node = KdlNode::new("name");
1108                    name_node.push(name.clone());
1109                    node_children.nodes_mut().push(name_node);
1110                }
1111                if let Some(cwd) = cwd {
1112                    let mut cwd_node = KdlNode::new("cwd");
1113                    cwd_node.push(cwd.display().to_string());
1114                    node_children.nodes_mut().push(cwd_node);
1115                }
1116                if let Some(payload) = payload {
1117                    let mut payload_node = KdlNode::new("payload");
1118                    payload_node.push(payload.clone());
1119                    node_children.nodes_mut().push(payload_node);
1120                }
1121                if *launch_new {
1122                    let mut launch_new_node = KdlNode::new("launch_new");
1123                    launch_new_node.push(KdlValue::Bool(true));
1124                    node_children.nodes_mut().push(launch_new_node);
1125                }
1126                if *skip_cache {
1127                    let mut skip_cache_node = KdlNode::new("skip_cache");
1128                    skip_cache_node.push(KdlValue::Bool(true));
1129                    node_children.nodes_mut().push(skip_cache_node);
1130                }
1131                if let Some(floating) = floating {
1132                    let mut floating_node = KdlNode::new("floating");
1133                    floating_node.push(KdlValue::Bool(*floating));
1134                    node_children.nodes_mut().push(floating_node);
1135                }
1136                if let Some(title) = pane_title {
1137                    let mut title_node = KdlNode::new("title");
1138                    title_node.push(title.clone());
1139                    node_children.nodes_mut().push(title_node);
1140                }
1141                if let Some(configuration) = configuration {
1142                    // we do this because the constructor removes the relevant config fields from
1143                    // above, otherwise we would have duplicates
1144                    let configuration = PluginUserConfiguration::new(configuration.clone());
1145                    let configuration = configuration.inner();
1146                    for (config_key, config_value) in configuration.iter() {
1147                        let mut node = KdlNode::new(config_key.clone());
1148                        node.push(config_value.clone());
1149                        node_children.nodes_mut().push(node);
1150                    }
1151                }
1152                if !node_children.nodes().is_empty() {
1153                    node.set_children(node_children);
1154                }
1155                Some(node)
1156            },
1157            Action::TogglePanePinned => Some(KdlNode::new("TogglePanePinned")),
1158            Action::TogglePaneInGroup => Some(KdlNode::new("TogglePaneInGroup")),
1159            Action::ToggleGroupMarking => Some(KdlNode::new("ToggleGroupMarking")),
1160            _ => None,
1161        }
1162    }
1163}
1164
1165impl TryFrom<(&str, &KdlDocument)> for PaletteColor {
1166    type Error = ConfigError;
1167
1168    fn try_from(
1169        (color_name, theme_colors): (&str, &KdlDocument),
1170    ) -> Result<PaletteColor, Self::Error> {
1171        let color = theme_colors
1172            .get(color_name)
1173            .ok_or(ConfigError::new_kdl_error(
1174                format!("Missing theme color: {}", color_name),
1175                theme_colors.span().offset(),
1176                theme_colors.span().len(),
1177            ))?;
1178        let entry_count = entry_count!(color);
1179        let is_rgb = || entry_count == 3;
1180        let is_three_digit_hex = || {
1181            match kdl_first_entry_as_string!(color) {
1182                // 4 including the '#' character
1183                Some(s) => entry_count == 1 && s.starts_with('#') && s.len() == 4,
1184                None => false,
1185            }
1186        };
1187        let is_six_digit_hex = || {
1188            match kdl_first_entry_as_string!(color) {
1189                // 7 including the '#' character
1190                Some(s) => entry_count == 1 && s.starts_with('#') && s.len() == 7,
1191                None => false,
1192            }
1193        };
1194        let is_eight_bit = || kdl_first_entry_as_i64!(color).is_some() && entry_count == 1;
1195        if is_rgb() {
1196            let mut channels = kdl_entries_as_i64!(color);
1197            let r = channels.next().unwrap().ok_or(ConfigError::new_kdl_error(
1198                format!("invalid rgb color"),
1199                color.span().offset(),
1200                color.span().len(),
1201            ))? as u8;
1202            let g = channels.next().unwrap().ok_or(ConfigError::new_kdl_error(
1203                format!("invalid rgb color"),
1204                color.span().offset(),
1205                color.span().len(),
1206            ))? as u8;
1207            let b = channels.next().unwrap().ok_or(ConfigError::new_kdl_error(
1208                format!("invalid rgb color"),
1209                color.span().offset(),
1210                color.span().len(),
1211            ))? as u8;
1212            Ok(PaletteColor::Rgb((r, g, b)))
1213        } else if is_three_digit_hex() {
1214            // eg. #fff (hex, will be converted to rgb)
1215            let mut s = String::from(kdl_first_entry_as_string!(color).unwrap());
1216            s.remove(0);
1217            let r = u8::from_str_radix(&s[0..1], 16).map_err(|_| {
1218                ConfigError::new_kdl_error(
1219                    "Failed to parse hex color".into(),
1220                    color.span().offset(),
1221                    color.span().len(),
1222                )
1223            })? * 0x11;
1224            let g = u8::from_str_radix(&s[1..2], 16).map_err(|_| {
1225                ConfigError::new_kdl_error(
1226                    "Failed to parse hex color".into(),
1227                    color.span().offset(),
1228                    color.span().len(),
1229                )
1230            })? * 0x11;
1231            let b = u8::from_str_radix(&s[2..3], 16).map_err(|_| {
1232                ConfigError::new_kdl_error(
1233                    "Failed to parse hex color".into(),
1234                    color.span().offset(),
1235                    color.span().len(),
1236                )
1237            })? * 0x11;
1238            Ok(PaletteColor::Rgb((r, g, b)))
1239        } else if is_six_digit_hex() {
1240            // eg. #ffffff (hex, will be converted to rgb)
1241            let mut s = String::from(kdl_first_entry_as_string!(color).unwrap());
1242            s.remove(0);
1243            let r = u8::from_str_radix(&s[0..2], 16).map_err(|_| {
1244                ConfigError::new_kdl_error(
1245                    "Failed to parse hex color".into(),
1246                    color.span().offset(),
1247                    color.span().len(),
1248                )
1249            })?;
1250            let g = u8::from_str_radix(&s[2..4], 16).map_err(|_| {
1251                ConfigError::new_kdl_error(
1252                    "Failed to parse hex color".into(),
1253                    color.span().offset(),
1254                    color.span().len(),
1255                )
1256            })?;
1257            let b = u8::from_str_radix(&s[4..6], 16).map_err(|_| {
1258                ConfigError::new_kdl_error(
1259                    "Failed to parse hex color".into(),
1260                    color.span().offset(),
1261                    color.span().len(),
1262                )
1263            })?;
1264            Ok(PaletteColor::Rgb((r, g, b)))
1265        } else if is_eight_bit() {
1266            let n = kdl_first_entry_as_i64!(color).ok_or(ConfigError::new_kdl_error(
1267                "Failed to parse color".into(),
1268                color.span().offset(),
1269                color.span().len(),
1270            ))?;
1271            Ok(PaletteColor::EightBit(n as u8))
1272        } else {
1273            Err(ConfigError::new_kdl_error(
1274                "Failed to parse color".into(),
1275                color.span().offset(),
1276                color.span().len(),
1277            ))
1278        }
1279    }
1280}
1281
1282impl PaletteColor {
1283    pub fn to_kdl(&self, color_name: &str) -> KdlNode {
1284        let mut node = KdlNode::new(color_name);
1285        match self {
1286            PaletteColor::Rgb((r, g, b)) => {
1287                node.push(KdlValue::Base10(*r as i64));
1288                node.push(KdlValue::Base10(*g as i64));
1289                node.push(KdlValue::Base10(*b as i64));
1290            },
1291            PaletteColor::EightBit(color_index) => {
1292                node.push(KdlValue::Base10(*color_index as i64));
1293            },
1294        }
1295        node
1296    }
1297}
1298
1299impl StyleDeclaration {
1300    pub fn to_kdl(&self, declaration_name: &str) -> KdlNode {
1301        let mut node = KdlNode::new(declaration_name);
1302        let mut doc = KdlDocument::new();
1303
1304        doc.nodes_mut().push(self.base.to_kdl("base"));
1305        doc.nodes_mut().push(self.background.to_kdl("background"));
1306        doc.nodes_mut().push(self.emphasis_0.to_kdl("emphasis_0"));
1307        doc.nodes_mut().push(self.emphasis_1.to_kdl("emphasis_1"));
1308        doc.nodes_mut().push(self.emphasis_2.to_kdl("emphasis_2"));
1309        doc.nodes_mut().push(self.emphasis_3.to_kdl("emphasis_3"));
1310        node.set_children(doc);
1311        node
1312    }
1313}
1314
1315impl MultiplayerColors {
1316    pub fn to_kdl(&self) -> KdlNode {
1317        let mut node = KdlNode::new("multiplayer_user_colors");
1318        let mut doc = KdlDocument::new();
1319        doc.nodes_mut().push(self.player_1.to_kdl("player_1"));
1320        doc.nodes_mut().push(self.player_2.to_kdl("player_2"));
1321        doc.nodes_mut().push(self.player_3.to_kdl("player_3"));
1322        doc.nodes_mut().push(self.player_4.to_kdl("player_4"));
1323        doc.nodes_mut().push(self.player_5.to_kdl("player_5"));
1324        doc.nodes_mut().push(self.player_6.to_kdl("player_6"));
1325        doc.nodes_mut().push(self.player_7.to_kdl("player_7"));
1326        doc.nodes_mut().push(self.player_8.to_kdl("player_8"));
1327        doc.nodes_mut().push(self.player_9.to_kdl("player_9"));
1328        doc.nodes_mut().push(self.player_10.to_kdl("player_10"));
1329        node.set_children(doc);
1330        node
1331    }
1332}
1333
1334impl TryFrom<(&KdlNode, &Options)> for Action {
1335    type Error = ConfigError;
1336    fn try_from((kdl_action, config_options): (&KdlNode, &Options)) -> Result<Self, Self::Error> {
1337        let action_name = kdl_name!(kdl_action);
1338        let action_arguments: Vec<&KdlEntry> = kdl_argument_values!(kdl_action);
1339        let action_children: Vec<&KdlDocument> = kdl_children!(kdl_action);
1340        match action_name {
1341            "Quit" => parse_kdl_action_arguments!(action_name, action_arguments, kdl_action),
1342            "FocusNextPane" => {
1343                parse_kdl_action_arguments!(action_name, action_arguments, kdl_action)
1344            },
1345            "FocusPreviousPane" => {
1346                parse_kdl_action_arguments!(action_name, action_arguments, kdl_action)
1347            },
1348            "SwitchFocus" => parse_kdl_action_arguments!(action_name, action_arguments, kdl_action),
1349            "EditScrollback" => {
1350                parse_kdl_action_arguments!(action_name, action_arguments, kdl_action)
1351            },
1352            "ScrollUp" => parse_kdl_action_arguments!(action_name, action_arguments, kdl_action),
1353            "ScrollDown" => parse_kdl_action_arguments!(action_name, action_arguments, kdl_action),
1354            "ScrollToBottom" => {
1355                parse_kdl_action_arguments!(action_name, action_arguments, kdl_action)
1356            },
1357            "ScrollToTop" => {
1358                parse_kdl_action_arguments!(action_name, action_arguments, kdl_action)
1359            },
1360            "PageScrollUp" => {
1361                parse_kdl_action_arguments!(action_name, action_arguments, kdl_action)
1362            },
1363            "PageScrollDown" => {
1364                parse_kdl_action_arguments!(action_name, action_arguments, kdl_action)
1365            },
1366            "HalfPageScrollUp" => {
1367                parse_kdl_action_arguments!(action_name, action_arguments, kdl_action)
1368            },
1369            "HalfPageScrollDown" => {
1370                parse_kdl_action_arguments!(action_name, action_arguments, kdl_action)
1371            },
1372            "ToggleFocusFullscreen" => {
1373                parse_kdl_action_arguments!(action_name, action_arguments, kdl_action)
1374            },
1375            "TogglePaneFrames" => {
1376                parse_kdl_action_arguments!(action_name, action_arguments, kdl_action)
1377            },
1378            "ToggleActiveSyncTab" => {
1379                parse_kdl_action_arguments!(action_name, action_arguments, kdl_action)
1380            },
1381            "TogglePaneEmbedOrFloating" => {
1382                parse_kdl_action_arguments!(action_name, action_arguments, kdl_action)
1383            },
1384            "ToggleFloatingPanes" => {
1385                parse_kdl_action_arguments!(action_name, action_arguments, kdl_action)
1386            },
1387            "CloseFocus" => parse_kdl_action_arguments!(action_name, action_arguments, kdl_action),
1388            "UndoRenamePane" => {
1389                parse_kdl_action_arguments!(action_name, action_arguments, kdl_action)
1390            },
1391            "NoOp" => parse_kdl_action_arguments!(action_name, action_arguments, kdl_action),
1392            "GoToNextTab" => parse_kdl_action_arguments!(action_name, action_arguments, kdl_action),
1393            "GoToPreviousTab" => {
1394                parse_kdl_action_arguments!(action_name, action_arguments, kdl_action)
1395            },
1396            "CloseTab" => parse_kdl_action_arguments!(action_name, action_arguments, kdl_action),
1397            "ToggleTab" => parse_kdl_action_arguments!(action_name, action_arguments, kdl_action),
1398            "UndoRenameTab" => {
1399                parse_kdl_action_arguments!(action_name, action_arguments, kdl_action)
1400            },
1401            "ToggleMouseMode" => {
1402                parse_kdl_action_arguments!(action_name, action_arguments, kdl_action)
1403            },
1404            "Detach" => parse_kdl_action_arguments!(action_name, action_arguments, kdl_action),
1405            "Copy" => parse_kdl_action_arguments!(action_name, action_arguments, kdl_action),
1406            "Clear" => parse_kdl_action_arguments!(action_name, action_arguments, kdl_action),
1407            "Confirm" => parse_kdl_action_arguments!(action_name, action_arguments, kdl_action),
1408            "Deny" => parse_kdl_action_arguments!(action_name, action_arguments, kdl_action),
1409            "Write" => parse_kdl_action_u8_arguments!(action_name, action_arguments, kdl_action),
1410            "WriteChars" => parse_kdl_action_char_or_string_arguments!(
1411                action_name,
1412                action_arguments,
1413                kdl_action
1414            ),
1415            "SwitchToMode" => parse_kdl_action_char_or_string_arguments!(
1416                action_name,
1417                action_arguments,
1418                kdl_action
1419            ),
1420            "Search" => parse_kdl_action_char_or_string_arguments!(
1421                action_name,
1422                action_arguments,
1423                kdl_action
1424            ),
1425            "Resize" => parse_kdl_action_char_or_string_arguments!(
1426                action_name,
1427                action_arguments,
1428                kdl_action
1429            ),
1430            "ResizeNew" => parse_kdl_action_char_or_string_arguments!(
1431                action_name,
1432                action_arguments,
1433                kdl_action
1434            ),
1435            "MoveFocus" => parse_kdl_action_char_or_string_arguments!(
1436                action_name,
1437                action_arguments,
1438                kdl_action
1439            ),
1440            "MoveTab" => parse_kdl_action_char_or_string_arguments!(
1441                action_name,
1442                action_arguments,
1443                kdl_action
1444            ),
1445            "MoveFocusOrTab" => parse_kdl_action_char_or_string_arguments!(
1446                action_name,
1447                action_arguments,
1448                kdl_action
1449            ),
1450            "MovePane" => parse_kdl_action_char_or_string_arguments!(
1451                action_name,
1452                action_arguments,
1453                kdl_action
1454            ),
1455            "MovePaneBackwards" => parse_kdl_action_char_or_string_arguments!(
1456                action_name,
1457                action_arguments,
1458                kdl_action
1459            ),
1460            "DumpScreen" => parse_kdl_action_char_or_string_arguments!(
1461                action_name,
1462                action_arguments,
1463                kdl_action
1464            ),
1465            "DumpLayout" => parse_kdl_action_char_or_string_arguments!(
1466                action_name,
1467                action_arguments,
1468                kdl_action
1469            ),
1470            "NewPane" => parse_kdl_action_char_or_string_arguments!(
1471                action_name,
1472                action_arguments,
1473                kdl_action
1474            ),
1475            "PaneNameInput" => {
1476                parse_kdl_action_u8_arguments!(action_name, action_arguments, kdl_action)
1477            },
1478            "NewTab" => {
1479                let command_metadata = action_children.iter().next();
1480                if command_metadata.is_none() {
1481                    return Ok(Action::NewTab(None, vec![], None, None, None, true, None));
1482                }
1483
1484                let current_dir = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
1485
1486                let layout = command_metadata
1487                    .and_then(|c_m| kdl_child_string_value_for_entry(c_m, "layout"))
1488                    .map(|layout_string| PathBuf::from(layout_string))
1489                    .or_else(|| config_options.default_layout.clone());
1490                let cwd = command_metadata
1491                    .and_then(|c_m| kdl_child_string_value_for_entry(c_m, "cwd"))
1492                    .map(|cwd_string| PathBuf::from(cwd_string))
1493                    .map(|cwd| current_dir.join(cwd));
1494                let name = command_metadata
1495                    .and_then(|c_m| kdl_child_string_value_for_entry(c_m, "name"))
1496                    .map(|name_string| name_string.to_string());
1497
1498                let layout_dir = config_options
1499                    .layout_dir
1500                    .clone()
1501                    .or_else(|| get_layout_dir(find_default_config_dir()));
1502                let (path_to_raw_layout, raw_layout, swap_layouts) =
1503                    Layout::stringified_from_path_or_default(layout.as_ref(), layout_dir).map_err(
1504                        |e| {
1505                            ConfigError::new_kdl_error(
1506                                format!("Failed to load layout: {}", e),
1507                                kdl_action.span().offset(),
1508                                kdl_action.span().len(),
1509                            )
1510                        },
1511                    )?;
1512
1513                let layout = Layout::from_str(
1514                    &raw_layout,
1515                    path_to_raw_layout,
1516                    swap_layouts.as_ref().map(|(f, p)| (f.as_str(), p.as_str())),
1517                    cwd.clone(),
1518                )
1519                .map_err(|e| {
1520                    ConfigError::new_kdl_error(
1521                        format!("Failed to load layout: {}", e),
1522                        kdl_action.span().offset(),
1523                        kdl_action.span().len(),
1524                    )
1525                })?;
1526
1527                let swap_tiled_layouts = Some(layout.swap_tiled_layouts.clone());
1528                let swap_floating_layouts = Some(layout.swap_floating_layouts.clone());
1529
1530                let mut tabs = layout.tabs();
1531                if tabs.len() > 1 {
1532                    return Err(ConfigError::new_kdl_error(
1533                        "Tab layout cannot itself have tabs".to_string(),
1534                        kdl_action.span().offset(),
1535                        kdl_action.span().len(),
1536                    ));
1537                } else if !tabs.is_empty() {
1538                    let (tab_name, layout, floating_panes_layout) = tabs.drain(..).next().unwrap();
1539                    let name = tab_name.or(name);
1540                    let should_change_focus_to_new_tab = layout.focus.unwrap_or(true);
1541
1542                    Ok(Action::NewTab(
1543                        Some(layout),
1544                        floating_panes_layout,
1545                        swap_tiled_layouts,
1546                        swap_floating_layouts,
1547                        name,
1548                        should_change_focus_to_new_tab,
1549                        cwd,
1550                    ))
1551                } else {
1552                    let (layout, floating_panes_layout) = layout.new_tab();
1553                    let should_change_focus_to_new_tab = layout.focus.unwrap_or(true);
1554
1555                    Ok(Action::NewTab(
1556                        Some(layout),
1557                        floating_panes_layout,
1558                        swap_tiled_layouts,
1559                        swap_floating_layouts,
1560                        name,
1561                        should_change_focus_to_new_tab,
1562                        cwd,
1563                    ))
1564                }
1565            },
1566            "GoToTab" => parse_kdl_action_u8_arguments!(action_name, action_arguments, kdl_action),
1567            "TabNameInput" => {
1568                parse_kdl_action_u8_arguments!(action_name, action_arguments, kdl_action)
1569            },
1570            "SearchInput" => {
1571                parse_kdl_action_u8_arguments!(action_name, action_arguments, kdl_action)
1572            },
1573            "SearchToggleOption" => parse_kdl_action_char_or_string_arguments!(
1574                action_name,
1575                action_arguments,
1576                kdl_action
1577            ),
1578            "Run" => {
1579                let arguments = action_arguments.iter().copied();
1580                let mut args = kdl_arguments_that_are_strings(arguments)?;
1581                if args.is_empty() {
1582                    return Err(ConfigError::new_kdl_error(
1583                        "No command found in Run action".into(),
1584                        kdl_action.span().offset(),
1585                        kdl_action.span().len(),
1586                    ));
1587                }
1588                let command = args.remove(0);
1589                let command_metadata = action_children.iter().next();
1590                let cwd = command_metadata
1591                    .and_then(|c_m| kdl_child_string_value_for_entry(c_m, "cwd"))
1592                    .map(|cwd_string| PathBuf::from(cwd_string));
1593                let name = command_metadata
1594                    .and_then(|c_m| kdl_child_string_value_for_entry(c_m, "name"))
1595                    .map(|name_string| name_string.to_string());
1596                let direction = command_metadata
1597                    .and_then(|c_m| kdl_child_string_value_for_entry(c_m, "direction"))
1598                    .and_then(|direction_string| Direction::from_str(direction_string).ok());
1599                let hold_on_close = command_metadata
1600                    .and_then(|c_m| kdl_child_bool_value_for_entry(c_m, "close_on_exit"))
1601                    .and_then(|close_on_exit| Some(!close_on_exit))
1602                    .unwrap_or(true);
1603                let hold_on_start = command_metadata
1604                    .and_then(|c_m| kdl_child_bool_value_for_entry(c_m, "start_suspended"))
1605                    .unwrap_or(false);
1606                let floating = command_metadata
1607                    .and_then(|c_m| kdl_child_bool_value_for_entry(c_m, "floating"))
1608                    .unwrap_or(false);
1609                let in_place = command_metadata
1610                    .and_then(|c_m| kdl_child_bool_value_for_entry(c_m, "in_place"))
1611                    .unwrap_or(false);
1612                let stacked = command_metadata
1613                    .and_then(|c_m| kdl_child_bool_value_for_entry(c_m, "stacked"))
1614                    .unwrap_or(false);
1615                let run_command_action = RunCommandAction {
1616                    command: PathBuf::from(command),
1617                    args,
1618                    cwd,
1619                    direction,
1620                    hold_on_close,
1621                    hold_on_start,
1622                    ..Default::default()
1623                };
1624                let x = command_metadata
1625                    .and_then(|c_m| kdl_child_string_value_for_entry(c_m, "x"))
1626                    .map(|s| s.to_owned());
1627                let y = command_metadata
1628                    .and_then(|c_m| kdl_child_string_value_for_entry(c_m, "y"))
1629                    .map(|s| s.to_owned());
1630                let width = command_metadata
1631                    .and_then(|c_m| kdl_child_string_value_for_entry(c_m, "width"))
1632                    .map(|s| s.to_owned());
1633                let height = command_metadata
1634                    .and_then(|c_m| kdl_child_string_value_for_entry(c_m, "height"))
1635                    .map(|s| s.to_owned());
1636                let pinned =
1637                    command_metadata.and_then(|c_m| kdl_child_bool_value_for_entry(c_m, "pinned"));
1638                if floating {
1639                    Ok(Action::NewFloatingPane(
1640                        Some(run_command_action),
1641                        name,
1642                        FloatingPaneCoordinates::new(x, y, width, height, pinned),
1643                    ))
1644                } else if in_place {
1645                    Ok(Action::NewInPlacePane(Some(run_command_action), name))
1646                } else if stacked {
1647                    Ok(Action::NewStackedPane(Some(run_command_action), name))
1648                } else {
1649                    Ok(Action::NewTiledPane(
1650                        direction,
1651                        Some(run_command_action),
1652                        name,
1653                    ))
1654                }
1655            },
1656            "LaunchOrFocusPlugin" => {
1657                let arguments = action_arguments.iter().copied();
1658                let mut args = kdl_arguments_that_are_strings(arguments)?;
1659                if args.is_empty() {
1660                    return Err(ConfigError::new_kdl_error(
1661                        "No plugin found to launch in LaunchOrFocusPlugin".into(),
1662                        kdl_action.span().offset(),
1663                        kdl_action.span().len(),
1664                    ));
1665                }
1666                let plugin_path = args.remove(0);
1667
1668                let command_metadata = action_children.iter().next();
1669                let should_float = command_metadata
1670                    .and_then(|c_m| kdl_child_bool_value_for_entry(c_m, "floating"))
1671                    .unwrap_or(false);
1672                let move_to_focused_tab = command_metadata
1673                    .and_then(|c_m| kdl_child_bool_value_for_entry(c_m, "move_to_focused_tab"))
1674                    .unwrap_or(false);
1675                let should_open_in_place = command_metadata
1676                    .and_then(|c_m| kdl_child_bool_value_for_entry(c_m, "in_place"))
1677                    .unwrap_or(false);
1678                let skip_plugin_cache = command_metadata
1679                    .and_then(|c_m| kdl_child_bool_value_for_entry(c_m, "skip_plugin_cache"))
1680                    .unwrap_or(false);
1681                let current_dir = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
1682                let configuration = KdlLayoutParser::parse_plugin_user_configuration(&kdl_action)?;
1683                let initial_cwd = kdl_get_string_property_or_child_value!(kdl_action, "cwd")
1684                    .map(|s| PathBuf::from(s));
1685                let run_plugin_or_alias = RunPluginOrAlias::from_url(
1686                    &plugin_path,
1687                    &Some(configuration.inner().clone()),
1688                    None,
1689                    Some(current_dir),
1690                )
1691                .map_err(|e| {
1692                    ConfigError::new_kdl_error(
1693                        format!("Failed to parse plugin: {}", e),
1694                        kdl_action.span().offset(),
1695                        kdl_action.span().len(),
1696                    )
1697                })?
1698                .with_initial_cwd(initial_cwd);
1699                Ok(Action::LaunchOrFocusPlugin(
1700                    run_plugin_or_alias,
1701                    should_float,
1702                    move_to_focused_tab,
1703                    should_open_in_place,
1704                    skip_plugin_cache,
1705                ))
1706            },
1707            "LaunchPlugin" => {
1708                let arguments = action_arguments.iter().copied();
1709                let mut args = kdl_arguments_that_are_strings(arguments)?;
1710                if args.is_empty() {
1711                    return Err(ConfigError::new_kdl_error(
1712                        "No plugin found to launch in LaunchPlugin".into(),
1713                        kdl_action.span().offset(),
1714                        kdl_action.span().len(),
1715                    ));
1716                }
1717                let plugin_path = args.remove(0);
1718
1719                let command_metadata = action_children.iter().next();
1720                let should_float = command_metadata
1721                    .and_then(|c_m| kdl_child_bool_value_for_entry(c_m, "floating"))
1722                    .unwrap_or(false);
1723                let should_open_in_place = command_metadata
1724                    .and_then(|c_m| kdl_child_bool_value_for_entry(c_m, "in_place"))
1725                    .unwrap_or(false);
1726                let skip_plugin_cache = command_metadata
1727                    .and_then(|c_m| kdl_child_bool_value_for_entry(c_m, "skip_plugin_cache"))
1728                    .unwrap_or(false);
1729                let current_dir = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
1730                let configuration = KdlLayoutParser::parse_plugin_user_configuration(&kdl_action)?;
1731                let run_plugin_or_alias = RunPluginOrAlias::from_url(
1732                    &plugin_path,
1733                    &Some(configuration.inner().clone()),
1734                    None,
1735                    Some(current_dir),
1736                )
1737                .map_err(|e| {
1738                    ConfigError::new_kdl_error(
1739                        format!("Failed to parse plugin: {}", e),
1740                        kdl_action.span().offset(),
1741                        kdl_action.span().len(),
1742                    )
1743                })?;
1744                Ok(Action::LaunchPlugin(
1745                    run_plugin_or_alias,
1746                    should_float,
1747                    should_open_in_place,
1748                    skip_plugin_cache,
1749                    None, // we explicitly do not send the current dir here so that it will be
1750                          // filled from the active pane == better UX
1751                ))
1752            },
1753            "PreviousSwapLayout" => Ok(Action::PreviousSwapLayout),
1754            "NextSwapLayout" => Ok(Action::NextSwapLayout),
1755            "BreakPane" => Ok(Action::BreakPane),
1756            "BreakPaneRight" => Ok(Action::BreakPaneRight),
1757            "BreakPaneLeft" => Ok(Action::BreakPaneLeft),
1758            "RenameSession" => parse_kdl_action_char_or_string_arguments!(
1759                action_name,
1760                action_arguments,
1761                kdl_action
1762            ),
1763            "MessagePlugin" => {
1764                let arguments = action_arguments.iter().copied();
1765                let mut args = kdl_arguments_that_are_strings(arguments)?;
1766                let plugin_path = if args.is_empty() {
1767                    None
1768                } else {
1769                    Some(args.remove(0))
1770                };
1771
1772                let command_metadata = action_children.iter().next();
1773                let launch_new = command_metadata
1774                    .and_then(|c_m| kdl_child_bool_value_for_entry(c_m, "launch_new"))
1775                    .unwrap_or(false);
1776                let skip_cache = command_metadata
1777                    .and_then(|c_m| kdl_child_bool_value_for_entry(c_m, "skip_cache"))
1778                    .unwrap_or(false);
1779                let should_float = command_metadata
1780                    .and_then(|c_m| kdl_child_bool_value_for_entry(c_m, "floating"))
1781                    .unwrap_or(false);
1782                let name = command_metadata
1783                    .and_then(|c_m| kdl_child_string_value_for_entry(c_m, "name"))
1784                    .map(|n| n.to_owned());
1785                let payload = command_metadata
1786                    .and_then(|c_m| kdl_child_string_value_for_entry(c_m, "payload"))
1787                    .map(|p| p.to_owned());
1788                let title = command_metadata
1789                    .and_then(|c_m| kdl_child_string_value_for_entry(c_m, "title"))
1790                    .map(|t| t.to_owned());
1791                let configuration = KdlLayoutParser::parse_plugin_user_configuration(&kdl_action)?;
1792                let configuration = if configuration.inner().is_empty() {
1793                    None
1794                } else {
1795                    Some(configuration.inner().clone())
1796                };
1797                let cwd = kdl_get_string_property_or_child_value!(kdl_action, "cwd")
1798                    .map(|s| PathBuf::from(s));
1799
1800                let name = name
1801                    // first we try to take the explicitly supplied message name
1802                    // then we use the plugin, to facilitate using aliases
1803                    .or_else(|| plugin_path.clone())
1804                    // then we use a uuid to at least have some sort of identifier for this message
1805                    .or_else(|| Some(Uuid::new_v4().to_string()));
1806
1807                Ok(Action::KeybindPipe {
1808                    name,
1809                    payload,
1810                    args: None, // TODO: consider supporting this if there's a need
1811                    plugin: plugin_path,
1812                    configuration,
1813                    launch_new,
1814                    skip_cache,
1815                    floating: Some(should_float),
1816                    in_place: None, // TODO: support this
1817                    cwd,
1818                    pane_title: title,
1819                    plugin_id: None,
1820                })
1821            },
1822            "MessagePluginId" => {
1823                let arguments = action_arguments.iter().copied();
1824                let mut args = kdl_arguments_that_are_digits(arguments)?;
1825                let plugin_id = if args.is_empty() {
1826                    None
1827                } else {
1828                    Some(args.remove(0) as u32)
1829                };
1830
1831                let command_metadata = action_children.iter().next();
1832                let launch_new = false;
1833                let skip_cache = false;
1834                let name = command_metadata
1835                    .and_then(|c_m| kdl_child_string_value_for_entry(c_m, "name"))
1836                    .map(|n| n.to_owned());
1837                let payload = command_metadata
1838                    .and_then(|c_m| kdl_child_string_value_for_entry(c_m, "payload"))
1839                    .map(|p| p.to_owned());
1840                let configuration = None;
1841
1842                let name = name
1843                    // if no name is provided, we use a uuid to at least have some sort of identifier for this message
1844                    .or_else(|| Some(Uuid::new_v4().to_string()));
1845
1846                Ok(Action::KeybindPipe {
1847                    name,
1848                    payload,
1849                    args: None, // TODO: consider supporting this if there's a need
1850                    plugin: None,
1851                    configuration,
1852                    launch_new,
1853                    skip_cache,
1854                    floating: None,
1855                    in_place: None, // TODO: support this
1856                    cwd: None,
1857                    pane_title: None,
1858                    plugin_id,
1859                })
1860            },
1861            "TogglePanePinned" => Ok(Action::TogglePanePinned),
1862            "TogglePaneInGroup" => Ok(Action::TogglePaneInGroup),
1863            "ToggleGroupMarking" => Ok(Action::ToggleGroupMarking),
1864            _ => Err(ConfigError::new_kdl_error(
1865                format!("Unsupported action: {}", action_name).into(),
1866                kdl_action.span().offset(),
1867                kdl_action.span().len(),
1868            )),
1869        }
1870    }
1871}
1872
1873#[macro_export]
1874macro_rules! kdl_property_first_arg_as_string {
1875    ( $kdl_node:expr, $property_name:expr ) => {
1876        $kdl_node
1877            .get($property_name)
1878            .and_then(|p| p.entries().iter().next())
1879            .and_then(|p| p.value().as_string())
1880    };
1881}
1882
1883#[macro_export]
1884macro_rules! kdl_property_first_arg_as_string_or_error {
1885    ( $kdl_node:expr, $property_name:expr ) => {{
1886        match $kdl_node.get($property_name) {
1887            Some(property) => match property.entries().iter().next() {
1888                Some(first_entry) => match first_entry.value().as_string() {
1889                    Some(string_entry) => Some((string_entry, first_entry)),
1890                    None => {
1891                        return Err(ConfigError::new_kdl_error(
1892                            format!(
1893                                "Property {} must be a string, found: {}",
1894                                $property_name,
1895                                first_entry.value()
1896                            ),
1897                            property.span().offset(),
1898                            property.span().len(),
1899                        ));
1900                    },
1901                },
1902                None => {
1903                    return Err(ConfigError::new_kdl_error(
1904                        format!("Property {} must have a value", $property_name),
1905                        property.span().offset(),
1906                        property.span().len(),
1907                    ));
1908                },
1909            },
1910            None => None,
1911        }
1912    }};
1913}
1914
1915#[macro_export]
1916macro_rules! kdl_property_first_arg_as_bool_or_error {
1917    ( $kdl_node:expr, $property_name:expr ) => {{
1918        match $kdl_node.get($property_name) {
1919            Some(property) => match property.entries().iter().next() {
1920                Some(first_entry) => match first_entry.value().as_bool() {
1921                    Some(bool_entry) => Some((bool_entry, first_entry)),
1922                    None => {
1923                        return Err(ConfigError::new_kdl_error(
1924                            format!(
1925                                "Property {} must be true or false, found {}",
1926                                $property_name,
1927                                first_entry.value()
1928                            ),
1929                            property.span().offset(),
1930                            property.span().len(),
1931                        ));
1932                    },
1933                },
1934                None => {
1935                    return Err(ConfigError::new_kdl_error(
1936                        format!("Property {} must have a value", $property_name),
1937                        property.span().offset(),
1938                        property.span().len(),
1939                    ));
1940                },
1941            },
1942            None => None,
1943        }
1944    }};
1945}
1946
1947#[macro_export]
1948macro_rules! kdl_property_first_arg_as_i64_or_error {
1949    ( $kdl_node:expr, $property_name:expr ) => {{
1950        match $kdl_node.get($property_name) {
1951            Some(property) => match property.entries().iter().next() {
1952                Some(first_entry) => match first_entry.value().as_i64() {
1953                    Some(int_entry) => Some((int_entry, first_entry)),
1954                    None => {
1955                        return Err(ConfigError::new_kdl_error(
1956                            format!(
1957                                "Property {} must be numeric, found {}",
1958                                $property_name,
1959                                first_entry.value()
1960                            ),
1961                            property.span().offset(),
1962                            property.span().len(),
1963                        ));
1964                    },
1965                },
1966                None => {
1967                    return Err(ConfigError::new_kdl_error(
1968                        format!("Property {} must have a value", $property_name),
1969                        property.span().offset(),
1970                        property.span().len(),
1971                    ));
1972                },
1973            },
1974            None => None,
1975        }
1976    }};
1977}
1978
1979#[macro_export]
1980macro_rules! kdl_has_string_argument {
1981    ( $kdl_node:expr, $string_argument:expr ) => {
1982        $kdl_node
1983            .entries()
1984            .iter()
1985            .find(|e| e.value().as_string() == Some($string_argument))
1986            .is_some()
1987    };
1988}
1989
1990#[macro_export]
1991macro_rules! kdl_children_property_first_arg_as_string {
1992    ( $kdl_node:expr, $property_name:expr ) => {
1993        $kdl_node
1994            .children()
1995            .and_then(|c| c.get($property_name))
1996            .and_then(|p| p.entries().iter().next())
1997            .and_then(|p| p.value().as_string())
1998    };
1999}
2000
2001#[macro_export]
2002macro_rules! kdl_property_first_arg_as_bool {
2003    ( $kdl_node:expr, $property_name:expr ) => {
2004        $kdl_node
2005            .get($property_name)
2006            .and_then(|p| p.entries().iter().next())
2007            .and_then(|p| p.value().as_bool())
2008    };
2009}
2010
2011#[macro_export]
2012macro_rules! kdl_children_property_first_arg_as_bool {
2013    ( $kdl_node:expr, $property_name:expr ) => {
2014        $kdl_node
2015            .children()
2016            .and_then(|c| c.get($property_name))
2017            .and_then(|p| p.entries().iter().next())
2018            .and_then(|p| p.value().as_bool())
2019    };
2020}
2021
2022#[macro_export]
2023macro_rules! kdl_property_first_arg_as_i64 {
2024    ( $kdl_node:expr, $property_name:expr ) => {
2025        $kdl_node
2026            .get($property_name)
2027            .and_then(|p| p.entries().iter().next())
2028            .and_then(|p| p.value().as_i64())
2029    };
2030}
2031
2032#[macro_export]
2033macro_rules! kdl_get_child {
2034    ( $kdl_node:expr, $child_name:expr ) => {
2035        $kdl_node.children().and_then(|c| c.get($child_name))
2036    };
2037}
2038
2039#[macro_export]
2040macro_rules! kdl_get_child_entry_bool_value {
2041    ( $kdl_node:expr, $child_name:expr ) => {
2042        $kdl_node
2043            .children()
2044            .and_then(|c| c.get($child_name))
2045            .and_then(|c| c.get(0))
2046            .and_then(|c| c.value().as_bool())
2047    };
2048}
2049
2050#[macro_export]
2051macro_rules! kdl_get_child_entry_string_value {
2052    ( $kdl_node:expr, $child_name:expr ) => {
2053        $kdl_node
2054            .children()
2055            .and_then(|c| c.get($child_name))
2056            .and_then(|c| c.get(0))
2057            .and_then(|c| c.value().as_string())
2058    };
2059}
2060
2061#[macro_export]
2062macro_rules! kdl_get_bool_property_or_child_value {
2063    ( $kdl_node:expr, $name:expr ) => {
2064        $kdl_node
2065            .get($name)
2066            .and_then(|e| e.value().as_bool())
2067            .or_else(|| {
2068                $kdl_node
2069                    .children()
2070                    .and_then(|c| c.get($name))
2071                    .and_then(|c| c.get(0))
2072                    .and_then(|c| c.value().as_bool())
2073            })
2074    };
2075}
2076
2077#[macro_export]
2078macro_rules! kdl_get_bool_property_or_child_value_with_error {
2079    ( $kdl_node:expr, $name:expr ) => {
2080        match $kdl_node.get($name) {
2081            Some(e) => match e.value().as_bool() {
2082                Some(bool_value) => Some(bool_value),
2083                None => {
2084                    return Err(kdl_parsing_error!(
2085                        format!(
2086                            "{} should be either true or false, found {}",
2087                            $name,
2088                            e.value()
2089                        ),
2090                        e
2091                    ))
2092                },
2093            },
2094            None => {
2095                let child_value = $kdl_node
2096                    .children()
2097                    .and_then(|c| c.get($name))
2098                    .and_then(|c| c.get(0));
2099                match child_value {
2100                    Some(e) => match e.value().as_bool() {
2101                        Some(bool_value) => Some(bool_value),
2102                        None => {
2103                            return Err(kdl_parsing_error!(
2104                                format!(
2105                                    "{} should be either true or false, found {}",
2106                                    $name,
2107                                    e.value()
2108                                ),
2109                                e
2110                            ))
2111                        },
2112                    },
2113                    None => {
2114                        if let Some(child_node) = kdl_child_with_name!($kdl_node, $name) {
2115                            return Err(kdl_parsing_error!(
2116                                format!(
2117                                    "{} must have a value, eg. '{} true'",
2118                                    child_node.name().value(),
2119                                    child_node.name().value()
2120                                ),
2121                                child_node
2122                            ));
2123                        }
2124                        None
2125                    },
2126                }
2127            },
2128        }
2129    };
2130}
2131
2132#[macro_export]
2133macro_rules! kdl_property_or_child_value_node {
2134    ( $kdl_node:expr, $name:expr ) => {
2135        $kdl_node.get($name).or_else(|| {
2136            $kdl_node
2137                .children()
2138                .and_then(|c| c.get($name))
2139                .and_then(|c| c.get(0))
2140        })
2141    };
2142}
2143
2144#[macro_export]
2145macro_rules! kdl_child_with_name {
2146    ( $kdl_node:expr, $name:expr ) => {{
2147        $kdl_node
2148            .children()
2149            .and_then(|children| children.nodes().iter().find(|c| c.name().value() == $name))
2150    }};
2151}
2152
2153#[macro_export]
2154macro_rules! kdl_child_with_name_or_error {
2155    ( $kdl_node:expr, $name:expr) => {{
2156        $kdl_node
2157            .children()
2158            .and_then(|children| children.nodes().iter().find(|c| c.name().value() == $name))
2159            .ok_or(ConfigError::new_kdl_error(
2160                format!("Missing node {}", $name).into(),
2161                $kdl_node.span().offset(),
2162                $kdl_node.span().len(),
2163            ))
2164    }};
2165}
2166
2167#[macro_export]
2168macro_rules! kdl_get_string_property_or_child_value_with_error {
2169    ( $kdl_node:expr, $name:expr ) => {
2170        match $kdl_node.get($name) {
2171            Some(e) => match e.value().as_string() {
2172                Some(string_value) => Some(string_value),
2173                None => {
2174                    return Err(kdl_parsing_error!(
2175                        format!(
2176                            "{} should be a string, found {} - not a string",
2177                            $name,
2178                            e.value()
2179                        ),
2180                        e
2181                    ))
2182                },
2183            },
2184            None => {
2185                let child_value = $kdl_node
2186                    .children()
2187                    .and_then(|c| c.get($name))
2188                    .and_then(|c| c.get(0));
2189                match child_value {
2190                    Some(e) => match e.value().as_string() {
2191                        Some(string_value) => Some(string_value),
2192                        None => {
2193                            return Err(kdl_parsing_error!(
2194                                format!(
2195                                    "{} should be a string, found {} - not a string",
2196                                    $name,
2197                                    e.value()
2198                                ),
2199                                e
2200                            ))
2201                        },
2202                    },
2203                    None => {
2204                        if let Some(child_node) = kdl_child_with_name!($kdl_node, $name) {
2205                            return Err(kdl_parsing_error!(
2206                                format!(
2207                                    "{} must have a value, eg. '{} \"foo\"'",
2208                                    child_node.name().value(),
2209                                    child_node.name().value()
2210                                ),
2211                                child_node
2212                            ));
2213                        }
2214                        None
2215                    },
2216                }
2217            },
2218        }
2219    };
2220}
2221
2222#[macro_export]
2223macro_rules! kdl_get_property_or_child {
2224    ( $kdl_node:expr, $name:expr ) => {
2225        $kdl_node.get($name).or_else(|| {
2226            $kdl_node
2227                .children()
2228                .and_then(|c| c.get($name))
2229                .and_then(|c| c.get(0))
2230        })
2231    };
2232}
2233
2234#[macro_export]
2235macro_rules! kdl_get_int_property_or_child_value {
2236    ( $kdl_node:expr, $name:expr ) => {
2237        $kdl_node
2238            .get($name)
2239            .and_then(|e| e.value().as_i64())
2240            .or_else(|| {
2241                $kdl_node
2242                    .children()
2243                    .and_then(|c| c.get($name))
2244                    .and_then(|c| c.get(0))
2245                    .and_then(|c| c.value().as_i64())
2246            })
2247    };
2248}
2249
2250#[macro_export]
2251macro_rules! kdl_get_string_entry {
2252    ( $kdl_node:expr, $entry_name:expr ) => {
2253        $kdl_node
2254            .get($entry_name)
2255            .and_then(|e| e.value().as_string())
2256    };
2257}
2258
2259#[macro_export]
2260macro_rules! kdl_get_int_entry {
2261    ( $kdl_node:expr, $entry_name:expr ) => {
2262        $kdl_node.get($entry_name).and_then(|e| e.value().as_i64())
2263    };
2264}
2265
2266impl Options {
2267    pub fn from_kdl(kdl_options: &KdlDocument) -> Result<Self, ConfigError> {
2268        let on_force_close =
2269            match kdl_property_first_arg_as_string_or_error!(kdl_options, "on_force_close") {
2270                Some((string, entry)) => Some(OnForceClose::from_str(string).map_err(|_| {
2271                    kdl_parsing_error!(
2272                        format!("Invalid value for on_force_close: '{}'", string),
2273                        entry
2274                    )
2275                })?),
2276                None => None,
2277            };
2278        let simplified_ui =
2279            kdl_property_first_arg_as_bool_or_error!(kdl_options, "simplified_ui").map(|(v, _)| v);
2280        let default_shell =
2281            kdl_property_first_arg_as_string_or_error!(kdl_options, "default_shell")
2282                .map(|(string, _entry)| PathBuf::from(string));
2283        let default_cwd = kdl_property_first_arg_as_string_or_error!(kdl_options, "default_cwd")
2284            .map(|(string, _entry)| PathBuf::from(string));
2285        let pane_frames =
2286            kdl_property_first_arg_as_bool_or_error!(kdl_options, "pane_frames").map(|(v, _)| v);
2287        let auto_layout =
2288            kdl_property_first_arg_as_bool_or_error!(kdl_options, "auto_layout").map(|(v, _)| v);
2289        let theme = kdl_property_first_arg_as_string_or_error!(kdl_options, "theme")
2290            .map(|(theme, _entry)| theme.to_string());
2291        let default_mode =
2292            match kdl_property_first_arg_as_string_or_error!(kdl_options, "default_mode") {
2293                Some((string, entry)) => Some(InputMode::from_str(string).map_err(|_| {
2294                    kdl_parsing_error!(format!("Invalid input mode: '{}'", string), entry)
2295                })?),
2296                None => None,
2297            };
2298        let default_layout =
2299            kdl_property_first_arg_as_string_or_error!(kdl_options, "default_layout")
2300                .map(|(string, _entry)| PathBuf::from(string));
2301        let layout_dir = kdl_property_first_arg_as_string_or_error!(kdl_options, "layout_dir")
2302            .map(|(string, _entry)| PathBuf::from(string));
2303        let theme_dir = kdl_property_first_arg_as_string_or_error!(kdl_options, "theme_dir")
2304            .map(|(string, _entry)| PathBuf::from(string));
2305        let mouse_mode =
2306            kdl_property_first_arg_as_bool_or_error!(kdl_options, "mouse_mode").map(|(v, _)| v);
2307        let scroll_buffer_size =
2308            kdl_property_first_arg_as_i64_or_error!(kdl_options, "scroll_buffer_size")
2309                .map(|(scroll_buffer_size, _entry)| scroll_buffer_size as usize);
2310        let copy_command = kdl_property_first_arg_as_string_or_error!(kdl_options, "copy_command")
2311            .map(|(copy_command, _entry)| copy_command.to_string());
2312        let copy_clipboard =
2313            match kdl_property_first_arg_as_string_or_error!(kdl_options, "copy_clipboard") {
2314                Some((string, entry)) => Some(Clipboard::from_str(string).map_err(|_| {
2315                    kdl_parsing_error!(
2316                        format!("Invalid value for copy_clipboard: '{}'", string),
2317                        entry
2318                    )
2319                })?),
2320                None => None,
2321            };
2322        let copy_on_select =
2323            kdl_property_first_arg_as_bool_or_error!(kdl_options, "copy_on_select").map(|(v, _)| v);
2324        let scrollback_editor =
2325            kdl_property_first_arg_as_string_or_error!(kdl_options, "scrollback_editor")
2326                .map(|(string, _entry)| PathBuf::from(string));
2327        let mirror_session =
2328            kdl_property_first_arg_as_bool_or_error!(kdl_options, "mirror_session").map(|(v, _)| v);
2329        let session_name = kdl_property_first_arg_as_string_or_error!(kdl_options, "session_name")
2330            .map(|(session_name, _entry)| session_name.to_string());
2331        let attach_to_session =
2332            kdl_property_first_arg_as_bool_or_error!(kdl_options, "attach_to_session")
2333                .map(|(v, _)| v);
2334        let session_serialization =
2335            kdl_property_first_arg_as_bool_or_error!(kdl_options, "session_serialization")
2336                .map(|(v, _)| v);
2337        let serialize_pane_viewport =
2338            kdl_property_first_arg_as_bool_or_error!(kdl_options, "serialize_pane_viewport")
2339                .map(|(v, _)| v);
2340        let scrollback_lines_to_serialize =
2341            kdl_property_first_arg_as_i64_or_error!(kdl_options, "scrollback_lines_to_serialize")
2342                .map(|(v, _)| v as usize);
2343        let styled_underlines =
2344            kdl_property_first_arg_as_bool_or_error!(kdl_options, "styled_underlines")
2345                .map(|(v, _)| v);
2346        let serialization_interval =
2347            kdl_property_first_arg_as_i64_or_error!(kdl_options, "serialization_interval")
2348                .map(|(scroll_buffer_size, _entry)| scroll_buffer_size as u64);
2349        let disable_session_metadata =
2350            kdl_property_first_arg_as_bool_or_error!(kdl_options, "disable_session_metadata")
2351                .map(|(v, _)| v);
2352        let support_kitty_keyboard_protocol = kdl_property_first_arg_as_bool_or_error!(
2353            kdl_options,
2354            "support_kitty_keyboard_protocol"
2355        )
2356        .map(|(v, _)| v);
2357        let web_server =
2358            kdl_property_first_arg_as_bool_or_error!(kdl_options, "web_server").map(|(v, _)| v);
2359        let web_sharing =
2360            match kdl_property_first_arg_as_string_or_error!(kdl_options, "web_sharing") {
2361                Some((string, entry)) => Some(WebSharing::from_str(string).map_err(|_| {
2362                    kdl_parsing_error!(
2363                        format!("Invalid value for web_sharing: '{}'", string),
2364                        entry
2365                    )
2366                })?),
2367                None => None,
2368            };
2369        let stacked_resize =
2370            kdl_property_first_arg_as_bool_or_error!(kdl_options, "stacked_resize").map(|(v, _)| v);
2371        let show_startup_tips =
2372            kdl_property_first_arg_as_bool_or_error!(kdl_options, "show_startup_tips")
2373                .map(|(v, _)| v);
2374        let show_release_notes =
2375            kdl_property_first_arg_as_bool_or_error!(kdl_options, "show_release_notes")
2376                .map(|(v, _)| v);
2377        let advanced_mouse_actions =
2378            kdl_property_first_arg_as_bool_or_error!(kdl_options, "advanced_mouse_actions")
2379                .map(|(v, _)| v);
2380        let web_server_ip =
2381            match kdl_property_first_arg_as_string_or_error!(kdl_options, "web_server_ip") {
2382                Some((string, entry)) => Some(IpAddr::from_str(string).map_err(|_| {
2383                    kdl_parsing_error!(
2384                        format!("Invalid value for web_server_ip: '{}'", string),
2385                        entry
2386                    )
2387                })?),
2388                None => None,
2389            };
2390        let web_server_port =
2391            kdl_property_first_arg_as_i64_or_error!(kdl_options, "web_server_port")
2392                .map(|(web_server_port, _entry)| web_server_port as u16);
2393        let web_server_cert =
2394            kdl_property_first_arg_as_string_or_error!(kdl_options, "web_server_cert")
2395                .map(|(string, _entry)| PathBuf::from(string));
2396        let web_server_key =
2397            kdl_property_first_arg_as_string_or_error!(kdl_options, "web_server_key")
2398                .map(|(string, _entry)| PathBuf::from(string));
2399        let enforce_https_for_localhost =
2400            kdl_property_first_arg_as_bool_or_error!(kdl_options, "enforce_https_for_localhost")
2401                .map(|(v, _)| v);
2402        let post_command_discovery_hook =
2403            kdl_property_first_arg_as_string_or_error!(kdl_options, "post_command_discovery_hook")
2404                .map(|(hook, _entry)| hook.to_string());
2405
2406        Ok(Options {
2407            simplified_ui,
2408            theme,
2409            default_mode,
2410            default_shell,
2411            default_cwd,
2412            default_layout,
2413            layout_dir,
2414            theme_dir,
2415            mouse_mode,
2416            pane_frames,
2417            mirror_session,
2418            on_force_close,
2419            scroll_buffer_size,
2420            copy_command,
2421            copy_clipboard,
2422            copy_on_select,
2423            scrollback_editor,
2424            session_name,
2425            attach_to_session,
2426            auto_layout,
2427            session_serialization,
2428            serialize_pane_viewport,
2429            scrollback_lines_to_serialize,
2430            styled_underlines,
2431            serialization_interval,
2432            disable_session_metadata,
2433            support_kitty_keyboard_protocol,
2434            web_server,
2435            web_sharing,
2436            stacked_resize,
2437            show_startup_tips,
2438            show_release_notes,
2439            advanced_mouse_actions,
2440            web_server_ip,
2441            web_server_port,
2442            web_server_cert,
2443            web_server_key,
2444            enforce_https_for_localhost,
2445            post_command_discovery_hook,
2446        })
2447    }
2448    pub fn from_string(stringified_keybindings: &String) -> Result<Self, ConfigError> {
2449        let document: KdlDocument = stringified_keybindings.parse()?;
2450        Options::from_kdl(&document)
2451    }
2452    fn simplified_ui_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
2453        let comment_text = format!(
2454            "{}\n{}\n{}\n{}\n{}\n{}",
2455            " ",
2456            "// Use a simplified UI without special fonts (arrow glyphs)",
2457            "// Options:",
2458            "//   - true",
2459            "//   - false (Default)",
2460            "// ",
2461        );
2462
2463        let create_node = |node_value: bool| -> KdlNode {
2464            let mut node = KdlNode::new("simplified_ui");
2465            node.push(KdlValue::Bool(node_value));
2466            node
2467        };
2468        if let Some(simplified_ui) = self.simplified_ui {
2469            let mut node = create_node(simplified_ui);
2470            if add_comments {
2471                node.set_leading(format!("{}\n", comment_text));
2472            }
2473            Some(node)
2474        } else if add_comments {
2475            let mut node = create_node(true);
2476            node.set_leading(format!("{}\n// ", comment_text));
2477            Some(node)
2478        } else {
2479            None
2480        }
2481    }
2482    fn theme_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
2483        let comment_text = format!(
2484            "{}\n{}\n{}\n{}",
2485            " ",
2486            "// Choose the theme that is specified in the themes section.",
2487            "// Default: default",
2488            "// ",
2489        );
2490
2491        let create_node = |node_value: &str| -> KdlNode {
2492            let mut node = KdlNode::new("theme");
2493            node.push(node_value.to_owned());
2494            node
2495        };
2496        if let Some(theme) = &self.theme {
2497            let mut node = create_node(theme);
2498            if add_comments {
2499                node.set_leading(format!("{}\n", comment_text));
2500            }
2501            Some(node)
2502        } else if add_comments {
2503            let mut node = create_node("dracula");
2504            node.set_leading(format!("{}\n// ", comment_text));
2505            Some(node)
2506        } else {
2507            None
2508        }
2509    }
2510    fn default_mode_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
2511        let comment_text = format!(
2512            "{}\n{}\n{}\n{}",
2513            " ", "// Choose the base input mode of zellij.", "// Default: normal", "// "
2514        );
2515
2516        let create_node = |default_mode: &InputMode| -> KdlNode {
2517            let mut node = KdlNode::new("default_mode");
2518            node.push(format!("{:?}", default_mode).to_lowercase());
2519            node
2520        };
2521        if let Some(default_mode) = &self.default_mode {
2522            let mut node = create_node(default_mode);
2523            if add_comments {
2524                node.set_leading(format!("{}\n", comment_text));
2525            }
2526            Some(node)
2527        } else if add_comments {
2528            let mut node = create_node(&InputMode::Locked);
2529            node.set_leading(format!("{}\n// ", comment_text));
2530            Some(node)
2531        } else {
2532            None
2533        }
2534    }
2535    fn default_shell_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
2536        let comment_text =
2537            format!("{}\n{}\n{}\n{}",
2538            " ",
2539            "// Choose the path to the default shell that zellij will use for opening new panes",
2540            "// Default: $SHELL",
2541            "// ",
2542        );
2543
2544        let create_node = |node_value: &str| -> KdlNode {
2545            let mut node = KdlNode::new("default_shell");
2546            node.push(node_value.to_owned());
2547            node
2548        };
2549        if let Some(default_shell) = &self.default_shell {
2550            let mut node = create_node(&default_shell.display().to_string());
2551            if add_comments {
2552                node.set_leading(format!("{}\n", comment_text));
2553            }
2554            Some(node)
2555        } else if add_comments {
2556            let mut node = create_node("fish");
2557            node.set_leading(format!("{}\n// ", comment_text));
2558            Some(node)
2559        } else {
2560            None
2561        }
2562    }
2563    fn default_cwd_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
2564        let comment_text = format!(
2565            "{}\n{}\n{}",
2566            " ",
2567            "// Choose the path to override cwd that zellij will use for opening new panes",
2568            "// ",
2569        );
2570
2571        let create_node = |node_value: &str| -> KdlNode {
2572            let mut node = KdlNode::new("default_cwd");
2573            node.push(node_value.to_owned());
2574            node
2575        };
2576        if let Some(default_cwd) = &self.default_cwd {
2577            let mut node = create_node(&default_cwd.display().to_string());
2578            if add_comments {
2579                node.set_leading(format!("{}\n", comment_text));
2580            }
2581            Some(node)
2582        } else if add_comments {
2583            let mut node = create_node("/tmp");
2584            node.set_leading(format!("{}\n// ", comment_text));
2585            Some(node)
2586        } else {
2587            None
2588        }
2589    }
2590    fn default_layout_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
2591        let comment_text = format!(
2592            "{}\n{}\n{}\n{}",
2593            " ",
2594            "// The name of the default layout to load on startup",
2595            "// Default: \"default\"",
2596            "// ",
2597        );
2598
2599        let create_node = |node_value: &str| -> KdlNode {
2600            let mut node = KdlNode::new("default_layout");
2601            node.push(node_value.to_owned());
2602            node
2603        };
2604        if let Some(default_layout) = &self.default_layout {
2605            let mut node = create_node(&default_layout.display().to_string());
2606            if add_comments {
2607                node.set_leading(format!("{}\n", comment_text));
2608            }
2609            Some(node)
2610        } else if add_comments {
2611            let mut node = create_node("compact");
2612            node.set_leading(format!("{}\n// ", comment_text));
2613            Some(node)
2614        } else {
2615            None
2616        }
2617    }
2618    fn layout_dir_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
2619        let comment_text = format!(
2620            "{}\n{}\n{}\n{}",
2621            " ",
2622            "// The folder in which Zellij will look for layouts",
2623            "// (Requires restart)",
2624            "// ",
2625        );
2626
2627        let create_node = |node_value: &str| -> KdlNode {
2628            let mut node = KdlNode::new("layout_dir");
2629            node.push(node_value.to_owned());
2630            node
2631        };
2632        if let Some(layout_dir) = &self.layout_dir {
2633            let mut node = create_node(&layout_dir.display().to_string());
2634            if add_comments {
2635                node.set_leading(format!("{}\n", comment_text));
2636            }
2637            Some(node)
2638        } else if add_comments {
2639            let mut node = create_node("/tmp");
2640            node.set_leading(format!("{}\n// ", comment_text));
2641            Some(node)
2642        } else {
2643            None
2644        }
2645    }
2646    fn theme_dir_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
2647        let comment_text = format!(
2648            "{}\n{}\n{}\n{}",
2649            " ",
2650            "// The folder in which Zellij will look for themes",
2651            "// (Requires restart)",
2652            "// ",
2653        );
2654
2655        let create_node = |node_value: &str| -> KdlNode {
2656            let mut node = KdlNode::new("theme_dir");
2657            node.push(node_value.to_owned());
2658            node
2659        };
2660        if let Some(theme_dir) = &self.theme_dir {
2661            let mut node = create_node(&theme_dir.display().to_string());
2662            if add_comments {
2663                node.set_leading(format!("{}\n", comment_text));
2664            }
2665            Some(node)
2666        } else if add_comments {
2667            let mut node = create_node("/tmp");
2668            node.set_leading(format!("{}\n// ", comment_text));
2669            Some(node)
2670        } else {
2671            None
2672        }
2673    }
2674    fn mouse_mode_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
2675        let comment_text = format!(
2676            "{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}",
2677            " ",
2678            "// Toggle enabling the mouse mode.",
2679            "// On certain configurations, or terminals this could",
2680            "// potentially interfere with copying text.",
2681            "// Options:",
2682            "//   - true (default)",
2683            "//   - false",
2684            "// ",
2685        );
2686
2687        let create_node = |node_value: bool| -> KdlNode {
2688            let mut node = KdlNode::new("mouse_mode");
2689            node.push(KdlValue::Bool(node_value));
2690            node
2691        };
2692        if let Some(mouse_mode) = self.mouse_mode {
2693            let mut node = create_node(mouse_mode);
2694            if add_comments {
2695                node.set_leading(format!("{}\n", comment_text));
2696            }
2697            Some(node)
2698        } else if add_comments {
2699            let mut node = create_node(false);
2700            node.set_leading(format!("{}\n// ", comment_text));
2701            Some(node)
2702        } else {
2703            None
2704        }
2705    }
2706    fn pane_frames_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
2707        let comment_text = format!(
2708            "{}\n{}\n{}\n{}\n{}\n{}",
2709            " ",
2710            "// Toggle having pane frames around the panes",
2711            "// Options:",
2712            "//   - true (default, enabled)",
2713            "//   - false",
2714            "// ",
2715        );
2716
2717        let create_node = |node_value: bool| -> KdlNode {
2718            let mut node = KdlNode::new("pane_frames");
2719            node.push(KdlValue::Bool(node_value));
2720            node
2721        };
2722        if let Some(pane_frames) = self.pane_frames {
2723            let mut node = create_node(pane_frames);
2724            if add_comments {
2725                node.set_leading(format!("{}\n", comment_text));
2726            }
2727            Some(node)
2728        } else if add_comments {
2729            let mut node = create_node(false);
2730            node.set_leading(format!("{}\n// ", comment_text));
2731            Some(node)
2732        } else {
2733            None
2734        }
2735    }
2736    fn mirror_session_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
2737        let comment_text = format!(
2738            "{}\n{}\n{}\n{}\n{}\n{}\n{}",
2739            " ",
2740            "// When attaching to an existing session with other users,",
2741            "// should the session be mirrored (true)",
2742            "// or should each user have their own cursor (false)",
2743            "// (Requires restart)",
2744            "// Default: false",
2745            "// ",
2746        );
2747
2748        let create_node = |node_value: bool| -> KdlNode {
2749            let mut node = KdlNode::new("mirror_session");
2750            node.push(KdlValue::Bool(node_value));
2751            node
2752        };
2753        if let Some(mirror_session) = self.mirror_session {
2754            let mut node = create_node(mirror_session);
2755            if add_comments {
2756                node.set_leading(format!("{}\n", comment_text));
2757            }
2758            Some(node)
2759        } else if add_comments {
2760            let mut node = create_node(true);
2761            node.set_leading(format!("{}\n// ", comment_text));
2762            Some(node)
2763        } else {
2764            None
2765        }
2766    }
2767    fn on_force_close_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
2768        let comment_text = format!(
2769            "{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}",
2770            " ",
2771            "// Choose what to do when zellij receives SIGTERM, SIGINT, SIGQUIT or SIGHUP",
2772            "// eg. when terminal window with an active zellij session is closed",
2773            "// (Requires restart)",
2774            "// Options:",
2775            "//   - detach (Default)",
2776            "//   - quit",
2777            "// ",
2778        );
2779
2780        let create_node = |node_value: &str| -> KdlNode {
2781            let mut node = KdlNode::new("on_force_close");
2782            node.push(node_value.to_owned());
2783            node
2784        };
2785        if let Some(on_force_close) = &self.on_force_close {
2786            let mut node = match on_force_close {
2787                OnForceClose::Detach => create_node("detach"),
2788                OnForceClose::Quit => create_node("quit"),
2789            };
2790            if add_comments {
2791                node.set_leading(format!("{}\n", comment_text));
2792            }
2793            Some(node)
2794        } else if add_comments {
2795            let mut node = create_node("quit");
2796            node.set_leading(format!("{}\n// ", comment_text));
2797            Some(node)
2798        } else {
2799            None
2800        }
2801    }
2802    fn scroll_buffer_size_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
2803        let comment_text = format!(
2804            "{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}",
2805            " ",
2806            "// Configure the scroll back buffer size",
2807            "// This is the number of lines zellij stores for each pane in the scroll back",
2808            "// buffer. Excess number of lines are discarded in a FIFO fashion.",
2809            "// (Requires restart)",
2810            "// Valid values: positive integers",
2811            "// Default value: 10000",
2812            "// ",
2813        );
2814
2815        let create_node = |node_value: usize| -> KdlNode {
2816            let mut node = KdlNode::new("scroll_buffer_size");
2817            node.push(KdlValue::Base10(node_value as i64));
2818            node
2819        };
2820        if let Some(scroll_buffer_size) = self.scroll_buffer_size {
2821            let mut node = create_node(scroll_buffer_size);
2822            if add_comments {
2823                node.set_leading(format!("{}\n", comment_text));
2824            }
2825            Some(node)
2826        } else if add_comments {
2827            let mut node = create_node(10000);
2828            node.set_leading(format!("{}\n// ", comment_text));
2829            Some(node)
2830        } else {
2831            None
2832        }
2833    }
2834    fn copy_command_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
2835        let comment_text = format!(
2836            "{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}",
2837            " ",
2838            "// Provide a command to execute when copying text. The text will be piped to",
2839            "// the stdin of the program to perform the copy. This can be used with",
2840            "// terminal emulators which do not support the OSC 52 ANSI control sequence",
2841            "// that will be used by default if this option is not set.",
2842            "// Examples:",
2843            "//",
2844            "// copy_command \"xclip -selection clipboard\" // x11",
2845            "// copy_command \"wl-copy\"                    // wayland",
2846            "// copy_command \"pbcopy\"                     // osx",
2847            "// ",
2848        );
2849
2850        let create_node = |node_value: &str| -> KdlNode {
2851            let mut node = KdlNode::new("copy_command");
2852            node.push(node_value.to_owned());
2853            node
2854        };
2855        if let Some(copy_command) = &self.copy_command {
2856            let mut node = create_node(copy_command);
2857            if add_comments {
2858                node.set_leading(format!("{}\n", comment_text));
2859            }
2860            Some(node)
2861        } else if add_comments {
2862            let mut node = create_node("pbcopy");
2863            node.set_leading(format!("{}\n// ", comment_text));
2864            Some(node)
2865        } else {
2866            None
2867        }
2868    }
2869    fn copy_clipboard_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
2870        let comment_text = format!("{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}",
2871            " ",
2872            "// Choose the destination for copied text",
2873            "// Allows using the primary selection buffer (on x11/wayland) instead of the system clipboard.",
2874            "// Does not apply when using copy_command.",
2875            "// Options:",
2876            "//   - system (default)",
2877            "//   - primary",
2878            "// ",
2879        );
2880
2881        let create_node = |node_value: &str| -> KdlNode {
2882            let mut node = KdlNode::new("copy_clipboard");
2883            node.push(node_value.to_owned());
2884            node
2885        };
2886        if let Some(copy_clipboard) = &self.copy_clipboard {
2887            let mut node = match copy_clipboard {
2888                Clipboard::Primary => create_node("primary"),
2889                Clipboard::System => create_node("system"),
2890            };
2891            if add_comments {
2892                node.set_leading(format!("{}\n", comment_text));
2893            }
2894            Some(node)
2895        } else if add_comments {
2896            let mut node = create_node("primary");
2897            node.set_leading(format!("{}\n// ", comment_text));
2898            Some(node)
2899        } else {
2900            None
2901        }
2902    }
2903    fn copy_on_select_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
2904        let comment_text = format!(
2905            "{}\n{}\n{}\n{}",
2906            " ",
2907            "// Enable automatic copying (and clearing) of selection when releasing mouse",
2908            "// Default: true",
2909            "// ",
2910        );
2911
2912        let create_node = |node_value: bool| -> KdlNode {
2913            let mut node = KdlNode::new("copy_on_select");
2914            node.push(KdlValue::Bool(node_value));
2915            node
2916        };
2917        if let Some(copy_on_select) = self.copy_on_select {
2918            let mut node = create_node(copy_on_select);
2919            if add_comments {
2920                node.set_leading(format!("{}\n", comment_text));
2921            }
2922            Some(node)
2923        } else if add_comments {
2924            let mut node = create_node(true);
2925            node.set_leading(format!("{}\n// ", comment_text));
2926            Some(node)
2927        } else {
2928            None
2929        }
2930    }
2931    fn scrollback_editor_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
2932        let comment_text = format!(
2933            "{}\n{}\n{}",
2934            " ",
2935            "// Path to the default editor to use to edit pane scrollbuffer",
2936            "// Default: $EDITOR or $VISUAL",
2937        );
2938
2939        let create_node = |node_value: &str| -> KdlNode {
2940            let mut node = KdlNode::new("scrollback_editor");
2941            node.push(node_value.to_owned());
2942            node
2943        };
2944        if let Some(scrollback_editor) = &self.scrollback_editor {
2945            let mut node = create_node(&scrollback_editor.display().to_string());
2946            if add_comments {
2947                node.set_leading(format!("{}\n", comment_text));
2948            }
2949            Some(node)
2950        } else if add_comments {
2951            let mut node = create_node("/usr/bin/vim");
2952            node.set_leading(format!("{}\n// ", comment_text));
2953            Some(node)
2954        } else {
2955            None
2956        }
2957    }
2958    fn session_name_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
2959        let comment_text = format!(
2960            "{}\n{}\n{}\n{}\n{}\n{}",
2961            " ",
2962            "// A fixed name to always give the Zellij session.",
2963            "// Consider also setting `attach_to_session true,`",
2964            "// otherwise this will error if such a session exists.",
2965            "// Default: <RANDOM>",
2966            "// ",
2967        );
2968
2969        let create_node = |node_value: &str| -> KdlNode {
2970            let mut node = KdlNode::new("session_name");
2971            node.push(node_value.to_owned());
2972            node
2973        };
2974        if let Some(session_name) = &self.session_name {
2975            let mut node = create_node(&session_name);
2976            if add_comments {
2977                node.set_leading(format!("{}\n", comment_text));
2978            }
2979            Some(node)
2980        } else if add_comments {
2981            let mut node = create_node("My singleton session");
2982            node.set_leading(format!("{}\n// ", comment_text));
2983            Some(node)
2984        } else {
2985            None
2986        }
2987    }
2988    fn attach_to_session_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
2989        let comment_text = format!(
2990            "{}\n{}\n{}\n{}\n{}",
2991            " ",
2992            "// When `session_name` is provided, attaches to that session",
2993            "// if it is already running or creates it otherwise.",
2994            "// Default: false",
2995            "// ",
2996        );
2997
2998        let create_node = |node_value: bool| -> KdlNode {
2999            let mut node = KdlNode::new("attach_to_session");
3000            node.push(KdlValue::Bool(node_value));
3001            node
3002        };
3003        if let Some(attach_to_session) = self.attach_to_session {
3004            let mut node = create_node(attach_to_session);
3005            if add_comments {
3006                node.set_leading(format!("{}\n", comment_text));
3007            }
3008            Some(node)
3009        } else if add_comments {
3010            let mut node = create_node(true);
3011            node.set_leading(format!("{}\n// ", comment_text));
3012            Some(node)
3013        } else {
3014            None
3015        }
3016    }
3017    fn auto_layout_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
3018        let comment_text = format!("{}\n{}\n{}\n{}\n{}\n{}",
3019            " ",
3020            "// Toggle between having Zellij lay out panes according to a predefined set of layouts whenever possible",
3021            "// Options:",
3022            "//   - true (default)",
3023            "//   - false",
3024            "// ",
3025        );
3026
3027        let create_node = |node_value: bool| -> KdlNode {
3028            let mut node = KdlNode::new("auto_layout");
3029            node.push(KdlValue::Bool(node_value));
3030            node
3031        };
3032        if let Some(auto_layout) = self.auto_layout {
3033            let mut node = create_node(auto_layout);
3034            if add_comments {
3035                node.set_leading(format!("{}\n", comment_text));
3036            }
3037            Some(node)
3038        } else if add_comments {
3039            let mut node = create_node(false);
3040            node.set_leading(format!("{}\n// ", comment_text));
3041            Some(node)
3042        } else {
3043            None
3044        }
3045    }
3046    fn session_serialization_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
3047        let comment_text = format!("{}\n{}\n{}\n{}\n{}\n{}",
3048            " ",
3049            "// Whether sessions should be serialized to the cache folder (including their tabs/panes, cwds and running commands) so that they can later be resurrected",
3050            "// Options:",
3051            "//   - true (default)",
3052            "//   - false",
3053            "// ",
3054        );
3055
3056        let create_node = |node_value: bool| -> KdlNode {
3057            let mut node = KdlNode::new("session_serialization");
3058            node.push(KdlValue::Bool(node_value));
3059            node
3060        };
3061        if let Some(session_serialization) = self.session_serialization {
3062            let mut node = create_node(session_serialization);
3063            if add_comments {
3064                node.set_leading(format!("{}\n", comment_text));
3065            }
3066            Some(node)
3067        } else if add_comments {
3068            let mut node = create_node(false);
3069            node.set_leading(format!("{}\n// ", comment_text));
3070            Some(node)
3071        } else {
3072            None
3073        }
3074    }
3075    fn serialize_pane_viewport_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
3076        let comment_text = format!(
3077            "{}\n{}\n{}\n{}\n{}\n{}",
3078            " ",
3079            "// Whether pane viewports are serialized along with the session, default is false",
3080            "// Options:",
3081            "//   - true",
3082            "//   - false (default)",
3083            "// ",
3084        );
3085
3086        let create_node = |node_value: bool| -> KdlNode {
3087            let mut node = KdlNode::new("serialize_pane_viewport");
3088            node.push(KdlValue::Bool(node_value));
3089            node
3090        };
3091        if let Some(serialize_pane_viewport) = self.serialize_pane_viewport {
3092            let mut node = create_node(serialize_pane_viewport);
3093            if add_comments {
3094                node.set_leading(format!("{}\n", comment_text));
3095            }
3096            Some(node)
3097        } else if add_comments {
3098            let mut node = create_node(false);
3099            node.set_leading(format!("{}\n// ", comment_text));
3100            Some(node)
3101        } else {
3102            None
3103        }
3104    }
3105    fn scrollback_lines_to_serialize_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
3106        let comment_text = format!("{}\n{}\n{}\n{}\n{}",
3107            " ",
3108            "// Scrollback lines to serialize along with the pane viewport when serializing sessions, 0",
3109            "// defaults to the scrollback size. If this number is higher than the scrollback size, it will",
3110            "// also default to the scrollback size. This does nothing if `serialize_pane_viewport` is not true.",
3111            "// ",
3112        );
3113
3114        let create_node = |node_value: usize| -> KdlNode {
3115            let mut node = KdlNode::new("scrollback_lines_to_serialize");
3116            node.push(KdlValue::Base10(node_value as i64));
3117            node
3118        };
3119        if let Some(scrollback_lines_to_serialize) = self.scrollback_lines_to_serialize {
3120            let mut node = create_node(scrollback_lines_to_serialize);
3121            if add_comments {
3122                node.set_leading(format!("{}\n", comment_text));
3123            }
3124            Some(node)
3125        } else if add_comments {
3126            let mut node = create_node(10000);
3127            node.set_leading(format!("{}\n// ", comment_text));
3128            Some(node)
3129        } else {
3130            None
3131        }
3132    }
3133    fn styled_underlines_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
3134        let comment_text = format!(
3135            "{}\n{}\n{}\n{}\n{}\n{}",
3136            " ",
3137            "// Enable or disable the rendering of styled and colored underlines (undercurl).",
3138            "// May need to be disabled for certain unsupported terminals",
3139            "// (Requires restart)",
3140            "// Default: true",
3141            "// ",
3142        );
3143
3144        let create_node = |node_value: bool| -> KdlNode {
3145            let mut node = KdlNode::new("styled_underlines");
3146            node.push(KdlValue::Bool(node_value));
3147            node
3148        };
3149        if let Some(styled_underlines) = self.styled_underlines {
3150            let mut node = create_node(styled_underlines);
3151            if add_comments {
3152                node.set_leading(format!("{}\n", comment_text));
3153            }
3154            Some(node)
3155        } else if add_comments {
3156            let mut node = create_node(false);
3157            node.set_leading(format!("{}\n// ", comment_text));
3158            Some(node)
3159        } else {
3160            None
3161        }
3162    }
3163    fn serialization_interval_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
3164        let comment_text = format!(
3165            "{}\n{}\n{}",
3166            " ", "// How often in seconds sessions are serialized", "// ",
3167        );
3168
3169        let create_node = |node_value: u64| -> KdlNode {
3170            let mut node = KdlNode::new("serialization_interval");
3171            node.push(KdlValue::Base10(node_value as i64));
3172            node
3173        };
3174        if let Some(serialization_interval) = self.serialization_interval {
3175            let mut node = create_node(serialization_interval);
3176            if add_comments {
3177                node.set_leading(format!("{}\n", comment_text));
3178            }
3179            Some(node)
3180        } else if add_comments {
3181            let mut node = create_node(10000);
3182            node.set_leading(format!("{}\n// ", comment_text));
3183            Some(node)
3184        } else {
3185            None
3186        }
3187    }
3188    fn disable_session_metadata_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
3189        let comment_text = format!("{}\n{}\n{}\n{}\n{}\n{}",
3190            " ",
3191            "// Enable or disable writing of session metadata to disk (if disabled, other sessions might not know",
3192            "// metadata info on this session)",
3193            "// (Requires restart)",
3194            "// Default: false",
3195            "// ",
3196        );
3197
3198        let create_node = |node_value: bool| -> KdlNode {
3199            let mut node = KdlNode::new("disable_session_metadata");
3200            node.push(KdlValue::Bool(node_value));
3201            node
3202        };
3203        if let Some(disable_session_metadata) = self.disable_session_metadata {
3204            let mut node = create_node(disable_session_metadata);
3205            if add_comments {
3206                node.set_leading(format!("{}\n", comment_text));
3207            }
3208            Some(node)
3209        } else if add_comments {
3210            let mut node = create_node(false);
3211            node.set_leading(format!("{}\n// ", comment_text));
3212            Some(node)
3213        } else {
3214            None
3215        }
3216    }
3217    fn support_kitty_keyboard_protocol_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
3218        let comment_text = format!("{}\n{}\n{}\n{}\n{}",
3219            " ",
3220            "// Enable or disable support for the enhanced Kitty Keyboard Protocol (the host terminal must also support it)",
3221            "// (Requires restart)",
3222            "// Default: true (if the host terminal supports it)",
3223            "// ",
3224        );
3225
3226        let create_node = |node_value: bool| -> KdlNode {
3227            let mut node = KdlNode::new("support_kitty_keyboard_protocol");
3228            node.push(KdlValue::Bool(node_value));
3229            node
3230        };
3231        if let Some(support_kitty_keyboard_protocol) = self.support_kitty_keyboard_protocol {
3232            let mut node = create_node(support_kitty_keyboard_protocol);
3233            if add_comments {
3234                node.set_leading(format!("{}\n", comment_text));
3235            }
3236            Some(node)
3237        } else if add_comments {
3238            let mut node = create_node(false);
3239            node.set_leading(format!("{}\n// ", comment_text));
3240            Some(node)
3241        } else {
3242            None
3243        }
3244    }
3245    fn web_server_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
3246        let comment_text = format!(
3247            "{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}",
3248            "// Whether to make sure a local web server is running when a new Zellij session starts.",
3249            "// This web server will allow creating new sessions and attaching to existing ones that have",
3250            "// opted in to being shared in the browser.",
3251            "// When enabled, navigate to http://127.0.0.1:8082",
3252            "// (Requires restart)",
3253            "// ",
3254            "// Note: a local web server can still be manually started from within a Zellij session or from the CLI.",
3255            "// If this is not desired, one can use a version of Zellij compiled without",
3256            "// `web_server_capability`",
3257            "// ",
3258            "// Possible values:",
3259            "// - true",
3260            "// - false",
3261            "// Default: false",
3262            "// ",
3263        );
3264
3265        let create_node = |node_value: bool| -> KdlNode {
3266            let mut node = KdlNode::new("web_server");
3267            node.push(KdlValue::Bool(node_value));
3268            node
3269        };
3270        if let Some(web_server) = self.web_server {
3271            let mut node = create_node(web_server);
3272            if add_comments {
3273                node.set_leading(format!("{}\n", comment_text));
3274            }
3275            Some(node)
3276        } else if add_comments {
3277            let mut node = create_node(false);
3278            node.set_leading(format!("{}\n// ", comment_text));
3279            Some(node)
3280        } else {
3281            None
3282        }
3283    }
3284    fn web_sharing_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
3285        let comment_text = format!(
3286            "{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}",
3287            "// Whether to allow sessions started in the terminal to be shared through a local web server, assuming one is",
3288            "// running (see the `web_server` option for more details).",
3289            "// (Requires restart)",
3290            "// ",
3291            "// Note: This is an administrative separation and not intended as a security measure.",
3292            "// ",
3293            "// Possible values:",
3294            "// - \"on\" (allow web sharing through the local web server if it",
3295            "// is online)",
3296            "// - \"off\" (do not allow web sharing unless sessions explicitly opt-in to it)",
3297            "// - \"disabled\" (do not allow web sharing and do not permit sessions started in the terminal to opt-in to it)",
3298            "// Default: \"off\"",
3299            "// ",
3300        );
3301
3302        let create_node = |node_value: &str| -> KdlNode {
3303            let mut node = KdlNode::new("web_sharing");
3304            node.push(node_value.to_owned());
3305            node
3306        };
3307        if let Some(web_sharing) = &self.web_sharing {
3308            let mut node = match web_sharing {
3309                WebSharing::On => create_node("on"),
3310                WebSharing::Off => create_node("off"),
3311                WebSharing::Disabled => create_node("disabled"),
3312            };
3313            if add_comments {
3314                node.set_leading(format!("{}\n", comment_text));
3315            }
3316            Some(node)
3317        } else if add_comments {
3318            let mut node = create_node("off");
3319            node.set_leading(format!("{}\n// ", comment_text));
3320            Some(node)
3321        } else {
3322            None
3323        }
3324    }
3325    fn web_server_cert_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
3326        let comment_text = format!(
3327            "{}\n{}\n{}",
3328            "// A path to a certificate file to be used when setting up the web client to serve the",
3329            "// connection over HTTPs",
3330            "// ",
3331        );
3332        let create_node = |node_value: &str| -> KdlNode {
3333            let mut node = KdlNode::new("web_server_cert");
3334            node.push(node_value.to_owned());
3335            node
3336        };
3337        if let Some(web_server_cert) = &self.web_server_cert {
3338            let mut node = create_node(&web_server_cert.display().to_string());
3339            if add_comments {
3340                node.set_leading(format!("{}\n", comment_text));
3341            }
3342            Some(node)
3343        } else if add_comments {
3344            let mut node = create_node("/path/to/cert.pem");
3345            node.set_leading(format!("{}\n// ", comment_text));
3346            Some(node)
3347        } else {
3348            None
3349        }
3350    }
3351    fn web_server_key_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
3352        let comment_text = format!(
3353            "{}\n{}\n{}",
3354            "// A path to a key file to be used when setting up the web client to serve the",
3355            "// connection over HTTPs",
3356            "// ",
3357        );
3358        let create_node = |node_value: &str| -> KdlNode {
3359            let mut node = KdlNode::new("web_server_key");
3360            node.push(node_value.to_owned());
3361            node
3362        };
3363        if let Some(web_server_key) = &self.web_server_key {
3364            let mut node = create_node(&web_server_key.display().to_string());
3365            if add_comments {
3366                node.set_leading(format!("{}\n", comment_text));
3367            }
3368            Some(node)
3369        } else if add_comments {
3370            let mut node = create_node("/path/to/key.pem");
3371            node.set_leading(format!("{}\n// ", comment_text));
3372            Some(node)
3373        } else {
3374            None
3375        }
3376    }
3377    fn enforce_https_for_localhost_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
3378        let comment_text = format!(
3379            "{}\n{}\n{}\n{}\n{}\n{}\n{}",
3380            "/// Whether to enforce https connections to the web server when it is bound to localhost",
3381            "/// (127.0.0.0/8)",
3382            "///",
3383            "/// Note: https is ALWAYS enforced when bound to non-local interfaces",
3384            "///",
3385            "/// Default: false",
3386            "// ",
3387        );
3388
3389        let create_node = |node_value: bool| -> KdlNode {
3390            let mut node = KdlNode::new("enforce_https_for_localhost");
3391            node.push(KdlValue::Bool(node_value));
3392            node
3393        };
3394        if let Some(enforce_https_for_localhost) = self.enforce_https_for_localhost {
3395            let mut node = create_node(enforce_https_for_localhost);
3396            if add_comments {
3397                node.set_leading(format!("{}\n", comment_text));
3398            }
3399            Some(node)
3400        } else if add_comments {
3401            let mut node = create_node(false);
3402            node.set_leading(format!("{}\n// ", comment_text));
3403            Some(node)
3404        } else {
3405            None
3406        }
3407    }
3408    fn stacked_resize_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
3409        let comment_text = format!(
3410            "{}\n{}\n{}\n{}",
3411            " ",
3412            "// Whether to stack panes when resizing beyond a certain size",
3413            "// Default: true",
3414            "// ",
3415        );
3416
3417        let create_node = |node_value: bool| -> KdlNode {
3418            let mut node = KdlNode::new("stacked_resize");
3419            node.push(KdlValue::Bool(node_value));
3420            node
3421        };
3422        if let Some(stacked_resize) = self.stacked_resize {
3423            let mut node = create_node(stacked_resize);
3424            if add_comments {
3425                node.set_leading(format!("{}\n", comment_text));
3426            }
3427            Some(node)
3428        } else if add_comments {
3429            let mut node = create_node(false);
3430            node.set_leading(format!("{}\n// ", comment_text));
3431            Some(node)
3432        } else {
3433            None
3434        }
3435    }
3436    fn show_startup_tips_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
3437        let comment_text = format!(
3438            "{}\n{}\n{}\n{}",
3439            " ", "// Whether to show tips on startup", "// Default: true", "// ",
3440        );
3441
3442        let create_node = |node_value: bool| -> KdlNode {
3443            let mut node = KdlNode::new("show_startup_tips");
3444            node.push(KdlValue::Bool(node_value));
3445            node
3446        };
3447        if let Some(show_startup_tips) = self.show_startup_tips {
3448            let mut node = create_node(show_startup_tips);
3449            if add_comments {
3450                node.set_leading(format!("{}\n", comment_text));
3451            }
3452            Some(node)
3453        } else if add_comments {
3454            let mut node = create_node(false);
3455            node.set_leading(format!("{}\n// ", comment_text));
3456            Some(node)
3457        } else {
3458            None
3459        }
3460    }
3461    fn show_release_notes_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
3462        let comment_text = format!(
3463            "{}\n{}\n{}\n{}",
3464            " ", "// Whether to show release notes on first version run", "// Default: true", "// ",
3465        );
3466
3467        let create_node = |node_value: bool| -> KdlNode {
3468            let mut node = KdlNode::new("show_release_notes");
3469            node.push(KdlValue::Bool(node_value));
3470            node
3471        };
3472        if let Some(show_release_notes) = self.show_release_notes {
3473            let mut node = create_node(show_release_notes);
3474            if add_comments {
3475                node.set_leading(format!("{}\n", comment_text));
3476            }
3477            Some(node)
3478        } else if add_comments {
3479            let mut node = create_node(false);
3480            node.set_leading(format!("{}\n// ", comment_text));
3481            Some(node)
3482        } else {
3483            None
3484        }
3485    }
3486    fn advanced_mouse_actions_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
3487        let comment_text = format!(
3488            "{}\n{}\n{}",
3489            " ",
3490            "// Whether to enable mouse hover effects and pane grouping functionality",
3491            "// default is true",
3492        );
3493
3494        let create_node = |node_value: bool| -> KdlNode {
3495            let mut node = KdlNode::new("advanced_mouse_actions");
3496            node.push(KdlValue::Bool(node_value));
3497            node
3498        };
3499        if let Some(advanced_mouse_actions) = self.advanced_mouse_actions {
3500            let mut node = create_node(advanced_mouse_actions);
3501            if add_comments {
3502                node.set_leading(format!("{}\n", comment_text));
3503            }
3504            Some(node)
3505        } else if add_comments {
3506            let mut node = create_node(false);
3507            node.set_leading(format!("{}\n// ", comment_text));
3508            Some(node)
3509        } else {
3510            None
3511        }
3512    }
3513    fn web_server_ip_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
3514        let comment_text = format!(
3515            "{}\n{}\n{}\n{}",
3516            " ",
3517            "// The ip address the web server should listen on when it starts",
3518            "// Default: \"127.0.0.1\"",
3519            "// (Requires restart)",
3520        );
3521
3522        let create_node = |node_value: IpAddr| -> KdlNode {
3523            let mut node = KdlNode::new("web_server_ip");
3524            node.push(KdlValue::String(node_value.to_string()));
3525            node
3526        };
3527        if let Some(web_server_ip) = self.web_server_ip {
3528            let mut node = create_node(web_server_ip);
3529            if add_comments {
3530                node.set_leading(format!("{}\n", comment_text));
3531            }
3532            Some(node)
3533        } else if add_comments {
3534            let mut node = create_node(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)));
3535            node.set_leading(format!("{}\n// ", comment_text));
3536            Some(node)
3537        } else {
3538            None
3539        }
3540    }
3541    fn web_server_port_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
3542        let comment_text = format!(
3543            "{}\n{}\n{}\n{}",
3544            " ",
3545            "// The port the web server should listen on when it starts",
3546            "// Default: 8082",
3547            "// (Requires restart)",
3548        );
3549
3550        let create_node = |node_value: u16| -> KdlNode {
3551            let mut node = KdlNode::new("web_server_port");
3552            node.push(KdlValue::Base10(node_value as i64));
3553            node
3554        };
3555        if let Some(web_server_port) = self.web_server_port {
3556            let mut node = create_node(web_server_port);
3557            if add_comments {
3558                node.set_leading(format!("{}\n", comment_text));
3559            }
3560            Some(node)
3561        } else if add_comments {
3562            let mut node = create_node(8082);
3563            node.set_leading(format!("{}\n// ", comment_text));
3564            Some(node)
3565        } else {
3566            None
3567        }
3568    }
3569    fn post_command_discovery_hook_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
3570        let comment_text = format!(
3571            "{}\n{}\n{}\n{}\n{}\n{}",
3572            " ",
3573            "// A command to run (will be wrapped with sh -c and provided the RESURRECT_COMMAND env variable) ",
3574            "// after Zellij attempts to discover a command inside a pane when resurrecting sessions, the STDOUT",
3575            "// of this command will be used instead of the discovered RESURRECT_COMMAND",
3576            "// can be useful for removing wrappers around commands",
3577            "// Note: be sure to escape backslashes and similar characters properly",
3578        );
3579
3580        let create_node = |node_value: &str| -> KdlNode {
3581            let mut node = KdlNode::new("post_command_discovery_hook");
3582            node.push(node_value.to_owned());
3583            node
3584        };
3585        if let Some(post_command_discovery_hook) = &self.post_command_discovery_hook {
3586            let mut node = create_node(&post_command_discovery_hook);
3587            if add_comments {
3588                node.set_leading(format!("{}\n", comment_text));
3589            }
3590            Some(node)
3591        } else if add_comments {
3592            let mut node = create_node("echo $RESURRECT_COMMAND | sed <your_regex_here>");
3593            node.set_leading(format!("{}\n// ", comment_text));
3594            Some(node)
3595        } else {
3596            None
3597        }
3598    }
3599    pub fn to_kdl(&self, add_comments: bool) -> Vec<KdlNode> {
3600        let mut nodes = vec![];
3601        if let Some(simplified_ui_node) = self.simplified_ui_to_kdl(add_comments) {
3602            nodes.push(simplified_ui_node);
3603        }
3604        if let Some(theme_node) = self.theme_to_kdl(add_comments) {
3605            nodes.push(theme_node);
3606        }
3607        if let Some(default_mode) = self.default_mode_to_kdl(add_comments) {
3608            nodes.push(default_mode);
3609        }
3610        if let Some(default_shell) = self.default_shell_to_kdl(add_comments) {
3611            nodes.push(default_shell);
3612        }
3613        if let Some(default_cwd) = self.default_cwd_to_kdl(add_comments) {
3614            nodes.push(default_cwd);
3615        }
3616        if let Some(default_layout) = self.default_layout_to_kdl(add_comments) {
3617            nodes.push(default_layout);
3618        }
3619        if let Some(layout_dir) = self.layout_dir_to_kdl(add_comments) {
3620            nodes.push(layout_dir);
3621        }
3622        if let Some(theme_dir) = self.theme_dir_to_kdl(add_comments) {
3623            nodes.push(theme_dir);
3624        }
3625        if let Some(mouse_mode) = self.mouse_mode_to_kdl(add_comments) {
3626            nodes.push(mouse_mode);
3627        }
3628        if let Some(pane_frames) = self.pane_frames_to_kdl(add_comments) {
3629            nodes.push(pane_frames);
3630        }
3631        if let Some(mirror_session) = self.mirror_session_to_kdl(add_comments) {
3632            nodes.push(mirror_session);
3633        }
3634        if let Some(on_force_close) = self.on_force_close_to_kdl(add_comments) {
3635            nodes.push(on_force_close);
3636        }
3637        if let Some(scroll_buffer_size) = self.scroll_buffer_size_to_kdl(add_comments) {
3638            nodes.push(scroll_buffer_size);
3639        }
3640        if let Some(copy_command) = self.copy_command_to_kdl(add_comments) {
3641            nodes.push(copy_command);
3642        }
3643        if let Some(copy_clipboard) = self.copy_clipboard_to_kdl(add_comments) {
3644            nodes.push(copy_clipboard);
3645        }
3646        if let Some(copy_on_select) = self.copy_on_select_to_kdl(add_comments) {
3647            nodes.push(copy_on_select);
3648        }
3649        if let Some(scrollback_editor) = self.scrollback_editor_to_kdl(add_comments) {
3650            nodes.push(scrollback_editor);
3651        }
3652        if let Some(session_name) = self.session_name_to_kdl(add_comments) {
3653            nodes.push(session_name);
3654        }
3655        if let Some(attach_to_session) = self.attach_to_session_to_kdl(add_comments) {
3656            nodes.push(attach_to_session);
3657        }
3658        if let Some(auto_layout) = self.auto_layout_to_kdl(add_comments) {
3659            nodes.push(auto_layout);
3660        }
3661        if let Some(session_serialization) = self.session_serialization_to_kdl(add_comments) {
3662            nodes.push(session_serialization);
3663        }
3664        if let Some(serialize_pane_viewport) = self.serialize_pane_viewport_to_kdl(add_comments) {
3665            nodes.push(serialize_pane_viewport);
3666        }
3667        if let Some(scrollback_lines_to_serialize) =
3668            self.scrollback_lines_to_serialize_to_kdl(add_comments)
3669        {
3670            nodes.push(scrollback_lines_to_serialize);
3671        }
3672        if let Some(styled_underlines) = self.styled_underlines_to_kdl(add_comments) {
3673            nodes.push(styled_underlines);
3674        }
3675        if let Some(serialization_interval) = self.serialization_interval_to_kdl(add_comments) {
3676            nodes.push(serialization_interval);
3677        }
3678        if let Some(disable_session_metadata) = self.disable_session_metadata_to_kdl(add_comments) {
3679            nodes.push(disable_session_metadata);
3680        }
3681        if let Some(support_kitty_keyboard_protocol) =
3682            self.support_kitty_keyboard_protocol_to_kdl(add_comments)
3683        {
3684            nodes.push(support_kitty_keyboard_protocol);
3685        }
3686        if let Some(web_server) = self.web_server_to_kdl(add_comments) {
3687            nodes.push(web_server);
3688        }
3689        if let Some(web_sharing) = self.web_sharing_to_kdl(add_comments) {
3690            nodes.push(web_sharing);
3691        }
3692        if let Some(web_server_cert) = self.web_server_cert_to_kdl(add_comments) {
3693            nodes.push(web_server_cert);
3694        }
3695        if let Some(web_server_key) = self.web_server_key_to_kdl(add_comments) {
3696            nodes.push(web_server_key);
3697        }
3698        if let Some(enforce_https_for_localhost) =
3699            self.enforce_https_for_localhost_to_kdl(add_comments)
3700        {
3701            nodes.push(enforce_https_for_localhost);
3702        }
3703        if let Some(stacked_resize) = self.stacked_resize_to_kdl(add_comments) {
3704            nodes.push(stacked_resize);
3705        }
3706        if let Some(show_startup_tips) = self.show_startup_tips_to_kdl(add_comments) {
3707            nodes.push(show_startup_tips);
3708        }
3709        if let Some(show_release_notes) = self.show_release_notes_to_kdl(add_comments) {
3710            nodes.push(show_release_notes);
3711        }
3712        if let Some(advanced_mouse_actions) = self.advanced_mouse_actions_to_kdl(add_comments) {
3713            nodes.push(advanced_mouse_actions);
3714        }
3715        if let Some(web_server_ip) = self.web_server_ip_to_kdl(add_comments) {
3716            nodes.push(web_server_ip);
3717        }
3718        if let Some(web_server_port) = self.web_server_port_to_kdl(add_comments) {
3719            nodes.push(web_server_port);
3720        }
3721        if let Some(post_command_discovery_hook) =
3722            self.post_command_discovery_hook_to_kdl(add_comments)
3723        {
3724            nodes.push(post_command_discovery_hook);
3725        }
3726        nodes
3727    }
3728}
3729
3730impl Layout {
3731    pub fn from_kdl(
3732        raw_layout: &str,
3733        file_name: Option<String>,
3734        raw_swap_layouts: Option<(&str, &str)>, // raw_swap_layouts swap_layouts_file_name
3735        cwd: Option<PathBuf>,
3736    ) -> Result<Self, ConfigError> {
3737        let mut kdl_layout_parser = KdlLayoutParser::new(raw_layout, cwd, file_name.clone());
3738        let layout = kdl_layout_parser.parse().map_err(|e| match e {
3739            ConfigError::KdlError(kdl_error) => ConfigError::KdlError(kdl_error.add_src(
3740                file_name.unwrap_or_else(|| "N/A".to_owned()),
3741                String::from(raw_layout),
3742            )),
3743            ConfigError::KdlDeserializationError(kdl_error) => kdl_layout_error(
3744                kdl_error,
3745                file_name.unwrap_or_else(|| "N/A".to_owned()),
3746                raw_layout,
3747            ),
3748            e => e,
3749        })?;
3750        match raw_swap_layouts {
3751            Some((raw_swap_layout_filename, raw_swap_layout)) => {
3752                // here we use the same parser to parse the swap layout so that we can reuse assets
3753                // (eg. pane and tab templates)
3754                kdl_layout_parser
3755                    .parse_external_swap_layouts(raw_swap_layout, layout)
3756                    .map_err(|e| match e {
3757                        ConfigError::KdlError(kdl_error) => {
3758                            ConfigError::KdlError(kdl_error.add_src(
3759                                String::from(raw_swap_layout_filename),
3760                                String::from(raw_swap_layout),
3761                            ))
3762                        },
3763                        ConfigError::KdlDeserializationError(kdl_error) => kdl_layout_error(
3764                            kdl_error,
3765                            raw_swap_layout_filename.into(),
3766                            raw_swap_layout,
3767                        ),
3768                        e => e,
3769                    })
3770            },
3771            None => Ok(layout),
3772        }
3773    }
3774}
3775
3776fn kdl_layout_error(kdl_error: kdl::KdlError, file_name: String, raw_layout: &str) -> ConfigError {
3777    let error_message = match kdl_error.kind {
3778        kdl::KdlErrorKind::Context("valid node terminator") => {
3779            format!("Failed to deserialize KDL node. \nPossible reasons:\n{}\n{}\n{}\n{}",
3780            "- Missing `;` after a node name, eg. { node; another_node; }",
3781            "- Missing quotations (\") around an argument node eg. { first_node \"argument_node\"; }",
3782            "- Missing an equal sign (=) between node arguments on a title line. eg. argument=\"value\"",
3783            "- Found an extraneous equal sign (=) between node child arguments and their values. eg. { argument=\"value\" }")
3784        },
3785        _ => String::from(kdl_error.help.unwrap_or("Kdl Deserialization Error")),
3786    };
3787    let kdl_error = KdlError {
3788        error_message,
3789        src: Some(NamedSource::new(file_name, String::from(raw_layout))),
3790        offset: Some(kdl_error.span.offset()),
3791        len: Some(kdl_error.span.len()),
3792        help_message: None,
3793    };
3794    ConfigError::KdlError(kdl_error)
3795}
3796
3797impl EnvironmentVariables {
3798    pub fn from_kdl(kdl_env_variables: &KdlNode) -> Result<Self, ConfigError> {
3799        let mut env: HashMap<String, String> = HashMap::new();
3800        for env_var in kdl_children_nodes_or_error!(kdl_env_variables, "empty env variable block") {
3801            let env_var_name = kdl_name!(env_var);
3802            let env_var_str_value =
3803                kdl_first_entry_as_string!(env_var).map(|s| format!("{}", s.to_string()));
3804            let env_var_int_value =
3805                kdl_first_entry_as_i64!(env_var).map(|s| format!("{}", s.to_string()));
3806            let env_var_value =
3807                env_var_str_value
3808                    .or(env_var_int_value)
3809                    .ok_or(ConfigError::new_kdl_error(
3810                        format!("Failed to parse env var: {:?}", env_var_name),
3811                        env_var.span().offset(),
3812                        env_var.span().len(),
3813                    ))?;
3814            env.insert(env_var_name.into(), env_var_value);
3815        }
3816        Ok(EnvironmentVariables::from_data(env))
3817    }
3818    pub fn to_kdl(&self) -> Option<KdlNode> {
3819        let mut has_env_vars = false;
3820        let mut env = KdlNode::new("env");
3821        let mut env_vars = KdlDocument::new();
3822
3823        let mut stable_sorted = BTreeMap::new();
3824        for (env_var_name, env_var_value) in self.inner() {
3825            stable_sorted.insert(env_var_name, env_var_value);
3826        }
3827        for (env_key, env_value) in stable_sorted {
3828            has_env_vars = true;
3829            let mut variable_key = KdlNode::new(env_key.to_owned());
3830            variable_key.push(env_value.to_owned());
3831            env_vars.nodes_mut().push(variable_key);
3832        }
3833
3834        if has_env_vars {
3835            env.set_children(env_vars);
3836            Some(env)
3837        } else {
3838            None
3839        }
3840    }
3841}
3842
3843impl Keybinds {
3844    fn bind_keys_in_block(
3845        block: &KdlNode,
3846        input_mode_keybinds: &mut HashMap<KeyWithModifier, Vec<Action>>,
3847        config_options: &Options,
3848    ) -> Result<(), ConfigError> {
3849        let all_nodes = kdl_children_nodes_or_error!(block, "no keybinding block for mode");
3850        let bind_nodes = all_nodes.iter().filter(|n| kdl_name!(n) == "bind");
3851        let unbind_nodes = all_nodes.iter().filter(|n| kdl_name!(n) == "unbind");
3852        for key_block in bind_nodes {
3853            Keybinds::bind_actions_for_each_key(key_block, input_mode_keybinds, config_options)?;
3854        }
3855        // we loop a second time so that the unbinds always happen after the binds
3856        for key_block in unbind_nodes {
3857            Keybinds::unbind_keys(key_block, input_mode_keybinds)?;
3858        }
3859        for key_block in all_nodes {
3860            if kdl_name!(key_block) != "bind" && kdl_name!(key_block) != "unbind" {
3861                return Err(ConfigError::new_kdl_error(
3862                    format!("Unknown keybind instruction: '{}'", kdl_name!(key_block)),
3863                    key_block.span().offset(),
3864                    key_block.span().len(),
3865                ));
3866            }
3867        }
3868        Ok(())
3869    }
3870    pub fn from_kdl(
3871        kdl_keybinds: &KdlNode,
3872        base_keybinds: Keybinds,
3873        config_options: &Options,
3874    ) -> Result<Self, ConfigError> {
3875        let clear_defaults = kdl_arg_is_truthy!(kdl_keybinds, "clear-defaults");
3876        let mut keybinds_from_config = if clear_defaults {
3877            Keybinds::default()
3878        } else {
3879            base_keybinds
3880        };
3881        for block in kdl_children_nodes_or_error!(kdl_keybinds, "keybindings with no children") {
3882            if kdl_name!(block) == "shared_except" || kdl_name!(block) == "shared" {
3883                let mut modes_to_exclude = vec![];
3884                for mode_name in kdl_string_arguments!(block) {
3885                    modes_to_exclude.push(InputMode::from_str(mode_name).map_err(|_| {
3886                        ConfigError::new_kdl_error(
3887                            format!("Invalid mode: '{}'", mode_name),
3888                            block.name().span().offset(),
3889                            block.name().span().len(),
3890                        )
3891                    })?);
3892                }
3893                for mode in InputMode::iter() {
3894                    if modes_to_exclude.contains(&mode) {
3895                        continue;
3896                    }
3897                    let mut input_mode_keybinds = keybinds_from_config.get_input_mode_mut(&mode);
3898                    Keybinds::bind_keys_in_block(block, &mut input_mode_keybinds, config_options)?;
3899                }
3900            }
3901            if kdl_name!(block) == "shared_among" {
3902                let mut modes_to_include = vec![];
3903                for mode_name in kdl_string_arguments!(block) {
3904                    modes_to_include.push(InputMode::from_str(mode_name)?);
3905                }
3906                for mode in InputMode::iter() {
3907                    if !modes_to_include.contains(&mode) {
3908                        continue;
3909                    }
3910                    let mut input_mode_keybinds = keybinds_from_config.get_input_mode_mut(&mode);
3911                    Keybinds::bind_keys_in_block(block, &mut input_mode_keybinds, config_options)?;
3912                }
3913            }
3914        }
3915        for mode in kdl_children_nodes_or_error!(kdl_keybinds, "keybindings with no children") {
3916            if kdl_name!(mode) == "unbind"
3917                || kdl_name!(mode) == "shared_except"
3918                || kdl_name!(mode) == "shared_among"
3919                || kdl_name!(mode) == "shared"
3920            {
3921                continue;
3922            }
3923            let mut input_mode_keybinds =
3924                Keybinds::input_mode_keybindings(mode, &mut keybinds_from_config)?;
3925            Keybinds::bind_keys_in_block(mode, &mut input_mode_keybinds, config_options)?;
3926        }
3927        if let Some(global_unbind) = kdl_keybinds.children().and_then(|c| c.get("unbind")) {
3928            Keybinds::unbind_keys_in_all_modes(global_unbind, &mut keybinds_from_config)?;
3929        };
3930        Ok(keybinds_from_config)
3931    }
3932    fn bind_actions_for_each_key(
3933        key_block: &KdlNode,
3934        input_mode_keybinds: &mut HashMap<KeyWithModifier, Vec<Action>>,
3935        config_options: &Options,
3936    ) -> Result<(), ConfigError> {
3937        let keys: Vec<KeyWithModifier> = keys_from_kdl!(key_block);
3938        let actions: Vec<Action> = actions_from_kdl!(key_block, config_options);
3939        for key in keys {
3940            input_mode_keybinds.insert(key, actions.clone());
3941        }
3942        Ok(())
3943    }
3944    fn unbind_keys(
3945        key_block: &KdlNode,
3946        input_mode_keybinds: &mut HashMap<KeyWithModifier, Vec<Action>>,
3947    ) -> Result<(), ConfigError> {
3948        let keys: Vec<KeyWithModifier> = keys_from_kdl!(key_block);
3949        for key in keys {
3950            input_mode_keybinds.remove(&key);
3951        }
3952        Ok(())
3953    }
3954    fn unbind_keys_in_all_modes(
3955        global_unbind: &KdlNode,
3956        keybinds_from_config: &mut Keybinds,
3957    ) -> Result<(), ConfigError> {
3958        let keys: Vec<KeyWithModifier> = keys_from_kdl!(global_unbind);
3959        for mode in keybinds_from_config.0.values_mut() {
3960            for key in &keys {
3961                mode.remove(&key);
3962            }
3963        }
3964        Ok(())
3965    }
3966    fn input_mode_keybindings<'a>(
3967        mode: &KdlNode,
3968        keybinds_from_config: &'a mut Keybinds,
3969    ) -> Result<&'a mut HashMap<KeyWithModifier, Vec<Action>>, ConfigError> {
3970        let mode_name = kdl_name!(mode);
3971        let input_mode = InputMode::from_str(mode_name).map_err(|_| {
3972            ConfigError::new_kdl_error(
3973                format!("Invalid mode: '{}'", mode_name),
3974                mode.name().span().offset(),
3975                mode.name().span().len(),
3976            )
3977        })?;
3978        let input_mode_keybinds = keybinds_from_config.get_input_mode_mut(&input_mode);
3979        let clear_defaults_for_mode = kdl_arg_is_truthy!(mode, "clear-defaults");
3980        if clear_defaults_for_mode {
3981            input_mode_keybinds.clear();
3982        }
3983        Ok(input_mode_keybinds)
3984    }
3985    pub fn from_string(
3986        stringified_keybindings: String,
3987        base_keybinds: Keybinds,
3988        config_options: &Options,
3989    ) -> Result<Self, ConfigError> {
3990        let document: KdlDocument = stringified_keybindings.parse()?;
3991        if let Some(kdl_keybinds) = document.get("keybinds") {
3992            Keybinds::from_kdl(&kdl_keybinds, base_keybinds, config_options)
3993        } else {
3994            Err(ConfigError::new_kdl_error(
3995                format!("Could not find keybinds node"),
3996                document.span().offset(),
3997                document.span().len(),
3998            ))
3999        }
4000    }
4001    // minimize keybind entries for serialization, so that duplicate entries will appear in
4002    // "shared" nodes later rather than once per mode
4003    fn minimize_entries(
4004        &self,
4005    ) -> BTreeMap<BTreeSet<InputMode>, BTreeMap<KeyWithModifier, Vec<Action>>> {
4006        let mut minimized: BTreeMap<BTreeSet<InputMode>, BTreeMap<KeyWithModifier, Vec<Action>>> =
4007            BTreeMap::new();
4008        let mut flattened: Vec<BTreeMap<KeyWithModifier, Vec<Action>>> = self
4009            .0
4010            .iter()
4011            .map(|(_input_mode, keybind)| keybind.clone().into_iter().collect())
4012            .collect();
4013        for keybind in flattened.drain(..) {
4014            for (key, actions) in keybind.into_iter() {
4015                let mut appears_in_modes: BTreeSet<InputMode> = BTreeSet::new();
4016                for (input_mode, keybinds) in self.0.iter() {
4017                    if keybinds.get(&key) == Some(&actions) {
4018                        appears_in_modes.insert(*input_mode);
4019                    }
4020                }
4021                minimized
4022                    .entry(appears_in_modes)
4023                    .or_insert_with(Default::default)
4024                    .insert(key, actions);
4025            }
4026        }
4027        minimized
4028    }
4029    fn serialize_mode_title_node(&self, input_modes: &BTreeSet<InputMode>) -> KdlNode {
4030        let all_modes: Vec<InputMode> = InputMode::iter().collect();
4031        let total_input_mode_count = all_modes.len();
4032        if input_modes.len() == 1 {
4033            let input_mode_name =
4034                format!("{:?}", input_modes.iter().next().unwrap()).to_lowercase();
4035            KdlNode::new(input_mode_name)
4036        } else if input_modes.len() == total_input_mode_count {
4037            KdlNode::new("shared")
4038        } else if input_modes.len() < total_input_mode_count / 2 {
4039            let mut node = KdlNode::new("shared_among");
4040            for input_mode in input_modes {
4041                node.push(format!("{:?}", input_mode).to_lowercase());
4042            }
4043            node
4044        } else {
4045            let mut node = KdlNode::new("shared_except");
4046            let mut modes = all_modes.clone();
4047            for input_mode in input_modes {
4048                modes.retain(|m| m != input_mode)
4049            }
4050            for mode in modes {
4051                node.push(format!("{:?}", mode).to_lowercase());
4052            }
4053            node
4054        }
4055    }
4056    fn serialize_mode_keybinds(
4057        &self,
4058        keybinds: &BTreeMap<KeyWithModifier, Vec<Action>>,
4059    ) -> KdlDocument {
4060        let mut mode_keybinds = KdlDocument::new();
4061        for keybind in keybinds {
4062            let mut keybind_node = KdlNode::new("bind");
4063            keybind_node.push(keybind.0.to_kdl());
4064            let mut actions = KdlDocument::new();
4065            let mut actions_have_children = false;
4066            for action in keybind.1 {
4067                if let Some(kdl_action) = action.to_kdl() {
4068                    if kdl_action.children().is_some() {
4069                        actions_have_children = true;
4070                    }
4071                    actions.nodes_mut().push(kdl_action);
4072                }
4073            }
4074            if !actions_have_children {
4075                for action in actions.nodes_mut() {
4076                    action.set_leading("");
4077                    action.set_trailing("; ");
4078                }
4079                actions.set_leading(" ");
4080                actions.set_trailing("");
4081            }
4082            keybind_node.set_children(actions);
4083            mode_keybinds.nodes_mut().push(keybind_node);
4084        }
4085        mode_keybinds
4086    }
4087    pub fn to_kdl(&self, should_clear_defaults: bool) -> KdlNode {
4088        let mut keybinds_node = KdlNode::new("keybinds");
4089        if should_clear_defaults {
4090            keybinds_node.insert("clear-defaults", true);
4091        }
4092        let mut minimized = self.minimize_entries();
4093        let mut keybinds_children = KdlDocument::new();
4094
4095        macro_rules! encode_single_input_mode {
4096            ($mode_name:ident) => {{
4097                if let Some(keybinds) = minimized.remove(&BTreeSet::from([InputMode::$mode_name])) {
4098                    let mut mode_node =
4099                        KdlNode::new(format!("{:?}", InputMode::$mode_name).to_lowercase());
4100                    let mode_keybinds = self.serialize_mode_keybinds(&keybinds);
4101                    mode_node.set_children(mode_keybinds);
4102                    keybinds_children.nodes_mut().push(mode_node);
4103                }
4104            }};
4105        }
4106        // we do this explicitly so that the sorting order of modes in the config is more Human
4107        // readable - this is actually less code (and clearer) than implementing Ord in this case
4108        encode_single_input_mode!(Normal);
4109        encode_single_input_mode!(Locked);
4110        encode_single_input_mode!(Pane);
4111        encode_single_input_mode!(Tab);
4112        encode_single_input_mode!(Resize);
4113        encode_single_input_mode!(Move);
4114        encode_single_input_mode!(Scroll);
4115        encode_single_input_mode!(Search);
4116        encode_single_input_mode!(Session);
4117
4118        for (input_modes, keybinds) in minimized {
4119            if input_modes.is_empty() {
4120                log::error!("invalid input mode for keybinds: {:#?}", keybinds);
4121                continue;
4122            }
4123            let mut mode_node = self.serialize_mode_title_node(&input_modes);
4124            let mode_keybinds = self.serialize_mode_keybinds(&keybinds);
4125            mode_node.set_children(mode_keybinds);
4126            keybinds_children.nodes_mut().push(mode_node);
4127        }
4128        keybinds_node.set_children(keybinds_children);
4129        keybinds_node
4130    }
4131}
4132
4133impl KeyWithModifier {
4134    pub fn to_kdl(&self) -> String {
4135        if self.key_modifiers.is_empty() {
4136            self.bare_key.to_kdl()
4137        } else {
4138            format!(
4139                "{} {}",
4140                self.key_modifiers
4141                    .iter()
4142                    .map(|m| m.to_string())
4143                    .collect::<Vec<_>>()
4144                    .join(" "),
4145                self.bare_key.to_kdl()
4146            )
4147        }
4148    }
4149}
4150
4151impl BareKey {
4152    pub fn to_kdl(&self) -> String {
4153        match self {
4154            BareKey::PageDown => format!("PageDown"),
4155            BareKey::PageUp => format!("PageUp"),
4156            BareKey::Left => format!("left"),
4157            BareKey::Down => format!("down"),
4158            BareKey::Up => format!("up"),
4159            BareKey::Right => format!("right"),
4160            BareKey::Home => format!("home"),
4161            BareKey::End => format!("end"),
4162            BareKey::Backspace => format!("backspace"),
4163            BareKey::Delete => format!("del"),
4164            BareKey::Insert => format!("insert"),
4165            BareKey::F(index) => format!("F{}", index),
4166            BareKey::Char(' ') => format!("space"),
4167            BareKey::Char(character) => format!("{}", character),
4168            BareKey::Tab => format!("tab"),
4169            BareKey::Esc => format!("esc"),
4170            BareKey::Enter => format!("enter"),
4171            BareKey::CapsLock => format!("capslock"),
4172            BareKey::ScrollLock => format!("scrolllock"),
4173            BareKey::NumLock => format!("numlock"),
4174            BareKey::PrintScreen => format!("printscreen"),
4175            BareKey::Pause => format!("pause"),
4176            BareKey::Menu => format!("menu"),
4177        }
4178    }
4179}
4180
4181impl Config {
4182    pub fn from_kdl(kdl_config: &str, base_config: Option<Config>) -> Result<Config, ConfigError> {
4183        let mut config = base_config.unwrap_or_else(|| Config::default());
4184        let kdl_config: KdlDocument = kdl_config.parse()?;
4185
4186        let config_options = Options::from_kdl(&kdl_config)?;
4187        config.options = config.options.merge(config_options);
4188
4189        // TODO: handle cases where we have more than one of these blocks (eg. two "keybinds")
4190        // this should give an informative parsing error
4191        if let Some(kdl_keybinds) = kdl_config.get("keybinds") {
4192            config.keybinds = Keybinds::from_kdl(&kdl_keybinds, config.keybinds, &config.options)?;
4193        }
4194        if let Some(kdl_themes) = kdl_config.get("themes") {
4195            let sourced_from_external_file = false;
4196            let config_themes = Themes::from_kdl(kdl_themes, sourced_from_external_file)?;
4197            config.themes = config.themes.merge(config_themes);
4198        }
4199        if let Some(kdl_plugin_aliases) = kdl_config.get("plugins") {
4200            let config_plugins = PluginAliases::from_kdl(kdl_plugin_aliases)?;
4201            config.plugins.merge(config_plugins);
4202        }
4203        if let Some(kdl_load_plugins) = kdl_config.get("load_plugins") {
4204            let load_plugins = load_plugins_from_kdl(kdl_load_plugins)?;
4205            config.background_plugins = load_plugins;
4206        }
4207        if let Some(kdl_ui_config) = kdl_config.get("ui") {
4208            let config_ui = UiConfig::from_kdl(&kdl_ui_config)?;
4209            config.ui = config.ui.merge(config_ui);
4210        }
4211        if let Some(env_config) = kdl_config.get("env") {
4212            let config_env = EnvironmentVariables::from_kdl(&env_config)?;
4213            config.env = config.env.merge(config_env);
4214        }
4215        if let Some(web_client_config) = kdl_config.get("web_client") {
4216            let config_web_client = WebClientConfig::from_kdl(&web_client_config)?;
4217            config.web_client = config.web_client.merge(config_web_client);
4218        }
4219        Ok(config)
4220    }
4221    pub fn to_string(&self, add_comments: bool) -> String {
4222        let mut document = KdlDocument::new();
4223
4224        let clear_defaults = true;
4225        let keybinds = self.keybinds.to_kdl(clear_defaults);
4226        document.nodes_mut().push(keybinds);
4227
4228        if let Some(themes) = self.themes.to_kdl() {
4229            document.nodes_mut().push(themes);
4230        }
4231
4232        let plugins = self.plugins.to_kdl(add_comments);
4233        document.nodes_mut().push(plugins);
4234
4235        let load_plugins = load_plugins_to_kdl(&self.background_plugins, add_comments);
4236        document.nodes_mut().push(load_plugins);
4237
4238        if let Some(ui_config) = self.ui.to_kdl() {
4239            document.nodes_mut().push(ui_config);
4240        }
4241
4242        if let Some(env) = self.env.to_kdl() {
4243            document.nodes_mut().push(env);
4244        }
4245
4246        document.nodes_mut().push(self.web_client.to_kdl());
4247
4248        document
4249            .nodes_mut()
4250            .append(&mut self.options.to_kdl(add_comments));
4251
4252        document.to_string()
4253    }
4254}
4255
4256impl PluginAliases {
4257    pub fn from_kdl(kdl_plugin_aliases: &KdlNode) -> Result<PluginAliases, ConfigError> {
4258        let mut aliases: BTreeMap<String, RunPlugin> = BTreeMap::new();
4259        if let Some(kdl_plugin_aliases) = kdl_children_nodes!(kdl_plugin_aliases) {
4260            for alias_definition in kdl_plugin_aliases {
4261                let alias_name = kdl_name!(alias_definition);
4262                if let Some(string_url) =
4263                    kdl_get_string_property_or_child_value!(alias_definition, "location")
4264                {
4265                    let configuration =
4266                        KdlLayoutParser::parse_plugin_user_configuration(&alias_definition)?;
4267                    let initial_cwd =
4268                        kdl_get_string_property_or_child_value!(alias_definition, "cwd")
4269                            .map(|s| PathBuf::from(s));
4270                    let run_plugin = RunPlugin::from_url(string_url)?
4271                        .with_configuration(configuration.inner().clone())
4272                        .with_initial_cwd(initial_cwd);
4273                    aliases.insert(alias_name.to_owned(), run_plugin);
4274                }
4275            }
4276        }
4277        Ok(PluginAliases { aliases })
4278    }
4279    pub fn to_kdl(&self, add_comments: bool) -> KdlNode {
4280        let mut plugins = KdlNode::new("plugins");
4281        let mut plugins_children = KdlDocument::new();
4282        for (alias_name, plugin_alias) in self.aliases.iter() {
4283            let mut plugin_alias_node = KdlNode::new(alias_name.clone());
4284            let mut plugin_alias_children = KdlDocument::new();
4285            let location_string = plugin_alias.location.display();
4286
4287            plugin_alias_node.insert("location", location_string);
4288            let cwd = plugin_alias.initial_cwd.as_ref();
4289            let mut has_children = false;
4290            if let Some(cwd) = cwd {
4291                has_children = true;
4292                let mut cwd_node = KdlNode::new("cwd");
4293                cwd_node.push(cwd.display().to_string());
4294                plugin_alias_children.nodes_mut().push(cwd_node);
4295            }
4296            let configuration = plugin_alias.configuration.inner();
4297            if !configuration.is_empty() {
4298                has_children = true;
4299                for (config_key, config_value) in configuration {
4300                    let mut node = KdlNode::new(config_key.to_owned());
4301                    if config_value == "true" {
4302                        node.push(KdlValue::Bool(true));
4303                    } else if config_value == "false" {
4304                        node.push(KdlValue::Bool(false));
4305                    } else {
4306                        node.push(config_value.to_string());
4307                    }
4308                    plugin_alias_children.nodes_mut().push(node);
4309                }
4310            }
4311            if has_children {
4312                plugin_alias_node.set_children(plugin_alias_children);
4313            }
4314            plugins_children.nodes_mut().push(plugin_alias_node);
4315        }
4316        plugins.set_children(plugins_children);
4317
4318        if add_comments {
4319            plugins.set_leading(format!(
4320                "\n{}\n{}\n",
4321                "// Plugin aliases - can be used to change the implementation of Zellij",
4322                "// changing these requires a restart to take effect",
4323            ));
4324        }
4325        plugins
4326    }
4327}
4328
4329pub fn load_plugins_to_kdl(
4330    background_plugins: &HashSet<RunPluginOrAlias>,
4331    add_comments: bool,
4332) -> KdlNode {
4333    let mut load_plugins = KdlNode::new("load_plugins");
4334    let mut load_plugins_children = KdlDocument::new();
4335    for run_plugin_or_alias in background_plugins.iter() {
4336        let mut background_plugin_node = KdlNode::new(run_plugin_or_alias.location_string());
4337        let mut background_plugin_children = KdlDocument::new();
4338
4339        let cwd = match run_plugin_or_alias {
4340            RunPluginOrAlias::RunPlugin(run_plugin) => run_plugin.initial_cwd.clone(),
4341            RunPluginOrAlias::Alias(plugin_alias) => plugin_alias.initial_cwd.clone(),
4342        };
4343        let mut has_children = false;
4344        if let Some(cwd) = cwd.as_ref() {
4345            has_children = true;
4346            let mut cwd_node = KdlNode::new("cwd");
4347            cwd_node.push(cwd.display().to_string());
4348            background_plugin_children.nodes_mut().push(cwd_node);
4349        }
4350        let configuration = match run_plugin_or_alias {
4351            RunPluginOrAlias::RunPlugin(run_plugin) => {
4352                Some(run_plugin.configuration.inner().clone())
4353            },
4354            RunPluginOrAlias::Alias(plugin_alias) => plugin_alias
4355                .configuration
4356                .as_ref()
4357                .map(|c| c.inner().clone()),
4358        };
4359        if let Some(configuration) = configuration {
4360            if !configuration.is_empty() {
4361                has_children = true;
4362                for (config_key, config_value) in configuration {
4363                    let mut node = KdlNode::new(config_key.to_owned());
4364                    if config_value == "true" {
4365                        node.push(KdlValue::Bool(true));
4366                    } else if config_value == "false" {
4367                        node.push(KdlValue::Bool(false));
4368                    } else {
4369                        node.push(config_value.to_string());
4370                    }
4371                    background_plugin_children.nodes_mut().push(node);
4372                }
4373            }
4374        }
4375        if has_children {
4376            background_plugin_node.set_children(background_plugin_children);
4377        }
4378        load_plugins_children
4379            .nodes_mut()
4380            .push(background_plugin_node);
4381    }
4382    load_plugins.set_children(load_plugins_children);
4383
4384    if add_comments {
4385        load_plugins.set_leading(format!(
4386            "\n{}\n{}\n{}\n",
4387            "// Plugins to load in the background when a new session starts",
4388            "// eg. \"file:/path/to/my-plugin.wasm\"",
4389            "// eg. \"https://example.com/my-plugin.wasm\"",
4390        ));
4391    }
4392    load_plugins
4393}
4394
4395fn load_plugins_from_kdl(
4396    kdl_load_plugins: &KdlNode,
4397) -> Result<HashSet<RunPluginOrAlias>, ConfigError> {
4398    let mut load_plugins: HashSet<RunPluginOrAlias> = HashSet::new();
4399    if let Some(kdl_load_plugins) = kdl_children_nodes!(kdl_load_plugins) {
4400        for plugin_block in kdl_load_plugins {
4401            let url_node = plugin_block.name();
4402            let string_url = url_node.value();
4403            let configuration = KdlLayoutParser::parse_plugin_user_configuration(&plugin_block)?;
4404            let cwd = kdl_get_string_property_or_child_value!(&plugin_block, "cwd")
4405                .map(|s| PathBuf::from(s));
4406            let run_plugin_or_alias = RunPluginOrAlias::from_url(
4407                &string_url,
4408                &Some(configuration.inner().clone()),
4409                None,
4410                cwd.clone(),
4411            )
4412            .map_err(|e| {
4413                ConfigError::new_kdl_error(
4414                    format!("Failed to parse plugin: {}", e),
4415                    url_node.span().offset(),
4416                    url_node.span().len(),
4417                )
4418            })?
4419            .with_initial_cwd(cwd);
4420            load_plugins.insert(run_plugin_or_alias);
4421        }
4422    }
4423    Ok(load_plugins)
4424}
4425
4426impl UiConfig {
4427    pub fn from_kdl(kdl_ui_config: &KdlNode) -> Result<UiConfig, ConfigError> {
4428        let mut ui_config = UiConfig::default();
4429        if let Some(pane_frames) = kdl_get_child!(kdl_ui_config, "pane_frames") {
4430            let rounded_corners =
4431                kdl_children_property_first_arg_as_bool!(pane_frames, "rounded_corners")
4432                    .unwrap_or(false);
4433            let hide_session_name =
4434                kdl_get_child_entry_bool_value!(pane_frames, "hide_session_name").unwrap_or(false);
4435            let frame_config = FrameConfig {
4436                rounded_corners,
4437                hide_session_name,
4438            };
4439            ui_config.pane_frames = frame_config;
4440        }
4441        Ok(ui_config)
4442    }
4443    pub fn to_kdl(&self) -> Option<KdlNode> {
4444        let mut ui_config = KdlNode::new("ui");
4445        let mut ui_config_children = KdlDocument::new();
4446        let mut frame_config = KdlNode::new("pane_frames");
4447        let mut frame_config_children = KdlDocument::new();
4448        let mut has_ui_config = false;
4449        if self.pane_frames.rounded_corners {
4450            has_ui_config = true;
4451            let mut rounded_corners = KdlNode::new("rounded_corners");
4452            rounded_corners.push(KdlValue::Bool(true));
4453            frame_config_children.nodes_mut().push(rounded_corners);
4454        }
4455        if self.pane_frames.hide_session_name {
4456            has_ui_config = true;
4457            let mut hide_session_name = KdlNode::new("hide_session_name");
4458            hide_session_name.push(KdlValue::Bool(true));
4459            frame_config_children.nodes_mut().push(hide_session_name);
4460        }
4461        if has_ui_config {
4462            frame_config.set_children(frame_config_children);
4463            ui_config_children.nodes_mut().push(frame_config);
4464            ui_config.set_children(ui_config_children);
4465            Some(ui_config)
4466        } else {
4467            None
4468        }
4469    }
4470}
4471
4472impl Themes {
4473    fn style_declaration_from_node(
4474        style_node: &KdlNode,
4475        style_descriptor: &str,
4476    ) -> Result<Option<StyleDeclaration>, ConfigError> {
4477        let descriptor_node = kdl_child_with_name!(style_node, style_descriptor);
4478
4479        match descriptor_node {
4480            Some(descriptor) => {
4481                let colors = kdl_children_or_error!(
4482                    descriptor,
4483                    format!("Missing colors for {}", style_descriptor)
4484                );
4485                Ok(Some(StyleDeclaration {
4486                    base: PaletteColor::try_from(("base", colors))?,
4487                    background: PaletteColor::try_from(("background", colors)).unwrap_or_default(),
4488                    emphasis_0: PaletteColor::try_from(("emphasis_0", colors))?,
4489                    emphasis_1: PaletteColor::try_from(("emphasis_1", colors))?,
4490                    emphasis_2: PaletteColor::try_from(("emphasis_2", colors))?,
4491                    emphasis_3: PaletteColor::try_from(("emphasis_3", colors))?,
4492                }))
4493            },
4494            None => Ok(None),
4495        }
4496    }
4497
4498    fn multiplayer_colors(style_node: &KdlNode) -> Result<MultiplayerColors, ConfigError> {
4499        let descriptor_node = kdl_child_with_name!(style_node, "multiplayer_user_colors");
4500        match descriptor_node {
4501            Some(descriptor) => {
4502                let colors = kdl_children_or_error!(
4503                    descriptor,
4504                    format!("Missing colors for {}", "multiplayer_user_colors")
4505                );
4506                Ok(MultiplayerColors {
4507                    player_1: PaletteColor::try_from(("player_1", colors))
4508                        .unwrap_or(DEFAULT_STYLES.multiplayer_user_colors.player_1),
4509                    player_2: PaletteColor::try_from(("player_2", colors))
4510                        .unwrap_or(DEFAULT_STYLES.multiplayer_user_colors.player_2),
4511                    player_3: PaletteColor::try_from(("player_3", colors))
4512                        .unwrap_or(DEFAULT_STYLES.multiplayer_user_colors.player_3),
4513                    player_4: PaletteColor::try_from(("player_4", colors))
4514                        .unwrap_or(DEFAULT_STYLES.multiplayer_user_colors.player_4),
4515                    player_5: PaletteColor::try_from(("player_5", colors))
4516                        .unwrap_or(DEFAULT_STYLES.multiplayer_user_colors.player_5),
4517                    player_6: PaletteColor::try_from(("player_6", colors))
4518                        .unwrap_or(DEFAULT_STYLES.multiplayer_user_colors.player_6),
4519                    player_7: PaletteColor::try_from(("player_7", colors))
4520                        .unwrap_or(DEFAULT_STYLES.multiplayer_user_colors.player_7),
4521                    player_8: PaletteColor::try_from(("player_8", colors))
4522                        .unwrap_or(DEFAULT_STYLES.multiplayer_user_colors.player_8),
4523                    player_9: PaletteColor::try_from(("player_9", colors))
4524                        .unwrap_or(DEFAULT_STYLES.multiplayer_user_colors.player_9),
4525                    player_10: PaletteColor::try_from(("player_10", colors))
4526                        .unwrap_or(DEFAULT_STYLES.multiplayer_user_colors.player_10),
4527                })
4528            },
4529            None => Ok(DEFAULT_STYLES.multiplayer_user_colors),
4530        }
4531    }
4532
4533    pub fn from_kdl(
4534        themes_from_kdl: &KdlNode,
4535        sourced_from_external_file: bool,
4536    ) -> Result<Self, ConfigError> {
4537        let mut themes: HashMap<String, Theme> = HashMap::new();
4538        for theme_config in kdl_children_nodes_or_error!(themes_from_kdl, "no themes found") {
4539            let theme_name = kdl_name!(theme_config);
4540            let theme_colors = kdl_children_or_error!(theme_config, "empty theme");
4541            let palette_color_names = HashSet::from([
4542                "fg", "bg", "red", "green", "blue", "yellow", "magenta", "orange", "cyan", "black",
4543                "white",
4544            ]);
4545            let theme = if theme_colors
4546                .nodes()
4547                .iter()
4548                .all(|n| palette_color_names.contains(n.name().value()))
4549            {
4550                // Older palette based theme definition
4551                let palette = Palette {
4552                    fg: PaletteColor::try_from(("fg", theme_colors))?,
4553                    bg: PaletteColor::try_from(("bg", theme_colors))?,
4554                    red: PaletteColor::try_from(("red", theme_colors))?,
4555                    green: PaletteColor::try_from(("green", theme_colors))?,
4556                    yellow: PaletteColor::try_from(("yellow", theme_colors))?,
4557                    blue: PaletteColor::try_from(("blue", theme_colors))?,
4558                    magenta: PaletteColor::try_from(("magenta", theme_colors))?,
4559                    orange: PaletteColor::try_from(("orange", theme_colors))?,
4560                    cyan: PaletteColor::try_from(("cyan", theme_colors))?,
4561                    black: PaletteColor::try_from(("black", theme_colors))?,
4562                    white: PaletteColor::try_from(("white", theme_colors))?,
4563                    ..Default::default()
4564                };
4565                Theme {
4566                    palette: palette.into(),
4567                    sourced_from_external_file,
4568                }
4569            } else {
4570                // Newer theme definition with named styles
4571                let s = Styling {
4572                    text_unselected: Themes::style_declaration_from_node(
4573                        theme_config,
4574                        "text_unselected",
4575                    )
4576                    .map(|maybe_style| maybe_style.unwrap_or(DEFAULT_STYLES.text_unselected))?,
4577                    text_selected: Themes::style_declaration_from_node(
4578                        theme_config,
4579                        "text_selected",
4580                    )
4581                    .map(|maybe_style| maybe_style.unwrap_or(DEFAULT_STYLES.text_selected))?,
4582                    ribbon_unselected: Themes::style_declaration_from_node(
4583                        theme_config,
4584                        "ribbon_unselected",
4585                    )
4586                    .map(|maybe_style| maybe_style.unwrap_or(DEFAULT_STYLES.ribbon_unselected))?,
4587                    ribbon_selected: Themes::style_declaration_from_node(
4588                        theme_config,
4589                        "ribbon_selected",
4590                    )
4591                    .map(|maybe_style| maybe_style.unwrap_or(DEFAULT_STYLES.ribbon_selected))?,
4592                    table_title: Themes::style_declaration_from_node(theme_config, "table_title")
4593                        .map(|maybe_style| {
4594                        maybe_style.unwrap_or(DEFAULT_STYLES.table_title)
4595                    })?,
4596                    table_cell_unselected: Themes::style_declaration_from_node(
4597                        theme_config,
4598                        "table_cell_unselected",
4599                    )
4600                    .map(|maybe_style| {
4601                        maybe_style.unwrap_or(DEFAULT_STYLES.table_cell_unselected)
4602                    })?,
4603                    table_cell_selected: Themes::style_declaration_from_node(
4604                        theme_config,
4605                        "table_cell_selected",
4606                    )
4607                    .map(|maybe_style| maybe_style.unwrap_or(DEFAULT_STYLES.table_cell_selected))?,
4608                    list_unselected: Themes::style_declaration_from_node(
4609                        theme_config,
4610                        "list_unselected",
4611                    )
4612                    .map(|maybe_style| maybe_style.unwrap_or(DEFAULT_STYLES.list_unselected))?,
4613                    list_selected: Themes::style_declaration_from_node(
4614                        theme_config,
4615                        "list_selected",
4616                    )
4617                    .map(|maybe_style| maybe_style.unwrap_or(DEFAULT_STYLES.list_selected))?,
4618                    frame_unselected: Themes::style_declaration_from_node(
4619                        theme_config,
4620                        "frame_unselected",
4621                    )?,
4622                    frame_selected: Themes::style_declaration_from_node(
4623                        theme_config,
4624                        "frame_selected",
4625                    )
4626                    .map(|maybe_style| maybe_style.unwrap_or(DEFAULT_STYLES.frame_selected))?,
4627                    frame_highlight: Themes::style_declaration_from_node(
4628                        theme_config,
4629                        "frame_highlight",
4630                    )
4631                    .map(|maybe_style| maybe_style.unwrap_or(DEFAULT_STYLES.frame_highlight))?,
4632                    exit_code_success: Themes::style_declaration_from_node(
4633                        theme_config,
4634                        "exit_code_success",
4635                    )
4636                    .map(|maybe_style| maybe_style.unwrap_or(DEFAULT_STYLES.exit_code_success))?,
4637                    exit_code_error: Themes::style_declaration_from_node(
4638                        theme_config,
4639                        "exit_code_error",
4640                    )
4641                    .map(|maybe_style| maybe_style.unwrap_or(DEFAULT_STYLES.exit_code_error))?,
4642                    multiplayer_user_colors: Themes::multiplayer_colors(theme_config)
4643                        .unwrap_or_default(),
4644                };
4645
4646                Theme {
4647                    palette: s,
4648                    sourced_from_external_file,
4649                }
4650            };
4651            themes.insert(theme_name.into(), theme);
4652        }
4653        let themes = Themes::from_data(themes);
4654        Ok(themes)
4655    }
4656
4657    pub fn from_string(
4658        raw_string: &String,
4659        sourced_from_external_file: bool,
4660    ) -> Result<Self, ConfigError> {
4661        let kdl_config: KdlDocument = raw_string.parse()?;
4662        let kdl_themes = kdl_config.get("themes").ok_or(ConfigError::new_kdl_error(
4663            "No theme node found in file".into(),
4664            kdl_config.span().offset(),
4665            kdl_config.span().len(),
4666        ))?;
4667        let all_themes_in_file = Themes::from_kdl(kdl_themes, sourced_from_external_file)?;
4668        Ok(all_themes_in_file)
4669    }
4670
4671    pub fn from_path(path_to_theme_file: PathBuf) -> Result<Self, ConfigError> {
4672        // String is the theme name
4673        let kdl_config = std::fs::read_to_string(&path_to_theme_file)
4674            .map_err(|e| ConfigError::IoPath(e, path_to_theme_file.clone()))?;
4675        let sourced_from_external_file = true;
4676        Themes::from_string(&kdl_config, sourced_from_external_file).map_err(|e| match e {
4677            ConfigError::KdlError(kdl_error) => ConfigError::KdlError(
4678                kdl_error.add_src(path_to_theme_file.display().to_string(), kdl_config),
4679            ),
4680            e => e,
4681        })
4682    }
4683
4684    pub fn from_dir(path_to_theme_dir: PathBuf) -> Result<Self, ConfigError> {
4685        let mut themes = Themes::default();
4686        for entry in std::fs::read_dir(&path_to_theme_dir)
4687            .map_err(|e| ConfigError::IoPath(e, path_to_theme_dir.clone()))?
4688        {
4689            let entry = entry.map_err(|e| ConfigError::IoPath(e, path_to_theme_dir.clone()))?;
4690            let path = entry.path();
4691            if let Some(extension) = path.extension() {
4692                if extension == "kdl" {
4693                    themes = themes.merge(Themes::from_path(path)?);
4694                }
4695            }
4696        }
4697        Ok(themes)
4698    }
4699    pub fn to_kdl(&self) -> Option<KdlNode> {
4700        let mut theme_node = KdlNode::new("themes");
4701        let mut themes = KdlDocument::new();
4702        let mut has_themes = false;
4703        let sorted_themes: BTreeMap<String, Theme> = self.inner().clone().into_iter().collect();
4704        for (theme_name, theme) in sorted_themes {
4705            if theme.sourced_from_external_file {
4706                // we do not serialize themes that have been defined in external files so as not to
4707                // clog up the configuration file definitions
4708                continue;
4709            }
4710            has_themes = true;
4711            let mut current_theme_node = KdlNode::new(theme_name.clone());
4712            let mut current_theme_node_children = KdlDocument::new();
4713
4714            current_theme_node_children
4715                .nodes_mut()
4716                .push(theme.palette.text_unselected.to_kdl("text_unselected"));
4717            current_theme_node_children
4718                .nodes_mut()
4719                .push(theme.palette.text_selected.to_kdl("text_selected"));
4720            current_theme_node_children
4721                .nodes_mut()
4722                .push(theme.palette.ribbon_selected.to_kdl("ribbon_selected"));
4723            current_theme_node_children
4724                .nodes_mut()
4725                .push(theme.palette.ribbon_unselected.to_kdl("ribbon_unselected"));
4726            current_theme_node_children
4727                .nodes_mut()
4728                .push(theme.palette.table_title.to_kdl("table_title"));
4729            current_theme_node_children.nodes_mut().push(
4730                theme
4731                    .palette
4732                    .table_cell_selected
4733                    .to_kdl("table_cell_selected"),
4734            );
4735            current_theme_node_children.nodes_mut().push(
4736                theme
4737                    .palette
4738                    .table_cell_unselected
4739                    .to_kdl("table_cell_unselected"),
4740            );
4741            current_theme_node_children
4742                .nodes_mut()
4743                .push(theme.palette.list_selected.to_kdl("list_selected"));
4744            current_theme_node_children
4745                .nodes_mut()
4746                .push(theme.palette.list_unselected.to_kdl("list_unselected"));
4747            current_theme_node_children
4748                .nodes_mut()
4749                .push(theme.palette.frame_selected.to_kdl("frame_selected"));
4750
4751            match theme.palette.frame_unselected {
4752                None => {},
4753                Some(frame_unselected_style) => {
4754                    current_theme_node_children
4755                        .nodes_mut()
4756                        .push(frame_unselected_style.to_kdl("frame_unselected"));
4757                },
4758            }
4759            current_theme_node_children
4760                .nodes_mut()
4761                .push(theme.palette.frame_highlight.to_kdl("frame_highlight"));
4762            current_theme_node_children
4763                .nodes_mut()
4764                .push(theme.palette.exit_code_success.to_kdl("exit_code_success"));
4765            current_theme_node_children
4766                .nodes_mut()
4767                .push(theme.palette.exit_code_error.to_kdl("exit_code_error"));
4768            current_theme_node_children
4769                .nodes_mut()
4770                .push(theme.palette.multiplayer_user_colors.to_kdl());
4771            current_theme_node.set_children(current_theme_node_children);
4772            themes.nodes_mut().push(current_theme_node);
4773        }
4774        if has_themes {
4775            theme_node.set_children(themes);
4776            Some(theme_node)
4777        } else {
4778            None
4779        }
4780    }
4781}
4782
4783impl PermissionCache {
4784    pub fn from_string(raw_string: String) -> Result<GrantedPermission, ConfigError> {
4785        let kdl_document: KdlDocument = raw_string.parse()?;
4786
4787        let mut granted_permission = GrantedPermission::default();
4788
4789        for node in kdl_document.nodes() {
4790            if let Some(children) = node.children() {
4791                let key = kdl_name!(node);
4792                let permissions: Vec<PermissionType> = children
4793                    .nodes()
4794                    .iter()
4795                    .filter_map(|p| {
4796                        let v = kdl_name!(p);
4797                        PermissionType::from_str(v).ok()
4798                    })
4799                    .collect();
4800
4801                granted_permission.insert(key.into(), permissions);
4802            }
4803        }
4804
4805        Ok(granted_permission)
4806    }
4807
4808    pub fn to_string(granted: &GrantedPermission) -> String {
4809        let mut kdl_doucment = KdlDocument::new();
4810
4811        granted.iter().for_each(|(k, v)| {
4812            let mut node = KdlNode::new(k.as_str());
4813            let mut children = KdlDocument::new();
4814
4815            let permissions: HashSet<PermissionType> = v.clone().into_iter().collect();
4816            permissions.iter().for_each(|f| {
4817                let n = KdlNode::new(f.to_string().as_str());
4818                children.nodes_mut().push(n);
4819            });
4820
4821            node.set_children(children);
4822            kdl_doucment.nodes_mut().push(node);
4823        });
4824
4825        kdl_doucment.fmt();
4826        kdl_doucment.to_string()
4827    }
4828}
4829
4830impl SessionInfo {
4831    pub fn from_string(raw_session_info: &str, current_session_name: &str) -> Result<Self, String> {
4832        let kdl_document: KdlDocument = raw_session_info
4833            .parse()
4834            .map_err(|e| format!("Failed to parse kdl document: {}", e))?;
4835        let name = kdl_document
4836            .get("name")
4837            .and_then(|n| n.entries().iter().next())
4838            .and_then(|e| e.value().as_string())
4839            .map(|s| s.to_owned())
4840            .ok_or("Failed to parse session name")?;
4841        let connected_clients = kdl_document
4842            .get("connected_clients")
4843            .and_then(|n| n.entries().iter().next())
4844            .and_then(|e| e.value().as_i64())
4845            .map(|c| c as usize)
4846            .ok_or("Failed to parse connected_clients")?;
4847        let tabs: Vec<TabInfo> = kdl_document
4848            .get("tabs")
4849            .and_then(|t| t.children())
4850            .and_then(|c| {
4851                let mut tab_nodes = vec![];
4852                for tab_node in c.nodes() {
4853                    if let Some(tab) = tab_node.children() {
4854                        tab_nodes.push(TabInfo::decode_from_kdl(tab).ok()?);
4855                    }
4856                }
4857                Some(tab_nodes)
4858            })
4859            .ok_or("Failed to parse tabs")?;
4860        let panes: PaneManifest = kdl_document
4861            .get("panes")
4862            .and_then(|p| p.children())
4863            .map(|p| PaneManifest::decode_from_kdl(p))
4864            .ok_or("Failed to parse panes")?;
4865        let available_layouts: Vec<LayoutInfo> = kdl_document
4866            .get("available_layouts")
4867            .and_then(|p| p.children())
4868            .map(|e| {
4869                e.nodes()
4870                    .iter()
4871                    .filter_map(|n| {
4872                        let layout_name = n.name().value().to_owned();
4873                        let layout_source = n
4874                            .entries()
4875                            .iter()
4876                            .find(|e| e.name().map(|n| n.value()) == Some("source"))
4877                            .and_then(|e| e.value().as_string());
4878                        match layout_source {
4879                            Some(layout_source) => match layout_source {
4880                                "built-in" => Some(LayoutInfo::BuiltIn(layout_name)),
4881                                "file" => Some(LayoutInfo::File(layout_name)),
4882                                _ => None,
4883                            },
4884                            None => None,
4885                        }
4886                    })
4887                    .collect()
4888            })
4889            .ok_or("Failed to parse available_layouts")?;
4890        let web_client_count = kdl_document
4891            .get("web_client_count")
4892            .and_then(|n| n.entries().iter().next())
4893            .and_then(|e| e.value().as_i64())
4894            .map(|c| c as usize)
4895            .unwrap_or(0);
4896        let web_clients_allowed = kdl_document
4897            .get("web_clients_allowed")
4898            .and_then(|n| n.entries().iter().next())
4899            .and_then(|e| e.value().as_bool())
4900            .unwrap_or(false);
4901        let is_current_session = name == current_session_name;
4902        let mut tab_history = BTreeMap::new();
4903        if let Some(kdl_tab_history) = kdl_document.get("tab_history").and_then(|p| p.children()) {
4904            for client_node in kdl_tab_history.nodes() {
4905                if let Some(client_id) = client_node.children().and_then(|c| {
4906                    c.get("id")
4907                        .and_then(|c| c.entries().iter().next().and_then(|e| e.value().as_i64()))
4908                }) {
4909                    let mut history = vec![];
4910                    if let Some(history_entries) = client_node
4911                        .children()
4912                        .and_then(|c| c.get("history"))
4913                        .map(|h| h.entries())
4914                    {
4915                        for entry in history_entries {
4916                            if let Some(entry) = entry.value().as_i64() {
4917                                history.push(entry as usize);
4918                            }
4919                        }
4920                    }
4921                    tab_history.insert(client_id as u16, history);
4922                }
4923            }
4924        }
4925        Ok(SessionInfo {
4926            name,
4927            tabs,
4928            panes,
4929            connected_clients,
4930            is_current_session,
4931            available_layouts,
4932            web_client_count,
4933            web_clients_allowed,
4934            plugins: Default::default(), // we do not serialize plugin information
4935            tab_history,
4936        })
4937    }
4938    pub fn to_string(&self) -> String {
4939        let mut kdl_document = KdlDocument::new();
4940
4941        let mut name = KdlNode::new("name");
4942        name.push(self.name.clone());
4943
4944        let mut connected_clients = KdlNode::new("connected_clients");
4945        connected_clients.push(self.connected_clients as i64);
4946
4947        let mut tabs = KdlNode::new("tabs");
4948        let mut tab_children = KdlDocument::new();
4949        for tab_info in &self.tabs {
4950            let mut tab = KdlNode::new("tab");
4951            let kdl_tab_info = tab_info.encode_to_kdl();
4952            tab.set_children(kdl_tab_info);
4953            tab_children.nodes_mut().push(tab);
4954        }
4955        tabs.set_children(tab_children);
4956
4957        let mut panes = KdlNode::new("panes");
4958        panes.set_children(self.panes.encode_to_kdl());
4959
4960        let mut web_client_count = KdlNode::new("web_client_count");
4961        web_client_count.push(self.web_client_count as i64);
4962
4963        let mut web_clients_allowed = KdlNode::new("web_clients_allowed");
4964        web_clients_allowed.push(self.web_clients_allowed);
4965
4966        let mut available_layouts = KdlNode::new("available_layouts");
4967        let mut available_layouts_children = KdlDocument::new();
4968        for layout_info in &self.available_layouts {
4969            let (layout_name, layout_source) = match layout_info {
4970                LayoutInfo::File(name) => (name.clone(), "file"),
4971                LayoutInfo::BuiltIn(name) => (name.clone(), "built-in"),
4972                LayoutInfo::Url(url) => (url.clone(), "url"),
4973                LayoutInfo::Stringified(_stringified) => ("stringified-layout".to_owned(), "N/A"),
4974            };
4975            let mut layout_node = KdlNode::new(format!("{}", layout_name));
4976            let layout_source = KdlEntry::new_prop("source", layout_source);
4977            layout_node.entries_mut().push(layout_source);
4978            available_layouts_children.nodes_mut().push(layout_node);
4979        }
4980        available_layouts.set_children(available_layouts_children);
4981
4982        let mut tab_history = KdlNode::new("tab_history");
4983        let mut tab_history_children = KdlDocument::new();
4984        for (client_id, client_tab_history) in &self.tab_history {
4985            let mut client_document = KdlDocument::new();
4986            let mut client_node = KdlNode::new("client");
4987            let mut id = KdlNode::new("id");
4988            id.push(*client_id as i64);
4989            client_document.nodes_mut().push(id);
4990            let mut history = KdlNode::new("history");
4991            for entry in client_tab_history {
4992                history.push(*entry as i64);
4993            }
4994            client_document.nodes_mut().push(history);
4995            client_node.set_children(client_document);
4996            tab_history_children.nodes_mut().push(client_node);
4997        }
4998        tab_history.set_children(tab_history_children);
4999
5000        kdl_document.nodes_mut().push(name);
5001        kdl_document.nodes_mut().push(tabs);
5002        kdl_document.nodes_mut().push(panes);
5003        kdl_document.nodes_mut().push(connected_clients);
5004        kdl_document.nodes_mut().push(web_clients_allowed);
5005        kdl_document.nodes_mut().push(web_client_count);
5006        kdl_document.nodes_mut().push(available_layouts);
5007        kdl_document.nodes_mut().push(tab_history);
5008        kdl_document.fmt();
5009        kdl_document.to_string()
5010    }
5011}
5012
5013impl TabInfo {
5014    pub fn decode_from_kdl(kdl_document: &KdlDocument) -> Result<Self, String> {
5015        macro_rules! int_node {
5016            ($name:expr, $type:ident) => {{
5017                kdl_document
5018                    .get($name)
5019                    .and_then(|n| n.entries().iter().next())
5020                    .and_then(|e| e.value().as_i64())
5021                    .map(|e| e as $type)
5022                    .ok_or(format!("Failed to parse tab {}", $name))?
5023            }};
5024        }
5025        macro_rules! string_node {
5026            ($name:expr) => {{
5027                kdl_document
5028                    .get($name)
5029                    .and_then(|n| n.entries().iter().next())
5030                    .and_then(|e| e.value().as_string())
5031                    .map(|s| s.to_owned())
5032                    .ok_or(format!("Failed to parse tab {}", $name))?
5033            }};
5034        }
5035        macro_rules! optional_string_node {
5036            ($name:expr) => {{
5037                kdl_document
5038                    .get($name)
5039                    .and_then(|n| n.entries().iter().next())
5040                    .and_then(|e| e.value().as_string())
5041                    .map(|s| s.to_owned())
5042            }};
5043        }
5044        macro_rules! optional_int_node {
5045            ($name:expr, $type:ident) => {{
5046                kdl_document
5047                    .get($name)
5048                    .and_then(|n| n.entries().iter().next())
5049                    .and_then(|e| e.value().as_i64())
5050                    .map(|e| e as $type)
5051            }};
5052        }
5053        macro_rules! bool_node {
5054            ($name:expr) => {{
5055                kdl_document
5056                    .get($name)
5057                    .and_then(|n| n.entries().iter().next())
5058                    .and_then(|e| e.value().as_bool())
5059                    .ok_or(format!("Failed to parse tab {}", $name))?
5060            }};
5061        }
5062
5063        let position = int_node!("position", usize);
5064        let name = string_node!("name");
5065        let active = bool_node!("active");
5066        let panes_to_hide = int_node!("panes_to_hide", usize);
5067        let is_fullscreen_active = bool_node!("is_fullscreen_active");
5068        let is_sync_panes_active = bool_node!("is_sync_panes_active");
5069        let are_floating_panes_visible = bool_node!("are_floating_panes_visible");
5070        let mut other_focused_clients = vec![];
5071        if let Some(tab_other_focused_clients) = kdl_document
5072            .get("other_focused_clients")
5073            .map(|n| n.entries())
5074        {
5075            for entry in tab_other_focused_clients {
5076                if let Some(entry_parsed) = entry.value().as_i64() {
5077                    other_focused_clients.push(entry_parsed as u16);
5078                }
5079            }
5080        }
5081        let active_swap_layout_name = optional_string_node!("active_swap_layout_name");
5082        let viewport_rows = optional_int_node!("viewport_rows", usize).unwrap_or(0);
5083        let viewport_columns = optional_int_node!("viewport_columns", usize).unwrap_or(0);
5084        let display_area_rows = optional_int_node!("display_area_rows", usize).unwrap_or(0);
5085        let display_area_columns = optional_int_node!("display_area_columns", usize).unwrap_or(0);
5086        let is_swap_layout_dirty = bool_node!("is_swap_layout_dirty");
5087        let selectable_tiled_panes_count =
5088            optional_int_node!("selectable_tiled_panes_count", usize).unwrap_or(0);
5089        let selectable_floating_panes_count =
5090            optional_int_node!("selectable_floating_panes_count", usize).unwrap_or(0);
5091        Ok(TabInfo {
5092            position,
5093            name,
5094            active,
5095            panes_to_hide,
5096            is_fullscreen_active,
5097            is_sync_panes_active,
5098            are_floating_panes_visible,
5099            other_focused_clients,
5100            active_swap_layout_name,
5101            is_swap_layout_dirty,
5102            viewport_rows,
5103            viewport_columns,
5104            display_area_rows,
5105            display_area_columns,
5106            selectable_tiled_panes_count,
5107            selectable_floating_panes_count,
5108        })
5109    }
5110    pub fn encode_to_kdl(&self) -> KdlDocument {
5111        let mut kdl_doucment = KdlDocument::new();
5112
5113        let mut position = KdlNode::new("position");
5114        position.push(self.position as i64);
5115        kdl_doucment.nodes_mut().push(position);
5116
5117        let mut name = KdlNode::new("name");
5118        name.push(self.name.clone());
5119        kdl_doucment.nodes_mut().push(name);
5120
5121        let mut active = KdlNode::new("active");
5122        active.push(self.active);
5123        kdl_doucment.nodes_mut().push(active);
5124
5125        let mut panes_to_hide = KdlNode::new("panes_to_hide");
5126        panes_to_hide.push(self.panes_to_hide as i64);
5127        kdl_doucment.nodes_mut().push(panes_to_hide);
5128
5129        let mut is_fullscreen_active = KdlNode::new("is_fullscreen_active");
5130        is_fullscreen_active.push(self.is_fullscreen_active);
5131        kdl_doucment.nodes_mut().push(is_fullscreen_active);
5132
5133        let mut is_sync_panes_active = KdlNode::new("is_sync_panes_active");
5134        is_sync_panes_active.push(self.is_sync_panes_active);
5135        kdl_doucment.nodes_mut().push(is_sync_panes_active);
5136
5137        let mut are_floating_panes_visible = KdlNode::new("are_floating_panes_visible");
5138        are_floating_panes_visible.push(self.are_floating_panes_visible);
5139        kdl_doucment.nodes_mut().push(are_floating_panes_visible);
5140
5141        if !self.other_focused_clients.is_empty() {
5142            let mut other_focused_clients = KdlNode::new("other_focused_clients");
5143            for client_id in &self.other_focused_clients {
5144                other_focused_clients.push(*client_id as i64);
5145            }
5146            kdl_doucment.nodes_mut().push(other_focused_clients);
5147        }
5148
5149        if let Some(active_swap_layout_name) = self.active_swap_layout_name.as_ref() {
5150            let mut active_swap_layout = KdlNode::new("active_swap_layout_name");
5151            active_swap_layout.push(active_swap_layout_name.to_string());
5152            kdl_doucment.nodes_mut().push(active_swap_layout);
5153        }
5154
5155        let mut viewport_rows = KdlNode::new("viewport_rows");
5156        viewport_rows.push(self.viewport_rows as i64);
5157        kdl_doucment.nodes_mut().push(viewport_rows);
5158
5159        let mut viewport_columns = KdlNode::new("viewport_columns");
5160        viewport_columns.push(self.viewport_columns as i64);
5161        kdl_doucment.nodes_mut().push(viewport_columns);
5162
5163        let mut display_area_columns = KdlNode::new("display_area_columns");
5164        display_area_columns.push(self.display_area_columns as i64);
5165        kdl_doucment.nodes_mut().push(display_area_columns);
5166
5167        let mut display_area_rows = KdlNode::new("display_area_rows");
5168        display_area_rows.push(self.display_area_rows as i64);
5169        kdl_doucment.nodes_mut().push(display_area_rows);
5170
5171        let mut is_swap_layout_dirty = KdlNode::new("is_swap_layout_dirty");
5172        is_swap_layout_dirty.push(self.is_swap_layout_dirty);
5173        kdl_doucment.nodes_mut().push(is_swap_layout_dirty);
5174
5175        let mut selectable_tiled_panes_count = KdlNode::new("selectable_tiled_panes_count");
5176        selectable_tiled_panes_count.push(self.selectable_tiled_panes_count as i64);
5177        kdl_doucment.nodes_mut().push(selectable_tiled_panes_count);
5178
5179        let mut selectable_floating_panes_count = KdlNode::new("selectable_floating_panes_count");
5180        selectable_floating_panes_count.push(self.selectable_floating_panes_count as i64);
5181        kdl_doucment
5182            .nodes_mut()
5183            .push(selectable_floating_panes_count);
5184
5185        kdl_doucment
5186    }
5187}
5188
5189impl PaneManifest {
5190    pub fn decode_from_kdl(kdl_doucment: &KdlDocument) -> Self {
5191        let mut panes: HashMap<usize, Vec<PaneInfo>> = HashMap::new();
5192        for node in kdl_doucment.nodes() {
5193            if node.name().to_string() == "pane" {
5194                if let Some(pane_document) = node.children() {
5195                    if let Ok((tab_position, pane_info)) = PaneInfo::decode_from_kdl(pane_document)
5196                    {
5197                        let panes_in_tab_position =
5198                            panes.entry(tab_position).or_insert_with(Vec::new);
5199                        panes_in_tab_position.push(pane_info);
5200                    }
5201                }
5202            }
5203        }
5204        PaneManifest { panes }
5205    }
5206    pub fn encode_to_kdl(&self) -> KdlDocument {
5207        let mut kdl_doucment = KdlDocument::new();
5208        for (tab_position, panes) in &self.panes {
5209            for pane in panes {
5210                let mut pane_node = KdlNode::new("pane");
5211                let mut pane = pane.encode_to_kdl();
5212
5213                let mut position_node = KdlNode::new("tab_position");
5214                position_node.push(*tab_position as i64);
5215                pane.nodes_mut().push(position_node);
5216
5217                pane_node.set_children(pane);
5218                kdl_doucment.nodes_mut().push(pane_node);
5219            }
5220        }
5221        kdl_doucment
5222    }
5223}
5224
5225impl PaneInfo {
5226    pub fn decode_from_kdl(kdl_document: &KdlDocument) -> Result<(usize, Self), String> {
5227        // usize is the tab position
5228        macro_rules! int_node {
5229            ($name:expr, $type:ident) => {{
5230                kdl_document
5231                    .get($name)
5232                    .and_then(|n| n.entries().iter().next())
5233                    .and_then(|e| e.value().as_i64())
5234                    .map(|e| e as $type)
5235                    .ok_or(format!("Failed to parse pane {}", $name))?
5236            }};
5237        }
5238        macro_rules! optional_int_node {
5239            ($name:expr, $type:ident) => {{
5240                kdl_document
5241                    .get($name)
5242                    .and_then(|n| n.entries().iter().next())
5243                    .and_then(|e| e.value().as_i64())
5244                    .map(|e| e as $type)
5245            }};
5246        }
5247        macro_rules! bool_node {
5248            ($name:expr) => {{
5249                kdl_document
5250                    .get($name)
5251                    .and_then(|n| n.entries().iter().next())
5252                    .and_then(|e| e.value().as_bool())
5253                    .ok_or(format!("Failed to parse pane {}", $name))?
5254            }};
5255        }
5256        macro_rules! string_node {
5257            ($name:expr) => {{
5258                kdl_document
5259                    .get($name)
5260                    .and_then(|n| n.entries().iter().next())
5261                    .and_then(|e| e.value().as_string())
5262                    .map(|s| s.to_owned())
5263                    .ok_or(format!("Failed to parse pane {}", $name))?
5264            }};
5265        }
5266        macro_rules! optional_string_node {
5267            ($name:expr) => {{
5268                kdl_document
5269                    .get($name)
5270                    .and_then(|n| n.entries().iter().next())
5271                    .and_then(|e| e.value().as_string())
5272                    .map(|s| s.to_owned())
5273            }};
5274        }
5275        let tab_position = int_node!("tab_position", usize);
5276        let id = int_node!("id", u32);
5277
5278        let is_plugin = bool_node!("is_plugin");
5279        let is_focused = bool_node!("is_focused");
5280        let is_fullscreen = bool_node!("is_fullscreen");
5281        let is_floating = bool_node!("is_floating");
5282        let is_suppressed = bool_node!("is_suppressed");
5283        let title = string_node!("title");
5284        let exited = bool_node!("exited");
5285        let exit_status = optional_int_node!("exit_status", i32);
5286        let is_held = bool_node!("is_held");
5287        let pane_x = int_node!("pane_x", usize);
5288        let pane_content_x = int_node!("pane_content_x", usize);
5289        let pane_y = int_node!("pane_y", usize);
5290        let pane_content_y = int_node!("pane_content_y", usize);
5291        let pane_rows = int_node!("pane_rows", usize);
5292        let pane_content_rows = int_node!("pane_content_rows", usize);
5293        let pane_columns = int_node!("pane_columns", usize);
5294        let pane_content_columns = int_node!("pane_content_columns", usize);
5295        let cursor_coordinates_in_pane = kdl_document
5296            .get("cursor_coordinates_in_pane")
5297            .map(|n| {
5298                let mut entries = n.entries().iter();
5299                (entries.next(), entries.next())
5300            })
5301            .and_then(|(x, y)| {
5302                let x = x.and_then(|x| x.value().as_i64()).map(|x| x as usize);
5303                let y = y.and_then(|y| y.value().as_i64()).map(|y| y as usize);
5304                match (x, y) {
5305                    (Some(x), Some(y)) => Some((x, y)),
5306                    _ => None,
5307                }
5308            });
5309        let terminal_command = optional_string_node!("terminal_command");
5310        let plugin_url = optional_string_node!("plugin_url");
5311        let is_selectable = bool_node!("is_selectable");
5312
5313        let pane_info = PaneInfo {
5314            id,
5315            is_plugin,
5316            is_focused,
5317            is_fullscreen,
5318            is_floating,
5319            is_suppressed,
5320            title,
5321            exited,
5322            exit_status,
5323            is_held,
5324            pane_x,
5325            pane_content_x,
5326            pane_y,
5327            pane_content_y,
5328            pane_rows,
5329            pane_content_rows,
5330            pane_columns,
5331            pane_content_columns,
5332            cursor_coordinates_in_pane,
5333            terminal_command,
5334            plugin_url,
5335            is_selectable,
5336            index_in_pane_group: Default::default(), // we don't serialize this
5337        };
5338        Ok((tab_position, pane_info))
5339    }
5340    pub fn encode_to_kdl(&self) -> KdlDocument {
5341        let mut kdl_doucment = KdlDocument::new();
5342        macro_rules! int_node {
5343            ($name:expr, $val:expr) => {{
5344                let mut att = KdlNode::new($name);
5345                att.push($val as i64);
5346                kdl_doucment.nodes_mut().push(att);
5347            }};
5348        }
5349        macro_rules! bool_node {
5350            ($name:expr, $val:expr) => {{
5351                let mut att = KdlNode::new($name);
5352                att.push($val);
5353                kdl_doucment.nodes_mut().push(att);
5354            }};
5355        }
5356        macro_rules! string_node {
5357            ($name:expr, $val:expr) => {{
5358                let mut att = KdlNode::new($name);
5359                att.push($val);
5360                kdl_doucment.nodes_mut().push(att);
5361            }};
5362        }
5363
5364        int_node!("id", self.id);
5365        bool_node!("is_plugin", self.is_plugin);
5366        bool_node!("is_focused", self.is_focused);
5367        bool_node!("is_fullscreen", self.is_fullscreen);
5368        bool_node!("is_floating", self.is_floating);
5369        bool_node!("is_suppressed", self.is_suppressed);
5370        string_node!("title", self.title.to_string());
5371        bool_node!("exited", self.exited);
5372        if let Some(exit_status) = self.exit_status {
5373            int_node!("exit_status", exit_status);
5374        }
5375        bool_node!("is_held", self.is_held);
5376        int_node!("pane_x", self.pane_x);
5377        int_node!("pane_content_x", self.pane_content_x);
5378        int_node!("pane_y", self.pane_y);
5379        int_node!("pane_content_y", self.pane_content_y);
5380        int_node!("pane_rows", self.pane_rows);
5381        int_node!("pane_content_rows", self.pane_content_rows);
5382        int_node!("pane_columns", self.pane_columns);
5383        int_node!("pane_content_columns", self.pane_content_columns);
5384        if let Some((cursor_x, cursor_y)) = self.cursor_coordinates_in_pane {
5385            let mut cursor_coordinates_in_pane = KdlNode::new("cursor_coordinates_in_pane");
5386            cursor_coordinates_in_pane.push(cursor_x as i64);
5387            cursor_coordinates_in_pane.push(cursor_y as i64);
5388            kdl_doucment.nodes_mut().push(cursor_coordinates_in_pane);
5389        }
5390        if let Some(terminal_command) = &self.terminal_command {
5391            string_node!("terminal_command", terminal_command.to_string());
5392        }
5393        if let Some(plugin_url) = &self.plugin_url {
5394            string_node!("plugin_url", plugin_url.to_string());
5395        }
5396        bool_node!("is_selectable", self.is_selectable);
5397        kdl_doucment
5398    }
5399}
5400
5401pub fn parse_plugin_user_configuration(
5402    plugin_block: &KdlNode,
5403) -> Result<BTreeMap<String, String>, ConfigError> {
5404    let mut configuration = BTreeMap::new();
5405    for user_configuration_entry in plugin_block.entries() {
5406        let name = user_configuration_entry.name();
5407        let value = user_configuration_entry.value();
5408        if let Some(name) = name {
5409            let name = name.to_string();
5410            if KdlLayoutParser::is_a_reserved_plugin_property(&name) {
5411                continue;
5412            }
5413            configuration.insert(name, value.to_string());
5414        }
5415    }
5416    if let Some(user_config) = kdl_children_nodes!(plugin_block) {
5417        for user_configuration_entry in user_config {
5418            let config_entry_name = kdl_name!(user_configuration_entry);
5419            if KdlLayoutParser::is_a_reserved_plugin_property(&config_entry_name) {
5420                continue;
5421            }
5422            let config_entry_str_value = kdl_first_entry_as_string!(user_configuration_entry)
5423                .map(|s| format!("{}", s.to_string()));
5424            let config_entry_int_value = kdl_first_entry_as_i64!(user_configuration_entry)
5425                .map(|s| format!("{}", s.to_string()));
5426            let config_entry_bool_value = kdl_first_entry_as_bool!(user_configuration_entry)
5427                .map(|s| format!("{}", s.to_string()));
5428            let config_entry_children = user_configuration_entry
5429                .children()
5430                .map(|s| format!("{}", s.to_string().trim()));
5431            let config_entry_value = config_entry_str_value
5432                .or(config_entry_int_value)
5433                .or(config_entry_bool_value)
5434                .or(config_entry_children)
5435                .ok_or(ConfigError::new_kdl_error(
5436                    format!(
5437                        "Failed to parse plugin block configuration: {:?}",
5438                        user_configuration_entry
5439                    ),
5440                    plugin_block.span().offset(),
5441                    plugin_block.span().len(),
5442                ))?;
5443            configuration.insert(config_entry_name.into(), config_entry_value);
5444        }
5445    }
5446    Ok(configuration)
5447}
5448
5449#[test]
5450fn serialize_and_deserialize_session_info() {
5451    let session_info = SessionInfo::default();
5452    let serialized = session_info.to_string();
5453    let deserealized = SessionInfo::from_string(&serialized, "not this session").unwrap();
5454    assert_eq!(session_info, deserealized);
5455    insta::assert_snapshot!(serialized);
5456}
5457
5458#[test]
5459fn serialize_and_deserialize_session_info_with_data() {
5460    let panes_list = vec![
5461        PaneInfo {
5462            id: 1,
5463            is_plugin: false,
5464            is_focused: true,
5465            is_fullscreen: true,
5466            is_floating: false,
5467            is_suppressed: false,
5468            title: "pane 1".to_owned(),
5469            exited: false,
5470            exit_status: None,
5471            is_held: false,
5472            pane_x: 0,
5473            pane_content_x: 1,
5474            pane_y: 0,
5475            pane_content_y: 1,
5476            pane_rows: 5,
5477            pane_content_rows: 4,
5478            pane_columns: 22,
5479            pane_content_columns: 21,
5480            cursor_coordinates_in_pane: Some((0, 0)),
5481            terminal_command: Some("foo".to_owned()),
5482            plugin_url: None,
5483            is_selectable: true,
5484            index_in_pane_group: Default::default(), // we don't serialize this
5485        },
5486        PaneInfo {
5487            id: 1,
5488            is_plugin: true,
5489            is_focused: true,
5490            is_fullscreen: true,
5491            is_floating: false,
5492            is_suppressed: false,
5493            title: "pane 1".to_owned(),
5494            exited: false,
5495            exit_status: None,
5496            is_held: false,
5497            pane_x: 0,
5498            pane_content_x: 1,
5499            pane_y: 0,
5500            pane_content_y: 1,
5501            pane_rows: 5,
5502            pane_content_rows: 4,
5503            pane_columns: 22,
5504            pane_content_columns: 21,
5505            cursor_coordinates_in_pane: Some((0, 0)),
5506            terminal_command: None,
5507            plugin_url: Some("i_am_a_fake_plugin".to_owned()),
5508            is_selectable: true,
5509            index_in_pane_group: Default::default(), // we don't serialize this
5510        },
5511    ];
5512    let mut panes = HashMap::new();
5513    panes.insert(0, panes_list);
5514    let session_info = SessionInfo {
5515        name: "my session name".to_owned(),
5516        tabs: vec![
5517            TabInfo {
5518                position: 0,
5519                name: "tab 1".to_owned(),
5520                active: true,
5521                panes_to_hide: 1,
5522                is_fullscreen_active: true,
5523                is_sync_panes_active: false,
5524                are_floating_panes_visible: true,
5525                other_focused_clients: vec![2, 3],
5526                active_swap_layout_name: Some("BASE".to_owned()),
5527                is_swap_layout_dirty: true,
5528                viewport_rows: 10,
5529                viewport_columns: 10,
5530                display_area_rows: 10,
5531                display_area_columns: 10,
5532                selectable_tiled_panes_count: 10,
5533                selectable_floating_panes_count: 10,
5534            },
5535            TabInfo {
5536                position: 1,
5537                name: "tab 2".to_owned(),
5538                active: true,
5539                panes_to_hide: 0,
5540                is_fullscreen_active: false,
5541                is_sync_panes_active: true,
5542                are_floating_panes_visible: true,
5543                other_focused_clients: vec![2, 3],
5544                active_swap_layout_name: None,
5545                is_swap_layout_dirty: false,
5546                viewport_rows: 10,
5547                viewport_columns: 10,
5548                display_area_rows: 10,
5549                display_area_columns: 10,
5550                selectable_tiled_panes_count: 10,
5551                selectable_floating_panes_count: 10,
5552            },
5553        ],
5554        panes: PaneManifest { panes },
5555        connected_clients: 2,
5556        is_current_session: false,
5557        available_layouts: vec![
5558            LayoutInfo::File("layout1".to_owned()),
5559            LayoutInfo::BuiltIn("layout2".to_owned()),
5560            LayoutInfo::File("layout3".to_owned()),
5561        ],
5562        plugins: Default::default(),
5563        web_client_count: 2,
5564        web_clients_allowed: true,
5565        tab_history: Default::default(),
5566    };
5567    let serialized = session_info.to_string();
5568    let deserealized = SessionInfo::from_string(&serialized, "not this session").unwrap();
5569    assert_eq!(session_info, deserealized);
5570    insta::assert_snapshot!(serialized);
5571}
5572
5573#[test]
5574fn keybinds_to_string() {
5575    let fake_config = r#"
5576        keybinds {
5577            normal {
5578                bind "Ctrl g" { SwitchToMode "Locked"; }
5579            }
5580        }"#;
5581    let document: KdlDocument = fake_config.parse().unwrap();
5582    let deserialized = Keybinds::from_kdl(
5583        document.get("keybinds").unwrap(),
5584        Default::default(),
5585        &Default::default(),
5586    )
5587    .unwrap();
5588    let clear_defaults = true;
5589    let serialized = Keybinds::to_kdl(&deserialized, clear_defaults);
5590    let deserialized_from_serialized = Keybinds::from_kdl(
5591        serialized
5592            .to_string()
5593            .parse::<KdlDocument>()
5594            .unwrap()
5595            .get("keybinds")
5596            .unwrap(),
5597        Default::default(),
5598        &Default::default(),
5599    )
5600    .unwrap();
5601    insta::assert_snapshot!(serialized.to_string());
5602    assert_eq!(
5603        deserialized, deserialized_from_serialized,
5604        "Deserialized serialized config equals original config"
5605    );
5606}
5607
5608#[test]
5609fn keybinds_to_string_without_clearing_defaults() {
5610    let fake_config = r#"
5611        keybinds {
5612            normal {
5613                bind "Ctrl g" { SwitchToMode "Locked"; }
5614            }
5615        }"#;
5616    let document: KdlDocument = fake_config.parse().unwrap();
5617    let deserialized = Keybinds::from_kdl(
5618        document.get("keybinds").unwrap(),
5619        Default::default(),
5620        &Default::default(),
5621    )
5622    .unwrap();
5623    let clear_defaults = false;
5624    let serialized = Keybinds::to_kdl(&deserialized, clear_defaults);
5625    let deserialized_from_serialized = Keybinds::from_kdl(
5626        serialized
5627            .to_string()
5628            .parse::<KdlDocument>()
5629            .unwrap()
5630            .get("keybinds")
5631            .unwrap(),
5632        Default::default(),
5633        &Default::default(),
5634    )
5635    .unwrap();
5636    insta::assert_snapshot!(serialized.to_string());
5637    assert_eq!(
5638        deserialized, deserialized_from_serialized,
5639        "Deserialized serialized config equals original config"
5640    );
5641}
5642
5643#[test]
5644fn keybinds_to_string_with_multiple_actions() {
5645    let fake_config = r#"
5646        keybinds {
5647            normal {
5648                bind "Ctrl n" { NewPane; SwitchToMode "Locked"; }
5649            }
5650        }"#;
5651    let document: KdlDocument = fake_config.parse().unwrap();
5652    let deserialized = Keybinds::from_kdl(
5653        document.get("keybinds").unwrap(),
5654        Default::default(),
5655        &Default::default(),
5656    )
5657    .unwrap();
5658    let clear_defaults = true;
5659    let serialized = Keybinds::to_kdl(&deserialized, clear_defaults);
5660    let deserialized_from_serialized = Keybinds::from_kdl(
5661        serialized
5662            .to_string()
5663            .parse::<KdlDocument>()
5664            .unwrap()
5665            .get("keybinds")
5666            .unwrap(),
5667        Default::default(),
5668        &Default::default(),
5669    )
5670    .unwrap();
5671    assert_eq!(
5672        deserialized, deserialized_from_serialized,
5673        "Deserialized serialized config equals original config"
5674    );
5675    insta::assert_snapshot!(serialized.to_string());
5676}
5677
5678#[test]
5679fn keybinds_to_string_with_all_actions() {
5680    let fake_config = r#"
5681        keybinds {
5682            normal {
5683                bind "Ctrl a" { Quit; }
5684                bind "Ctrl b" { Write 102 111 111; }
5685                bind "Ctrl c" { WriteChars "hi there!"; }
5686                bind "Ctrl d" { SwitchToMode "Locked"; }
5687                bind "Ctrl e" { Resize "Increase"; }
5688                bind "Ctrl f" { FocusNextPane; }
5689                bind "Ctrl g" { FocusPreviousPane; }
5690                bind "Ctrl h" { SwitchFocus; }
5691                bind "Ctrl i" { MoveFocus "Right"; }
5692                bind "Ctrl j" { MoveFocusOrTab "Right"; }
5693                bind "Ctrl k" { MovePane "Right"; }
5694                bind "Ctrl l" { MovePaneBackwards; }
5695                bind "Ctrl m" { Resize "Decrease Down"; }
5696                bind "Ctrl n" { DumpScreen "/tmp/dumped"; }
5697                bind "Ctrl o" { DumpLayout "/tmp/dumped-layout"; }
5698                bind "Ctrl p" { EditScrollback; }
5699                bind "Ctrl q" { ScrollUp; }
5700                bind "Ctrl r" { ScrollDown; }
5701                bind "Ctrl s" { ScrollToBottom; }
5702                bind "Ctrl t" { ScrollToTop; }
5703                bind "Ctrl u" { PageScrollUp; }
5704                bind "Ctrl v" { PageScrollDown; }
5705                bind "Ctrl w" { HalfPageScrollUp; }
5706                bind "Ctrl x" { HalfPageScrollDown; }
5707                bind "Ctrl y" { ToggleFocusFullscreen; }
5708                bind "Ctrl z" { TogglePaneFrames; }
5709                bind "Alt a" { ToggleActiveSyncTab; }
5710                bind "Alt b" { NewPane "Right"; }
5711                bind "Alt c" { TogglePaneEmbedOrFloating; }
5712                bind "Alt d" { ToggleFloatingPanes; }
5713                bind "Alt e" { CloseFocus; }
5714                bind "Alt f" { PaneNameInput 0; }
5715                bind "Alt g" { UndoRenamePane; }
5716                bind "Alt h" { NewTab; }
5717                bind "Alt i" { GoToNextTab; }
5718                bind "Alt j" { GoToPreviousTab; }
5719                bind "Alt k" { CloseTab; }
5720                bind "Alt l" { GoToTab 1; }
5721                bind "Alt m" { ToggleTab; }
5722                bind "Alt n" { TabNameInput 0; }
5723                bind "Alt o" { UndoRenameTab; }
5724                bind "Alt p" { MoveTab "Right"; }
5725                bind "Alt q" {
5726                    Run "ls" "-l" {
5727                        hold_on_start true;
5728                        hold_on_close false;
5729                        cwd "/tmp";
5730                        name "my cool pane";
5731                    };
5732                }
5733                bind "Alt r" {
5734                    Run "ls" "-l" {
5735                        hold_on_start true;
5736                        hold_on_close false;
5737                        cwd "/tmp";
5738                        name "my cool pane";
5739                        floating true;
5740                    };
5741                }
5742                bind "Alt s" {
5743                    Run "ls" "-l" {
5744                        hold_on_start true;
5745                        hold_on_close false;
5746                        cwd "/tmp";
5747                        name "my cool pane";
5748                        in_place true;
5749                    };
5750                }
5751                bind "Alt t" { Detach; }
5752                bind "Alt u" {
5753                    LaunchOrFocusPlugin "zellij:session-manager"{
5754                        floating true;
5755                        move_to_focused_tab true;
5756                        skip_plugin_cache true;
5757                        config_key_1 "config_value_1";
5758                        config_key_2 "config_value_2";
5759                    };
5760                }
5761                bind "Alt v" {
5762                    LaunchOrFocusPlugin "zellij:session-manager"{
5763                        in_place true;
5764                        move_to_focused_tab true;
5765                        skip_plugin_cache true;
5766                        config_key_1 "config_value_1";
5767                        config_key_2 "config_value_2";
5768                    };
5769                }
5770                bind "Alt w" {
5771                    LaunchPlugin "zellij:session-manager" {
5772                        floating true;
5773                        skip_plugin_cache true;
5774                        config_key_1 "config_value_1";
5775                        config_key_2 "config_value_2";
5776                    };
5777                }
5778                bind "Alt x" {
5779                    LaunchPlugin "zellij:session-manager"{
5780                        in_place true;
5781                        skip_plugin_cache true;
5782                        config_key_1 "config_value_1";
5783                        config_key_2 "config_value_2";
5784                    };
5785                }
5786                bind "Alt y" { Copy; }
5787                bind "Alt z" { SearchInput 0; }
5788                bind "Ctrl Alt a" { Search "Up"; }
5789                bind "Ctrl Alt b" { SearchToggleOption "CaseSensitivity"; }
5790                bind "Ctrl Alt c" { ToggleMouseMode; }
5791                bind "Ctrl Alt d" { PreviousSwapLayout; }
5792                bind "Ctrl Alt e" { NextSwapLayout; }
5793                bind "Ctrl Alt g" { BreakPane; }
5794                bind "Ctrl Alt h" { BreakPaneRight; }
5795                bind "Ctrl Alt i" { BreakPaneLeft; }
5796                bind "Ctrl Alt i" { BreakPaneLeft; }
5797                bind "Ctrl Alt j" {
5798                    MessagePlugin "zellij:session-manager"{
5799                        name "message_name";
5800                        payload "message_payload";
5801                        cwd "/tmp";
5802                        launch_new true;
5803                        skip_cache true;
5804                        floating true;
5805                        title "plugin_title";
5806                        config_key_1 "config_value_1";
5807                        config_key_2 "config_value_2";
5808                    };
5809                }
5810            }
5811        }"#;
5812    let document: KdlDocument = fake_config.parse().unwrap();
5813    let deserialized = Keybinds::from_kdl(
5814        document.get("keybinds").unwrap(),
5815        Default::default(),
5816        &Default::default(),
5817    )
5818    .unwrap();
5819    let clear_defaults = true;
5820    let serialized = Keybinds::to_kdl(&deserialized, clear_defaults);
5821    let deserialized_from_serialized = Keybinds::from_kdl(
5822        serialized
5823            .to_string()
5824            .parse::<KdlDocument>()
5825            .unwrap()
5826            .get("keybinds")
5827            .unwrap(),
5828        Default::default(),
5829        &Default::default(),
5830    )
5831    .unwrap();
5832    // uncomment the below lines for more easily debugging a failed assertion here
5833    //     for (input_mode, input_mode_keybinds) in deserialized.0 {
5834    //         if let Some(other_input_mode_keybinds) = deserialized_from_serialized.0.get(&input_mode) {
5835    //             for (keybind, action) in input_mode_keybinds {
5836    //                 if let Some(other_action) = other_input_mode_keybinds.get(&keybind) {
5837    //                     assert_eq!(&action, other_action);
5838    //                 } else {
5839    //                     eprintln!("keybind: {:?} not found in other", keybind);
5840    //                 }
5841    //             }
5842    //         }
5843    //     }
5844    assert_eq!(
5845        deserialized, deserialized_from_serialized,
5846        "Deserialized serialized config equals original config"
5847    );
5848    insta::assert_snapshot!(serialized.to_string());
5849}
5850
5851#[test]
5852fn keybinds_to_string_with_shared_modes() {
5853    let fake_config = r#"
5854        keybinds {
5855            normal {
5856                bind "Ctrl n" { NewPane; SwitchToMode "Locked"; }
5857            }
5858            locked {
5859                bind "Ctrl n" { NewPane; SwitchToMode "Locked"; }
5860            }
5861            shared_except "locked" "pane" {
5862                bind "Ctrl f" { TogglePaneEmbedOrFloating; }
5863            }
5864            shared_among "locked" "pane" {
5865                bind "Ctrl p" { WriteChars "foo"; }
5866            }
5867        }"#;
5868    let document: KdlDocument = fake_config.parse().unwrap();
5869    let deserialized = Keybinds::from_kdl(
5870        document.get("keybinds").unwrap(),
5871        Default::default(),
5872        &Default::default(),
5873    )
5874    .unwrap();
5875    let clear_defaults = true;
5876    let serialized = Keybinds::to_kdl(&deserialized, clear_defaults);
5877    let deserialized_from_serialized = Keybinds::from_kdl(
5878        serialized
5879            .to_string()
5880            .parse::<KdlDocument>()
5881            .unwrap()
5882            .get("keybinds")
5883            .unwrap(),
5884        Default::default(),
5885        &Default::default(),
5886    )
5887    .unwrap();
5888    assert_eq!(
5889        deserialized, deserialized_from_serialized,
5890        "Deserialized serialized config equals original config"
5891    );
5892    insta::assert_snapshot!(serialized.to_string());
5893}
5894
5895#[test]
5896fn keybinds_to_string_with_multiple_multiline_actions() {
5897    let fake_config = r#"
5898        keybinds {
5899            shared {
5900                bind "Ctrl n" {
5901                    NewPane
5902                    SwitchToMode "Locked"
5903                    MessagePlugin "zellij:session-manager"{
5904                        name "message_name";
5905                        payload "message_payload";
5906                        cwd "/tmp";
5907                        launch_new true;
5908                        skip_cache true;
5909                        floating true;
5910                        title "plugin_title";
5911                        config_key_1 "config_value_1";
5912                        config_key_2 "config_value_2";
5913                    };
5914                }
5915            }
5916        }"#;
5917    let document: KdlDocument = fake_config.parse().unwrap();
5918    let deserialized = Keybinds::from_kdl(
5919        document.get("keybinds").unwrap(),
5920        Default::default(),
5921        &Default::default(),
5922    )
5923    .unwrap();
5924    let clear_defaults = true;
5925    let serialized = Keybinds::to_kdl(&deserialized, clear_defaults);
5926    let deserialized_from_serialized = Keybinds::from_kdl(
5927        serialized
5928            .to_string()
5929            .parse::<KdlDocument>()
5930            .unwrap()
5931            .get("keybinds")
5932            .unwrap(),
5933        Default::default(),
5934        &Default::default(),
5935    )
5936    .unwrap();
5937    assert_eq!(
5938        deserialized, deserialized_from_serialized,
5939        "Deserialized serialized config equals original config"
5940    );
5941    insta::assert_snapshot!(serialized.to_string());
5942}
5943
5944#[test]
5945fn themes_to_string() {
5946    let fake_config = r#"
5947        themes {
5948           dracula {
5949                fg 248 248 242
5950                bg 40 42 54
5951                black 0 0 0
5952                red 255 85 85
5953                green 80 250 123
5954                yellow 241 250 140
5955                blue 98 114 164
5956                magenta 255 121 198
5957                cyan 139 233 253
5958                white 255 255 255
5959                orange 255 184 108
5960            }
5961        }"#;
5962    let document: KdlDocument = fake_config.parse().unwrap();
5963    let sourced_from_external_file = false;
5964    let deserialized =
5965        Themes::from_kdl(document.get("themes").unwrap(), sourced_from_external_file).unwrap();
5966    let serialized = Themes::to_kdl(&deserialized).unwrap();
5967    let deserialized_from_serialized = Themes::from_kdl(
5968        serialized
5969            .to_string()
5970            .parse::<KdlDocument>()
5971            .unwrap()
5972            .get("themes")
5973            .unwrap(),
5974        sourced_from_external_file,
5975    )
5976    .unwrap();
5977    assert_eq!(
5978        deserialized, deserialized_from_serialized,
5979        "Deserialized serialized config equals original config",
5980    );
5981    insta::assert_snapshot!(serialized.to_string());
5982}
5983
5984#[test]
5985fn themes_to_string_with_hex_definitions() {
5986    let fake_config = r##"
5987        themes {
5988            nord {
5989                fg "#D8DEE9"
5990                bg "#2E3440"
5991                black "#3B4252"
5992                red "#BF616A"
5993                green "#A3BE8C"
5994                yellow "#EBCB8B"
5995                blue "#81A1C1"
5996                magenta "#B48EAD"
5997                cyan "#88C0D0"
5998                white "#E5E9F0"
5999                orange "#D08770"
6000            }
6001        }"##;
6002    let document: KdlDocument = fake_config.parse().unwrap();
6003    let sourced_from_external_file = false;
6004    let deserialized =
6005        Themes::from_kdl(document.get("themes").unwrap(), sourced_from_external_file).unwrap();
6006    let serialized = Themes::to_kdl(&deserialized).unwrap();
6007    let deserialized_from_serialized = Themes::from_kdl(
6008        serialized
6009            .to_string()
6010            .parse::<KdlDocument>()
6011            .unwrap()
6012            .get("themes")
6013            .unwrap(),
6014        sourced_from_external_file,
6015    )
6016    .unwrap();
6017    assert_eq!(
6018        deserialized, deserialized_from_serialized,
6019        "Deserialized serialized config equals original config"
6020    );
6021    insta::assert_snapshot!(serialized.to_string());
6022}
6023
6024#[test]
6025fn themes_to_string_with_eight_bit_definitions() {
6026    let fake_config = r##"
6027        themes {
6028            default {
6029                fg 1
6030                bg 10
6031                black 20
6032                red 30
6033                green 40
6034                yellow 50
6035                blue 60
6036                magenta 70
6037                cyan 80
6038                white 90
6039                orange 254
6040            }
6041        }"##;
6042    let document: KdlDocument = fake_config.parse().unwrap();
6043    let sourced_from_external_file = false;
6044    let deserialized =
6045        Themes::from_kdl(document.get("themes").unwrap(), sourced_from_external_file).unwrap();
6046    let serialized = Themes::to_kdl(&deserialized).unwrap();
6047    let deserialized_from_serialized = Themes::from_kdl(
6048        serialized
6049            .to_string()
6050            .parse::<KdlDocument>()
6051            .unwrap()
6052            .get("themes")
6053            .unwrap(),
6054        sourced_from_external_file,
6055    )
6056    .unwrap();
6057    assert_eq!(
6058        deserialized, deserialized_from_serialized,
6059        "Deserialized serialized config equals original config"
6060    );
6061    insta::assert_snapshot!(serialized.to_string());
6062}
6063
6064#[test]
6065fn themes_to_string_with_combined_definitions() {
6066    let fake_config = r##"
6067        themes {
6068            default {
6069                fg 1
6070                bg 10
6071                black 20
6072                red 30
6073                green 40
6074                yellow 50
6075                blue 60
6076                magenta 70
6077                cyan 80
6078                white 255 255 255
6079                orange "#D08770"
6080            }
6081        }"##;
6082    let document: KdlDocument = fake_config.parse().unwrap();
6083    let sourced_from_external_file = false;
6084    let deserialized =
6085        Themes::from_kdl(document.get("themes").unwrap(), sourced_from_external_file).unwrap();
6086    let serialized = Themes::to_kdl(&deserialized).unwrap();
6087    let deserialized_from_serialized = Themes::from_kdl(
6088        serialized
6089            .to_string()
6090            .parse::<KdlDocument>()
6091            .unwrap()
6092            .get("themes")
6093            .unwrap(),
6094        sourced_from_external_file,
6095    )
6096    .unwrap();
6097    assert_eq!(
6098        deserialized, deserialized_from_serialized,
6099        "Deserialized serialized config equals original config"
6100    );
6101    insta::assert_snapshot!(serialized.to_string());
6102}
6103
6104#[test]
6105fn themes_to_string_with_multiple_theme_definitions() {
6106    let fake_config = r##"
6107        themes {
6108           nord {
6109               fg "#D8DEE9"
6110               bg "#2E3440"
6111               black "#3B4252"
6112               red "#BF616A"
6113               green "#A3BE8C"
6114               yellow "#EBCB8B"
6115               blue "#81A1C1"
6116               magenta "#B48EAD"
6117               cyan "#88C0D0"
6118               white "#E5E9F0"
6119               orange "#D08770"
6120           }
6121           dracula {
6122                fg 248 248 242
6123                bg 40 42 54
6124                black 0 0 0
6125                red 255 85 85
6126                green 80 250 123
6127                yellow 241 250 140
6128                blue 98 114 164
6129                magenta 255 121 198
6130                cyan 139 233 253
6131                white 255 255 255
6132                orange 255 184 108
6133            }
6134        }"##;
6135    let document: KdlDocument = fake_config.parse().unwrap();
6136    let sourced_from_external_file = false;
6137    let deserialized =
6138        Themes::from_kdl(document.get("themes").unwrap(), sourced_from_external_file).unwrap();
6139    let serialized = Themes::to_kdl(&deserialized).unwrap();
6140    let deserialized_from_serialized = Themes::from_kdl(
6141        serialized
6142            .to_string()
6143            .parse::<KdlDocument>()
6144            .unwrap()
6145            .get("themes")
6146            .unwrap(),
6147        sourced_from_external_file,
6148    )
6149    .unwrap();
6150    assert_eq!(
6151        deserialized, deserialized_from_serialized,
6152        "Deserialized serialized config equals original config"
6153    );
6154    insta::assert_snapshot!(serialized.to_string());
6155}
6156
6157#[test]
6158fn plugins_to_string() {
6159    let fake_config = r##"
6160        plugins {
6161            tab-bar location="zellij:tab-bar"
6162            status-bar location="zellij:status-bar"
6163            strider location="zellij:strider"
6164            compact-bar location="zellij:compact-bar"
6165            session-manager location="zellij:session-manager"
6166            welcome-screen location="zellij:session-manager" {
6167                welcome_screen true
6168            }
6169            filepicker location="zellij:strider" {
6170                cwd "/"
6171            }
6172        }"##;
6173    let document: KdlDocument = fake_config.parse().unwrap();
6174    let deserialized = PluginAliases::from_kdl(document.get("plugins").unwrap()).unwrap();
6175    let serialized = PluginAliases::to_kdl(&deserialized, true);
6176    let deserialized_from_serialized = PluginAliases::from_kdl(
6177        serialized
6178            .to_string()
6179            .parse::<KdlDocument>()
6180            .unwrap()
6181            .get("plugins")
6182            .unwrap(),
6183    )
6184    .unwrap();
6185    assert_eq!(
6186        deserialized, deserialized_from_serialized,
6187        "Deserialized serialized config equals original config"
6188    );
6189    insta::assert_snapshot!(serialized.to_string());
6190}
6191
6192#[test]
6193fn plugins_to_string_with_file_and_web() {
6194    let fake_config = r##"
6195        plugins {
6196            tab-bar location="https://foo.com/plugin.wasm"
6197            filepicker location="file:/path/to/my/plugin.wasm" {
6198                cwd "/"
6199            }
6200        }"##;
6201    let document: KdlDocument = fake_config.parse().unwrap();
6202    let deserialized = PluginAliases::from_kdl(document.get("plugins").unwrap()).unwrap();
6203    let serialized = PluginAliases::to_kdl(&deserialized, true);
6204    let deserialized_from_serialized = PluginAliases::from_kdl(
6205        serialized
6206            .to_string()
6207            .parse::<KdlDocument>()
6208            .unwrap()
6209            .get("plugins")
6210            .unwrap(),
6211    )
6212    .unwrap();
6213    assert_eq!(
6214        deserialized, deserialized_from_serialized,
6215        "Deserialized serialized config equals original config"
6216    );
6217    insta::assert_snapshot!(serialized.to_string());
6218}
6219
6220#[test]
6221fn ui_config_to_string() {
6222    let fake_config = r##"
6223        ui {
6224            pane_frames {
6225                rounded_corners true
6226                hide_session_name true
6227            }
6228        }"##;
6229    let document: KdlDocument = fake_config.parse().unwrap();
6230    let deserialized = UiConfig::from_kdl(document.get("ui").unwrap()).unwrap();
6231    let serialized = UiConfig::to_kdl(&deserialized).unwrap();
6232    let deserialized_from_serialized = UiConfig::from_kdl(
6233        serialized
6234            .to_string()
6235            .parse::<KdlDocument>()
6236            .unwrap()
6237            .get("ui")
6238            .unwrap(),
6239    )
6240    .unwrap();
6241    assert_eq!(
6242        deserialized, deserialized_from_serialized,
6243        "Deserialized serialized config equals original config"
6244    );
6245    insta::assert_snapshot!(serialized.to_string());
6246}
6247
6248#[test]
6249fn ui_config_to_string_with_no_ui_config() {
6250    let fake_config = r##"
6251        ui {
6252            pane_frames {
6253            }
6254        }"##;
6255    let document: KdlDocument = fake_config.parse().unwrap();
6256    let deserialized = UiConfig::from_kdl(document.get("ui").unwrap()).unwrap();
6257    assert_eq!(UiConfig::to_kdl(&deserialized), None);
6258}
6259
6260#[test]
6261fn env_vars_to_string() {
6262    let fake_config = r##"
6263        env {
6264            foo "bar"
6265            bar "foo"
6266            thing 1
6267            baz "true"
6268        }"##;
6269    let document: KdlDocument = fake_config.parse().unwrap();
6270    let deserialized = EnvironmentVariables::from_kdl(document.get("env").unwrap()).unwrap();
6271    let serialized = EnvironmentVariables::to_kdl(&deserialized).unwrap();
6272    let deserialized_from_serialized = EnvironmentVariables::from_kdl(
6273        serialized
6274            .to_string()
6275            .parse::<KdlDocument>()
6276            .unwrap()
6277            .get("env")
6278            .unwrap(),
6279    )
6280    .unwrap();
6281    assert_eq!(
6282        deserialized, deserialized_from_serialized,
6283        "Deserialized serialized config equals original config"
6284    );
6285    insta::assert_snapshot!(serialized.to_string());
6286}
6287
6288#[test]
6289fn env_vars_to_string_with_no_env_vars() {
6290    let fake_config = r##"
6291        env {
6292        }"##;
6293    let document: KdlDocument = fake_config.parse().unwrap();
6294    let deserialized = EnvironmentVariables::from_kdl(document.get("env").unwrap()).unwrap();
6295    assert_eq!(EnvironmentVariables::to_kdl(&deserialized), None);
6296}
6297
6298#[test]
6299fn config_options_to_string() {
6300    let fake_config = r##"
6301        simplified_ui true
6302        theme "dracula"
6303        default_mode "locked"
6304        default_shell "fish"
6305        default_cwd "/tmp/foo"
6306        default_layout "compact"
6307        layout_dir "/tmp/layouts"
6308        theme_dir "/tmp/themes"
6309        mouse_mode false
6310        pane_frames false
6311        mirror_session true
6312        on_force_close "quit"
6313        scroll_buffer_size 100
6314        copy_command "pbcopy"
6315        copy_clipboard "system"
6316        copy_on_select false
6317        scrollback_editor "vim"
6318        session_name "my_cool_session"
6319        attach_to_session false
6320        auto_layout false
6321        session_serialization true
6322        serialize_pane_viewport false
6323        scrollback_lines_to_serialize 1000
6324        styled_underlines false
6325        serialization_interval 1
6326        disable_session_metadata true
6327        support_kitty_keyboard_protocol false
6328        web_server true
6329        web_sharing "disabled"
6330    "##;
6331    let document: KdlDocument = fake_config.parse().unwrap();
6332    let deserialized = Options::from_kdl(&document).unwrap();
6333    let mut serialized = Options::to_kdl(&deserialized, false);
6334    let mut fake_document = KdlDocument::new();
6335    fake_document.nodes_mut().append(&mut serialized);
6336    let deserialized_from_serialized =
6337        Options::from_kdl(&fake_document.to_string().parse::<KdlDocument>().unwrap()).unwrap();
6338    assert_eq!(
6339        deserialized, deserialized_from_serialized,
6340        "Deserialized serialized config equals original config"
6341    );
6342    insta::assert_snapshot!(fake_document.to_string());
6343}
6344
6345#[test]
6346fn config_options_to_string_with_comments() {
6347    let fake_config = r##"
6348        simplified_ui true
6349        theme "dracula"
6350        default_mode "locked"
6351        default_shell "fish"
6352        default_cwd "/tmp/foo"
6353        default_layout "compact"
6354        layout_dir "/tmp/layouts"
6355        theme_dir "/tmp/themes"
6356        mouse_mode false
6357        pane_frames false
6358        mirror_session true
6359        on_force_close "quit"
6360        scroll_buffer_size 100
6361        copy_command "pbcopy"
6362        copy_clipboard "system"
6363        copy_on_select false
6364        scrollback_editor "vim"
6365        session_name "my_cool_session"
6366        attach_to_session false
6367        auto_layout false
6368        session_serialization true
6369        serialize_pane_viewport false
6370        scrollback_lines_to_serialize 1000
6371        styled_underlines false
6372        serialization_interval 1
6373        disable_session_metadata true
6374        support_kitty_keyboard_protocol false
6375        web_server true
6376        web_sharing "disabled"
6377    "##;
6378    let document: KdlDocument = fake_config.parse().unwrap();
6379    let deserialized = Options::from_kdl(&document).unwrap();
6380    let mut serialized = Options::to_kdl(&deserialized, true);
6381    let mut fake_document = KdlDocument::new();
6382    fake_document.nodes_mut().append(&mut serialized);
6383    let deserialized_from_serialized =
6384        Options::from_kdl(&fake_document.to_string().parse::<KdlDocument>().unwrap()).unwrap();
6385    assert_eq!(
6386        deserialized, deserialized_from_serialized,
6387        "Deserialized serialized config equals original config"
6388    );
6389    insta::assert_snapshot!(fake_document.to_string());
6390}
6391
6392#[test]
6393fn config_options_to_string_without_options() {
6394    let fake_config = r##"
6395    "##;
6396    let document: KdlDocument = fake_config.parse().unwrap();
6397    let deserialized = Options::from_kdl(&document).unwrap();
6398    let mut serialized = Options::to_kdl(&deserialized, false);
6399    let mut fake_document = KdlDocument::new();
6400    fake_document.nodes_mut().append(&mut serialized);
6401    let deserialized_from_serialized =
6402        Options::from_kdl(&fake_document.to_string().parse::<KdlDocument>().unwrap()).unwrap();
6403    assert_eq!(
6404        deserialized, deserialized_from_serialized,
6405        "Deserialized serialized config equals original config"
6406    );
6407    insta::assert_snapshot!(fake_document.to_string());
6408}
6409
6410#[test]
6411fn config_options_to_string_with_some_options() {
6412    let fake_config = r##"
6413        default_layout "compact"
6414    "##;
6415    let document: KdlDocument = fake_config.parse().unwrap();
6416    let deserialized = Options::from_kdl(&document).unwrap();
6417    let mut serialized = Options::to_kdl(&deserialized, false);
6418    let mut fake_document = KdlDocument::new();
6419    fake_document.nodes_mut().append(&mut serialized);
6420    let deserialized_from_serialized =
6421        Options::from_kdl(&fake_document.to_string().parse::<KdlDocument>().unwrap()).unwrap();
6422    assert_eq!(
6423        deserialized, deserialized_from_serialized,
6424        "Deserialized serialized config equals original config"
6425    );
6426    insta::assert_snapshot!(fake_document.to_string());
6427}
6428
6429#[test]
6430fn bare_config_from_default_assets_to_string() {
6431    let fake_config = Config::from_default_assets().unwrap();
6432    let fake_config_stringified = fake_config.to_string(false);
6433    let deserialized_from_serialized = Config::from_kdl(&fake_config_stringified, None).unwrap();
6434    assert_eq!(
6435        fake_config, deserialized_from_serialized,
6436        "Deserialized serialized config equals original config"
6437    );
6438    insta::assert_snapshot!(fake_config_stringified);
6439}
6440
6441#[test]
6442fn bare_config_from_default_assets_to_string_with_comments() {
6443    let fake_config = Config::from_default_assets().unwrap();
6444    let fake_config_stringified = fake_config.to_string(true);
6445    let deserialized_from_serialized = Config::from_kdl(&fake_config_stringified, None).unwrap();
6446    assert_eq!(
6447        fake_config, deserialized_from_serialized,
6448        "Deserialized serialized config equals original config"
6449    );
6450    insta::assert_snapshot!(fake_config_stringified);
6451}