use thiserror::Error;
pub type Result<T> = std::result::Result<T, InputError>;
#[derive(Error, Debug)]
pub enum InputError {
#[error("Portal remote desktop error: {0}")]
PortalError(String),
#[error("Scancode translation failed: {0}")]
ScancodeTranslationFailed(String),
#[error("Unknown scancode: 0x{0:04X}")]
UnknownScancode(u16),
#[error("Unknown keycode: {0}")]
UnknownKeycode(u32),
#[error("Coordinate transformation error: {0}")]
CoordinateTransformError(String),
#[error("Monitor not found: {0}")]
MonitorNotFound(u32),
#[error("Invalid coordinate: ({0}, {1})")]
InvalidCoordinate(f64, f64),
#[error("Invalid monitor configuration: {0}")]
InvalidMonitorConfig(String),
#[error("Keyboard layout error: {0}")]
LayoutError(String),
#[error("Layout not found: {0}")]
LayoutNotFound(String),
#[error("XKB error: {0}")]
XkbError(String),
#[error("Event queue is full")]
EventQueueFull,
#[error("Failed to send event")]
EventSendFailed,
#[error("Failed to receive event")]
EventReceiveFailed,
#[error("Input latency too high: {0}ms (max: {1}ms)")]
LatencyTooHigh(u64, u64),
#[error("Invalid state: {0}")]
InvalidState(String),
#[error("Portal session error: {0}")]
PortalSessionError(String),
#[error("DBus error: {0}")]
DBusError(String),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Invalid key event: {0}")]
InvalidKeyEvent(String),
#[error("Invalid mouse event: {0}")]
InvalidMouseEvent(String),
#[error("Unknown error: {0}")]
Unknown(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ErrorType {
Portal,
Translation,
Coordinate,
Layout,
EventQueue,
Performance,
State,
Unknown,
}
pub fn classify_error(error: &InputError) -> ErrorType {
match error {
InputError::PortalError(_) | InputError::PortalSessionError(_) | InputError::DBusError(_) => ErrorType::Portal,
InputError::ScancodeTranslationFailed(_) | InputError::UnknownScancode(_) | InputError::UnknownKeycode(_) => {
ErrorType::Translation
}
InputError::CoordinateTransformError(_)
| InputError::MonitorNotFound(_)
| InputError::InvalidCoordinate(_, _)
| InputError::InvalidMonitorConfig(_) => ErrorType::Coordinate,
InputError::LayoutError(_) | InputError::LayoutNotFound(_) | InputError::XkbError(_) => ErrorType::Layout,
InputError::EventQueueFull | InputError::EventSendFailed | InputError::EventReceiveFailed => {
ErrorType::EventQueue
}
InputError::LatencyTooHigh(_, _) => ErrorType::Performance,
InputError::InvalidState(_) => ErrorType::State,
_ => ErrorType::Unknown,
}
}
#[derive(Debug, Clone)]
pub struct ErrorContext {
pub scancode: Option<u16>,
pub keycode: Option<u32>,
pub coordinates: Option<(f64, f64)>,
pub monitor_id: Option<u32>,
pub layout: Option<String>,
pub attempt: u32,
pub details: String,
}
impl ErrorContext {
pub fn new() -> Self {
Self {
scancode: None,
keycode: None,
coordinates: None,
monitor_id: None,
layout: None,
attempt: 0,
details: String::new(),
}
}
pub fn with_scancode(mut self, scancode: u16) -> Self {
self.scancode = Some(scancode);
self
}
pub fn with_keycode(mut self, keycode: u32) -> Self {
self.keycode = Some(keycode);
self
}
pub fn with_coordinates(mut self, x: f64, y: f64) -> Self {
self.coordinates = Some((x, y));
self
}
pub fn with_monitor_id(mut self, id: u32) -> Self {
self.monitor_id = Some(id);
self
}
pub fn with_layout(mut self, layout: impl Into<String>) -> Self {
self.layout = Some(layout.into());
self
}
pub fn with_attempt(mut self, attempt: u32) -> Self {
self.attempt = attempt;
self
}
pub fn with_details(mut self, details: impl Into<String>) -> Self {
self.details = details.into();
self
}
}
impl Default for ErrorContext {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RecoveryAction {
Retry(RetryConfig),
UseFallbackMapping,
ClampCoordinates,
UseDefaultLayout,
Skip,
ResetState,
RequestNewSession,
IncreaseQueueSize,
Fail,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RetryConfig {
pub max_retries: u32,
pub initial_delay_ms: u64,
pub backoff_multiplier: u32,
pub max_delay_ms: u64,
}
impl Default for RetryConfig {
fn default() -> Self {
Self {
max_retries: 3,
initial_delay_ms: 10,
backoff_multiplier: 2,
max_delay_ms: 1000,
}
}
}
impl RetryConfig {
pub fn delay_for_attempt(&self, attempt: u32) -> std::time::Duration {
let delay = self.initial_delay_ms * (self.backoff_multiplier as u64).pow(attempt);
let delay = delay.min(self.max_delay_ms);
std::time::Duration::from_millis(delay)
}
}
pub fn recovery_action(error: &InputError, context: &ErrorContext) -> RecoveryAction {
match classify_error(error) {
ErrorType::Portal => {
if context.attempt < 2 {
RecoveryAction::Retry(RetryConfig::default())
} else {
RecoveryAction::RequestNewSession
}
}
ErrorType::Translation => {
if context.attempt == 0 {
RecoveryAction::UseFallbackMapping
} else {
RecoveryAction::Skip
}
}
ErrorType::Coordinate => match error {
InputError::InvalidCoordinate(_, _) => RecoveryAction::ClampCoordinates,
InputError::MonitorNotFound(_) => RecoveryAction::ClampCoordinates,
_ => RecoveryAction::Skip,
},
ErrorType::Layout => {
if context.attempt == 0 {
RecoveryAction::UseDefaultLayout
} else {
RecoveryAction::Skip
}
}
ErrorType::EventQueue => match error {
InputError::EventQueueFull => RecoveryAction::IncreaseQueueSize,
_ => {
if context.attempt < 2 {
RecoveryAction::Retry(RetryConfig {
max_retries: 2,
initial_delay_ms: 5,
backoff_multiplier: 2,
max_delay_ms: 100,
})
} else {
RecoveryAction::Fail
}
}
},
ErrorType::Performance => RecoveryAction::Skip,
ErrorType::State => RecoveryAction::ResetState,
ErrorType::Unknown => RecoveryAction::Fail,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_classification() {
let error = InputError::PortalError("test".to_string());
assert_eq!(classify_error(&error), ErrorType::Portal);
let error = InputError::UnknownScancode(0x1234);
assert_eq!(classify_error(&error), ErrorType::Translation);
let error = InputError::InvalidCoordinate(100.0, 200.0);
assert_eq!(classify_error(&error), ErrorType::Coordinate);
let error = InputError::LayoutError("test".to_string());
assert_eq!(classify_error(&error), ErrorType::Layout);
let error = InputError::EventQueueFull;
assert_eq!(classify_error(&error), ErrorType::EventQueue);
let error = InputError::LatencyTooHigh(100, 20);
assert_eq!(classify_error(&error), ErrorType::Performance);
}
#[test]
fn test_error_context() {
let ctx = ErrorContext::new()
.with_scancode(0x1E)
.with_keycode(30)
.with_coordinates(100.0, 200.0)
.with_monitor_id(1)
.with_layout("us")
.with_attempt(2)
.with_details("test error");
assert_eq!(ctx.scancode, Some(0x1E));
assert_eq!(ctx.keycode, Some(30));
assert_eq!(ctx.coordinates, Some((100.0, 200.0)));
assert_eq!(ctx.monitor_id, Some(1));
assert_eq!(ctx.layout, Some("us".to_string()));
assert_eq!(ctx.attempt, 2);
assert_eq!(ctx.details, "test error");
}
#[test]
fn test_retry_config() {
let config = RetryConfig::default();
assert_eq!(config.delay_for_attempt(0).as_millis(), 10);
assert_eq!(config.delay_for_attempt(1).as_millis(), 20);
assert_eq!(config.delay_for_attempt(2).as_millis(), 40);
assert_eq!(config.delay_for_attempt(10).as_millis(), 1000);
}
#[test]
fn test_recovery_action_portal_error() {
let error = InputError::PortalError("test".to_string());
let ctx = ErrorContext::new().with_attempt(0);
match recovery_action(&error, &ctx) {
RecoveryAction::Retry(_) => {}
_ => panic!("Expected Retry action"),
}
let ctx = ErrorContext::new().with_attempt(3);
match recovery_action(&error, &ctx) {
RecoveryAction::RequestNewSession => {}
_ => panic!("Expected RequestNewSession action"),
}
}
#[test]
fn test_recovery_action_translation_error() {
let error = InputError::UnknownScancode(0x1234);
let ctx = ErrorContext::new().with_attempt(0);
match recovery_action(&error, &ctx) {
RecoveryAction::UseFallbackMapping => {}
_ => panic!("Expected UseFallbackMapping action"),
}
}
#[test]
fn test_recovery_action_coordinate_error() {
let error = InputError::InvalidCoordinate(100.0, 200.0);
let ctx = ErrorContext::new();
match recovery_action(&error, &ctx) {
RecoveryAction::ClampCoordinates => {}
_ => panic!("Expected ClampCoordinates action"),
}
}
#[test]
fn test_recovery_action_queue_full() {
let error = InputError::EventQueueFull;
let ctx = ErrorContext::new();
match recovery_action(&error, &ctx) {
RecoveryAction::IncreaseQueueSize => {}
_ => panic!("Expected IncreaseQueueSize action"),
}
}
}