1use std::path::{Path, PathBuf};
2use std::sync::mpsc;
3use std::time::Duration;
4
5use anyhow::Result;
6use notify_debouncer_mini::{DebouncedEventKind, new_debouncer};
7
8pub fn wait_for_changes(flake_dir: &str) -> Result<PathBuf> {
17 let flake_path = Path::new(flake_dir).canonicalize()?;
18
19 let (tx, rx) = mpsc::channel();
20
21 let mut debouncer = new_debouncer(Duration::from_millis(500), tx)?;
22
23 debouncer
25 .watcher()
26 .watch(&flake_path, notify::RecursiveMode::Recursive)?;
27
28 loop {
30 match rx.recv() {
31 Ok(Ok(events)) => {
32 for event in &events {
33 if event.kind == DebouncedEventKind::Any && is_nix_file(&event.path) {
34 return Ok(event.path.clone());
35 }
36 }
37 }
38 Ok(Err(e)) => {
39 tracing::warn!("watch error: {e}");
40 }
41 Err(e) => {
42 anyhow::bail!("watch channel closed: {e}");
43 }
44 }
45 }
46}
47
48fn is_nix_file(path: &Path) -> bool {
50 let Some(ext) = path.extension() else {
51 return path
53 .file_name()
54 .is_some_and(|n| n == "flake.lock" || n == "flake.nix");
55 };
56
57 ext == "nix" || ext == "lock"
58}
59
60pub fn display_trigger(trigger: &Path, flake_dir: &str) -> String {
62 let flake_path = Path::new(flake_dir).canonicalize().ok();
63 if let Some(base) = flake_path
64 && let Ok(rel) = trigger.strip_prefix(&base)
65 {
66 return rel.display().to_string();
67 }
68 trigger
69 .file_name()
70 .map(|n| n.to_string_lossy().to_string())
71 .unwrap_or_else(|| trigger.display().to_string())
72}
73
74#[cfg(test)]
75mod tests {
76 use super::*;
77
78 #[test]
79 fn is_nix_file_flake_nix() {
80 assert!(is_nix_file(Path::new("/foo/flake.nix")));
81 }
82
83 #[test]
84 fn is_nix_file_flake_lock() {
85 assert!(is_nix_file(Path::new("/foo/flake.lock")));
86 }
87
88 #[test]
89 fn is_nix_file_module() {
90 assert!(is_nix_file(Path::new("/foo/bar/minimal-init.nix")));
91 }
92
93 #[test]
94 fn is_nix_file_rejects_rust() {
95 assert!(!is_nix_file(Path::new("/foo/main.rs")));
96 }
97
98 #[test]
99 fn is_nix_file_rejects_random() {
100 assert!(!is_nix_file(Path::new("/foo/README.md")));
101 }
102
103 #[test]
104 fn is_nix_file_lock_extension() {
105 assert!(is_nix_file(Path::new("/foo/something.lock")));
106 }
107
108 #[test]
109 fn display_trigger_relative() {
110 let dir = tempfile::tempdir().expect("temp dir");
112 let nix_file = dir.path().join("flake.nix");
113 std::fs::write(&nix_file, "").expect("write");
114
115 let result = display_trigger(&nix_file, dir.path().to_str().expect("utf8"));
116 assert_eq!(result, "flake.nix");
117 }
118
119 #[test]
120 fn display_trigger_fallback() {
121 let result = display_trigger(Path::new("/nonexistent/dir/foo.nix"), "/nonexistent/other");
122 assert_eq!(result, "foo.nix");
123 }
124}