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