use std::collections::VecDeque;
use std::time::{Duration, Instant};
use anyhow::Result;
use crossterm::event::{self, Event, KeyCode, KeyEventKind};
use ratatui::{
backend::Backend,
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Paragraph},
Frame, Terminal,
};
use sysinfo::{CpuRefreshKind, MemoryRefreshKind, RefreshKind, System};
use waveformchart::{WaveformMode, WaveformWidget};
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum DataSource {
Cpu,
Memory,
}
impl DataSource {
fn next(&self) -> Self {
match self {
Self::Cpu => Self::Memory,
Self::Memory => Self::Cpu,
}
}
fn label(&self) -> &'static str {
match self {
Self::Cpu => "CPU",
Self::Memory => "MEM",
}
}
}
pub struct App {
sys: System,
cpu_history: VecDeque<f64>,
mem_history: VecDeque<f64>,
max_history: usize,
pub running: bool,
pub tick_rate: Duration,
pub last_tick: Instant,
pub top_source: DataSource,
pub bottom_source: DataSource,
pub mode: WaveformMode,
pub fade_effect: bool,
pub gradient_effect: bool,
pub autoscale: bool,
pub top_color_idx: usize,
pub bottom_color_idx: usize,
pub colors: Vec<Color>,
}
impl App {
pub fn new() -> Self {
let mut sys = System::new_with_specifics(
RefreshKind::nothing()
.with_cpu(CpuRefreshKind::everything())
.with_memory(MemoryRefreshKind::everything()),
);
sys.refresh_cpu_all();
sys.refresh_memory();
Self {
sys,
cpu_history: VecDeque::with_capacity(500),
mem_history: VecDeque::with_capacity(500),
max_history: 500,
running: true,
tick_rate: Duration::from_millis(100),
last_tick: Instant::now(),
top_source: DataSource::Cpu,
bottom_source: DataSource::Memory,
mode: WaveformMode::HighResBraille,
fade_effect: false,
gradient_effect: false,
autoscale: false,
top_color_idx: 2, bottom_color_idx: 4, colors: vec![
Color::Reset,
Color::Red,
Color::Green,
Color::Yellow,
Color::Blue,
Color::Magenta,
Color::Cyan,
Color::White,
],
}
}
pub fn on_tick(&mut self) {
self.sys.refresh_cpu_all();
self.sys.refresh_memory();
let cpu_usage = self.sys.global_cpu_usage() as f64 / 100.0;
Self::push_history(&mut self.cpu_history, cpu_usage, self.max_history);
let total_mem = self.sys.total_memory() as f64;
let used_mem = self.sys.used_memory() as f64;
let mut mem_usage = if total_mem > 0.0 { used_mem / total_mem } else { 0.0 };
use rand::Rng;
let mut rng = rand::thread_rng();
let noise: f64 = rng.gen_range(-0.02..0.02);
mem_usage = (mem_usage + noise).clamp(0.0, 1.0);
Self::push_history(&mut self.mem_history, mem_usage, self.max_history);
}
fn push_history(history: &mut VecDeque<f64>, value: f64, max_history: usize) {
if history.len() >= max_history {
history.pop_front();
}
history.push_back(value);
}
pub fn handle_event(&mut self, event: Event) -> Result<()> {
if let Event::Key(key) = event {
if key.kind == KeyEventKind::Press {
match key.code {
KeyCode::Char('q') => self.running = false,
KeyCode::Char('1') => self.top_source = self.top_source.next(),
KeyCode::Char('2') => self.bottom_source = self.bottom_source.next(),
KeyCode::Char('+') => {
let new_millis = self.tick_rate.as_millis().saturating_sub(10);
if new_millis > 0 {
self.tick_rate = Duration::from_millis(new_millis as u64);
}
}
KeyCode::Char('-') => {
let new_millis = self.tick_rate.as_millis().saturating_add(10);
self.tick_rate = Duration::from_millis(new_millis as u64);
}
KeyCode::Char('c') => {
self.top_color_idx = (self.top_color_idx + 1) % self.colors.len();
self.bottom_color_idx = (self.bottom_color_idx + 1) % self.colors.len();
}
KeyCode::Char('m') => {
self.mode = match self.mode {
WaveformMode::HighResBraille => WaveformMode::UltraThinBlock,
WaveformMode::UltraThinBlock => WaveformMode::HighResBraille,
};
}
KeyCode::Char('f') => {
self.fade_effect = !self.fade_effect;
}
KeyCode::Char('g') => {
self.gradient_effect = !self.gradient_effect;
}
KeyCode::Char('s') => {
self.autoscale = !self.autoscale;
}
_ => {}
}
}
}
Ok(())
}
pub fn draw<B: Backend>(&mut self, f: &mut Frame<'_>) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Min(0),
Constraint::Length(1), ])
.split(f.area());
let main_area = chunks[0];
let status_area = chunks[1];
let width = main_area.width as usize;
self.cpu_history.make_contiguous();
self.mem_history.make_contiguous();
let top_data = match self.top_source {
DataSource::Cpu => self.cpu_history.as_slices().0,
DataSource::Memory => self.mem_history.as_slices().0,
};
let bottom_data = match self.bottom_source {
DataSource::Cpu => self.cpu_history.as_slices().0,
DataSource::Memory => self.mem_history.as_slices().0,
};
let top_len = top_data.len();
let top_start = top_len.saturating_sub(width);
let top_data = &top_data[top_start..];
let bottom_len = bottom_data.len();
let bottom_start = bottom_len.saturating_sub(width);
let bottom_data = &bottom_data[bottom_start..];
let top_color = self.colors[self.top_color_idx];
let bottom_color = self.colors[self.bottom_color_idx];
let top_max = if self.autoscale {
top_data.iter().fold(0.0f64, |a, &b| a.max(b)).max(0.001) } else {
1.0
};
let bottom_max = if self.autoscale {
bottom_data.iter().fold(0.0f64, |a, &b| a.max(b)).max(0.001)
} else {
1.0
};
let widget = WaveformWidget::new(top_data, bottom_data)
.block(Block::default().borders(Borders::ALL).title(" System Monitor "))
.mode(self.mode)
.fade_effect(self.fade_effect)
.gradient_effect(self.gradient_effect)
.top_style(Style::default().fg(top_color))
.bottom_style(Style::default().fg(bottom_color))
.top_max(top_max)
.bottom_max(bottom_max);
f.render_widget(widget, main_area);
let status_text = vec![
Span::raw(" [q] Quit "),
Span::raw(" [1] Top: "),
Span::styled(self.top_source.label(), Style::default().fg(top_color).add_modifier(Modifier::BOLD)),
Span::raw(" [2] Bot: "),
Span::styled(self.bottom_source.label(), Style::default().fg(bottom_color).add_modifier(Modifier::BOLD)),
Span::raw(format!(" [+/-] Speed: {}ms ", self.tick_rate.as_millis())),
Span::raw(" [c] Color "),
Span::raw(" [m] Mode "),
Span::raw(if self.fade_effect { " [f] Fade: ON " } else { " [f] Fade: OFF " }),
Span::raw(if self.fade_effect { " [f] Fade: ON " } else { " [f] Fade: OFF " }),
Span::raw(if self.gradient_effect { " [g] Grad: ON " } else { " [g] Grad: OFF " }),
Span::raw(if self.autoscale { " [s] Scale: AUTO " } else { " [s] Scale: 100% " }),
];
let status_paragraph = Paragraph::new(Line::from(status_text))
.style(Style::default().bg(Color::DarkGray).fg(Color::White));
f.render_widget(status_paragraph, status_area);
}
}