//! SearchItem implementation for repo menu items. use std::path::{Path, PathBuf}; use fuzzy_match::FuzzyMatchResult; use ordered_float::OrderedFloat; use galaxy_core::ui::theme::{AnsiColorIdentifier, Fill}; use galaxy_core::ui::Icon; use galaxyui::elements::{ConstrainedBox, Container, Highlight, Text}; use galaxyui::fonts::{Properties, Weight}; use galaxyui::text_layout::ClipConfig; use galaxyui::{AppContext, Element, SingletonEntity}; use crate::appearance::Appearance; use crate::search::result_renderer::ItemHighlightState; use crate::search::SearchItem; use crate::terminal::input::inline_menu::styles as inline_styles; use crate::terminal::input::repos::AcceptRepo; use crate::util::git::RepoGitSummary; /// Search item for rendering a repo in the inline repo menu. #[derive(Debug, Clone)] pub(super) struct RepoSearchItem { pub path: PathBuf, pub display_name: String, git_summary: Option, name_match_result: Option, } impl RepoSearchItem { pub fn new(path: PathBuf, git_summary: Option) -> Self { let display_name = repo_display_name(&path); Self { path, display_name, git_summary, name_match_result: None, } } pub fn with_name_match_result(mut self, result: Option) -> Self { self.name_match_result = result; self } } fn repo_display_name(repo_path: &Path) -> String { dirs::home_dir() .and_then(|home| repo_path.strip_prefix(&home).ok().map(|p| p.to_path_buf())) .map(|relative_path| format!("~/{}", relative_path.display())) .unwrap_or_else(|| repo_path.display().to_string()) } impl SearchItem for RepoSearchItem { type Action = AcceptRepo; fn render_icon( &self, _highlight_state: ItemHighlightState, appearance: &Appearance, ) -> Box { let icon_size = inline_styles::font_size(appearance); let icon_color = inline_styles::icon_color(appearance); let icon = ConstrainedBox::new(Icon::Folder.to_galaxyui_icon(icon_color).finish()) .with_width(icon_size) .with_height(icon_size) .finish(); Container::new(icon) .with_margin_right(inline_styles::ICON_MARGIN) .finish() } fn render_item( &self, _highlight_state: ItemHighlightState, app: &AppContext, ) -> Box { let appearance = Appearance::as_ref(app); let theme = appearance.theme(); let font_size = inline_styles::font_size(appearance); let background_color = inline_styles::menu_background_color(app); let primary_text_color = inline_styles::primary_text_color(theme, background_color.into()); let secondary_text_color = inline_styles::secondary_text_color(theme, background_color.into()); // Build primary text: "~/path/to/repo" with optional " branch +N -N" suffix. let branch_suffix = self .git_summary .as_ref() .map(|s| format!(" {}", s.branch.to_lowercase())) .unwrap_or_default(); let stats_suffix = self .git_summary .as_ref() .and_then(|s| { if s.lines_added > 0 || s.lines_removed > 0 { Some(format!(" +{} -{}", s.lines_added, s.lines_removed)) } else { None } }) .unwrap_or_default(); let name_len = self.display_name.len(); let mut full_text = String::with_capacity(name_len + branch_suffix.len() + stats_suffix.len()); full_text.push_str(&self.display_name); full_text.push_str(&branch_suffix); full_text.push_str(&stats_suffix); let mut name_text = Text::new_inline(full_text, appearance.ui_font_family(), font_size) .with_color(primary_text_color.into_solid()) .with_clip(ClipConfig::ellipsis()); // Highlight fuzzy match indices on the name portion. if let Some(match_result) = &self.name_match_result { if !match_result.matched_indices.is_empty() { name_text = name_text.with_single_highlight( Highlight::new().with_properties(Properties::default().weight(Weight::Bold)), match_result.matched_indices.clone(), ); } } // Style branch suffix in secondary color. let branch_end = name_len + branch_suffix.len(); if !branch_suffix.is_empty() { let branch_range: Vec = (name_len..branch_end).collect(); name_text = name_text.with_single_highlight( Highlight::new().with_foreground_color(secondary_text_color.into_solid()), branch_range, ); } // Inline diff stats colored green/red right after branch. if !stats_suffix.is_empty() { let terminal_colors = &theme.terminal_colors().normal; let add_color = AnsiColorIdentifier::Green .to_ansi_color(terminal_colors) .into(); let remove_color = AnsiColorIdentifier::Red .to_ansi_color(terminal_colors) .into(); // stats_suffix is " +N -N"; find the boundary between +N and -N. let stats_start = branch_end; let add_part_len = stats_suffix.find(" -").unwrap_or(stats_suffix.len()); let add_range: Vec = (stats_start..(stats_start + add_part_len)).collect(); let remove_range: Vec = ((stats_start + add_part_len)..(stats_start + stats_suffix.len())).collect(); name_text = name_text .with_single_highlight(Highlight::new().with_foreground_color(add_color), add_range) .with_single_highlight( Highlight::new().with_foreground_color(remove_color), remove_range, ); } name_text.finish() } fn item_background( &self, highlight_state: ItemHighlightState, appearance: &Appearance, ) -> Option { inline_styles::item_background(highlight_state, appearance) } fn score(&self) -> OrderedFloat { self.name_match_result .as_ref() .map(|m| OrderedFloat(m.score as f64)) .unwrap_or(OrderedFloat(f64::MIN)) } fn accept_result(&self) -> AcceptRepo { AcceptRepo { path: self.path.clone(), } } fn execute_result(&self) -> AcceptRepo { self.accept_result() } fn accessibility_label(&self) -> String { format!("Indexed repository: {}", self.display_name) } }