Skip to main content

cbtop/bricks/panels/
memory.rs

1//! Memory panel brick (Layer 3)
2//!
3//! Displays RAM, swap, and per-process memory usage.
4
5use crate::brick::{Brick, BrickAssertion, BrickBudget, BrickVerification};
6use presentar_core::{Canvas, Point, Rect, TextStyle, Widget};
7use presentar_terminal::{Meter, Theme};
8use std::any::Any;
9
10/// Memory metrics for display
11#[derive(Debug, Clone, Default)]
12pub struct MemoryMetrics {
13    /// Total RAM in bytes
14    pub total_ram: u64,
15    /// Used RAM in bytes
16    pub used_ram: u64,
17    /// Total swap in bytes
18    pub total_swap: u64,
19    /// Used swap in bytes
20    pub used_swap: u64,
21    /// Cached memory in bytes
22    pub cached: u64,
23    /// Buffer memory in bytes
24    pub buffers: u64,
25}
26
27impl MemoryMetrics {
28    /// RAM usage as percentage
29    pub fn ram_percent(&self) -> f64 {
30        if self.total_ram > 0 {
31            (self.used_ram as f64 / self.total_ram as f64) * 100.0
32        } else {
33            0.0
34        }
35    }
36
37    /// Swap usage as percentage
38    pub fn swap_percent(&self) -> f64 {
39        if self.total_swap > 0 {
40            (self.used_swap as f64 / self.total_swap as f64) * 100.0
41        } else {
42            0.0
43        }
44    }
45
46    /// Format bytes as human-readable
47    pub fn format_bytes(bytes: u64) -> String {
48        batuta_common::fmt::format_bytes(bytes)
49    }
50}
51
52/// Memory panel for system memory monitoring
53pub struct MemoryPanelBrick {
54    /// Current memory metrics
55    pub metrics: MemoryMetrics,
56    /// Theme for rendering
57    pub theme: Theme,
58}
59
60impl MemoryPanelBrick {
61    /// Create a new memory panel
62    pub fn new() -> Self {
63        Self {
64            metrics: MemoryMetrics::default(),
65            theme: Theme::tokyo_night(),
66        }
67    }
68
69    /// Paint the memory panel
70    pub fn paint(&self, canvas: &mut dyn Canvas, width: f32, _height: f32) {
71        let label_style = TextStyle {
72            color: self.theme.foreground,
73            ..Default::default()
74        };
75        let dim_style = TextStyle {
76            color: self.theme.dim,
77            ..Default::default()
78        };
79
80        canvas.draw_text("Memory Monitor", Point::new(2.0, 2.0), &label_style);
81
82        // RAM usage
83        let ram_pct = self.metrics.ram_percent();
84        canvas.draw_text("RAM:", Point::new(2.0, 4.0), &dim_style);
85
86        let mut ram_meter = Meter::new(ram_pct, 100.0).with_color(self.theme.memory_color(ram_pct));
87        ram_meter.layout(Rect::new(10.0, 4.0, width - 30.0, 1.0));
88        ram_meter.paint(canvas);
89
90        let ram_info = format!(
91            "{} / {} ({:.1}%)",
92            MemoryMetrics::format_bytes(self.metrics.used_ram),
93            MemoryMetrics::format_bytes(self.metrics.total_ram),
94            ram_pct
95        );
96        let ram_style = TextStyle {
97            color: self.theme.memory_color(ram_pct),
98            ..Default::default()
99        };
100        canvas.draw_text(&ram_info, Point::new(2.0, 5.0), &ram_style);
101
102        // Swap usage
103        let swap_pct = self.metrics.swap_percent();
104        canvas.draw_text("Swap:", Point::new(2.0, 7.0), &dim_style);
105
106        let mut swap_meter =
107            Meter::new(swap_pct, 100.0).with_color(self.theme.memory_color(swap_pct));
108        swap_meter.layout(Rect::new(10.0, 7.0, width - 30.0, 1.0));
109        swap_meter.paint(canvas);
110
111        let swap_info = format!(
112            "{} / {} ({:.1}%)",
113            MemoryMetrics::format_bytes(self.metrics.used_swap),
114            MemoryMetrics::format_bytes(self.metrics.total_swap),
115            swap_pct
116        );
117        let swap_style = TextStyle {
118            color: self.theme.memory_color(swap_pct),
119            ..Default::default()
120        };
121        canvas.draw_text(&swap_info, Point::new(2.0, 8.0), &swap_style);
122
123        // Cache/Buffer info
124        canvas.draw_text("Cached:", Point::new(2.0, 10.0), &dim_style);
125        canvas.draw_text(
126            &MemoryMetrics::format_bytes(self.metrics.cached),
127            Point::new(12.0, 10.0),
128            &label_style,
129        );
130
131        canvas.draw_text("Buffers:", Point::new(2.0, 11.0), &dim_style);
132        canvas.draw_text(
133            &MemoryMetrics::format_bytes(self.metrics.buffers),
134            Point::new(12.0, 11.0),
135            &label_style,
136        );
137    }
138}
139
140impl Default for MemoryPanelBrick {
141    fn default() -> Self {
142        Self::new()
143    }
144}
145
146impl Brick for MemoryPanelBrick {
147    fn brick_name(&self) -> &'static str {
148        "memory_panel"
149    }
150
151    fn assertions(&self) -> Vec<BrickAssertion> {
152        vec![
153            BrickAssertion::MinWidth(40),
154            BrickAssertion::MinHeight(12),
155            BrickAssertion::max_latency_ms(8),
156        ]
157    }
158
159    fn budget(&self) -> BrickBudget {
160        BrickBudget::FRAME_60FPS
161    }
162
163    fn verify(&self) -> BrickVerification {
164        let mut v = BrickVerification::new();
165        for assertion in self.assertions() {
166            v.check(&assertion);
167        }
168        v
169    }
170
171    fn as_any(&self) -> &dyn Any {
172        self
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179
180    #[test]
181    fn test_memory_panel_brick_name() {
182        let panel = MemoryPanelBrick::new();
183        assert_eq!(panel.brick_name(), "memory_panel");
184    }
185
186    #[test]
187    fn test_memory_panel_has_assertions() {
188        let panel = MemoryPanelBrick::new();
189        assert!(!panel.assertions().is_empty());
190    }
191
192    #[test]
193    fn test_memory_metrics_percent() {
194        let metrics = MemoryMetrics {
195            total_ram: 16_000_000_000,
196            used_ram: 8_000_000_000,
197            total_swap: 4_000_000_000,
198            used_swap: 1_000_000_000,
199            cached: 2_000_000_000,
200            buffers: 500_000_000,
201        };
202        assert!((metrics.ram_percent() - 50.0).abs() < 0.01);
203        assert!((metrics.swap_percent() - 25.0).abs() < 0.01);
204    }
205
206    #[test]
207    fn test_format_bytes() {
208        assert_eq!(MemoryMetrics::format_bytes(500), "500 B");
209        assert_eq!(MemoryMetrics::format_bytes(1024), "1.0 KB");
210        assert_eq!(MemoryMetrics::format_bytes(1_048_576), "1.0 MB");
211        assert_eq!(MemoryMetrics::format_bytes(1_073_741_824), "1.0 GB");
212    }
213}