use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use super::theme::{CustomThemeColors, ThemeName};
use crate::ui::app::{BarStyle, PinnedFlow};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Prefs {
#[serde(default)]
pub theme: ThemeName,
#[serde(default = "default_true")]
pub dns_resolution: bool,
#[serde(default = "default_true")]
pub port_resolution: bool,
#[serde(default = "default_true")]
pub show_ports: bool,
#[serde(default = "default_true")]
pub show_bars: bool,
#[serde(default)]
pub use_bytes: bool,
#[serde(default)]
pub show_processes: bool,
#[serde(default)]
pub show_cumulative: bool,
#[serde(default)]
pub bar_style: BarStyle,
#[serde(default)]
pub pinned: Vec<PinnedFlow>,
#[serde(default = "default_true")]
pub show_border: bool,
#[serde(default = "default_true")]
pub show_header: bool,
#[serde(default = "default_refresh")]
pub refresh_rate: u64,
#[serde(default)]
pub alert_threshold: f64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub interface: Option<String>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub custom_themes: HashMap<String, CustomThemeColors>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub active_custom_theme: Option<String>,
}
fn default_true() -> bool {
true
}
fn default_refresh() -> u64 {
1
}
impl Default for Prefs {
fn default() -> Self {
Prefs {
theme: ThemeName::default(),
dns_resolution: true,
port_resolution: true,
show_ports: true,
show_bars: true,
use_bytes: false,
show_processes: true,
show_cumulative: false,
bar_style: BarStyle::default(),
pinned: Vec::new(),
show_border: true,
show_header: true,
refresh_rate: 1,
alert_threshold: 0.0,
interface: None,
custom_themes: HashMap::new(),
active_custom_theme: None,
}
}
}
use std::sync::OnceLock;
static CUSTOM_CONFIG_PATH: OnceLock<std::path::PathBuf> = OnceLock::new();
pub fn set_config_path(path: std::path::PathBuf) {
let _ = CUSTOM_CONFIG_PATH.set(path);
}
fn prefs_path() -> Option<std::path::PathBuf> {
if let Some(p) = CUSTOM_CONFIG_PATH.get() {
return Some(p.clone());
}
dirs::home_dir().map(|h| h.join(".iftoprs.conf"))
}
pub fn load_prefs() -> Prefs {
let path = match prefs_path() {
Some(p) => p,
None => return Prefs::default(),
};
match std::fs::read_to_string(&path) {
Ok(contents) => toml::from_str(&contents).unwrap_or_default(),
Err(_) => {
let prefs = Prefs::default();
save_prefs(&prefs);
prefs
}
}
}
pub fn save_prefs(prefs: &Prefs) {
#[cfg(test)]
{ let _ = prefs; return; }
#[cfg(not(test))]
if let Some(path) = prefs_path()
&& let Ok(s) = toml::to_string_pretty(prefs) {
let _ = std::fs::write(path, s);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn prefs_default_values() {
let p = Prefs::default();
assert_eq!(p.theme, ThemeName::default());
assert!(p.dns_resolution);
assert!(p.port_resolution);
assert!(p.show_ports);
assert!(p.show_bars);
assert!(!p.use_bytes);
assert!(p.show_processes);
assert!(!p.show_cumulative);
assert_eq!(p.bar_style, BarStyle::default());
assert!(p.pinned.is_empty());
assert!(p.show_border);
assert!(p.show_header);
assert_eq!(p.refresh_rate, 1);
assert_eq!(p.alert_threshold, 0.0);
assert!(p.interface.is_none());
}
#[test]
fn prefs_serialize_deserialize() {
let p = Prefs::default();
let s = toml::to_string_pretty(&p).unwrap();
let p2: Prefs = toml::from_str(&s).unwrap();
assert_eq!(p2.theme, p.theme);
assert_eq!(p2.dns_resolution, p.dns_resolution);
assert_eq!(p2.show_border, p.show_border);
assert_eq!(p2.refresh_rate, p.refresh_rate);
}
#[test]
fn prefs_deserialize_empty_toml() {
let p: Prefs = toml::from_str("").unwrap();
assert_eq!(p.theme, ThemeName::default());
assert!(p.show_border);
assert_eq!(p.refresh_rate, 1);
}
#[test]
fn prefs_deserialize_partial_toml() {
let p: Prefs = toml::from_str("theme = \"BladeRunner\"\nuse_bytes = true").unwrap();
assert_eq!(p.theme, ThemeName::BladeRunner);
assert!(p.use_bytes);
assert!(p.show_border);
assert_eq!(p.refresh_rate, 1);
}
#[test]
fn prefs_interface_none_omitted() {
let p = Prefs::default();
let s = toml::to_string_pretty(&p).unwrap();
assert!(!s.contains("interface"));
}
#[test]
fn prefs_interface_some_included() {
let mut p = Prefs::default();
p.interface = Some("en0".into());
let s = toml::to_string_pretty(&p).unwrap();
assert!(s.contains("interface = \"en0\""));
}
#[test]
fn prefs_interface_roundtrip() {
let mut p = Prefs::default();
p.interface = Some("eth0".into());
let s = toml::to_string_pretty(&p).unwrap();
let p2: Prefs = toml::from_str(&s).unwrap();
assert_eq!(p2.interface, Some("eth0".into()));
}
#[test]
fn prefs_pinned_roundtrip() {
let mut p = Prefs::default();
p.pinned.push(crate::ui::app::PinnedFlow { src: "10.0.0.1".into(), dst: "10.0.0.2".into() });
let s = toml::to_string_pretty(&p).unwrap();
let p2: Prefs = toml::from_str(&s).unwrap();
assert_eq!(p2.pinned.len(), 1);
assert_eq!(p2.pinned[0].src, "10.0.0.1");
}
#[test]
fn prefs_custom_values_roundtrip() {
let mut p = Prefs::default();
p.theme = ThemeName::GlitchPop;
p.use_bytes = true;
p.show_border = false;
p.refresh_rate = 5;
p.alert_threshold = 1000.0;
p.bar_style = BarStyle::Thin;
let s = toml::to_string_pretty(&p).unwrap();
let p2: Prefs = toml::from_str(&s).unwrap();
assert_eq!(p2.theme, ThemeName::GlitchPop);
assert!(p2.use_bytes);
assert!(!p2.show_border);
assert_eq!(p2.refresh_rate, 5);
assert_eq!(p2.alert_threshold, 1000.0);
assert_eq!(p2.bar_style, BarStyle::Thin);
}
#[test]
fn save_prefs_no_op_in_test() {
let p = Prefs::default();
save_prefs(&p); }
#[test]
fn load_prefs_returns_valid() {
let p = load_prefs();
assert!(p.refresh_rate >= 1);
}
#[test]
fn default_true_helper() {
assert!(default_true());
}
#[test]
fn default_refresh_helper() {
assert_eq!(default_refresh(), 1);
}
}