use anyhow::Result;
use console::style;
use dialoguer::{theme::ColorfulTheme, Select};
use inquire::Text;
use super::provider::{ProviderFactory, SearchProvider};
use super::{GitConfig, Repository, SearchQuery};
use crate::ui::workflows::{execute_workflow, CloneWorkflow};
use crate::workspace::manager::WorkspaceManager;
pub struct SearchEngine {
providers: Vec<Box<dyn SearchProvider>>,
}
impl SearchEngine {
pub fn new(config: &GitConfig) -> Result<Self> {
let mut providers = Vec::new();
for provider_name in &config.search_providers {
match ProviderFactory::create_provider(provider_name) {
Ok(provider) => providers.push(provider),
Err(e) => eprintln!("Warning: Failed to create provider '{provider_name}': {e}"),
}
}
if providers.is_empty() {
anyhow::bail!("No search providers available");
}
Ok(Self { providers })
}
pub fn add_provider(&mut self, provider: Box<dyn SearchProvider>) {
self.providers.push(provider);
}
pub async fn search(&self, query: &SearchQuery) -> Result<Vec<Repository>> {
let mut all_results = Vec::new();
for provider in &self.providers {
match provider.search(query).await {
Ok(results) => all_results.extend(results),
Err(e) => eprintln!(
"Warning: Search failed for provider '{}': {}",
provider.name(),
e
),
}
}
all_results.sort_by(|a, b| a.full_name.cmp(&b.full_name));
all_results.dedup_by(|a, b| a.full_name == b.full_name);
Ok(all_results)
}
}
pub struct SearchCommand;
impl SearchCommand {
pub async fn execute_with_workflow(
workspace_manager: &mut WorkspaceManager,
config: &GitConfig,
) -> Result<()> {
Self::execute_interactive(workspace_manager, config).await
}
pub async fn execute_with_query(
query: &str,
workspace_manager: &mut WorkspaceManager,
config: &GitConfig,
) -> Result<()> {
println!(
"\n{} Searching for repositories matching '{}'...",
style("đ").blue(),
style(query).cyan().bold()
);
let search_query = SearchQuery {
keywords: query.split_whitespace().map(|s| s.to_string()).collect(),
tags: vec![],
language: None,
organization: None,
limit: Some(20),
sort: Default::default(),
};
let engine = SearchEngine::new(config)?;
let results = engine.search(&search_query).await?;
if results.is_empty() {
println!(
"{} No repositories found for '{}'",
style("â").red(),
query
);
return Ok(());
}
println!(
"\n{} Found {} matching repositories:",
style("đĻ").green(),
style(results.len()).green().bold()
);
let selected_repo = Self::display_interactive_results(&results, workspace_manager)?;
if let Some(repo) = selected_repo {
let workflow = CloneWorkflow {
url: repo.url.clone(),
app: None, };
execute_workflow(Box::new(workflow), workspace_manager).await?;
} else {
println!("{} No repository selected", style("âšī¸").blue());
}
Ok(())
}
pub async fn execute_interactive(
workspace_manager: &mut WorkspaceManager,
config: &GitConfig,
) -> Result<()> {
println!(
"\n{} {} {}",
style("đ").blue(),
style("GitHub Repository Search").cyan().bold(),
style("- Find and clone repositories").dim()
);
let query = Text::new("\nSearch GitHub repositories:")
.with_placeholder("e.g., rust web framework")
.prompt()?;
if query.trim().is_empty() {
println!("{} Search cancelled", style("â").red());
return Ok(());
}
println!("\n{} Searching repositories...", style("đ").blue());
let search_query = SearchQuery {
keywords: query.split_whitespace().map(|s| s.to_string()).collect(),
tags: vec![],
language: None,
organization: None,
limit: Some(20), sort: Default::default(), };
let engine = SearchEngine::new(config)?;
let results = engine.search(&search_query).await?;
if results.is_empty() {
println!(
"{} No repositories found for '{}'",
style("â").red(),
query
);
return Ok(());
}
println!(
"\n{} {} {} {}",
style("đĻ").green(),
style("Found").green().bold(),
style(format!("{} repositories", results.len())).dim(),
style(format!("(sorted by: {})", search_query.sort.display_name())).dim()
);
let selected_repo = Self::display_interactive_results(&results, workspace_manager)?;
if let Some(repo) = selected_repo {
let workflow = CloneWorkflow {
url: repo.url.clone(),
app: None, };
execute_workflow(Box::new(workflow), workspace_manager).await?;
} else {
println!("{} No repository selected", style("âšī¸").blue());
}
Ok(())
}
fn display_interactive_results(
results: &[Repository],
workspace_manager: &WorkspaceManager,
) -> Result<Option<Repository>> {
let items: Vec<String> = results
.iter()
.map(|repo| {
let stars = if repo.stars > 0 {
format!("â {} ", Self::format_stars(repo.stars))
} else {
"".to_string()
};
let lang = if let Some(language) = &repo.language {
format!(" [{language}]")
} else {
"".to_string()
};
let license = if let Some(license_key) = &repo.license {
format!(" [{}]", license_key.to_uppercase())
} else {
"".to_string()
};
let desc = repo.description.as_deref().unwrap_or("No description");
let truncated_desc = if desc.chars().count() > 40 {
let truncated: String = desc.chars().take(40).collect();
format!("{truncated}...")
} else {
desc.to_string()
};
format!(
"{}{}{}{} - {}",
stars, repo.full_name, lang, license, truncated_desc
)
})
.collect();
let selection = Select::with_theme(&ColorfulTheme::default())
.with_prompt("Select repository to clone (ESC to cancel)")
.items(&items)
.default(0)
.max_length(workspace_manager.get_git_search_results_page_size())
.interact_opt()?;
Ok(selection.map(|i| results[i].clone()))
}
fn format_stars(count: u32) -> String {
if count >= 1000 {
format!("{:.1}k", count as f32 / 1000.0)
} else {
count.to_string()
}
}
pub async fn execute_advanced_search(
workspace_manager: &mut WorkspaceManager,
config: &GitConfig,
query: Option<String>,
language: Option<String>,
organization: Option<String>,
) -> Result<()> {
println!(
"\n{} {} {}",
style("đ").blue(),
style("Advanced GitHub Search").cyan().bold(),
style("- Find repositories with filters").dim()
);
let query = if let Some(q) = query {
q
} else {
Text::new("\nSearch GitHub repositories:")
.with_placeholder("e.g., rust web framework")
.prompt()?
};
if query.trim().is_empty() {
println!("{} Search cancelled", style("â").red());
return Ok(());
}
println!("\n{} Searching repositories...", style("đ").blue());
let search_query = SearchQuery {
keywords: query.split_whitespace().map(|s| s.to_string()).collect(),
tags: vec![],
language,
organization,
limit: Some(30), sort: Default::default(),
};
let engine = SearchEngine::new(config)?;
let results = engine.search(&search_query).await?;
if results.is_empty() {
println!(
"{} No repositories found for '{}'",
style("â").red(),
query
);
return Ok(());
}
println!(
"\n{} {} {} {}",
style("đĻ").green(),
style("Found").green().bold(),
style(format!("{} repositories", results.len())).dim(),
style(format!("(sorted by: {})", search_query.sort.display_name())).dim()
);
let selected_repo = Self::display_enhanced_results(&results, workspace_manager)?;
if let Some(repo) = selected_repo {
let workflow = CloneWorkflow {
url: repo.url.clone(),
app: None, };
execute_workflow(Box::new(workflow), workspace_manager).await?;
} else {
println!("{} No repository selected", style("âšī¸").blue());
}
Ok(())
}
fn display_enhanced_results(
results: &[Repository],
workspace_manager: &WorkspaceManager,
) -> Result<Option<Repository>> {
let items: Vec<String> = results
.iter()
.enumerate()
.map(|(i, repo)| {
let stars = if repo.stars > 0 {
format!("â {} ", Self::format_stars(repo.stars))
} else {
"".to_string()
};
let lang = if let Some(language) = &repo.language {
format!(" [{language}]")
} else {
"".to_string()
};
let license = if let Some(license_key) = &repo.license {
format!(" [{}]", license_key.to_uppercase())
} else {
"".to_string()
};
let desc = repo.description.as_deref().unwrap_or("No description");
let truncated_desc = if desc.chars().count() > 50 {
let truncated: String = desc.chars().take(50).collect();
format!("{truncated}...")
} else {
desc.to_string()
};
format!(
"{:2}. {}{}{}{} - {}",
i + 1,
stars,
repo.full_name,
lang,
license,
truncated_desc
)
})
.collect();
let selection = Select::with_theme(&ColorfulTheme::default())
.with_prompt("Select repository to clone (ESC to cancel)")
.items(&items)
.default(0)
.max_length(workspace_manager.get_git_search_results_page_size())
.interact_opt()?;
Ok(selection.map(|i| results[i].clone()))
}
}