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 pub log_filter: String,
14 pub otel_endpoint: Option<String>,
16 pub ring_buffer_size: usize,
18 debounce_keys: Vec<u16>,
19 ignored_keys: Vec<u16>,
20}
21
22impl Config {
23 #[allow(clippy::too_many_arguments)] 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 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 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 let default_log_filter = if a.verbose {
114 "intercept_bounce=debug"
115 } else {
116 "intercept_bounce=info"
117 };
118 let log_filter =
120 std::env::var("RUST_LOG").unwrap_or_else(|_| default_log_filter.to_string()); 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}