1#[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), #[serde(alias = "fixed")]
73 Fixed(usize), }
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 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 .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 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>), Cwd(PathBuf),
242}
243
244impl Run {
245 pub fn merge(base: &Option<Run>, other: &Option<Run>) -> Option<Run> {
246 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 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 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 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, 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 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 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 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 (&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 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 PathBuf::from(decoded_path.as_ref())
585 } else if location.starts_with("file:~") {
586 PathBuf::from(location.strip_prefix("file:").unwrap())
588 } else {
589 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>); pub type SwapFloatingLayout = (
670 BTreeMap<LayoutConstraint, Vec<FloatingPaneLayout>>,
671 Option<String>,
672); #[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), Fixed(usize), }
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, 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 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 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 } 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 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 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 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 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 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 if max_panes <= 1 {
1062 while !self.children.is_empty() {
1063 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 }
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)>), 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 match layout_path {
1257 Some(layout_path) => {
1258 if layout_path.extension().is_some() || layout_path.components().count() > 1 {
1262 Layout::stringified_from_path(layout_path)
1264 } else {
1265 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 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))?; 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))?; 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))?; 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))?; Ok((layout, config))
1371 }
1372 pub fn from_str(
1373 raw: &str,
1374 path_to_raw_layout: String,
1375 swap_layouts: Option<(&str, &str)>, 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 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 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 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 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 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 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 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, 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"); }
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 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 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 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#[path = "./unit/layout_test.rs"]
1910#[cfg(test)]
1911mod layout_test;