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