1use anyhow::Result;
2use console::style;
3use dialoguer::{theme::ColorfulTheme, Select};
4use inquire::Text;
5
6use super::provider::{ProviderFactory, SearchProvider};
7use super::{GitConfig, Repository, SearchQuery};
8use crate::ui::workflows::{execute_workflow, CloneWorkflow};
9use crate::workspace::manager::WorkspaceManager;
10
11pub struct SearchEngine {
12 providers: Vec<Box<dyn SearchProvider>>,
13}
14
15impl SearchEngine {
16 pub fn new(config: &GitConfig) -> Result<Self> {
17 let mut providers = Vec::new();
18
19 for provider_name in &config.search_providers {
20 match ProviderFactory::create_provider(provider_name) {
21 Ok(provider) => providers.push(provider),
22 Err(e) => eprintln!("Warning: Failed to create provider '{provider_name}': {e}"),
23 }
24 }
25
26 if providers.is_empty() {
27 anyhow::bail!("No search providers available");
28 }
29
30 Ok(Self { providers })
31 }
32
33 pub fn add_provider(&mut self, provider: Box<dyn SearchProvider>) {
34 self.providers.push(provider);
35 }
36
37 pub async fn search(&self, query: &SearchQuery) -> Result<Vec<Repository>> {
38 let mut all_results = Vec::new();
39
40 for provider in &self.providers {
41 match provider.search(query).await {
42 Ok(results) => all_results.extend(results),
43 Err(e) => eprintln!(
44 "Warning: Search failed for provider '{}': {}",
45 provider.name(),
46 e
47 ),
48 }
49 }
50
51 all_results.sort_by(|a, b| a.full_name.cmp(&b.full_name));
53 all_results.dedup_by(|a, b| a.full_name == b.full_name);
54
55 Ok(all_results)
56 }
57}
58
59pub struct SearchCommand;
60
61impl SearchCommand {
62 pub async fn execute_with_workflow(
64 workspace_manager: &mut WorkspaceManager,
65 config: &GitConfig,
66 ) -> Result<()> {
67 Self::execute_interactive(workspace_manager, config).await
68 }
69
70 pub async fn execute_with_query(
72 query: &str,
73 workspace_manager: &mut WorkspaceManager,
74 config: &GitConfig,
75 ) -> Result<()> {
76 println!(
77 "\n{} Searching for repositories matching '{}'...",
78 style("đ").blue(),
79 style(query).cyan().bold()
80 );
81
82 let search_query = SearchQuery {
83 keywords: query.split_whitespace().map(|s| s.to_string()).collect(),
84 tags: vec![],
85 language: None,
86 organization: None,
87 limit: Some(20),
88 sort: Default::default(),
89 };
90
91 let engine = SearchEngine::new(config)?;
92 let results = engine.search(&search_query).await?;
93
94 if results.is_empty() {
95 println!(
96 "{} No repositories found for '{}'",
97 style("â").red(),
98 query
99 );
100 return Ok(());
101 }
102
103 println!(
104 "\n{} Found {} matching repositories:",
105 style("đĻ").green(),
106 style(results.len()).green().bold()
107 );
108
109 let selected_repo = Self::display_interactive_results(&results, workspace_manager)?;
111
112 if let Some(repo) = selected_repo {
113 let workflow = CloneWorkflow {
115 url: repo.url.clone(),
116 app: None, };
118
119 execute_workflow(Box::new(workflow), workspace_manager).await?;
120 } else {
121 println!("{} No repository selected", style("âšī¸").blue());
122 }
123
124 Ok(())
125 }
126
127 pub async fn execute_interactive(
128 workspace_manager: &mut WorkspaceManager,
129 config: &GitConfig,
130 ) -> Result<()> {
131 println!(
132 "\n{} {} {}",
133 style("đ").blue(),
134 style("GitHub Repository Search").cyan().bold(),
135 style("- Find and clone repositories").dim()
136 );
137
138 let query = Text::new("\nSearch GitHub repositories:")
140 .with_placeholder("e.g., rust web framework")
141 .prompt()?;
142
143 if query.trim().is_empty() {
144 println!("{} Search cancelled", style("â").red());
145 return Ok(());
146 }
147
148 println!("\n{} Searching repositories...", style("đ").blue());
149
150 let search_query = SearchQuery {
151 keywords: query.split_whitespace().map(|s| s.to_string()).collect(),
152 tags: vec![],
153 language: None,
154 organization: None,
155 limit: Some(20), sort: Default::default(), };
158
159 let engine = SearchEngine::new(config)?;
160 let results = engine.search(&search_query).await?;
161
162 if results.is_empty() {
163 println!(
164 "{} No repositories found for '{}'",
165 style("â").red(),
166 query
167 );
168 return Ok(());
169 }
170
171 println!(
172 "\n{} {} {} {}",
173 style("đĻ").green(),
174 style("Found").green().bold(),
175 style(format!("{} repositories", results.len())).dim(),
176 style(format!("(sorted by: {})", search_query.sort.display_name())).dim()
177 );
178
179 let selected_repo = Self::display_interactive_results(&results, workspace_manager)?;
181
182 if let Some(repo) = selected_repo {
183 let workflow = CloneWorkflow {
185 url: repo.url.clone(),
186 app: None, };
188
189 execute_workflow(Box::new(workflow), workspace_manager).await?;
190 } else {
191 println!("{} No repository selected", style("âšī¸").blue());
192 }
193
194 Ok(())
195 }
196
197 fn display_interactive_results(
198 results: &[Repository],
199 workspace_manager: &WorkspaceManager,
200 ) -> Result<Option<Repository>> {
201 let items: Vec<String> = results
202 .iter()
203 .map(|repo| {
204 let stars = if repo.stars > 0 {
205 format!("â {} ", Self::format_stars(repo.stars))
206 } else {
207 "".to_string()
208 };
209
210 let lang = if let Some(language) = &repo.language {
211 format!(" [{language}]")
212 } else {
213 "".to_string()
214 };
215
216 let license = if let Some(license_key) = &repo.license {
217 format!(" [{}]", license_key.to_uppercase())
218 } else {
219 "".to_string()
220 };
221
222 let desc = repo.description.as_deref().unwrap_or("No description");
223 let truncated_desc = if desc.chars().count() > 40 {
224 let truncated: String = desc.chars().take(40).collect();
225 format!("{truncated}...")
226 } else {
227 desc.to_string()
228 };
229
230 format!(
231 "{}{}{}{} - {}",
232 stars, repo.full_name, lang, license, truncated_desc
233 )
234 })
235 .collect();
236
237 let selection = Select::with_theme(&ColorfulTheme::default())
238 .with_prompt("Select repository to clone (ESC to cancel)")
239 .items(&items)
240 .default(0)
241 .max_length(workspace_manager.get_git_search_results_page_size())
242 .interact_opt()?;
243
244 Ok(selection.map(|i| results[i].clone()))
245 }
246
247 fn format_stars(count: u32) -> String {
248 if count >= 1000 {
249 format!("{:.1}k", count as f32 / 1000.0)
250 } else {
251 count.to_string()
252 }
253 }
254
255 pub async fn execute_advanced_search(
257 workspace_manager: &mut WorkspaceManager,
258 config: &GitConfig,
259 query: Option<String>,
260 language: Option<String>,
261 organization: Option<String>,
262 ) -> Result<()> {
263 println!(
264 "\n{} {} {}",
265 style("đ").blue(),
266 style("Advanced GitHub Search").cyan().bold(),
267 style("- Find repositories with filters").dim()
268 );
269
270 let query = if let Some(q) = query {
272 q
273 } else {
274 Text::new("\nSearch GitHub repositories:")
275 .with_placeholder("e.g., rust web framework")
276 .prompt()?
277 };
278
279 if query.trim().is_empty() {
280 println!("{} Search cancelled", style("â").red());
281 return Ok(());
282 }
283
284 println!("\n{} Searching repositories...", style("đ").blue());
285
286 let search_query = SearchQuery {
287 keywords: query.split_whitespace().map(|s| s.to_string()).collect(),
288 tags: vec![],
289 language,
290 organization,
291 limit: Some(30), sort: Default::default(),
293 };
294
295 let engine = SearchEngine::new(config)?;
296 let results = engine.search(&search_query).await?;
297
298 if results.is_empty() {
299 println!(
300 "{} No repositories found for '{}'",
301 style("â").red(),
302 query
303 );
304 return Ok(());
305 }
306
307 println!(
308 "\n{} {} {} {}",
309 style("đĻ").green(),
310 style("Found").green().bold(),
311 style(format!("{} repositories", results.len())).dim(),
312 style(format!("(sorted by: {})", search_query.sort.display_name())).dim()
313 );
314
315 let selected_repo = Self::display_enhanced_results(&results, workspace_manager)?;
317
318 if let Some(repo) = selected_repo {
319 let workflow = CloneWorkflow {
321 url: repo.url.clone(),
322 app: None, };
324
325 execute_workflow(Box::new(workflow), workspace_manager).await?;
326 } else {
327 println!("{} No repository selected", style("âšī¸").blue());
328 }
329
330 Ok(())
331 }
332
333 fn display_enhanced_results(
335 results: &[Repository],
336 workspace_manager: &WorkspaceManager,
337 ) -> Result<Option<Repository>> {
338 let items: Vec<String> = results
339 .iter()
340 .enumerate()
341 .map(|(i, repo)| {
342 let stars = if repo.stars > 0 {
343 format!("â {} ", Self::format_stars(repo.stars))
344 } else {
345 "".to_string()
346 };
347
348 let lang = if let Some(language) = &repo.language {
349 format!(" [{language}]")
350 } else {
351 "".to_string()
352 };
353
354 let license = if let Some(license_key) = &repo.license {
355 format!(" [{}]", license_key.to_uppercase())
356 } else {
357 "".to_string()
358 };
359
360 let desc = repo.description.as_deref().unwrap_or("No description");
361 let truncated_desc = if desc.chars().count() > 50 {
362 let truncated: String = desc.chars().take(50).collect();
363 format!("{truncated}...")
364 } else {
365 desc.to_string()
366 };
367
368 format!(
370 "{:2}. {}{}{}{} - {}",
371 i + 1,
372 stars,
373 repo.full_name,
374 lang,
375 license,
376 truncated_desc
377 )
378 })
379 .collect();
380
381 let selection = Select::with_theme(&ColorfulTheme::default())
382 .with_prompt("Select repository to clone (ESC to cancel)")
383 .items(&items)
384 .default(0)
385 .max_length(workspace_manager.get_git_search_results_page_size())
386 .interact_opt()?;
387
388 Ok(selection.map(|i| results[i].clone()))
389 }
390}