← KD100

Enhance driver interface with wheel descriptions, button highlighting, and OSD feedback

d1715443616c756ac102cc9226343d7688843363 · Claude · Feb 7, 2026 11:47

- Add wheel_description fields to config for human-readable function names
  instead of relying on inline comments after wheel function definitions
- Add sanitize_description() for input validation (max 64 chars, printable
  ASCII only) applied to both key and wheel descriptions
- Remove inline comments from wheel function lines in default.cfg
- Add wheel state tracking to OSD (current set, position, mode, descriptions)
- Add active button highlighting (green flash on press) in expanded view
- Add leader key visual feedback (orange highlight for leader, purple tint
  for leader-eligible keys when leader is active)
- Add 3-command history section to expanded keyboard layout view
- Add wheel set indicator with numbered boxes (green = active set)
- Add wheel direction labels showing both functions in current set
- Add mode indicator line (Sets/Sequential + leader state) in both views
- Add wheel action aggregation to avoid repeating same action on wheel turn
- Integrate wheel state, leader state, and button highlights from device
  event loop into OSD for real-time feedback

https://claude.ai/code/session_019MPUMMqDBcEYr7qFBLzJd6

+672 −153 · 6 files changed

  • default.cfg +31 −15
  • src/config.c +81 −4
  • src/config.h +4 −0
  • src/device.c +73 −0
  • src/osd.c +449 −134
  • src/osd.h +34 −0
diff --git a/default.cfg b/default.cfg
index 6adcabc..9bf8d20 100644
--- a/default.cfg
+++ b/default.cfg
@@ -355,24 +355,40 @@ type: 1
 function: swap
 //
 // Clockwise Turn (6 functions for 3 sets of 2)
-// Set 1: Brush size (function 0) ↔ Opacity (function 1)
-// Set 2: Flow (function 2) ↔ Rotation (function 3)
-// Set 3: Scatter (function 4) ↔ Spacing (function 5)
+// Set 1: Brush size (function 0) and Opacity (function 1)
+// Set 2: Flow (function 2) and Rotation (function 3)
+// Set 3: Scatter (function 4) and Spacing (function 5)
 //
 Wheel
-function: bracketright        // Set 1, function 0 - Brush size increase
-function: o                   // Set 1, function 1 - Opacity increase
-function: shift+bracketright  // Set 2, function 2 - Flow increase (Krita: Shift+])
-function: ctrl+bracketright   // Set 2, function 3 - Rotation clockwise
-function: alt+bracketright    // Set 3, function 4 - Scatter increase
-function: super+bracketright  // Set 3, function 5 - Spacing increase
+function: bracketright
+function: o
+function: shift+bracketright
+function: ctrl+bracketright
+function: alt+bracketright
+function: super+bracketright
 //
 // Counter-Clockwise Turn (6 functions for 3 sets of 2)
 //
 Wheel
-function: bracketleft         // Set 1, function 0 - Brush size decrease
-function: i                   // Set 1, function 1 - Opacity decrease
-function: shift+bracketleft   // Set 2, function 2 - Flow decrease (Krita: Shift+[)
-function: ctrl+bracketleft    // Set 2, function 3 - Rotation counter-clockwise
-function: alt+bracketleft     // Set 3, function 4 - Scatter decrease
-function: super+bracketleft   // Set 3, function 5 - Spacing decrease
+function: bracketleft
+function: i
+function: shift+bracketleft
+function: ctrl+bracketleft
+function: alt+bracketleft
+function: super+bracketleft
+//
+//      ==========================================
+//      WHEEL FUNCTION DESCRIPTIONS
+//      ==========================================
+//
+//      Each wheel function index maps to a pair (clockwise + counter-clockwise).
+//      Descriptions provide human-readable names shown in the OSD.
+//      Format: wheel_description_<index>: <description>
+//      Maximum 64 characters, printable ASCII only.
+//
+wheel_description_0: Brush Size
+wheel_description_1: Opacity
+wheel_description_2: Flow
+wheel_description_3: Rotation
+wheel_description_4: Scatter
+wheel_description_5: Spacing
diff --git a/src/config.c b/src/config.c
index 8c2fae3..b35aaa5 100644
--- a/src/config.c
+++ b/src/config.c
@@ -45,6 +45,43 @@ static char* strip_inline_comment(char* str) {
     return str;
 }
 
