Skip to content

Commit 03071a5

Browse files
committed
Add BLE reconnect detection, code review cleanup
IOHIDManager watcher detects Logitech HID device arrival/removal via CFRunLoop, triggering reconnect to re-divert buttons after BLE drops. Keepalive checks diversion state every 5 minutes as a safety net for silent BLE reconnects where the HID handle stays valid but firmware state resets. Code review fixes: - Extract connect_device(), execute_and_notify(), format_hex(), classify_error(), action_description() to eliminate duplication - ActionOutcome enum replaces ambiguous bool return from execute() - Move SAMPLE_CONFIG to config.rs, add ACCESSIBILITY_ERROR const - Fix run_listen_only with proper backoff and error deduplication - Deduplicate FFI declarations in platform.rs, fix c-string literals - Collapse permission error handling in main.rs - Fix service.rs plist label to match bundle ID (com.jlevere.hidpp) - Fix RESPONSE_TIMEOUT_MS type from i32 to u64
1 parent ed24ef6 commit 03071a5

7 files changed

Lines changed: 523 additions & 287 deletions

File tree

crates/hidpp-daemon/src/action.rs

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,17 @@ use tracing::{error, info, warn};
55

66
use crate::config::{Action, ExplicitAction};
77

8+
/// Outcome of executing an action.
9+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10+
pub enum ActionOutcome {
11+
/// Action executed successfully.
12+
Executed,
13+
/// Accessibility permission not granted — caller should show error.
14+
PermissionDenied,
15+
/// Action failed (bad keystroke, key error, etc.) — already logged.
16+
Failed,
17+
}
18+
819
/// Global enigo instance. Initialized lazily on first use.
920
static ENIGO: Mutex<Option<Enigo>> = Mutex::new(None);
1021

@@ -51,56 +62,56 @@ pub fn retry_init() {
5162
}
5263

5364
/// Execute an action. Initializes enigo lazily if needed.
54-
/// Returns false if Accessibility permission is missing.
55-
pub fn execute(action: &Action) -> bool {
65+
pub fn execute(action: &Action) -> ActionOutcome {
5666
match action {
5767
Action::Keystroke(keys) => execute_keystroke(keys),
5868
Action::Explicit(ExplicitAction::Keystroke { keys }) => execute_keystroke(keys),
5969
Action::Explicit(ExplicitAction::Command { run }) => {
6070
execute_command(run);
61-
true
71+
ActionOutcome::Executed
6272
}
6373
}
6474
}
6575

6676
/// Parse and execute a keystroke string like "ctrl+shift+left".
67-
/// Returns false if Accessibility permission is missing.
68-
fn execute_keystroke(keystroke: &str) -> bool {
77+
fn execute_keystroke(keystroke: &str) -> ActionOutcome {
6978
let parts: Vec<&str> = keystroke.split('+').map(str::trim).collect();
7079
if parts.is_empty() {
71-
return true;
80+
return ActionOutcome::Failed;
7281
}
7382

7483
let (modifier_strs, main_str) = parts.split_at(parts.len() - 1);
7584
let modifiers: Vec<Key> = modifier_strs.iter().filter_map(|s| parse_key(s)).collect();
7685

7786
let Some(main_key) = main_str.first().and_then(|s| parse_key(s)) else {
7887
error!("unknown key in keystroke: {keystroke}");
79-
return true;
88+
return ActionOutcome::Failed;
8089
};
8190

8291
if !ensure_init() {
83-
error!(
84-
"grant Accessibility permission: System Settings → Privacy & Security → Accessibility"
85-
);
86-
return false;
92+
return ActionOutcome::PermissionDenied;
8793
}
8894

8995
let mut guard = ENIGO.lock().unwrap();
9096
let Some(enigo) = guard.as_mut() else {
91-
return true;
97+
return ActionOutcome::Failed;
9298
};
9399

94100
// Press modifiers, click main key, release modifiers in reverse.
95101
for m in &modifiers {
96102
if let Err(e) = enigo.key(*m, Direction::Press) {
97103
error!("key press failed: {e}");
98-
return true;
104+
return ActionOutcome::Failed;
99105
}
100106
}
101107

102108
if let Err(e) = enigo.key(main_key, Direction::Click) {
103109
error!("key click failed: {e}");
110+
// Still release pressed modifiers before returning.
111+
for m in modifiers.iter().rev() {
112+
let _ = enigo.key(*m, Direction::Release);
113+
}
114+
return ActionOutcome::Failed;
104115
}
105116

106117
for m in modifiers.iter().rev() {
@@ -109,14 +120,11 @@ fn execute_keystroke(keystroke: &str) -> bool {
109120
}
110121
}
111122

112-
info!("keystroke: {keystroke}");
113-
true
123+
ActionOutcome::Executed
114124
}
115125

116126
/// Run a shell command in the background. Reaps the child on a separate thread.
117127
fn execute_command(cmd: &str) {
118-
info!("command: {cmd}");
119-
120128
#[cfg(unix)]
121129
let result = std::process::Command::new("sh").args(["-c", cmd]).spawn();
122130

crates/hidpp-daemon/src/config.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,49 @@ use std::path::{Path, PathBuf};
33

44
use serde::Deserialize;
55

6+
pub const SAMPLE_CONFIG: &str = r#"# hidppd config — maps diverted buttons and gestures to actions.
7+
#
8+
# Button CIDs for MX Master 3S:
9+
# 82 = Middle Click
10+
# 83 = Back
11+
# 86 = Forward
12+
# 195 = Gesture Button (thumb)
13+
# 196 = Mode Shift (scroll wheel click)
14+
#
15+
# Keystroke format: "modifier+modifier+key"
16+
# Modifiers: ctrl, alt, shift, cmd (or meta/super/win)
17+
# Keys: a-z, 0-9, f1-f20, left/right/up/down, tab, return,
18+
# space, escape, home, end, pageup, pagedown, delete,
19+
# playpause, next, prev, volumeup, volumedown, mute
20+
#
21+
# Simple button → keystroke:
22+
# [buttons]
23+
# 83 = "alt+left" # Back button → browser back
24+
# 86 = "alt+right" # Forward → browser forward
25+
#
26+
# Gesture button — hold + swipe for directional actions:
27+
# [gestures.195]
28+
# up = "ctrl+up" # Swipe up → Mission Control
29+
# down = "ctrl+down" # Swipe down → App Exposé
30+
# left = "ctrl+left" # Swipe left → prev desktop
31+
# right = "ctrl+right" # Swipe right → next desktop
32+
# tap = "playpause" # Quick tap → play/pause
33+
# threshold = 50 # Min displacement (default: 50)
34+
#
35+
# Command actions:
36+
# 83 = { type = "command", run = "open -a Safari" }
37+
38+
[buttons]
39+
83 = "alt+left"
40+
86 = "alt+right"
41+
42+
[gestures.195]
43+
up = "ctrl+up"
44+
down = "ctrl+down"
45+
left = "ctrl+left"
46+
right = "ctrl+right"
47+
"#;
48+
649
/// Raw config as deserialized from TOML (string keys).
750
#[derive(Debug, Deserialize, Default)]
851
struct RawConfig {

0 commit comments

Comments
 (0)