197 lines
6.7 KiB
Rust
197 lines
6.7 KiB
Rust
//! 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<RepoGitSummary>,
|
|
name_match_result: Option<FuzzyMatchResult>,
|
|
}
|
|
|
|
impl RepoSearchItem {
|
|
pub fn new(path: PathBuf, git_summary: Option<RepoGitSummary>) -> 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<FuzzyMatchResult>) -> 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<dyn Element> {
|
|
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<dyn Element> {
|
|
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<usize> = (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<usize> = (stats_start..(stats_start + add_part_len)).collect();
|
|
let remove_range: Vec<usize> =
|
|
((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<Fill> {
|
|
inline_styles::item_background(highlight_state, appearance)
|
|
}
|
|
|
|
fn score(&self) -> OrderedFloat<f64> {
|
|
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)
|
|
}
|
|
}
|