Rust · 7189 bytes Raw Blame History
1 //! Configuration loading and management for garnotify
2
3 use anyhow::{Context, Result};
4 use serde::{Deserialize, Serialize};
5 use std::path::PathBuf;
6
7 use crate::rules::Rule;
8
9 /// Get the default configuration file path
10 fn config_path() -> PathBuf {
11 dirs::config_dir()
12 .unwrap_or_else(|| PathBuf::from("~/.config"))
13 .join("garnotify")
14 .join("config.toml")
15 }
16
17 /// Main configuration structure
18 #[derive(Debug, Clone, Serialize, Deserialize)]
19 #[serde(default)]
20 pub struct Config {
21 pub general: GeneralConfig,
22 pub geometry: GeometryConfig,
23 pub timeouts: TimeoutConfig,
24 pub appearance: AppearanceConfig,
25 pub animation: AnimationConfig,
26 pub history: HistoryConfig,
27 /// Notification rules (use [[rules]] in TOML)
28 #[serde(rename = "rules", default)]
29 pub rules: Vec<Rule>,
30 }
31
32 impl Default for Config {
33 fn default() -> Self {
34 Self {
35 general: GeneralConfig::default(),
36 geometry: GeometryConfig::default(),
37 timeouts: TimeoutConfig::default(),
38 appearance: AppearanceConfig::default(),
39 animation: AnimationConfig::default(),
40 history: HistoryConfig::default(),
41 rules: Vec::new(),
42 }
43 }
44 }
45
46 /// General daemon settings
47 #[derive(Debug, Clone, Serialize, Deserialize)]
48 #[serde(default)]
49 pub struct GeneralConfig {
50 /// Monitor to show notifications on: "primary", "mouse", or monitor name
51 pub monitor: String,
52 /// Follow mouse to determine monitor
53 pub follow_mouse: bool,
54 }
55
56 impl Default for GeneralConfig {
57 fn default() -> Self {
58 Self {
59 monitor: "primary".into(),
60 follow_mouse: false,
61 }
62 }
63 }
64
65 /// Geometry and positioning settings
66 #[derive(Debug, Clone, Serialize, Deserialize)]
67 #[serde(default)]
68 pub struct GeometryConfig {
69 /// Position anchor: top-right, top-left, top-center, bottom-right, bottom-left, bottom-center
70 pub position: String,
71 /// Notification width in pixels
72 pub width: u32,
73 /// Maximum height per notification (0 = unlimited)
74 pub max_height: u32,
75 /// Horizontal offset from screen edge
76 pub offset_x: i32,
77 /// Vertical offset from screen edge
78 pub offset_y: i32,
79 /// Gap between stacked notifications
80 pub gap: i32,
81 /// Maximum number of visible notifications (0 = unlimited)
82 pub max_visible: u32,
83 }
84
85 impl Default for GeometryConfig {
86 fn default() -> Self {
87 Self {
88 position: "top-right".into(),
89 width: 350,
90 max_height: 150,
91 offset_x: 20,
92 offset_y: 40,
93 gap: 10,
94 max_visible: 5,
95 }
96 }
97 }
98
99 /// Timeout settings per urgency level
100 #[derive(Debug, Clone, Serialize, Deserialize)]
101 #[serde(default)]
102 pub struct TimeoutConfig {
103 /// Default timeout in milliseconds (-1 = server default 5000ms)
104 pub default: i32,
105 /// Timeout for low urgency notifications
106 pub low: i32,
107 /// Timeout for normal urgency notifications
108 pub normal: i32,
109 /// Timeout for critical urgency notifications (0 = never expire)
110 pub critical: i32,
111 }
112
113 impl Default for TimeoutConfig {
114 fn default() -> Self {
115 Self {
116 default: 5000,
117 low: 10000,
118 normal: 5000,
119 critical: 0,
120 }
121 }
122 }
123
124 /// Appearance settings
125 #[derive(Debug, Clone, Serialize, Deserialize)]
126 #[serde(default)]
127 pub struct AppearanceConfig {
128 /// Font specification (Pango format)
129 pub font: String,
130 /// Title/summary font
131 pub title_font: String,
132 /// Icon size in pixels
133 pub icon_size: u32,
134 /// Padding inside notification
135 pub padding: u32,
136 /// Corner radius
137 pub corner_radius: u32,
138 /// Border width
139 pub border_width: u32,
140 /// Enable Pango markup in body
141 pub markup_enabled: bool,
142 /// Color scheme
143 pub colors: ColorConfig,
144 }
145
146 impl Default for AppearanceConfig {
147 fn default() -> Self {
148 Self {
149 font: "Sans 11".into(),
150 title_font: "Sans Bold 12".into(),
151 icon_size: 48,
152 padding: 12,
153 corner_radius: 8,
154 border_width: 2,
155 markup_enabled: true,
156 colors: ColorConfig::default(),
157 }
158 }
159 }
160
161 /// Color configuration
162 #[derive(Debug, Clone, Serialize, Deserialize)]
163 #[serde(default)]
164 pub struct ColorConfig {
165 /// Default background color
166 pub background: String,
167 /// Default foreground (text) color
168 pub foreground: String,
169 /// Default border color
170 pub border: String,
171
172 /// Low urgency background
173 pub low_background: String,
174 /// Low urgency foreground
175 pub low_foreground: String,
176 /// Low urgency border
177 pub low_border: String,
178
179 /// Critical urgency background
180 pub critical_background: String,
181 /// Critical urgency foreground
182 pub critical_foreground: String,
183 /// Critical urgency border
184 pub critical_border: String,
185 }
186
187 impl Default for ColorConfig {
188 fn default() -> Self {
189 // Catppuccin-inspired colors
190 Self {
191 background: "#1e1e2e".into(),
192 foreground: "#cdd6f4".into(),
193 border: "#45475a".into(),
194
195 low_background: "#1e1e2e".into(),
196 low_foreground: "#6c7086".into(),
197 low_border: "#45475a".into(),
198
199 critical_background: "#f38ba8".into(),
200 critical_foreground: "#1e1e2e".into(),
201 critical_border: "#f38ba8".into(),
202 }
203 }
204 }
205
206 /// Animation settings
207 #[derive(Debug, Clone, Serialize, Deserialize)]
208 #[serde(default)]
209 pub struct AnimationConfig {
210 /// Enable animations
211 pub enabled: bool,
212 /// Fade-in duration in milliseconds
213 pub fade_in: u32,
214 /// Fade-out duration in milliseconds
215 pub fade_out: u32,
216 /// Slide direction: up, down, left, right, none
217 pub slide: String,
218 /// Slide distance in pixels
219 pub slide_distance: u32,
220 }
221
222 impl Default for AnimationConfig {
223 fn default() -> Self {
224 Self {
225 enabled: true,
226 fade_in: 150,
227 fade_out: 150,
228 slide: "down".into(),
229 slide_distance: 20,
230 }
231 }
232 }
233
234 /// History settings
235 #[derive(Debug, Clone, Serialize, Deserialize)]
236 #[serde(default)]
237 pub struct HistoryConfig {
238 /// Maximum notifications to keep in history
239 pub max_length: usize,
240 /// Persist history to disk
241 pub persist: bool,
242 }
243
244 impl Default for HistoryConfig {
245 fn default() -> Self {
246 Self {
247 max_length: 100,
248 persist: false,
249 }
250 }
251 }
252
253 /// Load configuration from file
254 pub fn load(path: Option<&str>) -> Result<Config> {
255 let config_path = path.map(PathBuf::from).unwrap_or_else(config_path);
256
257 if config_path.exists() {
258 let content = std::fs::read_to_string(&config_path)
259 .with_context(|| format!("Failed to read config file: {}", config_path.display()))?;
260
261 let config: Config = toml::from_str(&content)
262 .with_context(|| format!("Failed to parse config file: {}", config_path.display()))?;
263
264 Ok(config)
265 } else {
266 // Config file doesn't exist, use defaults
267 Ok(Config::default())
268 }
269 }
270