lamco_rdp_input/
error.rs

1//! Input Handling Error Types
2//!
3//! Comprehensive error handling for the input handling module.
4
5use thiserror::Error;
6
7/// Result type for input operations
8pub type Result<T> = std::result::Result<T, InputError>;
9
10/// Input module error types
11#[derive(Error, Debug)]
12pub enum InputError {
13    /// Portal remote desktop error
14    #[error("Portal remote desktop error: {0}")]
15    PortalError(String),
16
17    /// Scancode translation error
18    #[error("Scancode translation failed: {0}")]
19    ScancodeTranslationFailed(String),
20
21    /// Unknown scancode
22    #[error("Unknown scancode: 0x{0:04X}")]
23    UnknownScancode(u16),
24
25    /// Unknown keycode
26    #[error("Unknown keycode: {0}")]
27    UnknownKeycode(u32),
28
29    /// Coordinate transformation error
30    #[error("Coordinate transformation error: {0}")]
31    CoordinateTransformError(String),
32
33    /// Monitor not found
34    #[error("Monitor not found: {0}")]
35    MonitorNotFound(u32),
36
37    /// Invalid coordinate
38    #[error("Invalid coordinate: ({0}, {1})")]
39    InvalidCoordinate(f64, f64),
40
41    /// Invalid monitor configuration
42    #[error("Invalid monitor configuration: {0}")]
43    InvalidMonitorConfig(String),
44
45    /// Layout error
46    #[error("Keyboard layout error: {0}")]
47    LayoutError(String),
48
49    /// Layout not found
50    #[error("Layout not found: {0}")]
51    LayoutNotFound(String),
52
53    /// XKB error
54    #[error("XKB error: {0}")]
55    XkbError(String),
56
57    /// Event queue full
58    #[error("Event queue is full")]
59    EventQueueFull,
60
61    /// Event send error
62    #[error("Failed to send event")]
63    EventSendFailed,
64
65    /// Event receive error
66    #[error("Failed to receive event")]
67    EventReceiveFailed,
68
69    /// Input latency too high
70    #[error("Input latency too high: {0}ms (max: {1}ms)")]
71    LatencyTooHigh(u64, u64),
72
73    /// Invalid state
74    #[error("Invalid state: {0}")]
75    InvalidState(String),
76
77    /// Portal session error
78    #[error("Portal session error: {0}")]
79    PortalSessionError(String),
80
81    /// DBus error
82    #[error("DBus error: {0}")]
83    DBusError(String),
84
85    /// IO error
86    #[error("IO error: {0}")]
87    Io(#[from] std::io::Error),
88
89    /// Invalid key event
90    #[error("Invalid key event: {0}")]
91    InvalidKeyEvent(String),
92
93    /// Invalid mouse event
94    #[error("Invalid mouse event: {0}")]
95    InvalidMouseEvent(String),
96
97    /// Unknown error
98    #[error("Unknown error: {0}")]
99    Unknown(String),
100}
101
102/// Error classification for recovery strategies
103#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
104pub enum ErrorType {
105    /// Portal-related errors
106    Portal,
107    /// Translation errors
108    Translation,
109    /// Coordinate errors
110    Coordinate,
111    /// Layout errors
112    Layout,
113    /// Event queue errors
114    EventQueue,
115    /// Performance errors
116    Performance,
117    /// State errors
118    State,
119    /// Unknown error type
120    Unknown,
121}
122
123/// Classify error for recovery strategy selection
124pub 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/// Error context for recovery decisions
152#[derive(Debug, Clone)]
153pub struct ErrorContext {
154    /// Scancode if applicable
155    pub scancode: Option<u16>,
156
157    /// Keycode if applicable
158    pub keycode: Option<u32>,
159
160    /// Mouse coordinates if applicable
161    pub coordinates: Option<(f64, f64)>,
162
163    /// Monitor ID if applicable
164    pub monitor_id: Option<u32>,
165
166    /// Keyboard layout if applicable
167    pub layout: Option<String>,
168
169    /// Retry attempt number
170    pub attempt: u32,
171
172    /// Additional context information
173    pub details: String,
174}
175
176impl ErrorContext {
177    /// Create new error context
178    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    /// Set scancode
191    pub fn with_scancode(mut self, scancode: u16) -> Self {
192        self.scancode = Some(scancode);
193        self
194    }
195
196    /// Set keycode
197    pub fn with_keycode(mut self, keycode: u32) -> Self {
198        self.keycode = Some(keycode);
199        self
200    }
201
202    /// Set coordinates
203    pub fn with_coordinates(mut self, x: f64, y: f64) -> Self {
204        self.coordinates = Some((x, y));
205        self
206    }
207
208    /// Set monitor ID
209    pub fn with_monitor_id(mut self, id: u32) -> Self {
210        self.monitor_id = Some(id);
211        self
212    }
213
214    /// Set layout
215    pub fn with_layout(mut self, layout: impl Into<String>) -> Self {
216        self.layout = Some(layout.into());
217        self
218    }
219
220    /// Set attempt number
221    pub fn with_attempt(mut self, attempt: u32) -> Self {
222        self.attempt = attempt;
223        self
224    }
225
226    /// Set details
227    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/// Recovery action to take after error
240#[derive(Debug, Clone, PartialEq, Eq)]
241pub enum RecoveryAction {
242    /// Retry the operation
243    Retry(RetryConfig),
244
245    /// Use fallback scancode mapping
246    UseFallbackMapping,
247
248    /// Clamp coordinates to monitor bounds
249    ClampCoordinates,
250
251    /// Switch to default keyboard layout
252    UseDefaultLayout,
253
254    /// Skip this event
255    Skip,
256
257    /// Reset input state
258    ResetState,
259
260    /// Request new portal session
261    RequestNewSession,
262
263    /// Increase event queue size
264    IncreaseQueueSize,
265
266    /// Fail and propagate error
267    Fail,
268}
269
270/// Retry configuration
271#[derive(Debug, Clone, PartialEq, Eq)]
272pub struct RetryConfig {
273    /// Maximum number of retries
274    pub max_retries: u32,
275
276    /// Initial delay in milliseconds
277    pub initial_delay_ms: u64,
278
279    /// Backoff multiplier
280    pub backoff_multiplier: u32,
281
282    /// Maximum delay in milliseconds
283    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    /// Calculate delay for given attempt
299    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
306/// Determine recovery action for error
307pub 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        // Should cap at max_delay_ms
417        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}