1use std::path::{Path, PathBuf};
4
5use etcetera::BaseStrategy;
6
7use crate::config::ProjectPin;
8
9pub const PROJECT_FILE: &str = ".tracker.toml";
11
12#[derive(Debug, thiserror::Error)]
13pub enum PathsError {
14 #[error("no home directory found")]
15 NoHome(#[from] etcetera::HomeDirError),
16}
17
18pub fn config_file() -> Result<PathBuf, PathsError> {
21 Ok(etcetera::choose_base_strategy()?
22 .config_dir()
23 .join("ytcli")
24 .join("config.toml"))
25}
26
27#[must_use]
32pub fn find_project_pin(start: &Path) -> Option<(PathBuf, ProjectPin)> {
33 for dir in start.ancestors() {
34 let candidate = dir.join(PROJECT_FILE);
35 if !candidate.is_file() {
36 continue;
37 }
38 let Ok(text) = std::fs::read_to_string(&candidate) else {
39 continue;
40 };
41 match toml_from_str(&text) {
42 Some(pin) => return Some((candidate, pin)),
43 None => {
44 tracing::warn!(path = %candidate.display(), "ignoring malformed .tracker.toml");
45 }
46 }
47 }
48 None
49}
50
51fn toml_from_str(text: &str) -> Option<ProjectPin> {
52 use figment::Figment;
53 use figment::providers::{Format, Toml};
54
55 Figment::new().merge(Toml::string(text)).extract().ok()
56}
57
58#[cfg(test)]
59#[allow(clippy::expect_used)]
60mod tests {
61 use super::*;
62
63 #[test]
64 fn finds_pin_in_parent_directory() {
65 let root = tempfile::tempdir().expect("tempdir");
66 std::fs::write(
67 root.path().join(PROJECT_FILE),
68 "profile = \"work\"\nqueue = \"PROJ\"\n",
69 )
70 .expect("write pin");
71 let nested = root.path().join("a").join("b");
72 std::fs::create_dir_all(&nested).expect("create nested");
73
74 let (path, pin) = find_project_pin(&nested).expect("pin found");
75
76 assert_eq!(path, root.path().join(PROJECT_FILE));
77 assert_eq!(pin.profile.as_deref(), Some("work"));
78 assert_eq!(pin.queue.as_deref(), Some("PROJ"));
79 }
80
81 #[test]
82 fn malformed_pin_is_ignored_not_fatal() {
83 let root = tempfile::tempdir().expect("tempdir");
84 std::fs::write(root.path().join(PROJECT_FILE), "profile = [[[").expect("write pin");
85
86 assert!(find_project_pin(root.path()).is_none());
87 }
88}