monitr 0.3.41

A lightweight macOS activity monitor TUI built with Rust and Ratatui
use std::{fs, path::PathBuf, time::Duration};

use serde::{Deserialize, Serialize};

use crate::app::{SortKey, Tab};

const CONFIG_VERSION: u32 = 1;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Preferences {
    version: u32,
    pub tab: String,
    pub sort_key: String,
    pub sort_desc: bool,
    pub show_details: bool,
    pub overview_visible: bool,
    pub interval_ms: u64,
    pub filter: String,
    pub compact_mode: bool,
}

impl Default for Preferences {
    fn default() -> Self {
        Self {
            version: CONFIG_VERSION,
            tab: "Cpu".to_string(),
            sort_key: "Cpu".to_string(),
            sort_desc: true,
            show_details: true,
            overview_visible: true,
            interval_ms: 1_000,
            filter: String::new(),
            compact_mode: false,
        }
    }
}

impl Preferences {
    pub fn load() -> Self {
        let path = match config_path() {
            Some(p) => p,
            None => return Self::default(),
        };
        if !path.exists() {
            return Self::default();
        }
        match fs::read_to_string(&path) {
            Ok(data) => match serde_json::from_str::<Preferences>(&data) {
                Ok(prefs) => prefs.migrate(),
                Err(_) => Self::default(),
            },
            Err(_) => Self::default(),
        }
    }

    pub fn save(&self) {
        let Some(path) = config_path() else {
            return;
        };
        if let Some(parent) = path.parent() {
            let _ = fs::create_dir_all(parent);
        }
        let _ = fs::write(&path, serde_json::to_string_pretty(self).unwrap_or_default());
    }

    fn migrate(mut self) -> Self {
        if self.version < CONFIG_VERSION {
            self.version = CONFIG_VERSION;
        }
        self
    }

    pub fn apply_tab(&self) -> Tab {
        match self.tab.as_str() {
            "Cpu" => Tab::Cpu,
            "Memory" => Tab::Memory,
            "Energy" => Tab::Energy,
            "Disk" => Tab::Disk,
            "Network" => Tab::Network,
            "Movers" => Tab::Movers,
            _ => Tab::Cpu,
        }
    }

    pub fn apply_sort_key(&self) -> SortKey {
        match self.sort_key.as_str() {
            "Cpu" => SortKey::Cpu,
            "Memory" => SortKey::Memory,
            "Energy" => SortKey::Energy,
            "DiskRead" => SortKey::DiskRead,
            "DiskWrite" => SortKey::DiskWrite,
            "NetworkIn" => SortKey::NetworkIn,
            "NetworkOut" => SortKey::NetworkOut,
            "Trend" => SortKey::Trend,
            "Name" => SortKey::Name,
            "Pid" => SortKey::Pid,
            "User" => SortKey::User,
            "Runtime" => SortKey::Runtime,
            _ => SortKey::Cpu,
        }
    }

    pub fn from_app(app: &PreferencesSource) -> Self {
        Self {
            version: CONFIG_VERSION,
            tab: app.tab.title().to_string(),
            sort_key: app
                .sort_key
                .title()
                .replace("% CPU", "Cpu")
                .replace("Impact", "Energy")
                .trim()
                .to_string(),
            sort_desc: app.sort_desc,
            show_details: app.show_details,
            overview_visible: app.overview_visible,
            interval_ms: app.interval.as_millis() as u64,
            filter: app.filter.clone(),
            compact_mode: app.compact_mode,
        }
    }
}

pub struct PreferencesSource {
    pub tab: Tab,
    pub sort_key: SortKey,
    pub sort_desc: bool,
    pub show_details: bool,
    pub overview_visible: bool,
    pub interval: Duration,
    pub filter: String,
    pub compact_mode: bool,
}

fn config_path() -> Option<PathBuf> {
    let home = std::env::var("HOME").ok()?;
    Some(
        PathBuf::from(home)
            .join("Library")
            .join("Application Support")
            .join("monitr")
            .join("config.json"),
    )
}