1use std::fmt::Write as FmtWrite;
3use std::thread;
4use std::time::Duration;
5
6use sysinfo::System;
7
8use crate::modules::system_info::{SystemInfo, SystemReport};
9
10#[derive(Debug, Clone, Copy)]
11pub enum ViewType {
12 Stats,
13 Monitor,
14 Live,
15}
16
17impl std::fmt::Display for ViewType {
18 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
19 match self {
20 ViewType::Stats => write!(f, "Stats"),
21 ViewType::Monitor => write!(f, "Monitor"),
22 ViewType::Live => write!(f, "Live"),
23 }
24 }
25}
26
27struct StringBuffer {
28 buffer: String,
29}
30
31impl StringBuffer {
32 fn new() -> Self {
33 Self {
34 buffer: String::with_capacity(4096),
35 }
36 }
37
38 fn clear(&mut self) {
39 self.buffer.clear();
40 }
41
42 fn write_str(&mut self, s: &str) {
43 self.buffer.push_str(s);
44 }
45
46 fn write_fmt(&mut self, args: std::fmt::Arguments<'_>) {
47 let _ = self.buffer.write_fmt(args);
48 }
49
50 fn to_lines(&self) -> Vec<String> {
51 self.buffer.lines().map(String::from).collect()
52 }
53}
54
55pub struct SystemMonitor {
56 pub view: ViewType,
57 pub suggestion: bool,
58 pub verbose: bool,
59 pub left: Vec<String>,
60 pub right: Vec<String>,
61 sys: System,
62 left_buffer: StringBuffer,
63 right_buffer: StringBuffer,
64}
65
66impl SystemMonitor {
67 pub fn new(view: ViewType, suggestion: bool) -> Self {
68 Self::new_with_verbose(view, suggestion, false)
69 }
70
71 pub fn new_with_verbose(view: ViewType, suggestion: bool, verbose: bool) -> Self {
72 let sys = System::new_all();
73
74 Self {
75 view,
76 suggestion,
77 verbose,
78 left: Vec::new(),
79 right: Vec::new(),
80 sys,
81 left_buffer: StringBuffer::new(),
82 right_buffer: StringBuffer::new(),
83 }
84 }
85
86 pub fn update(&mut self) {
87 self.sys.refresh_cpu_all();
88 std::thread::sleep(Duration::from_millis(200));
89 self.sys.refresh_cpu_all();
90
91 let sys_info = SystemInfo::new();
92 let report = sys_info.generate_system_report(&self.sys);
93 self.format_system_info(&report);
94 }
95
96 fn format_option<T: std::fmt::Display + std::fmt::Debug>(
97 opt: Option<T>,
98 verbose: bool,
99 ) -> String {
100 if verbose {
101 format!("{:?}", opt)
102 } else {
103 opt.map(|v| v.to_string())
104 .unwrap_or_else(|| "Unknown".to_string())
105 }
106 }
107
108 fn format_battery_status(
109 is_charging: Option<bool>,
110 is_ac_plugged: Option<bool>,
111 verbose: bool,
112 ) -> String {
113 if verbose {
114 format!(
115 "is_charging: {:?}, is_ac_plugged: {:?}",
116 is_charging, is_ac_plugged
117 )
118 } else {
119 match (is_charging, is_ac_plugged) {
120 (Some(true), _) => "Charging".to_string(),
121 (Some(false), Some(false)) => "Discharging".to_string(),
122 (Some(false), Some(true)) => "Charged".to_string(),
123 _ => "Unknown".to_string(),
124 }
125 }
126 }
127
128 pub fn format_system_info(&mut self, report: &SystemReport) {
129 self.left_buffer.clear();
130 self.right_buffer.clear();
131
132 self.format_left_column(report);
133 self.format_right_column(report);
134
135 self.left = self.left_buffer.to_lines();
136 self.right = self.right_buffer.to_lines();
137 }
138
139 fn format_left_column(&mut self, report: &SystemReport) {
140 let buf = &mut self.left_buffer;
141
142 buf.write_str("System Information\n\n");
143 buf.write_fmt(format_args!(
144 "Linux distro: {} {}\n",
145 report.distro_name, report.distro_ver
146 ));
147 buf.write_fmt(format_args!("Linux kernel: {}\n", report.kernel_version));
148 buf.write_fmt(format_args!("Processor: {}\n", report.processor_model));
149
150 if self.verbose {
151 buf.write_fmt(format_args!("Cores: {:?}\n", report.total_core));
152 buf.write_fmt(format_args!("Driver: {:?}\n", report.cpu_driver));
153 } else {
154 buf.write_fmt(format_args!(
155 "Cores: {}\n",
156 Self::format_option(report.total_core, false)
157 ));
158 buf.write_fmt(format_args!(
159 "Driver: {}\n",
160 report.cpu_driver.as_deref().unwrap_or("Unknown")
161 ));
162 }
163
164 buf.write_fmt(format_args!("Architecture: {}\n\n", report.arch));
165
166 if crate::CONFIG.has_config() {
167 buf.write_fmt(format_args!(
168 "Using settings defined in {}\n\n",
169 crate::CONFIG.get_path().display()
170 ));
171 }
172
173 buf.write_str("Current CPU Stats\n\n");
174
175 if self.verbose {
176 buf.write_fmt(format_args!(
177 "CPU max frequency: {:?} MHz\n",
178 report.cpu_max_freq
179 ));
180 buf.write_fmt(format_args!(
181 "CPU min frequency: {:?} MHz\n\n",
182 report.cpu_min_freq
183 ));
184 } else {
185 let max_freq = report
186 .cpu_max_freq
187 .map(|f| format!("{:.0}", f))
188 .unwrap_or_else(|| "Unknown".to_string());
189 let min_freq = report
190 .cpu_min_freq
191 .map(|f| format!("{:.0}", f))
192 .unwrap_or_else(|| "Unknown".to_string());
193 buf.write_fmt(format_args!("CPU max frequency: {} MHz\n", max_freq));
194 buf.write_fmt(format_args!("CPU min frequency: {} MHz\n\n", min_freq));
195 }
196
197 buf.write_fmt(format_args!(
198 "{:<5} {:<7} {:<11} {:<8}\n",
199 "Core", "Usage", "Temp", "Freq"
200 ));
201
202 for core in &report.cores_info {
203 let temp_str = if core.temperature > 0.0 {
204 format!("{:.0}°C", core.temperature)
205 } else {
206 "--°C".to_string()
207 };
208
209 buf.write_fmt(format_args!(
210 "{:<5} {:>6.1}% {:<11} {:>5.0} MHz\n",
211 format!("CPU{}", core.id),
212 core.usage,
213 temp_str,
214 core.frequency
215 ));
216 }
217
218 if let Some(fan) = report.cpu_fan_speed {
219 buf.write_str("\n");
220 buf.write_fmt(format_args!("CPU fan speed: {} RPM\n", fan));
221 }
222 }
223
224 fn format_right_column(&mut self, report: &SystemReport) {
225 let buf = &mut self.right_buffer;
226
227 buf.write_str("Battery Stats\n\n");
228
229 if self.verbose {
230 buf.write_fmt(format_args!("Battery info: {:?}\n\n", report.battery_info));
231 } else {
232 let battery_status = Self::format_battery_status(
233 report.battery_info.is_charging,
234 report.battery_info.is_ac_plugged,
235 false,
236 );
237 buf.write_fmt(format_args!("Battery status: {}\n", battery_status));
238
239 let battery_level = report
240 .battery_info
241 .battery_level
242 .map(|b| format!("{}%", b))
243 .unwrap_or_else(|| "Unknown".to_string());
244 buf.write_fmt(format_args!("Battery level: {}\n", battery_level));
245
246 let ac_status = report
247 .battery_info
248 .is_ac_plugged
249 .map(|ac| if ac { "Yes" } else { "No" })
250 .unwrap_or("Unknown");
251 buf.write_fmt(format_args!("AC plugged: {}\n", ac_status));
252
253 let start_threshold = report
254 .battery_info
255 .charging_start_threshold
256 .map(|t| format!("{}%", t))
257 .unwrap_or_else(|| "Not set".to_string());
258 buf.write_fmt(format_args!("Start threshold: {}\n", start_threshold));
259
260 let stop_threshold = report
261 .battery_info
262 .charging_stop_threshold
263 .map(|t| format!("{}%", t))
264 .unwrap_or_else(|| "Not set".to_string());
265 buf.write_fmt(format_args!("Stop threshold: {}\n\n", stop_threshold));
266 }
267
268 buf.write_str("CPU Frequency Scaling\n\n");
269
270 if self.verbose {
271 buf.write_fmt(format_args!("Current governor: {:?}\n", report.current_gov));
272 buf.write_fmt(format_args!("EPP: {:?}\n", report.current_epp));
273 buf.write_fmt(format_args!("EPB: {:?}\n", report.current_epb));
274 } else {
275 let current_gov = report.current_gov.as_deref().unwrap_or("Unknown");
276 buf.write_fmt(format_args!("Current governor: {}\n", current_gov));
277
278 if let Some(epp) = &report.current_epp {
279 buf.write_fmt(format_args!("EPP: {}\n", epp));
280 } else {
281 buf.write_str("EPP: Not supported\n");
282 }
283
284 if let Some(epb) = &report.current_epb {
285 buf.write_fmt(format_args!("EPB: {}\n", epb));
286 }
287 }
288
289 if self.suggestion {
290 if let Some(sugg) = SystemInfo::governor_suggestion() {
291 if report.current_gov.as_deref() != Some(&sugg) {
292 buf.write_fmt(format_args!("Suggested governor: {}\n", sugg));
293 }
294 }
295 }
296
297 buf.write_str("\n");
298
299 buf.write_str("System Statistics\n\n");
300 buf.write_fmt(format_args!("CPU usage: {:.1}%\n", report.cpu_usage));
301 buf.write_fmt(format_args!("System load: {:.2}\n", report.load));
302
303 if !report.cores_info.is_empty() {
304 let avg_temp: f32 = report
305 .cores_info
306 .iter()
307 .map(|c| c.temperature)
308 .filter(|&t| t > 0.0)
309 .sum::<f32>();
310 let temp_count = report
311 .cores_info
312 .iter()
313 .filter(|c| c.temperature > 0.0)
314 .count();
315
316 if temp_count > 0 {
317 let avg_temp = avg_temp / temp_count as f32;
318 buf.write_fmt(format_args!("Average temp: {:.1} °C\n", avg_temp));
319 }
320 }
321
322 if let Some((a, b, c)) = report.avg_load {
323 let load_status = if report.load < 1.0 { "optimal" } else { "high" };
324 buf.write_fmt(format_args!(
325 "Load {}: {:.2}, {:.2}, {:.2}\n",
326 load_status, a, b, c
327 ));
328 }
329
330 if self.verbose {
331 buf.write_fmt(format_args!("Turbo boost: {:?}\n", report.is_turbo_on));
332 } else {
333 let turbo_status = match (report.is_turbo_on.0, report.is_turbo_on.1) {
334 (Some(on), _) => if on { "On" } else { "Off" }.to_string(),
335 (None, Some(auto)) => {
336 format!("Auto ({})", if auto { "enabled" } else { "disabled" })
337 }
338 _ => "Unknown".to_string(),
339 };
340 buf.write_fmt(format_args!("Turbo boost: {}\n", turbo_status));
341 }
342
343 if self.suggestion {
344 if let Some(on) = report.is_turbo_on.0 {
345 let sugg = SystemInfo::turbo_on_suggestion(&self.sys);
346 if sugg != on {
347 buf.write_fmt(format_args!(
348 "Suggested turbo: {}\n",
349 if sugg { "On" } else { "Off" }
350 ));
351 }
352 }
353 }
354 }
355
356 pub fn run_blocking(&mut self) {
357 loop {
358 self.update();
359
360 print!("\x1B[2J\x1B[1;1H");
361
362 let width = 100usize;
363 let half = width / 2 - 2;
364 let rows = std::cmp::max(self.left.len(), self.right.len());
365
366 for i in 0..rows {
367 let left = self.left.get(i).map(String::as_str).unwrap_or("");
368 let right = self.right.get(i).map(String::as_str).unwrap_or("");
369
370 if left.len() > half {
371 let truncate_at = half.saturating_sub(3);
372 println!(
373 "{:<half$}... │ {}",
374 &left[..truncate_at],
375 right,
376 half = half
377 );
378 } else {
379 println!("{:<half$} │ {}", left, right, half = half);
380 }
381 }
382
383 thread::sleep(Duration::from_secs(2));
384 }
385 }
386}
387
388#[cfg(test)]
389mod tests {
390 use super::*;
391
392 #[test]
393 fn test_string_buffer() {
394 let mut buf = StringBuffer::new();
395 buf.write_str("Hello\n");
396 buf.write_fmt(format_args!("World {}\n", 123));
397 let lines = buf.to_lines();
398 assert_eq!(lines.len(), 2);
399 assert_eq!(lines[0], "Hello");
400 assert_eq!(lines[1], "World 123");
401 }
402
403 #[test]
404 fn test_monitor_update() {
405 let mut monitor = SystemMonitor::new(ViewType::Monitor, false);
406 monitor.update();
407 assert!(!monitor.left.is_empty());
408 assert!(!monitor.right.is_empty());
409 }
410}