limit_tui/components/
activity.rs1use ratatui::{
7 buffer::Buffer,
8 layout::Rect,
9 style::{Color, Style},
10 text::{Line, Span},
11 widgets::{Paragraph, Widget},
12};
13use tracing::debug;
14
15const MAX_ACTIVITIES: usize = 5;
17
18#[derive(Debug, Clone)]
20pub struct Activity {
21 pub message: String,
23 pub in_progress: bool,
25}
26
27#[derive(Debug, Clone)]
32pub struct ActivityFeed {
33 activities: Vec<Activity>,
35}
36
37impl Default for ActivityFeed {
38 fn default() -> Self {
39 Self::new()
40 }
41}
42
43impl ActivityFeed {
44 pub fn new() -> Self {
46 debug!(component = %"ActivityFeed", "Component created");
47 Self {
48 activities: Vec::with_capacity(MAX_ACTIVITIES),
49 }
50 }
51
52 pub fn add(&mut self, message: String, in_progress: bool) {
57 self.activities.insert(
59 0,
60 Activity {
61 message,
62 in_progress,
63 },
64 );
65
66 if self.activities.len() > MAX_ACTIVITIES {
68 self.activities.truncate(MAX_ACTIVITIES);
69 }
70 }
71
72 pub fn complete_current(&mut self) {
77 for activity in &mut self.activities {
78 if activity.in_progress {
79 activity.in_progress = false;
80 break;
81 }
82 }
83 }
84
85 pub fn clear(&mut self) {
87 self.activities.clear();
88 }
89
90 pub fn complete_all(&mut self) {
92 for activity in &mut self.activities {
93 activity.in_progress = false;
94 }
95 }
96
97 pub fn is_empty(&self) -> bool {
99 self.activities.is_empty()
100 }
101
102 pub fn has_in_progress(&self) -> bool {
104 self.activities.iter().any(|a| a.in_progress)
105 }
106
107 pub fn len(&self) -> usize {
109 self.activities.len()
110 }
111
112 pub fn render(&self, area: Rect, buf: &mut Buffer) {
119 if area.height == 0 || self.activities.is_empty() {
120 return;
121 }
122
123 let mut lines = Vec::with_capacity(self.activities.len());
124
125 for activity in &self.activities {
126 let (indicator, color) = if activity.in_progress {
127 ("⏳", Color::Yellow)
128 } else {
129 ("✓", Color::Green)
130 };
131
132 lines.push(Line::from(vec![
133 Span::styled(indicator, Style::default().fg(color)),
134 Span::raw(" "),
135 Span::styled(&activity.message, Style::default().fg(Color::DarkGray)),
136 ]));
137 }
138
139 let paragraph = Paragraph::new(lines);
140 paragraph.render(area, buf);
141 }
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147
148 #[test]
149 fn test_activity_feed_new() {
150 let feed = ActivityFeed::new();
151 assert!(feed.is_empty());
152 assert_eq!(feed.len(), 0);
153 }
154
155 #[test]
156 fn test_activity_feed_default() {
157 let feed = ActivityFeed::default();
158 assert!(feed.is_empty());
159 }
160
161 #[test]
162 fn test_activity_feed_add() {
163 let mut feed = ActivityFeed::new();
164 feed.add("Reading file...".to_string(), true);
165 assert_eq!(feed.len(), 1);
166 }
167
168 #[test]
169 fn test_activity_feed_add_multiple() {
170 let mut feed = ActivityFeed::new();
171 feed.add("Reading file...".to_string(), false);
172 feed.add("Editing file...".to_string(), false);
173 assert_eq!(feed.len(), 2);
174 assert_eq!(feed.activities[0].message, "Editing file...");
176 }
177
178 #[test]
179 fn test_activity_feed_multiple_in_progress() {
180 let mut feed = ActivityFeed::new();
181 feed.add("Reading file...".to_string(), true);
182 feed.add("Editing file...".to_string(), true);
183 assert_eq!(feed.len(), 2);
185 assert_eq!(feed.activities[0].message, "Editing file...");
186 assert!(feed.activities[0].in_progress);
187 assert!(feed.activities[1].in_progress);
188 }
189
190 #[test]
191 fn test_activity_feed_complete_current() {
192 let mut feed = ActivityFeed::new();
193 feed.add("Reading file...".to_string(), true);
194 assert!(feed.activities[0].in_progress);
195 feed.complete_current();
196 assert!(!feed.activities[0].in_progress);
197 }
198
199 #[test]
200 fn test_activity_feed_has_in_progress() {
201 let mut feed = ActivityFeed::new();
202 assert!(!feed.has_in_progress());
203 feed.add("Reading file...".to_string(), true);
204 assert!(feed.has_in_progress());
205 feed.complete_current();
206 assert!(!feed.has_in_progress());
207 }
208
209 #[test]
210 fn test_activity_feed_prune() {
211 let mut feed = ActivityFeed::new();
212 feed.add("Reading file...".to_string(), true);
213 feed.clear();
214 assert!(feed.is_empty());
215 }
216
217 #[test]
218 fn test_activity_feed_render() {
219 let mut buffer = Buffer::empty(Rect {
220 x: 0,
221 y: 0,
222 width: 40,
223 height: 3,
224 });
225
226 let mut feed = ActivityFeed::new();
227 feed.add("Reading file...".to_string(), false);
228 feed.add("Editing file...".to_string(), true);
229 feed.render(
230 Rect {
231 x: 0,
232 y: 0,
233 width: 40,
234 height: 3,
235 },
236 &mut buffer,
237 );
238
239 assert!(!buffer.content.is_empty());
241 }
242}