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