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, 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 default_mode =
2787            match kdl_property_first_arg_as_string_or_error!(kdl_options, "default_mode") {
2788                Some((string, entry)) => Some(InputMode::from_str(string).map_err(|_| {
2789                    kdl_parsing_error!(format!("Invalid input mode: '{}'", string), entry)
2790                })?),
2791                None => None,
2792            };
2793        let default_layout =
2794            kdl_property_first_arg_as_string_or_error!(kdl_options, "default_layout")
2795                .map(|(string, _entry)| PathBuf::from(string));
2796        let layout_dir = kdl_property_first_arg_as_string_or_error!(kdl_options, "layout_dir")
2797            .map(|(string, _entry)| PathBuf::from(string));
2798        let theme_dir = kdl_property_first_arg_as_string_or_error!(kdl_options, "theme_dir")
2799            .map(|(string, _entry)| PathBuf::from(string));
2800        let mouse_mode =
2801            kdl_property_first_arg_as_bool_or_error!(kdl_options, "mouse_mode").map(|(v, _)| v);
2802        let scroll_buffer_size =
2803            kdl_property_first_arg_as_i64_or_error!(kdl_options, "scroll_buffer_size")
2804                .map(|(scroll_buffer_size, _entry)| scroll_buffer_size as usize);
2805        let copy_command = kdl_property_first_arg_as_string_or_error!(kdl_options, "copy_command")
2806            .map(|(copy_command, _entry)| copy_command.to_string());
2807        let copy_clipboard =
2808            match kdl_property_first_arg_as_string_or_error!(kdl_options, "copy_clipboard") {
2809                Some((string, entry)) => Some(Clipboard::from_str(string).map_err(|_| {
2810                    kdl_parsing_error!(
2811                        format!("Invalid value for copy_clipboard: '{}'", string),
2812                        entry
2813                    )
2814                })?),
2815                None => None,
2816            };
2817        let copy_on_select =
2818            kdl_property_first_arg_as_bool_or_error!(kdl_options, "copy_on_select").map(|(v, _)| v);
2819        let osc8_hyperlinks =
2820            kdl_property_first_arg_as_bool_or_error!(kdl_options, "osc8_hyperlinks")
2821                .map(|(v, _)| v);
2822        let scrollback_editor =
2823            kdl_property_first_arg_as_string_or_error!(kdl_options, "scrollback_editor")
2824                .map(|(string, _entry)| PathBuf::from(string));
2825        let mirror_session =
2826            kdl_property_first_arg_as_bool_or_error!(kdl_options, "mirror_session").map(|(v, _)| v);
2827        let session_name = kdl_property_first_arg_as_string_or_error!(kdl_options, "session_name")
2828            .map(|(session_name, _entry)| session_name.to_string());
2829        let attach_to_session =
2830            kdl_property_first_arg_as_bool_or_error!(kdl_options, "attach_to_session")
2831                .map(|(v, _)| v);
2832        let session_serialization =
2833            kdl_property_first_arg_as_bool_or_error!(kdl_options, "session_serialization")
2834                .map(|(v, _)| v);
2835        let serialize_pane_viewport =
2836            kdl_property_first_arg_as_bool_or_error!(kdl_options, "serialize_pane_viewport")
2837                .map(|(v, _)| v);
2838        let scrollback_lines_to_serialize =
2839            kdl_property_first_arg_as_i64_or_error!(kdl_options, "scrollback_lines_to_serialize")
2840                .map(|(v, _)| v as usize);
2841        let styled_underlines =
2842            kdl_property_first_arg_as_bool_or_error!(kdl_options, "styled_underlines")
2843                .map(|(v, _)| v);
2844        let serialization_interval =
2845            kdl_property_first_arg_as_i64_or_error!(kdl_options, "serialization_interval")
2846                .map(|(scroll_buffer_size, _entry)| scroll_buffer_size as u64);
2847        let disable_session_metadata =
2848            kdl_property_first_arg_as_bool_or_error!(kdl_options, "disable_session_metadata")
2849                .map(|(v, _)| v);
2850        let support_kitty_keyboard_protocol = kdl_property_first_arg_as_bool_or_error!(
2851            kdl_options,
2852            "support_kitty_keyboard_protocol"
2853        )
2854        .map(|(v, _)| v);
2855        let support_kitty_graphics_protocol = kdl_property_first_arg_as_bool_or_error!(
2856            kdl_options,
2857            "support_kitty_graphics_protocol"
2858        )
2859        .map(|(v, _)| v);
2860        let web_server =
2861            kdl_property_first_arg_as_bool_or_error!(kdl_options, "web_server").map(|(v, _)| v);
2862        let web_sharing =
2863            match kdl_property_first_arg_as_string_or_error!(kdl_options, "web_sharing") {
2864                Some((string, entry)) => Some(WebSharing::from_str(string).map_err(|_| {
2865                    kdl_parsing_error!(
2866                        format!("Invalid value for web_sharing: '{}'", string),
2867                        entry
2868                    )
2869                })?),
2870                None => None,
2871            };
2872        let stacked_resize =
2873            kdl_property_first_arg_as_bool_or_error!(kdl_options, "stacked_resize").map(|(v, _)| v);
2874        let stacked_pane_list =
2875            kdl_property_first_arg_as_bool_or_error!(kdl_options, "stacked_pane_list")
2876                .map(|(v, _)| v);
2877        let show_startup_tips =
2878            kdl_property_first_arg_as_bool_or_error!(kdl_options, "show_startup_tips")
2879                .map(|(v, _)| v);
2880        let show_release_notes =
2881            kdl_property_first_arg_as_bool_or_error!(kdl_options, "show_release_notes")
2882                .map(|(v, _)| v);
2883        let advanced_mouse_actions =
2884            kdl_property_first_arg_as_bool_or_error!(kdl_options, "advanced_mouse_actions")
2885                .map(|(v, _)| v);
2886        let mouse_scroll_resize =
2887            kdl_property_first_arg_as_bool_or_error!(kdl_options, "mouse_scroll_resize")
2888                .map(|(v, _)| v);
2889        let mouse_hover_effects =
2890            kdl_property_first_arg_as_bool_or_error!(kdl_options, "mouse_hover_effects")
2891                .map(|(v, _)| v);
2892        let mouse_hover_tips =
2893            kdl_property_first_arg_as_bool_or_error!(kdl_options, "mouse_hover_tips")
2894                .map(|(v, _)| v);
2895        let web_server_ip =
2896            match kdl_property_first_arg_as_string_or_error!(kdl_options, "web_server_ip") {
2897                Some((string, entry)) => Some(IpAddr::from_str(string).map_err(|_| {
2898                    kdl_parsing_error!(
2899                        format!("Invalid value for web_server_ip: '{}'", string),
2900                        entry
2901                    )
2902                })?),
2903                None => None,
2904            };
2905        let web_server_port =
2906            kdl_property_first_arg_as_i64_or_error!(kdl_options, "web_server_port")
2907                .map(|(web_server_port, _entry)| web_server_port as u16);
2908        let web_server_cert =
2909            kdl_property_first_arg_as_string_or_error!(kdl_options, "web_server_cert")
2910                .map(|(string, _entry)| PathBuf::from(string));
2911        let web_server_key =
2912            kdl_property_first_arg_as_string_or_error!(kdl_options, "web_server_key")
2913                .map(|(string, _entry)| PathBuf::from(string));
2914        let enforce_https_for_localhost =
2915            kdl_property_first_arg_as_bool_or_error!(kdl_options, "enforce_https_for_localhost")
2916                .map(|(v, _)| v);
2917        let post_command_discovery_hook =
2918            kdl_property_first_arg_as_string_or_error!(kdl_options, "post_command_discovery_hook")
2919                .map(|(hook, _entry)| hook.to_string());
2920        let client_async_worker_tasks =
2921            match kdl_property_first_arg_as_i64_or_error!(kdl_options, "client_async_worker_tasks")
2922            {
2923                Some((value, _)) if value >= 0 => Some(value as usize),
2924                Some((value, entry)) => {
2925                    return Err(kdl_parsing_error!(
2926                        format!(
2927                        "Number of client async worker tasks must be greater than 0, found '{}'",
2928                        value
2929                    ),
2930                        entry
2931                    ));
2932                },
2933                None => None,
2934            };
2935        let visual_bell =
2936            kdl_property_first_arg_as_bool_or_error!(kdl_options, "visual_bell").map(|(v, _)| v);
2937        let focus_follows_mouse =
2938            kdl_property_first_arg_as_bool_or_error!(kdl_options, "focus_follows_mouse")
2939                .map(|(v, _)| v);
2940        let mouse_click_through =
2941            kdl_property_first_arg_as_bool_or_error!(kdl_options, "mouse_click_through")
2942                .map(|(v, _)| v);
2943        let osc133_command_selection =
2944            kdl_property_first_arg_as_bool_or_error!(kdl_options, "osc133_command_selection")
2945                .map(|(v, _)| v);
2946        let word_separators =
2947            kdl_property_first_arg_as_string_or_error!(kdl_options, "word_separators")
2948                .map(|(separators, _entry)| separators.to_string());
2949        let nested_session_handling = match kdl_property_first_arg_as_string_or_error!(
2950            kdl_options,
2951            "nested_session_handling"
2952        ) {
2953            Some((value, entry)) => {
2954                use crate::input::options::NestedSessionHandling;
2955                match value.parse::<NestedSessionHandling>() {
2956                    Ok(v) => Some(v),
2957                    Err(e) => return Err(kdl_parsing_error!(e, entry)),
2958                }
2959            },
2960            None => None,
2961        };
2962        let host_notification_protocol = match kdl_property_first_arg_as_string_or_error!(
2963            kdl_options,
2964            "host_notification_protocol"
2965        ) {
2966            Some((value, entry)) => {
2967                use crate::input::options::HostNotificationProtocol;
2968                match value.parse::<HostNotificationProtocol>() {
2969                    Ok(v) => Some(v),
2970                    Err(e) => return Err(kdl_parsing_error!(e, entry)),
2971                }
2972            },
2973            None => None,
2974        };
2975        let dangerously_enable_paste_buffer_read = kdl_property_first_arg_as_bool_or_error!(
2976            kdl_options,
2977            "dangerously_enable_paste_buffer_read"
2978        )
2979        .map(|(v, _)| v);
2980
2981        Ok(Options {
2982            simplified_ui,
2983            theme,
2984            theme_dark,
2985            theme_light,
2986            default_mode,
2987            default_shell,
2988            default_cwd,
2989            default_layout,
2990            layout_dir,
2991            theme_dir,
2992            mouse_mode,
2993            pane_frames,
2994            pane_frame_style,
2995            mirror_session,
2996            on_force_close,
2997            scroll_buffer_size,
2998            copy_command,
2999            copy_clipboard,
3000            copy_on_select,
3001            osc8_hyperlinks,
3002            scrollback_editor,
3003            session_name,
3004            attach_to_session,
3005            auto_layout,
3006            session_serialization,
3007            serialize_pane_viewport,
3008            scrollback_lines_to_serialize,
3009            styled_underlines,
3010            serialization_interval,
3011            disable_session_metadata,
3012            support_kitty_keyboard_protocol,
3013            support_kitty_graphics_protocol,
3014            web_server,
3015            web_sharing,
3016            stacked_resize,
3017            stacked_pane_list,
3018            show_startup_tips,
3019            show_release_notes,
3020            advanced_mouse_actions,
3021            mouse_scroll_resize,
3022            mouse_hover_effects,
3023            mouse_hover_tips,
3024            visual_bell,
3025            focus_follows_mouse,
3026            mouse_click_through,
3027            osc133_command_selection,
3028            word_separators,
3029            host_notification_protocol,
3030            web_server_ip,
3031            web_server_port,
3032            web_server_cert,
3033            web_server_key,
3034            enforce_https_for_localhost,
3035            post_command_discovery_hook,
3036            client_async_worker_tasks,
3037            nested_session_handling,
3038            dangerously_enable_paste_buffer_read,
3039        })
3040    }
3041    pub fn from_string(stringified_keybindings: &String) -> Result<Self, ConfigError> {
3042        let document: KdlDocument = stringified_keybindings.parse()?;
3043        Options::from_kdl(&document)
3044    }
3045    fn simplified_ui_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
3046        let comment_text = format!(
3047            "{}\n{}\n{}\n{}\n{}\n{}",
3048            " ",
3049            "// Use a simplified UI without special fonts (arrow glyphs)",
3050            "// Options:",
3051            "//   - true",
3052            "//   - false (Default)",
3053            "// ",
3054        );
3055
3056        let create_node = |node_value: bool| -> KdlNode {
3057            let mut node = KdlNode::new("simplified_ui");
3058            node.push(KdlValue::Bool(node_value));
3059            node
3060        };
3061        if let Some(simplified_ui) = self.simplified_ui {
3062            let mut node = create_node(simplified_ui);
3063            if add_comments {
3064                node.set_leading(format!("{}\n", comment_text));
3065            }
3066            Some(node)
3067        } else if add_comments {
3068            let mut node = create_node(true);
3069            node.set_leading(format!("{}\n// ", comment_text));
3070            Some(node)
3071        } else {
3072            None
3073        }
3074    }
3075    fn osc8_hyperlinks_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
3076        let comment_text = format!(
3077            "{}\n{}\n{}\n{}\n{}\n{}",
3078            " ",
3079            "// Enable OSC8 hyperlink output",
3080            "// Options:",
3081            "//   - true (Default)",
3082            "//   - false",
3083            "// ",
3084        );
3085
3086        let create_node = |node_value: bool| -> KdlNode {
3087            let mut node = KdlNode::new("osc8_hyperlinks");
3088            node.push(KdlValue::Bool(node_value));
3089            node
3090        };
3091        if let Some(osc8_hyperlinks) = self.osc8_hyperlinks {
3092            let mut node = create_node(osc8_hyperlinks);
3093            if add_comments {
3094                node.set_leading(format!("{}\n", comment_text));
3095            }
3096            Some(node)
3097        } else if add_comments {
3098            let mut node = create_node(true);
3099            node.set_leading(format!("{}\n// ", comment_text));
3100            Some(node)
3101        } else {
3102            None
3103        }
3104    }
3105    fn theme_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
3106        let comment_text = format!(
3107            "{}\n{}\n{}\n{}",
3108            " ",
3109            "// Choose the theme that is specified in the themes section.",
3110            "// Default: default",
3111            "// ",
3112        );
3113
3114        let create_node = |node_value: &str| -> KdlNode {
3115            let mut node = KdlNode::new("theme");
3116            node.push(node_value.to_owned());
3117            node
3118        };
3119        if let Some(theme) = &self.theme {
3120            let mut node = create_node(theme);
3121            if add_comments {
3122                node.set_leading(format!("{}\n", comment_text));
3123            }
3124            Some(node)
3125        } else if add_comments {
3126            let mut node = create_node("dracula");
3127            node.set_leading(format!("{}\n// ", comment_text));
3128            Some(node)
3129        } else {
3130            None
3131        }
3132    }
3133    fn theme_dark_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
3134        let comment_text = format!(
3135            "{}\n{}\n{}\n{}",
3136            " ",
3137            "// Theme to use when the host terminal reports a dark color palette.",
3138            "// Requires `theme_light` to also be set; otherwise `theme` is used.",
3139            "// ",
3140        );
3141
3142        let create_node = |node_value: &str| -> KdlNode {
3143            let mut node = KdlNode::new("theme_dark");
3144            node.push(node_value.to_owned());
3145            node
3146        };
3147        if let Some(theme) = &self.theme_dark {
3148            let mut node = create_node(theme);
3149            if add_comments {
3150                node.set_leading(format!("{}\n", comment_text));
3151            }
3152            Some(node)
3153        } else if add_comments {
3154            let mut node = create_node("dracula");
3155            node.set_leading(format!("{}\n// ", comment_text));
3156            Some(node)
3157        } else {
3158            None
3159        }
3160    }
3161    fn theme_light_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
3162        let comment_text = format!(
3163            "{}\n{}\n{}\n{}",
3164            " ",
3165            "// Theme to use when the host terminal reports a light color palette.",
3166            "// Requires `theme_dark` to also be set; otherwise `theme` is used.",
3167            "// ",
3168        );
3169
3170        let create_node = |node_value: &str| -> KdlNode {
3171            let mut node = KdlNode::new("theme_light");
3172            node.push(node_value.to_owned());
3173            node
3174        };
3175        if let Some(theme) = &self.theme_light {
3176            let mut node = create_node(theme);
3177            if add_comments {
3178                node.set_leading(format!("{}\n", comment_text));
3179            }
3180            Some(node)
3181        } else if add_comments {
3182            let mut node = create_node("solarized-light");
3183            node.set_leading(format!("{}\n// ", comment_text));
3184            Some(node)
3185        } else {
3186            None
3187        }
3188    }
3189    fn default_mode_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
3190        let comment_text = format!(
3191            "{}\n{}\n{}\n{}",
3192            " ", "// Choose the base input mode of zellij.", "// Default: normal", "// "
3193        );
3194
3195        let create_node = |default_mode: &InputMode| -> KdlNode {
3196            let mut node = KdlNode::new("default_mode");
3197            node.push(format!("{:?}", default_mode).to_lowercase());
3198            node
3199        };
3200        if let Some(default_mode) = &self.default_mode {
3201            let mut node = create_node(default_mode);
3202            if add_comments {
3203                node.set_leading(format!("{}\n", comment_text));
3204            }
3205            Some(node)
3206        } else if add_comments {
3207            let mut node = create_node(&InputMode::Locked);
3208            node.set_leading(format!("{}\n// ", comment_text));
3209            Some(node)
3210        } else {
3211            None
3212        }
3213    }
3214    fn default_shell_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
3215        let comment_text =
3216            format!("{}\n{}\n{}\n{}",
3217            " ",
3218            "// Choose the path to the default shell that zellij will use for opening new panes",
3219            "// Default: $SHELL",
3220            "// ",
3221        );
3222
3223        let create_node = |node_value: &str| -> KdlNode {
3224            let mut node = KdlNode::new("default_shell");
3225            node.push(node_value.to_owned());
3226            node
3227        };
3228        if let Some(default_shell) = &self.default_shell {
3229            let mut node = create_node(&default_shell.display().to_string());
3230            if add_comments {
3231                node.set_leading(format!("{}\n", comment_text));
3232            }
3233            Some(node)
3234        } else if add_comments {
3235            let mut node = create_node("fish");
3236            node.set_leading(format!("{}\n// ", comment_text));
3237            Some(node)
3238        } else {
3239            None
3240        }
3241    }
3242    fn default_cwd_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
3243        let comment_text = format!(
3244            "{}\n{}\n{}",
3245            " ",
3246            "// Choose the path to override cwd that zellij will use for opening new panes",
3247            "// ",
3248        );
3249
3250        let create_node = |node_value: &str| -> KdlNode {
3251            let mut node = KdlNode::new("default_cwd");
3252            node.push(node_value.to_owned());
3253            node
3254        };
3255        if let Some(default_cwd) = &self.default_cwd {
3256            let mut node = create_node(&default_cwd.display().to_string());
3257            if add_comments {
3258                node.set_leading(format!("{}\n", comment_text));
3259            }
3260            Some(node)
3261        } else if add_comments {
3262            let mut node = create_node("/tmp");
3263            node.set_leading(format!("{}\n// ", comment_text));
3264            Some(node)
3265        } else {
3266            None
3267        }
3268    }
3269    fn default_layout_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
3270        let comment_text = format!(
3271            "{}\n{}\n{}\n{}",
3272            " ",
3273            "// The name of the default layout to load on startup",
3274            "// Default: \"default\"",
3275            "// ",
3276        );
3277
3278        let create_node = |node_value: &str| -> KdlNode {
3279            let mut node = KdlNode::new("default_layout");
3280            node.push(node_value.to_owned());
3281            node
3282        };
3283        if let Some(default_layout) = &self.default_layout {
3284            let mut node = create_node(&default_layout.display().to_string());
3285            if add_comments {
3286                node.set_leading(format!("{}\n", comment_text));
3287            }
3288            Some(node)
3289        } else if add_comments {
3290            let mut node = create_node("compact");
3291            node.set_leading(format!("{}\n// ", comment_text));
3292            Some(node)
3293        } else {
3294            None
3295        }
3296    }
3297    fn layout_dir_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
3298        let comment_text = format!(
3299            "{}\n{}\n{}\n{}",
3300            " ",
3301            "// The folder in which Zellij will look for layouts",
3302            "// (Requires restart)",
3303            "// ",
3304        );
3305
3306        let create_node = |node_value: &str| -> KdlNode {
3307            let mut node = KdlNode::new("layout_dir");
3308            node.push(node_value.to_owned());
3309            node
3310        };
3311        if let Some(layout_dir) = &self.layout_dir {
3312            let mut node = create_node(&layout_dir.display().to_string());
3313            if add_comments {
3314                node.set_leading(format!("{}\n", comment_text));
3315            }
3316            Some(node)
3317        } else if add_comments {
3318            let mut node = create_node("/tmp");
3319            node.set_leading(format!("{}\n// ", comment_text));
3320            Some(node)
3321        } else {
3322            None
3323        }
3324    }
3325    fn theme_dir_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
3326        let comment_text = format!(
3327            "{}\n{}\n{}\n{}",
3328            " ",
3329            "// The folder in which Zellij will look for themes",
3330            "// (Requires restart)",
3331            "// ",
3332        );
3333
3334        let create_node = |node_value: &str| -> KdlNode {
3335            let mut node = KdlNode::new("theme_dir");
3336            node.push(node_value.to_owned());
3337            node
3338        };
3339        if let Some(theme_dir) = &self.theme_dir {
3340            let mut node = create_node(&theme_dir.display().to_string());
3341            if add_comments {
3342                node.set_leading(format!("{}\n", comment_text));
3343            }
3344            Some(node)
3345        } else if add_comments {
3346            let mut node = create_node("/tmp");
3347            node.set_leading(format!("{}\n// ", comment_text));
3348            Some(node)
3349        } else {
3350            None
3351        }
3352    }
3353    fn mouse_mode_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
3354        let comment_text = format!(
3355            "{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}",
3356            " ",
3357            "// Toggle enabling the mouse mode.",
3358            "// On certain configurations, or terminals this could",
3359            "// potentially interfere with copying text.",
3360            "// Options:",
3361            "//   - true (default)",
3362            "//   - false",
3363            "// ",
3364        );
3365
3366        let create_node = |node_value: bool| -> KdlNode {
3367            let mut node = KdlNode::new("mouse_mode");
3368            node.push(KdlValue::Bool(node_value));
3369            node
3370        };
3371        if let Some(mouse_mode) = self.mouse_mode {
3372            let mut node = create_node(mouse_mode);
3373            if add_comments {
3374                node.set_leading(format!("{}\n", comment_text));
3375            }
3376            Some(node)
3377        } else if add_comments {
3378            let mut node = create_node(false);
3379            node.set_leading(format!("{}\n// ", comment_text));
3380            Some(node)
3381        } else {
3382            None
3383        }
3384    }
3385    fn pane_frames_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
3386        let comment_text = format!(
3387            "{}\n{}\n{}\n{}\n{}\n{}",
3388            " ",
3389            "// Toggle having pane frames around the panes",
3390            "// Options:",
3391            "//   - true (default, enabled)",
3392            "//   - false",
3393            "// ",
3394        );
3395
3396        let create_node = |node_value: bool| -> KdlNode {
3397            let mut node = KdlNode::new("pane_frames");
3398            node.push(KdlValue::Bool(node_value));
3399            node
3400        };
3401        if let Some(pane_frames) = self.pane_frames {
3402            let mut node = create_node(pane_frames);
3403            if add_comments {
3404                node.set_leading(format!("{}\n", comment_text));
3405            }
3406            Some(node)
3407        } else if add_comments {
3408            let mut node = create_node(false);
3409            node.set_leading(format!("{}\n// ", comment_text));
3410            Some(node)
3411        } else {
3412            None
3413        }
3414    }
3415    fn pane_frame_style_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
3416        let comment_text = format!(
3417            "{}\n{}\n{}\n{}\n{}\n{}",
3418            " ",
3419            "// Set the pane frame style when pane_frames is enabled",
3420            "// Options:",
3421            "//   - full",
3422            "//   - titles (default)",
3423            "// ",
3424        );
3425
3426        let style_as_str = |style: &PaneFrameStyle| -> &'static str {
3427            match style {
3428                PaneFrameStyle::Full => "full",
3429                PaneFrameStyle::Titles => "titles",
3430                PaneFrameStyle::None => "none",
3431            }
3432        };
3433
3434        let create_node = |node_value: &str| -> KdlNode {
3435            let mut node = KdlNode::new("pane_frame_style");
3436            node.push(node_value.to_owned());
3437            node
3438        };
3439        if let Some(pane_frame_style) = &self.pane_frame_style {
3440            let mut node = create_node(style_as_str(pane_frame_style));
3441            if add_comments {
3442                node.set_leading(format!("{}\n", comment_text));
3443            }
3444            Some(node)
3445        } else if add_comments {
3446            let mut node = create_node("titles");
3447            node.set_leading(format!("{}\n// ", comment_text));
3448            Some(node)
3449        } else {
3450            None
3451        }
3452    }
3453    fn mirror_session_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
3454        let comment_text = format!(
3455            "{}\n{}\n{}\n{}\n{}\n{}\n{}",
3456            " ",
3457            "// When attaching to an existing session with other users,",
3458            "// should the session be mirrored (true)",
3459            "// or should each user have their own cursor (false)",
3460            "// (Requires restart)",
3461            "// Default: false",
3462            "// ",
3463        );
3464
3465        let create_node = |node_value: bool| -> KdlNode {
3466            let mut node = KdlNode::new("mirror_session");
3467            node.push(KdlValue::Bool(node_value));
3468            node
3469        };
3470        if let Some(mirror_session) = self.mirror_session {
3471            let mut node = create_node(mirror_session);
3472            if add_comments {
3473                node.set_leading(format!("{}\n", comment_text));
3474            }
3475            Some(node)
3476        } else if add_comments {
3477            let mut node = create_node(true);
3478            node.set_leading(format!("{}\n// ", comment_text));
3479            Some(node)
3480        } else {
3481            None
3482        }
3483    }
3484    fn on_force_close_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
3485        let comment_text = format!(
3486            "{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}",
3487            " ",
3488            "// Choose what to do when zellij receives SIGTERM, SIGINT, SIGQUIT or SIGHUP",
3489            "// eg. when terminal window with an active zellij session is closed",
3490            "// (Requires restart)",
3491            "// Options:",
3492            "//   - detach (Default)",
3493            "//   - quit",
3494            "// ",
3495        );
3496
3497        let create_node = |node_value: &str| -> KdlNode {
3498            let mut node = KdlNode::new("on_force_close");
3499            node.push(node_value.to_owned());
3500            node
3501        };
3502        if let Some(on_force_close) = &self.on_force_close {
3503            let mut node = match on_force_close {
3504                OnForceClose::Detach => create_node("detach"),
3505                OnForceClose::Quit => create_node("quit"),
3506            };
3507            if add_comments {
3508                node.set_leading(format!("{}\n", comment_text));
3509            }
3510            Some(node)
3511        } else if add_comments {
3512            let mut node = create_node("quit");
3513            node.set_leading(format!("{}\n// ", comment_text));
3514            Some(node)
3515        } else {
3516            None
3517        }
3518    }
3519    fn scroll_buffer_size_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
3520        let comment_text = format!(
3521            "{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}",
3522            " ",
3523            "// Configure the scroll back buffer size",
3524            "// This is the number of lines zellij stores for each pane in the scroll back",
3525            "// buffer. Excess number of lines are discarded in a FIFO fashion.",
3526            "// (Requires restart)",
3527            "// Valid values: positive integers",
3528            "// Default value: 10000",
3529            "// ",
3530        );
3531
3532        let create_node = |node_value: usize| -> KdlNode {
3533            let mut node = KdlNode::new("scroll_buffer_size");
3534            node.push(KdlValue::Base10(node_value as i64));
3535            node
3536        };
3537        if let Some(scroll_buffer_size) = self.scroll_buffer_size {
3538            let mut node = create_node(scroll_buffer_size);
3539            if add_comments {
3540                node.set_leading(format!("{}\n", comment_text));
3541            }
3542            Some(node)
3543        } else if add_comments {
3544            let mut node = create_node(10000);
3545            node.set_leading(format!("{}\n// ", comment_text));
3546            Some(node)
3547        } else {
3548            None
3549        }
3550    }
3551    fn copy_command_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
3552        let comment_text = format!(
3553            "{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}",
3554            " ",
3555            "// Provide a command to execute when copying text. The text will be piped to",
3556            "// the stdin of the program to perform the copy. This can be used with",
3557            "// terminal emulators which do not support the OSC 52 ANSI control sequence",
3558            "// that will be used by default if this option is not set.",
3559            "// Examples:",
3560            "//",
3561            "// copy_command \"xclip -selection clipboard\" // x11",
3562            "// copy_command \"wl-copy\"                    // wayland",
3563            "// copy_command \"pbcopy\"                     // osx",
3564            "// ",
3565        );
3566
3567        let create_node = |node_value: &str| -> KdlNode {
3568            let mut node = KdlNode::new("copy_command");
3569            node.push(node_value.to_owned());
3570            node
3571        };
3572        if let Some(copy_command) = &self.copy_command {
3573            let mut node = create_node(copy_command);
3574            if add_comments {
3575                node.set_leading(format!("{}\n", comment_text));
3576            }
3577            Some(node)
3578        } else if add_comments {
3579            let mut node = create_node("pbcopy");
3580            node.set_leading(format!("{}\n// ", comment_text));
3581            Some(node)
3582        } else {
3583            None
3584        }
3585    }
3586    fn copy_clipboard_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
3587        let comment_text = format!("{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}",
3588            " ",
3589            "// Choose the destination for copied text",
3590            "// Allows using the primary selection buffer (on x11/wayland) instead of the system clipboard.",
3591            "// Does not apply when using copy_command.",
3592            "// Options:",
3593            "//   - system (default)",
3594            "//   - primary",
3595            "// ",
3596        );
3597
3598        let create_node = |node_value: &str| -> KdlNode {
3599            let mut node = KdlNode::new("copy_clipboard");
3600            node.push(node_value.to_owned());
3601            node
3602        };
3603        if let Some(copy_clipboard) = &self.copy_clipboard {
3604            let mut node = match copy_clipboard {
3605                Clipboard::Primary => create_node("primary"),
3606                Clipboard::System => create_node("system"),
3607            };
3608            if add_comments {
3609                node.set_leading(format!("{}\n", comment_text));
3610            }
3611            Some(node)
3612        } else if add_comments {
3613            let mut node = create_node("primary");
3614            node.set_leading(format!("{}\n// ", comment_text));
3615            Some(node)
3616        } else {
3617            None
3618        }
3619    }
3620    fn copy_on_select_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
3621        let comment_text = format!(
3622            "{}\n{}\n{}\n{}",
3623            " ",
3624            "// Enable automatic copying (and clearing) of selection when releasing mouse",
3625            "// Default: true",
3626            "// ",
3627        );
3628
3629        let create_node = |node_value: bool| -> KdlNode {
3630            let mut node = KdlNode::new("copy_on_select");
3631            node.push(KdlValue::Bool(node_value));
3632            node
3633        };
3634        if let Some(copy_on_select) = self.copy_on_select {
3635            let mut node = create_node(copy_on_select);
3636            if add_comments {
3637                node.set_leading(format!("{}\n", comment_text));
3638            }
3639            Some(node)
3640        } else if add_comments {
3641            let mut node = create_node(true);
3642            node.set_leading(format!("{}\n// ", comment_text));
3643            Some(node)
3644        } else {
3645            None
3646        }
3647    }
3648    fn scrollback_editor_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
3649        let comment_text = format!(
3650            "{}\n{}\n{}",
3651            " ",
3652            "// Path to the default editor to use to edit pane scrollbuffer",
3653            "// Default: $EDITOR or $VISUAL",
3654        );
3655
3656        let create_node = |node_value: &str| -> KdlNode {
3657            let mut node = KdlNode::new("scrollback_editor");
3658            node.push(node_value.to_owned());
3659            node
3660        };
3661        if let Some(scrollback_editor) = &self.scrollback_editor {
3662            let mut node = create_node(&scrollback_editor.display().to_string());
3663            if add_comments {
3664                node.set_leading(format!("{}\n", comment_text));
3665            }
3666            Some(node)
3667        } else if add_comments {
3668            let mut node = create_node("/usr/bin/vim");
3669            node.set_leading(format!("{}\n// ", comment_text));
3670            Some(node)
3671        } else {
3672            None
3673        }
3674    }
3675    fn session_name_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
3676        let comment_text = format!(
3677            "{}\n{}\n{}\n{}\n{}\n{}",
3678            " ",
3679            "// A fixed name to always give the Zellij session.",
3680            "// Consider also setting `attach_to_session true,`",
3681            "// otherwise this will error if such a session exists.",
3682            "// Default: <RANDOM>",
3683            "// ",
3684        );
3685
3686        let create_node = |node_value: &str| -> KdlNode {
3687            let mut node = KdlNode::new("session_name");
3688            node.push(node_value.to_owned());
3689            node
3690        };
3691        if let Some(session_name) = &self.session_name {
3692            let mut node = create_node(&session_name);
3693            if add_comments {
3694                node.set_leading(format!("{}\n", comment_text));
3695            }
3696            Some(node)
3697        } else if add_comments {
3698            let mut node = create_node("My singleton session");
3699            node.set_leading(format!("{}\n// ", comment_text));
3700            Some(node)
3701        } else {
3702            None
3703        }
3704    }
3705    fn attach_to_session_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
3706        let comment_text = format!(
3707            "{}\n{}\n{}\n{}\n{}",
3708            " ",
3709            "// When `session_name` is provided, attaches to that session",
3710            "// if it is already running or creates it otherwise.",
3711            "// Default: false",
3712            "// ",
3713        );
3714
3715        let create_node = |node_value: bool| -> KdlNode {
3716            let mut node = KdlNode::new("attach_to_session");
3717            node.push(KdlValue::Bool(node_value));
3718            node
3719        };
3720        if let Some(attach_to_session) = self.attach_to_session {
3721            let mut node = create_node(attach_to_session);
3722            if add_comments {
3723                node.set_leading(format!("{}\n", comment_text));
3724            }
3725            Some(node)
3726        } else if add_comments {
3727            let mut node = create_node(true);
3728            node.set_leading(format!("{}\n// ", comment_text));
3729            Some(node)
3730        } else {
3731            None
3732        }
3733    }
3734    fn auto_layout_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
3735        let comment_text = format!("{}\n{}\n{}\n{}\n{}\n{}",
3736            " ",
3737            "// Toggle between having Zellij lay out panes according to a predefined set of layouts whenever possible",
3738            "// Options:",
3739            "//   - true (default)",
3740            "//   - false",
3741            "// ",
3742        );
3743
3744        let create_node = |node_value: bool| -> KdlNode {
3745            let mut node = KdlNode::new("auto_layout");
3746            node.push(KdlValue::Bool(node_value));
3747            node
3748        };
3749        if let Some(auto_layout) = self.auto_layout {
3750            let mut node = create_node(auto_layout);
3751            if add_comments {
3752                node.set_leading(format!("{}\n", comment_text));
3753            }
3754            Some(node)
3755        } else if add_comments {
3756            let mut node = create_node(false);
3757            node.set_leading(format!("{}\n// ", comment_text));
3758            Some(node)
3759        } else {
3760            None
3761        }
3762    }
3763    fn session_serialization_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
3764        let comment_text = format!("{}\n{}\n{}\n{}\n{}\n{}",
3765            " ",
3766            "// Whether sessions should be serialized to the cache folder (including their tabs/panes, cwds and running commands) so that they can later be resurrected",
3767            "// Options:",
3768            "//   - true (default)",
3769            "//   - false",
3770            "// ",
3771        );
3772
3773        let create_node = |node_value: bool| -> KdlNode {
3774            let mut node = KdlNode::new("session_serialization");
3775            node.push(KdlValue::Bool(node_value));
3776            node
3777        };
3778        if let Some(session_serialization) = self.session_serialization {
3779            let mut node = create_node(session_serialization);
3780            if add_comments {
3781                node.set_leading(format!("{}\n", comment_text));
3782            }
3783            Some(node)
3784        } else if add_comments {
3785            let mut node = create_node(false);
3786            node.set_leading(format!("{}\n// ", comment_text));
3787            Some(node)
3788        } else {
3789            None
3790        }
3791    }
3792    fn serialize_pane_viewport_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
3793        let comment_text = format!(
3794            "{}\n{}\n{}\n{}\n{}\n{}",
3795            " ",
3796            "// Whether pane viewports are serialized along with the session, default is false",
3797            "// Options:",
3798            "//   - true",
3799            "//   - false (default)",
3800            "// ",
3801        );
3802
3803        let create_node = |node_value: bool| -> KdlNode {
3804            let mut node = KdlNode::new("serialize_pane_viewport");
3805            node.push(KdlValue::Bool(node_value));
3806            node
3807        };
3808        if let Some(serialize_pane_viewport) = self.serialize_pane_viewport {
3809            let mut node = create_node(serialize_pane_viewport);
3810            if add_comments {
3811                node.set_leading(format!("{}\n", comment_text));
3812            }
3813            Some(node)
3814        } else if add_comments {
3815            let mut node = create_node(false);
3816            node.set_leading(format!("{}\n// ", comment_text));
3817            Some(node)
3818        } else {
3819            None
3820        }
3821    }
3822    fn scrollback_lines_to_serialize_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
3823        let comment_text = format!("{}\n{}\n{}\n{}\n{}",
3824            " ",
3825            "// Scrollback lines to serialize along with the pane viewport when serializing sessions, 0",
3826            "// defaults to the scrollback size. If this number is higher than the scrollback size, it will",
3827            "// also default to the scrollback size. This does nothing if `serialize_pane_viewport` is not true.",
3828            "// ",
3829        );
3830
3831        let create_node = |node_value: usize| -> KdlNode {
3832            let mut node = KdlNode::new("scrollback_lines_to_serialize");
3833            node.push(KdlValue::Base10(node_value as i64));
3834            node
3835        };
3836        if let Some(scrollback_lines_to_serialize) = self.scrollback_lines_to_serialize {
3837            let mut node = create_node(scrollback_lines_to_serialize);
3838            if add_comments {
3839                node.set_leading(format!("{}\n", comment_text));
3840            }
3841            Some(node)
3842        } else if add_comments {
3843            let mut node = create_node(10000);
3844            node.set_leading(format!("{}\n// ", comment_text));
3845            Some(node)
3846        } else {
3847            None
3848        }
3849    }
3850    fn styled_underlines_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
3851        let comment_text = format!(
3852            "{}\n{}\n{}\n{}\n{}\n{}",
3853            " ",
3854            "// Enable or disable the rendering of styled and colored underlines (undercurl).",
3855            "// May need to be disabled for certain unsupported terminals",
3856            "// (Requires restart)",
3857            "// Default: true",
3858            "// ",
3859        );
3860
3861        let create_node = |node_value: bool| -> KdlNode {
3862            let mut node = KdlNode::new("styled_underlines");
3863            node.push(KdlValue::Bool(node_value));
3864            node
3865        };
3866        if let Some(styled_underlines) = self.styled_underlines {
3867            let mut node = create_node(styled_underlines);
3868            if add_comments {
3869                node.set_leading(format!("{}\n", comment_text));
3870            }
3871            Some(node)
3872        } else if add_comments {
3873            let mut node = create_node(false);
3874            node.set_leading(format!("{}\n// ", comment_text));
3875            Some(node)
3876        } else {
3877            None
3878        }
3879    }
3880    fn serialization_interval_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
3881        let comment_text = format!(
3882            "{}\n{}\n{}",
3883            " ", "// How often in seconds sessions are serialized", "// ",
3884        );
3885
3886        let create_node = |node_value: u64| -> KdlNode {
3887            let mut node = KdlNode::new("serialization_interval");
3888            node.push(KdlValue::Base10(node_value as i64));
3889            node
3890        };
3891        if let Some(serialization_interval) = self.serialization_interval {
3892            let mut node = create_node(serialization_interval);
3893            if add_comments {
3894                node.set_leading(format!("{}\n", comment_text));
3895            }
3896            Some(node)
3897        } else if add_comments {
3898            let mut node = create_node(10000);
3899            node.set_leading(format!("{}\n// ", comment_text));
3900            Some(node)
3901        } else {
3902            None
3903        }
3904    }
3905    fn disable_session_metadata_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
3906        let comment_text = format!("{}\n{}\n{}\n{}\n{}\n{}",
3907            " ",
3908            "// Enable or disable writing of session metadata to disk (if disabled, other sessions might not know",
3909            "// metadata info on this session)",
3910            "// (Requires restart)",
3911            "// Default: false",
3912            "// ",
3913        );
3914
3915        let create_node = |node_value: bool| -> KdlNode {
3916            let mut node = KdlNode::new("disable_session_metadata");
3917            node.push(KdlValue::Bool(node_value));
3918            node
3919        };
3920        if let Some(disable_session_metadata) = self.disable_session_metadata {
3921            let mut node = create_node(disable_session_metadata);
3922            if add_comments {
3923                node.set_leading(format!("{}\n", comment_text));
3924            }
3925            Some(node)
3926        } else if add_comments {
3927            let mut node = create_node(false);
3928            node.set_leading(format!("{}\n// ", comment_text));
3929            Some(node)
3930        } else {
3931            None
3932        }
3933    }
3934    fn support_kitty_keyboard_protocol_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
3935        let comment_text = format!("{}\n{}\n{}\n{}\n{}",
3936            " ",
3937            "// Enable or disable support for the enhanced Kitty Keyboard Protocol (the host terminal must also support it)",
3938            "// (Requires restart)",
3939            "// Default: true (if the host terminal supports it)",
3940            "// ",
3941        );
3942
3943        let create_node = |node_value: bool| -> KdlNode {
3944            let mut node = KdlNode::new("support_kitty_keyboard_protocol");
3945            node.push(KdlValue::Bool(node_value));
3946            node
3947        };
3948        if let Some(support_kitty_keyboard_protocol) = self.support_kitty_keyboard_protocol {
3949            let mut node = create_node(support_kitty_keyboard_protocol);
3950            if add_comments {
3951                node.set_leading(format!("{}\n", comment_text));
3952            }
3953            Some(node)
3954        } else if add_comments {
3955            let mut node = create_node(false);
3956            node.set_leading(format!("{}\n// ", comment_text));
3957            Some(node)
3958        } else {
3959            None
3960        }
3961    }
3962    fn support_kitty_graphics_protocol_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
3963        let comment_text = format!("{}\n{}\n{}\n{}\n{}",
3964            " ",
3965            "// Enable or disable support for the Kitty Graphics Protocol, used to display images (the host terminal must also support it)",
3966            "// (Requires restart)",
3967            "// Default: true (if the host terminal supports it)",
3968            "// ",
3969        );
3970
3971        let create_node = |node_value: bool| -> KdlNode {
3972            let mut node = KdlNode::new("support_kitty_graphics_protocol");
3973            node.push(KdlValue::Bool(node_value));
3974            node
3975        };
3976        if let Some(support_kitty_graphics_protocol) = self.support_kitty_graphics_protocol {
3977            let mut node = create_node(support_kitty_graphics_protocol);
3978            if add_comments {
3979                node.set_leading(format!("{}\n", comment_text));
3980            }
3981            Some(node)
3982        } else if add_comments {
3983            let mut node = create_node(false);
3984            node.set_leading(format!("{}\n// ", comment_text));
3985            Some(node)
3986        } else {
3987            None
3988        }
3989    }
3990    fn web_server_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
3991        let comment_text = format!(
3992            "{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}",
3993            "// Whether to make sure a local web server is running when a new Zellij session starts.",
3994            "// This web server will allow creating new sessions and attaching to existing ones that have",
3995            "// opted in to being shared in the browser.",
3996            "// When enabled, navigate to http://127.0.0.1:8082",
3997            "// (Requires restart)",
3998            "// ",
3999            "// Note: a local web server can still be manually started from within a Zellij session or from the CLI.",
4000            "// If this is not desired, one can use a version of Zellij compiled without",
4001            "// `web_server_capability`",
4002            "// ",
4003            "// Possible values:",
4004            "// - true",
4005            "// - false",
4006            "// Default: false",
4007            "// ",
4008        );
4009
4010        let create_node = |node_value: bool| -> KdlNode {
4011            let mut node = KdlNode::new("web_server");
4012            node.push(KdlValue::Bool(node_value));
4013            node
4014        };
4015        if let Some(web_server) = self.web_server {
4016            let mut node = create_node(web_server);
4017            if add_comments {
4018                node.set_leading(format!("{}\n", comment_text));
4019            }
4020            Some(node)
4021        } else if add_comments {
4022            let mut node = create_node(false);
4023            node.set_leading(format!("{}\n// ", comment_text));
4024            Some(node)
4025        } else {
4026            None
4027        }
4028    }
4029    fn web_sharing_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
4030        let comment_text = format!(
4031            "{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}",
4032            "// Whether to allow sessions started in the terminal to be shared through a local web server, assuming one is",
4033            "// running (see the `web_server` option for more details).",
4034            "// (Requires restart)",
4035            "// ",
4036            "// Note: This is an administrative separation and not intended as a security measure.",
4037            "// ",
4038            "// Possible values:",
4039            "// - \"on\" (allow web sharing through the local web server if it",
4040            "// is online)",
4041            "// - \"off\" (do not allow web sharing unless sessions explicitly opt-in to it)",
4042            "// - \"disabled\" (do not allow web sharing and do not permit sessions started in the terminal to opt-in to it)",
4043            "// Default: \"off\"",
4044            "// ",
4045        );
4046
4047        let create_node = |node_value: &str| -> KdlNode {
4048            let mut node = KdlNode::new("web_sharing");
4049            node.push(node_value.to_owned());
4050            node
4051        };
4052        if let Some(web_sharing) = &self.web_sharing {
4053            let mut node = match web_sharing {
4054                WebSharing::On => create_node("on"),
4055                WebSharing::Off => create_node("off"),
4056                WebSharing::Disabled => create_node("disabled"),
4057            };
4058            if add_comments {
4059                node.set_leading(format!("{}\n", comment_text));
4060            }
4061            Some(node)
4062        } else if add_comments {
4063            let mut node = create_node("off");
4064            node.set_leading(format!("{}\n// ", comment_text));
4065            Some(node)
4066        } else {
4067            None
4068        }
4069    }
4070    fn web_server_cert_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
4071        let comment_text = format!(
4072            "{}\n{}\n{}",
4073            "// A path to a certificate file to be used when setting up the web client to serve the",
4074            "// connection over HTTPs",
4075            "// ",
4076        );
4077        let create_node = |node_value: &str| -> KdlNode {
4078            let mut node = KdlNode::new("web_server_cert");
4079            node.push(node_value.to_owned());
4080            node
4081        };
4082        if let Some(web_server_cert) = &self.web_server_cert {
4083            let mut node = create_node(&web_server_cert.display().to_string());
4084            if add_comments {
4085                node.set_leading(format!("{}\n", comment_text));
4086            }
4087            Some(node)
4088        } else if add_comments {
4089            let mut node = create_node("/path/to/cert.pem");
4090            node.set_leading(format!("{}\n// ", comment_text));
4091            Some(node)
4092        } else {
4093            None
4094        }
4095    }
4096    fn web_server_key_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
4097        let comment_text = format!(
4098            "{}\n{}\n{}",
4099            "// A path to a key file to be used when setting up the web client to serve the",
4100            "// connection over HTTPs",
4101            "// ",
4102        );
4103        let create_node = |node_value: &str| -> KdlNode {
4104            let mut node = KdlNode::new("web_server_key");
4105            node.push(node_value.to_owned());
4106            node
4107        };
4108        if let Some(web_server_key) = &self.web_server_key {
4109            let mut node = create_node(&web_server_key.display().to_string());
4110            if add_comments {
4111                node.set_leading(format!("{}\n", comment_text));
4112            }
4113            Some(node)
4114        } else if add_comments {
4115            let mut node = create_node("/path/to/key.pem");
4116            node.set_leading(format!("{}\n// ", comment_text));
4117            Some(node)
4118        } else {
4119            None
4120        }
4121    }
4122    fn enforce_https_for_localhost_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
4123        let comment_text = format!(
4124            "{}\n{}\n{}\n{}\n{}\n{}\n{}",
4125            "/// Whether to enforce https connections to the web server when it is bound to localhost",
4126            "/// (127.0.0.0/8)",
4127            "///",
4128            "/// Note: https is ALWAYS enforced when bound to non-local interfaces",
4129            "///",
4130            "/// Default: false",
4131            "// ",
4132        );
4133
4134        let create_node = |node_value: bool| -> KdlNode {
4135            let mut node = KdlNode::new("enforce_https_for_localhost");
4136            node.push(KdlValue::Bool(node_value));
4137            node
4138        };
4139        if let Some(enforce_https_for_localhost) = self.enforce_https_for_localhost {
4140            let mut node = create_node(enforce_https_for_localhost);
4141            if add_comments {
4142                node.set_leading(format!("{}\n", comment_text));
4143            }
4144            Some(node)
4145        } else if add_comments {
4146            let mut node = create_node(false);
4147            node.set_leading(format!("{}\n// ", comment_text));
4148            Some(node)
4149        } else {
4150            None
4151        }
4152    }
4153    fn stacked_resize_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
4154        let comment_text = format!(
4155            "{}\n{}\n{}\n{}",
4156            " ",
4157            "// Whether to stack panes when resizing beyond a certain size",
4158            "// Default: true",
4159            "// ",
4160        );
4161
4162        let create_node = |node_value: bool| -> KdlNode {
4163            let mut node = KdlNode::new("stacked_resize");
4164            node.push(KdlValue::Bool(node_value));
4165            node
4166        };
4167        if let Some(stacked_resize) = self.stacked_resize {
4168            let mut node = create_node(stacked_resize);
4169            if add_comments {
4170                node.set_leading(format!("{}\n", comment_text));
4171            }
4172            Some(node)
4173        } else if add_comments {
4174            let mut node = create_node(false);
4175            node.set_leading(format!("{}\n// ", comment_text));
4176            Some(node)
4177        } else {
4178            None
4179        }
4180    }
4181    fn stacked_pane_list_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
4182        let comment_text = format!(
4183            "{}\n{}\n{}\n{}",
4184            " ",
4185            "// Whether stacked panes display as a list with the expanded pane pinned to the bottom",
4186            "// Default: true",
4187            "// ",
4188        );
4189
4190        let create_node = |node_value: bool| -> KdlNode {
4191            let mut node = KdlNode::new("stacked_pane_list");
4192            node.push(KdlValue::Bool(node_value));
4193            node
4194        };
4195        if let Some(stacked_pane_list) = self.stacked_pane_list {
4196            let mut node = create_node(stacked_pane_list);
4197            if add_comments {
4198                node.set_leading(format!("{}\n", comment_text));
4199            }
4200            Some(node)
4201        } else if add_comments {
4202            let mut node = create_node(false);
4203            node.set_leading(format!("{}\n// ", comment_text));
4204            Some(node)
4205        } else {
4206            None
4207        }
4208    }
4209    fn show_startup_tips_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
4210        let comment_text = format!(
4211            "{}\n{}\n{}\n{}",
4212            " ", "// Whether to show tips on startup", "// Default: true", "// ",
4213        );
4214
4215        let create_node = |node_value: bool| -> KdlNode {
4216            let mut node = KdlNode::new("show_startup_tips");
4217            node.push(KdlValue::Bool(node_value));
4218            node
4219        };
4220        if let Some(show_startup_tips) = self.show_startup_tips {
4221            let mut node = create_node(show_startup_tips);
4222            if add_comments {
4223                node.set_leading(format!("{}\n", comment_text));
4224            }
4225            Some(node)
4226        } else if add_comments {
4227            let mut node = create_node(false);
4228            node.set_leading(format!("{}\n// ", comment_text));
4229            Some(node)
4230        } else {
4231            None
4232        }
4233    }
4234    fn show_release_notes_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
4235        let comment_text = format!(
4236            "{}\n{}\n{}\n{}",
4237            " ", "// Whether to show release notes on first version run", "// Default: true", "// ",
4238        );
4239
4240        let create_node = |node_value: bool| -> KdlNode {
4241            let mut node = KdlNode::new("show_release_notes");
4242            node.push(KdlValue::Bool(node_value));
4243            node
4244        };
4245        if let Some(show_release_notes) = self.show_release_notes {
4246            let mut node = create_node(show_release_notes);
4247            if add_comments {
4248                node.set_leading(format!("{}\n", comment_text));
4249            }
4250            Some(node)
4251        } else if add_comments {
4252            let mut node = create_node(false);
4253            node.set_leading(format!("{}\n// ", comment_text));
4254            Some(node)
4255        } else {
4256            None
4257        }
4258    }
4259    fn advanced_mouse_actions_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
4260        let comment_text = format!(
4261            "{}\n{}\n{}",
4262            " ",
4263            "// Whether to enable mouse hover effects and pane grouping functionality",
4264            "// default is true",
4265        );
4266
4267        let create_node = |node_value: bool| -> KdlNode {
4268            let mut node = KdlNode::new("advanced_mouse_actions");
4269            node.push(KdlValue::Bool(node_value));
4270            node
4271        };
4272        if let Some(advanced_mouse_actions) = self.advanced_mouse_actions {
4273            let mut node = create_node(advanced_mouse_actions);
4274            if add_comments {
4275                node.set_leading(format!("{}\n", comment_text));
4276            }
4277            Some(node)
4278        } else if add_comments {
4279            let mut node = create_node(false);
4280            node.set_leading(format!("{}\n// ", comment_text));
4281            Some(node)
4282        } else {
4283            None
4284        }
4285    }
4286    fn mouse_scroll_resize_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
4287        let comment_text = format!(
4288            "{}\n{}\n{}",
4289            " ", "// Whether Ctrl+ScrollWheel resizes panes", "// default is true",
4290        );
4291
4292        let create_node = |node_value: bool| -> KdlNode {
4293            let mut node = KdlNode::new("mouse_scroll_resize");
4294            node.push(KdlValue::Bool(node_value));
4295            node
4296        };
4297        if let Some(mouse_scroll_resize) = self.mouse_scroll_resize {
4298            let mut node = create_node(mouse_scroll_resize);
4299            if add_comments {
4300                node.set_leading(format!("{}\n", comment_text));
4301            }
4302            Some(node)
4303        } else if add_comments {
4304            let mut node = create_node(false);
4305            node.set_leading(format!("{}\n// ", comment_text));
4306            Some(node)
4307        } else {
4308            None
4309        }
4310    }
4311    fn mouse_hover_tips_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
4312        let comment_text = format!(
4313            "{}\n{}\n{}",
4314            " ",
4315            "// Whether to show mouse hover help-text tips (resize help and group shortcuts)",
4316            "// default is true",
4317        );
4318
4319        let create_node = |node_value: bool| -> KdlNode {
4320            let mut node = KdlNode::new("mouse_hover_tips");
4321            node.push(KdlValue::Bool(node_value));
4322            node
4323        };
4324        if let Some(mouse_hover_tips) = self.mouse_hover_tips {
4325            let mut node = create_node(mouse_hover_tips);
4326            if add_comments {
4327                node.set_leading(format!("{}\n", comment_text));
4328            }
4329            Some(node)
4330        } else if add_comments {
4331            let mut node = create_node(false);
4332            node.set_leading(format!("{}\n// ", comment_text));
4333            Some(node)
4334        } else {
4335            None
4336        }
4337    }
4338    fn mouse_hover_effects_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
4339        let comment_text = format!(
4340            "{}\n{}\n{}",
4341            " ",
4342            "// Whether to enable mouse hover visual effects (frame highlight and help text)",
4343            "// default is true",
4344        );
4345
4346        let create_node = |node_value: bool| -> KdlNode {
4347            let mut node = KdlNode::new("mouse_hover_effects");
4348            node.push(KdlValue::Bool(node_value));
4349            node
4350        };
4351        if let Some(mouse_hover_effects) = self.mouse_hover_effects {
4352            let mut node = create_node(mouse_hover_effects);
4353            if add_comments {
4354                node.set_leading(format!("{}\n", comment_text));
4355            }
4356            Some(node)
4357        } else if add_comments {
4358            let mut node = create_node(false);
4359            node.set_leading(format!("{}\n// ", comment_text));
4360            Some(node)
4361        } else {
4362            None
4363        }
4364    }
4365    fn visual_bell_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
4366        let comment_text = format!(
4367            "{}\n{}\n{}",
4368            " ",
4369            "// Whether to show visual bell indicators (pane/tab frame flash and [!] suffix)",
4370            "// default is true",
4371        );
4372
4373        let create_node = |node_value: bool| -> KdlNode {
4374            let mut node = KdlNode::new("visual_bell");
4375            node.push(KdlValue::Bool(node_value));
4376            node
4377        };
4378        if let Some(visual_bell) = self.visual_bell {
4379            let mut node = create_node(visual_bell);
4380            if add_comments {
4381                node.set_leading(format!("{}\n", comment_text));
4382            }
4383            Some(node)
4384        } else if add_comments {
4385            let mut node = create_node(true);
4386            node.set_leading(format!("{}\n// ", comment_text));
4387            Some(node)
4388        } else {
4389            None
4390        }
4391    }
4392    fn focus_follows_mouse_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
4393        let comment_text = format!(
4394            "{}\n{}\n{}",
4395            " ", "// Whether to focus panes on mouse hover", "// default is false",
4396        );
4397
4398        let create_node = |node_value: bool| -> KdlNode {
4399            let mut node = KdlNode::new("focus_follows_mouse");
4400            node.push(KdlValue::Bool(node_value));
4401            node
4402        };
4403        if let Some(focus_follows_mouse) = self.focus_follows_mouse {
4404            let mut node = create_node(focus_follows_mouse);
4405            if add_comments {
4406                node.set_leading(format!("{}\n", comment_text));
4407            }
4408            Some(node)
4409        } else if add_comments {
4410            let mut node = create_node(false);
4411            node.set_leading(format!("{}\n// ", comment_text));
4412            Some(node)
4413        } else {
4414            None
4415        }
4416    }
4417    fn mouse_click_through_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
4418        let comment_text = format!(
4419            "{}\n{}\n{}",
4420            " ",
4421            "// Whether clicking a pane to focus it also sends the click into the pane",
4422            "// default is false",
4423        );
4424
4425        let create_node = |node_value: bool| -> KdlNode {
4426            let mut node = KdlNode::new("mouse_click_through");
4427            node.push(KdlValue::Bool(node_value));
4428            node
4429        };
4430        if let Some(mouse_click_through) = self.mouse_click_through {
4431            let mut node = create_node(mouse_click_through);
4432            if add_comments {
4433                node.set_leading(format!("{}\n", comment_text));
4434            }
4435            Some(node)
4436        } else if add_comments {
4437            let mut node = create_node(false);
4438            node.set_leading(format!("{}\n// ", comment_text));
4439            Some(node)
4440        } else {
4441            None
4442        }
4443    }
4444    fn osc133_command_selection_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
4445        let comment_text = format!(
4446            "{}\n{}\n{}\n{}",
4447            " ",
4448            "// Whether triple-clicking inside command output marked by the shell (OSC 133) selects",
4449            "// the command and its output instead of the logical line",
4450            "// default is true",
4451        );
4452
4453        let create_node = |node_value: bool| -> KdlNode {
4454            let mut node = KdlNode::new("osc133_command_selection");
4455            node.push(KdlValue::Bool(node_value));
4456            node
4457        };
4458        if let Some(osc133_command_selection) = self.osc133_command_selection {
4459            let mut node = create_node(osc133_command_selection);
4460            if add_comments {
4461                node.set_leading(format!("{}\n", comment_text));
4462            }
4463            Some(node)
4464        } else if add_comments {
4465            let mut node = create_node(false);
4466            node.set_leading(format!("{}\n// ", comment_text));
4467            Some(node)
4468        } else {
4469            None
4470        }
4471    }
4472    fn word_separators_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
4473        let comment_text = format!(
4474            "{}\n{}\n{}\n{}",
4475            " ",
4476            "// Characters that terminate a word when double-clicking to select it",
4477            "// whitespace is always a separator and need not be listed here",
4478            "// default is \"[]{}<>()\"",
4479        );
4480
4481        let create_node = |node_value: &str| -> KdlNode {
4482            let mut node = KdlNode::new("word_separators");
4483            node.push(node_value.to_owned());
4484            node
4485        };
4486        if let Some(word_separators) = &self.word_separators {
4487            let mut node = create_node(word_separators);
4488            if add_comments {
4489                node.set_leading(format!("{}\n", comment_text));
4490            }
4491            Some(node)
4492        } else if add_comments {
4493            let mut node = create_node(DEFAULT_WORD_SEPARATORS);
4494            node.set_leading(format!("{}\n// ", comment_text));
4495            Some(node)
4496        } else {
4497            None
4498        }
4499    }
4500    fn web_server_ip_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
4501        let comment_text = format!(
4502            "{}\n{}\n{}\n{}",
4503            " ",
4504            "// The ip address the web server should listen on when it starts",
4505            "// Default: \"127.0.0.1\"",
4506            "// (Requires restart)",
4507        );
4508
4509        let create_node = |node_value: IpAddr| -> KdlNode {
4510            let mut node = KdlNode::new("web_server_ip");
4511            node.push(KdlValue::String(node_value.to_string()));
4512            node
4513        };
4514        if let Some(web_server_ip) = self.web_server_ip {
4515            let mut node = create_node(web_server_ip);
4516            if add_comments {
4517                node.set_leading(format!("{}\n", comment_text));
4518            }
4519            Some(node)
4520        } else if add_comments {
4521            let mut node = create_node(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)));
4522            node.set_leading(format!("{}\n// ", comment_text));
4523            Some(node)
4524        } else {
4525            None
4526        }
4527    }
4528    fn web_server_port_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
4529        let comment_text = format!(
4530            "{}\n{}\n{}\n{}",
4531            " ",
4532            "// The port the web server should listen on when it starts",
4533            "// Default: 8082",
4534            "// (Requires restart)",
4535        );
4536
4537        let create_node = |node_value: u16| -> KdlNode {
4538            let mut node = KdlNode::new("web_server_port");
4539            node.push(KdlValue::Base10(node_value as i64));
4540            node
4541        };
4542        if let Some(web_server_port) = self.web_server_port {
4543            let mut node = create_node(web_server_port);
4544            if add_comments {
4545                node.set_leading(format!("{}\n", comment_text));
4546            }
4547            Some(node)
4548        } else if add_comments {
4549            let mut node = create_node(8082);
4550            node.set_leading(format!("{}\n// ", comment_text));
4551            Some(node)
4552        } else {
4553            None
4554        }
4555    }
4556    fn post_command_discovery_hook_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
4557        let comment_text = format!(
4558            "{}\n{}\n{}\n{}\n{}\n{}",
4559            " ",
4560            "// A command to run (will be wrapped with sh -c and provided the RESURRECT_COMMAND env variable) ",
4561            "// after Zellij attempts to discover a command inside a pane when resurrecting sessions, the STDOUT",
4562            "// of this command will be used instead of the discovered RESURRECT_COMMAND",
4563            "// can be useful for removing wrappers around commands",
4564            "// Note: be sure to escape backslashes and similar characters properly",
4565        );
4566
4567        let create_node = |node_value: &str| -> KdlNode {
4568            let mut node = KdlNode::new("post_command_discovery_hook");
4569            node.push(node_value.to_owned());
4570            node
4571        };
4572        if let Some(post_command_discovery_hook) = &self.post_command_discovery_hook {
4573            let mut node = create_node(&post_command_discovery_hook);
4574            if add_comments {
4575                node.set_leading(format!("{}\n", comment_text));
4576            }
4577            Some(node)
4578        } else if add_comments {
4579            let mut node = create_node("echo $RESURRECT_COMMAND | sed <your_regex_here>");
4580            node.set_leading(format!("{}\n// ", comment_text));
4581            Some(node)
4582        } else {
4583            None
4584        }
4585    }
4586    fn nested_session_handling_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
4587        use crate::input::options::NestedSessionHandling;
4588        let comment_text = format!(
4589            "{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}",
4590            " ",
4591            "// How to handle a nested Zellij session detected inside a pane.",
4592            "// Options:",
4593            "//   - \"ask\" (Default — prompt with a modal)",
4594            "//   - \"fullscreen\" (always zoom into the nested session)",
4595            "//   - \"descend\" (always control the nested session on focus)",
4596            "//   - \"never\" (never prompt or descend; do it manually)",
4597            "// ",
4598        );
4599        let create_node = |value: NestedSessionHandling| -> KdlNode {
4600            let mut node = KdlNode::new("nested_session_handling");
4601            let s = match value {
4602                NestedSessionHandling::Ask => "ask",
4603                NestedSessionHandling::Fullscreen => "fullscreen",
4604                NestedSessionHandling::Descend => "descend",
4605                NestedSessionHandling::Never => "never",
4606            };
4607            node.push(KdlValue::String(s.to_string()));
4608            node
4609        };
4610        if let Some(value) = self.nested_session_handling {
4611            let mut node = create_node(value);
4612            if add_comments {
4613                node.set_leading(format!("{}\n", comment_text));
4614            }
4615            Some(node)
4616        } else if add_comments {
4617            let mut node = create_node(NestedSessionHandling::Ask);
4618            node.set_leading(format!("{}\n// ", comment_text));
4619            Some(node)
4620        } else {
4621            None
4622        }
4623    }
4624    fn host_notification_protocol_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
4625        use crate::input::options::HostNotificationProtocol;
4626        let comment_text = format!(
4627            "{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}",
4628            " ",
4629            "// Which escape sequence desktop notifications coming from panes are",
4630            "// forwarded to the host terminal with.",
4631            "// Options:",
4632            "//   - \"auto\" (Default — detect from the host terminal environment)",
4633            "//   - \"osc9\" (the legacy iTerm2 protocol, understood by most terminals)",
4634            "//   - \"osc99\" (kitty's notification protocol)",
4635            "//   - \"bell\" (ring the terminal bell instead)",
4636            "//   - \"off\" (do not forward notifications to the host terminal)",
4637            "// ",
4638        );
4639        let create_node = |value: HostNotificationProtocol| -> KdlNode {
4640            let mut node = KdlNode::new("host_notification_protocol");
4641            node.push(KdlValue::String(value.as_str().to_string()));
4642            node
4643        };
4644        if let Some(value) = self.host_notification_protocol {
4645            let mut node = create_node(value);
4646            if add_comments {
4647                node.set_leading(format!("{}\n", comment_text));
4648            }
4649            Some(node)
4650        } else if add_comments {
4651            let mut node = create_node(HostNotificationProtocol::Auto);
4652            node.set_leading(format!("{}\n// ", comment_text));
4653            Some(node)
4654        } else {
4655            None
4656        }
4657    }
4658    fn dangerously_enable_paste_buffer_read_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
4659        let comment_text = format!(
4660            "{}\n{}\n{}\n{}\n{}\n{}",
4661            " ",
4662            "// Whether to let programs running inside panes read the paste buffer",
4663            "// (clipboard) with the OSC 52 escape sequence. When enabled, any program",
4664            "// in any pane - including one running on a remote machine over SSH - can",
4665            "// read the clipboard without the user being asked.",
4666            "// Default: false",
4667        );
4668        let create_node = |node_value: bool| -> KdlNode {
4669            let mut node = KdlNode::new("dangerously_enable_paste_buffer_read");
4670            node.push(KdlValue::Bool(node_value));
4671            node
4672        };
4673        if let Some(value) = self.dangerously_enable_paste_buffer_read {
4674            let mut node = create_node(value);
4675            if add_comments {
4676                node.set_leading(format!("{}\n", comment_text));
4677            }
4678            Some(node)
4679        } else if add_comments {
4680            let mut node = create_node(false);
4681            node.set_leading(format!("{}\n// ", comment_text));
4682            Some(node)
4683        } else {
4684            None
4685        }
4686    }
4687    fn client_async_worker_tasks_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
4688        let comment_text = r#"
4689// Number of async worker tasks to spawn per active client.
4690//
4691// Allocating few tasks may result in resource contention and lags. Small values (around 4) should
4692// typically work best. Set to 0 to use the number of (physical) CPU cores.
4693// Note: This only applies to web clients at the moment."#;
4694        let create_node = |node_value: usize| -> KdlNode {
4695            let mut node = KdlNode::new("client_async_worker_tasks");
4696            node.push(KdlValue::Base10(node_value as i64));
4697            node
4698        };
4699        if let Some(client_async_worker_tasks) = self.client_async_worker_tasks {
4700            let mut node = create_node(client_async_worker_tasks);
4701            if add_comments {
4702                node.set_leading(format!("{}\n", comment_text));
4703            }
4704            Some(node)
4705        } else if add_comments {
4706            let mut node = create_node(4usize);
4707            node.set_leading(format!("{}\n// ", comment_text));
4708            Some(node)
4709        } else {
4710            None
4711        }
4712    }
4713    pub fn to_kdl(&self, add_comments: bool) -> Vec<KdlNode> {
4714        let mut nodes = vec![];
4715        if let Some(simplified_ui_node) = self.simplified_ui_to_kdl(add_comments) {
4716            nodes.push(simplified_ui_node);
4717        }
4718        if let Some(osc8_hyperlinks_node) = self.osc8_hyperlinks_to_kdl(add_comments) {
4719            nodes.push(osc8_hyperlinks_node);
4720        }
4721        if let Some(theme_node) = self.theme_to_kdl(add_comments) {
4722            nodes.push(theme_node);
4723        }
4724        if let Some(theme_dark_node) = self.theme_dark_to_kdl(add_comments) {
4725            nodes.push(theme_dark_node);
4726        }
4727        if let Some(theme_light_node) = self.theme_light_to_kdl(add_comments) {
4728            nodes.push(theme_light_node);
4729        }
4730        if let Some(default_mode) = self.default_mode_to_kdl(add_comments) {
4731            nodes.push(default_mode);
4732        }
4733        if let Some(default_shell) = self.default_shell_to_kdl(add_comments) {
4734            nodes.push(default_shell);
4735        }
4736        if let Some(default_cwd) = self.default_cwd_to_kdl(add_comments) {
4737            nodes.push(default_cwd);
4738        }
4739        if let Some(default_layout) = self.default_layout_to_kdl(add_comments) {
4740            nodes.push(default_layout);
4741        }
4742        if let Some(layout_dir) = self.layout_dir_to_kdl(add_comments) {
4743            nodes.push(layout_dir);
4744        }
4745        if let Some(theme_dir) = self.theme_dir_to_kdl(add_comments) {
4746            nodes.push(theme_dir);
4747        }
4748        if let Some(mouse_mode) = self.mouse_mode_to_kdl(add_comments) {
4749            nodes.push(mouse_mode);
4750        }
4751        if let Some(pane_frames) = self.pane_frames_to_kdl(add_comments) {
4752            nodes.push(pane_frames);
4753        }
4754        if let Some(pane_frame_style) = self.pane_frame_style_to_kdl(add_comments) {
4755            nodes.push(pane_frame_style);
4756        }
4757        if let Some(mirror_session) = self.mirror_session_to_kdl(add_comments) {
4758            nodes.push(mirror_session);
4759        }
4760        if let Some(on_force_close) = self.on_force_close_to_kdl(add_comments) {
4761            nodes.push(on_force_close);
4762        }
4763        if let Some(scroll_buffer_size) = self.scroll_buffer_size_to_kdl(add_comments) {
4764            nodes.push(scroll_buffer_size);
4765        }
4766        if let Some(copy_command) = self.copy_command_to_kdl(add_comments) {
4767            nodes.push(copy_command);
4768        }
4769        if let Some(copy_clipboard) = self.copy_clipboard_to_kdl(add_comments) {
4770            nodes.push(copy_clipboard);
4771        }
4772        if let Some(copy_on_select) = self.copy_on_select_to_kdl(add_comments) {
4773            nodes.push(copy_on_select);
4774        }
4775        if let Some(scrollback_editor) = self.scrollback_editor_to_kdl(add_comments) {
4776            nodes.push(scrollback_editor);
4777        }
4778        if let Some(session_name) = self.session_name_to_kdl(add_comments) {
4779            nodes.push(session_name);
4780        }
4781        if let Some(attach_to_session) = self.attach_to_session_to_kdl(add_comments) {
4782            nodes.push(attach_to_session);
4783        }
4784        if let Some(auto_layout) = self.auto_layout_to_kdl(add_comments) {
4785            nodes.push(auto_layout);
4786        }
4787        if let Some(session_serialization) = self.session_serialization_to_kdl(add_comments) {
4788            nodes.push(session_serialization);
4789        }
4790        if let Some(serialize_pane_viewport) = self.serialize_pane_viewport_to_kdl(add_comments) {
4791            nodes.push(serialize_pane_viewport);
4792        }
4793        if let Some(scrollback_lines_to_serialize) =
4794            self.scrollback_lines_to_serialize_to_kdl(add_comments)
4795        {
4796            nodes.push(scrollback_lines_to_serialize);
4797        }
4798        if let Some(styled_underlines) = self.styled_underlines_to_kdl(add_comments) {
4799            nodes.push(styled_underlines);
4800        }
4801        if let Some(serialization_interval) = self.serialization_interval_to_kdl(add_comments) {
4802            nodes.push(serialization_interval);
4803        }
4804        if let Some(disable_session_metadata) = self.disable_session_metadata_to_kdl(add_comments) {
4805            nodes.push(disable_session_metadata);
4806        }
4807        if let Some(support_kitty_keyboard_protocol) =
4808            self.support_kitty_keyboard_protocol_to_kdl(add_comments)
4809        {
4810            nodes.push(support_kitty_keyboard_protocol);
4811        }
4812        if let Some(support_kitty_graphics_protocol) =
4813            self.support_kitty_graphics_protocol_to_kdl(add_comments)
4814        {
4815            nodes.push(support_kitty_graphics_protocol);
4816        }
4817        if let Some(web_server) = self.web_server_to_kdl(add_comments) {
4818            nodes.push(web_server);
4819        }
4820        if let Some(web_sharing) = self.web_sharing_to_kdl(add_comments) {
4821            nodes.push(web_sharing);
4822        }
4823        if let Some(web_server_cert) = self.web_server_cert_to_kdl(add_comments) {
4824            nodes.push(web_server_cert);
4825        }
4826        if let Some(web_server_key) = self.web_server_key_to_kdl(add_comments) {
4827            nodes.push(web_server_key);
4828        }
4829        if let Some(enforce_https_for_localhost) =
4830            self.enforce_https_for_localhost_to_kdl(add_comments)
4831        {
4832            nodes.push(enforce_https_for_localhost);
4833        }
4834        if let Some(stacked_resize) = self.stacked_resize_to_kdl(add_comments) {
4835            nodes.push(stacked_resize);
4836        }
4837        if let Some(stacked_pane_list) = self.stacked_pane_list_to_kdl(add_comments) {
4838            nodes.push(stacked_pane_list);
4839        }
4840        if let Some(show_startup_tips) = self.show_startup_tips_to_kdl(add_comments) {
4841            nodes.push(show_startup_tips);
4842        }
4843        if let Some(show_release_notes) = self.show_release_notes_to_kdl(add_comments) {
4844            nodes.push(show_release_notes);
4845        }
4846        if let Some(advanced_mouse_actions) = self.advanced_mouse_actions_to_kdl(add_comments) {
4847            nodes.push(advanced_mouse_actions);
4848        }
4849        if let Some(mouse_scroll_resize) = self.mouse_scroll_resize_to_kdl(add_comments) {
4850            nodes.push(mouse_scroll_resize);
4851        }
4852        if let Some(mouse_hover_effects) = self.mouse_hover_effects_to_kdl(add_comments) {
4853            nodes.push(mouse_hover_effects);
4854        }
4855        if let Some(mouse_hover_tips) = self.mouse_hover_tips_to_kdl(add_comments) {
4856            nodes.push(mouse_hover_tips);
4857        }
4858        if let Some(visual_bell) = self.visual_bell_to_kdl(add_comments) {
4859            nodes.push(visual_bell);
4860        }
4861        if let Some(focus_follows_mouse) = self.focus_follows_mouse_to_kdl(add_comments) {
4862            nodes.push(focus_follows_mouse);
4863        }
4864        if let Some(mouse_click_through) = self.mouse_click_through_to_kdl(add_comments) {
4865            nodes.push(mouse_click_through);
4866        }
4867        if let Some(osc133_command_selection) = self.osc133_command_selection_to_kdl(add_comments) {
4868            nodes.push(osc133_command_selection);
4869        }
4870        if let Some(word_separators) = self.word_separators_to_kdl(add_comments) {
4871            nodes.push(word_separators);
4872        }
4873        if let Some(web_server_ip) = self.web_server_ip_to_kdl(add_comments) {
4874            nodes.push(web_server_ip);
4875        }
4876        if let Some(web_server_port) = self.web_server_port_to_kdl(add_comments) {
4877            nodes.push(web_server_port);
4878        }
4879        if let Some(post_command_discovery_hook) =
4880            self.post_command_discovery_hook_to_kdl(add_comments)
4881        {
4882            nodes.push(post_command_discovery_hook);
4883        }
4884        if let Some(client_async_worker_tasks) = self.client_async_worker_tasks_to_kdl(add_comments)
4885        {
4886            nodes.push(client_async_worker_tasks);
4887        }
4888        if let Some(dangerously_enable_paste_buffer_read) =
4889            self.dangerously_enable_paste_buffer_read_to_kdl(add_comments)
4890        {
4891            nodes.push(dangerously_enable_paste_buffer_read);
4892        }
4893        if let Some(nested_session_handling) = self.nested_session_handling_to_kdl(add_comments) {
4894            nodes.push(nested_session_handling);
4895        }
4896        if let Some(host_notification_protocol) =
4897            self.host_notification_protocol_to_kdl(add_comments)
4898        {
4899            nodes.push(host_notification_protocol);
4900        }
4901        nodes
4902    }
4903}
4904
4905impl Layout {
4906    pub fn from_kdl(
4907        raw_layout: &str,
4908        file_name: Option<String>,
4909        raw_swap_layouts: Option<(&str, &str)>, // raw_swap_layouts swap_layouts_file_name
4910        cwd: Option<PathBuf>,
4911    ) -> Result<Self, ConfigError> {
4912        let mut kdl_layout_parser = KdlLayoutParser::new(raw_layout, cwd, file_name.clone());
4913        let layout = kdl_layout_parser.parse().map_err(|e| match e {
4914            ConfigError::KdlError(kdl_error) => ConfigError::KdlError(kdl_error.add_src(
4915                file_name.unwrap_or_else(|| "N/A".to_owned()),
4916                String::from(raw_layout),
4917            )),
4918            ConfigError::KdlDeserializationError(kdl_error) => kdl_layout_error(
4919                kdl_error,
4920                file_name.unwrap_or_else(|| "N/A".to_owned()),
4921                raw_layout,
4922            ),
4923            e => e,
4924        })?;
4925        match raw_swap_layouts {
4926            Some((raw_swap_layout_filename, raw_swap_layout)) => {
4927                // here we use the same parser to parse the swap layout so that we can reuse assets
4928                // (eg. pane and tab templates)
4929                kdl_layout_parser
4930                    .parse_external_swap_layouts(raw_swap_layout, layout)
4931                    .map_err(|e| match e {
4932                        ConfigError::KdlError(kdl_error) => {
4933                            ConfigError::KdlError(kdl_error.add_src(
4934                                String::from(raw_swap_layout_filename),
4935                                String::from(raw_swap_layout),
4936                            ))
4937                        },
4938                        ConfigError::KdlDeserializationError(kdl_error) => kdl_layout_error(
4939                            kdl_error,
4940                            raw_swap_layout_filename.into(),
4941                            raw_swap_layout,
4942                        ),
4943                        e => e,
4944                    })
4945            },
4946            None => Ok(layout),
4947        }
4948    }
4949}
4950
4951fn kdl_layout_error(kdl_error: kdl::KdlError, file_name: String, raw_layout: &str) -> ConfigError {
4952    let error_message = match kdl_error.kind {
4953        kdl::KdlErrorKind::Context("valid node terminator") => {
4954            format!("Failed to deserialize KDL node. \nPossible reasons:\n{}\n{}\n{}\n{}",
4955            "- Missing `;` after a node name, eg. { node; another_node; }",
4956            "- Missing quotations (\") around an argument node eg. { first_node \"argument_node\"; }",
4957            "- Missing an equal sign (=) between node arguments on a title line. eg. argument=\"value\"",
4958            "- Found an extraneous equal sign (=) between node child arguments and their values. eg. { argument=\"value\" }")
4959        },
4960        _ => String::from(kdl_error.help.unwrap_or("Kdl Deserialization Error")),
4961    };
4962    let kdl_error = KdlError {
4963        error_message,
4964        src: Some(NamedSource::new(file_name, String::from(raw_layout))),
4965        offset: Some(kdl_error.span.offset()),
4966        len: Some(kdl_error.span.len()),
4967        help_message: None,
4968    };
4969    ConfigError::KdlError(kdl_error)
4970}
4971
4972impl EnvironmentVariables {
4973    pub fn from_kdl(kdl_env_variables: &KdlNode) -> Result<Self, ConfigError> {
4974        let mut env: HashMap<String, String> = HashMap::new();
4975        for env_var in kdl_children_nodes_or_error!(kdl_env_variables, "empty env variable block") {
4976            let env_var_name = kdl_name!(env_var);
4977            let env_var_str_value =
4978                kdl_first_entry_as_string!(env_var).map(|s| format!("{}", s.to_string()));
4979            let env_var_int_value =
4980                kdl_first_entry_as_i64!(env_var).map(|s| format!("{}", s.to_string()));
4981            let env_var_value =
4982                env_var_str_value
4983                    .or(env_var_int_value)
4984                    .ok_or(ConfigError::new_kdl_error(
4985                        format!("Failed to parse env var: {:?}", env_var_name),
4986                        env_var.span().offset(),
4987                        env_var.span().len(),
4988                    ))?;
4989            env.insert(env_var_name.into(), env_var_value);
4990        }
4991        Ok(EnvironmentVariables::from_data(env))
4992    }
4993    pub fn to_kdl(&self) -> Option<KdlNode> {
4994        let mut has_env_vars = false;
4995        let mut env = KdlNode::new("env");
4996        let mut env_vars = KdlDocument::new();
4997
4998        let mut stable_sorted = BTreeMap::new();
4999        for (env_var_name, env_var_value) in self.inner() {
5000            stable_sorted.insert(env_var_name, env_var_value);
5001        }
5002        for (env_key, env_value) in stable_sorted {
5003            has_env_vars = true;
5004            let mut variable_key = KdlNode::new(env_key.to_owned());
5005            variable_key.push(env_value.to_owned());
5006            env_vars.nodes_mut().push(variable_key);
5007        }
5008
5009        if has_env_vars {
5010            env.set_children(env_vars);
5011            Some(env)
5012        } else {
5013            None
5014        }
5015    }
5016}
5017
5018impl Keybinds {
5019    fn bind_keys_in_block(
5020        block: &KdlNode,
5021        input_mode_keybinds: &mut HashMap<KeyWithModifier, Vec<Action>>,
5022        config_options: &Options,
5023    ) -> Result<(), ConfigError> {
5024        let all_nodes = kdl_children_nodes_or_error!(block, "no keybinding block for mode");
5025        let bind_nodes = all_nodes.iter().filter(|n| kdl_name!(n) == "bind");
5026        let unbind_nodes = all_nodes.iter().filter(|n| kdl_name!(n) == "unbind");
5027        for key_block in bind_nodes {
5028            Keybinds::bind_actions_for_each_key(key_block, input_mode_keybinds, config_options)?;
5029        }
5030        // we loop a second time so that the unbinds always happen after the binds
5031        for key_block in unbind_nodes {
5032            Keybinds::unbind_keys(key_block, input_mode_keybinds)?;
5033        }
5034        for key_block in all_nodes {
5035            if kdl_name!(key_block) != "bind" && kdl_name!(key_block) != "unbind" {
5036                return Err(ConfigError::new_kdl_error(
5037                    format!("Unknown keybind instruction: '{}'", kdl_name!(key_block)),
5038                    key_block.span().offset(),
5039                    key_block.span().len(),
5040                ));
5041            }
5042        }
5043        Ok(())
5044    }
5045    pub fn from_kdl(
5046        kdl_keybinds: &KdlNode,
5047        base_keybinds: Keybinds,
5048        config_options: &Options,
5049    ) -> Result<Self, ConfigError> {
5050        let clear_defaults = kdl_arg_is_truthy!(kdl_keybinds, "clear-defaults");
5051        let mut keybinds_from_config = if clear_defaults {
5052            Keybinds::default()
5053        } else {
5054            base_keybinds
5055        };
5056        for block in kdl_children_nodes_or_error!(kdl_keybinds, "keybindings with no children") {
5057            if kdl_name!(block) == "shared_except" || kdl_name!(block) == "shared" {
5058                let mut modes_to_exclude = vec![];
5059                for mode_name in kdl_string_arguments!(block) {
5060                    modes_to_exclude.push(InputMode::from_str(mode_name).map_err(|_| {
5061                        ConfigError::new_kdl_error(
5062                            format!("Invalid mode: '{}'", mode_name),
5063                            block.name().span().offset(),
5064                            block.name().span().len(),
5065                        )
5066                    })?);
5067                }
5068                for mode in InputMode::iter() {
5069                    if modes_to_exclude.contains(&mode) {
5070                        continue;
5071                    }
5072                    let mut input_mode_keybinds = keybinds_from_config.get_input_mode_mut(&mode);
5073                    Keybinds::bind_keys_in_block(block, &mut input_mode_keybinds, config_options)?;
5074                }
5075            }
5076            if kdl_name!(block) == "shared_among" {
5077                let mut modes_to_include = vec![];
5078                for mode_name in kdl_string_arguments!(block) {
5079                    modes_to_include.push(InputMode::from_str(mode_name)?);
5080                }
5081                for mode in InputMode::iter() {
5082                    if !modes_to_include.contains(&mode) {
5083                        continue;
5084                    }
5085                    let mut input_mode_keybinds = keybinds_from_config.get_input_mode_mut(&mode);
5086                    Keybinds::bind_keys_in_block(block, &mut input_mode_keybinds, config_options)?;
5087                }
5088            }
5089        }
5090        for mode in kdl_children_nodes_or_error!(kdl_keybinds, "keybindings with no children") {
5091            if kdl_name!(mode) == "unbind"
5092                || kdl_name!(mode) == "shared_except"
5093                || kdl_name!(mode) == "shared_among"
5094                || kdl_name!(mode) == "shared"
5095            {
5096                continue;
5097            }
5098            let mut input_mode_keybinds =
5099                Keybinds::input_mode_keybindings(mode, &mut keybinds_from_config)?;
5100            Keybinds::bind_keys_in_block(mode, &mut input_mode_keybinds, config_options)?;
5101        }
5102        if let Some(global_unbind) = kdl_keybinds.children().and_then(|c| c.get("unbind")) {
5103            Keybinds::unbind_keys_in_all_modes(global_unbind, &mut keybinds_from_config)?;
5104        };
5105        Ok(keybinds_from_config)
5106    }
5107    fn bind_actions_for_each_key(
5108        key_block: &KdlNode,
5109        input_mode_keybinds: &mut HashMap<KeyWithModifier, Vec<Action>>,
5110        config_options: &Options,
5111    ) -> Result<(), ConfigError> {
5112        let keys: Vec<KeyWithModifier> = keys_from_kdl!(key_block);
5113        let actions: Vec<Action> = actions_from_kdl!(key_block, config_options);
5114        for key in keys {
5115            input_mode_keybinds.insert(key, actions.clone());
5116        }
5117        Ok(())
5118    }
5119    fn unbind_keys(
5120        key_block: &KdlNode,
5121        input_mode_keybinds: &mut HashMap<KeyWithModifier, Vec<Action>>,
5122    ) -> Result<(), ConfigError> {
5123        let keys: Vec<KeyWithModifier> = keys_from_kdl!(key_block);
5124        for key in keys {
5125            input_mode_keybinds.remove(&key);
5126        }
5127        Ok(())
5128    }
5129    fn unbind_keys_in_all_modes(
5130        global_unbind: &KdlNode,
5131        keybinds_from_config: &mut Keybinds,
5132    ) -> Result<(), ConfigError> {
5133        let keys: Vec<KeyWithModifier> = keys_from_kdl!(global_unbind);
5134        for mode in keybinds_from_config.0.values_mut() {
5135            for key in &keys {
5136                mode.remove(&key);
5137            }
5138        }
5139        Ok(())
5140    }
5141    fn input_mode_keybindings<'a>(
5142        mode: &KdlNode,
5143        keybinds_from_config: &'a mut Keybinds,
5144    ) -> Result<&'a mut HashMap<KeyWithModifier, Vec<Action>>, ConfigError> {
5145        let mode_name = kdl_name!(mode);
5146        let input_mode = InputMode::from_str(mode_name).map_err(|_| {
5147            ConfigError::new_kdl_error(
5148                format!("Invalid mode: '{}'", mode_name),
5149                mode.name().span().offset(),
5150                mode.name().span().len(),
5151            )
5152        })?;
5153        let input_mode_keybinds = keybinds_from_config.get_input_mode_mut(&input_mode);
5154        let clear_defaults_for_mode = kdl_arg_is_truthy!(mode, "clear-defaults");
5155        if clear_defaults_for_mode {
5156            input_mode_keybinds.clear();
5157        }
5158        Ok(input_mode_keybinds)
5159    }
5160    pub fn from_string(
5161        stringified_keybindings: String,
5162        base_keybinds: Keybinds,
5163        config_options: &Options,
5164    ) -> Result<Self, ConfigError> {
5165        let document: KdlDocument = stringified_keybindings.parse()?;
5166        if let Some(kdl_keybinds) = document.get("keybinds") {
5167            Keybinds::from_kdl(&kdl_keybinds, base_keybinds, config_options)
5168        } else {
5169            Err(ConfigError::new_kdl_error(
5170                format!("Could not find keybinds node"),
5171                document.span().offset(),
5172                document.span().len(),
5173            ))
5174        }
5175    }
5176    // minimize keybind entries for serialization, so that duplicate entries will appear in
5177    // "shared" nodes later rather than once per mode
5178    fn minimize_entries(
5179        &self,
5180    ) -> BTreeMap<BTreeSet<InputMode>, BTreeMap<KeyWithModifier, Vec<Action>>> {
5181        let mut minimized: BTreeMap<BTreeSet<InputMode>, BTreeMap<KeyWithModifier, Vec<Action>>> =
5182            BTreeMap::new();
5183        let mut flattened: Vec<BTreeMap<KeyWithModifier, Vec<Action>>> = self
5184            .0
5185            .iter()
5186            .map(|(_input_mode, keybind)| keybind.clone().into_iter().collect())
5187            .collect();
5188        for keybind in flattened.drain(..) {
5189            for (key, actions) in keybind.into_iter() {
5190                let mut appears_in_modes: BTreeSet<InputMode> = BTreeSet::new();
5191                for (input_mode, keybinds) in self.0.iter() {
5192                    if keybinds.get(&key) == Some(&actions) {
5193                        appears_in_modes.insert(*input_mode);
5194                    }
5195                }
5196                minimized
5197                    .entry(appears_in_modes)
5198                    .or_insert_with(Default::default)
5199                    .insert(key, actions);
5200            }
5201        }
5202        minimized
5203    }
5204    fn serialize_mode_title_node(&self, input_modes: &BTreeSet<InputMode>) -> KdlNode {
5205        let all_modes: Vec<InputMode> = InputMode::iter().collect();
5206        let total_input_mode_count = all_modes.len();
5207        if input_modes.len() == 1 {
5208            let input_mode_name =
5209                format!("{:?}", input_modes.iter().next().unwrap()).to_lowercase();
5210            KdlNode::new(input_mode_name)
5211        } else if input_modes.len() == total_input_mode_count {
5212            KdlNode::new("shared")
5213        } else if input_modes.len() < total_input_mode_count / 2 {
5214            let mut node = KdlNode::new("shared_among");
5215            for input_mode in input_modes {
5216                node.push(format!("{:?}", input_mode).to_lowercase());
5217            }
5218            node
5219        } else {
5220            let mut node = KdlNode::new("shared_except");
5221            let mut modes = all_modes.clone();
5222            for input_mode in input_modes {
5223                modes.retain(|m| m != input_mode)
5224            }
5225            for mode in modes {
5226                node.push(format!("{:?}", mode).to_lowercase());
5227            }
5228            node
5229        }
5230    }
5231    fn serialize_mode_keybinds(
5232        &self,
5233        keybinds: &BTreeMap<KeyWithModifier, Vec<Action>>,
5234    ) -> KdlDocument {
5235        let mut mode_keybinds = KdlDocument::new();
5236        for keybind in keybinds {
5237            let mut keybind_node = KdlNode::new("bind");
5238            keybind_node.push(keybind.0.to_kdl());
5239            let mut actions = KdlDocument::new();
5240            let mut actions_have_children = false;
5241            for action in keybind.1 {
5242                if let Some(kdl_action) = action.to_kdl() {
5243                    if kdl_action.children().is_some() {
5244                        actions_have_children = true;
5245                    }
5246                    actions.nodes_mut().push(kdl_action);
5247                }
5248            }
5249            if !actions_have_children {
5250                for action in actions.nodes_mut() {
5251                    action.set_leading("");
5252                    action.set_trailing("; ");
5253                }
5254                actions.set_leading(" ");
5255                actions.set_trailing("");
5256            }
5257            keybind_node.set_children(actions);
5258            mode_keybinds.nodes_mut().push(keybind_node);
5259        }
5260        mode_keybinds
5261    }
5262    pub fn to_kdl(&self, should_clear_defaults: bool) -> KdlNode {
5263        let mut keybinds_node = KdlNode::new("keybinds");
5264        if should_clear_defaults {
5265            keybinds_node.insert("clear-defaults", true);
5266        }
5267        let mut minimized = self.minimize_entries();
5268        let mut keybinds_children = KdlDocument::new();
5269
5270        macro_rules! encode_single_input_mode {
5271            ($mode_name:ident) => {{
5272                if let Some(keybinds) = minimized.remove(&BTreeSet::from([InputMode::$mode_name])) {
5273                    let mut mode_node =
5274                        KdlNode::new(format!("{:?}", InputMode::$mode_name).to_lowercase());
5275                    let mode_keybinds = self.serialize_mode_keybinds(&keybinds);
5276                    mode_node.set_children(mode_keybinds);
5277                    keybinds_children.nodes_mut().push(mode_node);
5278                }
5279            }};
5280        }
5281        // we do this explicitly so that the sorting order of modes in the config is more Human
5282        // readable - this is actually less code (and clearer) than implementing Ord in this case
5283        encode_single_input_mode!(Normal);
5284        encode_single_input_mode!(Locked);
5285        encode_single_input_mode!(Pane);
5286        encode_single_input_mode!(Tab);
5287        encode_single_input_mode!(Resize);
5288        encode_single_input_mode!(Move);
5289        encode_single_input_mode!(Scroll);
5290        encode_single_input_mode!(Search);
5291        encode_single_input_mode!(Session);
5292
5293        for (input_modes, keybinds) in minimized {
5294            if input_modes.is_empty() {
5295                log::error!("invalid input mode for keybinds: {:#?}", keybinds);
5296                continue;
5297            }
5298            let mut mode_node = self.serialize_mode_title_node(&input_modes);
5299            let mode_keybinds = self.serialize_mode_keybinds(&keybinds);
5300            mode_node.set_children(mode_keybinds);
5301            keybinds_children.nodes_mut().push(mode_node);
5302        }
5303        keybinds_node.set_children(keybinds_children);
5304        keybinds_node
5305    }
5306}
5307
5308impl KeyWithModifier {
5309    pub fn to_kdl(&self) -> String {
5310        if self.key_modifiers.is_empty() {
5311            self.bare_key.to_kdl()
5312        } else {
5313            format!(
5314                "{} {}",
5315                self.key_modifiers
5316                    .iter()
5317                    .map(|m| m.to_string())
5318                    .collect::<Vec<_>>()
5319                    .join(" "),
5320                self.bare_key.to_kdl()
5321            )
5322        }
5323    }
5324}
5325
5326impl BareKey {
5327    pub fn to_kdl(&self) -> String {
5328        match self {
5329            BareKey::PageDown => format!("PageDown"),
5330            BareKey::PageUp => format!("PageUp"),
5331            BareKey::Left => format!("left"),
5332            BareKey::Down => format!("down"),
5333            BareKey::Up => format!("up"),
5334            BareKey::Right => format!("right"),
5335            BareKey::Home => format!("home"),
5336            BareKey::End => format!("end"),
5337            BareKey::Backspace => format!("backspace"),
5338            BareKey::Delete => format!("del"),
5339            BareKey::Insert => format!("insert"),
5340            BareKey::F(index) => format!("F{}", index),
5341            BareKey::Char(' ') => format!("space"),
5342            BareKey::Char(character) => format!("{}", character),
5343            BareKey::Tab => format!("tab"),
5344            BareKey::Esc => format!("esc"),
5345            BareKey::Enter => format!("enter"),
5346            BareKey::CapsLock => format!("capslock"),
5347            BareKey::ScrollLock => format!("scrolllock"),
5348            BareKey::NumLock => format!("numlock"),
5349            BareKey::PrintScreen => format!("printscreen"),
5350            BareKey::Pause => format!("pause"),
5351            BareKey::Menu => format!("menu"),
5352        }
5353    }
5354}
5355
5356impl Config {
5357    pub fn from_kdl(kdl_config: &str, base_config: Option<Config>) -> Result<Config, ConfigError> {
5358        let mut config = base_config.unwrap_or_else(|| Config::default());
5359        let kdl_config: KdlDocument = kdl_config.parse()?;
5360
5361        let config_options = Options::from_kdl(&kdl_config)?;
5362        config.options = config.options.merge(config_options);
5363
5364        // TODO: handle cases where we have more than one of these blocks (eg. two "keybinds")
5365        // this should give an informative parsing error
5366        if let Some(kdl_keybinds) = kdl_config.get("keybinds") {
5367            config.keybinds = Keybinds::from_kdl(&kdl_keybinds, config.keybinds, &config.options)?;
5368        }
5369        if let Some(kdl_themes) = kdl_config.get("themes") {
5370            let sourced_from_external_file = false;
5371            let config_themes = Themes::from_kdl(kdl_themes, sourced_from_external_file)?;
5372            config.themes = config.themes.merge(config_themes);
5373        }
5374        if let Some(kdl_plugin_aliases) = kdl_config.get("plugins") {
5375            let config_plugins = PluginAliases::from_kdl(kdl_plugin_aliases)?;
5376            config.plugins.merge(config_plugins);
5377        }
5378        if let Some(kdl_load_plugins) = kdl_config.get("load_plugins") {
5379            let load_plugins = load_plugins_from_kdl(kdl_load_plugins)?;
5380            config.background_plugins = load_plugins;
5381        }
5382        if let Some(kdl_ui_config) = kdl_config.get("ui") {
5383            let config_ui = UiConfig::from_kdl(&kdl_ui_config)?;
5384            config.ui = config.ui.merge(config_ui);
5385        }
5386        if let Some(env_config) = kdl_config.get("env") {
5387            let config_env = EnvironmentVariables::from_kdl(&env_config)?;
5388            config.env = config.env.merge(config_env);
5389        }
5390        if let Some(web_client_config) = kdl_config.get("web_client") {
5391            let config_web_client = WebClientConfig::from_kdl(&web_client_config)?;
5392            config.web_client = config.web_client.merge(config_web_client);
5393        }
5394        Ok(config)
5395    }
5396    pub fn to_string(&self, add_comments: bool) -> String {
5397        let mut document = KdlDocument::new();
5398
5399        let clear_defaults = true;
5400        let keybinds = self.keybinds.to_kdl(clear_defaults);
5401        document.nodes_mut().push(keybinds);
5402
5403        if let Some(themes) = self.themes.to_kdl() {
5404            document.nodes_mut().push(themes);
5405        }
5406
5407        let plugins = self.plugins.to_kdl(add_comments);
5408        document.nodes_mut().push(plugins);
5409
5410        let load_plugins = load_plugins_to_kdl(&self.background_plugins, add_comments);
5411        document.nodes_mut().push(load_plugins);
5412
5413        if let Some(ui_config) = self.ui.to_kdl() {
5414            document.nodes_mut().push(ui_config);
5415        }
5416
5417        if let Some(env) = self.env.to_kdl() {
5418            document.nodes_mut().push(env);
5419        }
5420
5421        document.nodes_mut().push(self.web_client.to_kdl());
5422
5423        document
5424            .nodes_mut()
5425            .append(&mut self.options.to_kdl(add_comments));
5426
5427        document.to_string()
5428    }
5429}
5430
5431impl PluginAliases {
5432    pub fn from_kdl(kdl_plugin_aliases: &KdlNode) -> Result<PluginAliases, ConfigError> {
5433        let mut aliases: BTreeMap<String, RunPlugin> = BTreeMap::new();
5434        if let Some(kdl_plugin_aliases) = kdl_children_nodes!(kdl_plugin_aliases) {
5435            for alias_definition in kdl_plugin_aliases {
5436                let alias_name = kdl_name!(alias_definition);
5437                if let Some(string_url) =
5438                    kdl_get_string_property_or_child_value!(alias_definition, "location")
5439                {
5440                    let configuration =
5441                        KdlLayoutParser::parse_plugin_user_configuration(&alias_definition)?;
5442                    let initial_cwd =
5443                        kdl_get_string_property_or_child_value!(alias_definition, "cwd")
5444                            .map(|s| PathBuf::from(s));
5445                    let run_plugin = RunPlugin::from_url(string_url)?
5446                        .with_configuration(configuration.inner().clone())
5447                        .with_initial_cwd(initial_cwd);
5448                    aliases.insert(alias_name.to_owned(), run_plugin);
5449                }
5450            }
5451        }
5452        Ok(PluginAliases { aliases })
5453    }
5454    pub fn to_kdl(&self, add_comments: bool) -> KdlNode {
5455        let mut plugins = KdlNode::new("plugins");
5456        let mut plugins_children = KdlDocument::new();
5457        for (alias_name, plugin_alias) in self.aliases.iter() {
5458            let mut plugin_alias_node = KdlNode::new(alias_name.clone());
5459            let mut plugin_alias_children = KdlDocument::new();
5460            let location_string = plugin_alias.location.display();
5461
5462            plugin_alias_node.insert("location", location_string);
5463            let cwd = plugin_alias.initial_cwd.as_ref();
5464            let mut has_children = false;
5465            if let Some(cwd) = cwd {
5466                has_children = true;
5467                let mut cwd_node = KdlNode::new("cwd");
5468                cwd_node.push(cwd.display().to_string());
5469                plugin_alias_children.nodes_mut().push(cwd_node);
5470            }
5471            let configuration = plugin_alias.configuration.inner();
5472            if !configuration.is_empty() {
5473                has_children = true;
5474                for (config_key, config_value) in configuration {
5475                    let mut node = KdlNode::new(config_key.to_owned());
5476                    if config_value == "true" {
5477                        node.push(KdlValue::Bool(true));
5478                    } else if config_value == "false" {
5479                        node.push(KdlValue::Bool(false));
5480                    } else {
5481                        node.push(config_value.to_string());
5482                    }
5483                    plugin_alias_children.nodes_mut().push(node);
5484                }
5485            }
5486            if has_children {
5487                plugin_alias_node.set_children(plugin_alias_children);
5488            }
5489            plugins_children.nodes_mut().push(plugin_alias_node);
5490        }
5491        plugins.set_children(plugins_children);
5492
5493        if add_comments {
5494            plugins.set_leading(format!(
5495                "\n{}\n{}\n",
5496                "// Plugin aliases - can be used to change the implementation of Zellij",
5497                "// changing these requires a restart to take effect",
5498            ));
5499        }
5500        plugins
5501    }
5502}
5503
5504pub fn load_plugins_to_kdl(
5505    background_plugins: &HashSet<RunPluginOrAlias>,
5506    add_comments: bool,
5507) -> KdlNode {
5508    let mut load_plugins = KdlNode::new("load_plugins");
5509    let mut load_plugins_children = KdlDocument::new();
5510    for run_plugin_or_alias in background_plugins.iter() {
5511        let mut background_plugin_node = KdlNode::new(run_plugin_or_alias.location_string());
5512        let mut background_plugin_children = KdlDocument::new();
5513
5514        let cwd = match run_plugin_or_alias {
5515            RunPluginOrAlias::RunPlugin(run_plugin) => run_plugin.initial_cwd.clone(),
5516            RunPluginOrAlias::Alias(plugin_alias) => plugin_alias.initial_cwd.clone(),
5517        };
5518        let mut has_children = false;
5519        if let Some(cwd) = cwd.as_ref() {
5520            has_children = true;
5521            let mut cwd_node = KdlNode::new("cwd");
5522            cwd_node.push(cwd.display().to_string());
5523            background_plugin_children.nodes_mut().push(cwd_node);
5524        }
5525        let configuration = match run_plugin_or_alias {
5526            RunPluginOrAlias::RunPlugin(run_plugin) => {
5527                Some(run_plugin.configuration.inner().clone())
5528            },
5529            RunPluginOrAlias::Alias(plugin_alias) => plugin_alias
5530                .configuration
5531                .as_ref()
5532                .map(|c| c.inner().clone()),
5533        };
5534        if let Some(configuration) = configuration {
5535            if !configuration.is_empty() {
5536                has_children = true;
5537                for (config_key, config_value) in configuration {
5538                    let mut node = KdlNode::new(config_key.to_owned());
5539                    if config_value == "true" {
5540                        node.push(KdlValue::Bool(true));
5541                    } else if config_value == "false" {
5542                        node.push(KdlValue::Bool(false));
5543                    } else {
5544                        node.push(config_value.to_string());
5545                    }
5546                    background_plugin_children.nodes_mut().push(node);
5547                }
5548            }
5549        }
5550        if has_children {
5551            background_plugin_node.set_children(background_plugin_children);
5552        }
5553        load_plugins_children
5554            .nodes_mut()
5555            .push(background_plugin_node);
5556    }
5557    load_plugins.set_children(load_plugins_children);
5558
5559    if add_comments {
5560        load_plugins.set_leading(format!(
5561            "\n{}\n{}\n{}\n",
5562            "// Plugins to load in the background when a new session starts",
5563            "// eg. \"file:/path/to/my-plugin.wasm\"",
5564            "// eg. \"https://example.com/my-plugin.wasm\"",
5565        ));
5566    }
5567    load_plugins
5568}
5569
5570fn load_plugins_from_kdl(
5571    kdl_load_plugins: &KdlNode,
5572) -> Result<HashSet<RunPluginOrAlias>, ConfigError> {
5573    let mut load_plugins: HashSet<RunPluginOrAlias> = HashSet::new();
5574    if let Some(kdl_load_plugins) = kdl_children_nodes!(kdl_load_plugins) {
5575        for plugin_block in kdl_load_plugins {
5576            let url_node = plugin_block.name();
5577            let string_url = url_node.value();
5578            let configuration = KdlLayoutParser::parse_plugin_user_configuration(&plugin_block)?;
5579            let cwd = kdl_get_string_property_or_child_value!(&plugin_block, "cwd")
5580                .map(|s| PathBuf::from(s));
5581            let run_plugin_or_alias = RunPluginOrAlias::from_url(
5582                &string_url,
5583                &Some(configuration.inner().clone()),
5584                None,
5585                cwd.clone(),
5586            )
5587            .map_err(|e| {
5588                ConfigError::new_kdl_error(
5589                    format!("Failed to parse plugin: {}", e),
5590                    url_node.span().offset(),
5591                    url_node.span().len(),
5592                )
5593            })?
5594            .with_initial_cwd(cwd);
5595            load_plugins.insert(run_plugin_or_alias);
5596        }
5597    }
5598    Ok(load_plugins)
5599}
5600
5601impl UiConfig {
5602    pub fn from_kdl(kdl_ui_config: &KdlNode) -> Result<UiConfig, ConfigError> {
5603        let mut ui_config = UiConfig::default();
5604        if let Some(pane_frames) = kdl_get_child!(kdl_ui_config, "pane_frames") {
5605            let rounded_corners =
5606                kdl_children_property_first_arg_as_bool!(pane_frames, "rounded_corners")
5607                    .unwrap_or(false);
5608            let hide_session_name =
5609                kdl_get_child_entry_bool_value!(pane_frames, "hide_session_name").unwrap_or(false);
5610            let frame_config = FrameConfig {
5611                rounded_corners,
5612                hide_session_name,
5613            };
5614            ui_config.pane_frames = frame_config;
5615        }
5616        Ok(ui_config)
5617    }
5618    pub fn to_kdl(&self) -> Option<KdlNode> {
5619        let mut ui_config = KdlNode::new("ui");
5620        let mut ui_config_children = KdlDocument::new();
5621        let mut frame_config = KdlNode::new("pane_frames");
5622        let mut frame_config_children = KdlDocument::new();
5623        let mut has_ui_config = false;
5624        if self.pane_frames.rounded_corners {
5625            has_ui_config = true;
5626            let mut rounded_corners = KdlNode::new("rounded_corners");
5627            rounded_corners.push(KdlValue::Bool(true));
5628            frame_config_children.nodes_mut().push(rounded_corners);
5629        }
5630        if self.pane_frames.hide_session_name {
5631            has_ui_config = true;
5632            let mut hide_session_name = KdlNode::new("hide_session_name");
5633            hide_session_name.push(KdlValue::Bool(true));
5634            frame_config_children.nodes_mut().push(hide_session_name);
5635        }
5636        if has_ui_config {
5637            frame_config.set_children(frame_config_children);
5638            ui_config_children.nodes_mut().push(frame_config);
5639            ui_config.set_children(ui_config_children);
5640            Some(ui_config)
5641        } else {
5642            None
5643        }
5644    }
5645}
5646
5647impl Themes {
5648    fn style_declaration_from_node(
5649        style_node: &KdlNode,
5650        style_descriptor: &str,
5651    ) -> Result<Option<StyleDeclaration>, ConfigError> {
5652        let descriptor_node = kdl_child_with_name!(style_node, style_descriptor);
5653
5654        match descriptor_node {
5655            Some(descriptor) => {
5656                let colors = kdl_children_or_error!(
5657                    descriptor,
5658                    format!("Missing colors for {}", style_descriptor)
5659                );
5660                Ok(Some(StyleDeclaration {
5661                    base: PaletteColor::try_from(("base", colors))?,
5662                    background: PaletteColor::try_from(("background", colors)).unwrap_or_default(),
5663                    emphasis_0: PaletteColor::try_from(("emphasis_0", colors))?,
5664                    emphasis_1: PaletteColor::try_from(("emphasis_1", colors))?,
5665                    emphasis_2: PaletteColor::try_from(("emphasis_2", colors))?,
5666                    emphasis_3: PaletteColor::try_from(("emphasis_3", colors))?,
5667                }))
5668            },
5669            None => Ok(None),
5670        }
5671    }
5672
5673    fn multiplayer_colors(style_node: &KdlNode) -> Result<MultiplayerColors, ConfigError> {
5674        let descriptor_node = kdl_child_with_name!(style_node, "multiplayer_user_colors");
5675        match descriptor_node {
5676            Some(descriptor) => {
5677                let colors = kdl_children_or_error!(
5678                    descriptor,
5679                    format!("Missing colors for {}", "multiplayer_user_colors")
5680                );
5681                Ok(MultiplayerColors {
5682                    player_1: PaletteColor::try_from(("player_1", colors))
5683                        .unwrap_or(DEFAULT_STYLES.multiplayer_user_colors.player_1),
5684                    player_2: PaletteColor::try_from(("player_2", colors))
5685                        .unwrap_or(DEFAULT_STYLES.multiplayer_user_colors.player_2),
5686                    player_3: PaletteColor::try_from(("player_3", colors))
5687                        .unwrap_or(DEFAULT_STYLES.multiplayer_user_colors.player_3),
5688                    player_4: PaletteColor::try_from(("player_4", colors))
5689                        .unwrap_or(DEFAULT_STYLES.multiplayer_user_colors.player_4),
5690                    player_5: PaletteColor::try_from(("player_5", colors))
5691                        .unwrap_or(DEFAULT_STYLES.multiplayer_user_colors.player_5),
5692                    player_6: PaletteColor::try_from(("player_6", colors))
5693                        .unwrap_or(DEFAULT_STYLES.multiplayer_user_colors.player_6),
5694                    player_7: PaletteColor::try_from(("player_7", colors))
5695                        .unwrap_or(DEFAULT_STYLES.multiplayer_user_colors.player_7),
5696                    player_8: PaletteColor::try_from(("player_8", colors))
5697                        .unwrap_or(DEFAULT_STYLES.multiplayer_user_colors.player_8),
5698                    player_9: PaletteColor::try_from(("player_9", colors))
5699                        .unwrap_or(DEFAULT_STYLES.multiplayer_user_colors.player_9),
5700                    player_10: PaletteColor::try_from(("player_10", colors))
5701                        .unwrap_or(DEFAULT_STYLES.multiplayer_user_colors.player_10),
5702                })
5703            },
5704            None => Ok(DEFAULT_STYLES.multiplayer_user_colors),
5705        }
5706    }
5707
5708    pub fn from_kdl(
5709        themes_from_kdl: &KdlNode,
5710        sourced_from_external_file: bool,
5711    ) -> Result<Self, ConfigError> {
5712        let mut themes: HashMap<String, Theme> = HashMap::new();
5713        for theme_config in kdl_children_nodes_or_error!(themes_from_kdl, "no themes found") {
5714            let theme_name = kdl_name!(theme_config);
5715            let theme_colors = kdl_children_or_error!(theme_config, "empty theme");
5716            let palette_color_names = HashSet::from([
5717                "fg", "bg", "red", "green", "blue", "yellow", "magenta", "orange", "cyan", "black",
5718                "white",
5719            ]);
5720            let theme = if theme_colors
5721                .nodes()
5722                .iter()
5723                .all(|n| palette_color_names.contains(n.name().value()))
5724            {
5725                // Older palette based theme definition
5726                let palette = Palette {
5727                    fg: PaletteColor::try_from(("fg", theme_colors))?,
5728                    bg: PaletteColor::try_from(("bg", theme_colors))?,
5729                    red: PaletteColor::try_from(("red", theme_colors))?,
5730                    green: PaletteColor::try_from(("green", theme_colors))?,
5731                    yellow: PaletteColor::try_from(("yellow", theme_colors))?,
5732                    blue: PaletteColor::try_from(("blue", theme_colors))?,
5733                    magenta: PaletteColor::try_from(("magenta", theme_colors))?,
5734                    orange: PaletteColor::try_from(("orange", theme_colors))?,
5735                    cyan: PaletteColor::try_from(("cyan", theme_colors))?,
5736                    black: PaletteColor::try_from(("black", theme_colors))?,
5737                    white: PaletteColor::try_from(("white", theme_colors))?,
5738                    ..Default::default()
5739                };
5740                Theme {
5741                    palette: palette.into(),
5742                    sourced_from_external_file,
5743                }
5744            } else {
5745                // Newer theme definition with named styles
5746                let s = Styling {
5747                    text_unselected: Themes::style_declaration_from_node(
5748                        theme_config,
5749                        "text_unselected",
5750                    )
5751                    .map(|maybe_style| maybe_style.unwrap_or(DEFAULT_STYLES.text_unselected))?,
5752                    text_selected: Themes::style_declaration_from_node(
5753                        theme_config,
5754                        "text_selected",
5755                    )
5756                    .map(|maybe_style| maybe_style.unwrap_or(DEFAULT_STYLES.text_selected))?,
5757                    ribbon_unselected: Themes::style_declaration_from_node(
5758                        theme_config,
5759                        "ribbon_unselected",
5760                    )
5761                    .map(|maybe_style| maybe_style.unwrap_or(DEFAULT_STYLES.ribbon_unselected))?,
5762                    ribbon_selected: Themes::style_declaration_from_node(
5763                        theme_config,
5764                        "ribbon_selected",
5765                    )
5766                    .map(|maybe_style| maybe_style.unwrap_or(DEFAULT_STYLES.ribbon_selected))?,
5767                    table_title: Themes::style_declaration_from_node(theme_config, "table_title")
5768                        .map(|maybe_style| {
5769                        maybe_style.unwrap_or(DEFAULT_STYLES.table_title)
5770                    })?,
5771                    table_cell_unselected: Themes::style_declaration_from_node(
5772                        theme_config,
5773                        "table_cell_unselected",
5774                    )
5775                    .map(|maybe_style| {
5776                        maybe_style.unwrap_or(DEFAULT_STYLES.table_cell_unselected)
5777                    })?,
5778                    table_cell_selected: Themes::style_declaration_from_node(
5779                        theme_config,
5780                        "table_cell_selected",
5781                    )
5782                    .map(|maybe_style| maybe_style.unwrap_or(DEFAULT_STYLES.table_cell_selected))?,
5783                    list_unselected: Themes::style_declaration_from_node(
5784                        theme_config,
5785                        "list_unselected",
5786                    )
5787                    .map(|maybe_style| maybe_style.unwrap_or(DEFAULT_STYLES.list_unselected))?,
5788                    list_selected: Themes::style_declaration_from_node(
5789                        theme_config,
5790                        "list_selected",
5791                    )
5792                    .map(|maybe_style| maybe_style.unwrap_or(DEFAULT_STYLES.list_selected))?,
5793                    frame_unselected: Themes::style_declaration_from_node(
5794                        theme_config,
5795                        "frame_unselected",
5796                    )?,
5797                    frame_selected: Themes::style_declaration_from_node(
5798                        theme_config,
5799                        "frame_selected",
5800                    )
5801                    .map(|maybe_style| maybe_style.unwrap_or(DEFAULT_STYLES.frame_selected))?,
5802                    frame_highlight: Themes::style_declaration_from_node(
5803                        theme_config,
5804                        "frame_highlight",
5805                    )
5806                    .map(|maybe_style| maybe_style.unwrap_or(DEFAULT_STYLES.frame_highlight))?,
5807                    exit_code_success: Themes::style_declaration_from_node(
5808                        theme_config,
5809                        "exit_code_success",
5810                    )
5811                    .map(|maybe_style| maybe_style.unwrap_or(DEFAULT_STYLES.exit_code_success))?,
5812                    exit_code_error: Themes::style_declaration_from_node(
5813                        theme_config,
5814                        "exit_code_error",
5815                    )
5816                    .map(|maybe_style| maybe_style.unwrap_or(DEFAULT_STYLES.exit_code_error))?,
5817                    multiplayer_user_colors: Themes::multiplayer_colors(theme_config)
5818                        .unwrap_or_default(),
5819                };
5820
5821                Theme {
5822                    palette: s,
5823                    sourced_from_external_file,
5824                }
5825            };
5826            themes.insert(theme_name.into(), theme);
5827        }
5828        let themes = Themes::from_data(themes);
5829        Ok(themes)
5830    }
5831
5832    pub fn from_string(
5833        raw_string: &String,
5834        sourced_from_external_file: bool,
5835    ) -> Result<Self, ConfigError> {
5836        let kdl_config: KdlDocument = raw_string.parse()?;
5837        let kdl_themes = kdl_config.get("themes").ok_or(ConfigError::new_kdl_error(
5838            "No theme node found in file".into(),
5839            kdl_config.span().offset(),
5840            kdl_config.span().len(),
5841        ))?;
5842        let all_themes_in_file = Themes::from_kdl(kdl_themes, sourced_from_external_file)?;
5843        Ok(all_themes_in_file)
5844    }
5845
5846    pub fn from_path(path_to_theme_file: PathBuf) -> Result<Self, ConfigError> {
5847        // String is the theme name
5848        let kdl_config = std::fs::read_to_string(&path_to_theme_file)
5849            .map_err(|e| ConfigError::IoPath(e, path_to_theme_file.clone()))?;
5850        let sourced_from_external_file = true;
5851        Themes::from_string(&kdl_config, sourced_from_external_file).map_err(|e| match e {
5852            ConfigError::KdlError(kdl_error) => ConfigError::KdlError(
5853                kdl_error.add_src(path_to_theme_file.display().to_string(), kdl_config),
5854            ),
5855            e => e,
5856        })
5857    }
5858
5859    pub fn from_dir(path_to_theme_dir: PathBuf) -> Result<Self, ConfigError> {
5860        let mut themes = Themes::default();
5861        for entry in std::fs::read_dir(&path_to_theme_dir)
5862            .map_err(|e| ConfigError::IoPath(e, path_to_theme_dir.clone()))?
5863        {
5864            let entry = entry.map_err(|e| ConfigError::IoPath(e, path_to_theme_dir.clone()))?;
5865            let path = entry.path();
5866            if let Some(extension) = path.extension() {
5867                if extension == "kdl" {
5868                    themes = themes.merge(Themes::from_path(path)?);
5869                }
5870            }
5871        }
5872        Ok(themes)
5873    }
5874    pub fn to_kdl(&self) -> Option<KdlNode> {
5875        let mut theme_node = KdlNode::new("themes");
5876        let mut themes = KdlDocument::new();
5877        let mut has_themes = false;
5878        let sorted_themes: BTreeMap<String, Theme> = self.inner().clone().into_iter().collect();
5879        for (theme_name, theme) in sorted_themes {
5880            if theme.sourced_from_external_file {
5881                // we do not serialize themes that have been defined in external files so as not to
5882                // clog up the configuration file definitions
5883                continue;
5884            }
5885            has_themes = true;
5886            let mut current_theme_node = KdlNode::new(theme_name.clone());
5887            let mut current_theme_node_children = KdlDocument::new();
5888
5889            current_theme_node_children
5890                .nodes_mut()
5891                .push(theme.palette.text_unselected.to_kdl("text_unselected"));
5892            current_theme_node_children
5893                .nodes_mut()
5894                .push(theme.palette.text_selected.to_kdl("text_selected"));
5895            current_theme_node_children
5896                .nodes_mut()
5897                .push(theme.palette.ribbon_selected.to_kdl("ribbon_selected"));
5898            current_theme_node_children
5899                .nodes_mut()
5900                .push(theme.palette.ribbon_unselected.to_kdl("ribbon_unselected"));
5901            current_theme_node_children
5902                .nodes_mut()
5903                .push(theme.palette.table_title.to_kdl("table_title"));
5904            current_theme_node_children.nodes_mut().push(
5905                theme
5906                    .palette
5907                    .table_cell_selected
5908                    .to_kdl("table_cell_selected"),
5909            );
5910            current_theme_node_children.nodes_mut().push(
5911                theme
5912                    .palette
5913                    .table_cell_unselected
5914                    .to_kdl("table_cell_unselected"),
5915            );
5916            current_theme_node_children
5917                .nodes_mut()
5918                .push(theme.palette.list_selected.to_kdl("list_selected"));
5919            current_theme_node_children
5920                .nodes_mut()
5921                .push(theme.palette.list_unselected.to_kdl("list_unselected"));
5922            current_theme_node_children
5923                .nodes_mut()
5924                .push(theme.palette.frame_selected.to_kdl("frame_selected"));
5925
5926            match theme.palette.frame_unselected {
5927                None => {},
5928                Some(frame_unselected_style) => {
5929                    current_theme_node_children
5930                        .nodes_mut()
5931                        .push(frame_unselected_style.to_kdl("frame_unselected"));
5932                },
5933            }
5934            current_theme_node_children
5935                .nodes_mut()
5936                .push(theme.palette.frame_highlight.to_kdl("frame_highlight"));
5937            current_theme_node_children
5938                .nodes_mut()
5939                .push(theme.palette.exit_code_success.to_kdl("exit_code_success"));
5940            current_theme_node_children
5941                .nodes_mut()
5942                .push(theme.palette.exit_code_error.to_kdl("exit_code_error"));
5943            current_theme_node_children
5944                .nodes_mut()
5945                .push(theme.palette.multiplayer_user_colors.to_kdl());
5946            current_theme_node.set_children(current_theme_node_children);
5947            themes.nodes_mut().push(current_theme_node);
5948        }
5949        if has_themes {
5950            theme_node.set_children(themes);
5951            Some(theme_node)
5952        } else {
5953            None
5954        }
5955    }
5956}
5957
5958impl PermissionCache {
5959    pub fn from_string(raw_string: String) -> Result<GrantedPermission, ConfigError> {
5960        let kdl_document: KdlDocument = raw_string.parse()?;
5961
5962        let mut granted_permission = GrantedPermission::default();
5963
5964        for node in kdl_document.nodes() {
5965            if let Some(children) = node.children() {
5966                let key = kdl_name!(node);
5967                let permissions: Vec<PermissionType> = children
5968                    .nodes()
5969                    .iter()
5970                    .filter_map(|p| {
5971                        let v = kdl_name!(p);
5972                        PermissionType::from_str(v).ok()
5973                    })
5974                    .collect();
5975
5976                granted_permission.insert(key.into(), permissions);
5977            }
5978        }
5979
5980        Ok(granted_permission)
5981    }
5982
5983    pub fn to_string(granted: &GrantedPermission) -> String {
5984        let mut kdl_doucment = KdlDocument::new();
5985
5986        granted.iter().for_each(|(k, v)| {
5987            let mut node = KdlNode::new(k.as_str());
5988            let mut children = KdlDocument::new();
5989
5990            let permissions: HashSet<PermissionType> = v.clone().into_iter().collect();
5991            permissions.iter().for_each(|f| {
5992                let n = KdlNode::new(f.to_string().as_str());
5993                children.nodes_mut().push(n);
5994            });
5995
5996            node.set_children(children);
5997            kdl_doucment.nodes_mut().push(node);
5998        });
5999
6000        kdl_doucment.fmt();
6001        kdl_doucment.to_string()
6002    }
6003}
6004
6005impl SessionInfo {
6006    pub fn from_string(raw_session_info: &str, current_session_name: &str) -> Result<Self, String> {
6007        let kdl_document: KdlDocument = raw_session_info
6008            .parse()
6009            .map_err(|e| format!("Failed to parse kdl document: {}", e))?;
6010        let name = kdl_document
6011            .get("name")
6012            .and_then(|n| n.entries().iter().next())
6013            .and_then(|e| e.value().as_string())
6014            .map(|s| s.to_owned())
6015            .ok_or("Failed to parse session name")?;
6016        let connected_clients = kdl_document
6017            .get("connected_clients")
6018            .and_then(|n| n.entries().iter().next())
6019            .and_then(|e| e.value().as_i64())
6020            .map(|c| c as usize)
6021            .ok_or("Failed to parse connected_clients")?;
6022        let tabs: Vec<TabInfo> = kdl_document
6023            .get("tabs")
6024            .and_then(|t| t.children())
6025            .and_then(|c| {
6026                let mut tab_nodes = vec![];
6027                for tab_node in c.nodes() {
6028                    if let Some(tab) = tab_node.children() {
6029                        tab_nodes.push(TabInfo::decode_from_kdl(tab).ok()?);
6030                    }
6031                }
6032                Some(tab_nodes)
6033            })
6034            .ok_or("Failed to parse tabs")?;
6035        let panes: PaneManifest = kdl_document
6036            .get("panes")
6037            .and_then(|p| p.children())
6038            .map(|p| PaneManifest::decode_from_kdl(p))
6039            .ok_or("Failed to parse panes")?;
6040        let available_layouts: Vec<LayoutInfo> = kdl_document
6041            .get("available_layouts")
6042            .and_then(|p| p.children())
6043            .map(|e| {
6044                e.nodes()
6045                    .iter()
6046                    .filter_map(|n| {
6047                        let layout_name = n.name().value().to_owned();
6048                        let layout_source = n
6049                            .entries()
6050                            .iter()
6051                            .find(|e| e.name().map(|n| n.value()) == Some("source"))
6052                            .and_then(|e| e.value().as_string());
6053                        match layout_source {
6054                            Some(layout_source) => match layout_source {
6055                                "built-in" => Some(LayoutInfo::BuiltIn(layout_name)),
6056                                "file" => {
6057                                    Some(LayoutInfo::File(layout_name, LayoutMetadata::default()))
6058                                },
6059                                _ => None,
6060                            },
6061                            None => None,
6062                        }
6063                    })
6064                    .collect()
6065            })
6066            .ok_or("Failed to parse available_layouts")?;
6067        let web_client_count = kdl_document
6068            .get("web_client_count")
6069            .and_then(|n| n.entries().iter().next())
6070            .and_then(|e| e.value().as_i64())
6071            .map(|c| c as usize)
6072            .unwrap_or(0);
6073        let web_clients_allowed = kdl_document
6074            .get("web_clients_allowed")
6075            .and_then(|n| n.entries().iter().next())
6076            .and_then(|e| e.value().as_bool())
6077            .unwrap_or(false);
6078        let is_current_session = name == current_session_name;
6079        let mut tab_history = BTreeMap::new();
6080        if let Some(kdl_tab_history) = kdl_document.get("tab_history").and_then(|p| p.children()) {
6081            for client_node in kdl_tab_history.nodes() {
6082                if let Some(client_id) = client_node.children().and_then(|c| {
6083                    c.get("id")
6084                        .and_then(|c| c.entries().iter().next().and_then(|e| e.value().as_i64()))
6085                }) {
6086                    let mut history = vec![];
6087                    if let Some(history_entries) = client_node
6088                        .children()
6089                        .and_then(|c| c.get("history"))
6090                        .map(|h| h.entries())
6091                    {
6092                        for entry in history_entries {
6093                            if let Some(entry) = entry.value().as_i64() {
6094                                history.push(entry as usize);
6095                            }
6096                        }
6097                    }
6098                    tab_history.insert(client_id as u16, history);
6099                }
6100            }
6101        }
6102        let mut pane_history = BTreeMap::new();
6103        if let Some(kdl_pane_history) = kdl_document.get("pane_history").and_then(|p| p.children())
6104        {
6105            for client_node in kdl_pane_history.nodes() {
6106                if let Some(client_id) = client_node.children().and_then(|c| {
6107                    c.get("id")
6108                        .and_then(|c| c.entries().iter().next().and_then(|e| e.value().as_i64()))
6109                }) {
6110                    let mut history = vec![];
6111                    if let Some(history_node) =
6112                        client_node.children().and_then(|c| c.get("history"))
6113                    {
6114                        if let Some(history_children) = history_node.children() {
6115                            for pane_id_node in history_children.nodes() {
6116                                if pane_id_node.name().value() == "pane_id" {
6117                                    let pane_type = pane_id_node
6118                                        .entries()
6119                                        .iter()
6120                                        .find(|e| e.name().map(|n| n.value()) == Some("type"))
6121                                        .and_then(|e| e.value().as_string());
6122                                    let id = pane_id_node
6123                                        .entries()
6124                                        .iter()
6125                                        .find(|e| e.name().is_none())
6126                                        .and_then(|e| e.value().as_i64())
6127                                        .map(|i| i as u32);
6128                                    if let (Some(pane_type), Some(id)) = (pane_type, id) {
6129                                        let pane_id = match pane_type {
6130                                            "terminal" => Some(PaneId::Terminal(id)),
6131                                            "plugin" => Some(PaneId::Plugin(id)),
6132                                            _ => None,
6133                                        };
6134                                        if let Some(pane_id) = pane_id {
6135                                            history.push(pane_id);
6136                                        }
6137                                    }
6138                                }
6139                            }
6140                        }
6141                    }
6142                    pane_history.insert(client_id as u16, history);
6143                }
6144            }
6145        }
6146        let creation_time = kdl_document
6147            .get("creation_time")
6148            .and_then(|n| n.entries().iter().next())
6149            .and_then(|e| e.value().as_i64())
6150            .map(|c| Duration::from_secs(c as u64))
6151            .unwrap_or_default();
6152        Ok(SessionInfo {
6153            name,
6154            tabs,
6155            panes,
6156            connected_clients,
6157            is_current_session,
6158            available_layouts,
6159            web_client_count,
6160            web_clients_allowed,
6161            plugins: Default::default(), // we do not serialize plugin information
6162            tab_history,
6163            pane_history,
6164            creation_time,
6165        })
6166    }
6167    pub fn to_string(&self) -> String {
6168        let mut kdl_document = KdlDocument::new();
6169
6170        let mut name = KdlNode::new("name");
6171        name.push(self.name.clone());
6172
6173        let mut connected_clients = KdlNode::new("connected_clients");
6174        connected_clients.push(self.connected_clients as i64);
6175
6176        let mut tabs = KdlNode::new("tabs");
6177        let mut tab_children = KdlDocument::new();
6178        for tab_info in &self.tabs {
6179            let mut tab = KdlNode::new("tab");
6180            let kdl_tab_info = tab_info.encode_to_kdl();
6181            tab.set_children(kdl_tab_info);
6182            tab_children.nodes_mut().push(tab);
6183        }
6184        tabs.set_children(tab_children);
6185
6186        let mut panes = KdlNode::new("panes");
6187        panes.set_children(self.panes.encode_to_kdl());
6188
6189        let mut web_client_count = KdlNode::new("web_client_count");
6190        web_client_count.push(self.web_client_count as i64);
6191
6192        let mut web_clients_allowed = KdlNode::new("web_clients_allowed");
6193        web_clients_allowed.push(self.web_clients_allowed);
6194
6195        let mut available_layouts = KdlNode::new("available_layouts");
6196        let mut available_layouts_children = KdlDocument::new();
6197        for layout_info in &self.available_layouts {
6198            let (layout_name, layout_source) = match layout_info {
6199                LayoutInfo::File(name, _layout_metadata) => (name.clone(), "file"),
6200                LayoutInfo::BuiltIn(name) => (name.clone(), "built-in"),
6201                LayoutInfo::Url(url) => (url.clone(), "url"),
6202                LayoutInfo::Stringified(_stringified) => ("stringified-layout".to_owned(), "N/A"),
6203            };
6204            let mut layout_node = KdlNode::new(format!("{}", layout_name));
6205            let layout_source = KdlEntry::new_prop("source", layout_source);
6206            layout_node.entries_mut().push(layout_source);
6207            available_layouts_children.nodes_mut().push(layout_node);
6208        }
6209        available_layouts.set_children(available_layouts_children);
6210
6211        let mut tab_history = KdlNode::new("tab_history");
6212        let mut tab_history_children = KdlDocument::new();
6213        for (client_id, client_tab_history) in &self.tab_history {
6214            let mut client_document = KdlDocument::new();
6215            let mut client_node = KdlNode::new("client");
6216            let mut id = KdlNode::new("id");
6217            id.push(*client_id as i64);
6218            client_document.nodes_mut().push(id);
6219            let mut history = KdlNode::new("history");
6220            for entry in client_tab_history {
6221                history.push(*entry as i64);
6222            }
6223            client_document.nodes_mut().push(history);
6224            client_node.set_children(client_document);
6225            tab_history_children.nodes_mut().push(client_node);
6226        }
6227        tab_history.set_children(tab_history_children);
6228
6229        let mut pane_history = KdlNode::new("pane_history");
6230        let mut pane_history_children = KdlDocument::new();
6231        for (client_id, client_pane_history) in &self.pane_history {
6232            let mut client_document = KdlDocument::new();
6233            let mut client_node = KdlNode::new("client");
6234            let mut id = KdlNode::new("id");
6235            id.push(*client_id as i64);
6236            client_document.nodes_mut().push(id);
6237            let mut history = KdlNode::new("history");
6238            for pane_id in client_pane_history {
6239                let mut pane_id_node = KdlNode::new("pane_id");
6240                match pane_id {
6241                    PaneId::Terminal(id) => {
6242                        pane_id_node.push(KdlEntry::new_prop("type", "terminal"));
6243                        pane_id_node.push(*id as i64);
6244                    },
6245                    PaneId::Plugin(id) => {
6246                        pane_id_node.push(KdlEntry::new_prop("type", "plugin"));
6247                        pane_id_node.push(*id as i64);
6248                    },
6249                }
6250                history.ensure_children().nodes_mut().push(pane_id_node);
6251            }
6252            client_document.nodes_mut().push(history);
6253            client_node.set_children(client_document);
6254            pane_history_children.nodes_mut().push(client_node);
6255        }
6256        pane_history.set_children(pane_history_children);
6257
6258        kdl_document.nodes_mut().push(name);
6259        kdl_document.nodes_mut().push(tabs);
6260        kdl_document.nodes_mut().push(panes);
6261        kdl_document.nodes_mut().push(connected_clients);
6262        kdl_document.nodes_mut().push(web_clients_allowed);
6263        kdl_document.nodes_mut().push(web_client_count);
6264        kdl_document.nodes_mut().push(available_layouts);
6265        kdl_document.nodes_mut().push(tab_history);
6266        kdl_document.nodes_mut().push(pane_history);
6267
6268        let mut creation_time_node = KdlNode::new("creation_time");
6269        creation_time_node.push(self.creation_time.as_secs() as i64);
6270        kdl_document.nodes_mut().push(creation_time_node);
6271
6272        kdl_document.fmt();
6273        kdl_document.to_string()
6274    }
6275}
6276
6277impl TabInfo {
6278    pub fn decode_from_kdl(kdl_document: &KdlDocument) -> Result<Self, String> {
6279        macro_rules! int_node {
6280            ($name:expr, $type:ident) => {{
6281                kdl_document
6282                    .get($name)
6283                    .and_then(|n| n.entries().iter().next())
6284                    .and_then(|e| e.value().as_i64())
6285                    .map(|e| e as $type)
6286                    .ok_or(format!("Failed to parse tab {}", $name))?
6287            }};
6288        }
6289        macro_rules! string_node {
6290            ($name:expr) => {{
6291                kdl_document
6292                    .get($name)
6293                    .and_then(|n| n.entries().iter().next())
6294                    .and_then(|e| e.value().as_string())
6295                    .map(|s| s.to_owned())
6296                    .ok_or(format!("Failed to parse tab {}", $name))?
6297            }};
6298        }
6299        macro_rules! optional_string_node {
6300            ($name:expr) => {{
6301                kdl_document
6302                    .get($name)
6303                    .and_then(|n| n.entries().iter().next())
6304                    .and_then(|e| e.value().as_string())
6305                    .map(|s| s.to_owned())
6306            }};
6307        }
6308        macro_rules! optional_int_node {
6309            ($name:expr, $type:ident) => {{
6310                kdl_document
6311                    .get($name)
6312                    .and_then(|n| n.entries().iter().next())
6313                    .and_then(|e| e.value().as_i64())
6314                    .map(|e| e as $type)
6315            }};
6316        }
6317        macro_rules! bool_node {
6318            ($name:expr) => {{
6319                kdl_document
6320                    .get($name)
6321                    .and_then(|n| n.entries().iter().next())
6322                    .and_then(|e| e.value().as_bool())
6323                    .ok_or(format!("Failed to parse tab {}", $name))?
6324            }};
6325        }
6326
6327        let position = int_node!("position", usize);
6328        let name = string_node!("name");
6329        let active = bool_node!("active");
6330        let panes_to_hide = int_node!("panes_to_hide", usize);
6331        let is_fullscreen_active = bool_node!("is_fullscreen_active");
6332        let is_sync_panes_active = bool_node!("is_sync_panes_active");
6333        let are_floating_panes_visible = bool_node!("are_floating_panes_visible");
6334        let mut other_focused_clients = vec![];
6335        if let Some(tab_other_focused_clients) = kdl_document
6336            .get("other_focused_clients")
6337            .map(|n| n.entries())
6338        {
6339            for entry in tab_other_focused_clients {
6340                if let Some(entry_parsed) = entry.value().as_i64() {
6341                    other_focused_clients.push(entry_parsed as u16);
6342                }
6343            }
6344        }
6345        let active_swap_layout_name = optional_string_node!("active_swap_layout_name");
6346        let viewport_rows = optional_int_node!("viewport_rows", usize).unwrap_or(0);
6347        let viewport_columns = optional_int_node!("viewport_columns", usize).unwrap_or(0);
6348        let display_area_rows = optional_int_node!("display_area_rows", usize).unwrap_or(0);
6349        let display_area_columns = optional_int_node!("display_area_columns", usize).unwrap_or(0);
6350        let is_swap_layout_dirty = bool_node!("is_swap_layout_dirty");
6351        let selectable_tiled_panes_count =
6352            optional_int_node!("selectable_tiled_panes_count", usize).unwrap_or(0);
6353        let selectable_floating_panes_count =
6354            optional_int_node!("selectable_floating_panes_count", usize).unwrap_or(0);
6355        let tab_id = optional_int_node!("tab_id", usize).unwrap_or(0);
6356        Ok(TabInfo {
6357            position,
6358            name,
6359            active,
6360            panes_to_hide,
6361            is_fullscreen_active,
6362            is_sync_panes_active,
6363            are_floating_panes_visible,
6364            other_focused_clients,
6365            active_swap_layout_name,
6366            is_swap_layout_dirty,
6367            viewport_rows,
6368            viewport_columns,
6369            display_area_rows,
6370            display_area_columns,
6371            selectable_tiled_panes_count,
6372            selectable_floating_panes_count,
6373            tab_id,
6374            has_bell_notification: false,
6375            is_flashing_bell: false,
6376        })
6377    }
6378    pub fn encode_to_kdl(&self) -> KdlDocument {
6379        let mut kdl_doucment = KdlDocument::new();
6380
6381        let mut position = KdlNode::new("position");
6382        position.push(self.position as i64);
6383        kdl_doucment.nodes_mut().push(position);
6384
6385        let mut name = KdlNode::new("name");
6386        name.push(self.name.clone());
6387        kdl_doucment.nodes_mut().push(name);
6388
6389        let mut active = KdlNode::new("active");
6390        active.push(self.active);
6391        kdl_doucment.nodes_mut().push(active);
6392
6393        let mut panes_to_hide = KdlNode::new("panes_to_hide");
6394        panes_to_hide.push(self.panes_to_hide as i64);
6395        kdl_doucment.nodes_mut().push(panes_to_hide);
6396
6397        let mut is_fullscreen_active = KdlNode::new("is_fullscreen_active");
6398        is_fullscreen_active.push(self.is_fullscreen_active);
6399        kdl_doucment.nodes_mut().push(is_fullscreen_active);
6400
6401        let mut is_sync_panes_active = KdlNode::new("is_sync_panes_active");
6402        is_sync_panes_active.push(self.is_sync_panes_active);
6403        kdl_doucment.nodes_mut().push(is_sync_panes_active);
6404
6405        let mut are_floating_panes_visible = KdlNode::new("are_floating_panes_visible");
6406        are_floating_panes_visible.push(self.are_floating_panes_visible);
6407        kdl_doucment.nodes_mut().push(are_floating_panes_visible);
6408
6409        if !self.other_focused_clients.is_empty() {
6410            let mut other_focused_clients = KdlNode::new("other_focused_clients");
6411            for client_id in &self.other_focused_clients {
6412                other_focused_clients.push(*client_id as i64);
6413            }
6414            kdl_doucment.nodes_mut().push(other_focused_clients);
6415        }
6416
6417        if let Some(active_swap_layout_name) = self.active_swap_layout_name.as_ref() {
6418            let mut active_swap_layout = KdlNode::new("active_swap_layout_name");
6419            active_swap_layout.push(active_swap_layout_name.to_string());
6420            kdl_doucment.nodes_mut().push(active_swap_layout);
6421        }
6422
6423        let mut viewport_rows = KdlNode::new("viewport_rows");
6424        viewport_rows.push(self.viewport_rows as i64);
6425        kdl_doucment.nodes_mut().push(viewport_rows);
6426
6427        let mut viewport_columns = KdlNode::new("viewport_columns");
6428        viewport_columns.push(self.viewport_columns as i64);
6429        kdl_doucment.nodes_mut().push(viewport_columns);
6430
6431        let mut display_area_columns = KdlNode::new("display_area_columns");
6432        display_area_columns.push(self.display_area_columns as i64);
6433        kdl_doucment.nodes_mut().push(display_area_columns);
6434
6435        let mut display_area_rows = KdlNode::new("display_area_rows");
6436        display_area_rows.push(self.display_area_rows as i64);
6437        kdl_doucment.nodes_mut().push(display_area_rows);
6438
6439        let mut is_swap_layout_dirty = KdlNode::new("is_swap_layout_dirty");
6440        is_swap_layout_dirty.push(self.is_swap_layout_dirty);
6441        kdl_doucment.nodes_mut().push(is_swap_layout_dirty);
6442
6443        let mut selectable_tiled_panes_count = KdlNode::new("selectable_tiled_panes_count");
6444        selectable_tiled_panes_count.push(self.selectable_tiled_panes_count as i64);
6445        kdl_doucment.nodes_mut().push(selectable_tiled_panes_count);
6446
6447        let mut selectable_floating_panes_count = KdlNode::new("selectable_floating_panes_count");
6448        selectable_floating_panes_count.push(self.selectable_floating_panes_count as i64);
6449        kdl_doucment
6450            .nodes_mut()
6451            .push(selectable_floating_panes_count);
6452
6453        let mut tab_id = KdlNode::new("tab_id");
6454        tab_id.push(self.tab_id as i64);
6455        kdl_doucment.nodes_mut().push(tab_id);
6456
6457        kdl_doucment
6458    }
6459}
6460
6461impl PaneManifest {
6462    pub fn decode_from_kdl(kdl_doucment: &KdlDocument) -> Self {
6463        let mut panes: HashMap<usize, Vec<PaneInfo>> = HashMap::new();
6464        for node in kdl_doucment.nodes() {
6465            if node.name().to_string() == "pane" {
6466                if let Some(pane_document) = node.children() {
6467                    if let Ok((tab_position, pane_info)) = PaneInfo::decode_from_kdl(pane_document)
6468                    {
6469                        let panes_in_tab_position =
6470                            panes.entry(tab_position).or_insert_with(Vec::new);
6471                        panes_in_tab_position.push(pane_info);
6472                    }
6473                }
6474            }
6475        }
6476        PaneManifest { panes }
6477    }
6478    pub fn encode_to_kdl(&self) -> KdlDocument {
6479        let mut kdl_doucment = KdlDocument::new();
6480        for (tab_position, panes) in &self.panes {
6481            for pane in panes {
6482                let mut pane_node = KdlNode::new("pane");
6483                let mut pane = pane.encode_to_kdl();
6484
6485                let mut position_node = KdlNode::new("tab_position");
6486                position_node.push(*tab_position as i64);
6487                pane.nodes_mut().push(position_node);
6488
6489                pane_node.set_children(pane);
6490                kdl_doucment.nodes_mut().push(pane_node);
6491            }
6492        }
6493        kdl_doucment
6494    }
6495}
6496
6497impl PaneInfo {
6498    pub fn decode_from_kdl(kdl_document: &KdlDocument) -> Result<(usize, Self), String> {
6499        // usize is the tab position
6500        macro_rules! int_node {
6501            ($name:expr, $type:ident) => {{
6502                kdl_document
6503                    .get($name)
6504                    .and_then(|n| n.entries().iter().next())
6505                    .and_then(|e| e.value().as_i64())
6506                    .map(|e| e as $type)
6507                    .ok_or(format!("Failed to parse pane {}", $name))?
6508            }};
6509        }
6510        macro_rules! optional_int_node {
6511            ($name:expr, $type:ident) => {{
6512                kdl_document
6513                    .get($name)
6514                    .and_then(|n| n.entries().iter().next())
6515                    .and_then(|e| e.value().as_i64())
6516                    .map(|e| e as $type)
6517            }};
6518        }
6519        macro_rules! bool_node {
6520            ($name:expr) => {{
6521                kdl_document
6522                    .get($name)
6523                    .and_then(|n| n.entries().iter().next())
6524                    .and_then(|e| e.value().as_bool())
6525                    .ok_or(format!("Failed to parse pane {}", $name))?
6526            }};
6527        }
6528        macro_rules! string_node {
6529            ($name:expr) => {{
6530                kdl_document
6531                    .get($name)
6532                    .and_then(|n| n.entries().iter().next())
6533                    .and_then(|e| e.value().as_string())
6534                    .map(|s| s.to_owned())
6535                    .ok_or(format!("Failed to parse pane {}", $name))?
6536            }};
6537        }
6538        macro_rules! optional_string_node {
6539            ($name:expr) => {{
6540                kdl_document
6541                    .get($name)
6542                    .and_then(|n| n.entries().iter().next())
6543                    .and_then(|e| e.value().as_string())
6544                    .map(|s| s.to_owned())
6545            }};
6546        }
6547        let tab_position = int_node!("tab_position", usize);
6548        let id = int_node!("id", u32);
6549
6550        let is_plugin = bool_node!("is_plugin");
6551        let is_focused = bool_node!("is_focused");
6552        let is_fullscreen = bool_node!("is_fullscreen");
6553        let is_floating = bool_node!("is_floating");
6554        let is_suppressed = bool_node!("is_suppressed");
6555        let title = string_node!("title");
6556        let exited = bool_node!("exited");
6557        let exit_status = optional_int_node!("exit_status", i32);
6558        let is_held = bool_node!("is_held");
6559        let pane_x = int_node!("pane_x", usize);
6560        let pane_content_x = int_node!("pane_content_x", usize);
6561        let pane_y = int_node!("pane_y", usize);
6562        let pane_content_y = int_node!("pane_content_y", usize);
6563        let pane_rows = int_node!("pane_rows", usize);
6564        let pane_content_rows = int_node!("pane_content_rows", usize);
6565        let pane_columns = int_node!("pane_columns", usize);
6566        let pane_content_columns = int_node!("pane_content_columns", usize);
6567        let cursor_coordinates_in_pane = kdl_document
6568            .get("cursor_coordinates_in_pane")
6569            .map(|n| {
6570                let mut entries = n.entries().iter();
6571                (entries.next(), entries.next())
6572            })
6573            .and_then(|(x, y)| {
6574                let x = x.and_then(|x| x.value().as_i64()).map(|x| x as usize);
6575                let y = y.and_then(|y| y.value().as_i64()).map(|y| y as usize);
6576                match (x, y) {
6577                    (Some(x), Some(y)) => Some((x, y)),
6578                    _ => None,
6579                }
6580            });
6581        let terminal_command = optional_string_node!("terminal_command");
6582        let plugin_url = optional_string_node!("plugin_url");
6583        let is_selectable = bool_node!("is_selectable");
6584
6585        let pane_info = PaneInfo {
6586            id,
6587            is_plugin,
6588            is_focused,
6589            is_fullscreen,
6590            is_floating,
6591            is_suppressed,
6592            title,
6593            exited,
6594            exit_status,
6595            is_held,
6596            pane_x,
6597            pane_content_x,
6598            pane_y,
6599            pane_content_y,
6600            pane_rows,
6601            pane_content_rows,
6602            pane_columns,
6603            pane_content_columns,
6604            cursor_coordinates_in_pane,
6605            terminal_command,
6606            plugin_url,
6607            is_selectable,
6608            index_in_pane_group: Default::default(), // we don't serialize this
6609            default_fg: None,
6610            default_bg: None,
6611        };
6612        Ok((tab_position, pane_info))
6613    }
6614    pub fn encode_to_kdl(&self) -> KdlDocument {
6615        let mut kdl_doucment = KdlDocument::new();
6616        macro_rules! int_node {
6617            ($name:expr, $val:expr) => {{
6618                let mut att = KdlNode::new($name);
6619                att.push($val as i64);
6620                kdl_doucment.nodes_mut().push(att);
6621            }};
6622        }
6623        macro_rules! bool_node {
6624            ($name:expr, $val:expr) => {{
6625                let mut att = KdlNode::new($name);
6626                att.push($val);
6627                kdl_doucment.nodes_mut().push(att);
6628            }};
6629        }
6630        macro_rules! string_node {
6631            ($name:expr, $val:expr) => {{
6632                let mut att = KdlNode::new($name);
6633                att.push($val);
6634                kdl_doucment.nodes_mut().push(att);
6635            }};
6636        }
6637
6638        int_node!("id", self.id);
6639        bool_node!("is_plugin", self.is_plugin);
6640        bool_node!("is_focused", self.is_focused);
6641        bool_node!("is_fullscreen", self.is_fullscreen);
6642        bool_node!("is_floating", self.is_floating);
6643        bool_node!("is_suppressed", self.is_suppressed);
6644        string_node!("title", self.title.to_string());
6645        bool_node!("exited", self.exited);
6646        if let Some(exit_status) = self.exit_status {
6647            int_node!("exit_status", exit_status);
6648        }
6649        bool_node!("is_held", self.is_held);
6650        int_node!("pane_x", self.pane_x);
6651        int_node!("pane_content_x", self.pane_content_x);
6652        int_node!("pane_y", self.pane_y);
6653        int_node!("pane_content_y", self.pane_content_y);
6654        int_node!("pane_rows", self.pane_rows);
6655        int_node!("pane_content_rows", self.pane_content_rows);
6656        int_node!("pane_columns", self.pane_columns);
6657        int_node!("pane_content_columns", self.pane_content_columns);
6658        if let Some((cursor_x, cursor_y)) = self.cursor_coordinates_in_pane {
6659            let mut cursor_coordinates_in_pane = KdlNode::new("cursor_coordinates_in_pane");
6660            cursor_coordinates_in_pane.push(cursor_x as i64);
6661            cursor_coordinates_in_pane.push(cursor_y as i64);
6662            kdl_doucment.nodes_mut().push(cursor_coordinates_in_pane);
6663        }
6664        if let Some(terminal_command) = &self.terminal_command {
6665            string_node!("terminal_command", terminal_command.to_string());
6666        }
6667        if let Some(plugin_url) = &self.plugin_url {
6668            string_node!("plugin_url", plugin_url.to_string());
6669        }
6670        bool_node!("is_selectable", self.is_selectable);
6671        kdl_doucment
6672    }
6673}
6674
6675pub fn parse_plugin_user_configuration(
6676    plugin_block: &KdlNode,
6677) -> Result<BTreeMap<String, String>, ConfigError> {
6678    let mut configuration = BTreeMap::new();
6679    for user_configuration_entry in plugin_block.entries() {
6680        let name = user_configuration_entry.name();
6681        let value = user_configuration_entry.value();
6682        if let Some(name) = name {
6683            let name = name.to_string();
6684            if KdlLayoutParser::is_a_reserved_plugin_property(&name) {
6685                continue;
6686            }
6687            configuration.insert(name, value.to_string());
6688        }
6689    }
6690    if let Some(user_config) = kdl_children_nodes!(plugin_block) {
6691        for user_configuration_entry in user_config {
6692            let config_entry_name = kdl_name!(user_configuration_entry);
6693            if KdlLayoutParser::is_a_reserved_plugin_property(&config_entry_name) {
6694                continue;
6695            }
6696            let config_entry_str_value = kdl_first_entry_as_string!(user_configuration_entry)
6697                .map(|s| format!("{}", s.to_string()));
6698            let config_entry_int_value = kdl_first_entry_as_i64!(user_configuration_entry)
6699                .map(|s| format!("{}", s.to_string()));
6700            let config_entry_bool_value = kdl_first_entry_as_bool!(user_configuration_entry)
6701                .map(|s| format!("{}", s.to_string()));
6702            let config_entry_children = user_configuration_entry
6703                .children()
6704                .map(|s| format!("{}", s.to_string().trim()));
6705            let config_entry_value = config_entry_str_value
6706                .or(config_entry_int_value)
6707                .or(config_entry_bool_value)
6708                .or(config_entry_children)
6709                .ok_or(ConfigError::new_kdl_error(
6710                    format!(
6711                        "Failed to parse plugin block configuration: {:?}",
6712                        user_configuration_entry
6713                    ),
6714                    plugin_block.span().offset(),
6715                    plugin_block.span().len(),
6716                ))?;
6717            configuration.insert(config_entry_name.into(), config_entry_value);
6718        }
6719    }
6720    Ok(configuration)
6721}
6722
6723#[test]
6724fn serialize_and_deserialize_session_info() {
6725    let session_info = SessionInfo::default();
6726    let serialized = session_info.to_string();
6727    let deserealized = SessionInfo::from_string(&serialized, "not this session").unwrap();
6728    assert_eq!(session_info, deserealized);
6729    insta::assert_snapshot!(serialized);
6730}
6731
6732#[test]
6733fn serialize_and_deserialize_session_info_with_data() {
6734    let panes_list = vec![
6735        PaneInfo {
6736            id: 1,
6737            is_plugin: false,
6738            is_focused: true,
6739            is_fullscreen: true,
6740            is_floating: false,
6741            is_suppressed: false,
6742            title: "pane 1".to_owned(),
6743            exited: false,
6744            exit_status: None,
6745            is_held: false,
6746            pane_x: 0,
6747            pane_content_x: 1,
6748            pane_y: 0,
6749            pane_content_y: 1,
6750            pane_rows: 5,
6751            pane_content_rows: 4,
6752            pane_columns: 22,
6753            pane_content_columns: 21,
6754            cursor_coordinates_in_pane: Some((0, 0)),
6755            terminal_command: Some("foo".to_owned()),
6756            plugin_url: None,
6757            is_selectable: true,
6758            index_in_pane_group: Default::default(), // we don't serialize this
6759            default_fg: None,
6760            default_bg: None,
6761        },
6762        PaneInfo {
6763            id: 1,
6764            is_plugin: true,
6765            is_focused: true,
6766            is_fullscreen: true,
6767            is_floating: false,
6768            is_suppressed: false,
6769            title: "pane 1".to_owned(),
6770            exited: false,
6771            exit_status: None,
6772            is_held: false,
6773            pane_x: 0,
6774            pane_content_x: 1,
6775            pane_y: 0,
6776            pane_content_y: 1,
6777            pane_rows: 5,
6778            pane_content_rows: 4,
6779            pane_columns: 22,
6780            pane_content_columns: 21,
6781            cursor_coordinates_in_pane: Some((0, 0)),
6782            terminal_command: None,
6783            plugin_url: Some("i_am_a_fake_plugin".to_owned()),
6784            is_selectable: true,
6785            index_in_pane_group: Default::default(), // we don't serialize this
6786            default_fg: None,
6787            default_bg: None,
6788        },
6789    ];
6790    let mut panes = HashMap::new();
6791    panes.insert(0, panes_list);
6792    let session_info = SessionInfo {
6793        name: "my session name".to_owned(),
6794        tabs: vec![
6795            TabInfo {
6796                position: 0,
6797                name: "tab 1".to_owned(),
6798                active: true,
6799                panes_to_hide: 1,
6800                is_fullscreen_active: true,
6801                is_sync_panes_active: false,
6802                are_floating_panes_visible: true,
6803                other_focused_clients: vec![2, 3],
6804                active_swap_layout_name: Some("BASE".to_owned()),
6805                is_swap_layout_dirty: true,
6806                viewport_rows: 10,
6807                viewport_columns: 10,
6808                display_area_rows: 10,
6809                display_area_columns: 10,
6810                selectable_tiled_panes_count: 10,
6811                selectable_floating_panes_count: 10,
6812                tab_id: 0,
6813                is_flashing_bell: false,
6814                has_bell_notification: false,
6815            },
6816            TabInfo {
6817                position: 1,
6818                name: "tab 2".to_owned(),
6819                active: true,
6820                panes_to_hide: 0,
6821                is_fullscreen_active: false,
6822                is_sync_panes_active: true,
6823                are_floating_panes_visible: true,
6824                other_focused_clients: vec![2, 3],
6825                active_swap_layout_name: None,
6826                is_swap_layout_dirty: false,
6827                viewport_rows: 10,
6828                viewport_columns: 10,
6829                display_area_rows: 10,
6830                display_area_columns: 10,
6831                selectable_tiled_panes_count: 10,
6832                selectable_floating_panes_count: 10,
6833                tab_id: 1,
6834                is_flashing_bell: false,
6835                has_bell_notification: false,
6836            },
6837        ],
6838        panes: PaneManifest { panes },
6839        connected_clients: 2,
6840        is_current_session: false,
6841        available_layouts: vec![
6842            LayoutInfo::File("layout1".to_owned(), LayoutMetadata::default()),
6843            LayoutInfo::BuiltIn("layout2".to_owned()),
6844            LayoutInfo::File("layout3".to_owned(), LayoutMetadata::default()),
6845        ],
6846        plugins: Default::default(),
6847        web_client_count: 2,
6848        web_clients_allowed: true,
6849        tab_history: Default::default(),
6850        pane_history: Default::default(),
6851        creation_time: Duration::from_secs(300),
6852    };
6853    let serialized = session_info.to_string();
6854    let deserealized = SessionInfo::from_string(&serialized, "not this session").unwrap();
6855    assert_eq!(session_info, deserealized);
6856    insta::assert_snapshot!(serialized);
6857}
6858
6859#[test]
6860fn keybinds_to_string() {
6861    let fake_config = r#"
6862        keybinds {
6863            normal {
6864                bind "Ctrl g" { SwitchToMode "Locked"; }
6865            }
6866        }"#;
6867    let document: KdlDocument = fake_config.parse().unwrap();
6868    let deserialized = Keybinds::from_kdl(
6869        document.get("keybinds").unwrap(),
6870        Default::default(),
6871        &Default::default(),
6872    )
6873    .unwrap();
6874    let clear_defaults = true;
6875    let serialized = Keybinds::to_kdl(&deserialized, clear_defaults);
6876    let deserialized_from_serialized = Keybinds::from_kdl(
6877        serialized
6878            .to_string()
6879            .parse::<KdlDocument>()
6880            .unwrap()
6881            .get("keybinds")
6882            .unwrap(),
6883        Default::default(),
6884        &Default::default(),
6885    )
6886    .unwrap();
6887    insta::assert_snapshot!(serialized.to_string());
6888    assert_eq!(
6889        deserialized, deserialized_from_serialized,
6890        "Deserialized serialized config equals original config"
6891    );
6892}
6893
6894#[test]
6895fn keybinds_to_string_without_clearing_defaults() {
6896    let fake_config = r#"
6897        keybinds {
6898            normal {
6899                bind "Ctrl g" { SwitchToMode "Locked"; }
6900            }
6901        }"#;
6902    let document: KdlDocument = fake_config.parse().unwrap();
6903    let deserialized = Keybinds::from_kdl(
6904        document.get("keybinds").unwrap(),
6905        Default::default(),
6906        &Default::default(),
6907    )
6908    .unwrap();
6909    let clear_defaults = false;
6910    let serialized = Keybinds::to_kdl(&deserialized, clear_defaults);
6911    let deserialized_from_serialized = Keybinds::from_kdl(
6912        serialized
6913            .to_string()
6914            .parse::<KdlDocument>()
6915            .unwrap()
6916            .get("keybinds")
6917            .unwrap(),
6918        Default::default(),
6919        &Default::default(),
6920    )
6921    .unwrap();
6922    insta::assert_snapshot!(serialized.to_string());
6923    assert_eq!(
6924        deserialized, deserialized_from_serialized,
6925        "Deserialized serialized config equals original config"
6926    );
6927}
6928
6929#[test]
6930fn keybinds_to_string_with_multiple_actions() {
6931    let fake_config = r#"
6932        keybinds {
6933            normal {
6934                bind "Ctrl n" { NewPane; SwitchToMode "Locked"; }
6935            }
6936        }"#;
6937    let document: KdlDocument = fake_config.parse().unwrap();
6938    let deserialized = Keybinds::from_kdl(
6939        document.get("keybinds").unwrap(),
6940        Default::default(),
6941        &Default::default(),
6942    )
6943    .unwrap();
6944    let clear_defaults = true;
6945    let serialized = Keybinds::to_kdl(&deserialized, clear_defaults);
6946    let deserialized_from_serialized = Keybinds::from_kdl(
6947        serialized
6948            .to_string()
6949            .parse::<KdlDocument>()
6950            .unwrap()
6951            .get("keybinds")
6952            .unwrap(),
6953        Default::default(),
6954        &Default::default(),
6955    )
6956    .unwrap();
6957    assert_eq!(
6958        deserialized, deserialized_from_serialized,
6959        "Deserialized serialized config equals original config"
6960    );
6961    insta::assert_snapshot!(serialized.to_string());
6962}
6963
6964#[test]
6965fn can_bind_theme_actions() {
6966    // Regression test for https://github.com/zellij-org/zellij/issues/5297
6967    // SetDarkTheme / SetLightTheme / ToggleTheme work via the CLI but used to be
6968    // rejected by the keybinding parser with "Unsupported action".
6969    let fake_config = r#"
6970        keybinds {
6971            normal {
6972                bind "Ctrl t" { ToggleTheme; }
6973                bind "Ctrl d" { SetDarkTheme; }
6974                bind "Ctrl l" { SetLightTheme; }
6975            }
6976        }"#;
6977    let document: KdlDocument = fake_config.parse().unwrap();
6978    let deserialized = Keybinds::from_kdl(
6979        document.get("keybinds").unwrap(),
6980        Default::default(),
6981        &Default::default(),
6982    )
6983    .unwrap();
6984    let ctrl_t = KeyWithModifier::new(BareKey::Char('t')).with_ctrl_modifier();
6985    assert_eq!(
6986        deserialized.get_actions_for_key_in_mode(&InputMode::Normal, &ctrl_t),
6987        Some(&vec![Action::ToggleTheme])
6988    );
6989    let ctrl_d = KeyWithModifier::new(BareKey::Char('d')).with_ctrl_modifier();
6990    assert_eq!(
6991        deserialized.get_actions_for_key_in_mode(&InputMode::Normal, &ctrl_d),
6992        Some(&vec![Action::SetDarkTheme])
6993    );
6994    let ctrl_l = KeyWithModifier::new(BareKey::Char('l')).with_ctrl_modifier();
6995    assert_eq!(
6996        deserialized.get_actions_for_key_in_mode(&InputMode::Normal, &ctrl_l),
6997        Some(&vec![Action::SetLightTheme])
6998    );
6999    // The bindings must also survive a serialize -> deserialize round-trip.
7000    let serialized = Keybinds::to_kdl(&deserialized, true);
7001    let deserialized_from_serialized = Keybinds::from_kdl(
7002        serialized
7003            .to_string()
7004            .parse::<KdlDocument>()
7005            .unwrap()
7006            .get("keybinds")
7007            .unwrap(),
7008        Default::default(),
7009        &Default::default(),
7010    )
7011    .unwrap();
7012    assert_eq!(deserialized, deserialized_from_serialized);
7013}
7014
7015#[test]
7016fn keybinds_to_string_with_all_actions() {
7017    let fake_config = r#"
7018        keybinds {
7019            normal {
7020                bind "Ctrl a" { Quit; }
7021                bind "Ctrl b" { Write 102 111 111; }
7022                bind "Ctrl c" { WriteChars "hi there!"; }
7023                bind "Ctrl d" { SwitchToMode "Locked"; }
7024                bind "Ctrl e" { Resize "Increase"; }
7025                bind "Ctrl f" { FocusNextPane; }
7026                bind "Ctrl g" { FocusPreviousPane; }
7027                bind "Ctrl h" { SwitchFocus; }
7028                bind "Ctrl i" { MoveFocus "Right"; }
7029                bind "Ctrl j" { MoveFocusOrTab "Right"; }
7030                bind "Ctrl k" { MovePane "Right"; }
7031                bind "Ctrl l" { MovePaneBackwards; }
7032                bind "Ctrl m" { Resize "Decrease Down"; }
7033                bind "Ctrl n" { DumpScreen "/tmp/dumped"; }
7034                bind "Ctrl o" { DumpLayout "/tmp/dumped-layout"; }
7035                bind "Ctrl p" { EditScrollback; }
7036                bind "Ctrl q" { ScrollUp; }
7037                bind "Ctrl r" { ScrollDown; }
7038                bind "Ctrl s" { ScrollToBottom; }
7039                bind "Ctrl t" { ScrollToTop; }
7040                bind "Ctrl u" { PageScrollUp; }
7041                bind "Ctrl v" { PageScrollDown; }
7042                bind "Ctrl w" { HalfPageScrollUp; }
7043                bind "Ctrl x" { HalfPageScrollDown; }
7044                bind "Ctrl y" { ToggleFocusFullscreen; }
7045                bind "Ctrl z" { TogglePaneFrames; }
7046                bind "Alt a" { ToggleActiveSyncTab; }
7047                bind "Alt b" { NewPane "Right"; }
7048                bind "Alt c" { TogglePaneEmbedOrFloating; }
7049                bind "Alt d" { ToggleFloatingPanes; }
7050                bind "Alt e" { CloseFocus; }
7051                bind "Alt f" { PaneNameInput 0; }
7052                bind "Alt g" { UndoRenamePane; }
7053                bind "Alt h" { NewTab; }
7054                bind "Alt i" { GoToNextTab; }
7055                bind "Alt j" { GoToPreviousTab; }
7056                bind "Alt k" { CloseTab; }
7057                bind "Alt l" { GoToTab 1; }
7058                bind "Alt m" { ToggleTab; }
7059                bind "Alt n" { TabNameInput 0; }
7060                bind "Alt o" { UndoRenameTab; }
7061                bind "Alt p" { MoveTab "Right"; }
7062                bind "Alt q" {
7063                    Run "ls" "-l" {
7064                        hold_on_start true;
7065                        hold_on_close false;
7066                        cwd "/tmp";
7067                        name "my cool pane";
7068                    };
7069                }
7070                bind "Alt r" {
7071                    Run "ls" "-l" {
7072                        hold_on_start true;
7073                        hold_on_close false;
7074                        cwd "/tmp";
7075                        name "my cool pane";
7076                        floating true;
7077                    };
7078                }
7079                bind "Alt s" {
7080                    Run "ls" "-l" {
7081                        hold_on_start true;
7082                        hold_on_close false;
7083                        cwd "/tmp";
7084                        name "my cool pane";
7085                        in_place true;
7086                    };
7087                }
7088                bind "Alt t" { Detach; }
7089                bind "Alt u" {
7090                    LaunchOrFocusPlugin "zellij:session-manager"{
7091                        floating true;
7092                        move_to_focused_tab true;
7093                        skip_plugin_cache true;
7094                        config_key_1 "config_value_1";
7095                        config_key_2 "config_value_2";
7096                    };
7097                }
7098                bind "Alt v" {
7099                    LaunchOrFocusPlugin "zellij:session-manager"{
7100                        in_place true;
7101                        move_to_focused_tab true;
7102                        skip_plugin_cache true;
7103                        config_key_1 "config_value_1";
7104                        config_key_2 "config_value_2";
7105                    };
7106                }
7107                bind "Alt w" {
7108                    LaunchPlugin "zellij:session-manager" {
7109                        floating true;
7110                        skip_plugin_cache true;
7111                        config_key_1 "config_value_1";
7112                        config_key_2 "config_value_2";
7113                    };
7114                }
7115                bind "Alt x" {
7116                    LaunchPlugin "zellij:session-manager"{
7117                        in_place true;
7118                        skip_plugin_cache true;
7119                        config_key_1 "config_value_1";
7120                        config_key_2 "config_value_2";
7121                    };
7122                }
7123                bind "Alt y" { Copy; }
7124                bind "Alt z" { SearchInput 0; }
7125                bind "Ctrl Alt a" { Search "Up"; }
7126                bind "Ctrl Alt b" { SearchToggleOption "CaseSensitivity"; }
7127                bind "Ctrl Alt c" { ToggleMouseMode; }
7128                bind "Ctrl Alt d" { PreviousSwapLayout; }
7129                bind "Ctrl Alt e" { NextSwapLayout; }
7130                bind "Ctrl Alt g" { BreakPane; }
7131                bind "Ctrl Alt h" { BreakPaneRight; }
7132                bind "Ctrl Alt i" { BreakPaneLeft; }
7133                bind "Ctrl Alt i" { BreakPaneLeft; }
7134                bind "Ctrl Alt j" {
7135                    MessagePlugin "zellij:session-manager"{
7136                        name "message_name";
7137                        payload "message_payload";
7138                        cwd "/tmp";
7139                        launch_new true;
7140                        skip_cache true;
7141                        floating true;
7142                        title "plugin_title";
7143                        config_key_1 "config_value_1";
7144                        config_key_2 "config_value_2";
7145                    };
7146                }
7147                bind "Ctrl Alt k" { FocusLastPane; }
7148            }
7149        }"#;
7150    let document: KdlDocument = fake_config.parse().unwrap();
7151    let deserialized = Keybinds::from_kdl(
7152        document.get("keybinds").unwrap(),
7153        Default::default(),
7154        &Default::default(),
7155    )
7156    .unwrap();
7157    let clear_defaults = true;
7158    let serialized = Keybinds::to_kdl(&deserialized, clear_defaults);
7159    let deserialized_from_serialized = Keybinds::from_kdl(
7160        serialized
7161            .to_string()
7162            .parse::<KdlDocument>()
7163            .unwrap()
7164            .get("keybinds")
7165            .unwrap(),
7166        Default::default(),
7167        &Default::default(),
7168    )
7169    .unwrap();
7170    // uncomment the below lines for more easily debugging a failed assertion here
7171    //     for (input_mode, input_mode_keybinds) in deserialized.0 {
7172    //         if let Some(other_input_mode_keybinds) = deserialized_from_serialized.0.get(&input_mode) {
7173    //             for (keybind, action) in input_mode_keybinds {
7174    //                 if let Some(other_action) = other_input_mode_keybinds.get(&keybind) {
7175    //                     assert_eq!(&action, other_action);
7176    //                 } else {
7177    //                     eprintln!("keybind: {:?} not found in other", keybind);
7178    //                 }
7179    //             }
7180    //         }
7181    //     }
7182    assert_eq!(
7183        deserialized, deserialized_from_serialized,
7184        "Deserialized serialized config equals original config"
7185    );
7186    insta::assert_snapshot!(serialized.to_string());
7187}
7188
7189#[test]
7190fn keybinds_to_string_with_shared_modes() {
7191    let fake_config = r#"
7192        keybinds {
7193            normal {
7194                bind "Ctrl n" { NewPane; SwitchToMode "Locked"; }
7195            }
7196            locked {
7197                bind "Ctrl n" { NewPane; SwitchToMode "Locked"; }
7198            }
7199            shared_except "locked" "pane" {
7200                bind "Ctrl f" { TogglePaneEmbedOrFloating; }
7201            }
7202            shared_among "locked" "pane" {
7203                bind "Ctrl p" { WriteChars "foo"; }
7204            }
7205        }"#;
7206    let document: KdlDocument = fake_config.parse().unwrap();
7207    let deserialized = Keybinds::from_kdl(
7208        document.get("keybinds").unwrap(),
7209        Default::default(),
7210        &Default::default(),
7211    )
7212    .unwrap();
7213    let clear_defaults = true;
7214    let serialized = Keybinds::to_kdl(&deserialized, clear_defaults);
7215    let deserialized_from_serialized = Keybinds::from_kdl(
7216        serialized
7217            .to_string()
7218            .parse::<KdlDocument>()
7219            .unwrap()
7220            .get("keybinds")
7221            .unwrap(),
7222        Default::default(),
7223        &Default::default(),
7224    )
7225    .unwrap();
7226    assert_eq!(
7227        deserialized, deserialized_from_serialized,
7228        "Deserialized serialized config equals original config"
7229    );
7230    insta::assert_snapshot!(serialized.to_string());
7231}
7232
7233#[test]
7234fn keybinds_to_string_with_multiple_multiline_actions() {
7235    let fake_config = r#"
7236        keybinds {
7237            shared {
7238                bind "Ctrl n" {
7239                    NewPane
7240                    SwitchToMode "Locked"
7241                    MessagePlugin "zellij:session-manager"{
7242                        name "message_name";
7243                        payload "message_payload";
7244                        cwd "/tmp";
7245                        launch_new true;
7246                        skip_cache true;
7247                        floating true;
7248                        title "plugin_title";
7249                        config_key_1 "config_value_1";
7250                        config_key_2 "config_value_2";
7251                    };
7252                }
7253            }
7254        }"#;
7255    let document: KdlDocument = fake_config.parse().unwrap();
7256    let deserialized = Keybinds::from_kdl(
7257        document.get("keybinds").unwrap(),
7258        Default::default(),
7259        &Default::default(),
7260    )
7261    .unwrap();
7262    let clear_defaults = true;
7263    let serialized = Keybinds::to_kdl(&deserialized, clear_defaults);
7264    let deserialized_from_serialized = Keybinds::from_kdl(
7265        serialized
7266            .to_string()
7267            .parse::<KdlDocument>()
7268            .unwrap()
7269            .get("keybinds")
7270            .unwrap(),
7271        Default::default(),
7272        &Default::default(),
7273    )
7274    .unwrap();
7275    assert_eq!(
7276        deserialized, deserialized_from_serialized,
7277        "Deserialized serialized config equals original config"
7278    );
7279    insta::assert_snapshot!(serialized.to_string());
7280}
7281
7282#[test]
7283fn themes_to_string() {
7284    let fake_config = r#"
7285        themes {
7286           dracula {
7287                fg 248 248 242
7288                bg 40 42 54
7289                black 0 0 0
7290                red 255 85 85
7291                green 80 250 123
7292                yellow 241 250 140
7293                blue 98 114 164
7294                magenta 255 121 198
7295                cyan 139 233 253
7296                white 255 255 255
7297                orange 255 184 108
7298            }
7299        }"#;
7300    let document: KdlDocument = fake_config.parse().unwrap();
7301    let sourced_from_external_file = false;
7302    let deserialized =
7303        Themes::from_kdl(document.get("themes").unwrap(), sourced_from_external_file).unwrap();
7304    let serialized = Themes::to_kdl(&deserialized).unwrap();
7305    let deserialized_from_serialized = Themes::from_kdl(
7306        serialized
7307            .to_string()
7308            .parse::<KdlDocument>()
7309            .unwrap()
7310            .get("themes")
7311            .unwrap(),
7312        sourced_from_external_file,
7313    )
7314    .unwrap();
7315    assert_eq!(
7316        deserialized, deserialized_from_serialized,
7317        "Deserialized serialized config equals original config",
7318    );
7319    insta::assert_snapshot!(serialized.to_string());
7320}
7321
7322#[test]
7323fn themes_to_string_with_hex_definitions() {
7324    let fake_config = r##"
7325        themes {
7326            nord {
7327                fg "#D8DEE9"
7328                bg "#2E3440"
7329                black "#3B4252"
7330                red "#BF616A"
7331                green "#A3BE8C"
7332                yellow "#EBCB8B"
7333                blue "#81A1C1"
7334                magenta "#B48EAD"
7335                cyan "#88C0D0"
7336                white "#E5E9F0"
7337                orange "#D08770"
7338            }
7339        }"##;
7340    let document: KdlDocument = fake_config.parse().unwrap();
7341    let sourced_from_external_file = false;
7342    let deserialized =
7343        Themes::from_kdl(document.get("themes").unwrap(), sourced_from_external_file).unwrap();
7344    let serialized = Themes::to_kdl(&deserialized).unwrap();
7345    let deserialized_from_serialized = Themes::from_kdl(
7346        serialized
7347            .to_string()
7348            .parse::<KdlDocument>()
7349            .unwrap()
7350            .get("themes")
7351            .unwrap(),
7352        sourced_from_external_file,
7353    )
7354    .unwrap();
7355    assert_eq!(
7356        deserialized, deserialized_from_serialized,
7357        "Deserialized serialized config equals original config"
7358    );
7359    insta::assert_snapshot!(serialized.to_string());
7360}
7361
7362#[test]
7363fn themes_to_string_with_eight_bit_definitions() {
7364    let fake_config = r##"
7365        themes {
7366            default {
7367                fg 1
7368                bg 10
7369                black 20
7370                red 30
7371                green 40
7372                yellow 50
7373                blue 60
7374                magenta 70
7375                cyan 80
7376                white 90
7377                orange 254
7378            }
7379        }"##;
7380    let document: KdlDocument = fake_config.parse().unwrap();
7381    let sourced_from_external_file = false;
7382    let deserialized =
7383        Themes::from_kdl(document.get("themes").unwrap(), sourced_from_external_file).unwrap();
7384    let serialized = Themes::to_kdl(&deserialized).unwrap();
7385    let deserialized_from_serialized = Themes::from_kdl(
7386        serialized
7387            .to_string()
7388            .parse::<KdlDocument>()
7389            .unwrap()
7390            .get("themes")
7391            .unwrap(),
7392        sourced_from_external_file,
7393    )
7394    .unwrap();
7395    assert_eq!(
7396        deserialized, deserialized_from_serialized,
7397        "Deserialized serialized config equals original config"
7398    );
7399    insta::assert_snapshot!(serialized.to_string());
7400}
7401
7402#[test]
7403fn themes_to_string_with_combined_definitions() {
7404    let fake_config = r##"
7405        themes {
7406            default {
7407                fg 1
7408                bg 10
7409                black 20
7410                red 30
7411                green 40
7412                yellow 50
7413                blue 60
7414                magenta 70
7415                cyan 80
7416                white 255 255 255
7417                orange "#D08770"
7418            }
7419        }"##;
7420    let document: KdlDocument = fake_config.parse().unwrap();
7421    let sourced_from_external_file = false;
7422    let deserialized =
7423        Themes::from_kdl(document.get("themes").unwrap(), sourced_from_external_file).unwrap();
7424    let serialized = Themes::to_kdl(&deserialized).unwrap();
7425    let deserialized_from_serialized = Themes::from_kdl(
7426        serialized
7427            .to_string()
7428            .parse::<KdlDocument>()
7429            .unwrap()
7430            .get("themes")
7431            .unwrap(),
7432        sourced_from_external_file,
7433    )
7434    .unwrap();
7435    assert_eq!(
7436        deserialized, deserialized_from_serialized,
7437        "Deserialized serialized config equals original config"
7438    );
7439    insta::assert_snapshot!(serialized.to_string());
7440}
7441
7442#[test]
7443fn themes_to_string_with_multiple_theme_definitions() {
7444    let fake_config = r##"
7445        themes {
7446           nord {
7447               fg "#D8DEE9"
7448               bg "#2E3440"
7449               black "#3B4252"
7450               red "#BF616A"
7451               green "#A3BE8C"
7452               yellow "#EBCB8B"
7453               blue "#81A1C1"
7454               magenta "#B48EAD"
7455               cyan "#88C0D0"
7456               white "#E5E9F0"
7457               orange "#D08770"
7458           }
7459           dracula {
7460                fg 248 248 242
7461                bg 40 42 54
7462                black 0 0 0
7463                red 255 85 85
7464                green 80 250 123
7465                yellow 241 250 140
7466                blue 98 114 164
7467                magenta 255 121 198
7468                cyan 139 233 253
7469                white 255 255 255
7470                orange 255 184 108
7471            }
7472        }"##;
7473    let document: KdlDocument = fake_config.parse().unwrap();
7474    let sourced_from_external_file = false;
7475    let deserialized =
7476        Themes::from_kdl(document.get("themes").unwrap(), sourced_from_external_file).unwrap();
7477    let serialized = Themes::to_kdl(&deserialized).unwrap();
7478    let deserialized_from_serialized = Themes::from_kdl(
7479        serialized
7480            .to_string()
7481            .parse::<KdlDocument>()
7482            .unwrap()
7483            .get("themes")
7484            .unwrap(),
7485        sourced_from_external_file,
7486    )
7487    .unwrap();
7488    assert_eq!(
7489        deserialized, deserialized_from_serialized,
7490        "Deserialized serialized config equals original config"
7491    );
7492    insta::assert_snapshot!(serialized.to_string());
7493}
7494
7495#[test]
7496fn plugins_to_string() {
7497    let fake_config = r##"
7498        plugins {
7499            tab-bar location="zellij:tab-bar"
7500            status-bar location="zellij:status-bar"
7501            strider location="zellij:strider"
7502            compact-bar location="zellij:compact-bar"
7503            session-manager location="zellij:session-manager"
7504            welcome-screen location="zellij:session-manager" {
7505                welcome_screen true
7506            }
7507            filepicker location="zellij:strider" {
7508                cwd "/"
7509            }
7510        }"##;
7511    let document: KdlDocument = fake_config.parse().unwrap();
7512    let deserialized = PluginAliases::from_kdl(document.get("plugins").unwrap()).unwrap();
7513    let serialized = PluginAliases::to_kdl(&deserialized, true);
7514    let deserialized_from_serialized = PluginAliases::from_kdl(
7515        serialized
7516            .to_string()
7517            .parse::<KdlDocument>()
7518            .unwrap()
7519            .get("plugins")
7520            .unwrap(),
7521    )
7522    .unwrap();
7523    assert_eq!(
7524        deserialized, deserialized_from_serialized,
7525        "Deserialized serialized config equals original config"
7526    );
7527    insta::assert_snapshot!(serialized.to_string());
7528}
7529
7530#[test]
7531fn plugins_to_string_with_file_and_web() {
7532    let fake_config = r##"
7533        plugins {
7534            tab-bar location="https://foo.com/plugin.wasm"
7535            filepicker location="file:/path/to/my/plugin.wasm" {
7536                cwd "/"
7537            }
7538        }"##;
7539    let document: KdlDocument = fake_config.parse().unwrap();
7540    let deserialized = PluginAliases::from_kdl(document.get("plugins").unwrap()).unwrap();
7541    let serialized = PluginAliases::to_kdl(&deserialized, true);
7542    let deserialized_from_serialized = PluginAliases::from_kdl(
7543        serialized
7544            .to_string()
7545            .parse::<KdlDocument>()
7546            .unwrap()
7547            .get("plugins")
7548            .unwrap(),
7549    )
7550    .unwrap();
7551    assert_eq!(
7552        deserialized, deserialized_from_serialized,
7553        "Deserialized serialized config equals original config"
7554    );
7555    insta::assert_snapshot!(serialized.to_string());
7556}
7557
7558#[test]
7559fn ui_config_to_string() {
7560    let fake_config = r##"
7561        ui {
7562            pane_frames {
7563                rounded_corners true
7564                hide_session_name true
7565            }
7566        }"##;
7567    let document: KdlDocument = fake_config.parse().unwrap();
7568    let deserialized = UiConfig::from_kdl(document.get("ui").unwrap()).unwrap();
7569    let serialized = UiConfig::to_kdl(&deserialized).unwrap();
7570    let deserialized_from_serialized = UiConfig::from_kdl(
7571        serialized
7572            .to_string()
7573            .parse::<KdlDocument>()
7574            .unwrap()
7575            .get("ui")
7576            .unwrap(),
7577    )
7578    .unwrap();
7579    assert_eq!(
7580        deserialized, deserialized_from_serialized,
7581        "Deserialized serialized config equals original config"
7582    );
7583    insta::assert_snapshot!(serialized.to_string());
7584}
7585
7586#[test]
7587fn ui_config_to_string_with_no_ui_config() {
7588    let fake_config = r##"
7589        ui {
7590            pane_frames {
7591            }
7592        }"##;
7593    let document: KdlDocument = fake_config.parse().unwrap();
7594    let deserialized = UiConfig::from_kdl(document.get("ui").unwrap()).unwrap();
7595    assert_eq!(UiConfig::to_kdl(&deserialized), None);
7596}
7597
7598#[test]
7599fn env_vars_to_string() {
7600    let fake_config = r##"
7601        env {
7602            foo "bar"
7603            bar "foo"
7604            thing 1
7605            baz "true"
7606        }"##;
7607    let document: KdlDocument = fake_config.parse().unwrap();
7608    let deserialized = EnvironmentVariables::from_kdl(document.get("env").unwrap()).unwrap();
7609    let serialized = EnvironmentVariables::to_kdl(&deserialized).unwrap();
7610    let deserialized_from_serialized = EnvironmentVariables::from_kdl(
7611        serialized
7612            .to_string()
7613            .parse::<KdlDocument>()
7614            .unwrap()
7615            .get("env")
7616            .unwrap(),
7617    )
7618    .unwrap();
7619    assert_eq!(
7620        deserialized, deserialized_from_serialized,
7621        "Deserialized serialized config equals original config"
7622    );
7623    insta::assert_snapshot!(serialized.to_string());
7624}
7625
7626#[test]
7627fn env_vars_to_string_with_no_env_vars() {
7628    let fake_config = r##"
7629        env {
7630        }"##;
7631    let document: KdlDocument = fake_config.parse().unwrap();
7632    let deserialized = EnvironmentVariables::from_kdl(document.get("env").unwrap()).unwrap();
7633    assert_eq!(EnvironmentVariables::to_kdl(&deserialized), None);
7634}
7635
7636#[test]
7637fn selection_options_from_kdl() {
7638    let fake_config = r##"
7639        osc133_command_selection false
7640        word_separators "[]{}<>():,"
7641    "##;
7642    let document: KdlDocument = fake_config.parse().unwrap();
7643    let deserialized = Options::from_kdl(&document).unwrap();
7644    assert_eq!(deserialized.osc133_command_selection, Some(false));
7645    assert_eq!(
7646        deserialized.word_separators,
7647        Some("[]{}<>():,".to_owned()),
7648        "word separators are parsed verbatim"
7649    );
7650}
7651
7652#[test]
7653fn selection_options_default_to_none_when_unspecified() {
7654    let document: KdlDocument = "".parse().unwrap();
7655    let deserialized = Options::from_kdl(&document).unwrap();
7656    assert_eq!(deserialized.osc133_command_selection, None);
7657    assert_eq!(deserialized.word_separators, None);
7658}
7659
7660#[test]
7661fn config_options_to_string() {
7662    let fake_config = r##"
7663        simplified_ui true
7664        theme "dracula"
7665        default_mode "locked"
7666        default_shell "fish"
7667        default_cwd "/tmp/foo"
7668        default_layout "compact"
7669        layout_dir "/tmp/layouts"
7670        theme_dir "/tmp/themes"
7671        mouse_mode false
7672        pane_frames false
7673        mirror_session true
7674        on_force_close "quit"
7675        scroll_buffer_size 100
7676        copy_command "pbcopy"
7677        copy_clipboard "system"
7678        copy_on_select false
7679        scrollback_editor "vim"
7680        session_name "my_cool_session"
7681        attach_to_session false
7682        auto_layout false
7683        session_serialization true
7684        serialize_pane_viewport false
7685        scrollback_lines_to_serialize 1000
7686        styled_underlines false
7687        serialization_interval 1
7688        disable_session_metadata true
7689        support_kitty_keyboard_protocol false
7690        web_server true
7691        web_sharing "disabled"
7692    "##;
7693    let document: KdlDocument = fake_config.parse().unwrap();
7694    let deserialized = Options::from_kdl(&document).unwrap();
7695    let mut serialized = Options::to_kdl(&deserialized, false);
7696    let mut fake_document = KdlDocument::new();
7697    fake_document.nodes_mut().append(&mut serialized);
7698    let deserialized_from_serialized =
7699        Options::from_kdl(&fake_document.to_string().parse::<KdlDocument>().unwrap()).unwrap();
7700    assert_eq!(
7701        deserialized, deserialized_from_serialized,
7702        "Deserialized serialized config equals original config"
7703    );
7704    insta::assert_snapshot!(fake_document.to_string());
7705}
7706
7707#[test]
7708fn config_options_to_string_with_comments() {
7709    let fake_config = r##"
7710        simplified_ui true
7711        theme "dracula"
7712        default_mode "locked"
7713        default_shell "fish"
7714        default_cwd "/tmp/foo"
7715        default_layout "compact"
7716        layout_dir "/tmp/layouts"
7717        theme_dir "/tmp/themes"
7718        mouse_mode false
7719        pane_frames false
7720        mirror_session true
7721        on_force_close "quit"
7722        scroll_buffer_size 100
7723        copy_command "pbcopy"
7724        copy_clipboard "system"
7725        copy_on_select false
7726        scrollback_editor "vim"
7727        session_name "my_cool_session"
7728        attach_to_session false
7729        auto_layout false
7730        session_serialization true
7731        serialize_pane_viewport false
7732        scrollback_lines_to_serialize 1000
7733        styled_underlines false
7734        serialization_interval 1
7735        disable_session_metadata true
7736        support_kitty_keyboard_protocol false
7737        web_server true
7738        web_sharing "disabled"
7739    "##;
7740    let document: KdlDocument = fake_config.parse().unwrap();
7741    let deserialized = Options::from_kdl(&document).unwrap();
7742    let mut serialized = Options::to_kdl(&deserialized, true);
7743    let mut fake_document = KdlDocument::new();
7744    fake_document.nodes_mut().append(&mut serialized);
7745    let deserialized_from_serialized =
7746        Options::from_kdl(&fake_document.to_string().parse::<KdlDocument>().unwrap()).unwrap();
7747    assert_eq!(
7748        deserialized, deserialized_from_serialized,
7749        "Deserialized serialized config equals original config"
7750    );
7751    insta::assert_snapshot!(fake_document.to_string());
7752}
7753
7754#[test]
7755fn config_options_to_string_without_options() {
7756    let fake_config = r##"
7757    "##;
7758    let document: KdlDocument = fake_config.parse().unwrap();
7759    let deserialized = Options::from_kdl(&document).unwrap();
7760    let mut serialized = Options::to_kdl(&deserialized, false);
7761    let mut fake_document = KdlDocument::new();
7762    fake_document.nodes_mut().append(&mut serialized);
7763    let deserialized_from_serialized =
7764        Options::from_kdl(&fake_document.to_string().parse::<KdlDocument>().unwrap()).unwrap();
7765    assert_eq!(
7766        deserialized, deserialized_from_serialized,
7767        "Deserialized serialized config equals original config"
7768    );
7769    insta::assert_snapshot!(fake_document.to_string());
7770}
7771
7772#[test]
7773fn nested_session_handling_kdl_round_trip_for_every_variant() {
7774    use crate::input::options::NestedSessionHandling;
7775    let cases = [
7776        ("ask", NestedSessionHandling::Ask),
7777        ("fullscreen", NestedSessionHandling::Fullscreen),
7778        ("descend", NestedSessionHandling::Descend),
7779        ("never", NestedSessionHandling::Never),
7780    ];
7781    for (value, expected) in cases {
7782        let fake_config = format!(
7783            r##"
7784                nested_session_handling "{value}"
7785            "##
7786        );
7787        let document: KdlDocument = fake_config.parse().unwrap();
7788        let parsed = Options::from_kdl(&document).unwrap();
7789        assert_eq!(
7790            parsed.nested_session_handling,
7791            Some(expected),
7792            "case: {value}"
7793        );
7794
7795        let mut serialized = Options::to_kdl(&parsed, false);
7796        let mut fake_document = KdlDocument::new();
7797        fake_document.nodes_mut().append(&mut serialized);
7798        let reparsed =
7799            Options::from_kdl(&fake_document.to_string().parse::<KdlDocument>().unwrap()).unwrap();
7800        assert_eq!(parsed, reparsed, "round-trip mismatch for {value}");
7801    }
7802}
7803
7804#[test]
7805fn host_notification_protocol_kdl_round_trip_for_every_variant() {
7806    use crate::input::options::HostNotificationProtocol;
7807    let cases = [
7808        ("auto", HostNotificationProtocol::Auto),
7809        ("osc9", HostNotificationProtocol::Osc9),
7810        ("osc99", HostNotificationProtocol::Osc99),
7811        ("bell", HostNotificationProtocol::Bell),
7812        ("off", HostNotificationProtocol::Off),
7813    ];
7814    for (value, expected) in cases {
7815        let fake_config = format!(
7816            r##"
7817                host_notification_protocol "{value}"
7818            "##
7819        );
7820        let document: KdlDocument = fake_config.parse().unwrap();
7821        let parsed = Options::from_kdl(&document).unwrap();
7822        assert_eq!(
7823            parsed.host_notification_protocol,
7824            Some(expected),
7825            "case: {value}"
7826        );
7827
7828        let mut serialized = Options::to_kdl(&parsed, false);
7829        let mut fake_document = KdlDocument::new();
7830        fake_document.nodes_mut().append(&mut serialized);
7831        let reparsed =
7832            Options::from_kdl(&fake_document.to_string().parse::<KdlDocument>().unwrap()).unwrap();
7833        assert_eq!(parsed, reparsed, "round-trip mismatch for {value}");
7834    }
7835}
7836
7837#[test]
7838fn an_unknown_host_notification_protocol_is_a_config_error() {
7839    let fake_config = r##"
7840        host_notification_protocol "carrier-pigeon"
7841    "##;
7842    let document: KdlDocument = fake_config.parse().unwrap();
7843    assert!(Options::from_kdl(&document).is_err());
7844}
7845
7846#[test]
7847fn an_unset_host_notification_protocol_parses_as_none() {
7848    let document: KdlDocument = r##"
7849        simplified_ui true
7850    "##
7851    .parse()
7852    .unwrap();
7853    let parsed = Options::from_kdl(&document).unwrap();
7854    assert_eq!(parsed.host_notification_protocol, None);
7855}
7856
7857#[test]
7858fn config_options_to_string_with_some_options() {
7859    let fake_config = r##"
7860        default_layout "compact"
7861    "##;
7862    let document: KdlDocument = fake_config.parse().unwrap();
7863    let deserialized = Options::from_kdl(&document).unwrap();
7864    let mut serialized = Options::to_kdl(&deserialized, false);
7865    let mut fake_document = KdlDocument::new();
7866    fake_document.nodes_mut().append(&mut serialized);
7867    let deserialized_from_serialized =
7868        Options::from_kdl(&fake_document.to_string().parse::<KdlDocument>().unwrap()).unwrap();
7869    assert_eq!(
7870        deserialized, deserialized_from_serialized,
7871        "Deserialized serialized config equals original config"
7872    );
7873    insta::assert_snapshot!(fake_document.to_string());
7874}
7875
7876#[test]
7877fn bare_config_from_default_assets_to_string() {
7878    let fake_config = Config::from_default_assets().unwrap();
7879    let fake_config_stringified = fake_config.to_string(false);
7880    let deserialized_from_serialized = Config::from_kdl(&fake_config_stringified, None).unwrap();
7881    assert_eq!(
7882        fake_config, deserialized_from_serialized,
7883        "Deserialized serialized config equals original config"
7884    );
7885    insta::assert_snapshot!(fake_config_stringified);
7886}
7887
7888#[test]
7889fn bare_config_from_default_assets_to_string_with_comments() {
7890    let fake_config = Config::from_default_assets().unwrap();
7891    let fake_config_stringified = fake_config.to_string(true);
7892    let deserialized_from_serialized = Config::from_kdl(&fake_config_stringified, None).unwrap();
7893    assert_eq!(
7894        fake_config, deserialized_from_serialized,
7895        "Deserialized serialized config equals original config"
7896    );
7897    insta::assert_snapshot!(fake_config_stringified);
7898}
7899
7900#[test]
7901fn osc8_hyperlinks_config_parsing() {
7902    let config_with_osc8_disabled = r#"
7903        osc8_hyperlinks false
7904    "#;
7905    let config = Config::from_kdl(config_with_osc8_disabled, None).unwrap();
7906    assert_eq!(config.options.osc8_hyperlinks, Some(false));
7907
7908    let config_with_osc8_enabled = r#"
7909        osc8_hyperlinks true
7910    "#;
7911    let config = Config::from_kdl(config_with_osc8_enabled, None).unwrap();
7912    assert_eq!(config.options.osc8_hyperlinks, Some(true));
7913
7914    // Test serialization roundtrip
7915    let serialized = config.to_string(false);
7916    let deserialized = Config::from_kdl(&serialized, None).unwrap();
7917    assert_eq!(deserialized.options.osc8_hyperlinks, Some(true));
7918}