linux_fan_utility/
config.rs1use crate::curve::{self, FanCurve};
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12use std::fs;
13use std::io;
14use std::path::{Path, PathBuf};
15
16pub const DEFAULT_CONFIG_PATH: &str = "/etc/fanctl/config.toml";
18
19pub const DEFAULT_SOCKET_PATH: &str = "/run/fanctl.sock";
21
22pub const DEFAULT_POLL_INTERVAL_MS: u64 = 2000;
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct Config {
32 #[serde(default)]
34 pub daemon: DaemonConfig,
35
36 #[serde(default)]
38 pub curves: Vec<FanCurve>,
39
40 #[serde(default)]
42 pub fans: HashMap<String, FanAssignment>,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct DaemonConfig {
48 #[serde(default = "default_poll_interval")]
50 pub poll_interval_ms: u64,
51
52 #[serde(default = "default_socket_path")]
54 pub socket_path: String,
55
56 #[serde(default = "default_true")]
58 pub restore_on_exit: bool,
59}
60
61impl Default for DaemonConfig {
62 fn default() -> Self {
63 Self {
64 poll_interval_ms: DEFAULT_POLL_INTERVAL_MS,
65 socket_path: DEFAULT_SOCKET_PATH.to_string(),
66 restore_on_exit: true,
67 }
68 }
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
73#[serde(tag = "mode")]
74pub enum FanAssignment {
75 #[serde(rename = "auto")]
77 Auto,
78
79 #[serde(rename = "manual")]
81 Manual {
82 pwm: u8,
84 },
85
86 #[serde(rename = "curve")]
88 Curve {
89 curve_name: String,
91 temp_sensor_id: String,
93 },
94}
95
96impl Default for Config {
97 fn default() -> Self {
98 Self {
99 daemon: DaemonConfig::default(),
100 curves: vec![
101 curve::default_silent_curve(),
102 curve::default_performance_curve(),
103 ],
104 fans: HashMap::new(),
105 }
106 }
107}
108
109pub fn load_config(path: &Path) -> io::Result<Config> {
115 if !path.exists() {
116 log::info!("No config file at {}, using defaults", path.display());
117 return Ok(Config::default());
118 }
119
120 let contents = fs::read_to_string(path)?;
121 let config: Config = toml::from_str(&contents).map_err(|e| {
122 io::Error::new(
123 io::ErrorKind::InvalidData,
124 format!("Failed to parse config: {e}"),
125 )
126 })?;
127
128 log::info!("Loaded config from {}", path.display());
129 Ok(config)
130}
131
132pub fn save_config(path: &Path, config: &Config) -> io::Result<()> {
134 if let Some(parent) = path.parent() {
135 fs::create_dir_all(parent)?;
136 }
137
138 let contents = toml::to_string_pretty(config).map_err(|e| {
139 io::Error::new(
140 io::ErrorKind::InvalidData,
141 format!("Failed to serialize config: {e}"),
142 )
143 })?;
144
145 fs::write(path, contents)?;
146 log::info!("Saved config to {}", path.display());
147 Ok(())
148}
149
150pub fn resolve_config_path(cli_path: Option<&str>) -> PathBuf {
152 cli_path
153 .map(PathBuf::from)
154 .unwrap_or_else(|| PathBuf::from(DEFAULT_CONFIG_PATH))
155}
156
157fn default_poll_interval() -> u64 {
162 DEFAULT_POLL_INTERVAL_MS
163}
164
165fn default_socket_path() -> String {
166 DEFAULT_SOCKET_PATH.to_string()
167}
168
169fn default_true() -> bool {
170 true
171}