#![allow(unused)]
use chrono::{DateTime, NaiveDateTime, TimeZone, Utc};
use ratatui::{
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::ListItem,
};
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct ThreadNode {
pub id: String,
pub subject: String,
pub sender: String,
pub date: String,
pub message_id: Option<String>,
pub article_references: Vec<String>,
pub children: Vec<ThreadNode>,
}
#[derive(Debug, Clone)]
pub struct ThreadedArticleView {
pub id: String,
pub message_id: Option<String>,
pub subject: String,
pub sender: String,
pub date: String,
pub depth: usize,
pub is_last_child: bool,
}
#[derive(Debug, Clone)]
pub struct FlatArticle {
pub article_id: String,
pub subject: String,
pub sender: String,
pub date: String,
pub message_id: Option<String>,
pub article_references: Option<String>,
}
pub fn build_thread_tree(articles: Vec<FlatArticle>) -> Vec<ThreadNode> {
let mut index_by_id: HashMap<String, usize> = HashMap::new();
for (i, article) in articles.iter().enumerate() {
let key = article.message_id.as_deref().unwrap_or(&article.article_id);
index_by_id.insert(key.to_string(), i);
}
let mut children_map: HashMap<String, Vec<usize>> = HashMap::new();
let mut root_indices: Vec<usize> = Vec::new();
for (i, article) in articles.iter().enumerate() {
let parent_id = if let Some(refs) = &article.article_references {
let ids: Vec<&str> = refs.split_whitespace().collect();
ids.last().and_then(|&last_id| {
if index_by_id.contains_key(last_id) {
Some(last_id.to_string())
} else {
None
}
})
} else {
None
};
if let Some(parent_key) = parent_id {
children_map.entry(parent_key).or_default().push(i);
} else {
root_indices.push(i);
}
}
let parse_date = |s: &str| {
NaiveDateTime::parse_from_str(s, "%a, %d %b %Y %H:%M").unwrap_or_else(|error| {
Utc.timestamp_opt(0, 0)
.single()
.expect("Failed to create timestamp")
.naive_utc()
})
};
root_indices
.sort_by(|&a, &b| parse_date(&articles[b].date).cmp(&parse_date(&articles[a].date)));
for children in children_map.values_mut() {
children
.sort_by(|&a, &b| parse_date(&articles[a].date).cmp(&parse_date(&articles[b].date)));
}
fn build_node(
idx: usize,
articles: &[FlatArticle],
children_map: &HashMap<String, Vec<usize>>,
index_by_id: &HashMap<String, usize>,
) -> ThreadNode {
let art = &articles[idx];
let key = art.message_id.as_deref().unwrap_or(&art.article_id);
let child_indices = children_map.get(key).cloned().unwrap_or_default();
let mut node = ThreadNode {
id: art.article_id.clone(),
subject: art.subject.clone(),
sender: art.sender.clone(),
date: art.date.clone(),
message_id: art.message_id.clone(),
article_references: art
.article_references
.as_ref()
.map(|r| r.split_whitespace().map(|s| s.to_string()).collect())
.unwrap_or_default(),
children: Vec::new(),
};
for child_idx in child_indices {
node.children
.push(build_node(child_idx, articles, children_map, index_by_id));
}
node
}
let mut root_nodes = Vec::new();
for idx in root_indices {
root_nodes.push(build_node(idx, &articles, &children_map, &index_by_id));
}
root_nodes
}
pub fn flatten_thread_tree(roots: &[ThreadNode], depth: usize, out: &mut Vec<ThreadedArticleView>) {
let len = roots.len();
for (i, node) in roots.iter().enumerate() {
let is_last = i == len - 1;
out.push(ThreadedArticleView {
id: node.id.clone(),
message_id: node.message_id.clone(),
subject: node.subject.clone(),
sender: node.sender.clone(),
date: node.date.clone(),
depth,
is_last_child: is_last,
});
if !node.children.is_empty() {
flatten_thread_tree(&node.children, depth + 1, out);
}
}
}
pub fn render_threaded_articles<'a>(
window: &'a [ThreadedArticleView],
start: usize,
selected_article: usize,
read_flags: &'a [bool],
cols_width: usize,
last_seen_id: u32,
) -> Vec<ListItem<'a>> {
let mut last_child_tracker = [false; 100];
window
.iter()
.enumerate()
.map(|(i, item)| {
let idx = start + i;
let is_selected = idx == selected_article;
let is_read = read_flags.get(idx).copied().unwrap_or(false);
last_child_tracker[item.depth] = item.is_last_child;
let mut indent = String::new();
for depth in 0..item.depth {
if last_child_tracker[depth] {
indent.push_str(" ");
} else {
indent.push_str("│ ");
}
}
let connector = if item.depth > 0 {
if item.is_last_child {
"└──"
} else {
"├─"
}
} else {
"├─"
};
let mark = if is_read {
"✅"
} else if item.depth == 0 && item.id.parse::<u32>().unwrap_or(0) > last_seen_id {
"🆕"
} else {
"🟪"
};
let prefix = if is_selected { "> " } else { " " };
let sender_label = format!("[{}]", item.sender);
let mut line = Line::from(vec![
Span::raw(prefix),
Span::raw(indent),
Span::raw(connector),
Span::raw(" "),
Span::styled(mark, Style::default().fg(Color::Green)),
Span::raw(" "),
Span::styled(
sender_label,
Style::default().fg(if is_read {
Color::DarkGray
} else {
Color::Rgb(186, 85, 211)
}),
),
Span::raw(" "),
Span::styled(
&item.subject,
Style::default()
.add_modifier(Modifier::BOLD)
.fg(if is_read {
Color::Gray
} else if item.depth == 0 {
Color::Yellow
} else {
Color::White
}),
),
]);
let max_len = cols_width - 2;
let content_width = line.width();
let padding = max_len.saturating_sub(content_width + item.date.len());
line.spans.push(Span::raw(format!(
"{:padding$}{}",
"",
item.date,
padding = padding
)));
let item = ListItem::new(line);
if is_selected {
item.style(
Style::default()
.fg(Color::LightGreen)
.add_modifier(Modifier::BOLD),
)
} else {
item
}
})
.collect()
}
pub fn render_articles<'a>(
threaded_mode: bool,
threaded_subjects: &'a [ThreadedArticleView],
flat_subjects: &'a [(String, String, String, String)],
start: usize,
selected_article: usize,
read_flags: &'a [bool],
cols_width: usize,
last_seen_id: u32,
) -> Vec<ListItem<'a>> {
if threaded_mode {
render_threaded_articles(
&threaded_subjects[start..],
start,
selected_article,
read_flags,
cols_width,
last_seen_id,
)
} else {
flat_subjects[start..]
.iter()
.enumerate()
.map(|(i, (_id, subj, sender, date))| {
let idx = start + i;
let is_selected = idx == selected_article;
let is_read = read_flags.get(idx).copied().unwrap_or(false);
let prefix = if is_selected { "> " } else { " " };
let mark = if is_read { "✅" } else { "🟪" };
let sender_color = if is_read {
Color::DarkGray
} else {
Color::Rgb(186, 85, 211)
};
let sender_label = format!("[{}]", sender);
let mut line = Line::from(vec![
Span::raw(prefix),
Span::raw(" "),
Span::styled(mark, Style::default().fg(Color::Green)),
Span::raw(" "),
Span::styled(sender_label, Style::default().fg(sender_color)),
Span::raw(" "),
Span::raw(subj),
]);
let max_len = cols_width;
let content_width = line.width();
let padding = max_len.saturating_sub(content_width + date.len());
line.spans.push(Span::raw(format!(
"{:padding$}{}",
"",
date,
padding = padding
)));
let item = ListItem::new(line);
if is_selected {
item.style(
Style::default()
.fg(Color::LightGreen)
.add_modifier(Modifier::BOLD),
)
} else {
item
}
})
.collect()
}
}