yuno 0.1.1

A declarative UI layout and rendering framework powered by Skia.
use std::collections::VecDeque;
use std::fs;
use std::io::{BufRead, BufReader};
use std::process::{Command, Stdio};
use std::sync::mpsc::{self, Receiver};
use std::thread;
use std::time::{Duration, Instant};

pub struct HwStats {
    pub cpu_usage: f32,
    pub cpu_history: VecDeque<f32>,
    pub mem_usage: f32, // RAM %

    pub gpu_usage: f32,
    pub gpu_history: VecDeque<f32>,
    pub vram_usage: f32, // VRAM %
    pub gpu_name: String,

    last_proc_time: u64,
    last_cpu_total: u64,
    last_update: Instant,
    nvtop_rx: Receiver<String>,
}

impl Default for HwStats {
    fn default() -> Self {
        let mut history = VecDeque::with_capacity(60);
        history.resize(60, 0.0);

        let mut gpu_history = VecDeque::with_capacity(60);
        gpu_history.resize(60, 0.0);

        let (tx, rx) = mpsc::channel();

        thread::spawn(move || {
            let mut child = match Command::new("nvtop")
                .args(["-l", "-d", "10"])
                .stdout(Stdio::piped())
                .stderr(Stdio::null())
                .spawn()
            {
                Ok(c) => c,
                Err(_) => return,
            };

            if let Some(stdout) = child.stdout.take() {
                let reader = BufReader::new(stdout);
                let mut json_buffer = String::new();
                let mut bracket_count = 0;

                for line in reader.lines().map_while(Result::ok) {
                    for c in line.chars() {
                        if c == '[' {
                            bracket_count += 1;
                        }
                        if bracket_count > 0 {
                            json_buffer.push(c);
                        }
                        if c == ']' {
                            bracket_count -= 1;
                            if bracket_count == 0 {
                                let _ = tx.send(json_buffer.clone());
                                json_buffer.clear();
                            }
                        }
                    }
                    if bracket_count > 0 {
                        json_buffer.push('\n');
                    }
                }
            }
            let _ = child.wait();
        });

        Self {
            cpu_usage: 0.0,
            cpu_history: history,
            mem_usage: 0.0,
            gpu_usage: 0.0,
            gpu_history,
            vram_usage: 0.0,
            gpu_name: "DRM Primary Device".to_string(),
            last_proc_time: 0,
            last_cpu_total: 0,
            last_update: Instant::now().checked_sub(Duration::from_secs(10)).unwrap(),
            nvtop_rx: rx,
        }
    }
}

fn parse_numeric(val: &str) -> f32 {
    val.chars()
        .take_while(|c| c.is_numeric() || *c == '.')
        .collect::<String>()
        .parse::<f32>()
        .unwrap_or(0.0)
}

impl HwStats {
    pub fn update_throttled(&mut self) {
        if self.last_update.elapsed().as_millis() < 1000 {
            return;
        }
        self.last_update = Instant::now();
        let my_pid = std::process::id().to_string();

        let mut mem_total = 0.0;
        if let Ok(meminfo) = fs::read_to_string("/proc/meminfo") {
            for line in meminfo.lines() {
                if line.starts_with("MemTotal:") {
                    mem_total = line
                        .split_whitespace()
                        .nth(1)
                        .unwrap_or("0")
                        .parse()
                        .unwrap_or(0.0);
                    break;
                }
            }
        }
        if mem_total > 0.0
            && let Ok(status) = fs::read_to_string("/proc/self/status")
        {
            for line in status.lines() {
                if line.starts_with("VmRSS:") {
                    let proc_rss: f32 = line
                        .split_whitespace()
                        .nth(1)
                        .unwrap_or("0")
                        .parse()
                        .unwrap_or(0.0);
                    self.mem_usage = (proc_rss / mem_total) * 100.0;
                    break;
                }
            }
        }

        let mut current_cpu_total = 0u64;
        if let Ok(stat) = fs::read_to_string("/proc/stat")
            && let Some(cpu_line) = stat.lines().next()
        {
            current_cpu_total = cpu_line
                .split_whitespace()
                .skip(1)
                .filter_map(|s| s.parse::<u64>().ok())
                .sum();
        }
        if let Ok(proc_stat) = fs::read_to_string("/proc/self/stat") {
            let parts: Vec<&str> = proc_stat.split_whitespace().collect();
            if parts.len() >= 15 {
                let utime: u64 = parts[13].parse().unwrap_or(0);
                let stime: u64 = parts[14].parse().unwrap_or(0);
                let current_proc_time = utime + stime;

                let diff_proc = current_proc_time.saturating_sub(self.last_proc_time);
                let diff_total = current_cpu_total.saturating_sub(self.last_cpu_total);

                if diff_total > 0 && self.last_cpu_total > 0 {
                    self.cpu_usage = (diff_proc as f32 / diff_total as f32) * 1000.0;
                    self.cpu_history.pop_front();
                    self.cpu_history.push_back(self.cpu_usage);
                }
                self.last_proc_time = current_proc_time;
                self.last_cpu_total = current_cpu_total;
            }
        }

        while let Ok(json_str) = self.nvtop_rx.try_recv() {
            if let Ok(devices) = serde_json::from_str::<Vec<serde_json::Value>>(&json_str) {
                for dev in devices {
                    let dev_name = dev["device_name"].as_str().unwrap_or("GPU");
                    let gpu_mem_total_str = dev["mem_total"].as_str().unwrap_or("0");
                    let gpu_mem_total = parse_numeric(gpu_mem_total_str);

                    if let Some(procs) = dev["processes"].as_array()
                        && let Some(my_proc) = procs
                            .iter()
                            .find(|p| p["pid"].as_str().unwrap_or("") == my_pid)
                    {
                        self.gpu_name = dev_name.to_string();
                        self.gpu_usage = my_proc["gpu_usage"]
                            .as_str()
                            .unwrap_or("0")
                            .parse::<f32>()
                            .unwrap_or(0.0);

                        let alloc_str = my_proc["gpu_mem_bytes_alloc"].as_str().unwrap_or("0");
                        let alloc = parse_numeric(alloc_str);

                        if gpu_mem_total > 0.0 {
                            self.vram_usage = (alloc / gpu_mem_total) * 100.0;
                        }

                        self.gpu_history.pop_front();
                        self.gpu_history.push_back(self.gpu_usage);
                        break;
                    }
                }
            }
        }
    }
}