use std::error::Error; use colored::Colorize; use terminal_size; use terminal_size::{Width, terminal_size as get_terminal_size}; use textwrap::{Options, fill}; use crate::models::{ config::Config, search::{SearchQueryParams, SearchResult}, }; pub struct Search { query: String, offset: Option, limit: Option, } impl Search { const BASE_URL: &str = "https://api.modrinth.com/v2"; pub fn new(query: String, offset: Option, limit: Option) -> Self { Search { query, offset, limit, } } pub async fn run(&self) -> Result<(), Box> { let file_res = tokio::fs::read_to_string("mcsm.toml").await; if file_res.is_err() { return Err("current directory is not a mcsm project".into()); } let config: Config = toml::from_str(&file_res.unwrap())?; if config.server.loader == "Vanilla" { return Err("cannot search for mods on Vanilla server".into()); } let client = reqwest::Client::new(); let search = format!("{}/search", Self::BASE_URL); let facets = format!( r#"[["project_type:mod"],["environment!=client_only"],["environment!=singleplayer_only"],["environment!=unknown"],["categories:{}"],["versions:{}"]]"#, config.server.loader.to_lowercase(), config.server.version, ); let params = SearchQueryParams { query: self.query.clone(), facets: facets.into(), offset: self.offset, limit: self.limit, }; let res: SearchResult = client .get(search) .query(¶ms) .send() .await? .json() .await?; self.print_results(&res); Ok(()) } fn print_results(&self, res: &SearchResult) { println!( "{}\n", format!( "Found {} result{}", res.total_hits, if res.total_hits == 1 { "" } else { "s" } ) .green() .bold() ); let width = get_terminal_size() .map(|(Width(w), _)| w as usize) .unwrap_or(80); for (i, hit) in res.hits.iter().enumerate() { let wrapped = fill( &hit.description, Options::new(width.saturating_sub(3)) .initial_indent(" ") .subsequent_indent(" "), ); println!( "{} {} {}\n by {}\n{}\n", format!("{}.", i + 1).bold(), hit.title.bold(), format!("({})", hit.slug).cyan(), hit.author.yellow(), wrapped, ); } } }