← KD100

Fix multi-click detection: remove blocking sleep

6776d93e24b2cb92c3bf91a104f513b0f12ea0dd · Claude · Jan 22, 2026 20:09

The previous implementation used usleep() to wait for additional clicks,
which blocked the main USB event loop and prevented subsequent clicks
from being detected. This made double-click and triple-click impossible.

Solution:
- Move click processing to the top of the event loop (non-blocking)
- When button 18 is pressed, just record the click timestamp and increment counter
- Check for timeout at the start of each loop iteration
- Process accumulated clicks only after timeout expires

This allows the driver to continue receiving button presses while waiting
for the multi-click timeout, enabling proper double-click and triple-click
detection.

Before: Every click was processed as single-click because the event loop
        was blocked during the timeout period
After:  Clicks accumulate properly and are processed as multi-clicks
        after the timeout expires

Files modified:
- src/device.c: Refactor multi-click logic to be non-blocking

+75 −64 · 1 file changed

  • src/device.c +75 −64
diff --git a/src/device.c b/src/device.c
index bc5e7de..3016a8f 100644
--- a/src/device.c
+++ b/src/device.c
@@ -301,6 +301,73 @@ void device_run(libusb_context* ctx, config_t* config, int debug, int accept, in
                         break;
                     }
 
+                    // Check if we have pending button 18 clicks that need to be processed
+                    if (config->wheel_mode == WHEEL_MODE_SETS && button18_click_count > 0 &&
+                        (last_button18_time.tv_sec != 0 || last_button18_time.tv_usec != 0)) {
+                        struct timeval now;
+                        gettimeofday(&now, NULL);
+                        long time_since_last_click =
+                            (now.tv_sec - last_button18_time.tv_sec) * 1000 +
+                            (now.tv_usec - last_button18_time.tv_usec) / 1000;
+
+                        // If timeout has expired, process the accumulated clicks
+                        if (time_since_last_click >= config->wheel_click_timeout_ms) {
+                            int final_click_count = button18_click_count;
+                            button18_click_count = 0;
+                            last_button18_time.tv_sec = 0;
+                            last_button18_time.tv_usec = 0;
+
+                            if (debug == 1) {
+                                printf("Sets mode - Button 18 clicks: %d\n", final_click_count);
+                            }
+
+                            // Process based on click count
+                            if (final_click_count == 1) {
+                                // Single-click: toggle within current set
+                                wheel_position_in_set = 1 - wheel_position_in_set;
+                            } else if (final_click_count == 2) {
+                                // Double-click: toggle between Set 0 and Set 1
+                                if (wheel_current_set == 0) {
+                                    wheel_current_set = 1;
+                                } else if (wheel_current_set == 1) {
+                                    wheel_current_set = 0;
+                                } else {
+                                    // From Set 2, go to Set 0
+                                    wheel_current_set = 0;
+                                }
+                                wheel_position_in_set = 0;  // Start at first function in new set
+                            } else if (final_click_count >= 3) {
+                                // Triple-click: toggle to/from Set 2
+                                if (wheel_current_set == 2) {
+                                    // From Set 2, go back to Set 0
+                                    wheel_current_set = 0;
+                                } else {
+                                    // From Set 0 or 1, go to Set 2
+                                    wheel_current_set = 2;
+                                }
+                                wheel_position_in_set = 0;  // Start at first function in new set
+                            }
+
+                            // Calculate actual wheel function index
+                            wheelFunction = (wheel_current_set * 2) + wheel_position_in_set;
+
+                            // Ensure we don't go beyond available wheels
+                            if (wheelFunction >= config->totalWheels) {
+                                wheelFunction = 0;
+                                wheel_current_set = 0;
+                                wheel_position_in_set = 0;
+                            }
+
+                            if (debug == 1) {
+                                printf("Set: %d | Position: %d | Wheel Function: %d\n",
+                                       wheel_current_set, wheel_position_in_set, wheelFunction);
+                                printf("Function: %s | %s\n",
+                                       config->wheelEvents[wheelFunction].left ? config->wheelEvents[wheelFunction].left : "(null)",
+                                       config->wheelEvents[wheelFunction].right ? config->wheelEvents[wheelFunction].right : "(null)");
+                            }
+                        }
+                    }
+
                     // Convert data to keycodes
                     if (data[4] != 0)
                         keycode = data[4];
