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, 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        // NOTE: Keep this in sync with what the `Hash` trait impl does.
447        self.name == other.name && self.configuration == other.configuration
448    }
449}
450
451impl std::hash::Hash for PluginAlias {
452    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
453        // NOTE: Keep this in sync with what the `PartiqlEq` trait impl does.
454        self.name.hash(state);
455        self.configuration.hash(state);
456    }
457}
458
459impl PluginAlias {
460    pub fn new(
461        name: &str,
462        configuration: &Option<BTreeMap<String, String>>,
463        initial_cwd: Option<PathBuf>,
464    ) -> Self {
465        PluginAlias {
466            name: name.to_owned(),
467            configuration: configuration
468                .as_ref()
469                .map(|c| PluginUserConfiguration::new(c.clone())),
470            initial_cwd,
471            ..Default::default()
472        }
473    }
474    pub fn set_caller_cwd_if_not_set(&mut self, caller_cwd: Option<PathBuf>) {
475        // we do this only for an alias because in all other cases this will be handled by the
476        // "cwd" configuration key above
477        // for an alias we might have cases where the cwd is defined on the alias but we still
478        // want to pass the "caller" cwd for the plugin the alias resolves into (eg. a
479        // filepicker that has access to the whole filesystem but wants to start in a specific
480        // folder)
481        if let Some(caller_cwd) = caller_cwd {
482            if self
483                .configuration
484                .as_ref()
485                .map(|c| c.inner().get("caller_cwd").is_none())
486                .unwrap_or(true)
487            {
488                let configuration = self
489                    .configuration
490                    .get_or_insert_with(|| PluginUserConfiguration::new(BTreeMap::new()));
491                configuration.insert("caller_cwd", caller_cwd.display().to_string());
492            }
493        }
494    }
495}
496
497#[allow(clippy::derive_hash_xor_eq)]
498impl PartialEq for RunPlugin {
499    fn eq(&self, other: &Self) -> bool {
500        // TODO: normalize paths here if the location is a file so that relative/absolute paths
501        // will work properly
502        (&self.location, &self.configuration) == (&other.location, &other.configuration)
503    }
504}
505impl Eq for RunPlugin {}
506
507#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
508pub struct PluginUserConfiguration(BTreeMap<String, String>);
509
510impl PluginUserConfiguration {
511    pub fn new(mut configuration: BTreeMap<String, String>) -> Self {
512        // reserved words
513        configuration.remove("hold_on_close");
514        configuration.remove("hold_on_start");
515        configuration.remove("cwd");
516        configuration.remove("name");
517        configuration.remove("direction");
518        configuration.remove("floating");
519        configuration.remove("move_to_focused_tab");
520        configuration.remove("launch_new");
521        configuration.remove("payload");
522        configuration.remove("skip_cache");
523        configuration.remove("title");
524        configuration.remove("in_place");
525        configuration.remove("skip_plugin_cache");
526
527        PluginUserConfiguration(configuration)
528    }
529    pub fn inner(&self) -> &BTreeMap<String, String> {
530        &self.0
531    }
532    pub fn insert(&mut self, config_key: impl Into<String>, config_value: impl Into<String>) {
533        self.0.insert(config_key.into(), config_value.into());
534    }
535    pub fn merge(&mut self, other_config: &BTreeMap<String, String>) {
536        for (key, value) in other_config {
537            self.0.insert(key.to_owned(), value.clone());
538        }
539    }
540}
541
542impl FromStr for PluginUserConfiguration {
543    type Err = &'static str;
544
545    fn from_str(s: &str) -> Result<Self, Self::Err> {
546        let mut ret = BTreeMap::new();
547        let configs = s.split(',');
548        for config in configs {
549            let mut config = config.split('=');
550            let key = config.next().ok_or("invalid configuration key")?.to_owned();
551            let value = config.map(|c| c.to_owned()).collect::<Vec<_>>().join("=");
552            ret.insert(key, value);
553        }
554        Ok(PluginUserConfiguration(ret))
555    }
556}
557
558#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)]
559pub enum RunPluginLocation {
560    File(PathBuf),
561    Zellij(PluginTag),
562    Remote(String),
563}
564
565impl Default for RunPluginLocation {
566    fn default() -> Self {
567        RunPluginLocation::File(Default::default())
568    }
569}
570
571impl RunPluginLocation {
572    pub fn parse(location: &str, cwd: Option<PathBuf>) -> Result<Self, PluginsConfigError> {
573        let url = Url::parse(location)?;
574
575        let decoded_path = percent_encoding::percent_decode_str(url.path()).decode_utf8_lossy();
576
577        match url.scheme() {
578            "zellij" => Ok(Self::Zellij(PluginTag::new(decoded_path))),
579            "file" => {
580                let path = if location.starts_with("file:/") {
581                    // Path is absolute, its safe to use URL path.
582                    //
583                    // This is the case if the scheme and : delimiter are followed by a / slash
584                    PathBuf::from(decoded_path.as_ref())
585                } else if location.starts_with("file:~") {
586                    // Unwrap is safe here since location is a valid URL
587                    PathBuf::from(location.strip_prefix("file:").unwrap())
588                } else {
589                    // URL dep doesn't handle relative paths with `file` schema properly,
590                    // it always makes them absolute. Use raw location string instead.
591                    //
592                    // Unwrap is safe here since location is a valid URL
593                    let stripped = location.strip_prefix("file:").unwrap();
594                    match cwd {
595                        Some(cwd) => cwd.join(stripped),
596                        None => PathBuf::from(stripped),
597                    }
598                };
599                let path = match shellexpand::full(&path.to_string_lossy().to_string()) {
600                    Ok(s) => PathBuf::from(s.as_ref()),
601                    Err(e) => {
602                        log::error!("Failed to shell expand plugin path: {}", e);
603                        path
604                    },
605                };
606                Ok(Self::File(path))
607            },
608            "https" | "http" => Ok(Self::Remote(url.as_str().to_owned())),
609            _ => Err(PluginsConfigError::InvalidUrlScheme(url)),
610        }
611    }
612    pub fn display(&self) -> String {
613        match self {
614            RunPluginLocation::File(pathbuf) => format!("file:{}", pathbuf.display()),
615            RunPluginLocation::Zellij(plugin_tag) => format!("zellij:{}", plugin_tag),
616            RunPluginLocation::Remote(url) => String::from(url),
617        }
618    }
619}
620
621impl From<&RunPluginLocation> for Url {
622    fn from(location: &RunPluginLocation) -> Self {
623        let url = match location {
624            RunPluginLocation::File(path) => format!(
625                "file:{}",
626                path.clone().into_os_string().into_string().unwrap()
627            ),
628            RunPluginLocation::Zellij(tag) => format!("zellij:{}", tag),
629            RunPluginLocation::Remote(url) => String::from(url),
630        };
631        Self::parse(&url).unwrap()
632    }
633}
634
635impl fmt::Display for RunPluginLocation {
636    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
637        match self {
638            Self::File(path) => write!(
639                f,
640                "{}",
641                path.clone().into_os_string().into_string().unwrap()
642            ),
643            Self::Zellij(tag) => write!(f, "{}", tag),
644            Self::Remote(url) => write!(f, "{}", url),
645        }
646    }
647}
648
649#[derive(Clone, Debug, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize)]
650pub enum LayoutConstraint {
651    MaxPanes(usize),
652    MinPanes(usize),
653    ExactPanes(usize),
654    NoConstraint,
655}
656
657impl Display for LayoutConstraint {
658    fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
659        match self {
660            LayoutConstraint::MaxPanes(max_panes) => write!(f, "max_panes={}", max_panes),
661            LayoutConstraint::MinPanes(min_panes) => write!(f, "min_panes={}", min_panes),
662            LayoutConstraint::ExactPanes(exact_panes) => write!(f, "exact_panes={}", exact_panes),
663            LayoutConstraint::NoConstraint => write!(f, ""),
664        }
665    }
666}
667
668pub type SwapTiledLayout = (BTreeMap<LayoutConstraint, TiledPaneLayout>, Option<String>); // Option<String> is the swap layout name
669pub type SwapFloatingLayout = (
670    BTreeMap<LayoutConstraint, Vec<FloatingPaneLayout>>,
671    Option<String>,
672); // Option<String> is the swap layout name
673
674#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)]
675pub struct Layout {
676    pub tabs: Vec<(Option<String>, TiledPaneLayout, Vec<FloatingPaneLayout>)>,
677    pub focused_tab_index: Option<usize>,
678    pub template: Option<(TiledPaneLayout, Vec<FloatingPaneLayout>)>,
679    pub swap_layouts: Vec<(TiledPaneLayout, Vec<FloatingPaneLayout>)>,
680    pub swap_tiled_layouts: Vec<SwapTiledLayout>,
681    pub swap_floating_layouts: Vec<SwapFloatingLayout>,
682}
683
684#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
685pub enum PercentOrFixed {
686    Percent(usize), // 1 to 100
687    Fixed(usize),   // An absolute number of columns or rows
688}
689
690impl From<Dimension> for PercentOrFixed {
691    fn from(dimension: Dimension) -> Self {
692        match dimension.constraint {
693            Constraint::Percent(percent) => PercentOrFixed::Percent(percent as usize),
694            Constraint::Fixed(fixed_size) => PercentOrFixed::Fixed(fixed_size),
695        }
696    }
697}
698
699impl PercentOrFixed {
700    pub fn to_position(&self, whole: usize) -> usize {
701        match self {
702            PercentOrFixed::Percent(percent) => {
703                (whole as f64 / 100.0 * *percent as f64).ceil() as usize
704            },
705            PercentOrFixed::Fixed(fixed) => {
706                if *fixed > whole {
707                    whole
708                } else {
709                    *fixed
710                }
711            },
712        }
713    }
714}
715
716impl PercentOrFixed {
717    pub fn is_zero(&self) -> bool {
718        match self {
719            PercentOrFixed::Percent(percent) => *percent == 0,
720            PercentOrFixed::Fixed(fixed) => *fixed == 0,
721        }
722    }
723}
724
725impl FromStr for PercentOrFixed {
726    type Err = Box<dyn std::error::Error>;
727    fn from_str(s: &str) -> Result<Self, Self::Err> {
728        if s.chars().last() == Some('%') {
729            let char_count = s.chars().count();
730            let percent_size = usize::from_str_radix(&s[..char_count.saturating_sub(1)], 10)?;
731            if percent_size <= 100 {
732                Ok(PercentOrFixed::Percent(percent_size))
733            } else {
734                Err("Percent must be between 0 and 100".into())
735            }
736        } else {
737            let fixed_size = usize::from_str_radix(s, 10)?;
738            Ok(PercentOrFixed::Fixed(fixed_size))
739        }
740    }
741}
742
743#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)]
744pub struct FloatingPaneLayout {
745    pub name: Option<String>,
746    pub height: Option<PercentOrFixed>,
747    pub width: Option<PercentOrFixed>,
748    pub x: Option<PercentOrFixed>,
749    pub y: Option<PercentOrFixed>,
750    pub pinned: Option<bool>,
751    pub run: Option<Run>,
752    pub focus: Option<bool>,
753    pub already_running: bool,
754    pub pane_initial_contents: Option<String>,
755    pub logical_position: Option<usize>,
756}
757
758impl FloatingPaneLayout {
759    pub fn new() -> Self {
760        FloatingPaneLayout {
761            name: None,
762            height: None,
763            width: None,
764            x: None,
765            y: None,
766            pinned: None,
767            run: None,
768            focus: None,
769            already_running: false,
770            pane_initial_contents: None,
771            logical_position: None,
772        }
773    }
774    pub fn add_cwd_to_layout(&mut self, cwd: &PathBuf) {
775        match self.run.as_mut() {
776            Some(run) => run.add_cwd(cwd),
777            None => {
778                self.run = Some(Run::Cwd(cwd.clone()));
779            },
780        }
781    }
782    pub fn add_start_suspended(&mut self, start_suspended: Option<bool>) {
783        if let Some(run) = self.run.as_mut() {
784            run.add_start_suspended(start_suspended);
785        }
786    }
787}
788
789impl From<&TiledPaneLayout> for FloatingPaneLayout {
790    fn from(pane_layout: &TiledPaneLayout) -> Self {
791        FloatingPaneLayout {
792            name: pane_layout.name.clone(),
793            run: pane_layout.run.clone(),
794            focus: pane_layout.focus,
795            ..Default::default()
796        }
797    }
798}
799
800#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)]
801pub struct TiledPaneLayout {
802    pub children_split_direction: SplitDirection,
803    pub name: Option<String>,
804    pub children: Vec<TiledPaneLayout>,
805    pub split_size: Option<SplitSize>,
806    pub run: Option<Run>,
807    pub borderless: bool,
808    pub focus: Option<bool>,
809    pub external_children_index: Option<usize>,
810    pub children_are_stacked: bool,
811    pub is_expanded_in_stack: bool,
812    pub exclude_from_sync: Option<bool>,
813    pub run_instructions_to_ignore: Vec<Option<Run>>,
814    pub hide_floating_panes: bool, // only relevant if this is the base layout
815    pub pane_initial_contents: Option<String>,
816}
817
818impl TiledPaneLayout {
819    pub fn insert_children_layout(
820        &mut self,
821        children_layout: &mut TiledPaneLayout,
822    ) -> Result<bool, ConfigError> {
823        // returns true if successfully inserted and false otherwise
824        match self.external_children_index {
825            Some(external_children_index) => {
826                self.children
827                    .insert(external_children_index, children_layout.clone());
828                self.external_children_index = None;
829                Ok(true)
830            },
831            None => {
832                for pane in self.children.iter_mut() {
833                    if pane.insert_children_layout(children_layout)? {
834                        return Ok(true);
835                    }
836                }
837                Ok(false)
838            },
839        }
840    }
841    pub fn insert_children_nodes(
842        &mut self,
843        children_nodes: &mut Vec<TiledPaneLayout>,
844    ) -> Result<bool, ConfigError> {
845        // returns true if successfully inserted and false otherwise
846        match self.external_children_index {
847            Some(external_children_index) => {
848                children_nodes.reverse();
849                for child_node in children_nodes.drain(..) {
850                    self.children.insert(external_children_index, child_node);
851                }
852                self.external_children_index = None;
853                Ok(true)
854            },
855            None => {
856                for pane in self.children.iter_mut() {
857                    if pane.insert_children_nodes(children_nodes)? {
858                        return Ok(true);
859                    }
860                }
861                Ok(false)
862            },
863        }
864    }
865    pub fn children_block_count(&self) -> usize {
866        let mut count = 0;
867        if self.external_children_index.is_some() {
868            count += 1;
869        }
870        for pane in &self.children {
871            count += pane.children_block_count();
872        }
873        count
874    }
875    pub fn pane_count(&self) -> usize {
876        if self.children.is_empty() {
877            1 // self
878        } else {
879            let mut pane_count = 0;
880            for child in &self.children {
881                pane_count += child.pane_count();
882            }
883            pane_count
884        }
885    }
886    pub fn position_panes_in_space(
887        &self,
888        space: &PaneGeom,
889        max_panes: Option<usize>,
890        ignore_percent_split_sizes: bool,
891        focus_layout_if_not_focused: bool,
892    ) -> Result<Vec<(TiledPaneLayout, PaneGeom)>, &'static str> {
893        let layouts = match max_panes {
894            Some(max_panes) => {
895                let mut layout_to_split = self.clone();
896                let pane_count_in_layout = layout_to_split.pane_count();
897                if max_panes > pane_count_in_layout {
898                    // the + 1 here is because this was previously an "actual" pane and will now
899                    // become just a container, so we need to account for it too
900                    // TODO: make sure this works when the `children` node has sibling nodes,
901                    // because we really should support that
902                    let children_count = (max_panes - pane_count_in_layout) + 1;
903                    let mut extra_children = vec![TiledPaneLayout::default(); children_count];
904                    if !layout_to_split.has_focused_node() && focus_layout_if_not_focused {
905                        if let Some(last_child) = extra_children.last_mut() {
906                            last_child.focus = Some(true);
907                        }
908                    }
909                    let _ = layout_to_split.insert_children_nodes(&mut extra_children);
910                } else {
911                    layout_to_split.truncate(max_panes);
912                }
913                if !layout_to_split.has_focused_node() && focus_layout_if_not_focused {
914                    layout_to_split.focus_deepest_pane();
915                }
916
917                let mut stack_id = 0;
918                split_space(
919                    space,
920                    &layout_to_split,
921                    space,
922                    ignore_percent_split_sizes,
923                    &mut stack_id,
924                )?
925            },
926            None => {
927                let mut stack_id = 0;
928                split_space(
929                    space,
930                    self,
931                    space,
932                    ignore_percent_split_sizes,
933                    &mut stack_id,
934                )?
935            },
936        };
937        for (_pane_layout, pane_geom) in layouts.iter() {
938            if !pane_geom.is_at_least_minimum_size() {
939                return Err("No room on screen for this layout!");
940            }
941        }
942        Ok(layouts)
943    }
944    pub fn extract_run_instructions(&self) -> Vec<Option<Run>> {
945        // the order of these run instructions is significant and needs to be the same
946        // as the order of the "flattened" layout panes received from eg. position_panes_in_space
947        let mut run_instructions = vec![];
948        if self.children.is_empty() {
949            run_instructions.push(self.run.clone());
950        }
951        let mut run_instructions_of_children = vec![];
952        for child in &self.children {
953            let mut child_run_instructions = child.extract_run_instructions();
954            // add the only first child to run_instructions only adding the others after all the
955            // childfree panes have been added so that the returned vec will be sorted breadth-first
956            if !child_run_instructions.is_empty() {
957                run_instructions.push(child_run_instructions.remove(0));
958            }
959            run_instructions_of_children.append(&mut child_run_instructions);
960        }
961        run_instructions.append(&mut run_instructions_of_children);
962        let mut successfully_ignored = 0;
963        for instruction_to_ignore in &self.run_instructions_to_ignore {
964            if let Some(position) = run_instructions
965                .iter()
966                .position(|i| i == instruction_to_ignore)
967            {
968                run_instructions.remove(position);
969                successfully_ignored += 1;
970            }
971        }
972        // we need to do this because if we have an ignored instruction that does not match any
973        // running instruction, we'll have an extra pane and our state will be messed up and we'll
974        // crash (this can happen for example when breaking a plugin pane into a new tab that does
975        // not have room for it but has a terminal instead)
976        if successfully_ignored < self.run_instructions_to_ignore.len() {
977            for _ in 0..self
978                .run_instructions_to_ignore
979                .len()
980                .saturating_sub(successfully_ignored)
981            {
982                if let Some(position) = run_instructions.iter().position(|i| {
983                    match i {
984                        // this is because a bare CWD instruction should be overidden by a terminal
985                        // in run_instructions_to_ignore (for cases where the cwd for example comes
986                        // from a global layout cwd and the pane is actually just a bare pane that
987                        // wants to be overidden)
988                        Some(Run::Cwd(_)) | None => true,
989                        _ => false,
990                    }
991                }) {
992                    run_instructions.remove(position);
993                }
994            }
995        }
996        run_instructions
997    }
998    pub fn ignore_run_instruction(&mut self, run_instruction: Option<Run>) {
999        self.run_instructions_to_ignore.push(run_instruction);
1000    }
1001    pub fn with_one_pane() -> Self {
1002        let mut default_layout = TiledPaneLayout::default();
1003        default_layout.children = vec![TiledPaneLayout::default()];
1004        default_layout
1005    }
1006    pub fn add_cwd_to_layout(&mut self, cwd: &PathBuf) {
1007        match self.run.as_mut() {
1008            Some(run) => run.add_cwd(cwd),
1009            None => {
1010                self.run = Some(Run::Cwd(cwd.clone()));
1011            },
1012        }
1013        for child in self.children.iter_mut() {
1014            child.add_cwd_to_layout(cwd);
1015        }
1016    }
1017    pub fn populate_plugin_aliases_in_layout(&mut self, plugin_aliases: &PluginAliases) {
1018        match self.run.as_mut() {
1019            Some(run) => run.populate_run_plugin_if_needed(plugin_aliases),
1020            _ => {},
1021        }
1022        for child in self.children.iter_mut() {
1023            child.populate_plugin_aliases_in_layout(plugin_aliases);
1024        }
1025    }
1026    pub fn deepest_depth(&self) -> usize {
1027        let mut deepest_child_depth = 0;
1028        for child in self.children.iter() {
1029            let child_deepest_depth = child.deepest_depth();
1030            if child_deepest_depth > deepest_child_depth {
1031                deepest_child_depth = child_deepest_depth;
1032            }
1033        }
1034        deepest_child_depth + 1
1035    }
1036    pub fn focus_deepest_pane(&mut self) {
1037        let mut deepest_child_index = None;
1038        let mut deepest_path = 0;
1039        for (i, child) in self.children.iter().enumerate() {
1040            let child_deepest_path = child.deepest_depth();
1041            if child_deepest_path >= deepest_path {
1042                deepest_path = child_deepest_path;
1043                deepest_child_index = Some(i)
1044            }
1045        }
1046        match deepest_child_index {
1047            Some(deepest_child_index) => {
1048                if let Some(child) = self.children.get_mut(deepest_child_index) {
1049                    child.focus_deepest_pane();
1050                }
1051            },
1052            None => {
1053                self.focus = Some(true);
1054            },
1055        }
1056    }
1057    pub fn truncate(&mut self, max_panes: usize) -> usize {
1058        // returns remaining children length
1059        // if max_panes is 1, it means there's only enough panes for this node,
1060        // if max_panes is 0, this is probably the root layout being called with 0 max panes
1061        if max_panes <= 1 {
1062            while !self.children.is_empty() {
1063                // this is a special case: we're truncating a pane that was previously a logical
1064                // container but now should be an actual pane - so here we'd like to use its
1065                // deepest "non-logical" child in order to get all its attributes (eg. borderless)
1066                let first_child = self.children.remove(0);
1067                drop(std::mem::replace(self, first_child));
1068            }
1069            self.children.clear();
1070        } else if max_panes <= self.children.len() {
1071            self.children.truncate(max_panes);
1072            self.children.iter_mut().for_each(|l| l.children.clear());
1073        } else {
1074            let mut remaining_panes = max_panes
1075                - self
1076                    .children
1077                    .iter()
1078                    .filter(|c| c.children.is_empty())
1079                    .count();
1080            for child in self.children.iter_mut() {
1081                if remaining_panes > 1 && child.children.len() > 0 {
1082                    remaining_panes =
1083                        remaining_panes.saturating_sub(child.truncate(remaining_panes));
1084                } else {
1085                    child.children.clear();
1086                }
1087            }
1088        }
1089        if self.children.len() > 0 {
1090            self.children.len()
1091        } else {
1092            1 // just me
1093        }
1094    }
1095    pub fn has_focused_node(&self) -> bool {
1096        if self.focus.map(|f| f).unwrap_or(false) {
1097            return true;
1098        };
1099        for child in &self.children {
1100            if child.has_focused_node() {
1101                return true;
1102            }
1103        }
1104        false
1105    }
1106    pub fn recursively_add_start_suspended(&mut self, start_suspended: Option<bool>) {
1107        if let Some(run) = self.run.as_mut() {
1108            run.add_start_suspended(start_suspended);
1109        }
1110        for child in self.children.iter_mut() {
1111            child.recursively_add_start_suspended(start_suspended);
1112        }
1113    }
1114}
1115
1116#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
1117pub enum LayoutParts {
1118    Tabs(Vec<(Option<String>, Layout)>), // String is the tab name
1119    Panes(Vec<Layout>),
1120}
1121
1122impl LayoutParts {
1123    pub fn is_empty(&self) -> bool {
1124        match self {
1125            LayoutParts::Panes(panes) => panes.is_empty(),
1126            LayoutParts::Tabs(tabs) => tabs.is_empty(),
1127        }
1128    }
1129    pub fn insert_pane(&mut self, index: usize, layout: Layout) -> Result<(), ConfigError> {
1130        match self {
1131            LayoutParts::Panes(panes) => {
1132                panes.insert(index, layout);
1133                Ok(())
1134            },
1135            LayoutParts::Tabs(_tabs) => Err(ConfigError::new_layout_kdl_error(
1136                "Trying to insert a pane into a tab layout".into(),
1137                0,
1138                0,
1139            )),
1140        }
1141    }
1142}
1143
1144impl Default for LayoutParts {
1145    fn default() -> Self {
1146        LayoutParts::Panes(vec![])
1147    }
1148}
1149
1150impl Layout {
1151    pub fn list_available_layouts(
1152        layout_dir: Option<PathBuf>,
1153        default_layout_name: &Option<String>,
1154    ) -> Vec<LayoutInfo> {
1155        let mut available_layouts = layout_dir
1156            .clone()
1157            .or_else(|| default_layout_dir())
1158            .and_then(|layout_dir| match std::fs::read_dir(layout_dir) {
1159                Ok(layout_files) => Some(layout_files),
1160                Err(_) => None,
1161            })
1162            .map(|layout_files| {
1163                let mut available_layouts = vec![];
1164                for file in layout_files {
1165                    if let Ok(file) = file {
1166                        if file.path().extension().map(|e| e.to_ascii_lowercase())
1167                            == Some(std::ffi::OsString::from("kdl"))
1168                        {
1169                            if Layout::from_path_or_default_without_config(
1170                                Some(&file.path()),
1171                                layout_dir.clone(),
1172                            )
1173                            .is_ok()
1174                            {
1175                                if let Some(file_name) = file.path().file_stem() {
1176                                    available_layouts.push(LayoutInfo::File(
1177                                        file_name.to_string_lossy().to_string(),
1178                                    ))
1179                                }
1180                            }
1181                        }
1182                    }
1183                }
1184                available_layouts
1185            })
1186            .unwrap_or_else(Default::default);
1187        let default_layout_name = default_layout_name
1188            .as_ref()
1189            .map(|d| d.as_str())
1190            .unwrap_or("default");
1191        available_layouts.push(LayoutInfo::BuiltIn("default".to_owned()));
1192        available_layouts.push(LayoutInfo::BuiltIn("strider".to_owned()));
1193        available_layouts.push(LayoutInfo::BuiltIn("disable-status-bar".to_owned()));
1194        available_layouts.push(LayoutInfo::BuiltIn("compact".to_owned()));
1195        available_layouts.push(LayoutInfo::BuiltIn("classic".to_owned()));
1196        available_layouts.sort_by(|a, b| {
1197            let a_name = a.name();
1198            let b_name = b.name();
1199            if a_name == default_layout_name {
1200                return Ordering::Less;
1201            } else if b_name == default_layout_name {
1202                return Ordering::Greater;
1203            } else {
1204                a_name.cmp(&b_name)
1205            }
1206        });
1207        available_layouts
1208    }
1209    pub fn from_layout_info(
1210        layout_dir: &Option<PathBuf>,
1211        layout_info: LayoutInfo,
1212    ) -> Result<Layout, ConfigError> {
1213        let mut should_start_layout_commands_suspended = false;
1214        let (path_to_raw_layout, raw_layout, raw_swap_layouts) = match layout_info {
1215            LayoutInfo::File(layout_name_without_extension) => {
1216                let layout_dir = layout_dir.clone().or_else(|| default_layout_dir());
1217                let (path_to_layout, stringified_layout, swap_layouts) =
1218                    Self::stringified_from_dir(
1219                        &PathBuf::from(layout_name_without_extension),
1220                        layout_dir.as_ref(),
1221                    )?;
1222                (Some(path_to_layout), stringified_layout, swap_layouts)
1223            },
1224            LayoutInfo::BuiltIn(layout_name) => {
1225                let (path_to_layout, stringified_layout, swap_layouts) =
1226                    Self::stringified_from_default_assets(&PathBuf::from(layout_name))?;
1227                (Some(path_to_layout), stringified_layout, swap_layouts)
1228            },
1229            LayoutInfo::Url(url) => {
1230                should_start_layout_commands_suspended = true;
1231                (Some(url.clone()), Self::stringified_from_url(&url)?, None)
1232            },
1233            LayoutInfo::Stringified(stringified_layout) => (None, stringified_layout, None),
1234        };
1235        let mut layout = Layout::from_kdl(
1236            &raw_layout,
1237            path_to_raw_layout,
1238            raw_swap_layouts
1239                .as_ref()
1240                .map(|(r, f)| (r.as_str(), f.as_str())),
1241            None,
1242        );
1243        if should_start_layout_commands_suspended {
1244            layout
1245                .iter_mut()
1246                .next()
1247                .map(|l| l.recursively_add_start_suspended_including_template(Some(true)));
1248        }
1249        layout
1250    }
1251    pub fn stringified_from_path_or_default(
1252        layout_path: Option<&PathBuf>,
1253        layout_dir: Option<PathBuf>,
1254    ) -> Result<(String, String, Option<(String, String)>), ConfigError> {
1255        // (path_to_layout as String, stringified_layout, Option<path_to_swap_layout as String, stringified_swap_layout>)
1256        match layout_path {
1257            Some(layout_path) => {
1258                // The way we determine where to look for the layout is similar to
1259                // how a path would look for an executable.
1260                // See the gh issue for more: https://github.com/zellij-org/zellij/issues/1412#issuecomment-1131559720
1261                if layout_path.extension().is_some() || layout_path.components().count() > 1 {
1262                    // We look localy!
1263                    Layout::stringified_from_path(layout_path)
1264                } else {
1265                    // We look in the default dir
1266                    Layout::stringified_from_dir(layout_path, layout_dir.as_ref())
1267                }
1268            },
1269            None => Layout::stringified_from_dir(
1270                &std::path::PathBuf::from("default"),
1271                layout_dir.as_ref(),
1272            ),
1273        }
1274    }
1275    #[cfg(not(target_family = "wasm"))]
1276    pub fn stringified_from_url(url: &str) -> Result<String, ConfigError> {
1277        let raw_layout = task::block_on(async move {
1278            let download = Downloader::download_without_cache(url).await;
1279            match download {
1280                Ok(stringified) => Ok(stringified),
1281                Err(e) => Err(ConfigError::DownloadError(format!("{}", e))),
1282            }
1283        })?;
1284        Ok(raw_layout)
1285    }
1286    #[cfg(target_family = "wasm")]
1287    pub fn stringified_from_url(_url: &str) -> Result<String, ConfigError> {
1288        // silently fail - this should not happen in plugins and legacy architecture is hard
1289        let raw_layout = String::new();
1290        Ok(raw_layout)
1291    }
1292    pub fn from_path_or_default(
1293        layout_path: Option<&PathBuf>,
1294        layout_dir: Option<PathBuf>,
1295        config: Config,
1296    ) -> Result<(Layout, Config), ConfigError> {
1297        let (path_to_raw_layout, raw_layout, raw_swap_layouts) =
1298            Layout::stringified_from_path_or_default(layout_path, layout_dir)?;
1299        let layout = Layout::from_kdl(
1300            &raw_layout,
1301            Some(path_to_raw_layout),
1302            raw_swap_layouts
1303                .as_ref()
1304                .map(|(r, f)| (r.as_str(), f.as_str())),
1305            None,
1306        )?;
1307        let config = Config::from_kdl(&raw_layout, Some(config))?; // this merges the two config, with
1308        Ok((layout, config))
1309    }
1310    #[cfg(not(target_family = "wasm"))]
1311    pub fn from_url(url: &str, config: Config) -> Result<(Layout, Config), ConfigError> {
1312        let raw_layout = task::block_on(async move {
1313            let download = Downloader::download_without_cache(url).await;
1314            match download {
1315                Ok(stringified) => Ok(stringified),
1316                Err(e) => Err(ConfigError::DownloadError(format!("{}", e))),
1317            }
1318        })?;
1319        let mut layout = Layout::from_kdl(&raw_layout, Some(url.into()), None, None)?;
1320        layout.recursively_add_start_suspended_including_template(Some(true));
1321        let config = Config::from_kdl(&raw_layout, Some(config))?; // this merges the two config, with
1322        Ok((layout, config))
1323    }
1324    pub fn from_stringified_layout(
1325        stringified_layout: &str,
1326        config: Config,
1327    ) -> Result<(Layout, Config), ConfigError> {
1328        let layout = Layout::from_kdl(&stringified_layout, None, None, None)?;
1329        let config = Config::from_kdl(&stringified_layout, Some(config))?; // this merges the two config, with
1330        Ok((layout, config))
1331    }
1332    #[cfg(target_family = "wasm")]
1333    pub fn from_url(_url: &str, _config: Config) -> Result<(Layout, Config), ConfigError> {
1334        Err(ConfigError::DownloadError(format!(
1335            "Unsupported platform, cannot download layout from the web"
1336        )))
1337    }
1338    pub fn from_path_or_default_without_config(
1339        layout_path: Option<&PathBuf>,
1340        layout_dir: Option<PathBuf>,
1341    ) -> Result<Layout, ConfigError> {
1342        let (path_to_raw_layout, raw_layout, raw_swap_layouts) =
1343            Layout::stringified_from_path_or_default(layout_path, layout_dir)?;
1344        let layout = Layout::from_kdl(
1345            &raw_layout,
1346            Some(path_to_raw_layout),
1347            raw_swap_layouts
1348                .as_ref()
1349                .map(|(r, f)| (r.as_str(), f.as_str())),
1350            None,
1351        )?;
1352        Ok(layout)
1353    }
1354    pub fn from_default_assets(
1355        layout_name: &Path,
1356        _layout_dir: Option<PathBuf>,
1357        config: Config,
1358    ) -> Result<(Layout, Config), ConfigError> {
1359        let (path_to_raw_layout, raw_layout, raw_swap_layouts) =
1360            Layout::stringified_from_default_assets(layout_name)?;
1361        let layout = Layout::from_kdl(
1362            &raw_layout,
1363            Some(path_to_raw_layout),
1364            raw_swap_layouts
1365                .as_ref()
1366                .map(|(r, f)| (r.as_str(), f.as_str())),
1367            None,
1368        )?;
1369        let config = Config::from_kdl(&raw_layout, Some(config))?; // this merges the two config, with
1370        Ok((layout, config))
1371    }
1372    pub fn from_str(
1373        raw: &str,
1374        path_to_raw_layout: String,
1375        swap_layouts: Option<(&str, &str)>, // Option<path_to_swap_layout, stringified_swap_layout>
1376        cwd: Option<PathBuf>,
1377    ) -> Result<Layout, ConfigError> {
1378        Layout::from_kdl(raw, Some(path_to_raw_layout), swap_layouts, cwd)
1379    }
1380    pub fn stringified_from_dir(
1381        layout: &PathBuf,
1382        layout_dir: Option<&PathBuf>,
1383    ) -> Result<(String, String, Option<(String, String)>), ConfigError> {
1384        // (path_to_layout as String, stringified_layout, Option<path_to_swap_layout as String, stringified_swap_layout>)
1385        match layout_dir {
1386            Some(dir) => {
1387                let layout_path = &dir.join(layout);
1388                if layout_path.with_extension("kdl").exists() {
1389                    Self::stringified_from_path(layout_path)
1390                } else {
1391                    Layout::stringified_from_default_assets(layout)
1392                }
1393            },
1394            None => {
1395                let home = find_default_config_dir();
1396                let Some(home) = home else {
1397                    return Layout::stringified_from_default_assets(layout);
1398                };
1399
1400                let layout_path = &home.join(layout);
1401                Self::stringified_from_path(layout_path)
1402            },
1403        }
1404    }
1405    pub fn stringified_from_path(
1406        layout_path: &Path,
1407    ) -> Result<(String, String, Option<(String, String)>), ConfigError> {
1408        // (path_to_layout as String, stringified_layout, Option<path_to_swap_layout as String, stringified_swap_layout>)
1409        let mut layout_file = File::open(&layout_path)
1410            .or_else(|_| File::open(&layout_path.with_extension("kdl")))
1411            .map_err(|e| ConfigError::IoPath(e, layout_path.into()))?;
1412
1413        let swap_layout_and_path = Layout::swap_layout_and_path(&layout_path);
1414
1415        let mut kdl_layout = String::new();
1416        layout_file
1417            .read_to_string(&mut kdl_layout)
1418            .map_err(|e| ConfigError::IoPath(e, layout_path.into()))?;
1419        Ok((
1420            layout_path.as_os_str().to_string_lossy().into(),
1421            kdl_layout,
1422            swap_layout_and_path,
1423        ))
1424    }
1425    pub fn stringified_from_default_assets(
1426        path: &Path,
1427    ) -> Result<(String, String, Option<(String, String)>), ConfigError> {
1428        // (path_to_layout as String, stringified_layout, Option<path_to_swap_layout as String, stringified_swap_layout>)
1429        // TODO: ideally these should not be hard-coded
1430        // we should load layouts by name from the config
1431        // and load them from a hashmap or some such
1432        match path.to_str() {
1433            Some("default") => Ok((
1434                "Default layout".into(),
1435                Self::stringified_default_from_assets()?,
1436                Some((
1437                    "Default swap layout".into(),
1438                    Self::stringified_default_swap_from_assets()?,
1439                )),
1440            )),
1441            Some("strider") => Ok((
1442                "Strider layout".into(),
1443                Self::stringified_strider_from_assets()?,
1444                Some((
1445                    "Strider swap layout".into(),
1446                    Self::stringified_strider_swap_from_assets()?,
1447                )),
1448            )),
1449            Some("disable-status-bar") => Ok((
1450                "Disable Status Bar layout".into(),
1451                Self::stringified_disable_status_from_assets()?,
1452                None,
1453            )),
1454            Some("compact") => Ok((
1455                "Compact layout".into(),
1456                Self::stringified_compact_from_assets()?,
1457                Some((
1458                    "Compact layout swap".into(),
1459                    Self::stringified_compact_swap_from_assets()?,
1460                )),
1461            )),
1462            Some("classic") => Ok((
1463                "Classic layout".into(),
1464                Self::stringified_classic_from_assets()?,
1465                Some((
1466                    "Classiclayout swap".into(),
1467                    Self::stringified_classic_swap_from_assets()?,
1468                )),
1469            )),
1470            Some("welcome") => Ok((
1471                "Welcome screen layout".into(),
1472                Self::stringified_welcome_from_assets()?,
1473                None,
1474            )),
1475            None | Some(_) => Err(ConfigError::IoPath(
1476                std::io::Error::new(std::io::ErrorKind::Other, "The layout was not found"),
1477                path.into(),
1478            )),
1479        }
1480    }
1481    pub fn stringified_default_from_assets() -> Result<String, ConfigError> {
1482        Ok(String::from_utf8(setup::DEFAULT_LAYOUT.to_vec())?)
1483    }
1484    pub fn stringified_default_swap_from_assets() -> Result<String, ConfigError> {
1485        Ok(String::from_utf8(setup::DEFAULT_SWAP_LAYOUT.to_vec())?)
1486    }
1487    pub fn stringified_strider_from_assets() -> Result<String, ConfigError> {
1488        Ok(String::from_utf8(setup::STRIDER_LAYOUT.to_vec())?)
1489    }
1490    pub fn stringified_strider_swap_from_assets() -> Result<String, ConfigError> {
1491        Ok(String::from_utf8(setup::STRIDER_SWAP_LAYOUT.to_vec())?)
1492    }
1493
1494    pub fn stringified_disable_status_from_assets() -> Result<String, ConfigError> {
1495        Ok(String::from_utf8(setup::NO_STATUS_LAYOUT.to_vec())?)
1496    }
1497
1498    pub fn stringified_compact_from_assets() -> Result<String, ConfigError> {
1499        Ok(String::from_utf8(setup::COMPACT_BAR_LAYOUT.to_vec())?)
1500    }
1501
1502    pub fn stringified_compact_swap_from_assets() -> Result<String, ConfigError> {
1503        Ok(String::from_utf8(setup::COMPACT_BAR_SWAP_LAYOUT.to_vec())?)
1504    }
1505
1506    pub fn stringified_classic_from_assets() -> Result<String, ConfigError> {
1507        Ok(String::from_utf8(setup::CLASSIC_LAYOUT.to_vec())?)
1508    }
1509
1510    pub fn stringified_classic_swap_from_assets() -> Result<String, ConfigError> {
1511        Ok(String::from_utf8(setup::CLASSIC_SWAP_LAYOUT.to_vec())?)
1512    }
1513
1514    pub fn stringified_welcome_from_assets() -> Result<String, ConfigError> {
1515        Ok(String::from_utf8(setup::WELCOME_LAYOUT.to_vec())?)
1516    }
1517
1518    pub fn new_tab(&self) -> (TiledPaneLayout, Vec<FloatingPaneLayout>) {
1519        self.template.clone().unwrap_or_default()
1520    }
1521
1522    pub fn is_empty(&self) -> bool {
1523        !self.tabs.is_empty()
1524    }
1525    // TODO: do we need both of these?
1526    pub fn has_tabs(&self) -> bool {
1527        !self.tabs.is_empty()
1528    }
1529
1530    pub fn tabs(&self) -> Vec<(Option<String>, TiledPaneLayout, Vec<FloatingPaneLayout>)> {
1531        // String is the tab name
1532        self.tabs.clone()
1533    }
1534
1535    pub fn focused_tab_index(&self) -> Option<usize> {
1536        self.focused_tab_index
1537    }
1538
1539    pub fn recursively_add_start_suspended(&mut self, start_suspended: Option<bool>) {
1540        for (_tab_name, tiled_panes, floating_panes) in self.tabs.iter_mut() {
1541            tiled_panes.recursively_add_start_suspended(start_suspended);
1542            for floating_pane in floating_panes.iter_mut() {
1543                floating_pane.add_start_suspended(start_suspended);
1544            }
1545        }
1546    }
1547    pub fn recursively_add_start_suspended_including_template(
1548        &mut self,
1549        start_suspended: Option<bool>,
1550    ) {
1551        if let Some((tiled_panes_template, floating_panes_template)) = self.template.as_mut() {
1552            tiled_panes_template.recursively_add_start_suspended(start_suspended);
1553            for floating_pane in floating_panes_template.iter_mut() {
1554                floating_pane.add_start_suspended(start_suspended);
1555            }
1556        }
1557        for (_tab_name, tiled_panes, floating_panes) in self.tabs.iter_mut() {
1558            tiled_panes.recursively_add_start_suspended(start_suspended);
1559            for floating_pane in floating_panes.iter_mut() {
1560                floating_pane.add_start_suspended(start_suspended);
1561            }
1562        }
1563    }
1564    fn swap_layout_and_path(path: &Path) -> Option<(String, String)> {
1565        // Option<path, stringified_swap_layout>
1566        let mut swap_layout_path = PathBuf::from(path);
1567        swap_layout_path.set_extension("swap.kdl");
1568        match File::open(&swap_layout_path) {
1569            Ok(mut stringified_swap_layout_file) => {
1570                let mut swap_kdl_layout = String::new();
1571                match stringified_swap_layout_file.read_to_string(&mut swap_kdl_layout) {
1572                    Ok(..) => Some((
1573                        swap_layout_path.as_os_str().to_string_lossy().into(),
1574                        swap_kdl_layout,
1575                    )),
1576                    Err(_e) => None,
1577                }
1578            },
1579            Err(_e) => None,
1580        }
1581    }
1582    pub fn populate_plugin_aliases_in_layout(&mut self, plugin_aliases: &PluginAliases) {
1583        for tab in self.tabs.iter_mut() {
1584            tab.1.populate_plugin_aliases_in_layout(plugin_aliases);
1585            for floating_pane_layout in tab.2.iter_mut() {
1586                floating_pane_layout
1587                    .run
1588                    .as_mut()
1589                    .map(|f| f.populate_run_plugin_if_needed(&plugin_aliases));
1590            }
1591        }
1592        if let Some(template) = self.template.as_mut() {
1593            template.0.populate_plugin_aliases_in_layout(plugin_aliases);
1594            for floating_pane_layout in template.1.iter_mut() {
1595                floating_pane_layout
1596                    .run
1597                    .as_mut()
1598                    .map(|f| f.populate_run_plugin_if_needed(&plugin_aliases));
1599            }
1600        }
1601    }
1602    pub fn add_cwd_to_layout(&mut self, cwd: &PathBuf) {
1603        for (_, tiled_pane_layout, floating_panes) in self.tabs.iter_mut() {
1604            tiled_pane_layout.add_cwd_to_layout(&cwd);
1605            for floating_pane in floating_panes {
1606                floating_pane.add_cwd_to_layout(&cwd);
1607            }
1608        }
1609        if let Some((tiled_pane_layout, floating_panes)) = self.template.as_mut() {
1610            tiled_pane_layout.add_cwd_to_layout(&cwd);
1611            for floating_pane in floating_panes {
1612                floating_pane.add_cwd_to_layout(&cwd);
1613            }
1614        }
1615    }
1616    pub fn pane_count(&self) -> usize {
1617        let mut pane_count = 0;
1618        if let Some((tiled_pane_layout, floating_panes)) = self.template.as_ref() {
1619            pane_count += tiled_pane_layout.pane_count();
1620            for _ in floating_panes {
1621                pane_count += 1;
1622            }
1623        }
1624        for (_, tiled_pane_layout, floating_panes) in &self.tabs {
1625            pane_count += tiled_pane_layout.pane_count();
1626            for _ in floating_panes {
1627                pane_count += 1;
1628            }
1629        }
1630        pane_count
1631    }
1632}
1633
1634fn split_space(
1635    space_to_split: &PaneGeom,
1636    layout: &TiledPaneLayout,
1637    total_space_to_split: &PaneGeom,
1638    ignore_percent_split_sizes: bool,
1639    next_stack_id: &mut usize,
1640) -> Result<Vec<(TiledPaneLayout, PaneGeom)>, &'static str> {
1641    let sizes: Vec<Option<SplitSize>> = if layout.children_are_stacked {
1642        let index_of_expanded_pane = layout.children.iter().position(|p| p.is_expanded_in_stack);
1643        let mut sizes: Vec<Option<SplitSize>> = layout
1644            .children
1645            .iter()
1646            .map(|_part| Some(SplitSize::Fixed(1)))
1647            .collect();
1648        if let Some(index_of_expanded_pane) = index_of_expanded_pane {
1649            *sizes.get_mut(index_of_expanded_pane).unwrap() = None;
1650        } else if let Some(last_size) = sizes.last_mut() {
1651            *last_size = None;
1652        }
1653        if sizes.len() > space_to_split.rows.as_usize().saturating_sub(3) {
1654            // 4 is MIN_TERMINAL_HEIGHT, minus 1 because sizes also includes the expanded pane in
1655            // the stack
1656            return Err("Not enough room for stacked panes in this layout");
1657        }
1658        sizes
1659    } else if ignore_percent_split_sizes {
1660        layout
1661            .children
1662            .iter()
1663            .map(|part| match part.split_size {
1664                Some(SplitSize::Percent(_)) => None,
1665                split_size => split_size,
1666            })
1667            .collect()
1668    } else {
1669        layout.children.iter().map(|part| part.split_size).collect()
1670    };
1671
1672    let mut split_geom = Vec::new();
1673    let (
1674        mut current_position,
1675        split_dimension_space,
1676        inherited_dimension,
1677        total_split_dimension_space,
1678    ) = match layout.children_split_direction {
1679        SplitDirection::Vertical => (
1680            space_to_split.x,
1681            space_to_split.cols,
1682            space_to_split.rows,
1683            total_space_to_split.cols,
1684        ),
1685        SplitDirection::Horizontal => (
1686            space_to_split.y,
1687            space_to_split.rows,
1688            space_to_split.cols,
1689            total_space_to_split.rows,
1690        ),
1691    };
1692
1693    let min_size_for_panes = sizes.iter().fold(0, |acc, size| match size {
1694        Some(SplitSize::Percent(_)) | None => acc + 1, // TODO: minimum height/width as relevant here
1695        Some(SplitSize::Fixed(fixed)) => acc + fixed,
1696    });
1697    if min_size_for_panes > split_dimension_space.as_usize() {
1698        return Err("Not enough room for panes"); // TODO: use error infra
1699    }
1700
1701    let flex_parts = sizes.iter().filter(|s| s.is_none()).count();
1702    let total_fixed_size = sizes.iter().fold(0, |acc, s| {
1703        if let Some(SplitSize::Fixed(fixed)) = s {
1704            acc + fixed
1705        } else {
1706            acc
1707        }
1708    });
1709    let stacked = if layout.children_are_stacked {
1710        let stack_id = *next_stack_id;
1711        *next_stack_id += 1;
1712        Some(stack_id)
1713    } else {
1714        None
1715    };
1716
1717    let mut total_pane_size = 0;
1718    for (&size, _part) in sizes.iter().zip(&*layout.children) {
1719        let mut split_dimension = match size {
1720            Some(SplitSize::Percent(percent)) => Dimension::percent(percent as f64),
1721            Some(SplitSize::Fixed(size)) => Dimension::fixed(size),
1722            None => {
1723                let free_percent = if let Some(p) = split_dimension_space.as_percent() {
1724                    p - sizes
1725                        .iter()
1726                        .map(|&s| match s {
1727                            Some(SplitSize::Percent(ip)) => ip as f64,
1728                            _ => 0.0,
1729                        })
1730                        .sum::<f64>()
1731                } else {
1732                    panic!("Implicit sizing within fixed-size panes is not supported");
1733                };
1734                Dimension::percent(free_percent / flex_parts as f64)
1735            },
1736        };
1737
1738        split_dimension.adjust_inner(
1739            total_split_dimension_space
1740                .as_usize()
1741                .saturating_sub(total_fixed_size),
1742        );
1743        total_pane_size += split_dimension.as_usize();
1744
1745        let geom = match layout.children_split_direction {
1746            SplitDirection::Vertical => PaneGeom {
1747                x: current_position,
1748                y: space_to_split.y,
1749                cols: split_dimension,
1750                rows: inherited_dimension,
1751                stacked,
1752                is_pinned: false,
1753                logical_position: None,
1754            },
1755            SplitDirection::Horizontal => PaneGeom {
1756                x: space_to_split.x,
1757                y: current_position,
1758                cols: inherited_dimension,
1759                rows: split_dimension,
1760                stacked,
1761                is_pinned: false,
1762                logical_position: None,
1763            },
1764        };
1765        split_geom.push(geom);
1766        current_position += split_dimension.as_usize();
1767    }
1768    adjust_geoms_for_rounding_errors(
1769        total_pane_size,
1770        &mut split_geom,
1771        split_dimension_space,
1772        layout.children_split_direction,
1773    );
1774    let mut pane_positions = Vec::new();
1775    let mut pane_positions_with_children = Vec::new();
1776    for (i, part) in layout.children.iter().enumerate() {
1777        let part_position_and_size = split_geom.get(i).unwrap();
1778        if !part.children.is_empty() {
1779            let mut part_positions = split_space(
1780                part_position_and_size,
1781                part,
1782                total_space_to_split,
1783                ignore_percent_split_sizes,
1784                next_stack_id,
1785            )?;
1786            // add the only first child to pane_positions only adding the others after all the
1787            // childfree panes have been added so that the returned vec will be sorted breadth-first
1788            if !part_positions.is_empty() {
1789                pane_positions.push(part_positions.remove(0));
1790            }
1791            pane_positions_with_children.append(&mut part_positions);
1792        } else {
1793            let part = part.clone();
1794            pane_positions.push((part, *part_position_and_size));
1795        }
1796    }
1797    pane_positions.append(&mut pane_positions_with_children);
1798    if pane_positions.is_empty() {
1799        let layout = layout.clone();
1800        pane_positions.push((layout, space_to_split.clone()));
1801    }
1802    Ok(pane_positions)
1803}
1804
1805fn adjust_geoms_for_rounding_errors(
1806    total_pane_size: usize,
1807    split_geoms: &mut Vec<PaneGeom>,
1808    split_dimension_space: Dimension,
1809    children_split_direction: SplitDirection,
1810) {
1811    if total_pane_size < split_dimension_space.as_usize() {
1812        // add extra space from rounding errors to the last pane
1813
1814        let increase_by = split_dimension_space
1815            .as_usize()
1816            .saturating_sub(total_pane_size);
1817        let position_of_last_flexible_geom = split_geoms
1818            .iter()
1819            .rposition(|s_g| s_g.is_flexible_in_direction(children_split_direction));
1820        position_of_last_flexible_geom
1821            .map(|p| split_geoms.iter_mut().skip(p))
1822            .map(|mut flexible_geom_and_following_geoms| {
1823                if let Some(flexible_geom) = flexible_geom_and_following_geoms.next() {
1824                    match children_split_direction {
1825                        SplitDirection::Vertical => flexible_geom.cols.increase_inner(increase_by),
1826                        SplitDirection::Horizontal => {
1827                            flexible_geom.rows.increase_inner(increase_by)
1828                        },
1829                    }
1830                }
1831                for following_geom in flexible_geom_and_following_geoms {
1832                    match children_split_direction {
1833                        SplitDirection::Vertical => {
1834                            following_geom.x += increase_by;
1835                        },
1836                        SplitDirection::Horizontal => {
1837                            following_geom.y += increase_by;
1838                        },
1839                    }
1840                }
1841            });
1842    } else if total_pane_size > split_dimension_space.as_usize() {
1843        // remove extra space from rounding errors to the last pane
1844        let decrease_by = total_pane_size - split_dimension_space.as_usize();
1845        let position_of_last_flexible_geom = split_geoms
1846            .iter()
1847            .rposition(|s_g| s_g.is_flexible_in_direction(children_split_direction));
1848        position_of_last_flexible_geom
1849            .map(|p| split_geoms.iter_mut().skip(p))
1850            .map(|mut flexible_geom_and_following_geoms| {
1851                if let Some(flexible_geom) = flexible_geom_and_following_geoms.next() {
1852                    match children_split_direction {
1853                        SplitDirection::Vertical => flexible_geom.cols.decrease_inner(decrease_by),
1854                        SplitDirection::Horizontal => {
1855                            flexible_geom.rows.decrease_inner(decrease_by)
1856                        },
1857                    }
1858                }
1859                for following_geom in flexible_geom_and_following_geoms {
1860                    match children_split_direction {
1861                        SplitDirection::Vertical => {
1862                            following_geom.x = following_geom.x.saturating_sub(decrease_by)
1863                        },
1864                        SplitDirection::Horizontal => {
1865                            following_geom.y = following_geom.y.saturating_sub(decrease_by)
1866                        },
1867                    }
1868                }
1869            });
1870    }
1871}
1872
1873impl Default for SplitDirection {
1874    fn default() -> Self {
1875        SplitDirection::Horizontal
1876    }
1877}
1878
1879impl FromStr for SplitDirection {
1880    type Err = Box<dyn std::error::Error>;
1881    fn from_str(s: &str) -> Result<Self, Self::Err> {
1882        match s {
1883            "vertical" | "Vertical" => Ok(SplitDirection::Vertical),
1884            "horizontal" | "Horizontal" => Ok(SplitDirection::Horizontal),
1885            _ => Err("split direction must be either vertical or horizontal".into()),
1886        }
1887    }
1888}
1889
1890impl FromStr for SplitSize {
1891    type Err = Box<dyn std::error::Error>;
1892    fn from_str(s: &str) -> Result<Self, Self::Err> {
1893        if s.chars().last() == Some('%') {
1894            let char_count = s.chars().count();
1895            let percent_size = usize::from_str_radix(&s[..char_count.saturating_sub(1)], 10)?;
1896            if percent_size > 0 && percent_size <= 100 {
1897                Ok(SplitSize::Percent(percent_size))
1898            } else {
1899                Err("Percent must be between 0 and 100".into())
1900            }
1901        } else {
1902            let fixed_size = usize::from_str_radix(s, 10)?;
1903            Ok(SplitSize::Fixed(fixed_size))
1904        }
1905    }
1906}
1907
1908// The unit test location.
1909#[path = "./unit/layout_test.rs"]
1910#[cfg(test)]
1911mod layout_test;