1use std::path::PathBuf;
2
3use anyhow::Result;
4
5pub fn expand_tilde(path: &str) -> PathBuf {
6 if let Some(stripped) = path.strip_prefix("~/") {
7 if let Some(home) = dirs::home_dir() {
8 return home.join(stripped);
9 }
10 }
11 if path == "~" {
12 if let Some(home) = dirs::home_dir() {
13 return home;
14 }
15 }
16 PathBuf::from(path)
17}
18
19pub fn config_dir() -> PathBuf {
20 dirs::config_dir()
21 .map(|d| d.join("podbox"))
22 .unwrap_or_else(|| PathBuf::from("~/.config/podbox"))
23}
24
25pub fn profiles_dir() -> PathBuf {
27 config_dir().join("profiles")
28}
29
30pub fn find_config_path(name: &str) -> Option<PathBuf> {
36 let canonical = profiles_dir().join(format!("{name}.toml"));
37 if canonical.is_file() {
38 return Some(canonical);
39 }
40
41 let legacy = config_dir().join(format!("{name}.toml"));
42 if legacy.is_file() {
43 tracing::debug!(
44 "Using legacy config path '{}'. Run `podbox migrate` to move to profiles/.",
45 legacy.display()
46 );
47 return Some(legacy);
48 }
49
50 None
51}
52
53pub fn find_legacy_root_configs() -> Vec<PathBuf> {
55 let root = config_dir();
56 let profiles = profiles_dir();
57 let Ok(entries) = std::fs::read_dir(&root) else {
58 return vec![];
59 };
60 let mut out = Vec::new();
61 for entry in entries.flatten() {
62 let path = entry.path();
63 if !path.is_file() {
64 continue;
65 }
66 if path.extension().is_some_and(|ext| ext == "toml") {
67 if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
69 if profiles.join(format!("{stem}.toml")).is_file() {
70 continue;
71 }
72 out.push(path);
73 }
74 }
75 }
76 out.sort();
77 out
78}
79
80pub fn find_definition() -> Option<PathBuf> {
81 let new_local = PathBuf::from(".podbox.toml");
82 if new_local.exists() {
83 return Some(new_local);
84 }
85
86 let old_local = PathBuf::from(".podmgr.toml");
87 if old_local.exists() {
88 eprintln!(
89 "Warning: '.podmgr.toml' found. Rename it to '.podbox.toml' to silence this warning."
90 );
91 return Some(old_local);
92 }
93
94 let configs = list_configs();
96 if !configs.is_empty() {
97 if configs.len() > 1 {
98 eprintln!(
99 "Warning: multiple configuration files found in {}. Selecting '{}' alphabetically. Use --config to specify a different file.",
100 config_dir().display(),
101 configs[0].display()
102 );
103 }
104 return Some(configs.into_iter().next().unwrap());
105 }
106
107 None
108}
109
110pub fn list_configs() -> Vec<PathBuf> {
111 use std::collections::BTreeMap;
112 let mut map: BTreeMap<String, PathBuf> = BTreeMap::new();
113
114 let root = config_dir();
116 if let Ok(entries) = std::fs::read_dir(&root) {
117 for entry in entries.flatten() {
118 let path = entry.path();
119 if path.is_file() && path.extension().is_some_and(|ext| ext == "toml") {
120 if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
121 map.insert(stem.to_string(), path);
122 }
123 }
124 }
125 }
126
127 let pdir = profiles_dir();
129 if let Ok(entries) = std::fs::read_dir(&pdir) {
130 for entry in entries.flatten() {
131 let path = entry.path();
132 if path.is_file() && path.extension().is_some_and(|ext| ext == "toml") {
133 if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
134 map.insert(stem.to_string(), path);
135 }
136 }
137 }
138 }
139
140 let mut out: Vec<PathBuf> = map.into_values().collect();
141 out.sort();
142 out
143}
144
145pub fn active_context_path() -> PathBuf {
146 config_dir().join(".active")
147}
148
149pub fn read_active_context() -> Option<String> {
150 let path = active_context_path();
151 let content = std::fs::read_to_string(&path).ok()?;
152 let name = content.trim().to_string();
153 if name.is_empty() {
154 let _ = std::fs::remove_file(&path);
155 return None;
156 }
157 if find_config_path(&name).is_some() {
158 Some(name)
159 } else {
160 let _ = std::fs::remove_file(&path);
161 None
162 }
163}
164
165pub fn write_active_context(name: &str) -> Result<()> {
166 let path = active_context_path();
167 std::fs::create_dir_all(path.parent().unwrap())?;
168 std::fs::write(&path, name)?;
169 Ok(())
170}
171
172pub fn clear_active_context() -> Result<()> {
173 let path = active_context_path();
174 if path.exists() {
175 std::fs::remove_file(&path)?;
176 }
177 Ok(())
178}
179
180#[cfg(test)]
181mod tests {
182 use super::*;
183
184 #[test]
185 fn test_expand_tilde() {
186 let home = dirs::home_dir().unwrap();
187 assert_eq!(expand_tilde("~/foo"), home.join("foo"));
188 assert_eq!(expand_tilde("~"), home.clone());
189 assert_eq!(expand_tilde("/foo/bar"), PathBuf::from("/foo/bar"));
190 }
191}