+// Sanitize a description string: enforce max length, printable ASCII only
+static char* sanitize_description(const char* input) {
+    if (input == NULL) return NULL;
+
+    // Skip leading whitespace
+    while (*input == ' ' || *input == '\t') input++;
+
+    size_t len = strlen(input);
+    if (len > MAX_DESCRIPTION_LEN) {
+        len = MAX_DESCRIPTION_LEN;
+    }
+
+    char* result = malloc(len + 1);
+    if (result == NULL) return NULL;
+
+    size_t j = 0;
+    for (size_t i = 0; i < len && input[i] != '\0'; i++) {
+        // Only allow printable ASCII (space through tilde)
+        if (input[i] >= 0x20 && input[i] <= 0x7E) {
+            result[j++] = input[i];
+        }
+    }
+    result[j] = '\0';
+
+    // Trim trailing whitespace
+    while (j > 0 && (result[j-1] == ' ' || result[j-1] == '\t')) {
+        result[--j] = '\0';
+    }
+
+    if (j == 0) {
+        free(result);
+        return NULL;
+    }
+
+    return result;
+}
+
 // Create a new configuration structure
 config_t* config_create(void) {
     config_t* config = malloc(sizeof(config_t));
@@ -86,6 +123,7 @@ config_t* config_create(void) {
 
     config->wheelEvents[0].right = NULL;
     config->wheelEvents[0].left = NULL;
+    config->wheelEvents[0].description = NULL;
 
     // Initialize OSD settings
     config->osd.enabled = 0;
@@ -135,6 +173,9 @@ void config_destroy(config_t* config) {
         if (config->wheelEvents[i].left != NULL) {
             free(config->wheelEvents[i].left);
         }
+        if (config->wheelEvents[i].description != NULL) {
+            free(config->wheelEvents[i].description);
+        }
     }
     free(config->wheelEvents);
 
@@ -404,8 +445,41 @@ int config_load(config_t* config, const char* filename, int debug) {
                     char* value = colon + 1;
                     while (*value == ' ') value++;
                     if (config->key_descriptions[btn]) free(config->key_descriptions[btn]);
-                    config->key_descriptions[btn] = strdup(value);
-                    if (debug) printf("Config: description_%d = %s\n", btn, config->key_descriptions[btn]);
+                    config->key_descriptions[btn] = sanitize_description(value);
+                    if (debug) printf("Config: description_%d = %s\n", btn,
+                                      config->key_descriptions[btn] ? config->key_descriptions[btn] : "(empty)");
+                }
+            }
+            continue;
+        }
+
+        // Parse wheel descriptions (wheel_description_0, wheel_description_1, etc.)
+        if (strncasecmp(line, "wheel_description_", 18) == 0) {
+            char* num_str = line + 18;
+            char* colon = strchr(num_str, ':');
+            if (colon) {
+                *colon = '\0';
+                int idx = atoi(num_str);
+                if (idx >= 0 && idx < 32) {  // reasonable limit
+                    char* value = colon + 1;
+                    while (*value == ' ') value++;
+                    // Ensure we have enough wheel slots
+                    if (idx >= (rightWheels > leftWheels ? rightWheels : leftWheels)) {
+                        int needed = idx + 1;
+                        wheel* temp = realloc(config->wheelEvents, needed * sizeof(*config->wheelEvents));
+                        if (temp != NULL) {
+                            config->wheelEvents = temp;
+                            for (int j = (rightWheels > leftWheels ? rightWheels : leftWheels); j < needed; j++) {
+                                config->wheelEvents[j].right = NULL;
+                                config->wheelEvents[j].left = NULL;
+                                config->wheelEvents[j].description = NULL;
+                            }
+                        }
+                    }
+                    if (config->wheelEvents[idx].description) free(config->wheelEvents[idx].description);
+                    config->wheelEvents[idx].description = sanitize_description(value);
+                    if (debug) printf("Config: wheel_description_%d = %s\n", idx,
+                                      config->wheelEvents[idx].description ? config->wheelEvents[idx].description : "(empty)");
                 }
             }
             continue;
@@ -496,6 +570,7 @@ int config_load(config_t* config, const char* filename, int debug) {
                     config->wheelEvents = temp;
                     config->wheelEvents[rightWheels].right = func_copy;
                     config->wheelEvents[rightWheels].left = NULL;
+                    config->wheelEvents[rightWheels].description = NULL;
                 } else {
                     config->wheelEvents[0].right = func_copy;
                     config->wheelEvents[0].left = NULL;
@@ -518,6 +593,7 @@ int config_load(config_t* config, const char* filename, int debug) {
                     config->wheelEvents = temp;
                     config->wheelEvents[leftWheels].left = func_copy;
                     config->wheelEvents[leftWheels].right = NULL;
+                    config->wheelEvents[leftWheels].description = NULL;
                 }
                 leftWheels++;
             }
@@ -575,9 +651,10 @@ void config_print(const config_t* config, int debug) {
 
     printf("\n=== Wheel Configuration ===\n");
     for (int i = 0; i < config->totalWheels; i++) {
-        printf("Wheel %d: Right: %s | Left: %s\n", i,
+        printf("Wheel %d: Right: %s | Left: %s | Desc: %s\n", i,
                config->wheelEvents[i].right ? config->wheelEvents[i].right : "(null)",
-               config->wheelEvents[i].left ? config->wheelEvents[i].left : "(null)");
+               config->wheelEvents[i].left ? config->wheelEvents[i].left : "(null)",
+               config->wheelEvents[i].description ? config->wheelEvents[i].description : "(none)");
     }
 
     printf("\n=== Leader Configuration ===\n");
diff --git a/src/config.h b/src/config.h
index 528d28f..dd1cb75 100644
--- a/src/config.h
+++ b/src/config.h
@@ -9,10 +9,14 @@ typedef enum {
     WHEEL_MODE_SETS         // Multi-click for set-based navigation
 } wheel_mode_t;
 
+// Maximum length for description fields (prevents buffer overflow)
+#define MAX_DESCRIPTION_LEN 64
+
 // Wheel event structure
 typedef struct {
     char* right;
     char* left;
+    char* description;  // Human-readable name for this wheel function pair (e.g., "Brush Size")
 } wheel;
 
 // OSD configuration
diff --git a/src/device.c b/src/device.c
index 9577ff7..453527d 100644
--- a/src/device.c
+++ b/src/device.c
@@ -72,6 +72,21 @@ void device_run(libusb_context* ctx, config_t* config, int debug, int accept, in
                     }
                 }
 
+                // Load wheel descriptions from config
+                for (int i = 0; i < config->totalWheels && i < 32; i++) {
+                    if (config->wheelEvents[i].description) {
+                        osd_set_wheel_description(osd, i, config->wheelEvents[i].description);
+                    }
+                }
+
+                // Set initial wheel state
+                osd_set_wheel_state(osd, 0, 0, 0,
+                                     config->wheel_mode == WHEEL_MODE_SETS ? 1 : 0,
+                                     config->totalWheels);
+
+                // Set leader button info
+                osd_set_leader_state(osd, 0, config->leader.leader_button);
+
                 // Show OSD if start_visible is set
                 if (config->osd.start_visible) {
                     osd_show(osd);
@@ -473,6 +488,28 @@ void device_run(libusb_context* ctx, config_t* config, int debug, int accept, in
                                     printf("Function: (not defined - incomplete set)\n");
                                 }
                             }
+
+                            // Update OSD wheel state
+                            if (osd) {
+                                osd_set_wheel_state(osd, wheel_current_set, wheel_position_in_set,
+                                                     wheelFunction, 1, config->totalWheels);
+
+                                // Record set change as an action with description
+                                const char* set_desc = NULL;
+                                if (wheelFunction >= 0 && wheelFunction < config->totalWheels &&
+                                    config->wheelEvents[wheelFunction].description) {
+                                    set_desc = config->wheelEvents[wheelFunction].description;
+                                }
+                                char set_action[128];
+                                if (set_desc) {
+                                    snprintf(set_action, sizeof(set_action), "Set %d: %s",
+                                             wheel_current_set + 1, set_desc);
+                                } else {
+                                    snprintf(set_action, sizeof(set_action), "Set %d",
+                                             wheel_current_set + 1);
+                                }
+                                osd_record_action(osd, 18, set_action);
+                            }
                         }
                     }
 
@@ -497,11 +534,21 @@ void device_run(libusb_context* ctx, config_t* config, int debug, int accept, in
                         if (wheelFunction >= 0 && wheelFunction < config->totalWheels &&
                             config->wheelEvents[wheelFunction].right != NULL) {
                             Handler(config->wheelEvents[wheelFunction].right, -1, debug);
+                            // Record aggregated wheel action to OSD
+                            if (osd) {
+                                const char* desc = config->wheelEvents[wheelFunction].description;
+                                osd_record_wheel_action(osd, "increase", desc);
+                            }
                         }
                     } else if (keycode == 642) {
                         if (wheelFunction >= 0 && wheelFunction < config->totalWheels &&
                             config->wheelEvents[wheelFunction].left != NULL) {
                             Handler(config->wheelEvents[wheelFunction].left, -1, debug);
+                            // Record aggregated wheel action to OSD
+                            if (osd) {
+                                const char* desc = config->wheelEvents[wheelFunction].description;
+                                osd_record_wheel_action(osd, "decrease", desc);
+                            }
                         }
                     } else {
                         int button_index = find_button_index(keycode);
@@ -515,6 +562,11 @@ void device_run(libusb_context* ctx, config_t* config, int debug, int accept, in
                                 }
                             }
 
