tegratop 0.2.1

TUI for monitoring Nvidia jetson boards
Documentation
use anyhow::Context;
use log::error;

use anyhow::Result;
use std::{
    fs::{self, File},
    io::{Read, Seek},
};
use strum_macros::Display;

use ratatui::{
    layout::{Constraint, Rect},
    style::{Style, Stylize},
    widgets::{Block, Borders, Padding, Row, Table},
    Frame,
};

#[derive(Debug, Default)]
pub struct Fan {
    rpm: Option<FanRPM>,
    profile: Option<FanProfile>,
}

#[derive(Debug)]
struct FanRPM {
    file: File,
    value: usize,
}

#[derive(Debug)]
struct FanProfile {
    file: File,
    value: Profile,
}

#[derive(Debug, Display)]
enum Profile {
    Quiet,
    Cool,
    Unknown,
}

impl FanRPM {
    fn new() -> Result<Option<Self>> {
        let entries = fs::read_dir("/sys/class/hwmon/")
            .context("Failed to read from the directory /sys/class/hwmon")?;

        for entry in entries {
            let entry = entry?;
            let entry_path = entry.path();

            if entry_path.join("rpm").exists() {
                let rpm_file_path = entry_path.join("rpm");
                let mut file = File::open(&rpm_file_path)
                    .context(format!("Failed to read from {}", &rpm_file_path.display()))?;
                let mut buffer = String::new();
                file.read_to_string(&mut buffer)?;

                let fan_rpm = buffer.trim().parse::<usize>()?;

                return Ok(Some(FanRPM {
                    file,
                    value: fan_rpm,
                }));
            }
        }

        Ok(None)
    }

    fn refresh(&mut self) -> Result<()> {
        self.file.seek(std::io::SeekFrom::Start(0))?;
        let mut buffer = String::new();
        self.file.read_to_string(&mut buffer)?;
        let fan_rpm = buffer.trim().parse::<usize>()?;
        self.value = fan_rpm;
        Ok(())
    }
}

impl FanProfile {
    fn new() -> Result<Option<Self>> {
        let mut file = File::open("/etc/nvfancontrol.conf")
            .context("Faile to read from /etc/nvfancontrol.conf")?;
        let mut buffer = String::new();
        file.read_to_string(&mut buffer)?;

        for line in buffer.lines() {
            let parts: Vec<&str> = line.split_whitespace().collect();
            if let ["FAN_DEFAULT_PROFILE", mode] = parts.as_slice() {
                match *mode {
                    "quiet" => {
                        return Ok(Some(FanProfile {
                            file,
                            value: Profile::Quiet,
                        }))
                    }
                    "cool" => {
                        return Ok(Some(FanProfile {
                            file,
                            value: Profile::Cool,
                        }))
                    }
                    _ => {
                        return Ok(Some(FanProfile {
                            file,
                            value: Profile::Unknown,
                        }))
                    }
                }
            }
        }

        Ok(None)
    }

    fn refresh(&mut self) -> Result<()> {
        self.file.seek(std::io::SeekFrom::Start(0))?;
        let mut buffer = String::new();
        self.file.read_to_string(&mut buffer)?;

        for line in buffer.lines() {
            let parts: Vec<&str> = line.split_whitespace().collect();
            if let ["FAN_DEFAULT_PROFILE", mode] = parts.as_slice() {
                match *mode {
                    "quiet" => {
                        self.value = Profile::Quiet;
                    }
                    "cool" => {
                        self.value = Profile::Cool;
                    }
                    _ => {
                        self.value = Profile::Unknown;
                    }
                }
            }
        }
        Ok(())
    }
}

impl Fan {
    pub fn new() -> Self {
        let rpm = match FanRPM::new() {
            Ok(rpm) => rpm,
            Err(e) => {
                error!("{}", e);
                None
            }
        };

        let profile = match FanProfile::new() {
            Ok(profile) => profile,
            Err(e) => {
                error!("{}", e);
                None
            }
        };

        Self { rpm, profile }
    }

    pub fn refresh(&mut self) {
        if let Some(rpm) = &mut self.rpm {
            if let Err(e) = rpm.refresh() {
                error!("{}", e);
            }
        }
        if let Some(profile) = &mut self.profile {
            if let Err(e) = profile.refresh() {
                error!("{}", e);
            }
        }
    }

    pub fn render(&self, frame: &mut Frame, block: Rect) {
        let rows: [Row; 1] = [Row::new(vec![
            match &self.profile {
                Some(profile) => profile.value.to_string().to_lowercase(),
                None => " - ".to_string(),
            },
            match &self.rpm {
                Some(rpm) => rpm.value.to_string(),
                None => " - ".to_string(),
            },
        ])];

        let widths = [Constraint::Length(10), Constraint::Fill(1)];

        let fan = Table::new(rows, widths)
            .header(Row::new(vec!["Profile", "RPM"]).style(Style::new().bold()))
            .block(
                Block::default()
                    .title("Fan")
                    .title_style(Style::new().bold())
                    .padding(Padding::horizontal(1))
                    .borders(Borders::ALL),
            );

        frame.render_widget(fan, block);
    }
}