@@ -360,6 +427,7 @@ void device_run(libusb_context* ctx, config_t* config, int debug, int accept, in
                                         }
                                     } else {
                                         // Sets mode: multi-click detection for set-based navigation
+                                        // Just record the click - processing happens at the top of the event loop
                                         struct timeval now;
                                         gettimeofday(&now, NULL);
 
@@ -373,76 +441,19 @@ void device_run(libusb_context* ctx, config_t* config, int debug, int accept, in
                                         // If click is within timeout window, increment count
                                         if (time_since_last_click > 0 && time_since_last_click < config->wheel_click_timeout_ms) {
                                             button18_click_count++;
+                                            if (debug == 1) {
+                                                printf("Button 18 click recorded (count now: %d)\n", button18_click_count);
+                                            }
                                         } else {
                                             // This is a new click sequence
                                             button18_click_count = 1;
-                                        }
-
-                                        last_button18_time = now;
-
-                                        // Wait for timeout to process the click(s)
-                                        usleep(config->wheel_click_timeout_ms * 1000);
-
-                                        // Check if more clicks arrived during the wait
-                                        gettimeofday(&now, NULL);
-                                        time_since_last_click =
-                                            (now.tv_sec - last_button18_time.tv_sec) * 1000 +
-                                            (now.tv_usec - last_button18_time.tv_usec) / 1000;
-
-                                        // Only process if no more clicks arrived
-                                        if (time_since_last_click >= config->wheel_click_timeout_ms) {
-                                            int final_click_count = button18_click_count;
-                                            button18_click_count = 0;
-
-                                            if (debug == 1) {
-                                                printf("Sets mode - Button 18 clicks: %d\n", final_click_count);
-                                            }
-
-                                            // Process based on click count
-                                            if (final_click_count == 1) {
-                                                // Single-click: toggle within current set
-                                                wheel_position_in_set = 1 - wheel_position_in_set;
-                                            } else if (final_click_count == 2) {
-                                                // Double-click: toggle between Set 0 and Set 1
-                                                if (wheel_current_set == 0) {
-                                                    wheel_current_set = 1;
-                                                } else if (wheel_current_set == 1) {
-                                                    wheel_current_set = 0;
-                                                } else {
-                                                    // From Set 2, go to Set 0
-                                                    wheel_current_set = 0;
-                                                }
-                                                wheel_position_in_set = 0;  // Start at first function in new set
-                                            } else if (final_click_count >= 3) {
-                                                // Triple-click: toggle to/from Set 2
-                                                if (wheel_current_set == 2) {
-                                                    // From Set 2, go back to Set 0
-                                                    wheel_current_set = 0;
-                                                } else {
-                                                    // From Set 0 or 1, go to Set 2
-                                                    wheel_current_set = 2;
-                                                }
-                                                wheel_position_in_set = 0;  // Start at first function in new set
-                                            }
-
-                                            // Calculate actual wheel function index
-                                            wheelFunction = (wheel_current_set * 2) + wheel_position_in_set;
-
-                                            // Ensure we don't go beyond available wheels
-                                            if (wheelFunction >= config->totalWheels) {
-                                                wheelFunction = 0;
-                                                wheel_current_set = 0;
-                                                wheel_position_in_set = 0;
-                                            }
-
                                             if (debug == 1) {
-                                                printf("Set: %d | Position: %d | Wheel Function: %d\n",
-                                                       wheel_current_set, wheel_position_in_set, wheelFunction);
-                                                printf("Function: %s | %s\n",
-                                                       config->wheelEvents[wheelFunction].left ? config->wheelEvents[wheelFunction].left : "(null)",
-                                                       config->wheelEvents[wheelFunction].right ? config->wheelEvents[wheelFunction].right : "(null)");
+                                                printf("Button 18 new click sequence started\n");
                                             }
                                         }
+
+                                        last_button18_time = now;
+                                        // Don't process yet - wait for timeout (checked at top of loop)
                                     }
                                 } else if (strcmp(config->events[button_index].function, "mouse1") == 0 ||
                                            strcmp(config->events[button_index].function, "mouse2") == 0 ||