← KD100

Add configurable wheel mode: sequential vs sets-based navigation

4743d04c83cae0c8f7c205f1922aebfd6e9a8adb · Claude · Jan 22, 2026 19:42

Add wheel_mode configuration option to allow users to choose between
classic sequential cycling and new multi-click set-based navigation.

Configuration:
- wheel_mode: sequential | sets
- Default: sequential (maintains backward compatibility)

Sequential Mode (Legacy):
- Single-click cycles through all wheel functions sequentially
- Example: 0 → 1 → 2 → 3 → 4 → 5 → 0...

Sets Mode (New):
- Single-click: Toggle within current set (functions 0↔1, 2↔3, 4↔5)
- Double-click: Switch between Set 1 ↔ Set 2
- Triple-click: Toggle to/from Set 3
- Supports up to 6 wheel functions in 3 sets of 2

Set Navigation Mapping:
- Set 1 → double-click → Set 2
- Set 1 → triple-click → Set 3
- Set 2 → double-click → Set 1
- Set 3 → triple-click → Set 1
- Set 3 → double-click → Set 1

Implementation:
- Add wheel_mode_t enum to config.h
- Add parse/print functions for wheel_mode in config.c
- Conditionally execute sequential or sets logic in device.c
- Update default.cfg with comprehensive documentation

Files modified:
- src/config.h: Add wheel_mode_t enum and field
- src/config.c: Add parsing, initialization, and debug printing
- src/device.c: Add mode-conditional logic for wheel toggle
- default.cfg: Add wheel_mode config with detailed documentation

+154 −81 · 4 files changed

  • default.cfg +30 −11
  • src/config.c +31 −0
  • src/config.h +7 −0
  • src/device.c +86 −70
diff --git a/default.cfg b/default.cfg
index 0016120..b105a95 100644
--- a/default.cfg
+++ b/default.cfg
@@ -76,21 +76,40 @@ leader_function: shift
 leader_timeout: 1000
 leader_mode: toggle
 
+//
+//     Wheel Toggle Mode Configuration
+//     wheel_mode: Choose how button 18 (wheel toggle) cycles through wheel functions
+//     Options:   sequential | sets
+//     Default:   sequential
+//
+//     sequential: Classic behavior - single-click cycles through all functions sequentially
+//                 Example: Click → Function 0 → Click → Function 1 → Click → Function 2 → etc.
+//
+//     sets:       Set-based navigation using multi-click detection
+//                 - Single-click:  Toggle within current set (e.g., function 0 ↔ 1)
+//                 - Double-click:  Switch between Set 1 ↔ Set 2
+//                 - Triple-click:  Toggle to/from Set 3
+//
+//                 Set Navigation:
+//                   • Set 1 → double-click → Set 2
+//                   • Set 1 → triple-click → Set 3
+//                   • Set 2 → double-click → Set 1
+//                   • Set 3 → triple-click → Set 1
+//                   • Set 3 → double-click → Set 1
+//
+//                 With 6 wheel functions defined:
+//                   Set 1: Wheel functions 0-1 (e.g., brush size ↔ opacity)
+//                   Set 2: Wheel functions 2-3 (e.g., flow ↔ rotation)
+//                   Set 3: Wheel functions 4-5 (e.g., scatter ↔ spacing)
+//
+wheel_mode: sequential
+
 //
 //     Wheel Click Timeout Configuration
-//     wheel_click_timeout: Time in milliseconds for multi-click detection on button 18 (wheel toggle)
+//     wheel_click_timeout: Time in milliseconds for multi-click detection on button 18
 //     Range:     20-990 ms (hard limit enforced)
 //     Default:   300 ms
-//
-//     Multi-click behavior for button 18 (wheel toggle):
-//     - Single-click:  Toggle within current set (e.g., function 0 ↔ 1)
-//     - Double-click:  Switch between Set 1 (functions 0-1) ↔ Set 2 (functions 2-3)
-//     - Triple-click:  Toggle to/from Set 3 (functions 4-5)
-//
-//     Example: With 6 wheel functions defined:
-//       Set 1: Wheel functions 0-1 (e.g., brush size ↔ opacity)
-//       Set 2: Wheel functions 2-3 (e.g., flow ↔ rotation)
-//       Set 3: Wheel functions 4-5 (e.g., scatter ↔ spacing)
+//     Note:      Only applies when wheel_mode is set to "sets"
 //
 wheel_click_timeout: 300
 
diff --git a/src/config.c b/src/config.c
index 4a5bf0e..8c06d10 100644
--- a/src/config.c
+++ b/src/config.c
@@ -6,6 +6,26 @@
 #include <pwd.h>
 #include <unistd.h>
 
