Skip to main content

zellij_utils/
session_serialization.rs

1use kdl::{KdlDocument, KdlEntry, KdlNode, KdlValue};
2use std::collections::{BTreeMap, HashMap, HashSet};
3use std::path::PathBuf;
4
5use crate::{
6    input::layout::PluginUserConfiguration,
7    input::layout::{
8        FloatingPaneLayout, Layout, LayoutConstraint, PercentOrFixed, Run, RunPluginOrAlias,
9        SplitDirection, SplitSize, SwapFloatingLayout, SwapTiledLayout, TiledPaneLayout,
10    },
11    pane_size::{Constraint, PaneGeom},
12};
13
14#[derive(Default, Debug, Clone)]
15pub struct GlobalLayoutManifest {
16    pub global_cwd: Option<PathBuf>,
17    pub default_shell: Option<PathBuf>,
18    pub default_layout: Box<Layout>,
19    pub tabs: Vec<(String, TabLayoutManifest)>,
20}
21
22#[derive(Default, Debug, Clone)]
23pub struct TabLayoutManifest {
24    pub tiled_panes: Vec<PaneLayoutManifest>,
25    pub floating_panes: Vec<PaneLayoutManifest>,
26    pub is_focused: bool,
27    pub hide_floating_panes: bool,
28}
29
30#[derive(Default, Debug, Clone)]
31pub struct PaneLayoutManifest {
32    pub geom: PaneGeom,
33    pub run: Option<Run>,
34    pub cwd: Option<PathBuf>,
35    pub is_borderless: bool,
36    pub title: Option<String>,
37    pub is_focused: bool,
38    pub pane_contents: Option<String>,
39    pub default_fg: Option<String>,
40    pub default_bg: Option<String>,
41}
42
43pub fn serialize_session_layout(
44    global_layout_manifest: GlobalLayoutManifest,
45) -> Result<(String, BTreeMap<String, String>), &'static str> {
46    // BTreeMap is the pane contents and their file names
47    let mut document = KdlDocument::new();
48    let mut pane_contents = BTreeMap::new();
49    let mut layout_node = KdlNode::new("layout");
50    let mut layout_node_children = KdlDocument::new();
51    if let Some(global_cwd) = serialize_global_cwd(&global_layout_manifest.global_cwd) {
52        layout_node_children.nodes_mut().push(global_cwd);
53    }
54    match serialize_multiple_tabs(global_layout_manifest.tabs, &mut pane_contents) {
55        Ok(mut serialized_tabs) => {
56            layout_node_children
57                .nodes_mut()
58                .append(&mut serialized_tabs);
59        },
60        Err(e) => {
61            return Err(e);
62        },
63    }
64    serialize_new_tab_template(
65        global_layout_manifest.default_layout.template,
66        &mut pane_contents,
67        &mut layout_node_children,
68    );
69    serialize_swap_tiled_layouts(
70        global_layout_manifest.default_layout.swap_tiled_layouts,
71        &mut pane_contents,
72        &mut layout_node_children,
73    );
74    serialize_swap_floating_layouts(
75        global_layout_manifest.default_layout.swap_floating_layouts,
76        &mut pane_contents,
77        &mut layout_node_children,
78    );
79
80    layout_node.set_children(layout_node_children);
81    document.nodes_mut().push(layout_node);
82    Ok((document.to_string(), pane_contents))
83}
84
85fn serialize_tab(
86    tab_name: String,
87    is_focused: bool,
88    hide_floating_panes: bool,
89    tiled_panes: &Vec<PaneLayoutManifest>,
90    floating_panes: &Vec<PaneLayoutManifest>,
91    pane_contents: &mut BTreeMap<String, String>,
92) -> Option<KdlNode> {
93    let mut serialized_tab = KdlNode::new("tab");
94    let mut serialized_tab_children = KdlDocument::new();
95    match get_tiled_panes_layout_from_panegeoms(tiled_panes, None) {
96        Some(tiled_panes_layout) => {
97            let floating_panes_layout = get_floating_panes_layout_from_panegeoms(floating_panes);
98            let tiled_panes = tiled_panes_to_serialize(tiled_panes_layout);
99            serialized_tab
100                .entries_mut()
101                .push(KdlEntry::new_prop("name", tab_name));
102            if is_focused {
103                serialized_tab
104                    .entries_mut()
105                    .push(KdlEntry::new_prop("focus", KdlValue::Bool(true)));
106            }
107            if hide_floating_panes {
108                serialized_tab.entries_mut().push(KdlEntry::new_prop(
109                    "hide_floating_panes",
110                    KdlValue::Bool(true),
111                ));
112            }
113
114            serialize_tiled_and_floating_panes(
115                &tiled_panes,
116                floating_panes_layout,
117                pane_contents,
118                &mut serialized_tab_children,
119            );
120
121            serialized_tab.set_children(serialized_tab_children);
122            Some(serialized_tab)
123        },
124        None => {
125            return None;
126        },
127    }
128}
129
130fn tiled_panes_to_serialize(root: TiledPaneLayout) -> Vec<TiledPaneLayout> {
131    let root_is_leaf = root.children.is_empty() && root.external_children_index.is_none();
132    if root_is_leaf {
133        if root == TiledPaneLayout::default() {
134            vec![]
135        } else {
136            vec![root]
137        }
138    } else if &root.children_split_direction != &SplitDirection::default()
139        || root.children_are_stacked
140    {
141        vec![root]
142    } else {
143        root.children
144    }
145}
146
147fn serialize_tiled_and_floating_panes(
148    tiled_panes: &Vec<TiledPaneLayout>,
149    floating_panes_layout: Vec<FloatingPaneLayout>,
150    pane_contents: &mut BTreeMap<String, String>,
151    serialized_tab_children: &mut KdlDocument,
152) {
153    for tiled_pane_layout in tiled_panes {
154        let ignore_size = false;
155        let tiled_pane_node = serialize_tiled_pane(tiled_pane_layout, ignore_size, pane_contents);
156        serialized_tab_children.nodes_mut().push(tiled_pane_node);
157    }
158    if !floating_panes_layout.is_empty() {
159        let mut floating_panes_node = KdlNode::new("floating_panes");
160        let mut floating_panes_node_children = KdlDocument::new();
161        for floating_pane in floating_panes_layout {
162            let pane_node = serialize_floating_pane(&floating_pane, pane_contents);
163            floating_panes_node_children.nodes_mut().push(pane_node);
164        }
165        floating_panes_node.set_children(floating_panes_node_children);
166        serialized_tab_children
167            .nodes_mut()
168            .push(floating_panes_node);
169    }
170}
171
172fn serialize_tiled_pane(
173    layout: &TiledPaneLayout,
174    ignore_size: bool,
175    pane_contents: &mut BTreeMap<String, String>,
176) -> KdlNode {
177    let (command, args) = extract_command_and_args(&layout.run);
178    let (plugin, plugin_config) = extract_plugin_and_config(&layout.run);
179    let (edit, _line_number) = extract_edit_and_line_number(&layout.run);
180    let cwd = layout.run.as_ref().and_then(|r| r.get_cwd());
181    let has_children = layout.external_children_index.is_some() || !layout.children.is_empty();
182
183    let mut tiled_pane_node = KdlNode::new("pane");
184    serialize_pane_title_and_attributes(
185        &command,
186        &edit,
187        &layout.name,
188        cwd,
189        layout.focus,
190        &layout.pane_initial_contents,
191        pane_contents,
192        has_children,
193        &mut tiled_pane_node,
194    );
195
196    serialize_tiled_layout_attributes(&layout, ignore_size, &mut tiled_pane_node);
197    if let Some(ref fg) = layout.default_fg {
198        tiled_pane_node
199            .entries_mut()
200            .push(KdlEntry::new_prop("default_fg", fg.to_owned()));
201    }
202    if let Some(ref bg) = layout.default_bg {
203        tiled_pane_node
204            .entries_mut()
205            .push(KdlEntry::new_prop("default_bg", bg.to_owned()));
206    }
207    let has_child_attributes = !layout.children.is_empty()
208        || layout.external_children_index.is_some()
209        || !args.is_empty()
210        || plugin.is_some()
211        || command.is_some();
212    if has_child_attributes {
213        let mut tiled_pane_node_children = KdlDocument::new();
214        serialize_args(args, &mut tiled_pane_node_children);
215        serialize_start_suspended(&command, &mut tiled_pane_node_children);
216        serialize_plugin(plugin, plugin_config, &mut tiled_pane_node_children);
217        if layout.children.is_empty() && layout.external_children_index.is_some() {
218            tiled_pane_node_children
219                .nodes_mut()
220                .push(KdlNode::new("children"));
221        }
222        for (i, pane) in layout.children.iter().enumerate() {
223            if Some(i) == layout.external_children_index {
224                tiled_pane_node_children
225                    .nodes_mut()
226                    .push(KdlNode::new("children"));
227            } else {
228                let ignore_size = layout.children_are_stacked;
229                let child_pane_node = serialize_tiled_pane(&pane, ignore_size, pane_contents);
230                tiled_pane_node_children.nodes_mut().push(child_pane_node);
231            }
232        }
233        tiled_pane_node.set_children(tiled_pane_node_children);
234    }
235    tiled_pane_node
236}
237
238pub fn extract_command_and_args(layout_run: &Option<Run>) -> (Option<String>, Vec<String>) {
239    match layout_run {
240        Some(Run::Command(run_command)) => (
241            Some(run_command.command.display().to_string()),
242            run_command.args.clone(),
243        ),
244        _ => (None, vec![]),
245    }
246}
247pub fn extract_plugin_and_config(
248    layout_run: &Option<Run>,
249) -> (Option<String>, Option<PluginUserConfiguration>) {
250    match &layout_run {
251        Some(Run::Plugin(run_plugin_or_alias)) => match run_plugin_or_alias {
252            RunPluginOrAlias::RunPlugin(run_plugin) => (
253                Some(run_plugin.location.display()),
254                Some(run_plugin.configuration.clone()),
255            ),
256            RunPluginOrAlias::Alias(plugin_alias) => {
257                // in this case, the aliases should already be populated by the RunPlugins they
258                // translate to - if they are not, the alias either does not exist or this is some
259                // sort of bug
260                let name = plugin_alias
261                    .run_plugin
262                    .as_ref()
263                    .map(|run_plugin| run_plugin.location.display().to_string())
264                    .unwrap_or_else(|| plugin_alias.name.clone());
265                let configuration = plugin_alias
266                    .run_plugin
267                    .as_ref()
268                    .map(|run_plugin| run_plugin.configuration.clone());
269                (Some(name), configuration)
270            },
271        },
272        _ => (None, None),
273    }
274}
275pub fn extract_edit_and_line_number(layout_run: &Option<Run>) -> (Option<String>, Option<usize>) {
276    match &layout_run {
277        // TODO: line number in layouts?
278        Some(Run::EditFile(path, line_number, _cwd)) => {
279            (Some(path.display().to_string()), line_number.clone())
280        },
281        _ => (None, None),
282    }
283}
284
285fn serialize_pane_title_and_attributes(
286    command: &Option<String>,
287    edit: &Option<String>,
288    name: &Option<String>,
289    cwd: Option<PathBuf>,
290    focus: Option<bool>,
291    initial_pane_contents: &Option<String>,
292    pane_contents: &mut BTreeMap<String, String>,
293    has_children: bool,
294    kdl_node: &mut KdlNode,
295) {
296    match (&command, &edit) {
297        (Some(command), _) => kdl_node
298            .entries_mut()
299            .push(KdlEntry::new_prop("command", command.to_owned())),
300        (None, Some(edit)) => kdl_node
301            .entries_mut()
302            .push(KdlEntry::new_prop("edit", edit.to_owned())),
303        _ => {},
304    };
305    if let Some(name) = name {
306        kdl_node
307            .entries_mut()
308            .push(KdlEntry::new_prop("name", name.to_owned()));
309    }
310    if let Some(cwd) = cwd {
311        let path = cwd.display().to_string();
312        if !path.is_empty() && !has_children {
313            kdl_node
314                .entries_mut()
315                .push(KdlEntry::new_prop("cwd", path.to_owned()));
316        }
317    }
318    if focus.unwrap_or(false) {
319        kdl_node
320            .entries_mut()
321            .push(KdlEntry::new_prop("focus", KdlValue::Bool(true)));
322    }
323    if let Some(initial_pane_contents) = initial_pane_contents.as_ref() {
324        if command.is_none() && edit.is_none() {
325            let file_name = format!("initial_contents_{}", pane_contents.keys().len() + 1);
326            kdl_node
327                .entries_mut()
328                .push(KdlEntry::new_prop("contents_file", file_name.clone()));
329
330            pane_contents.insert(file_name, initial_pane_contents.clone());
331        }
332    }
333}
334
335fn serialize_args(args: Vec<String>, pane_node_children: &mut KdlDocument) {
336    if !args.is_empty() {
337        let mut args_node = KdlNode::new("args");
338        for arg in &args {
339            args_node.entries_mut().push(KdlEntry::new(arg.to_owned()));
340        }
341        pane_node_children.nodes_mut().push(args_node);
342    }
343}
344
345fn serialize_plugin(
346    plugin: Option<String>,
347    plugin_config: Option<PluginUserConfiguration>,
348    pane_node_children: &mut KdlDocument,
349) {
350    if let Some(plugin) = plugin {
351        let mut plugin_node = KdlNode::new("plugin");
352        plugin_node
353            .entries_mut()
354            .push(KdlEntry::new_prop("location", plugin.to_owned()));
355        if let Some(plugin_config) =
356            plugin_config.and_then(|p| if p.inner().is_empty() { None } else { Some(p) })
357        {
358            let mut plugin_node_children = KdlDocument::new();
359            for (config_key, config_value) in plugin_config.inner() {
360                let mut config_node = KdlNode::new(config_key.to_owned());
361                config_node
362                    .entries_mut()
363                    .push(KdlEntry::new(config_value.to_owned()));
364                plugin_node_children.nodes_mut().push(config_node);
365            }
366            plugin_node.set_children(plugin_node_children);
367        }
368        pane_node_children.nodes_mut().push(plugin_node);
369    }
370}
371
372fn serialize_tiled_layout_attributes(
373    layout: &TiledPaneLayout,
374    ignore_size: bool,
375    kdl_node: &mut KdlNode,
376) {
377    if !ignore_size {
378        match layout.split_size {
379            Some(SplitSize::Fixed(size)) => kdl_node
380                .entries_mut()
381                .push(KdlEntry::new_prop("size", KdlValue::Base10(size as i64))),
382            Some(SplitSize::Percent(size)) => kdl_node
383                .entries_mut()
384                .push(KdlEntry::new_prop("size", format!("{size}%"))),
385            None => (),
386        };
387    }
388    if layout.borderless.unwrap_or(false) {
389        kdl_node
390            .entries_mut()
391            .push(KdlEntry::new_prop("borderless", KdlValue::Bool(true)));
392    }
393    if layout.children_are_stacked {
394        kdl_node
395            .entries_mut()
396            .push(KdlEntry::new_prop("stacked", KdlValue::Bool(true)));
397    }
398    if layout.is_expanded_in_stack {
399        kdl_node
400            .entries_mut()
401            .push(KdlEntry::new_prop("expanded", KdlValue::Bool(true)));
402    }
403    if layout.children_split_direction != SplitDirection::default() {
404        let direction = match layout.children_split_direction {
405            SplitDirection::Horizontal => "horizontal",
406            SplitDirection::Vertical => "vertical",
407        };
408        kdl_node
409            .entries_mut()
410            .push(KdlEntry::new_prop("split_direction", direction));
411    }
412}
413
414fn serialize_floating_layout_attributes(
415    layout: &FloatingPaneLayout,
416    pane_node_children: &mut KdlDocument,
417) {
418    match layout.height {
419        Some(PercentOrFixed::Fixed(fixed_height)) => {
420            let mut node = KdlNode::new("height");
421            node.entries_mut()
422                .push(KdlEntry::new(KdlValue::Base10(fixed_height as i64)));
423            pane_node_children.nodes_mut().push(node);
424        },
425        Some(PercentOrFixed::Percent(percent)) => {
426            let mut node = KdlNode::new("height");
427            node.entries_mut()
428                .push(KdlEntry::new(format!("{}%", percent)));
429            pane_node_children.nodes_mut().push(node);
430        },
431        None => {},
432    }
433    match layout.width {
434        Some(PercentOrFixed::Fixed(fixed_width)) => {
435            let mut node = KdlNode::new("width");
436            node.entries_mut()
437                .push(KdlEntry::new(KdlValue::Base10(fixed_width as i64)));
438            pane_node_children.nodes_mut().push(node);
439        },
440        Some(PercentOrFixed::Percent(percent)) => {
441            let mut node = KdlNode::new("width");
442            node.entries_mut()
443                .push(KdlEntry::new(format!("{}%", percent)));
444            pane_node_children.nodes_mut().push(node);
445        },
446        None => {},
447    }
448    match layout.x {
449        Some(PercentOrFixed::Fixed(fixed_x)) => {
450            let mut node = KdlNode::new("x");
451            node.entries_mut()
452                .push(KdlEntry::new(KdlValue::Base10(fixed_x as i64)));
453            pane_node_children.nodes_mut().push(node);
454        },
455        Some(PercentOrFixed::Percent(percent)) => {
456            let mut node = KdlNode::new("x");
457            node.entries_mut()
458                .push(KdlEntry::new(format!("{}%", percent)));
459            pane_node_children.nodes_mut().push(node);
460        },
461        None => {},
462    }
463    match layout.y {
464        Some(PercentOrFixed::Fixed(fixed_y)) => {
465            let mut node = KdlNode::new("y");
466            node.entries_mut()
467                .push(KdlEntry::new(KdlValue::Base10(fixed_y as i64)));
468            pane_node_children.nodes_mut().push(node);
469        },
470        Some(PercentOrFixed::Percent(percent)) => {
471            let mut node = KdlNode::new("y");
472            node.entries_mut()
473                .push(KdlEntry::new(format!("{}%", percent)));
474            pane_node_children.nodes_mut().push(node);
475        },
476        None => {},
477    }
478    match layout.pinned {
479        Some(true) => {
480            let mut node = KdlNode::new("pinned");
481            node.entries_mut().push(KdlEntry::new(KdlValue::Bool(true)));
482            pane_node_children.nodes_mut().push(node);
483        },
484        _ => {},
485    }
486}
487
488fn serialize_start_suspended(command: &Option<String>, pane_node_children: &mut KdlDocument) {
489    if command.is_some() {
490        let mut start_suspended_node = KdlNode::new("start_suspended");
491        start_suspended_node
492            .entries_mut()
493            .push(KdlEntry::new(KdlValue::Bool(true)));
494        pane_node_children.nodes_mut().push(start_suspended_node);
495    }
496}
497
498fn serialize_global_cwd(global_cwd: &Option<PathBuf>) -> Option<KdlNode> {
499    global_cwd.as_ref().map(|cwd| {
500        let mut node = KdlNode::new("cwd");
501        node.push(cwd.display().to_string());
502        node
503    })
504}
505
506fn serialize_new_tab_template(
507    new_tab_template: Option<(TiledPaneLayout, Vec<FloatingPaneLayout>)>,
508    pane_contents: &mut BTreeMap<String, String>,
509    layout_children_node: &mut KdlDocument,
510) {
511    if let Some((tiled_panes, floating_panes)) = new_tab_template {
512        let tiled_panes = tiled_panes_to_serialize(tiled_panes);
513        let mut new_tab_template_node = KdlNode::new("new_tab_template");
514        let mut new_tab_template_children = KdlDocument::new();
515
516        serialize_tiled_and_floating_panes(
517            &tiled_panes,
518            floating_panes,
519            pane_contents,
520            &mut new_tab_template_children,
521        );
522        new_tab_template_node.set_children(new_tab_template_children);
523        layout_children_node.nodes_mut().push(new_tab_template_node);
524    }
525}
526
527fn serialize_swap_tiled_layouts(
528    swap_tiled_layouts: Vec<SwapTiledLayout>,
529    pane_contents: &mut BTreeMap<String, String>,
530    layout_node_children: &mut KdlDocument,
531) {
532    for swap_tiled_layout in swap_tiled_layouts {
533        let mut swap_tiled_layout_node = KdlNode::new("swap_tiled_layout");
534        let mut swap_tiled_layout_node_children = KdlDocument::new();
535        let swap_tiled_layout_name = swap_tiled_layout.1;
536        if let Some(name) = swap_tiled_layout_name {
537            swap_tiled_layout_node
538                .entries_mut()
539                .push(KdlEntry::new_prop("name", name.to_owned()));
540        }
541
542        for (layout_constraint, tiled_panes_layout) in swap_tiled_layout.0 {
543            let tiled_panes_layout = tiled_panes_to_serialize(tiled_panes_layout);
544            let mut layout_step_node = KdlNode::new("tab");
545            let mut layout_step_node_children = KdlDocument::new();
546            if let Some(layout_constraint_entry) = serialize_layout_constraint(layout_constraint) {
547                layout_step_node.entries_mut().push(layout_constraint_entry);
548            }
549
550            serialize_tiled_and_floating_panes(
551                &tiled_panes_layout,
552                vec![],
553                pane_contents,
554                &mut layout_step_node_children,
555            );
556            layout_step_node.set_children(layout_step_node_children);
557            swap_tiled_layout_node_children
558                .nodes_mut()
559                .push(layout_step_node);
560        }
561        swap_tiled_layout_node.set_children(swap_tiled_layout_node_children);
562        layout_node_children
563            .nodes_mut()
564            .push(swap_tiled_layout_node);
565    }
566}
567
568fn serialize_layout_constraint(layout_constraint: LayoutConstraint) -> Option<KdlEntry> {
569    match layout_constraint {
570        LayoutConstraint::MaxPanes(max_panes) => Some(KdlEntry::new_prop(
571            "max_panes",
572            KdlValue::Base10(max_panes as i64),
573        )),
574        LayoutConstraint::MinPanes(min_panes) => Some(KdlEntry::new_prop(
575            "min_panes",
576            KdlValue::Base10(min_panes as i64),
577        )),
578        LayoutConstraint::ExactPanes(exact_panes) => Some(KdlEntry::new_prop(
579            "exact_panes",
580            KdlValue::Base10(exact_panes as i64),
581        )),
582        LayoutConstraint::NoConstraint => None,
583    }
584}
585
586fn serialize_swap_floating_layouts(
587    swap_floating_layouts: Vec<SwapFloatingLayout>,
588    pane_contents: &mut BTreeMap<String, String>,
589    layout_children_node: &mut KdlDocument,
590) {
591    for swap_floating_layout in swap_floating_layouts {
592        let mut swap_floating_layout_node = KdlNode::new("swap_floating_layout");
593        let mut swap_floating_layout_node_children = KdlDocument::new();
594        let swap_floating_layout_name = swap_floating_layout.1;
595        if let Some(name) = swap_floating_layout_name {
596            swap_floating_layout_node
597                .entries_mut()
598                .push(KdlEntry::new_prop("name", name.to_owned()));
599        }
600
601        for (layout_constraint, floating_panes_layout) in swap_floating_layout.0 {
602            let mut layout_step_node = KdlNode::new("floating_panes");
603            let mut layout_step_node_children = KdlDocument::new();
604            if let Some(layout_constraint_entry) = serialize_layout_constraint(layout_constraint) {
605                layout_step_node.entries_mut().push(layout_constraint_entry);
606            }
607
608            for floating_pane_layout in floating_panes_layout {
609                let floating_pane_node =
610                    serialize_floating_pane(&floating_pane_layout, pane_contents);
611                layout_step_node_children
612                    .nodes_mut()
613                    .push(floating_pane_node);
614            }
615            layout_step_node.set_children(layout_step_node_children);
616            swap_floating_layout_node_children
617                .nodes_mut()
618                .push(layout_step_node);
619        }
620        swap_floating_layout_node.set_children(swap_floating_layout_node_children);
621        layout_children_node
622            .nodes_mut()
623            .push(swap_floating_layout_node);
624    }
625}
626
627fn serialize_multiple_tabs(
628    tabs: Vec<(String, TabLayoutManifest)>,
629    pane_contents: &mut BTreeMap<String, String>,
630) -> Result<Vec<KdlNode>, &'static str> {
631    let mut serialized_tabs: Vec<KdlNode> = vec![];
632    for (tab_name, tab_layout_manifest) in tabs {
633        let tiled_panes = tab_layout_manifest.tiled_panes;
634        let floating_panes = tab_layout_manifest.floating_panes;
635        let hide_floating_panes = tab_layout_manifest.hide_floating_panes;
636        let serialized = serialize_tab(
637            tab_name.clone(),
638            tab_layout_manifest.is_focused,
639            hide_floating_panes,
640            &tiled_panes,
641            &floating_panes,
642            pane_contents,
643        );
644        if let Some(serialized) = serialized {
645            serialized_tabs.push(serialized);
646        } else {
647            return Err("Failed to serialize session state");
648        }
649    }
650    Ok(serialized_tabs)
651}
652
653fn serialize_floating_pane(
654    layout: &FloatingPaneLayout,
655    pane_contents: &mut BTreeMap<String, String>,
656) -> KdlNode {
657    let mut floating_pane_node = KdlNode::new("pane");
658    let mut floating_pane_node_children = KdlDocument::new();
659    let (command, args) = extract_command_and_args(&layout.run);
660    let (plugin, plugin_config) = extract_plugin_and_config(&layout.run);
661    let (edit, _line_number) = extract_edit_and_line_number(&layout.run);
662    let cwd = layout.run.as_ref().and_then(|r| r.get_cwd());
663    let has_children = false;
664    serialize_pane_title_and_attributes(
665        &command,
666        &edit,
667        &layout.name,
668        cwd,
669        layout.focus,
670        &layout.pane_initial_contents,
671        pane_contents,
672        has_children,
673        &mut floating_pane_node,
674    );
675    if let Some(ref fg) = layout.default_fg {
676        floating_pane_node
677            .entries_mut()
678            .push(KdlEntry::new_prop("default_fg", fg.to_owned()));
679    }
680    if let Some(ref bg) = layout.default_bg {
681        floating_pane_node
682            .entries_mut()
683            .push(KdlEntry::new_prop("default_bg", bg.to_owned()));
684    }
685    serialize_start_suspended(&command, &mut floating_pane_node_children);
686    serialize_floating_layout_attributes(&layout, &mut floating_pane_node_children);
687    serialize_args(args, &mut floating_pane_node_children);
688    serialize_plugin(plugin, plugin_config, &mut floating_pane_node_children);
689    floating_pane_node.set_children(floating_pane_node_children);
690    floating_pane_node
691}
692
693fn stack_layout_from_manifest(
694    geoms: &Vec<PaneLayoutManifest>,
695    split_size: Option<SplitSize>,
696) -> Option<TiledPaneLayout> {
697    let mut children_stacks: HashMap<usize, Vec<PaneLayoutManifest>> = HashMap::new();
698    for p in geoms {
699        if let Some(stack_id) = p.geom.stacked {
700            children_stacks
701                .entry(stack_id)
702                .or_insert_with(Default::default)
703                .push(p.clone());
704        }
705    }
706    let mut stack_nodes = vec![];
707    for (_stack_id, mut stacked_panes) in children_stacks.into_iter() {
708        stacked_panes.sort_by_key(|p| p.geom.y);
709        stack_nodes.push(TiledPaneLayout {
710            split_size,
711            children: stacked_panes
712                .iter()
713                .map(|p| tiled_pane_layout_from_manifest(Some(p), None))
714                .collect(),
715            children_are_stacked: true,
716            ..Default::default()
717        })
718    }
719    if stack_nodes.len() == 1 {
720        // if there's only one stack, we return it without a wrapper
721        stack_nodes.iter().next().cloned()
722    } else {
723        // here there is more than one stack, so we wrap it in a logical container node
724        Some(TiledPaneLayout {
725            split_size,
726            children: stack_nodes,
727            ..Default::default()
728        })
729    }
730}
731
732fn tiled_pane_layout_from_manifest(
733    manifest: Option<&PaneLayoutManifest>,
734    split_size: Option<SplitSize>,
735) -> TiledPaneLayout {
736    let (
737        run,
738        borderless,
739        is_expanded_in_stack,
740        name,
741        focus,
742        pane_initial_contents,
743        default_fg,
744        default_bg,
745    ) = manifest
746        .map(|g| {
747            let mut run = g.run.clone();
748            if let Some(cwd) = &g.cwd {
749                if let Some(run) = run.as_mut() {
750                    run.add_cwd(cwd);
751                } else {
752                    run = Some(Run::Cwd(cwd.clone()));
753                }
754            }
755            (
756                run,
757                Some(g.is_borderless),
758                g.geom.is_stacked() && g.geom.rows.inner > 1,
759                g.title.clone(),
760                Some(g.is_focused),
761                g.pane_contents.clone(),
762                g.default_fg.clone(),
763                g.default_bg.clone(),
764            )
765        })
766        .unwrap_or((None, None, false, None, None, None, None, None));
767    TiledPaneLayout {
768        split_size,
769        run,
770        borderless,
771        is_expanded_in_stack,
772        name,
773        focus,
774        pane_initial_contents,
775        default_fg,
776        default_bg,
777        ..Default::default()
778    }
779}
780
781/// Tab-level parsing
782fn get_tiled_panes_layout_from_panegeoms(
783    geoms: &Vec<PaneLayoutManifest>,
784    split_size: Option<SplitSize>,
785) -> Option<TiledPaneLayout> {
786    let (children_split_direction, splits) = match get_splits(&geoms) {
787        Some(x) => x,
788        None => {
789            if geoms.len() > 1 {
790                // this can only happen if all geoms belong to one or more stacks
791                // since stack splits are discounted in the get_splits method
792                return stack_layout_from_manifest(geoms, split_size);
793            } else {
794                return Some(tiled_pane_layout_from_manifest(
795                    geoms.iter().next(),
796                    split_size,
797                ));
798            }
799        },
800    };
801    let mut children = Vec::new();
802    let mut remaining_geoms = geoms.clone();
803    let mut new_geoms = Vec::new();
804    let mut new_constraints = Vec::new();
805    for i in 1..splits.len() {
806        let (v_min, v_max) = (splits[i - 1], splits[i]);
807        let subgeoms: Vec<PaneLayoutManifest>;
808        (subgeoms, remaining_geoms) = match children_split_direction {
809            SplitDirection::Horizontal => remaining_geoms
810                .clone()
811                .into_iter()
812                .partition(|g| g.geom.y + g.geom.rows.as_usize() <= v_max),
813            SplitDirection::Vertical => remaining_geoms
814                .clone()
815                .into_iter()
816                .partition(|g| g.geom.x + g.geom.cols.as_usize() <= v_max),
817        };
818        match get_domain_constraint(&subgeoms, &children_split_direction, (v_min, v_max)) {
819            Some(constraint) => {
820                new_geoms.push(subgeoms);
821                new_constraints.push(constraint);
822            },
823            None => {
824                return None;
825            },
826        }
827    }
828
829    let new_split_sizes = get_split_sizes(&new_constraints);
830
831    for (subgeoms, subsplit_size) in new_geoms.iter().zip(new_split_sizes) {
832        match get_tiled_panes_layout_from_panegeoms(&subgeoms, subsplit_size) {
833            Some(child) => {
834                children.push(child);
835            },
836            None => {
837                return None;
838            },
839        }
840    }
841    let children_are_stacked = children_split_direction == SplitDirection::Horizontal
842        && all_geoms_are_from_the_same_stack(&new_geoms);
843    Some(TiledPaneLayout {
844        children_split_direction,
845        split_size,
846        children,
847        children_are_stacked,
848        ..Default::default()
849    })
850}
851
852fn all_geoms_are_from_the_same_stack(manifests: &Vec<Vec<PaneLayoutManifest>>) -> bool {
853    let mut stack_ids = HashSet::new();
854    for manifest_group in manifests {
855        for pane_layout_manifest in manifest_group {
856            stack_ids.insert(pane_layout_manifest.geom.stacked);
857        }
858    }
859    stack_ids.len() == 1 && !stack_ids.contains(&None)
860}
861
862fn get_floating_panes_layout_from_panegeoms(
863    manifests: &Vec<PaneLayoutManifest>,
864) -> Vec<FloatingPaneLayout> {
865    manifests
866        .iter()
867        .map(|m| {
868            let mut run = m.run.clone();
869            if let Some(cwd) = &m.cwd {
870                run.as_mut().map(|r| r.add_cwd(cwd));
871            }
872            FloatingPaneLayout {
873                name: m.title.clone(),
874                height: Some(m.geom.rows.into()),
875                width: Some(m.geom.cols.into()),
876                x: Some(PercentOrFixed::Fixed(m.geom.x)),
877                y: Some(PercentOrFixed::Fixed(m.geom.y)),
878                pinned: Some(m.geom.is_pinned),
879                run,
880                focus: Some(m.is_focused),
881                already_running: false,
882                pane_initial_contents: m.pane_contents.clone(),
883                logical_position: None,
884                borderless: Some(m.is_borderless),
885                default_fg: m.default_fg.clone(),
886                default_bg: m.default_bg.clone(),
887            }
888        })
889        .collect()
890}
891
892fn get_x_lims(geoms: &Vec<PaneLayoutManifest>) -> Option<(usize, usize)> {
893    match (
894        geoms.iter().map(|g| g.geom.x).min(),
895        geoms
896            .iter()
897            .map(|g| g.geom.x + g.geom.cols.as_usize())
898            .max(),
899    ) {
900        (Some(x_min), Some(x_max)) => Some((x_min, x_max)),
901        _ => None,
902    }
903}
904
905fn get_y_lims(geoms: &Vec<PaneLayoutManifest>) -> Option<(usize, usize)> {
906    match (
907        geoms.iter().map(|g| g.geom.y).min(),
908        geoms
909            .iter()
910            .map(|g| g.geom.y + g.geom.rows.as_usize())
911            .max(),
912    ) {
913        (Some(y_min), Some(y_max)) => Some((y_min, y_max)),
914        _ => None,
915    }
916}
917
918/// Returns the `SplitDirection` as well as the values, on the axis
919/// perpendicular the `SplitDirection`, for which there is a split spanning
920/// the max_cols or max_rows of the domain. The values are ordered
921/// increasingly and contains the boundaries of the domain.
922fn get_splits(geoms: &Vec<PaneLayoutManifest>) -> Option<(SplitDirection, Vec<usize>)> {
923    if geoms.len() == 1 {
924        return None;
925    }
926    let (x_lims, y_lims) = match (get_x_lims(&geoms), get_y_lims(&geoms)) {
927        (Some(x_lims), Some(y_lims)) => (x_lims, y_lims),
928        _ => return None,
929    };
930    let mut direction = SplitDirection::default();
931    let mut splits = match direction {
932        SplitDirection::Vertical => get_col_splits(&geoms, &x_lims, &y_lims),
933        SplitDirection::Horizontal => get_row_splits(&geoms, &x_lims, &y_lims),
934    };
935    if splits.len() <= 2 {
936        // ie only the boundaries are present and no real split has been found
937        direction = !direction;
938        splits = match direction {
939            SplitDirection::Vertical => get_col_splits(&geoms, &x_lims, &y_lims),
940            SplitDirection::Horizontal => get_row_splits(&geoms, &x_lims, &y_lims),
941        };
942    }
943    if splits.len() <= 2 {
944        // ie no real split has been found in both directions
945        None
946    } else {
947        Some((direction, splits))
948    }
949}
950
951/// Returns a vector containing the abscisse (x) of the cols that split the
952/// domain including the boundaries, ie the min and max abscisse values.
953fn get_col_splits(
954    geoms: &Vec<PaneLayoutManifest>,
955    (_, x_max): &(usize, usize),
956    (y_min, y_max): &(usize, usize),
957) -> Vec<usize> {
958    let max_rows = y_max - y_min;
959    let mut splits = Vec::new();
960    let mut sorted_geoms = geoms.clone();
961    sorted_geoms.sort_by_key(|g| g.geom.x);
962    for x in sorted_geoms.iter().map(|g| g.geom.x) {
963        if splits.contains(&x) {
964            continue;
965        }
966        if sorted_geoms
967            .iter()
968            .filter(|g| g.geom.x == x)
969            .map(|g| g.geom.rows.as_usize())
970            .sum::<usize>()
971            == max_rows
972        {
973            splits.push(x);
974        };
975    }
976    splits.push(*x_max); // Necessary as `g.x` is from the upper-left corner
977    splits
978}
979
980/// Returns a vector containing the coordinate (y) of the rows that split the
981/// domain including the boundaries, ie the min and max coordinate values.
982fn get_row_splits(
983    geoms: &Vec<PaneLayoutManifest>,
984    (x_min, x_max): &(usize, usize),
985    (_, y_max): &(usize, usize),
986) -> Vec<usize> {
987    let max_cols = x_max - x_min;
988    let mut splits = Vec::new();
989    let mut sorted_geoms = geoms.clone();
990    sorted_geoms.sort_by_key(|g| g.geom.y);
991
992    //  here we make sure the various panes in all the stacks aren't counted as splits, since
993    //  stacked panes must always stay togethyer - we group them into one "geom" for the purposes
994    //  of figuring out their splits
995    let mut stack_geoms: HashMap<usize, Vec<PaneLayoutManifest>> = HashMap::new();
996    let mut all_geoms = vec![];
997    for pane_layout_manifest in sorted_geoms.drain(..) {
998        if let Some(stack_id) = pane_layout_manifest.geom.stacked {
999            stack_geoms
1000                .entry(stack_id)
1001                .or_insert_with(Default::default)
1002                .push(pane_layout_manifest)
1003        } else {
1004            all_geoms.push(pane_layout_manifest);
1005        }
1006    }
1007    for (_stack_id, mut geoms_in_stack) in stack_geoms.into_iter() {
1008        let mut geom_of_whole_stack = geoms_in_stack.remove(0);
1009        if let Some(last_geom) = geoms_in_stack.last() {
1010            geom_of_whole_stack
1011                .geom
1012                .rows
1013                .set_inner(last_geom.geom.y + last_geom.geom.rows.as_usize())
1014        }
1015        all_geoms.push(geom_of_whole_stack);
1016    }
1017
1018    all_geoms.sort_by_key(|g| g.geom.y);
1019
1020    for y in all_geoms.iter().map(|g| g.geom.y) {
1021        if splits.contains(&y) {
1022            continue;
1023        }
1024        if all_geoms
1025            .iter()
1026            .filter(|g| g.geom.y == y)
1027            .map(|g| g.geom.cols.as_usize())
1028            .sum::<usize>()
1029            == max_cols
1030        {
1031            splits.push(y);
1032        };
1033    }
1034    splits.push(*y_max); // Necessary as `g.y` is from the upper-left corner
1035    splits
1036}
1037
1038/// Get the constraint of the domain considered, base on the rows or columns,
1039/// depending on the split direction provided.
1040fn get_domain_constraint(
1041    geoms: &Vec<PaneLayoutManifest>,
1042    split_direction: &SplitDirection,
1043    (v_min, v_max): (usize, usize),
1044) -> Option<Constraint> {
1045    match split_direction {
1046        SplitDirection::Horizontal => get_domain_row_constraint(&geoms, (v_min, v_max)),
1047        SplitDirection::Vertical => get_domain_col_constraint(&geoms, (v_min, v_max)),
1048    }
1049}
1050
1051fn get_domain_col_constraint(
1052    geoms: &Vec<PaneLayoutManifest>,
1053    (x_min, x_max): (usize, usize),
1054) -> Option<Constraint> {
1055    let mut percent = 0.0;
1056    let mut x = x_min;
1057    while x != x_max {
1058        // we only look at one (ie the last) geom that has value `x` for `g.x`
1059        let geom = geoms.iter().filter(|g| g.geom.x == x).last();
1060        match geom {
1061            Some(geom) => {
1062                if let Some(size) = geom.geom.cols.as_percent() {
1063                    percent += size;
1064                }
1065                x += geom.geom.cols.as_usize();
1066            },
1067            None => {
1068                return None;
1069            },
1070        }
1071    }
1072    if percent == 0.0 {
1073        Some(Constraint::Fixed(x_max - x_min))
1074    } else {
1075        Some(Constraint::Percent(percent))
1076    }
1077}
1078
1079fn get_domain_row_constraint(
1080    geoms: &Vec<PaneLayoutManifest>,
1081    (y_min, y_max): (usize, usize),
1082) -> Option<Constraint> {
1083    let mut percent = 0.0;
1084    let mut y = y_min;
1085    while y != y_max {
1086        // we only look at one (ie the last) geom that has value `y` for `g.y`
1087        let geom = geoms.iter().filter(|g| g.geom.y == y).last();
1088        match geom {
1089            Some(geom) => {
1090                if let Some(size) = geom.geom.rows.as_percent() {
1091                    percent += size;
1092                }
1093                y += geom.geom.rows.as_usize();
1094            },
1095            None => {
1096                return None;
1097            },
1098        }
1099    }
1100    if percent == 0.0 {
1101        Some(Constraint::Fixed(y_max - y_min))
1102    } else {
1103        Some(Constraint::Percent(percent))
1104    }
1105}
1106
1107/// Returns split sizes for all the children of a `TiledPaneLayout` based on
1108/// their constraints.
1109fn get_split_sizes(constraints: &Vec<Constraint>) -> Vec<Option<SplitSize>> {
1110    let mut split_sizes = Vec::new();
1111    let max_percent = constraints
1112        .iter()
1113        .filter_map(|c| match c {
1114            Constraint::Percent(size) => Some(size),
1115            _ => None,
1116        })
1117        .sum::<f64>();
1118    for constraint in constraints {
1119        let split_size = match constraint {
1120            Constraint::Fixed(size) => Some(SplitSize::Fixed(*size)),
1121            Constraint::Percent(size) => {
1122                if size == &max_percent {
1123                    None
1124                } else {
1125                    Some(SplitSize::Percent((100.0 * size / max_percent) as usize))
1126                }
1127            },
1128        };
1129        split_sizes.push(split_size);
1130    }
1131    split_sizes
1132}
1133
1134#[cfg(test)]
1135mod tests {
1136
1137    use super::*;
1138    use crate::pane_size::Dimension;
1139    use expect_test::expect;
1140    use insta::assert_snapshot;
1141    use serde_json::Value;
1142    use std::collections::HashMap;
1143    const PANEGEOMS_JSON: &[&[&str]] = &[
1144        &[
1145            r#"{ "x": 0, "y": 1, "rows": { "constraint": "Percent(100.0)", "inner": 43 }, "cols": { "constraint": "Percent(100.0)", "inner": 211 }, "is_stacked": false }"#,
1146            r#"{ "x": 0, "y": 0, "rows": { "constraint": "Fixed(1)", "inner": 1 }, "cols": { "constraint": "Percent(100.0)", "inner": 211 }, "is_stacked": false }"#,
1147            r#"{ "x": 0, "y": 44, "rows": { "constraint": "Fixed(2)", "inner": 2 }, "cols": { "constraint": "Percent(100.0)", "inner": 211 }, "is_stacked": false }"#,
1148        ],
1149        &[
1150            r#"{ "x": 0, "y": 0, "rows": { "constraint": "Percent(100.0)", "inner": 26 }, "cols": { "constraint": "Percent(100.0)", "inner": 211 }, "is_stacked": false }"#,
1151            r#"{ "x": 0, "y": 26, "rows": { "constraint": "Fixed(20)", "inner": 20 }, "cols": { "constraint": "Fixed(50)", "inner": 50 }, "is_stacked": false }"#,
1152            r#"{ "x": 50, "y": 26, "rows": { "constraint": "Fixed(20)", "inner": 20 }, "cols": { "constraint": "Percent(100.0)", "inner": 161 }, "is_stacked": false }"#,
1153        ],
1154        &[
1155            r#"{ "x": 0, "y": 0, "rows": { "constraint": "Fixed(10)", "inner": 10 }, "cols": { "constraint": "Percent(50.0)", "inner": 106 }, "is_stacked": false }"#,
1156            r#"{ "x": 106, "y": 0, "rows": { "constraint": "Fixed(10)", "inner": 10 }, "cols": { "constraint": "Percent(50.0)", "inner": 105 }, "is_stacked": false }"#,
1157            r#"{ "x": 0, "y": 10, "rows": { "constraint": "Percent(100.0)", "inner": 26 }, "cols": { "constraint": "Fixed(40)", "inner": 40 }, "is_stacked": false }"#,
1158            r#"{ "x": 40, "y": 10, "rows": { "constraint": "Percent(100.0)", "inner": 26 }, "cols": { "constraint": "Percent(100.0)", "inner": 131 }, "is_stacked": false }"#,
1159            r#"{ "x": 171, "y": 10, "rows": { "constraint": "Percent(100.0)", "inner": 26 }, "cols": { "constraint": "Fixed(40)", "inner": 40 }, "is_stacked": false }"#,
1160            r#"{ "x": 0, "y": 36, "rows": { "constraint": "Fixed(10)", "inner": 10 }, "cols": { "constraint": "Percent(50.0)", "inner": 106 }, "is_stacked": false }"#,
1161            r#"{ "x": 106, "y": 36, "rows": { "constraint": "Fixed(10)", "inner": 10 }, "cols": { "constraint": "Percent(50.0)", "inner": 105 }, "is_stacked": false }"#,
1162        ],
1163        &[
1164            r#"{ "x": 0, "y": 0, "rows": { "constraint": "Percent(30.0)", "inner": 11 }, "cols": { "constraint": "Percent(35.0)", "inner": 74 }, "is_stacked": false }"#,
1165            r#"{ "x": 0, "y": 11, "rows": { "constraint": "Percent(30.0)", "inner": 11 }, "cols": { "constraint": "Percent(35.0)", "inner": 74 }, "is_stacked": false }"#,
1166            r#"{ "x": 0, "y": 22, "rows": { "constraint": "Percent(40.0)", "inner": 14 }, "cols": { "constraint": "Percent(35.0)", "inner": 74 }, "is_stacked": false }"#,
1167            r#"{ "x": 74, "y": 0, "rows": { "constraint": "Percent(100.0)", "inner": 36 }, "cols": { "constraint": "Percent(35.0)", "inner": 74 }, "is_stacked": false }"#,
1168            r#"{ "x": 0, "y": 36, "rows": { "constraint": "Fixed(10)", "inner": 10 }, "cols": { "constraint": "Percent(70.0)", "inner": 148 }, "is_stacked": false }"#,
1169            r#"{ "x": 148, "y": 0, "rows": { "constraint": "Percent(100.0)", "inner": 46 }, "cols": { "constraint": "Percent(30.0)", "inner": 63 }, "is_stacked": false }"#,
1170        ],
1171        &[
1172            r#"{ "x": 0, "y": 0, "rows": { "constraint": "Fixed(5)", "inner": 5 }, "cols": { "constraint": "Percent(100.0)", "inner": 211 }, "is_stacked": false }"#,
1173            r#"{ "x": 0, "y": 5, "rows": { "constraint": "Percent(100.0)", "inner": 36 }, "cols": { "constraint": "Fixed(20)", "inner": 20 }, "is_stacked": false }"#,
1174            r#"{ "x": 20, "y": 5, "rows": { "constraint": "Percent(100.0)", "inner": 36 }, "cols": { "constraint": "Percent(50.0)", "inner": 86 }, "is_stacked": false }"#,
1175            r#"{ "x": 106, "y": 5, "rows": { "constraint": "Percent(100.0)", "inner": 36 }, "cols": { "constraint": "Percent(50.0)", "inner": 85 }, "is_stacked": false }"#,
1176            r#"{ "x": 191, "y": 5, "rows": { "constraint": "Percent(100.0)", "inner": 36 }, "cols": { "constraint": "Fixed(20)", "inner": 20 }, "is_stacked": false }"#,
1177            r#"{ "x": 0, "y": 41, "rows": { "constraint": "Fixed(5)", "inner": 5 }, "cols": { "constraint": "Percent(100.0)", "inner": 211 }, "is_stacked": false }"#,
1178        ],
1179    ];
1180
1181    #[test]
1182    fn geoms() {
1183        let geoms = PANEGEOMS_JSON[0]
1184            .iter()
1185            .map(|pg| parse_panegeom_from_json(pg))
1186            .map(|geom| PaneLayoutManifest {
1187                geom,
1188                ..Default::default()
1189            })
1190            .collect();
1191        let tab_layout_manifest = TabLayoutManifest {
1192            tiled_panes: geoms,
1193            ..Default::default()
1194        };
1195        let global_layout_manifest = GlobalLayoutManifest {
1196            tabs: vec![("Tab #1".to_owned(), tab_layout_manifest)],
1197            ..Default::default()
1198        };
1199        let kdl = serialize_session_layout(global_layout_manifest).unwrap();
1200        expect![[r#"
1201            layout {
1202                tab name="Tab #1" {
1203                    pane size=1
1204                    pane
1205                    pane size=2
1206                }
1207            }
1208        "#]]
1209        .assert_eq(&kdl.0);
1210
1211        let geoms = PANEGEOMS_JSON[1]
1212            .iter()
1213            .map(|pg| parse_panegeom_from_json(pg))
1214            .map(|geom| PaneLayoutManifest {
1215                geom,
1216                ..Default::default()
1217            })
1218            .collect();
1219        let tab_layout_manifest = TabLayoutManifest {
1220            tiled_panes: geoms,
1221            ..Default::default()
1222        };
1223        let global_layout_manifest = GlobalLayoutManifest {
1224            tabs: vec![("Tab #1".to_owned(), tab_layout_manifest)],
1225            ..Default::default()
1226        };
1227        let kdl = serialize_session_layout(global_layout_manifest).unwrap();
1228        expect![[r#"
1229            layout {
1230                tab name="Tab #1" {
1231                    pane
1232                    pane size=20 split_direction="vertical" {
1233                        pane size=50
1234                        pane
1235                    }
1236                }
1237            }
1238        "#]]
1239        .assert_eq(&kdl.0);
1240
1241        let geoms = PANEGEOMS_JSON[2]
1242            .iter()
1243            .map(|pg| parse_panegeom_from_json(pg))
1244            .map(|geom| PaneLayoutManifest {
1245                geom,
1246                ..Default::default()
1247            })
1248            .collect();
1249        let tab_layout_manifest = TabLayoutManifest {
1250            tiled_panes: geoms,
1251            ..Default::default()
1252        };
1253        let global_layout_manifest = GlobalLayoutManifest {
1254            tabs: vec![("Tab #1".to_owned(), tab_layout_manifest)],
1255            ..Default::default()
1256        };
1257        let kdl = serialize_session_layout(global_layout_manifest).unwrap();
1258        expect![[r#"
1259            layout {
1260                tab name="Tab #1" {
1261                    pane size=10 split_direction="vertical" {
1262                        pane size="50%"
1263                        pane size="50%"
1264                    }
1265                    pane split_direction="vertical" {
1266                        pane size=40
1267                        pane
1268                        pane size=40
1269                    }
1270                    pane size=10 split_direction="vertical" {
1271                        pane size="50%"
1272                        pane size="50%"
1273                    }
1274                }
1275            }
1276        "#]]
1277        .assert_eq(&kdl.0);
1278
1279        let geoms = PANEGEOMS_JSON[3]
1280            .iter()
1281            .map(|pg| parse_panegeom_from_json(pg))
1282            .map(|geom| PaneLayoutManifest {
1283                geom,
1284                ..Default::default()
1285            })
1286            .collect();
1287        let tab_layout_manifest = TabLayoutManifest {
1288            tiled_panes: geoms,
1289            ..Default::default()
1290        };
1291        let global_layout_manifest = GlobalLayoutManifest {
1292            tabs: vec![("Tab #1".to_owned(), tab_layout_manifest)],
1293            ..Default::default()
1294        };
1295        let kdl = serialize_session_layout(global_layout_manifest).unwrap();
1296        expect![[r#"
1297            layout {
1298                tab name="Tab #1" {
1299                    pane split_direction="vertical" {
1300                        pane size="70%" {
1301                            pane split_direction="vertical" {
1302                                pane size="50%" {
1303                                    pane size="30%"
1304                                    pane size="30%"
1305                                    pane size="40%"
1306                                }
1307                                pane size="50%"
1308                            }
1309                            pane size=10
1310                        }
1311                        pane size="30%"
1312                    }
1313                }
1314            }
1315        "#]]
1316        .assert_eq(&kdl.0);
1317
1318        let geoms = PANEGEOMS_JSON[4]
1319            .iter()
1320            .map(|pg| parse_panegeom_from_json(pg))
1321            .map(|geom| PaneLayoutManifest {
1322                geom,
1323                ..Default::default()
1324            })
1325            .collect();
1326        let tab_layout_manifest = TabLayoutManifest {
1327            tiled_panes: geoms,
1328            ..Default::default()
1329        };
1330        let global_layout_manifest = GlobalLayoutManifest {
1331            tabs: vec![("Tab #1".to_owned(), tab_layout_manifest)],
1332            ..Default::default()
1333        };
1334        let kdl = serialize_session_layout(global_layout_manifest).unwrap();
1335        expect![[r#"
1336            layout {
1337                tab name="Tab #1" {
1338                    pane size=5
1339                    pane split_direction="vertical" {
1340                        pane size=20
1341                        pane size="50%"
1342                        pane size="50%"
1343                        pane size=20
1344                    }
1345                    pane size=5
1346                }
1347            }
1348        "#]]
1349        .assert_eq(&kdl.0);
1350    }
1351
1352    #[test]
1353    fn global_cwd() {
1354        let global_layout_manifest = GlobalLayoutManifest {
1355            global_cwd: Some(PathBuf::from("/path/to/m\"y/global cwd")),
1356            ..Default::default()
1357        };
1358        let kdl = serialize_session_layout(global_layout_manifest).unwrap();
1359        assert_snapshot!(kdl.0);
1360    }
1361
1362    #[test]
1363    fn can_serialize_tab_with_a_single_tiled_pane() {
1364        use crate::input::command::RunCommand;
1365        let tab_layout_manifest = TabLayoutManifest {
1366            tiled_panes: vec![PaneLayoutManifest {
1367                run: Some(Run::Command(RunCommand {
1368                    command: PathBuf::from("/bin/sleep"),
1369                    args: vec!["10000".to_owned()],
1370                    ..Default::default()
1371                })),
1372                title: Some("my-only-pane".to_owned()),
1373                geom: PaneGeom {
1374                    x: 0,
1375                    y: 0,
1376                    rows: Dimension::fixed(10),
1377                    cols: Dimension::fixed(10),
1378                    stacked: None,
1379                    is_pinned: false,
1380                    logical_position: None,
1381                },
1382                ..Default::default()
1383            }],
1384            ..Default::default()
1385        };
1386        let global_layout_manifest = GlobalLayoutManifest {
1387            tabs: vec![("Tab #1".to_owned(), tab_layout_manifest)],
1388            ..Default::default()
1389        };
1390        let kdl = serialize_session_layout(global_layout_manifest).unwrap();
1391        expect![[r#"
1392            layout {
1393                tab name="Tab #1" {
1394                    pane command="/bin/sleep" name="my-only-pane" {
1395                        args "10000"
1396                        start_suspended true
1397                    }
1398                }
1399            }
1400        "#]]
1401        .assert_eq(&kdl.0);
1402    }
1403
1404    #[test]
1405    fn can_serialize_tab_with_a_single_tiled_pane_and_a_floating_pane() {
1406        let tab_layout_manifest = TabLayoutManifest {
1407            tiled_panes: vec![PaneLayoutManifest {
1408                title: Some("my-only-tiled-pane".to_owned()),
1409                geom: PaneGeom {
1410                    x: 0,
1411                    y: 0,
1412                    rows: Dimension::fixed(10),
1413                    cols: Dimension::fixed(10),
1414                    stacked: None,
1415                    is_pinned: false,
1416                    logical_position: None,
1417                },
1418                ..Default::default()
1419            }],
1420            floating_panes: vec![PaneLayoutManifest {
1421                title: Some("my-floating-pane".to_owned()),
1422                geom: PaneGeom {
1423                    x: 1,
1424                    y: 1,
1425                    rows: Dimension::fixed(5),
1426                    cols: Dimension::fixed(5),
1427                    stacked: None,
1428                    is_pinned: false,
1429                    logical_position: None,
1430                },
1431                ..Default::default()
1432            }],
1433            ..Default::default()
1434        };
1435        let global_layout_manifest = GlobalLayoutManifest {
1436            tabs: vec![("Tab #1".to_owned(), tab_layout_manifest)],
1437            ..Default::default()
1438        };
1439        let kdl = serialize_session_layout(global_layout_manifest).unwrap();
1440        expect![[r#"
1441            layout {
1442                tab name="Tab #1" {
1443                    pane name="my-only-tiled-pane"
1444                    floating_panes {
1445                        pane name="my-floating-pane" {
1446                            height 5
1447                            width 5
1448                            x 1
1449                            y 1
1450                        }
1451                    }
1452                }
1453            }
1454        "#]]
1455        .assert_eq(&kdl.0);
1456    }
1457
1458    #[test]
1459    fn can_serialize_new_tab_template_with_a_single_pane() {
1460        let tiled_panes_layout = TiledPaneLayout {
1461            name: Some("my-only-template-pane".to_owned()),
1462            ..Default::default()
1463        };
1464        let mut default_layout = Layout::default();
1465        default_layout.template = Some((tiled_panes_layout, vec![]));
1466        let global_layout_manifest = GlobalLayoutManifest {
1467            default_layout: Box::new(default_layout),
1468            ..Default::default()
1469        };
1470        let kdl = serialize_session_layout(global_layout_manifest).unwrap();
1471        expect![[r#"
1472            layout {
1473                new_tab_template {
1474                    pane name="my-only-template-pane"
1475                }
1476            }
1477        "#]]
1478        .assert_eq(&kdl.0);
1479    }
1480
1481    #[test]
1482    fn can_serialize_swap_tiled_layout_with_a_single_pane() {
1483        let tiled_panes_layout = TiledPaneLayout {
1484            name: Some("my-only-swap-pane".to_owned()),
1485            ..Default::default()
1486        };
1487        let mut swap_tiled_layout = BTreeMap::new();
1488        swap_tiled_layout.insert(LayoutConstraint::NoConstraint, tiled_panes_layout);
1489        let mut default_layout = Layout::default();
1490        default_layout.swap_tiled_layouts = vec![(swap_tiled_layout, Some("my-swap".to_owned()))];
1491        let global_layout_manifest = GlobalLayoutManifest {
1492            default_layout: Box::new(default_layout),
1493            ..Default::default()
1494        };
1495        let kdl = serialize_session_layout(global_layout_manifest).unwrap();
1496        expect![[r#"
1497            layout {
1498                swap_tiled_layout name="my-swap" {
1499                    tab {
1500                        pane name="my-only-swap-pane"
1501                    }
1502                }
1503            }
1504        "#]]
1505        .assert_eq(&kdl.0);
1506    }
1507
1508    #[test]
1509    fn can_serialize_tab_name() {
1510        let global_layout_manifest = GlobalLayoutManifest {
1511            tabs: vec![("my \"tab \\name".to_owned(), TabLayoutManifest::default())],
1512            ..Default::default()
1513        };
1514        let kdl = serialize_session_layout(global_layout_manifest).unwrap();
1515        assert_snapshot!(kdl.0);
1516    }
1517    #[test]
1518    fn can_serialize_tab_focus() {
1519        let tab_layout_manifest = TabLayoutManifest {
1520            is_focused: true,
1521            ..Default::default()
1522        };
1523        let global_layout_manifest = GlobalLayoutManifest {
1524            tabs: vec![("Tab #1".to_owned(), tab_layout_manifest)],
1525            ..Default::default()
1526        };
1527        let kdl = serialize_session_layout(global_layout_manifest).unwrap();
1528        assert_snapshot!(kdl.0);
1529    }
1530    #[test]
1531    fn can_serialize_tab_hide_floating_panes() {
1532        let tab_layout_manifest = TabLayoutManifest {
1533            hide_floating_panes: true,
1534            ..Default::default()
1535        };
1536        let global_layout_manifest = GlobalLayoutManifest {
1537            tabs: vec![("Tab #1".to_owned(), tab_layout_manifest)],
1538            ..Default::default()
1539        };
1540        let kdl = serialize_session_layout(global_layout_manifest).unwrap();
1541        assert_snapshot!(kdl.0);
1542    }
1543    #[test]
1544    fn can_serialize_tab_with_tiled_panes() {
1545        use crate::input::command::RunCommand;
1546        use crate::input::layout::RunPlugin;
1547        let mut plugin_configuration = BTreeMap::new();
1548        plugin_configuration.insert("key 1\"\\".to_owned(), "val 1\"\\".to_owned());
1549        plugin_configuration.insert("key 2\"\\".to_owned(), "val 2\"\\".to_owned());
1550        let tab_layout_manifest = TabLayoutManifest {
1551            tiled_panes: vec![
1552                PaneLayoutManifest {
1553                    geom: PaneGeom {
1554                        x: 0,
1555                        y: 0,
1556                        rows: Dimension::fixed(10),
1557                        cols: Dimension::fixed(10),
1558                        stacked: None,
1559                        is_pinned: false,
1560                        logical_position: None,
1561                    },
1562                    ..Default::default()
1563                },
1564                PaneLayoutManifest {
1565                    run: Some(Run::Cwd(PathBuf::from("/tmp/\"my/cool cwd"))),
1566                    geom: PaneGeom {
1567                        x: 0,
1568                        y: 10,
1569                        rows: Dimension::fixed(10),
1570                        cols: Dimension::fixed(10),
1571                        stacked: None,
1572                        is_pinned: false,
1573                        logical_position: None,
1574                    },
1575                    ..Default::default()
1576                },
1577                PaneLayoutManifest {
1578                    run: Some(Run::EditFile(
1579                        PathBuf::from("/tmp/\"my/cool cwd/my-file"),
1580                        None,
1581                        None,
1582                    )),
1583                    geom: PaneGeom {
1584                        x: 0,
1585                        y: 20,
1586                        rows: Dimension::fixed(10),
1587                        cols: Dimension::fixed(10),
1588                        stacked: None,
1589                        is_pinned: false,
1590                        logical_position: None,
1591                    },
1592                    ..Default::default()
1593                },
1594                PaneLayoutManifest {
1595                    run: Some(Run::Command(RunCommand {
1596                        command: PathBuf::from("/tmp/\"my/cool cwd/command.sh"),
1597                        ..Default::default()
1598                    })),
1599                    geom: PaneGeom {
1600                        x: 0,
1601                        y: 30,
1602                        rows: Dimension::fixed(10),
1603                        cols: Dimension::fixed(10),
1604                        stacked: None,
1605                        is_pinned: false,
1606                        logical_position: None,
1607                    },
1608                    ..Default::default()
1609                },
1610                PaneLayoutManifest {
1611                    run: Some(Run::Command(RunCommand {
1612                        command: PathBuf::from("/tmp/\"my/cool cwd/command.sh"),
1613                        args: vec![
1614                            "--arg1".to_owned(),
1615                            "arg\"2".to_owned(),
1616                            "arg > \\3".to_owned(),
1617                        ],
1618                        ..Default::default()
1619                    })),
1620                    geom: PaneGeom {
1621                        x: 0,
1622                        y: 40,
1623                        rows: Dimension::fixed(10),
1624                        cols: Dimension::fixed(10),
1625                        stacked: None,
1626                        is_pinned: false,
1627                        logical_position: None,
1628                    },
1629                    ..Default::default()
1630                },
1631                PaneLayoutManifest {
1632                    run: Some(Run::Plugin(RunPluginOrAlias::RunPlugin(
1633                        RunPlugin::from_url("file:/tmp/\"my/cool cwd/plugin.wasm").unwrap(),
1634                    ))),
1635                    geom: PaneGeom {
1636                        x: 0,
1637                        y: 50,
1638                        rows: Dimension::fixed(10),
1639                        cols: Dimension::fixed(10),
1640                        stacked: None,
1641                        is_pinned: false,
1642                        logical_position: None,
1643                    },
1644                    ..Default::default()
1645                },
1646                PaneLayoutManifest {
1647                    run: Some(Run::Plugin(RunPluginOrAlias::RunPlugin(
1648                        RunPlugin::from_url("file:/tmp/\"my/cool cwd/plugin.wasm")
1649                            .unwrap()
1650                            .with_configuration(plugin_configuration),
1651                    ))),
1652                    geom: PaneGeom {
1653                        x: 0,
1654                        y: 60,
1655                        rows: Dimension::fixed(10),
1656                        cols: Dimension::fixed(10),
1657                        stacked: None,
1658                        is_pinned: false,
1659                        logical_position: None,
1660                    },
1661                    ..Default::default()
1662                },
1663                PaneLayoutManifest {
1664                    is_borderless: true,
1665                    geom: PaneGeom {
1666                        x: 0,
1667                        y: 70,
1668                        rows: Dimension::fixed(10),
1669                        cols: Dimension::fixed(10),
1670                        stacked: None,
1671                        is_pinned: false,
1672                        logical_position: None,
1673                    },
1674                    ..Default::default()
1675                },
1676                PaneLayoutManifest {
1677                    title: Some("my cool \\ \"pane_title\"".to_owned()),
1678                    is_focused: true,
1679                    pane_contents: Some("can has pane contents".to_owned()),
1680                    geom: PaneGeom {
1681                        x: 0,
1682                        y: 80,
1683                        rows: Dimension::fixed(10),
1684                        cols: Dimension::fixed(10),
1685                        stacked: None,
1686                        is_pinned: false,
1687                        logical_position: None,
1688                    },
1689                    ..Default::default()
1690                },
1691            ],
1692            ..Default::default()
1693        };
1694        let global_layout_manifest = GlobalLayoutManifest {
1695            tabs: vec![("Tab with \"tiled panes\"".to_owned(), tab_layout_manifest)],
1696            ..Default::default()
1697        };
1698        let kdl = serialize_session_layout(global_layout_manifest).unwrap();
1699        assert_snapshot!(kdl.0);
1700    }
1701    #[test]
1702    fn can_serialize_tab_with_floating_panes() {
1703        use crate::input::command::RunCommand;
1704        use crate::input::layout::RunPlugin;
1705        let mut plugin_configuration = BTreeMap::new();
1706        plugin_configuration.insert("key 1\"\\".to_owned(), "val 1\"\\".to_owned());
1707        plugin_configuration.insert("key 2\"\\".to_owned(), "val 2\"\\".to_owned());
1708        let tab_layout_manifest = TabLayoutManifest {
1709            floating_panes: vec![
1710                PaneLayoutManifest {
1711                    geom: PaneGeom {
1712                        x: 0,
1713                        y: 0,
1714                        rows: Dimension::fixed(10),
1715                        cols: Dimension::fixed(10),
1716                        stacked: None,
1717                        is_pinned: false,
1718                        logical_position: None,
1719                    },
1720                    ..Default::default()
1721                },
1722                PaneLayoutManifest {
1723                    run: Some(Run::Cwd(PathBuf::from("/tmp/\"my/cool cwd"))),
1724                    geom: PaneGeom {
1725                        x: 0,
1726                        y: 10,
1727                        rows: Dimension::fixed(10),
1728                        cols: Dimension::fixed(10),
1729                        stacked: None,
1730                        is_pinned: false,
1731                        logical_position: None,
1732                    },
1733                    ..Default::default()
1734                },
1735                PaneLayoutManifest {
1736                    run: Some(Run::EditFile(
1737                        PathBuf::from("/tmp/\"my/cool cwd/my-file"),
1738                        None,
1739                        None,
1740                    )),
1741                    geom: PaneGeom {
1742                        x: 0,
1743                        y: 20,
1744                        rows: Dimension::fixed(10),
1745                        cols: Dimension::fixed(10),
1746                        stacked: None,
1747                        is_pinned: false,
1748                        logical_position: None,
1749                    },
1750                    ..Default::default()
1751                },
1752                PaneLayoutManifest {
1753                    run: Some(Run::Command(RunCommand {
1754                        command: PathBuf::from("/tmp/\"my/cool cwd/command.sh"),
1755                        ..Default::default()
1756                    })),
1757                    geom: PaneGeom {
1758                        x: 0,
1759                        y: 30,
1760                        rows: Dimension::fixed(10),
1761                        cols: Dimension::fixed(10),
1762                        stacked: None,
1763                        is_pinned: false,
1764                        logical_position: None,
1765                    },
1766                    ..Default::default()
1767                },
1768                PaneLayoutManifest {
1769                    run: Some(Run::Command(RunCommand {
1770                        command: PathBuf::from("/tmp/\"my/cool cwd/command.sh"),
1771                        args: vec![
1772                            "--arg1".to_owned(),
1773                            "arg\"2".to_owned(),
1774                            "arg > \\3".to_owned(),
1775                        ],
1776                        ..Default::default()
1777                    })),
1778                    geom: PaneGeom {
1779                        x: 0,
1780                        y: 40,
1781                        rows: Dimension::fixed(10),
1782                        cols: Dimension::fixed(10),
1783                        stacked: None,
1784                        is_pinned: false,
1785                        logical_position: None,
1786                    },
1787                    ..Default::default()
1788                },
1789                PaneLayoutManifest {
1790                    run: Some(Run::Plugin(RunPluginOrAlias::RunPlugin(
1791                        RunPlugin::from_url("file:/tmp/\"my/cool cwd/plugin.wasm").unwrap(),
1792                    ))),
1793                    geom: PaneGeom {
1794                        x: 0,
1795                        y: 50,
1796                        rows: Dimension::fixed(10),
1797                        cols: Dimension::fixed(10),
1798                        stacked: None,
1799                        is_pinned: false,
1800                        logical_position: None,
1801                    },
1802                    ..Default::default()
1803                },
1804                PaneLayoutManifest {
1805                    run: Some(Run::Plugin(RunPluginOrAlias::RunPlugin(
1806                        RunPlugin::from_url("file:/tmp/\"my/cool cwd/plugin.wasm")
1807                            .unwrap()
1808                            .with_configuration(plugin_configuration),
1809                    ))),
1810                    geom: PaneGeom {
1811                        x: 0,
1812                        y: 60,
1813                        rows: Dimension::fixed(10),
1814                        cols: Dimension::fixed(10),
1815                        stacked: None,
1816                        is_pinned: false,
1817                        logical_position: None,
1818                    },
1819                    ..Default::default()
1820                },
1821                PaneLayoutManifest {
1822                    // note that in this case, `is_borderless` should be ignored because this is a
1823                    // floating pane
1824                    is_borderless: true,
1825                    geom: PaneGeom {
1826                        x: 0,
1827                        y: 70,
1828                        rows: Dimension::fixed(10),
1829                        cols: Dimension::fixed(10),
1830                        stacked: None,
1831                        is_pinned: false,
1832                        logical_position: None,
1833                    },
1834                    ..Default::default()
1835                },
1836                PaneLayoutManifest {
1837                    title: Some("my cool \\ \"pane_title\"".to_owned()),
1838                    is_focused: true,
1839                    pane_contents: Some("can has pane contents".to_owned()),
1840                    geom: PaneGeom {
1841                        x: 0,
1842                        y: 80,
1843                        rows: Dimension::fixed(10),
1844                        cols: Dimension::fixed(10),
1845                        stacked: None,
1846                        is_pinned: false,
1847                        logical_position: None,
1848                    },
1849                    ..Default::default()
1850                },
1851            ],
1852            ..Default::default()
1853        };
1854        let global_layout_manifest = GlobalLayoutManifest {
1855            tabs: vec![(
1856                "Tab with \"floating panes\"".to_owned(),
1857                tab_layout_manifest,
1858            )],
1859            ..Default::default()
1860        };
1861        let kdl = serialize_session_layout(global_layout_manifest).unwrap();
1862        assert_snapshot!(kdl.0);
1863    }
1864    #[test]
1865    fn can_serialize_tab_with_stacked_panes() {
1866        let tab_layout_manifest = TabLayoutManifest {
1867            tiled_panes: vec![
1868                PaneLayoutManifest {
1869                    geom: PaneGeom {
1870                        x: 0,
1871                        y: 0,
1872                        rows: Dimension::fixed(1),
1873                        cols: Dimension::fixed(10),
1874                        stacked: Some(0),
1875                        is_pinned: false,
1876                        logical_position: None,
1877                    },
1878                    ..Default::default()
1879                },
1880                PaneLayoutManifest {
1881                    geom: PaneGeom {
1882                        x: 0,
1883                        y: 1,
1884                        rows: Dimension::fixed(10),
1885                        cols: Dimension::fixed(10),
1886                        stacked: Some(0),
1887                        is_pinned: false,
1888                        logical_position: None,
1889                    },
1890                    ..Default::default()
1891                },
1892                PaneLayoutManifest {
1893                    geom: PaneGeom {
1894                        x: 0,
1895                        y: 11,
1896                        rows: Dimension::fixed(1),
1897                        cols: Dimension::fixed(10),
1898                        stacked: Some(0),
1899                        is_pinned: false,
1900                        logical_position: None,
1901                    },
1902                    ..Default::default()
1903                },
1904            ],
1905            ..Default::default()
1906        };
1907        let global_layout_manifest = GlobalLayoutManifest {
1908            tabs: vec![("Tab with \"stacked panes\"".to_owned(), tab_layout_manifest)],
1909            ..Default::default()
1910        };
1911        let kdl = serialize_session_layout(global_layout_manifest).unwrap();
1912        assert_snapshot!(kdl.0);
1913    }
1914    #[test]
1915    fn can_serialize_tab_with_multiple_stacked_panes_in_the_same_node() {
1916        let tab_layout_manifest = TabLayoutManifest {
1917            tiled_panes: vec![
1918                PaneLayoutManifest {
1919                    geom: PaneGeom {
1920                        x: 0,
1921                        y: 0,
1922                        rows: Dimension::fixed(1),
1923                        cols: Dimension::fixed(10),
1924                        stacked: Some(0),
1925                        is_pinned: false,
1926                        logical_position: None,
1927                    },
1928                    ..Default::default()
1929                },
1930                PaneLayoutManifest {
1931                    geom: PaneGeom {
1932                        x: 0,
1933                        y: 1,
1934                        rows: Dimension::fixed(10),
1935                        cols: Dimension::fixed(10),
1936                        stacked: Some(0),
1937                        is_pinned: false,
1938                        logical_position: None,
1939                    },
1940                    ..Default::default()
1941                },
1942                PaneLayoutManifest {
1943                    geom: PaneGeom {
1944                        x: 0,
1945                        y: 11,
1946                        rows: Dimension::fixed(1),
1947                        cols: Dimension::fixed(10),
1948                        stacked: Some(0),
1949                        is_pinned: false,
1950                        logical_position: None,
1951                    },
1952                    ..Default::default()
1953                },
1954                PaneLayoutManifest {
1955                    geom: PaneGeom {
1956                        x: 0,
1957                        y: 12,
1958                        rows: Dimension::fixed(10),
1959                        cols: Dimension::fixed(10),
1960                        stacked: None,
1961                        is_pinned: false,
1962                        logical_position: None,
1963                    },
1964                    ..Default::default()
1965                },
1966                PaneLayoutManifest {
1967                    geom: PaneGeom {
1968                        x: 0,
1969                        y: 22,
1970                        rows: Dimension::fixed(1),
1971                        cols: Dimension::fixed(10),
1972                        stacked: Some(1),
1973                        is_pinned: false,
1974                        logical_position: None,
1975                    },
1976                    ..Default::default()
1977                },
1978                PaneLayoutManifest {
1979                    geom: PaneGeom {
1980                        x: 0,
1981                        y: 23,
1982                        rows: Dimension::fixed(10),
1983                        cols: Dimension::fixed(10),
1984                        stacked: Some(1),
1985                        is_pinned: false,
1986                        logical_position: None,
1987                    },
1988                    ..Default::default()
1989                },
1990                PaneLayoutManifest {
1991                    geom: PaneGeom {
1992                        x: 0,
1993                        y: 33,
1994                        rows: Dimension::fixed(1),
1995                        cols: Dimension::fixed(10),
1996                        stacked: Some(1),
1997                        is_pinned: false,
1998                        logical_position: None,
1999                    },
2000                    ..Default::default()
2001                },
2002            ],
2003            ..Default::default()
2004        };
2005        let global_layout_manifest = GlobalLayoutManifest {
2006            tabs: vec![("Tab with \"stacked panes\"".to_owned(), tab_layout_manifest)],
2007            ..Default::default()
2008        };
2009        let kdl = serialize_session_layout(global_layout_manifest).unwrap();
2010        assert_snapshot!(kdl.0);
2011    }
2012    #[test]
2013    fn can_serialize_tab_with_multiple_stacks_next_to_eachother() {
2014        let tab_layout_manifest = TabLayoutManifest {
2015            tiled_panes: vec![
2016                PaneLayoutManifest {
2017                    geom: PaneGeom {
2018                        x: 0,
2019                        y: 0,
2020                        rows: Dimension::fixed(1),
2021                        cols: Dimension::fixed(10),
2022                        stacked: Some(0),
2023                        is_pinned: false,
2024                        logical_position: None,
2025                    },
2026                    ..Default::default()
2027                },
2028                PaneLayoutManifest {
2029                    geom: PaneGeom {
2030                        x: 0,
2031                        y: 1,
2032                        rows: Dimension::fixed(10),
2033                        cols: Dimension::fixed(10),
2034                        stacked: Some(0),
2035                        is_pinned: false,
2036                        logical_position: None,
2037                    },
2038                    ..Default::default()
2039                },
2040                PaneLayoutManifest {
2041                    geom: PaneGeom {
2042                        x: 0,
2043                        y: 11,
2044                        rows: Dimension::fixed(1),
2045                        cols: Dimension::fixed(10),
2046                        stacked: Some(0),
2047                        is_pinned: false,
2048                        logical_position: None,
2049                    },
2050                    ..Default::default()
2051                },
2052                PaneLayoutManifest {
2053                    geom: PaneGeom {
2054                        x: 0,
2055                        y: 12,
2056                        rows: Dimension::fixed(10),
2057                        cols: Dimension::fixed(10),
2058                        stacked: None,
2059                        is_pinned: false,
2060                        logical_position: None,
2061                    },
2062                    ..Default::default()
2063                },
2064                PaneLayoutManifest {
2065                    geom: PaneGeom {
2066                        x: 0,
2067                        y: 22,
2068                        rows: Dimension::fixed(1),
2069                        cols: Dimension::fixed(10),
2070                        stacked: Some(1),
2071                        is_pinned: false,
2072                        logical_position: None,
2073                    },
2074                    ..Default::default()
2075                },
2076                PaneLayoutManifest {
2077                    geom: PaneGeom {
2078                        x: 0,
2079                        y: 23,
2080                        rows: Dimension::fixed(10),
2081                        cols: Dimension::fixed(10),
2082                        stacked: Some(1),
2083                        is_pinned: false,
2084                        logical_position: None,
2085                    },
2086                    ..Default::default()
2087                },
2088                PaneLayoutManifest {
2089                    geom: PaneGeom {
2090                        x: 0,
2091                        y: 33,
2092                        rows: Dimension::fixed(1),
2093                        cols: Dimension::fixed(10),
2094                        stacked: Some(1),
2095                        is_pinned: false,
2096                        logical_position: None,
2097                    },
2098                    ..Default::default()
2099                },
2100                PaneLayoutManifest {
2101                    geom: PaneGeom {
2102                        x: 10,
2103                        y: 0,
2104                        rows: Dimension::fixed(1),
2105                        cols: Dimension::fixed(10),
2106                        stacked: Some(2),
2107                        is_pinned: false,
2108                        logical_position: None,
2109                    },
2110                    ..Default::default()
2111                },
2112                PaneLayoutManifest {
2113                    geom: PaneGeom {
2114                        x: 10,
2115                        y: 1,
2116                        rows: Dimension::fixed(10),
2117                        cols: Dimension::fixed(10),
2118                        stacked: Some(2),
2119                        is_pinned: false,
2120                        logical_position: None,
2121                    },
2122                    ..Default::default()
2123                },
2124                PaneLayoutManifest {
2125                    geom: PaneGeom {
2126                        x: 10,
2127                        y: 11,
2128                        rows: Dimension::fixed(1),
2129                        cols: Dimension::fixed(10),
2130                        stacked: Some(2),
2131                        is_pinned: false,
2132                        logical_position: None,
2133                    },
2134                    ..Default::default()
2135                },
2136                PaneLayoutManifest {
2137                    geom: PaneGeom {
2138                        x: 10,
2139                        y: 12,
2140                        rows: Dimension::fixed(10),
2141                        cols: Dimension::fixed(10),
2142                        stacked: None,
2143                        is_pinned: false,
2144                        logical_position: None,
2145                    },
2146                    ..Default::default()
2147                },
2148                PaneLayoutManifest {
2149                    geom: PaneGeom {
2150                        x: 10,
2151                        y: 22,
2152                        rows: Dimension::fixed(1),
2153                        cols: Dimension::fixed(10),
2154                        stacked: Some(3),
2155                        is_pinned: false,
2156                        logical_position: None,
2157                    },
2158                    ..Default::default()
2159                },
2160                PaneLayoutManifest {
2161                    geom: PaneGeom {
2162                        x: 10,
2163                        y: 23,
2164                        rows: Dimension::fixed(10),
2165                        cols: Dimension::fixed(10),
2166                        stacked: Some(3),
2167                        is_pinned: false,
2168                        logical_position: None,
2169                    },
2170                    ..Default::default()
2171                },
2172                PaneLayoutManifest {
2173                    geom: PaneGeom {
2174                        x: 10,
2175                        y: 33,
2176                        rows: Dimension::fixed(1),
2177                        cols: Dimension::fixed(10),
2178                        stacked: Some(3),
2179                        is_pinned: false,
2180                        logical_position: None,
2181                    },
2182                    ..Default::default()
2183                },
2184            ],
2185            ..Default::default()
2186        };
2187        let global_layout_manifest = GlobalLayoutManifest {
2188            tabs: vec![("Tab with \"stacked panes\"".to_owned(), tab_layout_manifest)],
2189            ..Default::default()
2190        };
2191        let kdl = serialize_session_layout(global_layout_manifest).unwrap();
2192        assert_snapshot!(kdl.0);
2193    }
2194    #[test]
2195    fn can_serialize_multiple_tabs() {
2196        let tab_1_layout_manifest = TabLayoutManifest {
2197            tiled_panes: vec![PaneLayoutManifest {
2198                geom: PaneGeom {
2199                    x: 0,
2200                    y: 0,
2201                    rows: Dimension::percent(100.0),
2202                    cols: Dimension::percent(100.0),
2203                    stacked: None,
2204                    is_pinned: false,
2205                    logical_position: None,
2206                },
2207                ..Default::default()
2208            }],
2209            ..Default::default()
2210        };
2211        let tab_2_layout_manifest = TabLayoutManifest {
2212            tiled_panes: vec![
2213                PaneLayoutManifest {
2214                    geom: PaneGeom {
2215                        x: 0,
2216                        y: 0,
2217                        rows: Dimension::fixed(10),
2218                        cols: Dimension::fixed(10),
2219                        stacked: None,
2220                        is_pinned: false,
2221                        logical_position: None,
2222                    },
2223                    ..Default::default()
2224                },
2225                PaneLayoutManifest {
2226                    geom: PaneGeom {
2227                        x: 10,
2228                        y: 0,
2229                        rows: Dimension::fixed(10),
2230                        cols: Dimension::fixed(10),
2231                        stacked: None,
2232                        is_pinned: false,
2233                        logical_position: None,
2234                    },
2235                    ..Default::default()
2236                },
2237            ],
2238            ..Default::default()
2239        };
2240        let global_layout_manifest = GlobalLayoutManifest {
2241            tabs: vec![
2242                ("First tab".to_owned(), tab_1_layout_manifest),
2243                ("Second tab".to_owned(), tab_2_layout_manifest),
2244            ],
2245            ..Default::default()
2246        };
2247        let kdl = serialize_session_layout(global_layout_manifest).unwrap();
2248        assert_snapshot!(kdl.0);
2249    }
2250    #[test]
2251    fn can_serialize_new_tab_template() {
2252        let tiled_panes_layout = TiledPaneLayout {
2253            children: vec![TiledPaneLayout::default(), TiledPaneLayout::default()],
2254            ..Default::default()
2255        };
2256
2257        let floating_panes_layout = vec![
2258            FloatingPaneLayout::default(),
2259            FloatingPaneLayout::default(),
2260            FloatingPaneLayout::default(),
2261        ];
2262        let mut default_layout = Layout::default();
2263        default_layout.template = Some((tiled_panes_layout, floating_panes_layout));
2264        let default_layout = Box::new(default_layout);
2265        let global_layout_manifest = GlobalLayoutManifest {
2266            default_layout,
2267            ..Default::default()
2268        };
2269        let kdl = serialize_session_layout(global_layout_manifest).unwrap();
2270        assert_snapshot!(kdl.0);
2271    }
2272    #[test]
2273    fn can_serialize_swap_tiled_panes() {
2274        let tiled_panes_layout = TiledPaneLayout {
2275            children: vec![TiledPaneLayout::default(), TiledPaneLayout::default()],
2276            ..Default::default()
2277        };
2278        let mut default_layout = Layout::default();
2279        let mut swap_tiled_layout_1 = BTreeMap::new();
2280        let mut swap_tiled_layout_2 = BTreeMap::new();
2281        swap_tiled_layout_1.insert(LayoutConstraint::MaxPanes(1), tiled_panes_layout.clone());
2282        swap_tiled_layout_1.insert(LayoutConstraint::MinPanes(1), tiled_panes_layout.clone());
2283        swap_tiled_layout_1.insert(LayoutConstraint::ExactPanes(1), tiled_panes_layout.clone());
2284        swap_tiled_layout_1.insert(LayoutConstraint::NoConstraint, tiled_panes_layout.clone());
2285        swap_tiled_layout_2.insert(LayoutConstraint::MaxPanes(2), tiled_panes_layout.clone());
2286        swap_tiled_layout_2.insert(LayoutConstraint::MinPanes(2), tiled_panes_layout.clone());
2287        swap_tiled_layout_2.insert(LayoutConstraint::ExactPanes(2), tiled_panes_layout.clone());
2288        swap_tiled_layout_2.insert(LayoutConstraint::NoConstraint, tiled_panes_layout.clone());
2289
2290        let swap_tiled_layouts = vec![
2291            (swap_tiled_layout_1, None),
2292            (swap_tiled_layout_2, Some("swap_tiled_layout_2".to_owned())),
2293        ];
2294        default_layout.swap_tiled_layouts = swap_tiled_layouts;
2295        let default_layout = Box::new(default_layout);
2296        let global_layout_manifest = GlobalLayoutManifest {
2297            default_layout,
2298            ..Default::default()
2299        };
2300        let kdl = serialize_session_layout(global_layout_manifest).unwrap();
2301        assert_snapshot!(kdl.0);
2302    }
2303    #[test]
2304    fn can_serialize_swap_floating_panes() {
2305        let floating_panes_layout = vec![
2306            FloatingPaneLayout::default(),
2307            FloatingPaneLayout::default(),
2308            FloatingPaneLayout::default(),
2309        ];
2310        let mut default_layout = Layout::default();
2311        let mut swap_floating_layout_1 = BTreeMap::new();
2312        let mut swap_floating_layout_2 = BTreeMap::new();
2313        swap_floating_layout_1.insert(LayoutConstraint::MaxPanes(1), floating_panes_layout.clone());
2314        swap_floating_layout_1.insert(LayoutConstraint::MinPanes(1), floating_panes_layout.clone());
2315        swap_floating_layout_1.insert(
2316            LayoutConstraint::ExactPanes(1),
2317            floating_panes_layout.clone(),
2318        );
2319        swap_floating_layout_1.insert(
2320            LayoutConstraint::NoConstraint,
2321            floating_panes_layout.clone(),
2322        );
2323        swap_floating_layout_2.insert(LayoutConstraint::MaxPanes(2), floating_panes_layout.clone());
2324        swap_floating_layout_2.insert(LayoutConstraint::MinPanes(2), floating_panes_layout.clone());
2325        swap_floating_layout_2.insert(
2326            LayoutConstraint::ExactPanes(2),
2327            floating_panes_layout.clone(),
2328        );
2329        swap_floating_layout_2.insert(
2330            LayoutConstraint::NoConstraint,
2331            floating_panes_layout.clone(),
2332        );
2333
2334        let swap_floating_layouts = vec![
2335            (swap_floating_layout_1, None),
2336            (
2337                swap_floating_layout_2,
2338                Some("swap_floating_layout_2".to_owned()),
2339            ),
2340        ];
2341        default_layout.swap_floating_layouts = swap_floating_layouts;
2342        let default_layout = Box::new(default_layout);
2343        let global_layout_manifest = GlobalLayoutManifest {
2344            default_layout,
2345            ..Default::default()
2346        };
2347        let kdl = serialize_session_layout(global_layout_manifest).unwrap();
2348        assert_snapshot!(kdl.0);
2349    }
2350
2351    // utility functions
2352    fn parse_panegeom_from_json(data_str: &str) -> PaneGeom {
2353        //
2354        // Expects this input
2355        //
2356        //  r#"{ "x": 0, "y": 1, "rows": { "constraint": "Percent(100.0)", "inner": 43 }, "cols": { "constraint": "Percent(100.0)", "inner": 211 }, "is_stacked": false }"#,
2357        //
2358        let data: HashMap<String, Value> = serde_json::from_str(data_str).unwrap();
2359        PaneGeom {
2360            x: data["x"].to_string().parse().unwrap(),
2361            y: data["y"].to_string().parse().unwrap(),
2362            rows: get_dim(&data["rows"]),
2363            cols: get_dim(&data["cols"]),
2364            stacked: None,
2365            is_pinned: false,
2366            logical_position: None,
2367        }
2368    }
2369
2370    fn get_dim(dim_hm: &Value) -> Dimension {
2371        let constr_str = dim_hm["constraint"].to_string();
2372        let dim = if constr_str.contains("Fixed") {
2373            let value = &constr_str[7..constr_str.len() - 2];
2374            Dimension::fixed(value.parse().unwrap())
2375        } else if constr_str.contains("Percent") {
2376            let value = &constr_str[9..constr_str.len() - 2];
2377            let mut dim = Dimension::percent(value.parse().unwrap());
2378            dim.set_inner(dim_hm["inner"].to_string().parse().unwrap());
2379            dim
2380        } else {
2381            panic!("Constraint is nor a percent nor fixed");
2382        };
2383        dim
2384    }
2385}