use crate::{models::Entry, utils};
const CONTENT_MAX_LEN: usize = 120;
const RELATED_MAX_LEN: usize = 72;
#[derive(Clone, Copy)]
pub struct DisplayOptions {
pub color: bool,
}
pub fn print_entry(
entry: &Entry,
terms: Option<&[String]>,
related: &[&Entry],
options: DisplayOptions,
) {
println!("{}", entry_header(entry, options));
println!("{}", format_content(&entry.content, terms, CONTENT_MAX_LEN));
if !entry.tags.is_empty() {
println!("Tags: {}", entry.tags.join(", "));
}
if !related.is_empty() {
println!("Related:");
for related_entry in related {
println!(
"- {}: {}",
related_label(related_entry, options),
format_content(&related_entry.content, None, RELATED_MAX_LEN)
);
}
}
println!();
}
fn entry_header(entry: &Entry, options: DisplayOptions) -> String {
let timestamp = utils::format_timestamp(&entry.timestamp);
let status = match (entry.entry_type.as_str(), entry.success) {
("command", Some(true)) => colorize("[SUCCESS]", "32", options.color),
("command", Some(false)) => colorize("[FAILED ]", "31", options.color),
_ => " ".to_string(),
};
let label = colorize(
&format!("[{:<7}]", entry_type_label(entry)),
entry_type_color(entry),
options.color,
);
format!("{}{} {} {}", label, status, entry.project, timestamp)
}
fn highlight_terms(text: &str, terms: Option<&[String]>) -> String {
let Some(terms) = terms.filter(|terms| !terms.is_empty()) else {
return text.to_string();
};
let lower = text.to_lowercase();
let mut matches: Vec<(usize, usize)> = Vec::new();
for term in terms {
if term.is_empty() {
continue;
}
let mut start = 0;
while let Some(found) = lower[start..].find(term) {
let match_start = start + found;
let match_end = match_start + term.len();
matches.push((match_start, match_end));
start = match_end;
}
}
if matches.is_empty() {
return text.to_string();
}
matches.sort_unstable_by(|a, b| a.0.cmp(&b.0).then_with(|| a.1.cmp(&b.1)));
let mut merged: Vec<(usize, usize)> = Vec::new();
for (start, end) in matches {
if let Some((_, last_end)) = merged.last_mut() {
if start <= *last_end {
*last_end = (*last_end).max(end);
continue;
}
}
merged.push((start, end));
}
let mut result = String::new();
let mut last = 0;
for (start, end) in merged {
result.push_str(&text[last..start]);
result.push_str("**");
result.push_str(&text[start..end]);
result.push_str("**");
last = end;
}
result.push_str(&text[last..]);
result
}
fn related_label(entry: &Entry, options: DisplayOptions) -> String {
let label = colorize(
&format!("[{:<7}]", entry_type_label(entry)),
entry_type_color(entry),
options.color,
);
format!("{} {}", label, utils::format_timestamp(&entry.timestamp))
}
fn entry_type_label(entry: &Entry) -> &'static str {
match entry.entry_type.as_str() {
"command" => "COMMAND",
"log" => "LOG",
"error" => "ERROR",
_ => "ENTRY",
}
}
fn entry_type_color(entry: &Entry) -> &'static str {
match entry.entry_type.as_str() {
"command" => "36",
"error" => "33",
_ => "0",
}
}
fn format_content(content: &str, terms: Option<&[String]>, max_len: usize) -> String {
let content = truncate_content(content, max_len);
highlight_terms(&content, terms)
}
fn truncate_content(content: &str, max_len: usize) -> String {
if content.chars().count() <= max_len {
return content.to_string();
}
let truncated: String = content.chars().take(max_len - 3).collect();
format!("{}...", truncated)
}
fn colorize(text: &str, code: &str, enabled: bool) -> String {
if !enabled || code == "0" {
return text.to_string();
}
format!("\x1b[{}m{}\x1b[0m", code, text)
}