use crate::paths::Paths;
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
pub struct Settings {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub proxy_auto: Option<bool>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub disabled: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub priority: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub proxy_threshold: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub proxy_strategy: Option<String>,
}
impl Settings {
pub fn auto(&self) -> bool {
self.proxy_auto.unwrap_or(false)
}
pub fn threshold(&self) -> Option<f64> {
self.proxy_threshold.map(|t| t.clamp(0.05, 1.0))
}
pub fn is_disabled(&self, name: &str) -> bool {
self.disabled.iter().any(|d| d == name)
}
pub fn toggle_disabled(&mut self, name: &str) -> bool {
if let Some(i) = self.disabled.iter().position(|d| d == name) {
self.disabled.remove(i);
false
} else {
self.disabled.push(name.to_string());
true
}
}
pub fn strategy(&self) -> crate::proxy::pick::Strategy {
self.proxy_strategy
.as_deref()
.and_then(crate::proxy::pick::Strategy::parse)
.unwrap_or_default()
}
pub fn rank(&self, name: &str) -> usize {
self.priority
.iter()
.position(|p| p == name)
.unwrap_or(usize::MAX)
}
}
fn file(paths: &Paths) -> std::path::PathBuf {
paths.store_dir().join("settings.json")
}
pub fn load(paths: &Paths) -> Settings {
std::fs::read(file(paths))
.ok()
.and_then(|b| serde_json::from_slice(&b).ok())
.unwrap_or_default()
}
pub fn save(paths: &Paths, s: &Settings) -> Result<()> {
let path = file(paths);
std::fs::create_dir_all(paths.store_dir()).context("create store dir")?;
let bytes = serde_json::to_vec_pretty(s)?;
crate::atomic::write_secret(&path, &bytes).context("write settings.json")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_threshold_persists_and_is_clamped_to_a_meaningful_range() {
let root = tempfile::tempdir().unwrap();
let paths = Paths::rooted(root.path());
assert_eq!(load(&paths).threshold(), None, "off until asked for");
save(
&paths,
&Settings {
proxy_threshold: Some(0.9),
..Default::default()
},
)
.unwrap();
assert_eq!(load(&paths).threshold(), Some(0.9));
let low = Settings {
proxy_threshold: Some(0.0),
..Default::default()
};
assert_eq!(low.threshold(), Some(0.05));
let high = Settings {
proxy_threshold: Some(5.0),
..Default::default()
};
assert_eq!(high.threshold(), Some(1.0));
}
#[test]
fn disabled_accounts_toggle_and_persist() {
let root = tempfile::tempdir().unwrap();
let paths = Paths::rooted(root.path());
let mut s = load(&paths);
assert!(!s.is_disabled("rnd"));
assert!(s.toggle_disabled("rnd"), "first toggle disables");
assert!(s.is_disabled("rnd"));
save(&paths, &s).unwrap();
let mut back = load(&paths);
assert!(back.is_disabled("rnd"), "the choice persists");
assert!(!back.toggle_disabled("rnd"), "toggling again re-enables");
assert!(!back.is_disabled("rnd"));
}
#[test]
fn ranked_accounts_sort_before_unranked_ones() {
let s = Settings {
priority: vec!["work".into(), "rnd".into()],
..Default::default()
};
assert!(s.rank("work") < s.rank("rnd"), "listed order is the order");
assert!(
s.rank("rnd") < s.rank("anything-else"),
"ranked beats unranked"
);
assert_eq!(
s.rank("a"),
s.rank("b"),
"unranked accounts keep their existing order"
);
}
#[test]
fn defaults_when_absent_and_round_trips_when_set() {
let root = tempfile::tempdir().unwrap();
let paths = Paths::rooted(root.path());
assert_eq!(load(&paths), Settings::default());
assert!(!load(&paths).auto(), "auto is off until asked for");
save(
&paths,
&Settings {
proxy_auto: Some(true),
..Default::default()
},
)
.unwrap();
assert!(load(&paths).auto(), "the preference persists");
save(
&paths,
&Settings {
proxy_auto: Some(false),
..Default::default()
},
)
.unwrap();
assert!(!load(&paths).auto(), "and can be turned back off");
}
#[test]
fn a_corrupt_file_reads_as_defaults_rather_than_failing() {
let root = tempfile::tempdir().unwrap();
let paths = Paths::rooted(root.path());
std::fs::create_dir_all(paths.store_dir()).unwrap();
std::fs::write(super::file(&paths), b"{ not json").unwrap();
assert_eq!(load(&paths), Settings::default());
}
}