use ratatui::{
buffer::Buffer,
layout::Rect,
style::{Color, Style},
text::{Line, Span},
widgets::{Paragraph, Widget},
};
use tracing::debug;
const MAX_ACTIVITIES: usize = 5;
#[derive(Debug, Clone)]
pub struct Activity {
pub message: String,
pub in_progress: bool,
}
#[derive(Debug, Clone)]
pub struct ActivityFeed {
activities: Vec<Activity>,
}
impl Default for ActivityFeed {
fn default() -> Self {
Self::new()
}
}
impl ActivityFeed {
pub fn new() -> Self {
debug!(component = %"ActivityFeed", "Component created");
Self {
activities: Vec::with_capacity(MAX_ACTIVITIES),
}
}
pub fn add(&mut self, message: String, in_progress: bool) {
self.activities.insert(
0,
Activity {
message,
in_progress,
},
);
if self.activities.len() > MAX_ACTIVITIES {
self.activities.truncate(MAX_ACTIVITIES);
}
}
pub fn complete_current(&mut self) {
for activity in &mut self.activities {
if activity.in_progress {
activity.in_progress = false;
break;
}
}
}
pub fn clear(&mut self) {
self.activities.clear();
}
pub fn complete_all(&mut self) {
for activity in &mut self.activities {
activity.in_progress = false;
}
}
pub fn is_empty(&self) -> bool {
self.activities.is_empty()
}
pub fn has_in_progress(&self) -> bool {
self.activities.iter().any(|a| a.in_progress)
}
pub fn len(&self) -> usize {
self.activities.len()
}
pub fn render(&self, area: Rect, buf: &mut Buffer) {
if area.height == 0 || self.activities.is_empty() {
return;
}
let mut lines = Vec::with_capacity(self.activities.len());
for activity in &self.activities {
let (indicator, color) = if activity.in_progress {
("⏳", Color::Yellow)
} else {
("✓", Color::Green)
};
lines.push(Line::from(vec![
Span::styled(indicator, Style::default().fg(color)),
Span::raw(" "),
Span::styled(&activity.message, Style::default().fg(Color::DarkGray)),
]));
}
let paragraph = Paragraph::new(lines);
paragraph.render(area, buf);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_activity_feed_new() {
let feed = ActivityFeed::new();
assert!(feed.is_empty());
assert_eq!(feed.len(), 0);
}
#[test]
fn test_activity_feed_default() {
let feed = ActivityFeed::default();
assert!(feed.is_empty());
}
#[test]
fn test_activity_feed_add() {
let mut feed = ActivityFeed::new();
feed.add("Reading file...".to_string(), true);
assert_eq!(feed.len(), 1);
}
#[test]
fn test_activity_feed_add_multiple() {
let mut feed = ActivityFeed::new();
feed.add("Reading file...".to_string(), false);
feed.add("Editing file...".to_string(), false);
assert_eq!(feed.len(), 2);
assert_eq!(feed.activities[0].message, "Editing file...");
}
#[test]
fn test_activity_feed_multiple_in_progress() {
let mut feed = ActivityFeed::new();
feed.add("Reading file...".to_string(), true);
feed.add("Editing file...".to_string(), true);
assert_eq!(feed.len(), 2);
assert_eq!(feed.activities[0].message, "Editing file...");
assert!(feed.activities[0].in_progress);
assert!(feed.activities[1].in_progress);
}
#[test]
fn test_activity_feed_complete_current() {
let mut feed = ActivityFeed::new();
feed.add("Reading file...".to_string(), true);
assert!(feed.activities[0].in_progress);
feed.complete_current();
assert!(!feed.activities[0].in_progress);
}
#[test]
fn test_activity_feed_has_in_progress() {
let mut feed = ActivityFeed::new();
assert!(!feed.has_in_progress());
feed.add("Reading file...".to_string(), true);
assert!(feed.has_in_progress());
feed.complete_current();
assert!(!feed.has_in_progress());
}
#[test]
fn test_activity_feed_prune() {
let mut feed = ActivityFeed::new();
feed.add("Reading file...".to_string(), true);
feed.clear();
assert!(feed.is_empty());
}
#[test]
fn test_activity_feed_render() {
let mut buffer = Buffer::empty(Rect {
x: 0,
y: 0,
width: 40,
height: 3,
});
let mut feed = ActivityFeed::new();
feed.add("Reading file...".to_string(), false);
feed.add("Editing file...".to_string(), true);
feed.render(
Rect {
x: 0,
y: 0,
width: 40,
height: 3,
},
&mut buffer,
);
assert!(!buffer.content.is_empty());
}
}