tmux_layout/config/
loader.rs

1use shellexpand::LookupError;
2use std::env::VarError;
3use std::os::unix::prelude::OsStrExt;
4use std::path::{Path, PathBuf};
5use std::{fs, io};
6use thiserror::Error;
7
8use crate::show_warning;
9
10use super::{Config, PartialConfig};
11
12pub fn load_config_at(path: &Path) -> Result<Config, Error> {
13    let partial_config = load_partial_config_at(path)?;
14    let mut config = Config {
15        selected_session: partial_config.selected_session,
16        sessions: partial_config.sessions,
17        windows: partial_config.windows,
18        ..Default::default()
19    };
20
21    for included_path in partial_config.includes.0 {
22        let included_path = shellexpand::full(&included_path)?;
23        let included_path = path
24            .parent()
25            .unwrap()
26            .join(Path::new(included_path.as_ref()));
27
28        let mut included_config = load_config_at(&included_path)?;
29        // Merge sessions and windows
30        config.sessions.append(&mut included_config.sessions);
31        config.windows.append(&mut included_config.windows);
32
33        // Merge selected session
34        if let Some(select_session) = included_config.selected_session {
35            if config.selected_session.is_none() {
36                config.selected_session = Some(select_session);
37            } else {
38                show_warning(&format!(
39                    "ignoring selected session \"{}\" from {:?}",
40                    select_session, included_path
41                ))
42            }
43        }
44    }
45    Ok(config)
46}
47
48pub fn load_partial_config_at(path: &Path) -> Result<PartialConfig, Error> {
49    let config_bytes = fs::read(path).map_err(|error| Error::Io {
50        path: path.to_owned(),
51        error,
52    })?;
53
54    match path.extension().map(|s| s.as_bytes()) {
55        Some(b"toml") => {
56            let config_str =
57                std::str::from_utf8(&config_bytes).map_err(|err| Error::ParseError {
58                    path: path.to_owned(),
59                    message: format!("UTF-8 error: {}", err),
60                })?;
61
62            toml::from_str(config_str).map_err(|err| Error::ParseError {
63                path: path.to_owned(),
64                message: format!("{}", err),
65            })
66        }
67        Some(b"yml") | Some(b"yaml") => {
68            serde_yaml::from_slice(&config_bytes).map_err(|err| Error::ParseError {
69                path: path.to_owned(),
70                message: format!("{}", err),
71            })
72        }
73        _ => Err(Error::UnsupportedFormat),
74    }
75}
76
77pub fn find_default_config_file() -> Option<PathBuf> {
78    const BASENAME: &str = ".tmux-layout";
79    const EXTS: [&str; 3] = ["yaml", "yml", "toml"];
80
81    let current_dir = std::env::current_dir().ok()?;
82    let home_dir = dirs::home_dir()?;
83
84    for dir in &[current_dir, home_dir] {
85        for ext in &EXTS {
86            let file_path = dir.join(format!("{}.{}", BASENAME, ext));
87            if file_path.exists() {
88                return Some(file_path);
89            }
90        }
91    }
92
93    None
94}
95#[derive(Debug, Error)]
96pub enum Error {
97    #[error("failed to load config file at {path:?}: {error}")]
98    Io { path: PathBuf, error: io::Error },
99    #[error("failed to parse config file at {path:?}: {message}")]
100    ParseError { path: PathBuf, message: String },
101    #[error("unsupported config format (supported: YAML, TOML)")]
102    UnsupportedFormat,
103    #[error("variable lookup error: {0}")]
104    LookupError(#[from] LookupError<VarError>),
105}