Skip to main content

zellij_utils/kdl/
mod.rs

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