Skip to main content

focus_tracker_core/
config.rs

1use bon::bon;
2
3use crate::{FocusTrackerError, FocusTrackerResult};
4use std::time::Duration;
5
6fn validate_icon_size(size: u32) -> FocusTrackerResult<u32> {
7    if size == 0 {
8        return Err(FocusTrackerError::InvalidConfig {
9            reason: "icon size cannot be zero".into(),
10        });
11    }
12    if size > 512 {
13        return Err(FocusTrackerError::InvalidConfig {
14            reason: "icon size cannot be greater than 512 pixels".into(),
15        });
16    }
17    Ok(size)
18}
19
20fn validate_poll_interval(interval: Duration) -> FocusTrackerResult<Duration> {
21    if interval.is_zero() {
22        return Err(FocusTrackerError::InvalidConfig {
23            reason: "poll interval cannot be zero".into(),
24        });
25    }
26    if interval > Duration::from_secs(10) {
27        return Err(FocusTrackerError::InvalidConfig {
28            reason: "poll interval cannot be greater than 10 seconds".into(),
29        });
30    }
31    Ok(interval)
32}
33
34fn validate_icon_cache_capacity(capacity: usize) -> FocusTrackerResult<usize> {
35    if capacity == 0 {
36        return Err(FocusTrackerError::InvalidConfig {
37            reason: "icon cache capacity cannot be zero".into(),
38        });
39    }
40    Ok(capacity)
41}
42
43#[derive(Debug, Clone)]
44pub struct IconConfig {
45    pub size: Option<u32>,
46    pub filter_type: image::imageops::FilterType,
47}
48
49impl Default for IconConfig {
50    fn default() -> Self {
51        Self {
52            size: None,
53            filter_type: image::imageops::FilterType::Lanczos3,
54        }
55    }
56}
57
58#[bon]
59impl IconConfig {
60    /// Creates a new icon configuration using the builder pattern.
61    ///
62    /// # Example
63    ///
64    /// ```
65    /// use focus_tracker_core::IconConfig;
66    ///
67    /// // Default config (no custom size, Lanczos3 filter)
68    /// let config = IconConfig::builder().build();
69    ///
70    /// // Custom 64×64 icon size
71    /// let config = IconConfig::builder()
72    ///     .size(64)
73    ///     .unwrap()
74    ///     .build();
75    /// ```
76    #[builder]
77    pub fn new(
78        #[builder(with = |size: u32| -> Result<_, FocusTrackerError> {
79            validate_icon_size(size)
80        })]
81        size: Option<u32>,
82
83        #[builder(default = image::imageops::FilterType::Lanczos3)]
84        filter_type: image::imageops::FilterType,
85    ) -> Self {
86        Self { size, filter_type }
87    }
88}
89
90impl IconConfig {
91    #[must_use]
92    pub fn get_size_or_default(&self) -> u32 {
93        self.size.unwrap_or(128)
94    }
95}
96
97#[derive(Debug, Clone)]
98pub struct FocusTrackerConfig {
99    pub poll_interval: Duration,
100    pub icon: IconConfig,
101    pub icon_cache_capacity: usize,
102}
103
104impl Default for FocusTrackerConfig {
105    fn default() -> Self {
106        Self {
107            poll_interval: Duration::from_millis(100),
108            icon: IconConfig::default(),
109            icon_cache_capacity: 64,
110        }
111    }
112}
113
114#[bon]
115impl FocusTrackerConfig {
116    /// Creates a new focus tracker configuration using the builder pattern.
117    ///
118    /// # Example
119    ///
120    /// ```
121    /// use focus_tracker_core::{FocusTrackerConfig, IconConfig};
122    /// use std::time::Duration;
123    ///
124    /// let config = FocusTrackerConfig::builder()
125    ///     .poll_interval(Duration::from_millis(50))
126    ///     .unwrap()
127    ///     .icon(IconConfig::builder().size(64).unwrap().build())
128    ///     .build();
129    /// ```
130    #[builder]
131    pub fn new(
132        #[builder(
133            default = Duration::from_millis(100),
134            with = |interval: Duration| -> Result<_, FocusTrackerError> {
135                validate_poll_interval(interval)
136            },
137        )]
138        poll_interval: Duration,
139        #[builder(default)] icon: IconConfig,
140        #[builder(
141            default = 64,
142            with = |capacity: usize| -> Result<_, FocusTrackerError> {
143                validate_icon_cache_capacity(capacity)
144            },
145        )]
146        icon_cache_capacity: usize,
147    ) -> Self {
148        Self {
149            poll_interval,
150            icon,
151            icon_cache_capacity,
152        }
153    }
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159
160    #[test]
161    fn default_icon_config() {
162        let config = IconConfig::default();
163        assert_eq!(config.size, None);
164        assert_eq!(config.get_size_or_default(), 128);
165    }
166
167    #[test]
168    fn icon_builder_defaults() {
169        let config = IconConfig::builder().build();
170        assert_eq!(config.size, None);
171        assert_eq!(config.get_size_or_default(), 128);
172    }
173
174    #[test]
175    fn icon_builder_with_size() {
176        let config = IconConfig::builder().size(256).unwrap().build();
177        assert_eq!(config.size, Some(256));
178        assert_eq!(config.get_size_or_default(), 256);
179    }
180
181    #[test]
182    fn icon_builder_max_size() {
183        let config = IconConfig::builder().size(512).unwrap().build();
184        assert_eq!(config.size, Some(512));
185    }
186
187    #[test]
188    fn icon_builder_min_size() {
189        let config = IconConfig::builder().size(1).unwrap().build();
190        assert_eq!(config.size, Some(1));
191    }
192
193    #[test]
194    fn icon_builder_zero_size_errors() {
195        assert!(IconConfig::builder().size(0).is_err());
196    }
197
198    #[test]
199    fn icon_builder_oversized_errors() {
200        assert!(IconConfig::builder().size(513).is_err());
201        assert!(IconConfig::builder().size(1024).is_err());
202    }
203
204    #[test]
205    fn icon_builder_custom_filter() {
206        let config = IconConfig::builder()
207            .filter_type(image::imageops::FilterType::Nearest)
208            .build();
209        assert!(matches!(
210            config.filter_type,
211            image::imageops::FilterType::Nearest
212        ));
213    }
214
215    #[test]
216    fn default_config() {
217        let config = FocusTrackerConfig::default();
218        assert_eq!(config.poll_interval, Duration::from_millis(100));
219        assert_eq!(config.icon.size, None);
220        assert_eq!(config.icon_cache_capacity, 64);
221    }
222
223    #[test]
224    fn config_builder_defaults() {
225        let config = FocusTrackerConfig::builder().build();
226        assert_eq!(config.poll_interval, Duration::from_millis(100));
227        assert_eq!(config.icon.size, None);
228        assert_eq!(config.icon_cache_capacity, 64);
229    }
230
231    #[test]
232    fn config_builder_icon_cache_capacity() {
233        let config = FocusTrackerConfig::builder()
234            .icon_cache_capacity(128)
235            .unwrap()
236            .build();
237        assert_eq!(config.icon_cache_capacity, 128);
238    }
239
240    #[test]
241    fn config_builder_zero_cache_capacity_errors() {
242        assert!(
243            FocusTrackerConfig::builder()
244                .icon_cache_capacity(0)
245                .is_err()
246        );
247    }
248
249    #[test]
250    fn config_builder_poll_interval() {
251        let config = FocusTrackerConfig::builder()
252            .poll_interval(Duration::from_millis(250))
253            .unwrap()
254            .build();
255        assert_eq!(config.poll_interval, Duration::from_millis(250));
256    }
257
258    #[test]
259    fn config_builder_max_interval() {
260        let config = FocusTrackerConfig::builder()
261            .poll_interval(Duration::from_secs(10))
262            .unwrap()
263            .build();
264        assert_eq!(config.poll_interval, Duration::from_secs(10));
265    }
266
267    #[test]
268    fn config_builder_zero_interval_errors() {
269        assert!(
270            FocusTrackerConfig::builder()
271                .poll_interval(Duration::ZERO)
272                .is_err()
273        );
274    }
275
276    #[test]
277    fn config_builder_large_interval_errors() {
278        assert!(
279            FocusTrackerConfig::builder()
280                .poll_interval(Duration::from_secs(11))
281                .is_err()
282        );
283    }
284
285    #[test]
286    fn config_builder_with_icon() {
287        let icon = IconConfig::builder().size(64).unwrap().build();
288        let config = FocusTrackerConfig::builder().icon(icon).build();
289        assert_eq!(config.icon.size, Some(64));
290    }
291
292    #[test]
293    fn config_builder_full() {
294        let config = FocusTrackerConfig::builder()
295            .poll_interval(Duration::from_millis(50))
296            .unwrap()
297            .icon(IconConfig::builder().size(64).unwrap().build())
298            .build();
299
300        assert_eq!(config.poll_interval, Duration::from_millis(50));
301        assert_eq!(config.icon.size, Some(64));
302        assert_eq!(config.icon.get_size_or_default(), 64);
303    }
304}