1use ratatui::{
4 buffer::Buffer,
5 layout::Rect,
6 style::{Color, Style},
7 widgets::{Block, Widget},
8};
9use std::collections::HashMap;
10
11#[derive(Debug, Clone)]
13pub struct MemoryData {
14 pub current: usize,
15 pub peak: usize,
16 pub history: Vec<usize>,
17 pub has_started: bool, }
19
20impl MemoryData {
21 pub fn new() -> Self {
22 Self {
23 current: 0,
24 peak: 0,
25 history: Vec::new(),
26 has_started: false,
27 }
28 }
29
30 pub fn update(&mut self, current: usize, max_history: usize) {
31 if current > 0 && !self.has_started {
33 self.has_started = true;
34 }
35
36 self.current = current;
37 if current > self.peak {
38 self.peak = current;
39 }
40
41 self.history.push(current);
42 if self.history.len() > max_history {
43 self.history.remove(0);
44 }
45 }
46
47 pub fn reset(&mut self) {
48 self.current = 0;
49 self.peak = 0;
50 self.history.clear();
51 self.has_started = false;
52 }
53}
54
55impl Default for MemoryData {
56 fn default() -> Self {
57 Self::new()
58 }
59}
60
61#[derive(Debug, Clone)]
63pub struct MemoryGraph {
64 data: HashMap<String, MemoryData>,
65 max_history: usize,
66 style: Style,
67 current_style: Style,
68 peak_style: Style,
69 block: Option<Block<'static>>,
70 title: Option<String>,
71 show_values: bool,
72}
73
74impl MemoryGraph {
75 pub fn new() -> Self {
77 Self {
78 data: HashMap::new(),
79 max_history: 100,
80 style: Style::default(),
81 current_style: Style::default().fg(Color::Green),
82 peak_style: Style::default().fg(Color::Red),
83 block: None,
84 title: None,
85 show_values: true,
86 }
87 }
88
89 pub fn max_history(mut self, max: usize) -> Self {
91 self.max_history = max.max(1);
92 self
93 }
94
95 pub fn style(mut self, style: Style) -> Self {
97 self.style = style;
98 self
99 }
100
101 pub fn current_style(mut self, style: Style) -> Self {
103 self.current_style = style;
104 self
105 }
106
107 pub fn peak_style(mut self, style: Style) -> Self {
109 self.peak_style = style;
110 self
111 }
112
113 pub fn block(mut self, block: Block<'static>) -> Self {
115 self.block = Some(block);
116 self
117 }
118
119 pub fn title<T>(mut self, title: T) -> Self
121 where
122 T: Into<String>,
123 {
124 self.title = Some(title.into());
125 self
126 }
127
128 pub fn show_values(mut self, show: bool) -> Self {
130 self.show_values = show;
131 self
132 }
133
134 pub fn update_algorithm(&mut self, name: &str, current_bytes: usize) {
136 let entry = self.data.entry(name.to_string()).or_default();
137 entry.update(current_bytes, self.max_history);
138 }
139
140 pub fn get_algorithm_data(&self, name: &str) -> Option<&MemoryData> {
142 self.data.get(name)
143 }
144
145 pub fn algorithm_names(&self) -> Vec<&String> {
147 self.data.keys().collect()
148 }
149
150 pub fn is_empty(&self) -> bool {
152 self.data.is_empty()
153 }
154
155 pub fn clear(&mut self) {
157 self.data.clear();
158 }
159
160 pub fn reset_all(&mut self) {
162 for data in self.data.values_mut() {
163 data.reset();
164 }
165 }
166
167 fn format_bytes(bytes: usize) -> String {
169 const UNITS: &[&str] = &["B", "KB", "MB", "GB"];
170 let mut size = bytes as f64;
171 let mut unit_index = 0;
172
173 while size >= 1024.0 && unit_index < UNITS.len() - 1 {
174 size /= 1024.0;
175 unit_index += 1;
176 }
177
178 if unit_index == 0 {
179 format!("{}B", bytes)
180 } else {
181 format!("{:.1}{}", size, UNITS[unit_index])
182 }
183 }
184
185 pub fn render_widget(&self, area: Rect, buf: &mut Buffer) {
187 if area.width < 3 || area.height < 3 {
188 return;
189 }
190
191 let inner_area = if let Some(ref block) = self.block {
192 let inner = block.inner(area);
193 block.render(area, buf);
194 inner
195 } else {
196 area
197 };
198
199 if self.data.is_empty() {
200 let msg = "No memory data available";
202 let msg_x = inner_area.left() + (inner_area.width.saturating_sub(msg.len() as u16)) / 2;
203 let msg_y = inner_area.top() + inner_area.height / 2;
204
205 for (i, ch) in msg.chars().enumerate() {
206 let x = msg_x + i as u16;
207 if x < inner_area.right() && msg_y < inner_area.bottom() {
208 buf[(x, msg_y)]
209 .set_symbol(&ch.to_string())
210 .set_style(self.style);
211 }
212 }
213 return;
214 }
215
216 let mut algorithms: Vec<_> = self.data.keys().cloned().collect();
217 algorithms.sort(); let algorithm_count = algorithms.len();
219
220 if algorithm_count == 0 {
221 return;
222 }
223
224 let line_height = if inner_area.height > algorithm_count as u16 {
225 inner_area.height / algorithm_count as u16
226 } else {
227 1
228 };
229
230 let max_memory = self.data.values()
232 .map(|data| data.peak.max(data.current))
233 .max()
234 .unwrap_or(1);
235
236 for (i, algorithm) in algorithms.iter().enumerate() {
237 if let Some(memory_data) = self.data.get(algorithm) {
238 let y_start = inner_area.top() + (i as u16 * line_height);
239 let _y_end = (y_start + line_height).min(inner_area.bottom());
240
241 let name_y = y_start;
243 if name_y < inner_area.bottom() {
244 let display_name = if algorithm.len() > 10 {
245 &algorithm[..10]
246 } else {
247 algorithm
248 };
249
250 for (char_idx, ch) in display_name.chars().enumerate() {
251 let char_x = inner_area.left() + char_idx as u16;
252 if char_x < inner_area.right() {
253 buf[(char_x, name_y)]
254 .set_symbol(&ch.to_string())
255 .set_style(self.style);
256 }
257 }
258 }
259
260 if line_height > 1 && y_start + 1 < inner_area.bottom() {
262 let bar_y = y_start + 1;
263 let available_width = inner_area.width.saturating_sub(15); if max_memory > 0 && memory_data.has_started {
267 let current_width = if memory_data.current > 0 {
268 ((memory_data.current as f64 / max_memory as f64) * available_width as f64).max(1.0) as u16
270 } else {
271 0
272 };
273 for x in 0..current_width {
274 let char_x = inner_area.left() + x;
275 if char_x < inner_area.right() && bar_y < inner_area.bottom() {
276 buf[(char_x, bar_y)]
277 .set_symbol("█")
278 .set_style(self.current_style);
279 }
280 }
281
282 let peak_width = ((memory_data.peak as f64 / max_memory as f64) * available_width as f64) as u16;
284 if peak_width > current_width {
285 for x in current_width..peak_width {
286 let char_x = inner_area.left() + x;
287 if char_x < inner_area.right() && bar_y < inner_area.bottom() {
288 buf[(char_x, bar_y)]
289 .set_symbol("░")
290 .set_style(self.peak_style);
291 }
292 }
293 }
294 }
295
296 if self.show_values && memory_data.has_started {
298 let values_x = inner_area.right().saturating_sub(14);
299 if values_x > inner_area.left() {
300 let current_str = Self::format_bytes(memory_data.current);
301 let peak_str = Self::format_bytes(memory_data.peak);
302 let value_text = format!("{}/{}", current_str, peak_str);
303
304 for (char_idx, ch) in value_text.chars().enumerate() {
305 let char_x = values_x + char_idx as u16;
306 if char_x < inner_area.right() && bar_y < inner_area.bottom() {
307 buf[(char_x, bar_y)]
308 .set_symbol(&ch.to_string())
309 .set_style(self.style);
310 }
311 }
312 }
313 } else if self.show_values && !memory_data.has_started {
314 let values_x = inner_area.right().saturating_sub(14);
316 if values_x > inner_area.left() && bar_y < inner_area.bottom() {
317 buf[(values_x, bar_y)]
318 .set_symbol("-")
319 .set_style(self.style);
320 }
321 }
322 }
323 }
324 }
325 }
326}
327
328impl Default for MemoryGraph {
329 fn default() -> Self {
330 Self::new()
331 }
332}
333
334impl Widget for MemoryGraph {
335 fn render(self, area: Rect, buf: &mut Buffer) {
336 self.render_widget(area, buf);
337 }
338}
339
340impl Widget for &MemoryGraph {
341 fn render(self, area: Rect, buf: &mut Buffer) {
342 self.render_widget(area, buf);
343 }
344}
345
346#[cfg(test)]
347mod tests {
348 use super::*;
349 use ratatui::{
350 buffer::Buffer,
351 layout::Rect,
352 };
353
354 #[test]
355 fn test_memory_graph_creation() {
356 let graph = MemoryGraph::new();
357 assert_eq!(graph.data.len(), 0);
358 assert_eq!(graph.max_history, 100);
359 }
360
361 #[test]
362 fn test_update_algorithm() {
363 let mut graph = MemoryGraph::new();
364 graph.update_algorithm("QuickSort", 1024);
365 graph.update_algorithm("MergeSort", 2048);
366
367 assert_eq!(graph.data.len(), 2);
368
369 let quick_data = graph.get_algorithm_data("QuickSort").unwrap();
370 assert_eq!(quick_data.current, 1024);
371 assert_eq!(quick_data.peak, 1024);
372
373 let merge_data = graph.get_algorithm_data("MergeSort").unwrap();
374 assert_eq!(merge_data.current, 2048);
375 assert_eq!(merge_data.peak, 2048);
376 }
377
378 #[test]
379 fn test_peak_memory_tracking() {
380 let mut graph = MemoryGraph::new();
381 graph.update_algorithm("TestSort", 1024);
382 graph.update_algorithm("TestSort", 2048);
383 graph.update_algorithm("TestSort", 1536);
384
385 let data = graph.get_algorithm_data("TestSort").unwrap();
386 assert_eq!(data.current, 1536);
387 assert_eq!(data.peak, 2048);
388 assert_eq!(data.history.len(), 3);
389 }
390
391 #[test]
392 fn test_history_limit() {
393 let mut graph = MemoryGraph::new().max_history(3);
394 for i in 1..=5 {
395 graph.update_algorithm("TestSort", i * 100);
396 }
397
398 let data = graph.get_algorithm_data("TestSort").unwrap();
399 assert_eq!(data.history.len(), 3);
400 assert_eq!(data.history, vec![300, 400, 500]);
401 }
402
403 #[test]
404 fn test_format_bytes() {
405 assert_eq!(MemoryGraph::format_bytes(512), "512B");
406 assert_eq!(MemoryGraph::format_bytes(1024), "1.0KB");
407 assert_eq!(MemoryGraph::format_bytes(1536), "1.5KB");
408 assert_eq!(MemoryGraph::format_bytes(1048576), "1.0MB");
409 assert_eq!(MemoryGraph::format_bytes(1073741824), "1.0GB");
410 }
411
412 #[test]
413 fn test_render_widget() {
414 let mut graph = MemoryGraph::new();
415 graph.update_algorithm("QuickSort", 1024);
416 graph.update_algorithm("MergeSort", 2048);
417
418 let area = Rect::new(0, 0, 50, 10);
419 let mut buffer = Buffer::empty(area);
420
421 graph.render_widget(area, &mut buffer);
422
423 let content = buffer.content();
425 assert!(!content.is_empty());
426 }
427
428 #[test]
429 fn test_clear() {
430 let mut graph = MemoryGraph::new();
431 graph.update_algorithm("TestSort", 1024);
432 assert_eq!(graph.data.len(), 1);
433
434 graph.clear();
435 assert_eq!(graph.data.len(), 0);
436 }
437}