use std::collections::HashMap;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub struct StintConfig {
pub idle_threshold_secs: i64,
pub default_rate_cents: Option<i64>,
pub auto_discover: bool,
pub default_tags: Vec<String>,
}
impl Default for StintConfig {
fn default() -> Self {
Self {
idle_threshold_secs: 300,
default_rate_cents: None,
auto_discover: true,
default_tags: vec![],
}
}
}
impl StintConfig {
pub fn default_path() -> PathBuf {
let config_dir = dirs::config_dir().unwrap_or_else(|| PathBuf::from(".config"));
config_dir.join("stint").join("config.toml")
}
pub fn load() -> Self {
let path = Self::default_path();
Self::load_from(&path).unwrap_or_default()
}
pub fn load_from(path: &Path) -> Option<Self> {
let contents = std::fs::read_to_string(path).ok()?;
Some(Self::parse(&contents))
}
fn parse(contents: &str) -> Self {
let mut config = Self::default();
let mut values: HashMap<String, String> = HashMap::new();
for line in contents.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Some((key, value)) = line.split_once('=') {
let key = key.trim().to_string();
let value = value.trim().trim_matches('"').to_string();
values.insert(key, value);
}
}
if let Some(v) = values.get("idle_threshold") {
if let Ok(secs) = v.parse::<i64>() {
config.idle_threshold_secs = secs;
}
}
if let Some(v) = values.get("default_rate") {
if let Ok(cents) = v.parse::<i64>() {
config.default_rate_cents = Some(cents);
}
}
if let Some(v) = values.get("auto_discover") {
match v.trim().to_ascii_lowercase().as_str() {
"true" | "1" | "yes" | "on" => config.auto_discover = true,
"false" | "0" | "no" | "off" => config.auto_discover = false,
_ => {} }
}
if let Some(v) = values.get("default_tags") {
config.default_tags = v
.split(',')
.map(|t| t.trim().to_string())
.filter(|t| !t.is_empty())
.collect();
}
config
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_config() {
let config = StintConfig::default();
assert_eq!(config.idle_threshold_secs, 300);
assert!(config.auto_discover);
assert!(config.default_rate_cents.is_none());
}
#[test]
fn parse_all_fields() {
let input = r#"
# Stint configuration
idle_threshold = 600
default_rate = 15000
auto_discover = true
default_tags = rust, cli
"#;
let config = StintConfig::parse(input);
assert_eq!(config.idle_threshold_secs, 600);
assert_eq!(config.default_rate_cents, Some(15000));
assert!(config.auto_discover);
assert_eq!(config.default_tags, vec!["rust", "cli"]);
}
#[test]
fn parse_disables_auto_discover() {
let input = "auto_discover = false";
let config = StintConfig::parse(input);
assert!(!config.auto_discover);
}
#[test]
fn parse_ignores_unknown_keys() {
let input = "unknown_key = whatever\nidle_threshold = 120";
let config = StintConfig::parse(input);
assert_eq!(config.idle_threshold_secs, 120);
}
#[test]
fn parse_empty_string() {
let config = StintConfig::parse("");
assert_eq!(config.idle_threshold_secs, 300); }
#[test]
fn missing_file_returns_none() {
assert!(StintConfig::load_from(Path::new("/nonexistent/path")).is_none());
}
}