+// Helper function to convert wheel mode enum to string
+static const char* wheel_mode_to_string(wheel_mode_t mode) {
+    switch (mode) {
+        case WHEEL_MODE_SEQUENTIAL: return "sequential";
+        case WHEEL_MODE_SETS: return "sets";
+        default: return "unknown";
+    }
+}
+
+// Helper function to parse wheel mode string to enum
+static wheel_mode_t parse_wheel_mode(const char* mode_str) {
+    if (strcasecmp(mode_str, "sequential") == 0) {
+        return WHEEL_MODE_SEQUENTIAL;
+    } else if (strcasecmp(mode_str, "sets") == 0) {
+        return WHEEL_MODE_SETS;
+    }
+    // Default to sequential for backward compatibility
+    return WHEEL_MODE_SEQUENTIAL;
+}
+
 // Create a new configuration structure
 config_t* config_create(void) {
     config_t* config = malloc(sizeof(config_t));
@@ -32,6 +52,7 @@ config_t* config_create(void) {
     config->totalWheels = 0;
     config->enable_uclogic = 0;
     config->wheel_click_timeout_ms = 300;  // 300ms default timeout
+    config->wheel_mode = WHEEL_MODE_SEQUENTIAL;  // Default to sequential (legacy behavior)
 
     // Initialize leader state
     config->leader.leader_button = -1;
@@ -153,6 +174,15 @@ int config_load(config_t* config, const char* filename, int debug) {
             continue;
         }
 
+        // Parse wheel_mode
+        if (strncasecmp(line, "wheel_mode:", 11) == 0) {
+            char* value = line + 11;
+            while (*value == ' ') value++;
+            config->wheel_mode = parse_wheel_mode(value);
+            if (debug) printf("Config: wheel_mode = %s\n", wheel_mode_to_string(config->wheel_mode));
+            continue;
+        }
+
         // Parse leader_button
         if (strncasecmp(line, "leader_button:", 14) == 0) {
             char* value = line + 14;
@@ -365,5 +395,6 @@ void config_print(const config_t* config, int debug) {
 
     printf("\n=== Wheel Click Configuration ===\n");
     printf("Multi-click timeout: %d ms\n", config->wheel_click_timeout_ms);
+    printf("Wheel mode: %s\n", wheel_mode_to_string(config->wheel_mode));
     printf("\n");
 }
diff --git a/src/config.h b/src/config.h
index 5c0649b..91cc4f1 100644
--- a/src/config.h
+++ b/src/config.h
@@ -3,6 +3,12 @@
 
 #include "leader.h"
 
+// Wheel mode enumeration
+typedef enum {
+    WHEEL_MODE_SEQUENTIAL,  // Single-click cycles through all functions (default/legacy)
+    WHEEL_MODE_SETS         // Multi-click for set-based navigation
+} wheel_mode_t;
+
 // Wheel event structure
 typedef struct {
     char* right;
@@ -18,6 +24,7 @@ typedef struct {
     leader_state leader;
     int enable_uclogic;
     int wheel_click_timeout_ms;  // Multi-click detection timeout (20-990ms)
+    wheel_mode_t wheel_mode;     // Wheel toggle mode (sequential or sets)
 } config_t;
 
 // Configuration functions
diff --git a/src/device.c b/src/device.c
index 1a5d878..6ba266d 100644
--- a/src/device.c
+++ b/src/device.c
@@ -339,88 +339,104 @@ void device_run(libusb_context* ctx, config_t* config, int debug, int accept, in
                                         prevEvent.function = "";
                                     }
                                 } else if (strcmp(config->events[button_index].function, "swap") == 0) {
-                                    // Multi-click detection for button 18
-                                    struct timeval now;
-                                    gettimeofday(&now, NULL);
-
-                                    long time_since_last_click = 0;
-                                    if (last_button18_time.tv_sec != 0 || last_button18_time.tv_usec != 0) {
-                                        time_since_last_click =
-                                            (now.tv_sec - last_button18_time.tv_sec) * 1000 +
-                                            (now.tv_usec - last_button18_time.tv_usec) / 1000;
-                                    }
-
-                                    // 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++;
+                                    // Check wheel mode
+                                    if (config->wheel_mode == WHEEL_MODE_SEQUENTIAL) {
+                                        // Sequential mode: simple cycling through all functions
+                                        if (wheelFunction != config->totalWheels - 1) {
+                                            wheelFunction++;
+                                        } else {
+                                            wheelFunction = 0;
+                                        }
+                                        if (debug == 1) {
+                                            printf("Sequential mode - Wheel Function: %d\n", wheelFunction);
+                                            printf("Function: %s | %s\n",
+                                                   config->wheelEvents[wheelFunction].left ? config->wheelEvents[wheelFunction].left : "(null)",
+                                                   config->wheelEvents[wheelFunction].right ? config->wheelEvents[wheelFunction].right : "(null)");
+                                        }
                                     } else {
-                                        // This is a new click sequence
-                                        button18_click_count = 1;
-                                    }
+                                        // Sets mode: multi-click detection for set-based navigation
+                                        struct timeval now;
+                                        gettimeofday(&now, NULL);
+
+                                        long time_since_last_click = 0;
+                                        if (last_button18_time.tv_sec != 0 || last_button18_time.tv_usec != 0) {
+                                            time_since_last_click =
+                                                (now.tv_sec - last_button18_time.tv_sec) * 1000 +
+                                                (now.tv_usec - last_button18_time.tv_usec) / 1000;
+                                        }
 
-                                    last_button18_time = now;
+                                        // 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++;
+                                        } else {
+                                            // This is a new click sequence
+                                            button18_click_count = 1;
+                                        }
 
-                                    // Wait for timeout to process the click(s)
-                                    usleep(config->wheel_click_timeout_ms * 1000);
+                                        last_button18_time = now;
 
-                                    // 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;
+                                        // Wait for timeout to process the click(s)
+                                        usleep(config->wheel_click_timeout_ms * 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;
+                                        // 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;
 
-                                        if (debug == 1) {
-                                            printf("Button 18 clicks: %d\n", final_click_count);
-                                        }
+                                        // 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;
 
-                                        // 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;
+                                            if (debug == 1) {
+                                                printf("Sets mode - Button 18 clicks: %d\n", final_click_count);
                                             }
-                                            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;
+
+                                            // 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
                                             }
-                                            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;
+                                            // 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;
-                                        }
+                                            // 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)");
+                                            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)");
+                                            }
                                         }
                                     }
                                 } else if (strcmp(config->events[button_index].function, "mouse1") == 0 ||