use std::collections::{HashMap, HashSet};
use std::path::PathBuf;
use ratatui::style::{Modifier, Style};
use ratatui::text::Span;
use crate::domain::repo::{Repo, is_dir_target};
use crate::domain::stats::{CodeEntry, GitStats};
use crate::tui::columns::{CellSource, StatColumn, cell_text};
use crate::tui::git_columns::git_marker_errored;
use crate::tui::presentation::IconSet;
use crate::tui::skin::Colors;
pub struct GlyphContext<'a> {
pub example_mode: bool,
pub missing: &'a HashSet<PathBuf>,
pub icons: &'a IconSet,
pub colors: &'a Colors,
}
pub fn marker_span(repo: &Repo, ctx: &GlyphContext) -> Span<'static> {
let is_errored = git_marker_errored(repo, ctx.example_mode)
|| ctx.missing.contains(&repo.path);
if !is_errored {
return Span::raw(" ");
}
Span::styled(
ctx.icons.missing.to_string(),
Style::default()
.fg(ctx.colors.danger)
.add_modifier(Modifier::BOLD),
)
}
pub fn fav_span(repo: &Repo, ctx: &GlyphContext) -> Span<'static> {
if !repo.fav {
return Span::raw(" ");
}
Span::styled(
ctx.icons.favourite.to_string(),
Style::default().fg(ctx.colors.favourite),
)
}
pub fn type_label(repo: &Repo) -> &'static str {
if is_dir_target(&repo.path) {
"folder"
} else {
"file"
}
}
pub struct StatContext<'a> {
pub code: &'a HashMap<PathBuf, CodeEntry>,
pub git: &'a HashMap<PathBuf, GitStats>,
pub computing: &'a HashSet<PathBuf>,
pub spinner: Option<(&'a HashSet<PathBuf>, &'a str)>,
pub now: i64,
}
impl<'a> StatContext<'a> {
pub fn text(&self, repo: &Repo, column: StatColumn) -> String {
cell_text(column, self.source(repo))
.unwrap_or_else(|| self.spinner_glyph().to_string())
}
pub fn spinner_glyph(&self) -> &'a str {
self.spinner.map_or("-", |(_, glyph)| glyph)
}
pub fn is_in_flight(&self, repo: &Repo) -> bool {
self.spinner
.is_some_and(|(in_flight, _)| in_flight.contains(&repo.path))
}
fn source(&self, repo: &Repo) -> CellSource<'_> {
CellSource {
code: self.code.get(&repo.path),
git: self.git.get(&repo.path),
open_count: repo.open_count,
last_used: repo.last_used,
pending: self.computing.contains(&repo.path),
now: self.now,
}
}
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use super::*;
use crate::config::Config;
use crate::domain::repo::RepoKind;
use crate::theme::GlyphVariant;
fn repo_at(path: &str) -> Repo {
let mut repo = Repo::new(PathBuf::from(path));
repo.kind = RepoKind::Path;
repo
}
fn icon_set() -> IconSet {
IconSet::new(GlyphVariant::Unicode)
}
fn colour_roles() -> Colors {
Colors::from_palette(&Config::default().palette())
}
fn context<'a>(
missing: &'a HashSet<PathBuf>,
icons: &'a IconSet,
colors: &'a Colors,
) -> GlyphContext<'a> {
GlyphContext {
example_mode: false,
missing,
icons,
colors,
}
}
#[test]
fn a_path_entry_is_marked_only_once_the_existence_check_flagged_it() {
let icons = icon_set();
let colors = colour_roles();
let mut missing = HashSet::new();
let repo = repo_at("/tmp/gone");
let clean = context(&missing, &icons, &colors);
assert_eq!(marker_span(&repo, &clean).content, " ");
missing.insert(PathBuf::from("/tmp/gone"));
let flagged = context(&missing, &icons, &colors);
assert_eq!(
marker_span(&repo, &flagged).content,
icons.missing.to_string()
);
}
#[test]
fn only_a_favourited_entry_gets_the_star() {
let icons = icon_set();
let colors = colour_roles();
let missing = HashSet::new();
let ctx = context(&missing, &icons, &colors);
let mut repo = repo_at("/tmp/one");
assert_eq!(fav_span(&repo, &ctx).content, " ");
repo.fav = true;
assert_eq!(fav_span(&repo, &ctx).content, icons.favourite.to_string());
}
#[test]
fn the_type_label_follows_the_trailing_slash_for_a_missing_path() {
assert_eq!(type_label(&repo_at("/tmp/nowhere/")), "folder");
assert_eq!(type_label(&repo_at("/tmp/nowhere")), "file");
}
#[test]
fn an_unreported_statistic_shows_the_spinner_and_a_dash_outside_a_run() {
let code = HashMap::new();
let git = HashMap::new();
let mut computing = HashSet::new();
computing.insert(PathBuf::from("/tmp/one"));
let in_flight = computing.clone();
let repo = repo_at("/tmp/one");
let idle = StatContext {
code: &code,
git: &git,
computing: &computing,
spinner: None,
now: 0,
};
assert_eq!(idle.spinner_glyph(), "-");
assert!(!idle.is_in_flight(&repo));
let running = StatContext {
spinner: Some((&in_flight, "*")),
..idle
};
assert_eq!(running.spinner_glyph(), "*");
assert!(running.is_in_flight(&repo));
}
}