rust_filesearch/px/
commands.rs1use crate::config::PxConfig;
6use crate::errors::{FsError, Result};
7use crate::px::index::ProjectIndex;
8use crate::px::search::ProjectSearcher;
9use chrono::Duration;
10use std::path::PathBuf;
11use std::process::Command;
12
13pub fn cmd_init() -> Result<()> {
15 PxConfig::init()
16}
17
18pub fn cmd_sync(index: &mut ProjectIndex, scan_dirs: &[PathBuf]) -> Result<()> {
20 if scan_dirs.is_empty() {
21 println!("⚠️ No scan directories configured!");
22 println!("Run `px init` to create a config file, then edit:");
23 println!(" {}", PxConfig::config_file_path()?.display());
24 return Ok(());
25 }
26
27 println!("Scanning {} directories...", scan_dirs.len());
28 for dir in scan_dirs {
29 println!(" • {}", dir.display());
30 }
31 println!();
32
33 let start = std::time::Instant::now();
34 let count = index.sync(scan_dirs)?;
35 let elapsed = start.elapsed();
36
37 println!("✓ Indexed {} projects in {:.2}s", count, elapsed.as_secs_f64());
38
39 Ok(())
40}
41
42pub fn cmd_list(index: &ProjectIndex, filter: Option<String>) -> Result<()> {
44 let mut projects: Vec<_> = index.sorted_projects();
45
46 if let Some(ref filter_name) = filter {
48 projects.retain(|p| match filter_name.as_str() {
49 "has-changes" => p.git_status.has_uncommitted,
50 "inactive-30d" => {
51 let cutoff = chrono::Utc::now() - Duration::days(30);
52 p.last_accessed.map_or(true, |t| t < cutoff)
53 }
54 "inactive-90d" => {
55 let cutoff = chrono::Utc::now() - Duration::days(90);
56 p.last_accessed.map_or(true, |t| t < cutoff)
57 }
58 _ => true,
59 });
60 }
61
62 if projects.is_empty() {
63 if filter.is_some() {
64 println!("No projects found matching filter");
65 } else {
66 println!("No projects indexed yet. Run `px sync` to scan for projects.");
67 }
68 return Ok(());
69 }
70
71 println!("{:<30} {:<15} {:<8}", "Project", "Branch", "Status");
73 println!("{}", "─".repeat(60));
74
75 for project in &projects {
77 let status = if project.git_status.has_uncommitted {
78 "⚠ changes"
79 } else if project.git_status.ahead > 0 {
80 "↑ ahead"
81 } else if project.git_status.behind > 0 {
82 "↓ behind"
83 } else {
84 "✓ clean"
85 };
86
87 println!(
88 "{:<30} {:<15} {:<8}",
89 truncate(&project.name, 28),
90 truncate(&project.git_status.current_branch, 13),
91 status
92 );
93 }
94
95 println!();
96 println!("Total: {} projects", projects.len());
97
98 Ok(())
99}
100
101pub fn cmd_open(index: &mut ProjectIndex, query: &str, editor: &str) -> Result<()> {
103 let searcher = ProjectSearcher::new();
104 let projects: Vec<_> = index.projects.values().cloned().collect();
105 let results = searcher.search(&projects, query);
106
107 if results.is_empty() {
108 println!("No projects found matching '{}'", query);
109 return Ok(());
110 }
111
112 let project = results[0];
113 let project_path = project.path.clone();
114 let project_name = project.name.clone();
115
116 println!("Opening {} in {} + iTerm2...", project_name, editor);
117 println!(" Path: {}", project_path.display());
118
119 let editor_status = Command::new(editor)
121 .arg(&project_path)
122 .status()
123 .map_err(|e| FsError::IoError {
124 context: format!("Failed to spawn editor '{}'", editor),
125 source: e,
126 })?;
127
128 if !editor_status.success() {
129 eprintln!("⚠️ Editor '{}' exited with error", editor);
130 }
131
132 let applescript = format!(
134 r#"
135 tell application "iTerm"
136 create window with default profile
137 tell current session of current window
138 write text "cd '{}'"
139 write text "clear"
140 end tell
141 end tell
142 "#,
143 project_path.display()
144 );
145
146 let iterm_result = Command::new("osascript")
147 .arg("-e")
148 .arg(&applescript)
149 .status();
150
151 match iterm_result {
152 Ok(status) if status.success() => {
153 println!("✓ Opened iTerm2 window at project directory");
154 }
155 Ok(_) => {
156 eprintln!("⚠️ Failed to open iTerm2 window (check if iTerm2 is installed)");
157 }
158 Err(e) => {
159 eprintln!("⚠️ Could not execute osascript: {}", e);
160 }
161 }
162
163 index.record_access(&project_path.to_string_lossy())?;
165
166 Ok(())
167}
168
169pub fn cmd_info(index: &ProjectIndex, query: &str) -> Result<()> {
171 let searcher = ProjectSearcher::new();
172 let projects: Vec<_> = index.projects.values().cloned().collect();
173 let results = searcher.search(&projects, query);
174
175 if results.is_empty() {
176 println!("No projects found matching '{}'", query);
177 return Ok(());
178 }
179
180 let project = results[0];
181
182 println!();
184 println!("📁 {}", project.name);
185 println!("{}", "=".repeat(60));
186
187 println!("Path: {}", project.path.display());
189 println!("Branch: {}", project.git_status.current_branch);
190
191 let status = if project.git_status.has_uncommitted {
193 "⚠️ Uncommitted changes"
194 } else {
195 "✓ Clean"
196 };
197 println!("Status: {}", status);
198
199 if project.git_status.ahead > 0 || project.git_status.behind > 0 {
201 println!(
202 "Sync: ↑ {} ahead, ↓ {} behind",
203 project.git_status.ahead, project.git_status.behind
204 );
205 }
206
207 if let Some(ref commit) = project.git_status.last_commit {
209 println!();
210 println!("Last commit:");
211 println!(" {} - {}", commit.hash, commit.message);
212 println!(
213 " by {} at {}",
214 commit.author,
215 commit.timestamp.format("%Y-%m-%d %H:%M")
216 );
217 }
218
219 if let Some(ref readme) = project.readme_excerpt {
221 println!();
222 println!("README:");
223 println!(" {}", readme);
224 }
225
226 if project.access_count > 0 {
228 println!();
229 println!("Access stats:");
230 println!(" Count: {}", project.access_count);
231 if let Some(last_access) = project.last_accessed {
232 println!(
233 " Last: {}",
234 last_access.format("%Y-%m-%d %H:%M")
235 );
236 }
237 println!(" Score: {:.1}", project.frecency_score);
238 }
239
240 println!();
241
242 Ok(())
243}
244
245fn truncate(s: &str, max_len: usize) -> String {
247 if s.len() <= max_len {
248 s.to_string()
249 } else {
250 format!("{}...", &s[..max_len - 3])
251 }
252}
253