docker_stats/
display.rs

1use crate::{data::DockerStats, utils::*};
2use byte_unit::Byte;
3use colored::Colorize;
4use std::io::{self, Write};
5
6pub struct StatsDisplay {
7    width: usize,
8    compact: bool,
9    full: bool
10}
11
12impl StatsDisplay {
13    pub fn new(width: usize, compact: bool, full: bool) -> Self {
14        // Hide cursor once at start
15        print!("\x1B[?25l");
16        let _ = io::stdout().flush();
17        Self { width, compact, full }
18    }
19
20    /// Print a line after erasing the current one to avoid leftover characters
21    fn out_line(&self, line: &str) {
22        // 2K – erase entire line, \r – carriage return, then newline
23        print!("\x1B[2K\r{}\n", line);
24    }
25
26    // Ensure cursor is shown again when the display is dropped (program exit)
27}
28
29impl Drop for StatsDisplay {
30    fn drop(&mut self) {
31        // Show cursor back
32        print!("\x1B[?25h");
33        let _ = io::stdout().flush();
34    }
35}
36
37impl StatsDisplay {
38    pub fn print_stats(&self, containers: &[DockerStats]) {
39        // Move cursor to home (top-left) without erasing the entire screen
40        print!("\x1B[H");
41
42        let mut max = 100f32;
43
44        if containers.is_empty() {
45            self.out_line("Waiting for container stats...");
46        } else {
47            // Calculate global scale
48            for stats in containers {
49                let mem_perc = perc_to_float(&stats.mem_perc);
50                let cpu_perc = perc_to_float(&stats.cpu_perc);
51                max = max.max(mem_perc).max(cpu_perc);
52            }
53
54            for (i, stats) in containers.iter().enumerate() {
55                self.print_container_stats(stats, i, containers.len(), max);
56            }
57        }
58
59        self.out_line("Press Ctrl+C to exit");
60
61        // Clear anything below the current cursor position (in case the new frame is shorter)
62        print!("\x1B[J");
63
64        // Flush to ensure the frame is pushed in one burst, reducing flicker
65        let _ = io::stdout().flush();
66    }
67
68    fn print_container_stats(&self, stats: &DockerStats, index: usize, total: usize, max: f32) {
69        // LAYOUT
70        if !self.compact || index == 0 {
71            self.out_line(&format!("┌─ {} {}┐", stats.name, filler("─", self.width, stats.name.len() + 5)));
72        } else {
73            self.out_line(&format!("├─ {} {}┤", stats.name, fill_on_even("─", self.width, stats.name.len() + 5)));
74        }
75
76        let mem_perc = perc_to_float(&stats.mem_perc);
77        let cpu_perc = perc_to_float(&stats.cpu_perc);
78
79        // CPU
80        let scale_factor = (self.width - 18) as f32 / max;
81        let cpu_perc_scaled = (cpu_perc * scale_factor) as usize;
82        let cpu_padding = filler(" ", 7, stats.cpu_perc.len());
83        let cpu_status = usize_to_status(cpu_perc_scaled, self.width);
84        let cpu_fill = filler("░", self.width, cpu_perc_scaled + 18).dimmed();
85
86        self.out_line(&format!("│ CPU | {cpu_padding}{} {cpu_status}{cpu_fill} │", stats.cpu_perc));
87
88        // RAM
89        let mem_usage_len = stats.mem_usage.len() + 1;
90        let scale_factor = (self.width - (18 + mem_usage_len)) as f32 / max;
91        let mem_perc_scaled = (mem_perc * scale_factor) as usize;
92        let mem_padding = filler(" ", 7, stats.mem_perc.len());
93        let mem_status = usize_to_status(mem_perc_scaled, self.width - (18 + mem_usage_len));
94        let mem_fill = filler("░", self.width, mem_perc_scaled + (18 + mem_usage_len)).dimmed();
95        let mem_spacing = filler(" ", mem_usage_len, mem_usage_len);
96
97        self.out_line(&format!(
98            "│ RAM | {mem_padding}{} {mem_status}{mem_fill}{mem_spacing} {} │",
99            stats.mem_perc, stats.mem_usage
100        ));
101
102        if self.full {
103            self.print_full_stats(stats);
104        }
105
106        if !self.compact || index == total - 1 {
107            self.out_line(&format!("└{}┘", filler("─", self.width, 2)));
108        }
109    }
110
111    fn print_full_stats(&self, stats: &DockerStats) {
112        self.out_line(&format!("│{}│", fill_on_even("─", self.width, 2).dimmed()));
113
114        // NET
115        if let Ok(net) = self.parse_network_stats(&stats.net_io) {
116            self.out_line(&format!(
117                "│ NET | {}{}{} │",
118                filler("▒", self.width - 11, net[0]).green(),
119                "░".dimmed(),
120                filler("▒", self.width - 11, net[1]).red()
121            ));
122        }
123
124        // IO
125        if let Ok(io) = self.parse_block_stats(&stats.block_io) {
126            self.out_line(&format!(
127                "│  IO | {}{}{} │",
128                filler("▒", io[0], 0).white(),
129                "░".dimmed(),
130                filler("▒", io[1], 0).black()
131            ));
132        }
133    }
134
135    fn parse_network_stats(&self, net_io: &str) -> Result<Vec<usize>, Box<dyn std::error::Error>> {
136        let parts: Vec<&str> = net_io.split(" / ").collect();
137        if parts.len() != 2 {
138            return Ok(balanced_split(self.width - 11));
139        }
140
141        let bytes = vec![
142            Byte::parse_str(parts[0], true)?.as_u128(),
143            Byte::parse_str(parts[1], true)?.as_u128(),
144        ];
145
146        Ok(scale_between(bytes, 1, self.width - 12).unwrap_or_else(|| balanced_split(self.width - 11)))
147    }
148
149    fn parse_block_stats(&self, block_io: &str) -> Result<Vec<usize>, Box<dyn std::error::Error>> {
150        let parts: Vec<&str> = block_io.split(" / ").collect();
151        if parts.len() != 2 {
152            return Ok(balanced_split(self.width - 11));
153        }
154
155        let bytes = vec![
156            Byte::parse_str(parts[0], true)?.as_u128(),
157            Byte::parse_str(parts[1], true)?.as_u128(),
158        ];
159
160        Ok(scale_between(bytes, 1, self.width - 12).unwrap_or_else(|| balanced_split(self.width - 11)))
161    }
162}