Skip to main content

zellij_utils/input/
layout.rs

1//! The layout system.
2//  Layouts have been moved from [`zellij-server`] to
3//  [`zellij-utils`] in order to provide more helpful
4//  error messages to the user until a more general
5//  logging system is in place.
6//  In case there is a logging system in place evaluate,
7//  if [`zellij-utils`], or [`zellij-server`] is a proper
8//  place.
9//  If plugins should be able to depend on the layout system
10//  then [`zellij-utils`] could be a proper place.
11#[cfg(not(target_family = "wasm"))]
12use crate::downloader::Downloader;
13use crate::{
14    data::{Direction, LayoutInfo, LayoutMetadata, LayoutParsingError, LayoutWithError},
15    home::{default_layout_dir, find_default_config_dir},
16    input::{
17        command::RunCommand,
18        config::{Config, ConfigError},
19    },
20    pane_size::{Constraint, Dimension, PaneGeom},
21    setup::{self},
22};
23
24use std::cmp::Ordering;
25use std::fmt::{Display, Formatter};
26use std::str::FromStr;
27
28use super::plugins::{PluginAliases, PluginTag, PluginsConfigError};
29use serde::{Deserialize, Serialize};
30use std::collections::BTreeMap;
31use std::vec::Vec;
32use std::{
33    fmt,
34    ops::Not,
35    path::{Path, PathBuf},
36};
37use std::{fs::File, io::prelude::*};
38use url::Url;
39
40#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone, Copy)]
41pub enum SplitDirection {
42    Horizontal,
43    Vertical,
44}
45
46impl Not for SplitDirection {
47    type Output = Self;
48
49    fn not(self) -> Self::Output {
50        match self {
51            SplitDirection::Horizontal => SplitDirection::Vertical,
52            SplitDirection::Vertical => SplitDirection::Horizontal,
53        }
54    }
55}
56
57impl From<Direction> for SplitDirection {
58    fn from(direction: Direction) -> Self {
59        match direction {
60            Direction::Left | Direction::Right => SplitDirection::Horizontal,
61            Direction::Down | Direction::Up => SplitDirection::Vertical,
62        }
63    }
64}
65
66#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
67pub enum SplitSize {
68    #[serde(alias = "percent")]
69    Percent(usize), // 1 to 100
70    #[serde(alias = "fixed")]
71    Fixed(usize), // An absolute number of columns or rows
72}
73
74impl From<PercentOrFixed> for SplitSize {
75    fn from(pof: PercentOrFixed) -> Self {
76        match pof {
77            PercentOrFixed::Percent(p) => SplitSize::Percent(p),
78            PercentOrFixed::Fixed(f) => SplitSize::Fixed(f),
79        }
80    }
81}
82
83impl From<SplitSize> for PercentOrFixed {
84    fn from(ss: SplitSize) -> Self {
85        match ss {
86            SplitSize::Percent(p) => PercentOrFixed::Percent(p),
87            SplitSize::Fixed(f) => PercentOrFixed::Fixed(f),
88        }
89    }
90}
91
92impl SplitSize {
93    pub fn to_fixed(&self, full_size: usize) -> usize {
94        match self {
95            SplitSize::Percent(percent) => {
96                ((*percent as f64 / 100.0) * full_size as f64).floor() as usize
97            },
98            SplitSize::Fixed(fixed) => *fixed,
99        }
100    }
101}
102
103#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)]
104pub enum RunPluginOrAlias {
105    RunPlugin(RunPlugin),
106    Alias(PluginAlias),
107}
108
109impl Default for RunPluginOrAlias {
110    fn default() -> Self {
111        RunPluginOrAlias::RunPlugin(Default::default())
112    }
113}
114
115impl RunPluginOrAlias {
116    pub fn location_string(&self) -> String {
117        match self {
118            RunPluginOrAlias::RunPlugin(run_plugin) => run_plugin.location.display(),
119            RunPluginOrAlias::Alias(plugin_alias) => plugin_alias.name.clone(),
120        }
121    }
122    pub fn populate_run_plugin_if_needed(&mut self, plugin_aliases: &PluginAliases) {
123        if let RunPluginOrAlias::Alias(run_plugin_alias) = self {
124            if run_plugin_alias.run_plugin.is_some() {
125                log::warn!("Overriding plugin alias");
126            }
127            let merged_run_plugin = plugin_aliases
128                .aliases
129                .get(run_plugin_alias.name.as_str())
130                .map(|r| {
131                    let mut merged_run_plugin = r.clone().merge_configuration(
132                        &run_plugin_alias
133                            .configuration
134                            .as_ref()
135                            .map(|c| c.inner().clone()),
136                    );
137                    // if the alias has its own cwd, it should always override the alias
138                    // value's cwd
139                    if run_plugin_alias.initial_cwd.is_some() {
140                        merged_run_plugin.initial_cwd = run_plugin_alias.initial_cwd.clone();
141                    }
142                    merged_run_plugin
143                });
144            run_plugin_alias.run_plugin = merged_run_plugin;
145        }
146    }
147    pub fn get_run_plugin(&self) -> Option<RunPlugin> {
148        match self {
149            RunPluginOrAlias::RunPlugin(run_plugin) => Some(run_plugin.clone()),
150            RunPluginOrAlias::Alias(plugin_alias) => plugin_alias.run_plugin.clone(),
151        }
152    }
153    pub fn get_configuration(&self) -> Option<PluginUserConfiguration> {
154        self.get_run_plugin().map(|r| r.configuration.clone())
155    }
156    pub fn get_initial_cwd(&self) -> Option<PathBuf> {
157        self.get_run_plugin().and_then(|r| r.initial_cwd.clone())
158    }
159    pub fn from_url(
160        url: &str,
161        configuration: &Option<BTreeMap<String, String>>,
162        alias_dict: Option<&PluginAliases>,
163        cwd: Option<PathBuf>,
164    ) -> Result<Self, String> {
165        match RunPluginLocation::parse(&url, cwd) {
166            Ok(location) => Ok(RunPluginOrAlias::RunPlugin(RunPlugin {
167                _allow_exec_host_cmd: false,
168                location,
169                configuration: configuration
170                    .as_ref()
171                    .map(|c| PluginUserConfiguration::new(c.clone()))
172                    .unwrap_or_default(),
173                ..Default::default()
174            })),
175            Err(PluginsConfigError::InvalidUrlScheme(_))
176            | Err(PluginsConfigError::InvalidUrl(..)) => {
177                let mut plugin_alias = PluginAlias::new(&url, configuration, None);
178                if let Some(alias_dict) = alias_dict {
179                    plugin_alias.run_plugin = alias_dict
180                        .aliases
181                        .get(url)
182                        .map(|r| r.clone().merge_configuration(configuration));
183                }
184                Ok(RunPluginOrAlias::Alias(plugin_alias))
185            },
186            Err(e) => {
187                return Err(format!("Failed to parse plugin location {url}: {}", e));
188            },
189        }
190    }
191    pub fn is_equivalent_to_run(&self, run: &Option<Run>) -> bool {
192        match (self, run) {
193            (
194                RunPluginOrAlias::Alias(self_alias),
195                Some(Run::Plugin(RunPluginOrAlias::Alias(run_alias))),
196            ) => {
197                self_alias.name == run_alias.name
198                    && self_alias
199                        .configuration
200                        .as_ref()
201                        // we do the is_empty() checks because an empty configuration is the same as no
202                        // configuration (i.e. None)
203                        .and_then(|c| if c.inner().is_empty() { None } else { Some(c) })
204                        == run_alias.configuration.as_ref().and_then(|c| {
205                            let mut to_compare = c.inner().clone();
206                            // caller_cwd is a special attribute given to alias and should not be
207                            // considered when weighing configuration equivalency
208                            to_compare.remove("caller_cwd");
209                            if to_compare.is_empty() {
210                                None
211                            } else {
212                                Some(c)
213                            }
214                        })
215            },
216            (
217                RunPluginOrAlias::Alias(self_alias),
218                Some(Run::Plugin(RunPluginOrAlias::RunPlugin(other_run_plugin))),
219            ) => self_alias.run_plugin.as_ref() == Some(other_run_plugin),
220            (
221                RunPluginOrAlias::RunPlugin(self_run_plugin),
222                Some(Run::Plugin(RunPluginOrAlias::RunPlugin(other_run_plugin))),
223            ) => self_run_plugin == other_run_plugin,
224            _ => false,
225        }
226    }
227    pub fn with_initial_cwd(mut self, initial_cwd: Option<PathBuf>) -> Self {
228        match self {
229            RunPluginOrAlias::RunPlugin(ref mut run_plugin) => {
230                run_plugin.initial_cwd = initial_cwd;
231            },
232            RunPluginOrAlias::Alias(ref mut alias) => {
233                alias.initial_cwd = initial_cwd;
234            },
235        }
236        self
237    }
238    pub fn add_initial_cwd(&mut self, initial_cwd: &PathBuf) {
239        match self {
240            RunPluginOrAlias::RunPlugin(ref mut run_plugin) => {
241                run_plugin.initial_cwd = Some(initial_cwd.clone());
242            },
243            RunPluginOrAlias::Alias(ref mut alias) => {
244                alias.initial_cwd = Some(initial_cwd.clone());
245            },
246        }
247    }
248    pub fn is_builtin_plugin(&self) -> bool {
249        self.get_run_plugin()
250            .map(|r| matches!(r.location, RunPluginLocation::Zellij(_)))
251            .unwrap_or(false)
252    }
253}
254
255#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
256pub enum Run {
257    #[serde(rename = "plugin")]
258    Plugin(RunPluginOrAlias),
259    #[serde(rename = "command")]
260    Command(RunCommand),
261    EditFile(PathBuf, Option<usize>, Option<PathBuf>),
262    Cwd(PathBuf),
263}
264
265impl Run {
266    pub fn merge(base: &Option<Run>, other: &Option<Run>) -> Option<Run> {
267        // This method is necessary to merge between pane_templates and their consumers
268        // TODO: reconsider the way we parse command/edit/plugin pane_templates from layouts to prevent this
269        // madness
270        // TODO: handle Plugin variants once there's a need
271        match (base, other) {
272            (Some(Run::Command(base_run_command)), Some(Run::Command(other_run_command))) => {
273                let mut merged = other_run_command.clone();
274                if merged.cwd.is_none() && base_run_command.cwd.is_some() {
275                    merged.cwd = base_run_command.cwd.clone();
276                }
277                if merged.args.is_empty() && !base_run_command.args.is_empty() {
278                    merged.args = base_run_command.args.clone();
279                }
280                Some(Run::Command(merged))
281            },
282            (Some(Run::Command(base_run_command)), Some(Run::Cwd(other_cwd))) => {
283                let mut merged = base_run_command.clone();
284                merged.cwd = Some(other_cwd.clone());
285                Some(Run::Command(merged))
286            },
287            (Some(Run::Cwd(base_cwd)), Some(Run::Command(other_command))) => {
288                let mut merged = other_command.clone();
289                if merged.cwd.is_none() {
290                    merged.cwd = Some(base_cwd.clone());
291                }
292                Some(Run::Command(merged))
293            },
294            (
295                Some(Run::Command(base_run_command)),
296                Some(Run::EditFile(file_to_edit, line_number, edit_cwd)),
297            ) => match &base_run_command.cwd {
298                Some(cwd) => Some(Run::EditFile(
299                    cwd.join(&file_to_edit),
300                    *line_number,
301                    Some(cwd.join(edit_cwd.clone().unwrap_or_default())),
302                )),
303                None => Some(Run::EditFile(
304                    file_to_edit.clone(),
305                    *line_number,
306                    edit_cwd.clone(),
307                )),
308            },
309            (Some(Run::Cwd(cwd)), Some(Run::EditFile(file_to_edit, line_number, edit_cwd))) => {
310                let cwd = edit_cwd.clone().unwrap_or(cwd.clone());
311                Some(Run::EditFile(
312                    cwd.join(&file_to_edit),
313                    *line_number,
314                    Some(cwd),
315                ))
316            },
317            (Some(_base), Some(other)) => Some(other.clone()),
318            (Some(base), _) => Some(base.clone()),
319            (None, Some(other)) => Some(other.clone()),
320            (None, None) => None,
321        }
322    }
323    pub fn add_cwd(&mut self, cwd: &PathBuf) {
324        match self {
325            Run::Command(run_command) => match run_command.cwd.as_mut() {
326                Some(run_cwd) => {
327                    *run_cwd = cwd.join(&run_cwd);
328                },
329                None => {
330                    run_command.cwd = Some(cwd.clone());
331                },
332            },
333            Run::EditFile(path_to_file, _line_number, edit_cwd) => {
334                match edit_cwd.as_mut() {
335                    Some(edit_cwd) => {
336                        *edit_cwd = cwd.join(&edit_cwd);
337                    },
338                    None => {
339                        let _ = edit_cwd.insert(cwd.clone());
340                    },
341                };
342                *path_to_file = cwd.join(&path_to_file);
343            },
344            Run::Cwd(path) => {
345                *path = cwd.join(&path);
346            },
347            Run::Plugin(run_plugin_or_alias) => {
348                run_plugin_or_alias.add_initial_cwd(&cwd);
349            },
350        }
351    }
352    pub fn add_args(&mut self, args: Option<Vec<String>>) {
353        // overrides the args of a Run::Command if they are Some
354        // and not empty
355        if let Some(args) = args {
356            if let Run::Command(run_command) = self {
357                if !args.is_empty() {
358                    run_command.args = args.clone();
359                }
360            }
361        }
362    }
363    pub fn add_close_on_exit(&mut self, close_on_exit: Option<bool>) {
364        // overrides the hold_on_close of a Run::Command if it is Some
365        // and not empty
366        if let Some(close_on_exit) = close_on_exit {
367            if let Run::Command(run_command) = self {
368                run_command.hold_on_close = !close_on_exit;
369            }
370        }
371    }
372    pub fn add_start_suspended(&mut self, start_suspended: Option<bool>) {
373        // overrides the hold_on_start of a Run::Command if they are Some
374        // and not empty
375        if let Some(start_suspended) = start_suspended {
376            if let Run::Command(run_command) = self {
377                run_command.hold_on_start = start_suspended;
378            }
379        }
380    }
381    pub fn is_same_category(first: &Option<Run>, second: &Option<Run>) -> bool {
382        match (first, second) {
383            (Some(Run::Plugin(..)), Some(Run::Plugin(..))) => true,
384            (Some(Run::Command(..)), Some(Run::Command(..))) => true,
385            (Some(Run::EditFile(..)), Some(Run::EditFile(..))) => true,
386            (Some(Run::Cwd(..)), Some(Run::Cwd(..))) => true,
387            _ => false,
388        }
389    }
390    pub fn is_terminal(run: &Option<Run>) -> bool {
391        match run {
392            Some(Run::Command(..)) | Some(Run::EditFile(..)) | Some(Run::Cwd(..)) | None => true,
393            _ => false,
394        }
395    }
396    pub fn get_cwd(&self) -> Option<PathBuf> {
397        match self {
398            Run::Plugin(_) => None, // TBD
399            Run::Command(run_command) => run_command.cwd.clone(),
400            Run::EditFile(_file, _line_num, cwd) => cwd.clone(),
401            Run::Cwd(cwd) => Some(cwd.clone()),
402        }
403    }
404    pub fn get_run_plugin(&self) -> Option<RunPlugin> {
405        match self {
406            Run::Plugin(RunPluginOrAlias::RunPlugin(run_plugin)) => Some(run_plugin.clone()),
407            Run::Plugin(RunPluginOrAlias::Alias(plugin_alias)) => {
408                plugin_alias.run_plugin.as_ref().map(|r| r.clone())
409            },
410            _ => None,
411        }
412    }
413    pub fn populate_run_plugin_if_needed(&mut self, alias_dict: &PluginAliases) {
414        match self {
415            Run::Plugin(run_plugin_alias) => {
416                run_plugin_alias.populate_run_plugin_if_needed(alias_dict)
417            },
418            _ => {},
419        }
420    }
421}
422
423#[allow(clippy::derive_hash_xor_eq)]
424#[derive(Debug, Serialize, Deserialize, Clone, Hash, Default)]
425pub struct RunPlugin {
426    #[serde(default)]
427    pub _allow_exec_host_cmd: bool,
428    pub location: RunPluginLocation,
429    pub configuration: PluginUserConfiguration,
430    pub initial_cwd: Option<PathBuf>,
431}
432
433impl RunPlugin {
434    pub fn from_url(url: &str) -> Result<Self, PluginsConfigError> {
435        let location = RunPluginLocation::parse(url, None)?;
436        Ok(RunPlugin {
437            location,
438            ..Default::default()
439        })
440    }
441    pub fn with_configuration(mut self, configuration: BTreeMap<String, String>) -> Self {
442        self.configuration = PluginUserConfiguration::new(configuration);
443        self
444    }
445    pub fn with_initial_cwd(mut self, initial_cwd: Option<PathBuf>) -> Self {
446        self.initial_cwd = initial_cwd;
447        self
448    }
449    pub fn merge_configuration(mut self, configuration: &Option<BTreeMap<String, String>>) -> Self {
450        if let Some(configuration) = configuration {
451            self.configuration.merge(configuration);
452        }
453        self
454    }
455}
456
457#[derive(Debug, Serialize, Deserialize, Clone, Default, Eq)]
458pub struct PluginAlias {
459    pub name: String,
460    pub configuration: Option<PluginUserConfiguration>,
461    pub initial_cwd: Option<PathBuf>,
462    pub run_plugin: Option<RunPlugin>,
463}
464
465impl PartialEq for PluginAlias {
466    fn eq(&self, other: &Self) -> bool {
467        // NOTE: Keep this in sync with what the `Hash` trait impl does.
468        self.name == other.name && self.configuration == other.configuration
469    }
470}
471
472impl std::hash::Hash for PluginAlias {
473    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
474        // NOTE: Keep this in sync with what the `PartiqlEq` trait impl does.
475        self.name.hash(state);
476        self.configuration.hash(state);
477    }
478}
479
480impl PluginAlias {
481    pub fn new(
482        name: &str,
483        configuration: &Option<BTreeMap<String, String>>,
484        initial_cwd: Option<PathBuf>,
485    ) -> Self {
486        PluginAlias {
487            name: name.to_owned(),
488            configuration: configuration
489                .as_ref()
490                .map(|c| PluginUserConfiguration::new(c.clone())),
491            initial_cwd,
492            ..Default::default()
493        }
494    }
495    pub fn set_caller_cwd_if_not_set(&mut self, caller_cwd: Option<PathBuf>) {
496        // we do this only for an alias because in all other cases this will be handled by the
497        // "cwd" configuration key above
498        // for an alias we might have cases where the cwd is defined on the alias but we still
499        // want to pass the "caller" cwd for the plugin the alias resolves into (eg. a
500        // filepicker that has access to the whole filesystem but wants to start in a specific
501        // folder)
502        if let Some(caller_cwd) = caller_cwd {
503            if self
504                .configuration
505                .as_ref()
506                .map(|c| c.inner().get("caller_cwd").is_none())
507                .unwrap_or(true)
508            {
509                let configuration = self
510                    .configuration
511                    .get_or_insert_with(|| PluginUserConfiguration::new(BTreeMap::new()));
512                configuration.insert("caller_cwd", caller_cwd.display().to_string());
513            }
514        }
515    }
516}
517
518#[allow(clippy::derive_hash_xor_eq)]
519impl PartialEq for RunPlugin {
520    fn eq(&self, other: &Self) -> bool {
521        // TODO: normalize paths here if the location is a file so that relative/absolute paths
522        // will work properly
523        (&self.location, &self.configuration) == (&other.location, &other.configuration)
524    }
525}
526impl Eq for RunPlugin {}
527
528#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
529pub struct PluginUserConfiguration(BTreeMap<String, String>);
530
531impl PluginUserConfiguration {
532    pub fn new(mut configuration: BTreeMap<String, String>) -> Self {
533        // reserved words
534        configuration.remove("hold_on_close");
535        configuration.remove("hold_on_start");
536        configuration.remove("cwd");
537        configuration.remove("name");
538        configuration.remove("direction");
539        configuration.remove("floating");
540        configuration.remove("move_to_focused_tab");
541        configuration.remove("launch_new");
542        configuration.remove("payload");
543        configuration.remove("skip_cache");
544        configuration.remove("title");
545        configuration.remove("in_place");
546        configuration.remove("skip_plugin_cache");
547
548        PluginUserConfiguration(configuration)
549    }
550    pub fn inner(&self) -> &BTreeMap<String, String> {
551        &self.0
552    }
553    pub fn insert(&mut self, config_key: impl Into<String>, config_value: impl Into<String>) {
554        self.0.insert(config_key.into(), config_value.into());
555    }
556    pub fn merge(&mut self, other_config: &BTreeMap<String, String>) {
557        for (key, value) in other_config {
558            self.0.insert(key.to_owned(), value.clone());
559        }
560    }
561}
562
563impl FromStr for PluginUserConfiguration {
564    type Err = &'static str;
565
566    fn from_str(s: &str) -> Result<Self, Self::Err> {
567        let mut ret = BTreeMap::new();
568        let configs = s.split(',');
569        for config in configs {
570            let mut config = config.split('=');
571            let key = config.next().ok_or("invalid configuration key")?.to_owned();
572            let value = config.map(|c| c.to_owned()).collect::<Vec<_>>().join("=");
573            ret.insert(key, value);
574        }
575        Ok(PluginUserConfiguration(ret))
576    }
577}
578
579#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)]
580pub enum RunPluginLocation {
581    File(PathBuf),
582    Zellij(PluginTag),
583    Remote(String),
584}
585
586impl Default for RunPluginLocation {
587    fn default() -> Self {
588        RunPluginLocation::File(Default::default())
589    }
590}
591
592impl RunPluginLocation {
593    pub fn parse(location: &str, cwd: Option<PathBuf>) -> Result<Self, PluginsConfigError> {
594        let url = Url::parse(location)?;
595
596        let decoded_path = percent_encoding::percent_decode_str(url.path()).decode_utf8_lossy();
597
598        match url.scheme() {
599            "zellij" => Ok(Self::Zellij(PluginTag::new(decoded_path))),
600            "file" => {
601                let path = if location.starts_with("file:/") {
602                    // Path is absolute, its safe to use URL path.
603                    //
604                    // This is the case if the scheme and : delimiter are followed by a / slash
605                    PathBuf::from(decoded_path.as_ref())
606                } else if location.starts_with("file:~") {
607                    // Unwrap is safe here since location is a valid URL
608                    PathBuf::from(location.strip_prefix("file:").unwrap())
609                } else {
610                    // URL dep doesn't handle relative paths with `file` schema properly,
611                    // it always makes them absolute. Use raw location string instead.
612                    //
613                    // Unwrap is safe here since location is a valid URL
614                    let stripped = location.strip_prefix("file:").unwrap();
615                    match cwd {
616                        Some(cwd) => cwd.join(stripped),
617                        None => PathBuf::from(stripped),
618                    }
619                };
620                let path = match shellexpand::full(&path.to_string_lossy().to_string()) {
621                    Ok(s) => PathBuf::from(s.as_ref()),
622                    Err(e) => {
623                        log::error!("Failed to shell expand plugin path: {}", e);
624                        path
625                    },
626                };
627                Ok(Self::File(path))
628            },
629            "https" | "http" => Ok(Self::Remote(url.as_str().to_owned())),
630            _ => Err(PluginsConfigError::InvalidUrlScheme(url)),
631        }
632    }
633    pub fn display(&self) -> String {
634        match self {
635            RunPluginLocation::File(pathbuf) => format!("file:{}", pathbuf.display()),
636            RunPluginLocation::Zellij(plugin_tag) => format!("zellij:{}", plugin_tag),
637            RunPluginLocation::Remote(url) => String::from(url),
638        }
639    }
640}
641
642impl From<&RunPluginLocation> for Url {
643    fn from(location: &RunPluginLocation) -> Self {
644        let url = match location {
645            RunPluginLocation::File(path) => format!(
646                "file:{}",
647                path.clone().into_os_string().into_string().unwrap()
648            ),
649            RunPluginLocation::Zellij(tag) => format!("zellij:{}", tag),
650            RunPluginLocation::Remote(url) => String::from(url),
651        };
652        Self::parse(&url).unwrap()
653    }
654}
655
656impl fmt::Display for RunPluginLocation {
657    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
658        match self {
659            Self::File(path) => write!(
660                f,
661                "{}",
662                path.clone().into_os_string().into_string().unwrap()
663            ),
664            Self::Zellij(tag) => write!(f, "{}", tag),
665            Self::Remote(url) => write!(f, "{}", url),
666        }
667    }
668}
669
670#[derive(Clone, Debug, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize)]
671pub enum LayoutConstraint {
672    MaxPanes(usize),
673    MinPanes(usize),
674    ExactPanes(usize),
675    NoConstraint,
676}
677
678impl Display for LayoutConstraint {
679    fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
680        match self {
681            LayoutConstraint::MaxPanes(max_panes) => write!(f, "max_panes={}", max_panes),
682            LayoutConstraint::MinPanes(min_panes) => write!(f, "min_panes={}", min_panes),
683            LayoutConstraint::ExactPanes(exact_panes) => write!(f, "exact_panes={}", exact_panes),
684            LayoutConstraint::NoConstraint => write!(f, ""),
685        }
686    }
687}
688
689pub type SwapTiledLayout = (BTreeMap<LayoutConstraint, TiledPaneLayout>, Option<String>); // Option<String> is the swap layout name
690pub type SwapFloatingLayout = (
691    BTreeMap<LayoutConstraint, Vec<FloatingPaneLayout>>,
692    Option<String>,
693); // Option<String> is the swap layout name
694
695#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)]
696pub struct Layout {
697    pub tabs: Vec<(Option<String>, TiledPaneLayout, Vec<FloatingPaneLayout>)>,
698    pub focused_tab_index: Option<usize>,
699    pub template: Option<(TiledPaneLayout, Vec<FloatingPaneLayout>)>,
700    pub swap_layouts: Vec<(TiledPaneLayout, Vec<FloatingPaneLayout>)>,
701    pub swap_tiled_layouts: Vec<SwapTiledLayout>,
702    pub swap_floating_layouts: Vec<SwapFloatingLayout>,
703}
704
705/// Layout configuration for a single tab in multi-tab override
706#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
707pub struct TabLayoutInfo {
708    pub tab_index: usize,
709    pub tab_name: Option<String>,
710    pub tiled_layout: TiledPaneLayout,
711    pub floating_layouts: Vec<FloatingPaneLayout>,
712    pub swap_tiled_layouts: Option<Vec<SwapTiledLayout>>,
713    pub swap_floating_layouts: Option<Vec<SwapFloatingLayout>>,
714}
715
716#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
717pub enum PercentOrFixed {
718    Percent(usize), // 1 to 100
719    Fixed(usize),   // An absolute number of columns or rows
720}
721
722impl From<Dimension> for PercentOrFixed {
723    fn from(dimension: Dimension) -> Self {
724        match dimension.constraint {
725            Constraint::Percent(percent) => PercentOrFixed::Percent(percent as usize),
726            Constraint::Fixed(fixed_size) => PercentOrFixed::Fixed(fixed_size),
727        }
728    }
729}
730
731impl PercentOrFixed {
732    pub fn to_position(&self, whole: usize) -> usize {
733        match self {
734            PercentOrFixed::Percent(percent) => {
735                (whole as f64 / 100.0 * *percent as f64).ceil() as usize
736            },
737            PercentOrFixed::Fixed(fixed) => {
738                if *fixed > whole {
739                    whole
740                } else {
741                    *fixed
742                }
743            },
744        }
745    }
746    pub fn to_fixed(&self, whole: usize) -> usize {
747        match self {
748            PercentOrFixed::Percent(percent) => {
749                ((*percent as f64 / 100.0) * whole as f64).floor() as usize
750            },
751            PercentOrFixed::Fixed(fixed) => *fixed,
752        }
753    }
754}
755
756impl PercentOrFixed {
757    pub fn is_zero(&self) -> bool {
758        match self {
759            PercentOrFixed::Percent(percent) => *percent == 0,
760            PercentOrFixed::Fixed(fixed) => *fixed == 0,
761        }
762    }
763}
764
765impl FromStr for PercentOrFixed {
766    type Err = Box<dyn std::error::Error>;
767    fn from_str(s: &str) -> Result<Self, Self::Err> {
768        if s.chars().last() == Some('%') {
769            let char_count = s.chars().count();
770            let percent_size = usize::from_str_radix(&s[..char_count.saturating_sub(1)], 10)?;
771            if percent_size <= 100 {
772                Ok(PercentOrFixed::Percent(percent_size))
773            } else {
774                Err("Percent must be between 0 and 100".into())
775            }
776        } else {
777            let fixed_size = usize::from_str_radix(s, 10)?;
778            Ok(PercentOrFixed::Fixed(fixed_size))
779        }
780    }
781}
782
783#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)]
784pub struct FloatingPaneLayout {
785    pub name: Option<String>,
786    pub height: Option<PercentOrFixed>,
787    pub width: Option<PercentOrFixed>,
788    pub x: Option<PercentOrFixed>,
789    pub y: Option<PercentOrFixed>,
790    pub pinned: Option<bool>,
791    pub borderless: Option<bool>,
792    pub run: Option<Run>,
793    pub focus: Option<bool>,
794    pub already_running: bool,
795    pub pane_initial_contents: Option<String>,
796    pub logical_position: Option<usize>,
797    pub default_fg: Option<String>,
798    pub default_bg: Option<String>,
799}
800
801impl FloatingPaneLayout {
802    pub fn new() -> Self {
803        FloatingPaneLayout {
804            name: None,
805            height: None,
806            width: None,
807            x: None,
808            y: None,
809            pinned: None,
810            borderless: None,
811            run: None,
812            focus: None,
813            already_running: false,
814            pane_initial_contents: None,
815            logical_position: None,
816            default_fg: None,
817            default_bg: None,
818        }
819    }
820    pub fn add_cwd_to_layout(&mut self, cwd: &PathBuf) {
821        match self.run.as_mut() {
822            Some(run) => run.add_cwd(cwd),
823            None => {
824                self.run = Some(Run::Cwd(cwd.clone()));
825            },
826        }
827    }
828    pub fn add_start_suspended(&mut self, start_suspended: Option<bool>) {
829        if let Some(run) = self.run.as_mut() {
830            run.add_start_suspended(start_suspended);
831        }
832    }
833}
834
835impl From<&TiledPaneLayout> for FloatingPaneLayout {
836    fn from(pane_layout: &TiledPaneLayout) -> Self {
837        FloatingPaneLayout {
838            name: pane_layout.name.clone(),
839            run: pane_layout.run.clone(),
840            focus: pane_layout.focus,
841            ..Default::default()
842        }
843    }
844}
845
846#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)]
847pub struct TiledPaneLayout {
848    pub children_split_direction: SplitDirection,
849    pub name: Option<String>,
850    pub children: Vec<TiledPaneLayout>,
851    pub split_size: Option<SplitSize>,
852    pub run: Option<Run>,
853    pub borderless: Option<bool>,
854    pub focus: Option<bool>,
855    pub external_children_index: Option<usize>,
856    pub children_are_stacked: bool,
857    pub is_expanded_in_stack: bool,
858    pub exclude_from_sync: Option<bool>,
859    pub run_instructions_to_ignore: Vec<Option<Run>>,
860    pub hide_floating_panes: bool, // only relevant if this is the base layout
861    pub pane_initial_contents: Option<String>,
862    pub default_fg: Option<String>,
863    pub default_bg: Option<String>,
864}
865
866impl TiledPaneLayout {
867    pub fn insert_children_layout(
868        &mut self,
869        children_layout: &mut TiledPaneLayout,
870    ) -> Result<bool, ConfigError> {
871        // returns true if successfully inserted and false otherwise
872        match self.external_children_index {
873            Some(external_children_index) => {
874                self.children
875                    .insert(external_children_index, children_layout.clone());
876                self.external_children_index = None;
877                Ok(true)
878            },
879            None => {
880                for pane in self.children.iter_mut() {
881                    if pane.insert_children_layout(children_layout)? {
882                        return Ok(true);
883                    }
884                }
885                Ok(false)
886            },
887        }
888    }
889    pub fn insert_children_nodes(
890        &mut self,
891        children_nodes: &mut Vec<TiledPaneLayout>,
892    ) -> Result<bool, ConfigError> {
893        // returns true if successfully inserted and false otherwise
894        match self.external_children_index {
895            Some(external_children_index) => {
896                children_nodes.reverse();
897                for child_node in children_nodes.drain(..) {
898                    self.children.insert(external_children_index, child_node);
899                }
900                self.external_children_index = None;
901                Ok(true)
902            },
903            None => {
904                for pane in self.children.iter_mut() {
905                    if pane.insert_children_nodes(children_nodes)? {
906                        return Ok(true);
907                    }
908                }
909                Ok(false)
910            },
911        }
912    }
913    pub fn children_block_count(&self) -> usize {
914        let mut count = 0;
915        if self.external_children_index.is_some() {
916            count += 1;
917        }
918        for pane in &self.children {
919            count += pane.children_block_count();
920        }
921        count
922    }
923    pub fn pane_count(&self) -> usize {
924        if self.children.is_empty() {
925            1 // self
926        } else {
927            let mut pane_count = 0;
928            for child in &self.children {
929                pane_count += child.pane_count();
930            }
931            pane_count
932        }
933    }
934    pub fn position_panes_in_space(
935        &self,
936        space: &PaneGeom,
937        max_panes: Option<usize>,
938        ignore_percent_split_sizes: bool,
939        focus_layout_if_not_focused: bool,
940    ) -> Result<Vec<(TiledPaneLayout, PaneGeom)>, &'static str> {
941        let layouts = match max_panes {
942            Some(max_panes) => {
943                let mut layout_to_split = self.clone();
944                let pane_count_in_layout = layout_to_split.pane_count();
945                if max_panes > pane_count_in_layout {
946                    // the + 1 here is because this was previously an "actual" pane and will now
947                    // become just a container, so we need to account for it too
948                    // TODO: make sure this works when the `children` node has sibling nodes,
949                    // because we really should support that
950                    let children_count = (max_panes - pane_count_in_layout) + 1;
951                    let mut extra_children = vec![TiledPaneLayout::default(); children_count];
952                    if !layout_to_split.has_focused_node() && focus_layout_if_not_focused {
953                        if let Some(last_child) = extra_children.last_mut() {
954                            last_child.focus = Some(true);
955                        }
956                    }
957                    let _ = layout_to_split.insert_children_nodes(&mut extra_children);
958                } else {
959                    layout_to_split.truncate(max_panes);
960                }
961                if !layout_to_split.has_focused_node() && focus_layout_if_not_focused {
962                    layout_to_split.focus_deepest_pane();
963                }
964
965                let mut stack_id = 0;
966                split_space(
967                    space,
968                    &layout_to_split,
969                    space,
970                    ignore_percent_split_sizes,
971                    &mut stack_id,
972                )?
973            },
974            None => {
975                let mut stack_id = 0;
976                split_space(
977                    space,
978                    self,
979                    space,
980                    ignore_percent_split_sizes,
981                    &mut stack_id,
982                )?
983            },
984        };
985        for (_pane_layout, pane_geom) in layouts.iter() {
986            if !pane_geom.is_at_least_minimum_size() {
987                return Err("No room on screen for this layout!");
988            }
989        }
990        Ok(layouts)
991    }
992    pub fn extract_run_instructions(&self) -> Vec<Option<Run>> {
993        // the order of these run instructions is significant and needs to be the same
994        // as the order of the "flattened" layout panes received from eg. position_panes_in_space
995        let mut run_instructions = vec![];
996        if self.children.is_empty() {
997            run_instructions.push(self.run.clone());
998        }
999        let mut run_instructions_of_children = vec![];
1000        for child in &self.children {
1001            let mut child_run_instructions = child.extract_run_instructions();
1002            // add the only first child to run_instructions only adding the others after all the
1003            // childfree panes have been added so that the returned vec will be sorted breadth-first
1004            if !child_run_instructions.is_empty() {
1005                run_instructions.push(child_run_instructions.remove(0));
1006            }
1007            run_instructions_of_children.append(&mut child_run_instructions);
1008        }
1009        run_instructions.append(&mut run_instructions_of_children);
1010        let mut successfully_ignored = 0;
1011        for instruction_to_ignore in &self.run_instructions_to_ignore {
1012            if let Some(position) = run_instructions
1013                .iter()
1014                .position(|i| i == instruction_to_ignore)
1015            {
1016                run_instructions.remove(position);
1017                successfully_ignored += 1;
1018            }
1019        }
1020        // we need to do this because if we have an ignored instruction that does not match any
1021        // running instruction, we'll have an extra pane and our state will be messed up and we'll
1022        // crash (this can happen for example when breaking a plugin pane into a new tab that does
1023        // not have room for it but has a terminal instead)
1024        if successfully_ignored < self.run_instructions_to_ignore.len() {
1025            for _ in 0..self
1026                .run_instructions_to_ignore
1027                .len()
1028                .saturating_sub(successfully_ignored)
1029            {
1030                if let Some(position) = run_instructions.iter().position(|i| {
1031                    match i {
1032                        // this is because a bare CWD instruction should be overidden by a terminal
1033                        // in run_instructions_to_ignore (for cases where the cwd for example comes
1034                        // from a global layout cwd and the pane is actually just a bare pane that
1035                        // wants to be overidden)
1036                        Some(Run::Cwd(_)) | None => true,
1037                        _ => false,
1038                    }
1039                }) {
1040                    run_instructions.remove(position);
1041                }
1042            }
1043        }
1044        run_instructions
1045    }
1046    pub fn replace_next_empty_slot_with_run(&mut self, run_to_insert: Run) -> bool {
1047        // Replaces the first empty slot (None or Run::Cwd) with the given Run instruction.
1048        // Returns true if a replacement was made, false if no empty slot was found.
1049        // Traversal order matches extract_run_instructions (breadth-first).
1050
1051        if self.children.is_empty() {
1052            // This is a leaf node - check if it's an empty slot
1053            match &self.run {
1054                None | Some(Run::Cwd(_)) => {
1055                    self.run = Some(run_to_insert);
1056                    return true;
1057                },
1058                _ => return false,
1059            }
1060        }
1061
1062        // Check first child of each child (breadth-first first level)
1063        for child in self.children.iter_mut() {
1064            if child.children.is_empty() {
1065                match &child.run {
1066                    None | Some(Run::Cwd(_)) => {
1067                        child.run = Some(run_to_insert);
1068                        return true;
1069                    },
1070                    _ => {},
1071                }
1072            }
1073        }
1074
1075        // Recursively check deeper levels (breadth-first continuation)
1076        for child in self.children.iter_mut() {
1077            if child.replace_next_empty_slot_with_run(run_to_insert.clone()) {
1078                return true;
1079            }
1080        }
1081
1082        false
1083    }
1084    pub fn ignore_run_instruction(&mut self, run_instruction: Option<Run>) {
1085        self.run_instructions_to_ignore.push(run_instruction);
1086    }
1087    pub fn with_one_pane() -> Self {
1088        let mut default_layout = TiledPaneLayout::default();
1089        default_layout.children = vec![TiledPaneLayout::default()];
1090        default_layout
1091    }
1092    pub fn add_cwd_to_layout(&mut self, cwd: &PathBuf) {
1093        match self.run.as_mut() {
1094            Some(run) => run.add_cwd(cwd),
1095            None => {
1096                self.run = Some(Run::Cwd(cwd.clone()));
1097            },
1098        }
1099        for child in self.children.iter_mut() {
1100            child.add_cwd_to_layout(cwd);
1101        }
1102    }
1103    pub fn populate_plugin_aliases_in_layout(&mut self, plugin_aliases: &PluginAliases) {
1104        match self.run.as_mut() {
1105            Some(run) => run.populate_run_plugin_if_needed(plugin_aliases),
1106            _ => {},
1107        }
1108        for child in self.children.iter_mut() {
1109            child.populate_plugin_aliases_in_layout(plugin_aliases);
1110        }
1111    }
1112    pub fn deepest_depth(&self) -> usize {
1113        let mut deepest_child_depth = 0;
1114        for child in self.children.iter() {
1115            let child_deepest_depth = child.deepest_depth();
1116            if child_deepest_depth > deepest_child_depth {
1117                deepest_child_depth = child_deepest_depth;
1118            }
1119        }
1120        deepest_child_depth + 1
1121    }
1122    pub fn focus_deepest_pane(&mut self) {
1123        let mut deepest_child_index = None;
1124        let mut deepest_path = 0;
1125        for (i, child) in self.children.iter().enumerate() {
1126            let child_deepest_path = child.deepest_depth();
1127            if child_deepest_path >= deepest_path {
1128                deepest_path = child_deepest_path;
1129                deepest_child_index = Some(i)
1130            }
1131        }
1132        match deepest_child_index {
1133            Some(deepest_child_index) => {
1134                if let Some(child) = self.children.get_mut(deepest_child_index) {
1135                    child.focus_deepest_pane();
1136                }
1137            },
1138            None => {
1139                self.focus = Some(true);
1140            },
1141        }
1142    }
1143    pub fn truncate(&mut self, max_panes: usize) -> usize {
1144        // returns remaining children length
1145        // if max_panes is 1, it means there's only enough panes for this node,
1146        // if max_panes is 0, this is probably the root layout being called with 0 max panes
1147        if max_panes <= 1 {
1148            while !self.children.is_empty() {
1149                // this is a special case: we're truncating a pane that was previously a logical
1150                // container but now should be an actual pane - so here we'd like to use its
1151                // deepest "non-logical" child in order to get all its attributes (eg. borderless)
1152                let first_child = self.children.remove(0);
1153                drop(std::mem::replace(self, first_child));
1154            }
1155            self.children.clear();
1156        } else if max_panes <= self.children.len() {
1157            self.children.truncate(max_panes);
1158            self.children.iter_mut().for_each(|l| l.children.clear());
1159        } else {
1160            let mut remaining_panes = max_panes
1161                - self
1162                    .children
1163                    .iter()
1164                    .filter(|c| c.children.is_empty())
1165                    .count();
1166            for child in self.children.iter_mut() {
1167                if remaining_panes > 1 && child.children.len() > 0 {
1168                    remaining_panes =
1169                        remaining_panes.saturating_sub(child.truncate(remaining_panes));
1170                } else {
1171                    child.children.clear();
1172                }
1173            }
1174        }
1175        if self.children.len() > 0 {
1176            self.children.len()
1177        } else {
1178            1 // just me
1179        }
1180    }
1181    pub fn has_focused_node(&self) -> bool {
1182        if self.focus.map(|f| f).unwrap_or(false) {
1183            return true;
1184        };
1185        for child in &self.children {
1186            if child.has_focused_node() {
1187                return true;
1188            }
1189        }
1190        false
1191    }
1192    pub fn recursively_add_start_suspended(&mut self, start_suspended: Option<bool>) {
1193        if let Some(run) = self.run.as_mut() {
1194            run.add_start_suspended(start_suspended);
1195        }
1196        for child in self.children.iter_mut() {
1197            child.recursively_add_start_suspended(start_suspended);
1198        }
1199    }
1200}
1201
1202#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
1203pub enum LayoutParts {
1204    Tabs(Vec<(Option<String>, Layout)>), // String is the tab name
1205    Panes(Vec<Layout>),
1206}
1207
1208impl LayoutParts {
1209    pub fn is_empty(&self) -> bool {
1210        match self {
1211            LayoutParts::Panes(panes) => panes.is_empty(),
1212            LayoutParts::Tabs(tabs) => tabs.is_empty(),
1213        }
1214    }
1215    pub fn insert_pane(&mut self, index: usize, layout: Layout) -> Result<(), ConfigError> {
1216        match self {
1217            LayoutParts::Panes(panes) => {
1218                panes.insert(index, layout);
1219                Ok(())
1220            },
1221            LayoutParts::Tabs(_tabs) => Err(ConfigError::new_layout_kdl_error(
1222                "Trying to insert a pane into a tab layout".into(),
1223                0,
1224                0,
1225            )),
1226        }
1227    }
1228}
1229
1230impl Default for LayoutParts {
1231    fn default() -> Self {
1232        LayoutParts::Panes(vec![])
1233    }
1234}
1235
1236impl Layout {
1237    pub fn list_available_layouts(
1238        layout_dir: Option<PathBuf>,
1239        default_layout_name: &Option<String>,
1240    ) -> (Vec<LayoutInfo>, Vec<LayoutWithError>) {
1241        let (mut available_layouts, layouts_with_errors) = layout_dir
1242            .clone()
1243            .or_else(|| default_layout_dir())
1244            .and_then(|layout_dir| match std::fs::read_dir(&layout_dir) {
1245                Ok(layout_files) => Some((layout_files, layout_dir)),
1246                Err(_) => None,
1247            })
1248            .map(|(layout_files, layout_dir)| {
1249                let mut available_layouts = vec![];
1250                let mut layouts_with_errors = vec![];
1251                for file in layout_files {
1252                    if let Ok(file) = file {
1253                        if file.path().extension().map(|e| e.to_ascii_lowercase())
1254                            == Some(std::ffi::OsString::from("kdl"))
1255                        {
1256                            let layout_name = file
1257                                .path()
1258                                .file_stem()
1259                                .and_then(|f| f.to_str())
1260                                .map(|f| f.to_string())
1261                                .unwrap_or_default();
1262
1263                            match Layout::from_path_or_default_without_config(
1264                                Some(&file.path()),
1265                                Some(layout_dir.clone()),
1266                            ) {
1267                                Ok(_layout) => {
1268                                    let file_path = layout_dir.join(file.path()); // TODO: do we
1269                                                                                  // need
1270                                                                                  // file_stem()
1271                                                                                  // here too?
1272                                    available_layouts.push(LayoutInfo::File(
1273                                        layout_name,
1274                                        LayoutMetadata::from(&file_path),
1275                                    ))
1276                                },
1277                                Err(config_error) => {
1278                                    let file_path = file.path();
1279                                    let file_name =
1280                                        file_path.to_str().unwrap_or("unknown").to_string();
1281                                    let source_code =
1282                                        std::fs::read_to_string(&file_path).unwrap_or_default();
1283
1284                                    let error = match config_error {
1285                                        ConfigError::KdlError(kdl_err) => {
1286                                            LayoutParsingError::KdlError {
1287                                                kdl_error: kdl_err,
1288                                                file_name,
1289                                                source_code,
1290                                            }
1291                                        },
1292                                        _ => LayoutParsingError::SyntaxError,
1293                                    };
1294                                    layouts_with_errors
1295                                        .push(LayoutWithError { layout_name, error });
1296                                },
1297                            }
1298                        }
1299                    }
1300                }
1301                (available_layouts, layouts_with_errors)
1302            })
1303            .unwrap_or_else(|| (Default::default(), Default::default()));
1304        let default_layout_name = default_layout_name
1305            .as_ref()
1306            .map(|d| d.as_str())
1307            .unwrap_or("default");
1308        available_layouts.push(LayoutInfo::BuiltIn("default".to_owned()));
1309        available_layouts.push(LayoutInfo::BuiltIn("strider".to_owned()));
1310        available_layouts.push(LayoutInfo::BuiltIn("disable-status-bar".to_owned()));
1311        available_layouts.push(LayoutInfo::BuiltIn("compact".to_owned()));
1312        available_layouts.push(LayoutInfo::BuiltIn("classic".to_owned()));
1313        available_layouts.sort_by(|a, b| {
1314            let a_name = a.name();
1315            let b_name = b.name();
1316            if a_name == default_layout_name {
1317                return Ordering::Less;
1318            } else if b_name == default_layout_name {
1319                return Ordering::Greater;
1320            } else {
1321                a_name.cmp(&b_name)
1322            }
1323        });
1324        (available_layouts, layouts_with_errors)
1325    }
1326    pub fn from_layout_info(
1327        layout_dir: &Option<PathBuf>,
1328        layout_info: LayoutInfo,
1329    ) -> Result<Layout, ConfigError> {
1330        let mut should_start_layout_commands_suspended = false;
1331        let (path_to_raw_layout, raw_layout, raw_swap_layouts) = match layout_info {
1332            LayoutInfo::File(layout_name_without_extension, _layout_metadata) => {
1333                let layout_dir = layout_dir.clone().or_else(|| default_layout_dir());
1334                let (path_to_layout, stringified_layout, swap_layouts) =
1335                    Self::stringified_from_dir(
1336                        &PathBuf::from(layout_name_without_extension),
1337                        layout_dir.as_ref(),
1338                    )?;
1339                (Some(path_to_layout), stringified_layout, swap_layouts)
1340            },
1341            LayoutInfo::BuiltIn(layout_name) => {
1342                let (path_to_layout, stringified_layout, swap_layouts) =
1343                    Self::stringified_from_default_assets(&PathBuf::from(layout_name))?;
1344                (Some(path_to_layout), stringified_layout, swap_layouts)
1345            },
1346            LayoutInfo::Url(url) => {
1347                should_start_layout_commands_suspended = true;
1348                (Some(url.clone()), Self::stringified_from_url(&url)?, None)
1349            },
1350            LayoutInfo::Stringified(stringified_layout) => (None, stringified_layout, None),
1351        };
1352        let mut layout = Layout::from_kdl(
1353            &raw_layout,
1354            path_to_raw_layout,
1355            raw_swap_layouts
1356                .as_ref()
1357                .map(|(r, f)| (r.as_str(), f.as_str())),
1358            None,
1359        );
1360        if should_start_layout_commands_suspended {
1361            layout
1362                .iter_mut()
1363                .next()
1364                .map(|l| l.recursively_add_start_suspended_including_template(Some(true)));
1365        }
1366        layout
1367    }
1368    pub fn from_layout_info_with_config(
1369        layout_dir: &Option<PathBuf>,
1370        layout_info: &LayoutInfo,
1371        config: Option<Config>,
1372    ) -> Result<(Layout, Config), ConfigError> {
1373        let mut should_start_layout_commands_suspended = false;
1374        let (path_to_raw_layout, raw_layout, raw_swap_layouts) = match layout_info {
1375            LayoutInfo::File(layout_name_without_extension, _layout_metadata) => {
1376                let layout_dir = layout_dir.clone().or_else(|| default_layout_dir());
1377                let (path_to_layout, stringified_layout, swap_layouts) =
1378                    Self::stringified_from_dir(
1379                        &PathBuf::from(layout_name_without_extension),
1380                        layout_dir.as_ref(),
1381                    )?;
1382                (Some(path_to_layout), stringified_layout, swap_layouts)
1383            },
1384            LayoutInfo::BuiltIn(layout_name) => {
1385                let (path_to_layout, stringified_layout, swap_layouts) =
1386                    Self::stringified_from_default_assets(&PathBuf::from(layout_name))?;
1387                (Some(path_to_layout), stringified_layout, swap_layouts)
1388            },
1389            LayoutInfo::Url(url) => {
1390                should_start_layout_commands_suspended = true;
1391                (Some(url.clone()), Self::stringified_from_url(&url)?, None)
1392            },
1393            LayoutInfo::Stringified(stringified_layout) => (None, stringified_layout.clone(), None),
1394        };
1395        let mut layout = Layout::from_kdl(
1396            &raw_layout,
1397            path_to_raw_layout,
1398            raw_swap_layouts
1399                .as_ref()
1400                .map(|(r, f)| (r.as_str(), f.as_str())),
1401            None,
1402        );
1403        if should_start_layout_commands_suspended {
1404            layout
1405                .iter_mut()
1406                .next()
1407                .map(|l| l.recursively_add_start_suspended_including_template(Some(true)));
1408        }
1409        let config = Config::from_kdl(&raw_layout, config)?; // this merges the two config, with
1410        layout.map(|l| (l, config))
1411    }
1412    pub fn stringified_from_path_or_default(
1413        layout_path: Option<&PathBuf>,
1414        layout_dir: Option<PathBuf>,
1415    ) -> Result<(String, String, Option<(String, String)>), ConfigError> {
1416        // (path_to_layout as String, stringified_layout, Option<path_to_swap_layout as String, stringified_swap_layout>)
1417        match layout_path {
1418            Some(layout_path) => {
1419                // The way we determine where to look for the layout is similar to
1420                // how a path would look for an executable.
1421                // See the gh issue for more: https://github.com/zellij-org/zellij/issues/1412#issuecomment-1131559720
1422                if layout_path.extension().is_some() || layout_path.components().count() > 1 {
1423                    // We look localy!
1424                    Layout::stringified_from_path(layout_path)
1425                } else {
1426                    // We look in the default dir
1427                    Layout::stringified_from_dir(layout_path, layout_dir.as_ref())
1428                }
1429            },
1430            None => Layout::stringified_from_dir(
1431                &std::path::PathBuf::from("default"),
1432                layout_dir.as_ref(),
1433            ),
1434        }
1435    }
1436    #[cfg(not(target_family = "wasm"))]
1437    pub fn stringified_from_url(url: &str) -> Result<String, ConfigError> {
1438        Downloader::download_without_cache_blocking(url)
1439            .map_err(|e| ConfigError::DownloadError(format!("{}", e)))
1440    }
1441    #[cfg(target_family = "wasm")]
1442    pub fn stringified_from_url(_url: &str) -> Result<String, ConfigError> {
1443        // silently fail - this should not happen in plugins and legacy architecture is hard
1444        let raw_layout = String::new();
1445        Ok(raw_layout)
1446    }
1447    pub fn from_path_without_config(layout_path: &PathBuf) -> Result<Layout, ConfigError> {
1448        // (path_to_layout as String, stringified_layout, Option<path_to_swap_layout as String, stringified_swap_layout>)
1449        let (path_to_layout, raw_layout, raw_swap_layouts) =
1450            Layout::stringified_from_path(layout_path)?;
1451        Layout::from_kdl(
1452            &raw_layout,
1453            Some(path_to_layout),
1454            raw_swap_layouts
1455                .as_ref()
1456                .map(|(r, f)| (r.as_str(), f.as_str())),
1457            None,
1458        )
1459    }
1460    pub fn from_path_or_default(
1461        layout_path: Option<&PathBuf>,
1462        layout_dir: Option<PathBuf>,
1463        config: Config,
1464    ) -> Result<(Layout, Config), ConfigError> {
1465        let (path_to_raw_layout, raw_layout, raw_swap_layouts) =
1466            Layout::stringified_from_path_or_default(layout_path, layout_dir)?;
1467        let layout = Layout::from_kdl(
1468            &raw_layout,
1469            Some(path_to_raw_layout),
1470            raw_swap_layouts
1471                .as_ref()
1472                .map(|(r, f)| (r.as_str(), f.as_str())),
1473            None,
1474        )?;
1475        let config = Config::from_kdl(&raw_layout, Some(config))?; // this merges the two config, with
1476        Ok((layout, config))
1477    }
1478    #[cfg(not(target_family = "wasm"))]
1479    pub fn from_url(url: &str, config: Config) -> Result<(Layout, Config), ConfigError> {
1480        let raw_layout = Downloader::download_without_cache_blocking(url)
1481            .map_err(|e| ConfigError::DownloadError(format!("{}", e)))?;
1482        let mut layout = Layout::from_kdl(&raw_layout, Some(url.into()), None, None)?;
1483        layout.recursively_add_start_suspended_including_template(Some(true));
1484        let config = Config::from_kdl(&raw_layout, Some(config))?; // this merges the two config, with
1485        Ok((layout, config))
1486    }
1487    pub fn from_stringified_layout(
1488        stringified_layout: &str,
1489        config: Config,
1490    ) -> Result<(Layout, Config), ConfigError> {
1491        let layout = Layout::from_kdl(&stringified_layout, None, None, None)?;
1492        let config = Config::from_kdl(&stringified_layout, Some(config))?; // this merges the two config, with
1493        Ok((layout, config))
1494    }
1495    #[cfg(target_family = "wasm")]
1496    pub fn from_url(_url: &str, _config: Config) -> Result<(Layout, Config), ConfigError> {
1497        Err(ConfigError::DownloadError(format!(
1498            "Unsupported platform, cannot download layout from the web"
1499        )))
1500    }
1501    pub fn from_path_or_default_without_config(
1502        layout_path: Option<&PathBuf>,
1503        layout_dir: Option<PathBuf>,
1504    ) -> Result<Layout, ConfigError> {
1505        let (path_to_raw_layout, raw_layout, raw_swap_layouts) =
1506            Layout::stringified_from_path_or_default(layout_path, layout_dir)?;
1507        let layout = Layout::from_kdl(
1508            &raw_layout,
1509            Some(path_to_raw_layout),
1510            raw_swap_layouts
1511                .as_ref()
1512                .map(|(r, f)| (r.as_str(), f.as_str())),
1513            None,
1514        )?;
1515        Ok(layout)
1516    }
1517    pub fn from_default_assets(
1518        layout_name: &Path,
1519        _layout_dir: Option<PathBuf>,
1520        config: Config,
1521    ) -> Result<(Layout, Config), ConfigError> {
1522        let (path_to_raw_layout, raw_layout, raw_swap_layouts) =
1523            Layout::stringified_from_default_assets(layout_name)?;
1524        let layout = Layout::from_kdl(
1525            &raw_layout,
1526            Some(path_to_raw_layout),
1527            raw_swap_layouts
1528                .as_ref()
1529                .map(|(r, f)| (r.as_str(), f.as_str())),
1530            None,
1531        )?;
1532        let config = Config::from_kdl(&raw_layout, Some(config))?; // this merges the two config, with
1533        Ok((layout, config))
1534    }
1535    pub fn default_layout_asset() -> Layout {
1536        Layout::from_kdl(
1537            &Self::stringified_default_from_assets().unwrap(),
1538            None,
1539            Some((
1540                "",
1541                Self::stringified_default_swap_from_assets()
1542                    .unwrap()
1543                    .as_str(),
1544            )),
1545            None,
1546        )
1547        .unwrap()
1548    }
1549    pub fn from_str(
1550        raw: &str,
1551        path_to_raw_layout: String,
1552        swap_layouts: Option<(&str, &str)>, // Option<path_to_swap_layout, stringified_swap_layout>
1553        cwd: Option<PathBuf>,
1554    ) -> Result<Layout, ConfigError> {
1555        Layout::from_kdl(raw, Some(path_to_raw_layout), swap_layouts, cwd)
1556    }
1557    pub fn stringified_from_dir(
1558        layout: &PathBuf,
1559        layout_dir: Option<&PathBuf>,
1560    ) -> Result<(String, String, Option<(String, String)>), ConfigError> {
1561        // (path_to_layout as String, stringified_layout, Option<path_to_swap_layout as String, stringified_swap_layout>)
1562        match layout_dir {
1563            Some(dir) => {
1564                let layout_path = &dir.join(layout);
1565                if layout_path.with_extension("kdl").exists() {
1566                    Self::stringified_from_path(layout_path)
1567                } else {
1568                    Layout::stringified_from_default_assets(layout)
1569                }
1570            },
1571            None => {
1572                let home = find_default_config_dir();
1573                let Some(home) = home else {
1574                    return Layout::stringified_from_default_assets(layout);
1575                };
1576
1577                let layout_path = &home.join(layout);
1578                Self::stringified_from_path(layout_path)
1579            },
1580        }
1581    }
1582    pub fn stringified_from_path(
1583        layout_path: &Path,
1584    ) -> Result<(String, String, Option<(String, String)>), ConfigError> {
1585        // (path_to_layout as String, stringified_layout, Option<path_to_swap_layout as String, stringified_swap_layout>)
1586        let mut layout_file = File::open(&layout_path)
1587            .or_else(|_| File::open(&layout_path.with_extension("kdl")))
1588            .map_err(|e| ConfigError::IoPath(e, layout_path.into()))?;
1589
1590        let swap_layout_and_path = Layout::swap_layout_and_path(&layout_path);
1591
1592        let mut kdl_layout = String::new();
1593        layout_file
1594            .read_to_string(&mut kdl_layout)
1595            .map_err(|e| ConfigError::IoPath(e, layout_path.into()))?;
1596        Ok((
1597            layout_path.as_os_str().to_string_lossy().into(),
1598            kdl_layout,
1599            swap_layout_and_path,
1600        ))
1601    }
1602    pub fn stringified_from_default_assets(
1603        path: &Path,
1604    ) -> Result<(String, String, Option<(String, String)>), ConfigError> {
1605        // (path_to_layout as String, stringified_layout, Option<path_to_swap_layout as String, stringified_swap_layout>)
1606        // TODO: ideally these should not be hard-coded
1607        // we should load layouts by name from the config
1608        // and load them from a hashmap or some such
1609        match path.to_str() {
1610            Some("default") => Ok((
1611                "Default layout".into(),
1612                Self::stringified_default_from_assets()?,
1613                Some((
1614                    "Default swap layout".into(),
1615                    Self::stringified_default_swap_from_assets()?,
1616                )),
1617            )),
1618            Some("strider") => Ok((
1619                "Strider layout".into(),
1620                Self::stringified_strider_from_assets()?,
1621                Some((
1622                    "Strider swap layout".into(),
1623                    Self::stringified_strider_swap_from_assets()?,
1624                )),
1625            )),
1626            Some("disable-status-bar") => Ok((
1627                "Disable Status Bar layout".into(),
1628                Self::stringified_disable_status_from_assets()?,
1629                None,
1630            )),
1631            Some("compact") => Ok((
1632                "Compact layout".into(),
1633                Self::stringified_compact_from_assets()?,
1634                Some((
1635                    "Compact layout swap".into(),
1636                    Self::stringified_compact_swap_from_assets()?,
1637                )),
1638            )),
1639            Some("classic") => Ok((
1640                "Classic layout".into(),
1641                Self::stringified_classic_from_assets()?,
1642                Some((
1643                    "Classiclayout swap".into(),
1644                    Self::stringified_classic_swap_from_assets()?,
1645                )),
1646            )),
1647            Some("welcome") => Ok((
1648                "Welcome screen layout".into(),
1649                Self::stringified_welcome_from_assets()?,
1650                None,
1651            )),
1652            None | Some(_) => Err(ConfigError::IoPath(
1653                std::io::Error::new(std::io::ErrorKind::Other, "The layout was not found"),
1654                path.into(),
1655            )),
1656        }
1657    }
1658    pub fn stringified_default_from_assets() -> Result<String, ConfigError> {
1659        Ok(String::from_utf8(setup::DEFAULT_LAYOUT.to_vec())?)
1660    }
1661    pub fn stringified_default_swap_from_assets() -> Result<String, ConfigError> {
1662        Ok(String::from_utf8(setup::DEFAULT_SWAP_LAYOUT.to_vec())?)
1663    }
1664    pub fn stringified_strider_from_assets() -> Result<String, ConfigError> {
1665        Ok(String::from_utf8(setup::STRIDER_LAYOUT.to_vec())?)
1666    }
1667    pub fn stringified_strider_swap_from_assets() -> Result<String, ConfigError> {
1668        Ok(String::from_utf8(setup::STRIDER_SWAP_LAYOUT.to_vec())?)
1669    }
1670
1671    pub fn stringified_disable_status_from_assets() -> Result<String, ConfigError> {
1672        Ok(String::from_utf8(setup::NO_STATUS_LAYOUT.to_vec())?)
1673    }
1674
1675    pub fn stringified_compact_from_assets() -> Result<String, ConfigError> {
1676        Ok(String::from_utf8(setup::COMPACT_BAR_LAYOUT.to_vec())?)
1677    }
1678
1679    pub fn stringified_compact_swap_from_assets() -> Result<String, ConfigError> {
1680        Ok(String::from_utf8(setup::COMPACT_BAR_SWAP_LAYOUT.to_vec())?)
1681    }
1682
1683    pub fn stringified_classic_from_assets() -> Result<String, ConfigError> {
1684        Ok(String::from_utf8(setup::CLASSIC_LAYOUT.to_vec())?)
1685    }
1686
1687    pub fn stringified_classic_swap_from_assets() -> Result<String, ConfigError> {
1688        Ok(String::from_utf8(setup::CLASSIC_SWAP_LAYOUT.to_vec())?)
1689    }
1690
1691    pub fn stringified_welcome_from_assets() -> Result<String, ConfigError> {
1692        Ok(String::from_utf8(setup::WELCOME_LAYOUT.to_vec())?)
1693    }
1694
1695    pub fn new_tab(&self) -> (TiledPaneLayout, Vec<FloatingPaneLayout>) {
1696        self.template.clone().unwrap_or_default()
1697    }
1698
1699    pub fn is_empty(&self) -> bool {
1700        !self.tabs.is_empty()
1701    }
1702    // TODO: do we need both of these?
1703    pub fn has_tabs(&self) -> bool {
1704        !self.tabs.is_empty()
1705    }
1706
1707    pub fn tabs(&self) -> Vec<(Option<String>, TiledPaneLayout, Vec<FloatingPaneLayout>)> {
1708        // String is the tab name
1709        self.tabs.clone()
1710    }
1711
1712    pub fn focused_tab_index(&self) -> Option<usize> {
1713        self.focused_tab_index
1714    }
1715
1716    pub fn recursively_add_start_suspended(&mut self, start_suspended: Option<bool>) {
1717        for (_tab_name, tiled_panes, floating_panes) in self.tabs.iter_mut() {
1718            tiled_panes.recursively_add_start_suspended(start_suspended);
1719            for floating_pane in floating_panes.iter_mut() {
1720                floating_pane.add_start_suspended(start_suspended);
1721            }
1722        }
1723    }
1724    pub fn recursively_add_start_suspended_including_template(
1725        &mut self,
1726        start_suspended: Option<bool>,
1727    ) {
1728        if let Some((tiled_panes_template, floating_panes_template)) = self.template.as_mut() {
1729            tiled_panes_template.recursively_add_start_suspended(start_suspended);
1730            for floating_pane in floating_panes_template.iter_mut() {
1731                floating_pane.add_start_suspended(start_suspended);
1732            }
1733        }
1734        for (_tab_name, tiled_panes, floating_panes) in self.tabs.iter_mut() {
1735            tiled_panes.recursively_add_start_suspended(start_suspended);
1736            for floating_pane in floating_panes.iter_mut() {
1737                floating_pane.add_start_suspended(start_suspended);
1738            }
1739        }
1740    }
1741    fn swap_layout_and_path(path: &Path) -> Option<(String, String)> {
1742        // Option<path, stringified_swap_layout>
1743        let mut swap_layout_path = PathBuf::from(path);
1744        swap_layout_path.set_extension("swap.kdl");
1745        match File::open(&swap_layout_path) {
1746            Ok(mut stringified_swap_layout_file) => {
1747                let mut swap_kdl_layout = String::new();
1748                match stringified_swap_layout_file.read_to_string(&mut swap_kdl_layout) {
1749                    Ok(..) => Some((
1750                        swap_layout_path.as_os_str().to_string_lossy().into(),
1751                        swap_kdl_layout,
1752                    )),
1753                    Err(_e) => None,
1754                }
1755            },
1756            Err(_e) => None,
1757        }
1758    }
1759    pub fn populate_plugin_aliases_in_layout(&mut self, plugin_aliases: &PluginAliases) {
1760        for tab in self.tabs.iter_mut() {
1761            tab.1.populate_plugin_aliases_in_layout(plugin_aliases);
1762            for floating_pane_layout in tab.2.iter_mut() {
1763                floating_pane_layout
1764                    .run
1765                    .as_mut()
1766                    .map(|f| f.populate_run_plugin_if_needed(&plugin_aliases));
1767            }
1768        }
1769        if let Some(template) = self.template.as_mut() {
1770            template.0.populate_plugin_aliases_in_layout(plugin_aliases);
1771            for floating_pane_layout in template.1.iter_mut() {
1772                floating_pane_layout
1773                    .run
1774                    .as_mut()
1775                    .map(|f| f.populate_run_plugin_if_needed(&plugin_aliases));
1776            }
1777        }
1778        for swap_tiled_layout in &mut self.swap_tiled_layouts {
1779            for (_constraint, tiled_pane_layout) in &mut swap_tiled_layout.0 {
1780                tiled_pane_layout.populate_plugin_aliases_in_layout(plugin_aliases);
1781            }
1782        }
1783        for swap_floating_layout in &mut self.swap_floating_layouts {
1784            for (_constraint, floating_pane_layouts) in &mut swap_floating_layout.0 {
1785                for floating_pane_layout in floating_pane_layouts {
1786                    floating_pane_layout
1787                        .run
1788                        .as_mut()
1789                        .map(|f| f.populate_run_plugin_if_needed(plugin_aliases));
1790                }
1791            }
1792        }
1793    }
1794    pub fn add_cwd_to_layout(&mut self, cwd: &PathBuf) {
1795        for (_, tiled_pane_layout, floating_panes) in self.tabs.iter_mut() {
1796            tiled_pane_layout.add_cwd_to_layout(&cwd);
1797            for floating_pane in floating_panes {
1798                floating_pane.add_cwd_to_layout(&cwd);
1799            }
1800        }
1801        if let Some((tiled_pane_layout, floating_panes)) = self.template.as_mut() {
1802            tiled_pane_layout.add_cwd_to_layout(&cwd);
1803            for floating_pane in floating_panes {
1804                floating_pane.add_cwd_to_layout(&cwd);
1805            }
1806        }
1807    }
1808    pub fn pane_count(&self) -> usize {
1809        let mut pane_count = 0;
1810        if let Some((tiled_pane_layout, floating_panes)) = self.template.as_ref() {
1811            pane_count += tiled_pane_layout.pane_count();
1812            for _ in floating_panes {
1813                pane_count += 1;
1814            }
1815        }
1816        for (_, tiled_pane_layout, floating_panes) in &self.tabs {
1817            pane_count += tiled_pane_layout.pane_count();
1818            for _ in floating_panes {
1819                pane_count += 1;
1820            }
1821        }
1822        pane_count
1823    }
1824}
1825
1826fn split_space(
1827    space_to_split: &PaneGeom,
1828    layout: &TiledPaneLayout,
1829    total_space_to_split: &PaneGeom,
1830    ignore_percent_split_sizes: bool,
1831    next_stack_id: &mut usize,
1832) -> Result<Vec<(TiledPaneLayout, PaneGeom)>, &'static str> {
1833    let sizes: Vec<Option<SplitSize>> = if layout.children_are_stacked {
1834        let index_of_expanded_pane = layout.children.iter().position(|p| p.is_expanded_in_stack);
1835        let mut sizes: Vec<Option<SplitSize>> = layout
1836            .children
1837            .iter()
1838            .map(|_part| Some(SplitSize::Fixed(1)))
1839            .collect();
1840        if let Some(index_of_expanded_pane) = index_of_expanded_pane {
1841            *sizes.get_mut(index_of_expanded_pane).unwrap() = None;
1842        } else if let Some(last_size) = sizes.last_mut() {
1843            *last_size = None;
1844        }
1845        if sizes.len() > space_to_split.rows.as_usize().saturating_sub(3) {
1846            // 4 is MIN_TERMINAL_HEIGHT, minus 1 because sizes also includes the expanded pane in
1847            // the stack
1848            return Err("Not enough room for stacked panes in this layout");
1849        }
1850        sizes
1851    } else if ignore_percent_split_sizes {
1852        layout
1853            .children
1854            .iter()
1855            .map(|part| match part.split_size {
1856                Some(SplitSize::Percent(_)) => None,
1857                split_size => split_size,
1858            })
1859            .collect()
1860    } else {
1861        layout.children.iter().map(|part| part.split_size).collect()
1862    };
1863
1864    let mut split_geom = Vec::new();
1865    let (
1866        mut current_position,
1867        split_dimension_space,
1868        inherited_dimension,
1869        total_split_dimension_space,
1870    ) = match layout.children_split_direction {
1871        SplitDirection::Vertical => (
1872            space_to_split.x,
1873            space_to_split.cols,
1874            space_to_split.rows,
1875            total_space_to_split.cols,
1876        ),
1877        SplitDirection::Horizontal => (
1878            space_to_split.y,
1879            space_to_split.rows,
1880            space_to_split.cols,
1881            total_space_to_split.rows,
1882        ),
1883    };
1884
1885    let min_size_for_panes = sizes.iter().fold(0, |acc, size| match size {
1886        Some(SplitSize::Percent(_)) | None => acc + 1, // TODO: minimum height/width as relevant here
1887        Some(SplitSize::Fixed(fixed)) => acc + fixed,
1888    });
1889    if min_size_for_panes > split_dimension_space.as_usize() {
1890        return Err("Not enough room for panes"); // TODO: use error infra
1891    }
1892
1893    let flex_parts = sizes.iter().filter(|s| s.is_none()).count();
1894    let total_fixed_size = sizes.iter().fold(0, |acc, s| {
1895        if let Some(SplitSize::Fixed(fixed)) = s {
1896            acc + fixed
1897        } else {
1898            acc
1899        }
1900    });
1901    let stacked = if layout.children_are_stacked {
1902        let stack_id = *next_stack_id;
1903        *next_stack_id += 1;
1904        Some(stack_id)
1905    } else {
1906        None
1907    };
1908
1909    let mut total_pane_size = 0;
1910    for (&size, _part) in sizes.iter().zip(&*layout.children) {
1911        let mut split_dimension = match size {
1912            Some(SplitSize::Percent(percent)) => Dimension::percent(percent as f64),
1913            Some(SplitSize::Fixed(size)) => Dimension::fixed(size),
1914            None => {
1915                let free_percent = if let Some(p) = split_dimension_space.as_percent() {
1916                    p - sizes
1917                        .iter()
1918                        .map(|&s| match s {
1919                            Some(SplitSize::Percent(ip)) => ip as f64,
1920                            _ => 0.0,
1921                        })
1922                        .sum::<f64>()
1923                } else {
1924                    panic!("Implicit sizing within fixed-size panes is not supported");
1925                };
1926                Dimension::percent(free_percent / flex_parts as f64)
1927            },
1928        };
1929
1930        split_dimension.adjust_inner(
1931            total_split_dimension_space
1932                .as_usize()
1933                .saturating_sub(total_fixed_size),
1934        );
1935        total_pane_size += split_dimension.as_usize();
1936
1937        let geom = match layout.children_split_direction {
1938            SplitDirection::Vertical => PaneGeom {
1939                x: current_position,
1940                y: space_to_split.y,
1941                cols: split_dimension,
1942                rows: inherited_dimension,
1943                stacked,
1944                is_pinned: false,
1945                logical_position: None,
1946            },
1947            SplitDirection::Horizontal => PaneGeom {
1948                x: space_to_split.x,
1949                y: current_position,
1950                cols: inherited_dimension,
1951                rows: split_dimension,
1952                stacked,
1953                is_pinned: false,
1954                logical_position: None,
1955            },
1956        };
1957        split_geom.push(geom);
1958        current_position += split_dimension.as_usize();
1959    }
1960    adjust_geoms_for_rounding_errors(
1961        total_pane_size,
1962        &mut split_geom,
1963        split_dimension_space,
1964        layout.children_split_direction,
1965    );
1966    let mut pane_positions = Vec::new();
1967    let mut pane_positions_with_children = Vec::new();
1968    for (i, part) in layout.children.iter().enumerate() {
1969        let part_position_and_size = split_geom.get(i).unwrap();
1970        if !part.children.is_empty() {
1971            let mut part_positions = split_space(
1972                part_position_and_size,
1973                part,
1974                total_space_to_split,
1975                ignore_percent_split_sizes,
1976                next_stack_id,
1977            )?;
1978            // add the only first child to pane_positions only adding the others after all the
1979            // childfree panes have been added so that the returned vec will be sorted breadth-first
1980            if !part_positions.is_empty() {
1981                pane_positions.push(part_positions.remove(0));
1982            }
1983            pane_positions_with_children.append(&mut part_positions);
1984        } else {
1985            let part = part.clone();
1986            pane_positions.push((part, *part_position_and_size));
1987        }
1988    }
1989    pane_positions.append(&mut pane_positions_with_children);
1990    if pane_positions.is_empty() {
1991        let layout = layout.clone();
1992        pane_positions.push((layout, space_to_split.clone()));
1993    }
1994    Ok(pane_positions)
1995}
1996
1997fn adjust_geoms_for_rounding_errors(
1998    total_pane_size: usize,
1999    split_geoms: &mut Vec<PaneGeom>,
2000    split_dimension_space: Dimension,
2001    children_split_direction: SplitDirection,
2002) {
2003    if total_pane_size < split_dimension_space.as_usize() {
2004        // add extra space from rounding errors to the last pane
2005
2006        let increase_by = split_dimension_space
2007            .as_usize()
2008            .saturating_sub(total_pane_size);
2009        let position_of_last_flexible_geom = split_geoms
2010            .iter()
2011            .rposition(|s_g| s_g.is_flexible_in_direction(children_split_direction));
2012        position_of_last_flexible_geom
2013            .map(|p| split_geoms.iter_mut().skip(p))
2014            .map(|mut flexible_geom_and_following_geoms| {
2015                if let Some(flexible_geom) = flexible_geom_and_following_geoms.next() {
2016                    match children_split_direction {
2017                        SplitDirection::Vertical => flexible_geom.cols.increase_inner(increase_by),
2018                        SplitDirection::Horizontal => {
2019                            flexible_geom.rows.increase_inner(increase_by)
2020                        },
2021                    }
2022                }
2023                for following_geom in flexible_geom_and_following_geoms {
2024                    match children_split_direction {
2025                        SplitDirection::Vertical => {
2026                            following_geom.x += increase_by;
2027                        },
2028                        SplitDirection::Horizontal => {
2029                            following_geom.y += increase_by;
2030                        },
2031                    }
2032                }
2033            });
2034    } else if total_pane_size > split_dimension_space.as_usize() {
2035        // remove extra space from rounding errors to the last pane
2036        let decrease_by = total_pane_size - split_dimension_space.as_usize();
2037        let position_of_last_flexible_geom = split_geoms
2038            .iter()
2039            .rposition(|s_g| s_g.is_flexible_in_direction(children_split_direction));
2040        position_of_last_flexible_geom
2041            .map(|p| split_geoms.iter_mut().skip(p))
2042            .map(|mut flexible_geom_and_following_geoms| {
2043                if let Some(flexible_geom) = flexible_geom_and_following_geoms.next() {
2044                    match children_split_direction {
2045                        SplitDirection::Vertical => flexible_geom.cols.decrease_inner(decrease_by),
2046                        SplitDirection::Horizontal => {
2047                            flexible_geom.rows.decrease_inner(decrease_by)
2048                        },
2049                    }
2050                }
2051                for following_geom in flexible_geom_and_following_geoms {
2052                    match children_split_direction {
2053                        SplitDirection::Vertical => {
2054                            following_geom.x = following_geom.x.saturating_sub(decrease_by)
2055                        },
2056                        SplitDirection::Horizontal => {
2057                            following_geom.y = following_geom.y.saturating_sub(decrease_by)
2058                        },
2059                    }
2060                }
2061            });
2062    }
2063}
2064
2065impl Default for SplitDirection {
2066    fn default() -> Self {
2067        SplitDirection::Horizontal
2068    }
2069}
2070
2071impl FromStr for SplitDirection {
2072    type Err = Box<dyn std::error::Error>;
2073    fn from_str(s: &str) -> Result<Self, Self::Err> {
2074        match s {
2075            "vertical" | "Vertical" => Ok(SplitDirection::Vertical),
2076            "horizontal" | "Horizontal" => Ok(SplitDirection::Horizontal),
2077            _ => Err("split direction must be either vertical or horizontal".into()),
2078        }
2079    }
2080}
2081
2082impl FromStr for SplitSize {
2083    type Err = Box<dyn std::error::Error>;
2084    fn from_str(s: &str) -> Result<Self, Self::Err> {
2085        if s.chars().last() == Some('%') {
2086            let char_count = s.chars().count();
2087            let percent_size = usize::from_str_radix(&s[..char_count.saturating_sub(1)], 10)?;
2088            if percent_size > 0 && percent_size <= 100 {
2089                Ok(SplitSize::Percent(percent_size))
2090            } else {
2091                Err("Percent must be between 0 and 100".into())
2092            }
2093        } else {
2094            let fixed_size = usize::from_str_radix(s, 10)?;
2095            Ok(SplitSize::Fixed(fixed_size))
2096        }
2097    }
2098}
2099
2100// The unit test location.
2101#[path = "./unit/layout_test.rs"]
2102#[cfg(test)]
2103mod layout_test;