1use thiserror::Error;
6
7pub type Result<T> = std::result::Result<T, InputError>;
9
10#[derive(Error, Debug)]
12pub enum InputError {
13 #[error("Portal remote desktop error: {0}")]
15 PortalError(String),
16
17 #[error("Scancode translation failed: {0}")]
19 ScancodeTranslationFailed(String),
20
21 #[error("Unknown scancode: 0x{0:04X}")]
23 UnknownScancode(u16),
24
25 #[error("Unknown keycode: {0}")]
27 UnknownKeycode(u32),
28
29 #[error("Coordinate transformation error: {0}")]
31 CoordinateTransformError(String),
32
33 #[error("Monitor not found: {0}")]
35 MonitorNotFound(u32),
36
37 #[error("Invalid coordinate: ({0}, {1})")]
39 InvalidCoordinate(f64, f64),
40
41 #[error("Invalid monitor configuration: {0}")]
43 InvalidMonitorConfig(String),
44
45 #[error("Keyboard layout error: {0}")]
47 LayoutError(String),
48
49 #[error("Layout not found: {0}")]
51 LayoutNotFound(String),
52
53 #[error("XKB error: {0}")]
55 XkbError(String),
56
57 #[error("Event queue is full")]
59 EventQueueFull,
60
61 #[error("Failed to send event")]
63 EventSendFailed,
64
65 #[error("Failed to receive event")]
67 EventReceiveFailed,
68
69 #[error("Input latency too high: {0}ms (max: {1}ms)")]
71 LatencyTooHigh(u64, u64),
72
73 #[error("Invalid state: {0}")]
75 InvalidState(String),
76
77 #[error("Portal session error: {0}")]
79 PortalSessionError(String),
80
81 #[error("DBus error: {0}")]
83 DBusError(String),
84
85 #[error("IO error: {0}")]
87 Io(#[from] std::io::Error),
88
89 #[error("Invalid key event: {0}")]
91 InvalidKeyEvent(String),
92
93 #[error("Invalid mouse event: {0}")]
95 InvalidMouseEvent(String),
96
97 #[error("Unknown error: {0}")]
99 Unknown(String),
100}
101
102#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
104pub enum ErrorType {
105 Portal,
107 Translation,
109 Coordinate,
111 Layout,
113 EventQueue,
115 Performance,
117 State,
119 Unknown,
121}
122
123pub fn classify_error(error: &InputError) -> ErrorType {
125 match error {
126 InputError::PortalError(_) | InputError::PortalSessionError(_) | InputError::DBusError(_) => ErrorType::Portal,
127
128 InputError::ScancodeTranslationFailed(_) | InputError::UnknownScancode(_) | InputError::UnknownKeycode(_) => {
129 ErrorType::Translation
130 }
131
132 InputError::CoordinateTransformError(_)
133 | InputError::MonitorNotFound(_)
134 | InputError::InvalidCoordinate(_, _)
135 | InputError::InvalidMonitorConfig(_) => ErrorType::Coordinate,
136
137 InputError::LayoutError(_) | InputError::LayoutNotFound(_) | InputError::XkbError(_) => ErrorType::Layout,
138
139 InputError::EventQueueFull | InputError::EventSendFailed | InputError::EventReceiveFailed => {
140 ErrorType::EventQueue
141 }
142
143 InputError::LatencyTooHigh(_, _) => ErrorType::Performance,
144
145 InputError::InvalidState(_) => ErrorType::State,
146
147 _ => ErrorType::Unknown,
148 }
149}
150
151#[derive(Debug, Clone)]
153pub struct ErrorContext {
154 pub scancode: Option<u16>,
156
157 pub keycode: Option<u32>,
159
160 pub coordinates: Option<(f64, f64)>,
162
163 pub monitor_id: Option<u32>,
165
166 pub layout: Option<String>,
168
169 pub attempt: u32,
171
172 pub details: String,
174}
175
176impl ErrorContext {
177 pub fn new() -> Self {
179 Self {
180 scancode: None,
181 keycode: None,
182 coordinates: None,
183 monitor_id: None,
184 layout: None,
185 attempt: 0,
186 details: String::new(),
187 }
188 }
189
190 pub fn with_scancode(mut self, scancode: u16) -> Self {
192 self.scancode = Some(scancode);
193 self
194 }
195
196 pub fn with_keycode(mut self, keycode: u32) -> Self {
198 self.keycode = Some(keycode);
199 self
200 }
201
202 pub fn with_coordinates(mut self, x: f64, y: f64) -> Self {
204 self.coordinates = Some((x, y));
205 self
206 }
207
208 pub fn with_monitor_id(mut self, id: u32) -> Self {
210 self.monitor_id = Some(id);
211 self
212 }
213
214 pub fn with_layout(mut self, layout: impl Into<String>) -> Self {
216 self.layout = Some(layout.into());
217 self
218 }
219
220 pub fn with_attempt(mut self, attempt: u32) -> Self {
222 self.attempt = attempt;
223 self
224 }
225
226 pub fn with_details(mut self, details: impl Into<String>) -> Self {
228 self.details = details.into();
229 self
230 }
231}
232
233impl Default for ErrorContext {
234 fn default() -> Self {
235 Self::new()
236 }
237}
238
239#[derive(Debug, Clone, PartialEq, Eq)]
241pub enum RecoveryAction {
242 Retry(RetryConfig),
244
245 UseFallbackMapping,
247
248 ClampCoordinates,
250
251 UseDefaultLayout,
253
254 Skip,
256
257 ResetState,
259
260 RequestNewSession,
262
263 IncreaseQueueSize,
265
266 Fail,
268}
269
270#[derive(Debug, Clone, PartialEq, Eq)]
272pub struct RetryConfig {
273 pub max_retries: u32,
275
276 pub initial_delay_ms: u64,
278
279 pub backoff_multiplier: u32,
281
282 pub max_delay_ms: u64,
284}
285
286impl Default for RetryConfig {
287 fn default() -> Self {
288 Self {
289 max_retries: 3,
290 initial_delay_ms: 10,
291 backoff_multiplier: 2,
292 max_delay_ms: 1000,
293 }
294 }
295}
296
297impl RetryConfig {
298 pub fn delay_for_attempt(&self, attempt: u32) -> std::time::Duration {
300 let delay = self.initial_delay_ms * (self.backoff_multiplier as u64).pow(attempt);
301 let delay = delay.min(self.max_delay_ms);
302 std::time::Duration::from_millis(delay)
303 }
304}
305
306pub fn recovery_action(error: &InputError, context: &ErrorContext) -> RecoveryAction {
308 match classify_error(error) {
309 ErrorType::Portal => {
310 if context.attempt < 2 {
311 RecoveryAction::Retry(RetryConfig::default())
312 } else {
313 RecoveryAction::RequestNewSession
314 }
315 }
316
317 ErrorType::Translation => {
318 if context.attempt == 0 {
319 RecoveryAction::UseFallbackMapping
320 } else {
321 RecoveryAction::Skip
322 }
323 }
324
325 ErrorType::Coordinate => match error {
326 InputError::InvalidCoordinate(_, _) => RecoveryAction::ClampCoordinates,
327 InputError::MonitorNotFound(_) => RecoveryAction::ClampCoordinates,
328 _ => RecoveryAction::Skip,
329 },
330
331 ErrorType::Layout => {
332 if context.attempt == 0 {
333 RecoveryAction::UseDefaultLayout
334 } else {
335 RecoveryAction::Skip
336 }
337 }
338
339 ErrorType::EventQueue => match error {
340 InputError::EventQueueFull => RecoveryAction::IncreaseQueueSize,
341 _ => {
342 if context.attempt < 2 {
343 RecoveryAction::Retry(RetryConfig {
344 max_retries: 2,
345 initial_delay_ms: 5,
346 backoff_multiplier: 2,
347 max_delay_ms: 100,
348 })
349 } else {
350 RecoveryAction::Fail
351 }
352 }
353 },
354
355 ErrorType::Performance => RecoveryAction::Skip,
356
357 ErrorType::State => RecoveryAction::ResetState,
358
359 ErrorType::Unknown => RecoveryAction::Fail,
360 }
361}
362
363#[cfg(test)]
364mod tests {
365 use super::*;
366
367 #[test]
368 fn test_error_classification() {
369 let error = InputError::PortalError("test".to_string());
370 assert_eq!(classify_error(&error), ErrorType::Portal);
371
372 let error = InputError::UnknownScancode(0x1234);
373 assert_eq!(classify_error(&error), ErrorType::Translation);
374
375 let error = InputError::InvalidCoordinate(100.0, 200.0);
376 assert_eq!(classify_error(&error), ErrorType::Coordinate);
377
378 let error = InputError::LayoutError("test".to_string());
379 assert_eq!(classify_error(&error), ErrorType::Layout);
380
381 let error = InputError::EventQueueFull;
382 assert_eq!(classify_error(&error), ErrorType::EventQueue);
383
384 let error = InputError::LatencyTooHigh(100, 20);
385 assert_eq!(classify_error(&error), ErrorType::Performance);
386 }
387
388 #[test]
389 fn test_error_context() {
390 let ctx = ErrorContext::new()
391 .with_scancode(0x1E)
392 .with_keycode(30)
393 .with_coordinates(100.0, 200.0)
394 .with_monitor_id(1)
395 .with_layout("us")
396 .with_attempt(2)
397 .with_details("test error");
398
399 assert_eq!(ctx.scancode, Some(0x1E));
400 assert_eq!(ctx.keycode, Some(30));
401 assert_eq!(ctx.coordinates, Some((100.0, 200.0)));
402 assert_eq!(ctx.monitor_id, Some(1));
403 assert_eq!(ctx.layout, Some("us".to_string()));
404 assert_eq!(ctx.attempt, 2);
405 assert_eq!(ctx.details, "test error");
406 }
407
408 #[test]
409 fn test_retry_config() {
410 let config = RetryConfig::default();
411
412 assert_eq!(config.delay_for_attempt(0).as_millis(), 10);
413 assert_eq!(config.delay_for_attempt(1).as_millis(), 20);
414 assert_eq!(config.delay_for_attempt(2).as_millis(), 40);
415
416 assert_eq!(config.delay_for_attempt(10).as_millis(), 1000);
418 }
419
420 #[test]
421 fn test_recovery_action_portal_error() {
422 let error = InputError::PortalError("test".to_string());
423 let ctx = ErrorContext::new().with_attempt(0);
424
425 match recovery_action(&error, &ctx) {
426 RecoveryAction::Retry(_) => {}
427 _ => panic!("Expected Retry action"),
428 }
429
430 let ctx = ErrorContext::new().with_attempt(3);
431 match recovery_action(&error, &ctx) {
432 RecoveryAction::RequestNewSession => {}
433 _ => panic!("Expected RequestNewSession action"),
434 }
435 }
436
437 #[test]
438 fn test_recovery_action_translation_error() {
439 let error = InputError::UnknownScancode(0x1234);
440 let ctx = ErrorContext::new().with_attempt(0);
441
442 match recovery_action(&error, &ctx) {
443 RecoveryAction::UseFallbackMapping => {}
444 _ => panic!("Expected UseFallbackMapping action"),
445 }
446 }
447
448 #[test]
449 fn test_recovery_action_coordinate_error() {
450 let error = InputError::InvalidCoordinate(100.0, 200.0);
451 let ctx = ErrorContext::new();
452
453 match recovery_action(&error, &ctx) {
454 RecoveryAction::ClampCoordinates => {}
455 _ => panic!("Expected ClampCoordinates action"),
456 }
457 }
458
459 #[test]
460 fn test_recovery_action_queue_full() {
461 let error = InputError::EventQueueFull;
462 let ctx = ErrorContext::new();
463
464 match recovery_action(&error, &ctx) {
465 RecoveryAction::IncreaseQueueSize => {}
466 _ => panic!("Expected IncreaseQueueSize action"),
467 }
468 }
469}