1use fuzzy_matcher::FuzzyMatcher;
2use fuzzy_matcher::skim::SkimMatcherV2;
3use ignore::WalkBuilder;
4use std::path::Path;
5
6pub struct FileListingUtils;
8
9impl FileListingUtils {
10 pub fn list_files(
12 root_path: &Path,
13 query: Option<&str>,
14 max_results: Option<usize>,
15 ) -> Result<Vec<String>, std::io::Error> {
16 let mut files = Vec::new();
17
18 let walker = WalkBuilder::new(root_path)
20 .hidden(false) .build();
22
23 for entry in walker {
24 let entry = entry.map_err(|e| {
25 std::io::Error::other(format!("Failed to read directory entry: {e}"))
26 })?;
27
28 if entry.path() == root_path {
30 continue;
31 }
32
33 if let Ok(relative_path) = entry.path().strip_prefix(root_path) {
35 if let Some(path_str) = relative_path.to_str() {
36 if !path_str.is_empty() {
37 if entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false) {
39 files.push(format!("{path_str}/"));
40 } else {
41 files.push(path_str.to_string());
42 }
43 }
44 }
45 }
46 }
47
48 let mut filtered_files = if let Some(query) = query {
50 if query.is_empty() {
51 files
52 } else {
53 let matcher = SkimMatcherV2::default();
54 let mut scored_files: Vec<(i64, String)> = files
55 .into_iter()
56 .filter_map(|file| matcher.fuzzy_match(&file, query).map(|score| (score, file)))
57 .collect();
58
59 scored_files.sort_by(|a, b| b.0.cmp(&a.0));
61
62 scored_files.into_iter().map(|(_, file)| file).collect()
63 }
64 } else {
65 files
66 };
67
68 if let Some(max) = max_results {
70 if max > 0 && filtered_files.len() > max {
71 filtered_files.truncate(max);
72 }
73 }
74
75 Ok(filtered_files)
76 }
77}
78
79pub struct GitStatusUtils;
81
82impl GitStatusUtils {
83 pub fn get_git_status(repo_path: &Path) -> Result<String, std::io::Error> {
85 let mut result = String::new();
86
87 let repo = gix::discover(repo_path)
88 .map_err(|e| std::io::Error::other(format!("Failed to open git repository: {e}")))?;
89
90 match repo.head_name() {
92 Ok(Some(name)) => {
93 let branch = name.as_bstr().to_string();
94 let branch = branch.strip_prefix("refs/heads/").unwrap_or(&branch);
96 result.push_str(&format!("Current branch: {branch}\n\n"));
97 }
98 Ok(None) => {
99 result.push_str("Current branch: HEAD (detached)\n\n");
100 }
101 Err(e) => {
102 if e.to_string().contains("does not exist") {
104 result.push_str("Current branch: <unborn>\n\n");
105 } else {
106 return Err(std::io::Error::other(format!("Failed to get HEAD: {e}")));
107 }
108 }
109 }
110
111 let iter = repo
113 .status(gix::progress::Discard)
114 .map_err(|e| std::io::Error::other(format!("Failed to get git status: {e}")))?
115 .into_index_worktree_iter(Vec::new())
116 .map_err(|e| std::io::Error::other(format!("Failed to get git status: {e}")))?;
117 result.push_str("Status:\n");
118 use gix::bstr::ByteSlice;
119 use gix::status::index_worktree::iter::Summary;
120 let mut has_changes = false;
121 for item_res in iter {
122 let item = item_res
123 .map_err(|e| std::io::Error::other(format!("Failed to get git status: {e}")))?;
124 if let Some(summary) = item.summary() {
125 has_changes = true;
126 let path = item.rela_path().to_str_lossy();
127 let (status_char, wt_char) = match summary {
128 Summary::Added => (" ", "?"),
129 Summary::Removed => ("D", " "),
130 Summary::Modified => ("M", " "),
131 Summary::TypeChange => ("T", " "),
132 Summary::Renamed => ("R", " "),
133 Summary::Copied => ("C", " "),
134 Summary::IntentToAdd => ("A", " "),
135 Summary::Conflict => ("U", "U"),
136 };
137 result.push_str(&format!("{status_char}{wt_char} {path}\n"));
138 }
139 }
140 if !has_changes {
141 result.push_str("Working tree clean\n");
142 }
143
144 result.push_str("\nRecent commits:\n");
146 match repo.head_id() {
147 Ok(head_id) => {
148 let oid = head_id.detach();
149 let mut count = 0;
150 if let Ok(object) = repo.find_object(oid) {
151 if let Ok(commit) = object.try_into_commit() {
152 let summary_bytes = commit.message_raw_sloppy();
154 let summary = summary_bytes
155 .lines()
156 .next()
157 .and_then(|line| std::str::from_utf8(line).ok())
158 .unwrap_or("<no summary>");
159 let short_id = oid.to_hex().to_string();
160 let short_id = &short_id[..7.min(short_id.len())];
161 result.push_str(&format!("{short_id} {summary}\n"));
162 count = 1;
163 }
164 }
165 if count == 0 {
166 result.push_str("<no commits>\n");
167 }
168 }
169 Err(_) => {
170 result.push_str("<no commits>\n");
171 }
172 }
173
174 Ok(result)
175 }
176}
177
178pub struct DirectoryStructureUtils;
180
181impl DirectoryStructureUtils {
182 pub fn get_directory_structure(
184 root_path: &Path,
185 max_depth: usize,
186 ) -> Result<String, std::io::Error> {
187 let mut structure = vec![root_path.display().to_string()];
188
189 Self::collect_directory_paths(root_path, &mut structure, 0, max_depth)?;
191
192 structure.sort();
193 Ok(structure.join("\n"))
194 }
195
196 fn collect_directory_paths(
198 dir: &Path,
199 paths: &mut Vec<String>,
200 current_depth: usize,
201 max_depth: usize,
202 ) -> Result<(), std::io::Error> {
203 if current_depth >= max_depth {
204 return Ok(());
205 }
206
207 let entries = std::fs::read_dir(dir)?;
208 for entry in entries {
209 let entry = entry?;
210 let path = entry.path();
211
212 if let Some(rel_path) = path.file_name() {
214 let path_str = rel_path.to_string_lossy().to_string();
215 if path.is_dir() {
216 paths.push(format!("{path_str}/"));
217 Self::collect_directory_paths(&path, paths, current_depth + 1, max_depth)?;
218 } else {
219 paths.push(path_str);
220 }
221 }
222 }
223
224 Ok(())
225 }
226}
227
228pub struct EnvironmentUtils;
230
231impl EnvironmentUtils {
232 pub fn get_platform() -> &'static str {
234 if cfg!(target_os = "windows") {
235 "windows"
236 } else if cfg!(target_os = "macos") {
237 "macos"
238 } else if cfg!(target_os = "linux") {
239 "linux"
240 } else {
241 "unknown"
242 }
243 }
244
245 pub fn get_current_date() -> String {
247 use chrono::Local;
248 Local::now().format("%Y-%m-%d").to_string()
249 }
250
251 pub fn is_git_repo(path: &Path) -> bool {
253 gix::discover(path).is_ok()
254 }
255
256 pub fn read_readme(path: &Path) -> Option<String> {
258 let readme_path = path.join("README.md");
259 std::fs::read_to_string(readme_path).ok()
260 }
261
262 pub fn read_claude_md(path: &Path) -> Option<String> {
264 let claude_path = path.join("CLAUDE.md");
265 std::fs::read_to_string(claude_path).ok()
266 }
267}
268
269#[cfg(test)]
270mod tests {
271 use super::*;
272 use tempfile::tempdir;
273
274 #[test]
275 fn test_list_files_empty_dir() {
276 let temp_dir = tempdir().unwrap();
277 let files = FileListingUtils::list_files(temp_dir.path(), None, None).unwrap();
278 assert!(files.is_empty());
279 }
280
281 #[test]
282 fn test_list_files_with_content() {
283 let temp_dir = tempdir().unwrap();
284
285 std::fs::write(temp_dir.path().join("test.rs"), "test").unwrap();
287 std::fs::write(temp_dir.path().join("main.rs"), "main").unwrap();
288 std::fs::create_dir(temp_dir.path().join("src")).unwrap();
289 std::fs::write(temp_dir.path().join("src/lib.rs"), "lib").unwrap();
290
291 let files = FileListingUtils::list_files(temp_dir.path(), None, None).unwrap();
292 assert_eq!(files.len(), 4); assert!(files.contains(&"test.rs".to_string()));
294 assert!(files.contains(&"main.rs".to_string()));
295 assert!(files.contains(&"src/".to_string()));
296 assert!(files.contains(&"src/lib.rs".to_string()));
297 }
298
299 #[test]
300 fn test_list_files_with_query() {
301 let temp_dir = tempdir().unwrap();
302 std::fs::write(temp_dir.path().join("test.rs"), "test").unwrap();
303 std::fs::write(temp_dir.path().join("main.rs"), "main").unwrap();
304
305 let files = FileListingUtils::list_files(temp_dir.path(), Some("test"), None).unwrap();
306 assert_eq!(files.len(), 1);
307 assert_eq!(files[0], "test.rs");
308 }
309
310 #[test]
311 fn test_platform_detection() {
312 let platform = EnvironmentUtils::get_platform();
313 assert!(["windows", "macos", "linux", "unknown"].contains(&platform));
314 }
315
316 #[test]
317 fn test_date_format() {
318 let date = EnvironmentUtils::get_current_date();
319 assert_eq!(date.len(), 10);
321 assert_eq!(date.chars().nth(4), Some('-'));
322 assert_eq!(date.chars().nth(7), Some('-'));
323 }
324
325 #[test]
326 fn test_git_repo_detection() {
327 let temp_dir = tempdir().unwrap();
328 assert!(!EnvironmentUtils::is_git_repo(temp_dir.path()));
329
330 gix::init(temp_dir.path()).unwrap();
332 assert!(EnvironmentUtils::is_git_repo(temp_dir.path()));
333 }
334}