use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use crate::assignee::assignee_display;
use crate::model::Priority;
use super::board::Card;
pub fn priority_glyph(priority: &Priority) -> &'static str {
match priority {
Priority::High => "!",
Priority::Other(s) if s.eq_ignore_ascii_case("urgent") => "!!",
_ => "",
}
}
pub fn priority_style(priority: &Priority) -> Style {
match priority {
Priority::High => Style::default().fg(Color::Red),
Priority::Other(s) if s.eq_ignore_ascii_case("urgent") => {
Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)
}
Priority::Low => Style::default().fg(Color::DarkGray),
_ => Style::default(),
}
}
pub fn card_line(card: &Card, _width: usize) -> Line<'static> {
let glyph = priority_glyph(&card.priority);
let num = format!("#{}", card.number);
let assignee_str = card
.assignee
.as_deref()
.map(|a| format!(" {}", assignee_display(a)))
.unwrap_or_default();
let labels_str: String = card
.labels
.iter()
.take(3)
.map(|l| format!(" [{l}]"))
.collect();
let mut spans: Vec<Span<'static>> = Vec::new();
spans.push(Span::styled(num, Style::default().fg(Color::Cyan)));
spans.push(Span::raw(" "));
spans.push(Span::styled(card.title.clone(), Style::default()));
if !glyph.is_empty() {
spans.push(Span::styled(
format!(" {glyph}"),
priority_style(&card.priority),
));
}
if !assignee_str.is_empty() {
spans.push(Span::styled(
assignee_str,
Style::default().fg(Color::Green),
));
}
if !labels_str.is_empty() {
spans.push(Span::styled(labels_str, Style::default().fg(Color::Yellow)));
}
Line::from(spans)
}
#[cfg(test)]
mod tests {
use super::*;
fn card(priority: Priority) -> Card {
Card {
number: 1,
title: "Test".into(),
priority,
labels: vec![],
assignee: None,
filename: "test.md".into(),
commit: None,
}
}
#[test]
fn glyph_high_is_bang() {
assert_eq!(priority_glyph(&Priority::High), "!");
}
#[test]
fn glyph_urgent_is_double_bang() {
assert_eq!(priority_glyph(&Priority::Other("urgent".into())), "!!");
}
#[test]
fn glyph_medium_is_empty() {
assert_eq!(priority_glyph(&Priority::Medium), "");
}
#[test]
fn card_line_renders_number_and_title() {
let c = card(Priority::Medium);
let line = card_line(&c, 40);
let text: String = line.spans.iter().map(|s| s.content.as_ref()).collect();
assert!(text.contains("#1"));
assert!(text.contains("Test"));
}
#[test]
fn card_line_high_priority_shows_glyph() {
let c = card(Priority::High);
let line = card_line(&c, 40);
let text: String = line.spans.iter().map(|s| s.content.as_ref()).collect();
assert!(text.contains("!"));
}
#[test]
fn card_line_assignee_shows_at_prefix() {
let c = Card {
assignee: Some("alice".into()),
..card(Priority::Medium)
};
let line = card_line(&c, 40);
let text: String = line.spans.iter().map(|s| s.content.as_ref()).collect();
assert!(text.contains("@alice"));
}
#[test]
fn card_line_identity_shows_name_only() {
let c = Card {
assignee: Some("Herbert Maschke <thyseus@pm.me>".into()),
..card(Priority::Medium)
};
let line = card_line(&c, 40);
let text: String = line.spans.iter().map(|s| s.content.as_ref()).collect();
assert!(text.contains("Herbert Maschke"));
assert!(
!text.contains("thyseus@pm.me"),
"identity email must not leak into the board line: {text:?}"
);
}
#[test]
fn card_line_shows_first_three_labels() {
let c = Card {
labels: vec!["a".into(), "b".into(), "c".into(), "d".into()],
..card(Priority::Medium)
};
let line = card_line(&c, 40);
let text: String = line.spans.iter().map(|s| s.content.as_ref()).collect();
assert!(text.contains("[a]"));
assert!(text.contains("[b]"));
assert!(text.contains("[c]"));
assert!(!text.contains("[d]"));
}
}