1use std::path::{Path, PathBuf};
19
20pub fn detect(home: &Path, rc_file_name: &str, dropin_dir_name: &str) -> Option<PathBuf> {
27 let dir = home.join(dropin_dir_name);
28 if !dir.is_dir() {
29 return None;
30 }
31 let rc_contents = std::fs::read_to_string(home.join(rc_file_name)).ok()?;
32 if rc_references_dropin(&rc_contents, dropin_dir_name) {
33 Some(dir)
34 } else {
35 None
36 }
37}
38
39pub fn rc_references_dropin(rc_contents: &str, dropin_dir_name: &str) -> bool {
45 rc_contents.lines().any(|line| {
46 let trimmed = line.trim_start();
47 if trimmed.starts_with('#') {
48 return false;
49 }
50 line.contains(dropin_dir_name)
51 })
52}
53
54pub fn write(dir: &Path, filename: &str, content: &str, quiet: bool, label: &str) {
59 if let Err(e) = std::fs::create_dir_all(dir) {
60 tracing::error!("Cannot create {}: {e}", dir.display());
61 return;
62 }
63 let file = dir.join(filename);
64 let body = format!("{}\n", content.trim_end_matches('\n'));
65 if let Err(e) = std::fs::write(&file, body) {
66 tracing::error!("Cannot write {}: {e}", file.display());
67 return;
68 }
69 if !quiet {
70 eprintln!(" Installed {label} at {}", file.display());
71 }
72}
73
74pub fn remove(dir: &Path, filename: &str, quiet: bool, label: &str) {
76 let file = dir.join(filename);
77 if !file.exists() {
78 return;
79 }
80 if let Err(e) = std::fs::remove_file(&file) {
81 tracing::error!("Cannot remove {}: {e}", file.display());
82 return;
83 }
84 if !quiet {
85 println!(" Removed {label} from {}", file.display());
86 }
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92
93 fn fixture() -> tempfile::TempDir {
94 tempfile::tempdir().expect("tempdir")
95 }
96
97 #[test]
98 fn rc_references_dropin_matches_source_loop() {
99 let rc = r#"# top
100if [[ -d "$HOME/.zshenv.d" ]]; then
101 for f in "$HOME/.zshenv.d"/*.zsh(N); do source "$f"; done
102fi
103"#;
104 assert!(rc_references_dropin(rc, ".zshenv.d"));
105 }
106
107 #[test]
108 fn rc_references_dropin_ignores_comment_only_mentions() {
109 let rc = "# Once we have a ~/.zshenv.d we should adopt it.\nexport PATH=/usr/bin\n";
110 assert!(!rc_references_dropin(rc, ".zshenv.d"));
111 }
112
113 #[test]
114 fn rc_references_dropin_handles_empty_file() {
115 assert!(!rc_references_dropin("", ".zshenv.d"));
116 }
117
118 #[test]
119 fn detect_returns_none_without_directory() {
120 let tmp = fixture();
121 std::fs::write(
122 tmp.path().join(".zshenv"),
123 "for f in $HOME/.zshenv.d/*.zsh; do source $f; done\n",
124 )
125 .unwrap();
126 assert!(detect(tmp.path(), ".zshenv", ".zshenv.d").is_none());
127 }
128
129 #[test]
130 fn detect_returns_none_with_directory_but_no_reference() {
131 let tmp = fixture();
132 std::fs::create_dir_all(tmp.path().join(".zshenv.d")).unwrap();
133 std::fs::write(tmp.path().join(".zshenv"), "export PATH=/usr/bin\n").unwrap();
134 assert!(detect(tmp.path(), ".zshenv", ".zshenv.d").is_none());
135 }
136
137 #[test]
138 fn detect_returns_dir_when_loop_and_directory_present() {
139 let tmp = fixture();
140 std::fs::create_dir_all(tmp.path().join(".zshenv.d")).unwrap();
141 std::fs::write(
142 tmp.path().join(".zshenv"),
143 "if [[ -d \"$HOME/.zshenv.d\" ]]; then\n for f in $HOME/.zshenv.d/*.zsh; do source $f; done\nfi\n",
144 )
145 .unwrap();
146 let got = detect(tmp.path(), ".zshenv", ".zshenv.d");
147 assert_eq!(got, Some(tmp.path().join(".zshenv.d")));
148 }
149
150 #[test]
151 fn detect_returns_none_when_rc_file_missing() {
152 let tmp = fixture();
153 std::fs::create_dir_all(tmp.path().join(".zshenv.d")).unwrap();
154 assert!(detect(tmp.path(), ".zshenv", ".zshenv.d").is_none());
155 }
156
157 #[test]
158 fn write_then_remove_roundtrip() {
159 let tmp = fixture();
160 let dir = tmp.path().join(".zshenv.d");
161 write(&dir, "00-lean-ctx.zsh", "echo hi", true, "test");
162 let file = dir.join("00-lean-ctx.zsh");
163 assert!(file.exists());
164 let body = std::fs::read_to_string(&file).unwrap();
165 assert_eq!(body, "echo hi\n");
166 remove(&dir, "00-lean-ctx.zsh", true, "test");
167 assert!(!file.exists());
168 }
169
170 #[test]
171 fn write_creates_missing_directory() {
172 let tmp = fixture();
173 let dir = tmp.path().join("nested").join(".zshenv.d");
174 write(&dir, "00-lean-ctx.zsh", "echo hi", true, "test");
175 assert!(dir.join("00-lean-ctx.zsh").exists());
176 }
177
178 #[test]
179 fn write_is_idempotent_for_identical_content() {
180 let tmp = fixture();
181 let dir = tmp.path().join(".zshenv.d");
182 write(&dir, "00-lean-ctx.zsh", "echo hi", true, "test");
183 let first = std::fs::read(dir.join("00-lean-ctx.zsh")).unwrap();
184 write(&dir, "00-lean-ctx.zsh", "echo hi", true, "test");
185 let second = std::fs::read(dir.join("00-lean-ctx.zsh")).unwrap();
186 assert_eq!(first, second);
187 }
188
189 #[test]
190 fn write_overwrites_changed_content() {
191 let tmp = fixture();
192 let dir = tmp.path().join(".zshenv.d");
193 write(&dir, "00-lean-ctx.zsh", "echo old", true, "test");
194 write(&dir, "00-lean-ctx.zsh", "echo new", true, "test");
195 let body = std::fs::read_to_string(dir.join("00-lean-ctx.zsh")).unwrap();
196 assert_eq!(body, "echo new\n");
197 }
198
199 #[test]
200 fn remove_is_noop_when_file_missing() {
201 let tmp = fixture();
202 let dir = tmp.path().join(".zshenv.d");
203 std::fs::create_dir_all(&dir).unwrap();
204 remove(&dir, "00-lean-ctx.zsh", true, "test");
205 }
206
207 #[test]
208 fn remove_is_noop_when_directory_missing() {
209 let tmp = fixture();
210 let dir = tmp.path().join(".zshenv.d");
211 remove(&dir, "00-lean-ctx.zsh", true, "test");
213 }
214}