intercept_bounce/
config.rs

1use std::time::Duration;
2
3#[derive(Clone, Debug)]
4pub struct Config {
5    debounce_time: Duration,
6    near_miss_threshold: Duration,
7    log_interval: Duration,
8    pub log_all_events: bool,
9    pub log_bounces: bool,
10    pub stats_json: bool,
11    pub verbose: bool,
12    // Add log filter string
13    pub log_filter: String,
14    // OTLP endpoint
15    pub otel_endpoint: Option<String>,
16    // Ring buffer size for debugging
17    pub ring_buffer_size: usize,
18    debounce_keys: Vec<u16>,
19    ignored_keys: Vec<u16>,
20}
21
22impl Config {
23    /// Creates a new Config instance (primarily for testing/benchmarking).
24    #[allow(clippy::too_many_arguments)] // Allow many args for test/bench helper
25    pub fn new(
26        debounce_time: Duration,
27        near_miss_threshold: Duration,
28        log_interval: Duration,
29        log_all_events: bool,
30        log_bounces: bool,
31        stats_json: bool,
32        verbose: bool,
33        log_filter: String,
34        otel_endpoint: Option<String>,
35        ring_buffer_size: usize,
36        debounce_keys: Vec<u16>,
37        ignored_keys: Vec<u16>,
38    ) -> Self {
39        let mut debounce_keys = debounce_keys;
40        debounce_keys.sort_unstable();
41        debounce_keys.dedup();
42        let mut ignored_keys = ignored_keys;
43        ignored_keys.sort_unstable();
44        ignored_keys.dedup();
45        Self {
46            debounce_time,
47            near_miss_threshold,
48            log_interval,
49            log_all_events,
50            log_bounces,
51            stats_json,
52            verbose,
53            log_filter,
54            otel_endpoint,
55            ring_buffer_size,
56            debounce_keys,
57            ignored_keys,
58        }
59    }
60
61    // Provide accessor methods that return Duration
62    pub fn debounce_time(&self) -> Duration {
63        self.debounce_time
64    }
65    pub fn near_miss_threshold(&self) -> Duration {
66        self.near_miss_threshold
67    }
68    pub fn log_interval(&self) -> Duration {
69        self.log_interval
70    }
71
72    pub fn ignored_keys(&self) -> &[u16] {
73        &self.ignored_keys
74    }
75
76    pub fn debounce_keys(&self) -> &[u16] {
77        &self.debounce_keys
78    }
79
80    pub fn should_debounce(&self, key_code: u16) -> bool {
81        if !self.debounce_keys.is_empty() {
82            return self.debounce_keys.binary_search(&key_code).is_ok();
83        }
84
85        self.ignored_keys.binary_search(&key_code).is_err()
86    }
87
88    pub fn is_key_ignored(&self, key_code: u16) -> bool {
89        !self.should_debounce(key_code)
90    }
91
92    // Provide accessor methods that return u64 microseconds for internal use
93    pub fn debounce_us(&self) -> u64 {
94        self.debounce_time
95            .as_micros()
96            .try_into()
97            .unwrap_or(u64::MAX)
98    }
99    pub fn near_miss_threshold_us(&self) -> u64 {
100        self.near_miss_threshold
101            .as_micros()
102            .try_into()
103            .unwrap_or(u64::MAX)
104    }
105    pub fn log_interval_us(&self) -> u64 {
106        self.log_interval.as_micros().try_into().unwrap_or(u64::MAX)
107    }
108}
109
110impl From<&crate::cli::Args> for Config {
111    fn from(a: &crate::cli::Args) -> Self {
112        // Determine default log filter based on verbosity
113        let default_log_filter = if a.verbose {
114            "intercept_bounce=debug"
115        } else {
116            "intercept_bounce=info"
117        };
118        // Allow overriding with RUST_LOG environment variable
119        let log_filter =
120            std::env::var("RUST_LOG").unwrap_or_else(|_| default_log_filter.to_string()); // Keep to_string
121
122        Config::new(
123            a.debounce_time,
124            a.near_miss_threshold_time,
125            a.log_interval,
126            a.log_all_events,
127            a.log_bounces,
128            a.stats_json,
129            a.verbose,
130            log_filter,
131            a.otel_endpoint.clone(),
132            a.ring_buffer_size,
133            a.debounce_keys.clone(),
134            a.ignore_keys.clone(),
135        )
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::Config;
142    use std::time::Duration;
143
144    fn base_config() -> Config {
145        Config::new(
146            Duration::from_millis(25),
147            Duration::from_millis(100),
148            Duration::from_secs(15 * 60),
149            false,
150            false,
151            false,
152            false,
153            "intercept_bounce=info".to_string(),
154            None,
155            0,
156            Vec::new(),
157            Vec::new(),
158        )
159    }
160
161    #[test]
162    fn ignores_configured_keys_when_no_debounce_allowlist() {
163        let cfg = Config::new(
164            Duration::from_millis(25),
165            Duration::from_millis(100),
166            Duration::from_secs(15 * 60),
167            false,
168            false,
169            false,
170            false,
171            "intercept_bounce=info".to_string(),
172            None,
173            0,
174            Vec::new(),
175            vec![30],
176        );
177
178        assert!(cfg.is_key_ignored(30));
179        assert!(!cfg.should_debounce(30));
180        assert!(cfg.should_debounce(31));
181    }
182
183    #[test]
184    fn debounce_keys_take_precedence_over_ignore_keys() {
185        let cfg = Config::new(
186            Duration::from_millis(25),
187            Duration::from_millis(100),
188            Duration::from_secs(15 * 60),
189            false,
190            false,
191            false,
192            false,
193            "intercept_bounce=info".to_string(),
194            None,
195            0,
196            vec![30, 40],
197            vec![30],
198        );
199
200        assert!(
201            cfg.should_debounce(30),
202            "allowlisted keys must be debounced even if ignored"
203        );
204        assert!(cfg.should_debounce(40));
205        assert!(!cfg.should_debounce(31));
206        assert!(!cfg.should_debounce(0));
207    }
208
209    #[test]
210    fn should_debounce_respects_sorted_dedup_lists() {
211        let cfg = Config::new(
212            Duration::from_millis(25),
213            Duration::from_millis(100),
214            Duration::from_secs(15 * 60),
215            false,
216            false,
217            false,
218            false,
219            "intercept_bounce=info".to_string(),
220            None,
221            0,
222            vec![40, 30, 30],
223            vec![10, 10],
224        );
225
226        assert!(cfg.should_debounce(30));
227        assert!(cfg.should_debounce(40));
228        assert!(!cfg.should_debounce(10));
229    }
230
231    #[test]
232    fn base_config_debounces_all_keys_by_default() {
233        let cfg = base_config();
234        assert!(cfg.should_debounce(0));
235        assert!(cfg.should_debounce(u16::MAX));
236    }
237}