+                            // Set active button highlight on OSD
+                            if (osd) {
+                                osd_set_active_button(osd, button_index);
+                            }
+
                             // Record action to OSD
                             if (osd && active_config && button_index < active_config->totalButtons) {
                                 const char* action = active_config->events[button_index].function;
@@ -526,6 +578,14 @@ void device_run(libusb_context* ctx, config_t* config, int debug, int accept, in
                             // Process button press with leader system
                             process_leader_combination(&active_config->leader, active_config->events, button_index, debug);
 
+                            // Update leader state on OSD
+                            if (osd) {
+                                int leader_is_active = active_config->leader.leader_active ||
+                                                       active_config->leader.toggle_state;
+                                osd_set_leader_state(osd, leader_is_active,
+                                                      active_config->leader.leader_button);
+                            }
+
                             // Also handle legacy single-button events for compatibility
                             if (active_config->events[button_index].function != NULL) {
                                 if (strcmp(active_config->events[button_index].function, "NULL") == 0) {
@@ -549,6 +609,19 @@ void device_run(libusb_context* ctx, config_t* config, int debug, int accept, in
                                                    config->wheelEvents[wheelFunction].left ? config->wheelEvents[wheelFunction].left : "(null)",
                                                    config->wheelEvents[wheelFunction].right ? config->wheelEvents[wheelFunction].right : "(null)");
                                         }
+                                        // Update OSD
+                                        if (osd) {
+                                            osd_set_wheel_state(osd, 0, 0, wheelFunction, 0, config->totalWheels);
+                                            const char* desc = (wheelFunction >= 0 && wheelFunction < config->totalWheels) ?
+                                                                config->wheelEvents[wheelFunction].description : NULL;
+                                            char seq_action[128];
+                                            if (desc) {
+                                                snprintf(seq_action, sizeof(seq_action), "Swap to: %s", desc);
+                                            } else {
+                                                snprintf(seq_action, sizeof(seq_action), "Swap to: Fn %d", wheelFunction);
+                                            }
+                                            osd_record_action(osd, 18, seq_action);
+                                        }
                                     } else {
                                         // Sets mode: multi-click detection for set-based navigation
                                         // Just record the click - processing happens at the top of the event loop
diff --git a/src/osd.c b/src/osd.c
index 81c3b52..a4ec5a7 100644
--- a/src/osd.c
+++ b/src/osd.c
@@ -99,6 +99,27 @@ osd_state_t* osd_create(config_t* config) {
         osd->key_descriptions[i] = NULL;
     }
 
+    // Initialize active button state
+    osd->active_button = -1;
+    osd->active_button_time_ms = 0;
+
+    // Initialize leader state
+    osd->leader_active = 0;
+    osd->leader_button = -1;
+
+    // Initialize wheel state
+    osd->wheel.current_set = 0;
+    osd->wheel.position_in_set = 0;
+    osd->wheel.wheel_function = 0;
+    osd->wheel.wheel_mode = 0;
+    osd->wheel.total_wheels = 0;
+    osd->wheel.last_wheel_action = NULL;
+    osd->wheel.last_wheel_time_ms = 0;
+    osd->wheel.wheel_action_count = 0;
+    for (int i = 0; i < 32; i++) {
+        osd->wheel.descriptions[i] = NULL;
+    }
+
     return osd;
 }
 
@@ -117,6 +138,12 @@ void osd_destroy(osd_state_t* osd) {
         if (osd->key_descriptions[i]) free(osd->key_descriptions[i]);
     }
 
+    // Free wheel descriptions
+    for (int i = 0; i < 32; i++) {
+        if (osd->wheel.descriptions[i]) free(osd->wheel.descriptions[i]);
+    }
+    if (osd->wheel.last_wheel_action) free(osd->wheel.last_wheel_action);
+
     // Close X11 resources
     if (osd->display) {
         Display* dpy = (Display*)osd->display;
@@ -299,20 +326,25 @@ void osd_set_mode(osd_state_t* osd, osd_mode_t mode) {
 
     // Update window size based on mode (dynamically calculated from font size)
     if (mode == OSD_MODE_MINIMAL) {
-        // Minimal: title + "Recent Actions:" + 3 action lines + padding
-        osd->width = (int)(200 * scale);
-        osd->height = title_height + padding + line_height + (int)(5 * scale) + (3 * line_height) + padding;
+        // Minimal: title + mode indicator + wheel info + 3 action lines + padding
+        osd->width = (int)(250 * scale);
+        osd->height = title_height + padding + line_height + (int)(5 * scale)
+                     + line_height + (int)(5 * scale)  // wheel set + direction info
+                     + (3 * line_height) + padding;
     } else {
         // Expanded: calculate based on grid dimensions
         int key_width = (int)(85 * scale);
         int key_height = (int)(45 * scale);
         int grid_padding = (int)(5 * scale);
         int grid_width = 4 * key_width + 3 * grid_padding;
-        // Height: title + "Keyboard Layout:" + wheel + 4 rows + bottom row + padding
+        // Height: title + mode line + keyboard layout + grid + wheel info + recent actions + padding
         int wheel_height = key_height - (int)(10 * scale);
         int grid_height = wheel_height + 5 * (key_height + grid_padding) + grid_padding;
+        int wheel_info_height = 3 * line_height + (int)(10 * scale);  // set indicator + direction
+        int history_height = line_height + (int)(5 * scale) + 3 * line_height;  // header + 3 actions
         osd->width = grid_width + 2 * padding;
-        osd->height = title_height + padding + line_height + (int)(10 * scale) + grid_height + padding;
+        osd->height = title_height + padding + line_height + (int)(10 * scale)
+                     + grid_height + wheel_info_height + history_height + padding;
     }
 
     XResizeWindow(dpy, win, osd->width, osd->height);
@@ -344,6 +376,10 @@ void osd_record_action(osd_state_t* osd, int button_index, const char* action) {
     osd->recent_actions[slot].timestamp_ms = now;
     osd->last_action_time_ms = now;
 
+    // Track active button for highlighting
+    osd->active_button = button_index;
+    osd->active_button_time_ms = now;
+
     // Auto-show OSD if hidden and auto_show is enabled
     if (osd->mode == OSD_MODE_HIDDEN && osd->auto_show && osd->display != NULL) {
         osd_set_mode(osd, OSD_MODE_MINIMAL);
@@ -352,6 +388,153 @@ void osd_record_action(osd_state_t* osd, int button_index, const char* action) {
     }
 }
 
+// Helper: draw a single button key in the grid with optional highlighting
+static void draw_button_key(Display* dpy, Window win, GC gc, osd_state_t* osd,
+                             int btn, int x, int y, int w, int h, float scale) {
+    int text_offset_x = (int)(5 * scale);
+    int text_offset_y1 = (int)(15 * scale);
+    int text_offset_y2 = (int)(35 * scale);
+    unsigned long fg_color = ((unsigned long)255 << 24) | 0xFFFFFF;
+    unsigned long accent_color = ((unsigned long)255 << 24) | 0x4488FF;
+    unsigned long highlight_color = ((unsigned long)200 << 24) | 0x444444;
+    (void)accent_color; // used below conditionally
+
+    long now = osd_get_time_ms();
+    int is_active = (osd->active_button == btn && (now - osd->active_button_time_ms) < 500);
+    int is_leader = (osd->leader_active && btn == osd->leader_button);
+    int is_leader_modified = (osd->leader_active && btn != osd->leader_button &&
+                              osd->config && btn < osd->config->totalButtons &&
+                              osd->config->events[btn].leader_eligible == 1);
+
+    // Choose background color based on state
+    unsigned long bg;
+    if (is_active) {
+        bg = ((unsigned long)255 << 24) | 0x44AA44;  // Green for active press
+    } else if (is_leader) {
+        bg = ((unsigned long)255 << 24) | 0xAA6622;  // Orange for active leader
+    } else if (is_leader_modified) {
+        bg = ((unsigned long)220 << 24) | 0x3A3A5A;  // Subtle purple for leader-eligible
+    } else {
+        bg = highlight_color;
+    }
+
+    XSetForeground(dpy, gc, bg);
+    XFillRectangle(dpy, win, gc, x, y, w, h);
+
+    // Draw border
+    XSetForeground(dpy, gc, is_active ? (((unsigned long)255 << 24) | 0x88FF88) : fg_color);
+    XDrawRectangle(dpy, win, gc, x, y, w - 1, h - 1);
+
+    // Draw button number
+    XSetForeground(dpy, gc, fg_color);
+    char num[16];
+    snprintf(num, sizeof(num), "%d", btn);
+    XDrawString(dpy, win, gc, x + text_offset_x, y + text_offset_y1, num, strlen(num));
+
+    // Draw description or function
+    const char* desc = NULL;
+    if (osd->key_descriptions[btn]) {
+        desc = osd->key_descriptions[btn];
+    } else if (osd->config && btn < osd->config->totalButtons &&
+               osd->config->events[btn].function) {
+        desc = osd->config->events[btn].function;
+    }
+
+    if (desc && h >= (int)(40 * scale)) {
+        // Truncate if too long for the key width
+        int max_chars = (w - 2 * text_offset_x) / (osd->font_size / 2);
+        if (max_chars < 3) max_chars = 3;
+        char truncated[32];
+        if ((int)strlen(desc) > max_chars) {
+            int copy_len = max_chars - 2;
+            if (copy_len < 1) copy_len = 1;
+            strncpy(truncated, desc, copy_len);
+            truncated[copy_len] = '.';
+            truncated[copy_len + 1] = '.';
+            truncated[copy_len + 2] = '\0';
+            desc = truncated;
+        }
+        XSetForeground(dpy, gc, accent_color);
+        XDrawString(dpy, win, gc, x + text_offset_x, y + text_offset_y2, desc, strlen(desc));
+    }
+}
+
+// Helper: draw wheel set indicator boxes
+static void draw_wheel_set_indicator(Display* dpy, Window win, GC gc, osd_state_t* osd,
+                                      int x, int y, float scale) {
+    int box_w = (int)(30 * scale);
+    int box_h = (int)(18 * scale);
+    int box_gap = (int)(8 * scale);
+    unsigned long fg_color = ((unsigned long)255 << 24) | 0xFFFFFF;
+    unsigned long active_color = ((unsigned long)255 << 24) | 0x44AA44;  // Green
+    unsigned long inactive_color = ((unsigned long)150 << 24) | 0x555555;
+
+    int num_sets = (osd->wheel.total_wheels + 1) / 2;
+    if (num_sets < 1) num_sets = 1;
+    if (num_sets > 3) num_sets = 3;
+
+    for (int i = 0; i < num_sets; i++) {
+        int bx = x + i * (box_w + box_gap);
+        int is_active = (i == osd->wheel.current_set);
+
+        XSetForeground(dpy, gc, is_active ? active_color : inactive_color);
+        XFillRectangle(dpy, win, gc, bx, y, box_w, box_h);
+        XSetForeground(dpy, gc, fg_color);
+        XDrawRectangle(dpy, win, gc, bx, y, box_w - 1, box_h - 1);
+
+        char label[16];
+        snprintf(label, sizeof(label), "%d", i + 1);
+        XDrawString(dpy, win, gc, bx + (int)(11 * scale), y + (int)(13 * scale), label, strlen(label));
+    }
+}
+
+// Helper: draw recent actions list (used in both minimal and expanded modes)
+static int draw_recent_actions(Display* dpy, Window win, GC gc, osd_state_t* osd,
+                                int x, int y, int max_items, float scale) {
+    int line_height = osd->font_size + (int)(3 * scale);
+    unsigned long highlight_color = ((unsigned long)200 << 24) | 0x444444;
+    long now = osd_get_time_ms();
+    int shown = 0;
+    int y_off = y;
+
+    for (int i = 0; i < osd->recent_count && shown < max_items; i++) {
+        int idx = (osd->recent_head - 1 - i + 10) % 10;
+        recent_action_t* action = &osd->recent_actions[idx];
+
+        long age = now - action->timestamp_ms;
+        if (age > osd->display_duration_ms && osd->display_duration_ms > 0) continue;
+
+        char line[128];
+        const char* desc = osd->key_descriptions[action->button_index];
+        if (desc && strlen(desc) > 0) {
+            snprintf(line, sizeof(line), "%s - %s (%s)", action->key_name, desc, action->action);
+        } else {
+            snprintf(line, sizeof(line), "%s - (%s)", action->key_name, action->action);
+        }
+
+        // Fade out effect
+        if (age < osd->display_duration_ms) {
+            float fade = 1.0f - ((float)age / osd->display_duration_ms * 0.5f);
+            unsigned char text_alpha = (unsigned char)(255 * fade);
+            unsigned long text_color = ((unsigned long)text_alpha << 24) | 0xFFFFFF;
+            XSetForeground(dpy, gc, text_color);
+        }
+
+        XDrawString(dpy, win, gc, x, y_off, line, strlen(line));
+        y_off += line_height;
+        shown++;
+    }
+
+    if (shown == 0) {
+        XSetForeground(dpy, gc, highlight_color);
+        const char* empty_msg = "(no recent actions)";
+        XDrawString(dpy, win, gc, x, y_off, empty_msg, strlen(empty_msg));
+        y_off += line_height;
+    }
+
+    return y_off;
+}
+
 // Redraw the OSD
 void osd_redraw(osd_state_t* osd) {
     if (osd == NULL || osd->display == NULL || osd->mode == OSD_MODE_HIDDEN) return;
@@ -370,10 +553,17 @@ void osd_redraw(osd_state_t* osd) {
     int title_height = osd->font_size + (int)(12 * scale);
 
     // Create colors with alpha
-    unsigned long bg_color = ((unsigned long)alpha << 24) | 0x202020;  // Dark gray with alpha
-    unsigned long fg_color = ((unsigned long)255 << 24) | 0xFFFFFF;   // White, full alpha
-    unsigned long accent_color = ((unsigned long)255 << 24) | 0x4488FF; // Blue accent
-    unsigned long highlight_color = ((unsigned long)200 << 24) | 0x444444; // Highlight
+    unsigned long bg_color = ((unsigned long)alpha << 24) | 0x202020;
+    unsigned long fg_color = ((unsigned long)255 << 24) | 0xFFFFFF;
+    unsigned long accent_color = ((unsigned long)255 << 24) | 0x4488FF;
+    unsigned long highlight_color = ((unsigned long)200 << 24) | 0x444444;
+    unsigned long dim_color = ((unsigned long)180 << 24) | 0xAAAAAA;
+
+    // Clear active button highlight after timeout
+    long now = osd_get_time_ms();
+    if (osd->active_button >= 0 && (now - osd->active_button_time_ms) > 500) {
+        osd->active_button = -1;
+    }
 
     // Clear window
     XSetForeground(dpy, gc, bg_color);
@@ -383,15 +573,13 @@ void osd_redraw(osd_state_t* osd) {
     XSetForeground(dpy, gc, accent_color);
     XDrawRectangle(dpy, win, gc, 0, 0, osd->width - 1, osd->height - 1);
 
-    // Use stored font (set in init)
+    // Use stored font
     XFontStruct* font = (XFontStruct*)osd->font;
     if (font) {
         XSetFont(dpy, gc, font->fid);
     }
 
-    int y_offset = padding + line_height;
-
-    // Draw title bar with mode indicator
+    // Draw title bar
     XSetForeground(dpy, gc, accent_color);
     XFillRectangle(dpy, win, gc, 0, 0, osd->width, title_height);
 
@@ -399,176 +587,223 @@ void osd_redraw(osd_state_t* osd) {
     const char* title = (osd->mode == OSD_MODE_MINIMAL) ? "KD100 [+] click to expand" : "KD100 [-] click to collapse";
     XDrawString(dpy, win, gc, padding, title_height - (int)(5 * scale), title, strlen(title));
 
-    y_offset = title_height + padding;
+    int y_offset = title_height + padding;
 
     if (osd->mode == OSD_MODE_MINIMAL) {
-        // Minimal mode: show recent actions
-        XSetForeground(dpy, gc, fg_color);
-        XDrawString(dpy, win, gc, padding, y_offset, "Recent Actions:", 15);
-        y_offset += line_height + (int)(5 * scale);
-
-        long now = osd_get_time_ms();
-        int shown = 0;
-
-        // Show up to 3 recent actions
-        for (int i = 0; i < osd->recent_count && shown < 3; i++) {
-            int idx = (osd->recent_head - 1 - i + 10) % 10;
-            recent_action_t* action = &osd->recent_actions[idx];
-
-            // Check if action is still within display duration
-            long age = now - action->timestamp_ms;
-            if (age > osd->display_duration_ms && osd->display_duration_ms > 0) continue;
+        // -- Mode indicator line --
+        char mode_line[128];
+        const char* mode_str = osd->wheel.wheel_mode ? "Sets" : "Sequential";
+        if (osd->leader_active) {
+            snprintf(mode_line, sizeof(mode_line), "Mode: %s | Leader: ON", mode_str);
+        } else {
+            snprintf(mode_line, sizeof(mode_line), "Mode: %s", mode_str);
+        }
+        XSetForeground(dpy, gc, dim_color);
+        XDrawString(dpy, win, gc, padding, y_offset, mode_line, strlen(mode_line));
+        y_offset += line_height + (int)(3 * scale);
+
+        // -- Wheel set + direction info --
+        if (osd->wheel.wheel_mode == 1) {
+            // Sets mode: show current set and direction labels
+            int func_idx = osd->wheel.wheel_function;
+            const char* func_desc = NULL;
+            if (func_idx >= 0 && func_idx < 32) {
+                func_desc = osd->wheel.descriptions[func_idx];
+            }
 
-            // Format: "B0 - Brush (b)" or "B0 - (b)" if no description
-            char line[128];
-            const char* desc = osd->key_descriptions[action->button_index];
-            if (desc && strlen(desc) > 0) {
-                snprintf(line, sizeof(line), "%s - %s (%s)", action->key_name, desc, action->action);
+            char wheel_line[128];
+            if (func_desc) {
+                snprintf(wheel_line, sizeof(wheel_line), "Set %d: %s",
+                         osd->wheel.current_set + 1, func_desc);
             } else {
-                snprintf(line, sizeof(line), "%s - (%s)", action->key_name, action->action);
+                snprintf(wheel_line, sizeof(wheel_line), "Set %d: Fn %d",
+                         osd->wheel.current_set + 1, func_idx);
             }
-
-            // Fade out effect based on age
-            if (age < osd->display_duration_ms) {
-                float fade = 1.0f - ((float)age / osd->display_duration_ms * 0.5f);
-                unsigned char text_alpha = (unsigned char)(255 * fade);
-                unsigned long text_color = ((unsigned long)text_alpha << 24) | 0xFFFFFF;
-                XSetForeground(dpy, gc, text_color);
+            XSetForeground(dpy, gc, accent_color);
+            XDrawString(dpy, win, gc, padding, y_offset, wheel_line, strlen(wheel_line));
+
+            // Show both functions in the set
+            int pair_idx = osd->wheel.current_set * 2;
+            const char* desc_a = (pair_idx < 32) ? osd->wheel.descriptions[pair_idx] : NULL;
+            const char* desc_b = (pair_idx + 1 < 32) ? osd->wheel.descriptions[pair_idx + 1] : NULL;
+            if (desc_a || desc_b) {
+                y_offset += line_height;
+                char pair_line[128];
+                snprintf(pair_line, sizeof(pair_line), "  L: %s | R: %s",
+                         desc_a ? desc_a : "---", desc_b ? desc_b : "---");
+                XSetForeground(dpy, gc, dim_color);
+                XDrawString(dpy, win, gc, padding, y_offset, pair_line, strlen(pair_line));
             }
-
-            XDrawString(dpy, win, gc, padding, y_offset, line, strlen(line));
-            y_offset += line_height;
-            shown++;
+        } else {
+            // Sequential mode: show current wheel function
+            int func_idx = osd->wheel.wheel_function;
+            const char* func_desc = (func_idx >= 0 && func_idx < 32) ?
+                                     osd->wheel.descriptions[func_idx] : NULL;
+            char wheel_line[128];
+            if (func_desc) {
+                snprintf(wheel_line, sizeof(wheel_line), "Wheel: %s (Fn %d)", func_desc, func_idx);
+            } else {
+                snprintf(wheel_line, sizeof(wheel_line), "Wheel: Fn %d", func_idx);
+            }
+            XSetForeground(dpy, gc, accent_color);
+            XDrawString(dpy, win, gc, padding, y_offset, wheel_line, strlen(wheel_line));
         }
+        y_offset += line_height + (int)(5 * scale);
 
-        if (shown == 0) {
-            XSetForeground(dpy, gc, highlight_color);
-            XDrawString(dpy, win, gc, padding, y_offset, "(no recent actions)", 19);
-        }
+        // -- Recent Actions --
+        XSetForeground(dpy, gc, fg_color);
+        XDrawString(dpy, win, gc, padding, y_offset, "Recent Actions:", 15);
+        y_offset += line_height + (int)(3 * scale);
+
+        draw_recent_actions(dpy, win, gc, osd, padding, y_offset, 3, scale);
 
     } else {
-        // Expanded mode: show full keyboard layout
-        XSetForeground(dpy, gc, fg_color);
-        XDrawString(dpy, win, gc, padding, y_offset, "Keyboard Layout:", 16);
+        // ========== EXPANDED MODE ==========
+
+        // -- Mode indicator line --
+        char mode_line[128];
+        const char* mode_str = osd->wheel.wheel_mode ? "Sets" : "Sequential";
+        if (osd->leader_active) {
+            snprintf(mode_line, sizeof(mode_line), "Mode: %s | Leader: ON", mode_str);
+        } else {
+            snprintf(mode_line, sizeof(mode_line), "Mode: %s", mode_str);
+        }
+        XSetForeground(dpy, gc, dim_color);
+        XDrawString(dpy, win, gc, padding, y_offset, mode_line, strlen(mode_line));
         y_offset += line_height + (int)(10 * scale);
 
-        // Draw keyboard grid (scaled)
+        // -- Keyboard grid --
         int key_width = (int)(85 * scale);
         int key_height = (int)(45 * scale);
         int grid_padding = (int)(5 * scale);
-        int text_offset_x = (int)(5 * scale);
-        int text_offset_y1 = (int)(15 * scale);
-        int text_offset_y2 = (int)(35 * scale);
         int start_x = padding;
         int start_y = y_offset;
-
-        // Calculate grid width (4 columns)
         int grid_width = 4 * key_width + 3 * grid_padding;
         int wheel_height = key_height - (int)(10 * scale);
 
-        // Draw wheel button (top, spanning same width as grid)
+        // Draw wheel button (top)
         int wheel_y = start_y;
-        XSetForeground(dpy, gc, highlight_color);
+        int wheel_active = (osd->active_button == 18 && (now - osd->active_button_time_ms) < 500);
+        unsigned long wheel_bg = wheel_active ?
+            (((unsigned long)255 << 24) | 0x44AA44) : highlight_color;
+        XSetForeground(dpy, gc, wheel_bg);
         XFillRectangle(dpy, win, gc, start_x, wheel_y, grid_width, wheel_height);
-        XSetForeground(dpy, gc, fg_color);
+        XSetForeground(dpy, gc, wheel_active ? (((unsigned long)255 << 24) | 0x88FF88) : fg_color);
         XDrawRectangle(dpy, win, gc, start_x, wheel_y, grid_width - 1, wheel_height - 1);
 
-        // Wheel button label
         const char* wheel_desc = osd->key_descriptions[18] ? osd->key_descriptions[18] : "Wheel Toggle";
-        XDrawString(dpy, win, gc, start_x + text_offset_x, wheel_y + (int)(22 * scale), wheel_desc, strlen(wheel_desc));
+        XSetForeground(dpy, gc, fg_color);
+        XDrawString(dpy, win, gc, start_x + (int)(5 * scale), wheel_y + (int)(22 * scale),
+                    wheel_desc, strlen(wheel_desc));
 
         start_y += key_height;
 
-        // Draw main button grid (4 rows x 4 columns)
-        // Note: Button 15 is drawn separately with extended height
+        // Draw main button grid (rows 1-4, skipping button 15)
         for (int row = 1; row < 5; row++) {
             for (int col = 0; col < 4; col++) {
                 int btn = BUTTON_LAYOUT[row][col];
-                if (btn < 0 || btn == 15) continue;  // Skip button 15, drawn separately
+                if (btn < 0 || btn == 15) continue;
 
                 int x = start_x + col * (key_width + grid_padding);
                 int y = start_y + (row - 1) * (key_height + grid_padding);
 
-                // Draw key background
-                XSetForeground(dpy, gc, highlight_color);
-                XFillRectangle(dpy, win, gc, x, y, key_width, key_height);
-
-                // Draw key border
-                XSetForeground(dpy, gc, fg_color);
-                XDrawRectangle(dpy, win, gc, x, y, key_width - 1, key_height - 1);
-
-                // Draw button number
-                char num[16];
-                snprintf(num, sizeof(num), "%d", btn);
-                XDrawString(dpy, win, gc, x + text_offset_x, y + text_offset_y1, num, strlen(num));
-
-                // Draw description or function
-                const char* desc = NULL;
-                if (osd->key_descriptions[btn]) {
-                    desc = osd->key_descriptions[btn];
-                } else if (osd->config && btn < osd->config->totalButtons &&
-                           osd->config->events[btn].function) {
-                    desc = osd->config->events[btn].function;
-                }
-
-                if (desc) {
-                    // Truncate if too long
-                    char truncated[12];
-                    if (strlen(desc) > 10) {
-                        strncpy(truncated, desc, 9);
-                        truncated[9] = '.';
-                        truncated[10] = '.';
-                        truncated[11] = '\0';
-                        desc = truncated;
-                    }
-                    XSetForeground(dpy, gc, accent_color);
-                    XDrawString(dpy, win, gc, x + text_offset_x, y + text_offset_y2, desc, strlen(desc));
-                }
+                draw_button_key(dpy, win, gc, osd, btn, x, y, key_width, key_height, scale);
             }
         }
 
-        // Draw bottom row (buttons 15, 16, and 17 have special layout)
-        int bottom_y = start_y + 4 * (key_height + grid_padding);
-
-        // Button 15 (tall - extends from row 4 down to bottom row)
+        // Button 15 (tall)
         int btn15_x = start_x + 3 * (key_width + grid_padding);
         int btn15_y = start_y + 3 * (key_height + grid_padding);
         int btn15_height = 2 * key_height + grid_padding;
-        XSetForeground(dpy, gc, highlight_color);
-        XFillRectangle(dpy, win, gc, btn15_x, btn15_y, key_width, btn15_height);
-        XSetForeground(dpy, gc, fg_color);
-        XDrawRectangle(dpy, win, gc, btn15_x, btn15_y, key_width - 1, btn15_height - 1);
-        XDrawString(dpy, win, gc, btn15_x + text_offset_x, btn15_y + text_offset_y1, "15", 2);
-        if (osd->key_descriptions[15]) {
-            XSetForeground(dpy, gc, accent_color);
-            XDrawString(dpy, win, gc, btn15_x + text_offset_x, btn15_y + text_offset_y2, osd->key_descriptions[15],
-                        strlen(osd->key_descriptions[15]) > 10 ? 10 : strlen(osd->key_descriptions[15]));
-        }
+        draw_button_key(dpy, win, gc, osd, 15, btn15_x, btn15_y, key_width, btn15_height, scale);
 
         // Button 16 (wide)
-        XSetForeground(dpy, gc, highlight_color);
-        XFillRectangle(dpy, win, gc, start_x, bottom_y, key_width * 2 + grid_padding, key_height);
-        XSetForeground(dpy, gc, fg_color);
-        XDrawRectangle(dpy, win, gc, start_x, bottom_y, key_width * 2 + grid_padding - 1, key_height - 1);
-        XDrawString(dpy, win, gc, start_x + text_offset_x, bottom_y + text_offset_y1, "16", 2);
-        if (osd->key_descriptions[16]) {
-            XSetForeground(dpy, gc, accent_color);
-            XDrawString(dpy, win, gc, start_x + text_offset_x, bottom_y + text_offset_y2, osd->key_descriptions[16],
-                        strlen(osd->key_descriptions[16]) > 15 ? 15 : strlen(osd->key_descriptions[16]));
-        }
+        int bottom_y = start_y + 4 * (key_height + grid_padding);
+        draw_button_key(dpy, win, gc, osd, 16, start_x, bottom_y,
+                        key_width * 2 + grid_padding, key_height, scale);
 
         // Button 17
         int x17 = start_x + 2 * (key_width + grid_padding);
-        XSetForeground(dpy, gc, highlight_color);
-        XFillRectangle(dpy, win, gc, x17, bottom_y, key_width, key_height);
+        draw_button_key(dpy, win, gc, osd, 17, x17, bottom_y, key_width, key_height, scale);
+
+        y_offset = bottom_y + key_height + (int)(10 * scale);
+
+        // -- Wheel Set Indicator --
         XSetForeground(dpy, gc, fg_color);
-        XDrawRectangle(dpy, win, gc, x17, bottom_y, key_width - 1, key_height - 1);
-        XDrawString(dpy, win, gc, x17 + text_offset_x, bottom_y + text_offset_y1, "17", 2);
-        if (osd->key_descriptions[17]) {
+        const char* wheel_label = "Wheel Sets:";
+        XDrawString(dpy, win, gc, padding, y_offset, wheel_label, strlen(wheel_label));
+
+        // Draw set boxes next to label
+        int label_width = (int)(80 * scale);
+        draw_wheel_set_indicator(dpy, win, gc, osd, padding + label_width, y_offset - (int)(12 * scale), scale);
+        y_offset += line_height + (int)(3 * scale);
+
+        // -- Wheel direction labels for current set --
+        if (osd->wheel.wheel_mode == 1) {
+            int pair_idx = osd->wheel.current_set * 2;
+            const char* desc_a = (pair_idx < 32) ? osd->wheel.descriptions[pair_idx] : NULL;
+            const char* desc_b = (pair_idx + 1 < 32) ? osd->wheel.descriptions[pair_idx + 1] : NULL;
+
+            char dir_line[128];
+            // Position 0 = active highlighted
+            int pos = osd->wheel.position_in_set;
+            if (desc_a && desc_b) {
+                snprintf(dir_line, sizeof(dir_line), "%s%s  |  %s%s",
+                         pos == 0 ? "> " : "  ", desc_a,
+                         pos == 1 ? "> " : "  ", desc_b);
+            } else if (desc_a) {
+                snprintf(dir_line, sizeof(dir_line), "%s%s  |  ---",
+                         pos == 0 ? "> " : "  ", desc_a);
+            } else if (desc_b) {
+                snprintf(dir_line, sizeof(dir_line), "---  |  %s%s",
+                         pos == 1 ? "> " : "  ", desc_b);
+            } else {
+                snprintf(dir_line, sizeof(dir_line), "Fn %d  |  Fn %d", pair_idx, pair_idx + 1);
+            }
+            XSetForeground(dpy, gc, accent_color);
+            XDrawString(dpy, win, gc, padding, y_offset, dir_line, strlen(dir_line));
+            y_offset += line_height;
+
+            // Show last wheel action (aggregated)
+            if (osd->wheel.last_wheel_action && (now - osd->wheel.last_wheel_time_ms) < osd->display_duration_ms) {
+                XSetForeground(dpy, gc, dim_color);
+                XDrawString(dpy, win, gc, padding, y_offset,
+                            osd->wheel.last_wheel_action, strlen(osd->wheel.last_wheel_action));
+            }
+            y_offset += line_height;
+        } else {
+            // Sequential mode
+            int func_idx = osd->wheel.wheel_function;
+            const char* func_desc = (func_idx >= 0 && func_idx < 32) ?
+                                     osd->wheel.descriptions[func_idx] : NULL;
+            char seq_line[128];
+            if (func_desc) {
+                snprintf(seq_line, sizeof(seq_line), "Active: %s (Fn %d/%d)",
+                         func_desc, func_idx, osd->wheel.total_wheels);
+            } else {
+                snprintf(seq_line, sizeof(seq_line), "Active: Fn %d/%d",
+                         func_idx, osd->wheel.total_wheels);
+            }
             XSetForeground(dpy, gc, accent_color);
-            XDrawString(dpy, win, gc, x17 + text_offset_x, bottom_y + text_offset_y2, osd->key_descriptions[17],
-                        strlen(osd->key_descriptions[17]) > 10 ? 10 : strlen(osd->key_descriptions[17]));
+            XDrawString(dpy, win, gc, padding, y_offset, seq_line, strlen(seq_line));
+            y_offset += line_height;
+
+            if (osd->wheel.last_wheel_action && (now - osd->wheel.last_wheel_time_ms) < osd->display_duration_ms) {
+                XSetForeground(dpy, gc, dim_color);
+                XDrawString(dpy, win, gc, padding, y_offset,
+                            osd->wheel.last_wheel_action, strlen(osd->wheel.last_wheel_action));
+            }
+            y_offset += line_height;
         }
+
+        y_offset += (int)(5 * scale);
+
+        // -- Recent Actions (3 items in expanded view too) --
+        XSetForeground(dpy, gc, fg_color);
+        XDrawString(dpy, win, gc, padding, y_offset, "Recent Actions:", 15);
+        y_offset += line_height + (int)(3 * scale);
+
+        draw_recent_actions(dpy, win, gc, osd, padding, y_offset, 3, scale);
     }
 
     XFlush(dpy);
@@ -747,3 +982,83 @@ void osd_set_display_duration(osd_state_t* osd, int duration_ms) {
     if (osd == NULL) return;
     osd->display_duration_ms = duration_ms;
 }
+
+// Record a wheel action with aggregation (don't repeat same action)
+void osd_record_wheel_action(osd_state_t* osd, const char* direction, const char* description) {
+    if (osd == NULL) return;
+
+    long now = osd_get_time_ms();
+    char action_str[128];
+
+    if (description && strlen(description) > 0) {
+        snprintf(action_str, sizeof(action_str), "%s %s", description, direction ? direction : "");
+    } else {
+        snprintf(action_str, sizeof(action_str), "Wheel %s", direction ? direction : "turn");
+    }
+
+    // Aggregate: if same action within 500ms, just update timestamp
+    if (osd->wheel.last_wheel_action &&
+        strcmp(osd->wheel.last_wheel_action, action_str) == 0 &&
+        (now - osd->wheel.last_wheel_time_ms) < 500) {
+        osd->wheel.last_wheel_time_ms = now;
+        osd->wheel.wheel_action_count++;
+    } else {
+        if (osd->wheel.last_wheel_action) free(osd->wheel.last_wheel_action);
+        osd->wheel.last_wheel_action = strdup(action_str);
+        osd->wheel.last_wheel_time_ms = now;
+        osd->wheel.wheel_action_count = 1;
+    }
+
+    osd->last_action_time_ms = now;
+
+    // Auto-show if hidden
+    if (osd->mode == OSD_MODE_HIDDEN && osd->auto_show && osd->display != NULL) {
+        osd_set_mode(osd, OSD_MODE_MINIMAL);
+    } else if (osd->mode != OSD_MODE_HIDDEN) {
+        osd_redraw(osd);
+    }
+}
+
+// Set wheel state for OSD display
+void osd_set_wheel_state(osd_state_t* osd, int current_set, int position_in_set,
+                          int wheel_function, int wheel_mode, int total_wheels) {
+    if (osd == NULL) return;
+    osd->wheel.current_set = current_set;
+    osd->wheel.position_in_set = position_in_set;
+    osd->wheel.wheel_function = wheel_function;
+    osd->wheel.wheel_mode = wheel_mode;
+    osd->wheel.total_wheels = total_wheels;
+
+    if (osd->mode != OSD_MODE_HIDDEN) {
+        osd_redraw(osd);
+    }
+}
+
+// Set a wheel function description
+void osd_set_wheel_description(osd_state_t* osd, int index, const char* description) {
+    if (osd == NULL || index < 0 || index >= 32) return;
+    if (osd->wheel.descriptions[index]) free(osd->wheel.descriptions[index]);
+    osd->wheel.descriptions[index] = description ? strdup(description) : NULL;
+}
+
+// Set active button for highlighting
+void osd_set_active_button(osd_state_t* osd, int button_index) {
+    if (osd == NULL) return;
+    osd->active_button = button_index;
+    osd->active_button_time_ms = osd_get_time_ms();
+
+    if (osd->mode != OSD_MODE_HIDDEN) {
+        osd_redraw(osd);
+    }
+}
+
+// Set leader state for visual feedback
+void osd_set_leader_state(osd_state_t* osd, int active, int leader_button) {
+    if (osd == NULL) return;
+    osd->leader_active = active;
+    osd->leader_button = leader_button;
+
+    if (osd->mode != OSD_MODE_HIDDEN) {
+        osd_redraw(osd);
+    }
+}
diff --git a/src/osd.h b/src/osd.h
index 63fdd00..62a1467 100644
--- a/src/osd.h
+++ b/src/osd.h
@@ -18,6 +18,19 @@ typedef struct {
     long timestamp_ms;    // When it was pressed
 } recent_action_t;
 
+// Wheel state information for OSD display
+typedef struct {
+    int current_set;              // Current active set (0, 1, 2)
+    int position_in_set;          // Position within set (0 or 1)
+    int wheel_function;           // Current wheel function index
+    int wheel_mode;               // 0 = sequential, 1 = sets
+    int total_wheels;             // Total number of wheel functions
+    char* descriptions[32];       // Descriptions for each wheel function
+    char* last_wheel_action;      // Last aggregated wheel action description
+    long last_wheel_time_ms;      // Timestamp of last wheel event
+    int wheel_action_count;       // Count of repeated wheel actions (for aggregation)
+} osd_wheel_state_t;
+
 // OSD state structure
 typedef struct {
     osd_mode_t mode;              // Current display mode
@@ -61,6 +74,17 @@ typedef struct {
 
     // Key descriptions for the current profile
     char* key_descriptions[19];   // Descriptions for buttons 0-18
+
+    // Active button highlighting
+    int active_button;            // Currently pressed button (-1 = none)
+    long active_button_time_ms;   // When the button was pressed
+
+    // Leader state feedback
+    int leader_active;            // Is leader key currently active
+    int leader_button;            // Which button is the leader
+
+    // Wheel state
+    osd_wheel_state_t wheel;
 } osd_state_t;
 
 // OSD lifecycle functions
@@ -76,6 +100,7 @@ void osd_set_mode(osd_state_t* osd, osd_mode_t mode);
 
 // OSD update functions
 void osd_record_action(osd_state_t* osd, int button_index, const char* action);
+void osd_record_wheel_action(osd_state_t* osd, const char* direction, const char* description);
 void osd_update(osd_state_t* osd);       // Process X11 events and redraw if needed
 void osd_redraw(osd_state_t* osd);       // Force redraw
 
@@ -92,4 +117,13 @@ void osd_clear_descriptions(osd_state_t* osd);
 void osd_set_opacity(osd_state_t* osd, float opacity);
 void osd_set_display_duration(osd_state_t* osd, int duration_ms);
 
+// Wheel state functions
+void osd_set_wheel_state(osd_state_t* osd, int current_set, int position_in_set,
+                          int wheel_function, int wheel_mode, int total_wheels);
+void osd_set_wheel_description(osd_state_t* osd, int index, const char* description);
+
+// Active button and leader state
+void osd_set_active_button(osd_state_t* osd, int button_index);
+void osd_set_leader_state(osd_state_t* osd, int active, int leader_button);
+
 #endif // OSD_H