Enhance driver interface with wheel descriptions, button highlighting, and OSD feedback
- 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
@@ -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 @@ -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"); @@ -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 @@ -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 @@ -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); + } +} @@ -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