Add on-screen display (OSD) and profile system
Features: - Semi-transparent OSD overlay (33% opacity by default) - Two modes: minimal (recent actions) and expanded (full keyboard layout) - Draggable by clicking and dragging the title bar - Click title bar to toggle between minimal and expanded modes - Shows key descriptions for all 19 buttons in expanded mode Profile System: - Automatic profile switching based on active window title - Wildcard pattern matching (e.g., "krita*", "*photoshop*") - Case-insensitive matching - Per-profile key descriptions - Priority-based profile selection Configuration: - osd_enabled, osd_opacity, osd_position settings - profiles_file, profile_auto_switch settings - description_0 through description_18 for key labels - Example profiles.cfg with presets for Krita, GIMP, Blender, etc. Technical: - New modules: osd.c/h, window.c/h, profiles.c/h - X11/Xrender for transparent overlay rendering - Active window tracking via _NET_ACTIVE_WINDOW - Requires: libx11-dev, libxrender-dev, libxext-dev https://claude.ai/code/session_01H5sr8TkRyt6U1vdkVZRRvy
+2433 −50 · 13 files changed
-
Makefile+19 −5 -
default.cfg+95 −4 -
profiles.cfg+263 −0 -
src/config.c+178 −0 -
src/config.h+25 −0 -
src/device.c+144 −17 -
src/main.c+25 −24 -
src/osd.c+652 −0 -
src/osd.h+89 −0 -
src/profiles.c+556 −0 -
src/profiles.h+85 −0 -
src/window.c+259 −0 -
src/window.h+43 −0
@@ -1,19 +1,20 @@ # Compiler and flags CC = gcc -CFLAGS = -Wall -Wextra -Wpedantic -Isrc -LDFLAGS = -lusb-1.0 -ldl +CFLAGS = -Wall -Wextra -Wpedantic -Isrc $(shell pkg-config --cflags x11 xrender xext 2>/dev/null) +LDFLAGS = -lusb-1.0 -ldl $(shell pkg-config --libs x11 xrender xext 2>/dev/null) USER = $(shell id -u) DIR = $(shell pwd) HOME = "/home/"$(shell logname) TARGET = KD100 DEBUG_TARGET = KD100-debug -VERSION = 1.5.1 +VERSION = 1.6.0 # Source files SRC_DIR = src SOURCES = $(SRC_DIR)/main.c $(SRC_DIR)/config.c $(SRC_DIR)/device.c \ $(SRC_DIR)/handler.c $(SRC_DIR)/leader.c $(SRC_DIR)/utils.c \ - $(SRC_DIR)/compat.c + $(SRC_DIR)/compat.c $(SRC_DIR)/osd.c $(SRC_DIR)/window.c \ + $(SRC_DIR)/profiles.c OBJECTS = $(SOURCES:.c=.o) # Debug flags @@ -123,6 +124,13 @@ clean: rm -f $(OBJECTS) *.o *.log core core.* @echo "Cleaned build artifacts" +# Check for X11 dependencies +check-deps: + @echo "Checking dependencies..." + @pkg-config --exists x11 xrender xext && echo "X11 libraries: OK" || echo "X11 libraries: MISSING (install libx11-dev libxrender-dev libxext-dev)" + @which xdotool > /dev/null && echo "xdotool: OK" || echo "xdotool: MISSING (install xdotool)" + @pkg-config --exists libusb-1.0 && echo "libusb: OK" || echo "libusb: MISSING (install libusb-1.0-0-dev)" + # Help target help: @echo "Available targets:" @@ -136,13 +144,19 @@ help: @echo " make analyze - Analyze core dump (if exists)" @echo " make asan - Build with AddressSanitizer" @echo " make ubsan - Build with UndefinedBehaviorSanitizer" + @echo " make check-deps - Check for required dependencies" @echo " make clean - Remove all build artifacts" @echo " make help - Show this help" + @echo "" + @echo "New in v$(VERSION):" + @echo " - On-screen display (OSD) for key actions" + @echo " - Profile system with window title matching" + @echo " - Requires: libx11-dev libxrender-dev libxext-dev" # Version info version: @echo "KD100 Makefile v$(VERSION)" @echo "Compiler: $(shell $(CC) --version | head -1)" -.PHONY: all debug release install gdb valgrind strace analyze asan ubsan clean help version +.PHONY: all debug release install gdb valgrind strace analyze asan ubsan check-deps clean help version @@ -1,5 +1,5 @@ -// KD100 config file -// +// KD100 config file - Version 1.6.0 +// // Assumptions: // - (B)utton defines a new button and the following number specifies which button it is. // - (B)uttons do not need to be listed in order but their type and function must be listed on separate lines afterwards. @@ -17,13 +17,13 @@ // - '//' is not required to add comments to the file but anything after a button, type, or function will be included in the program // - All other text is skipped by the program // - (W)heel functions cannot run programs or act as mouse input -// +// // (B)utton Types: // 0: Key - The pressed button acts as a key or a combination of keys // ex) a = key 'A' | ctrl+a = control and 'A' at the same time // 1: function - The pressed button runs a bash command/script // ex) krita | echo Hello world | gpio www.example.com -// NOTE: "swap" changes the wheel buttons function +// NOTE: "swap" changes the wheel buttons function // 2: Mouse buttons - Specify mouse1, 2, 3, 4, or 5 activates mouse buttons (left/middle/right/scroll up/ scroll down) // ex) type: 2 function: mouse1 // @@ -46,6 +46,97 @@ // +-------------------+ // +// +// ========================================== +// ON-SCREEN DISPLAY (OSD) CONFIGURATION +// ========================================== +// +// The OSD shows key actions as you press them (like Blender's screencast keys). +// It has two modes: +// - Minimal: Shows recent actions in a small box +// - Expanded: Shows full keyboard layout with all key mappings +// +// Click the title bar to toggle between modes. +// Drag the title bar to move the OSD. +// +// OSD Settings: +// osd_enabled: true/false - Enable/disable OSD feature +// osd_start_visible: true/false - Show OSD when driver starts +// osd_position: x,y - Initial position on screen +// osd_opacity: 0.0-1.0 - Background opacity (0.67 = 33% transparent) +// osd_display_duration: milliseconds - How long actions stay visible +// osd_min_size: width,height - Size in minimal mode +// osd_expanded_size: width,height - Size in expanded mode +// osd_toggle_button: 0-17 - Button to toggle OSD visibility (-1 = disabled) +// +osd_enabled: true +osd_start_visible: false +osd_position: 50,50 +osd_opacity: 0.67 +osd_display_duration: 3000 +osd_min_size: 200,100 +osd_expanded_size: 400,350 +osd_toggle_button: -1 + +// +// ========================================== +// PROFILE CONFIGURATION +// ========================================== +// +// Profiles allow different key configurations for different applications. +// Profiles are matched by window title using wildcard patterns: +// * - matches any sequence of characters +// ? - matches any single character +// Matching is case-insensitive. +// +// Example patterns: +// krita* - Matches "Krita", "krita-5.0", "Krita - Untitled" +// *photoshop* - Matches any window with "photoshop" in the title +// paintstorm* - Matches "PaintStorm Studio", "paintstorm-2.5" +// *blender* - Matches any Blender window +// +// Profile Settings: +// profiles_file: Path to profiles configuration file +// profile_auto_switch: true/false - Automatically switch profiles +// profile_check_interval: milliseconds - How often to check active window +// +// See profiles.cfg for profile definitions. +// +profiles_file: profiles.cfg +profile_auto_switch: true +profile_check_interval: 500 + +// +// ========================================== +// KEY DESCRIPTIONS (Default Profile) +// ========================================== +// +// Key descriptions are shown in the OSD expanded view. +// These are the descriptions for the default profile. +// Per-application descriptions are defined in profiles.cfg. +// +// Format: description_<button_number>: <description> +// +description_0: Brush +description_1: Color Pick +description_2: Transform +description_3: Lasso +description_4: New Layer +description_5: Duplicate +description_6: Merge Down +description_7: Eraser +description_8: Deselect +description_9: Delete +description_10: Undo +description_11: Inv Select +description_12: Pan +description_13: Zoom +description_14: Rotate +description_15: Mirror +description_16: SHIFT +description_17: Redo +description_18: Wheel Fn + // // Leader configuration // Options: @@ -0,0 +1,263 @@ +// KD100 Profiles Configuration +// +// This file defines application-specific profiles for the KD100 keypad. +// Profiles are matched by window title using wildcard patterns. +// +// Pattern Syntax: +// * - matches any sequence of characters +// ? - matches any single character +// Matching is case-insensitive +// +// Example patterns: +// krita* - Matches "Krita", "krita-5.0", "Krita - Untitled" +// *photoshop* - Matches any window with "photoshop" in the title +// gimp* - Matches "GIMP", "gimp-2.10", "GIMP - New Image" +// *blender* - Matches any Blender window +// +// Profile Format: +// profile: <name> - Profile name (required) +// pattern: <window_pattern> - Window title/class pattern (required) +// priority: <number> - Higher priority matched first (default: 0) +// config: <config_file> - Alternate config file (optional) +// default: true/false - Is this the fallback profile? (optional) +// description_<N>: <text> - Key description for button N (optional) +// +// ============================================================ + +// Default Profile (fallback when no window matches) +profile: Default +pattern: * +priority: -1000 +default: true +description_0: Brush +description_1: Color Pick +description_2: Transform +description_3: Lasso +description_4: New Layer +description_5: Duplicate +description_6: Merge Down +description_7: Eraser +description_8: Deselect +description_9: Delete +description_10: Undo +description_11: Invert Sel +description_12: Pan +description_13: Zoom +description_14: Rotate +description_15: Mirror +description_16: SHIFT +description_17: Redo +description_18: Wheel + +// ============================================================ +// KRITA +// ============================================================ +profile: Krita +pattern: krita* +priority: 10 +description_0: Brush (B) +description_1: Color Picker (P) +description_2: Transform (T) +description_3: Lasso (L) +description_4: New Layer +description_5: Duplicate Lyr +description_6: Merge Down +description_7: Eraser (E) +description_8: Deselect +description_9: Delete Sel +description_10: Undo +description_11: Invert Sel +description_12: Pan (Space) +description_13: Zoom +description_14: Rotate +description_15: Mirror View +description_16: Shift Mod +description_17: Redo +description_18: Brush Size + +// ============================================================ +// GIMP +// ============================================================ +profile: GIMP +pattern: gimp* +priority: 10 +description_0: Paintbrush +description_1: Color Picker +description_2: Transform +description_3: Free Select +description_4: New Layer +description_5: Duplicate +description_6: Merge Down +description_7: Eraser +description_8: Select None +description_9: Clear +description_10: Undo +description_11: Invert Sel +description_12: Pan +description_13: Zoom +description_14: Rotate +description_15: Flip Horiz +description_16: Shift +description_17: Redo +description_18: Brush Size + +// ============================================================ +// INKSCAPE +// ============================================================ +profile: Inkscape +pattern: inkscape* +priority: 10 +description_0: Bezier Tool +description_1: Color Picker +description_2: Transform +description_3: Freehand +description_4: New Layer +description_5: Duplicate +description_6: Group +description_7: Eraser +description_8: Deselect +description_9: Delete +description_10: Undo +description_11: Invert Sel +description_12: Pan +description_13: Zoom +description_14: Rotate +description_15: Flip Horiz +description_16: Shift +description_17: Redo +description_18: Zoom + +// ============================================================ +// BLENDER +// ============================================================ +profile: Blender +pattern: *blender* +priority: 10 +description_0: Brush +description_1: Sample +description_2: Grab +description_3: Lasso +description_4: Add Object +description_5: Duplicate +description_6: Join +description_7: Erase +description_8: Deselect +description_9: Delete +description_10: Undo +description_11: Invert Sel +description_12: Pan +description_13: Zoom +description_14: Rotate View +description_15: Mirror +description_16: Shift +description_17: Redo +description_18: Brush Size + +// ============================================================ +// PAINTSTORM STUDIO +// ============================================================ +profile: PaintStorm +pattern: paintstorm* +priority: 10 +description_0: Brush +description_1: Picker +description_2: Transform +description_3: Lasso +description_4: New Layer +description_5: Duplicate +description_6: Merge +description_7: Eraser +description_8: Deselect +description_9: Delete +description_10: Undo +description_11: Invert +description_12: Pan +description_13: Zoom +description_14: Rotate +description_15: Flip View +description_16: Shift +description_17: Redo +description_18: Brush Size + +// ============================================================ +// PHOTOSHOP (via Wine/Bottles) +// ============================================================ +profile: Photoshop +pattern: *photoshop* +priority: 10 +description_0: Brush +description_1: Eyedropper +description_2: Transform +description_3: Lasso +description_4: New Layer +description_5: Duplicate +description_6: Merge Down +description_7: Eraser +description_8: Deselect +description_9: Delete +description_10: Undo +description_11: Invert Sel +description_12: Hand Tool +description_13: Zoom +description_14: Rotate View +description_15: Flip Canvas +description_16: Shift +description_17: Redo +description_18: Brush Size + +// ============================================================ +// CLIP STUDIO PAINT (via Wine/Bottles) +// ============================================================ +profile: ClipStudio +pattern: *clip studio* +priority: 10 +description_0: Pen +description_1: Eyedropper +description_2: Transform +description_3: Lasso +description_4: New Layer +description_5: Duplicate +description_6: Merge Down +description_7: Eraser +description_8: Deselect +description_9: Clear +description_10: Undo +description_11: Invert Sel +description_12: Hand +description_13: Zoom +description_14: Rotate +description_15: Flip +description_16: Shift +description_17: Redo +description_18: Brush Size + +// ============================================================ +// TERMINAL / CONSOLE (for development) +// ============================================================ +profile: Terminal +pattern: *terminal* +priority: 5 +description_0: - +description_1: - +description_2: - +description_3: - +description_4: - +description_5: - +description_6: - +description_7: - +description_8: - +description_9: - +description_10: Undo +description_11: - +description_12: - +description_13: - +description_14: - +description_15: - +description_16: Shift +description_17: Redo +description_18: Scroll + +// You can add more profiles below following the same format. +// Each profile needs at minimum: +// profile: <name> +// pattern: <window_pattern> @@ -68,6 +68,29 @@ config_t* config_create(void) { config->wheelEvents[0].right = NULL; config->wheelEvents[0].left = NULL; + // Initialize OSD settings + config->osd.enabled = 0; + config->osd.start_visible = 0; + config->osd.pos_x = 50; + config->osd.pos_y = 50; + config->osd.opacity = 0.67f; // 33% transparent = 67% opaque + config->osd.display_duration_ms = 3000; + config->osd.min_width = 200; + config->osd.min_height = 100; + config->osd.expanded_width = 400; + config->osd.expanded_height = 350; + config->osd.osd_toggle_button = -1; // Disabled by default + + // Initialize profile settings + config->profile.profiles_file = NULL; + config->profile.auto_switch = 1; // Enabled by default + config->profile.check_interval_ms = 500; + + // Initialize key descriptions + for (int i = 0; i < 19; i++) { + config->key_descriptions[i] = NULL; + } + return config; } @@ -99,6 +122,18 @@ void config_destroy(config_t* config) { free(config->leader.leader_function); } + // Free profile settings + if (config->profile.profiles_file != NULL) { + free(config->profile.profiles_file); + } + + // Free key descriptions + for (int i = 0; i < 19; i++) { + if (config->key_descriptions[i] != NULL) { + free(config->key_descriptions[i]); + } + } + free(config); } @@ -222,6 +257,121 @@ int config_load(config_t* config, const char* filename, int debug) { continue; } + // Parse OSD settings + if (strncasecmp(line, "osd_enabled:", 12) == 0) { + char* value = line + 12; + while (*value == ' ') value++; + config->osd.enabled = (strncasecmp(value, "true", 4) == 0 || strcmp(value, "1") == 0); + if (debug) printf("Config: osd_enabled = %s\n", config->osd.enabled ? "true" : "false"); + continue; + } + + if (strncasecmp(line, "osd_start_visible:", 18) == 0) { + char* value = line + 18; + while (*value == ' ') value++; + config->osd.start_visible = (strncasecmp(value, "true", 4) == 0 || strcmp(value, "1") == 0); + if (debug) printf("Config: osd_start_visible = %s\n", config->osd.start_visible ? "true" : "false"); + continue; + } + + if (strncasecmp(line, "osd_position:", 13) == 0) { + char* value = line + 13; + while (*value == ' ') value++; + if (sscanf(value, "%d,%d", &config->osd.pos_x, &config->osd.pos_y) == 2) { + if (debug) printf("Config: osd_position = %d,%d\n", config->osd.pos_x, config->osd.pos_y); + } + continue; + } + + if (strncasecmp(line, "osd_opacity:", 12) == 0) { + char* value = line + 12; + while (*value == ' ') value++; + float opacity = atof(value); + if (opacity < 0.0f) opacity = 0.0f; + if (opacity > 1.0f) opacity = 1.0f; + config->osd.opacity = opacity; + if (debug) printf("Config: osd_opacity = %.2f\n", config->osd.opacity); + continue; + } + + if (strncasecmp(line, "osd_display_duration:", 21) == 0) { + char* value = line + 21; + while (*value == ' ') value++; + config->osd.display_duration_ms = atoi(value); + if (debug) printf("Config: osd_display_duration = %d ms\n", config->osd.display_duration_ms); + continue; + } + + if (strncasecmp(line, "osd_min_size:", 13) == 0) { + char* value = line + 13; + while (*value == ' ') value++; + if (sscanf(value, "%d,%d", &config->osd.min_width, &config->osd.min_height) == 2) { + if (debug) printf("Config: osd_min_size = %dx%d\n", config->osd.min_width, config->osd.min_height); + } + continue; + } + + if (strncasecmp(line, "osd_expanded_size:", 18) == 0) { + char* value = line + 18; + while (*value == ' ') value++; + if (sscanf(value, "%d,%d", &config->osd.expanded_width, &config->osd.expanded_height) == 2) { + if (debug) printf("Config: osd_expanded_size = %dx%d\n", config->osd.expanded_width, config->osd.expanded_height); + } + continue; + } + + if (strncasecmp(line, "osd_toggle_button:", 18) == 0) { + char* value = line + 18; + while (*value == ' ') value++; + config->osd.osd_toggle_button = atoi(value); + if (debug) printf("Config: osd_toggle_button = %d\n", config->osd.osd_toggle_button); + continue; + } + + // Parse profile settings + if (strncasecmp(line, "profiles_file:", 14) == 0) { + char* value = line + 14; + while (*value == ' ') value++; + if (config->profile.profiles_file) free(config->profile.profiles_file); + config->profile.profiles_file = strdup(value); + if (debug) printf("Config: profiles_file = %s\n", config->profile.profiles_file); + continue; + } + + if (strncasecmp(line, "profile_auto_switch:", 20) == 0) { + char* value = line + 20; + while (*value == ' ') value++; + config->profile.auto_switch = (strncasecmp(value, "true", 4) == 0 || strcmp(value, "1") == 0); + if (debug) printf("Config: profile_auto_switch = %s\n", config->profile.auto_switch ? "true" : "false"); + continue; + } + + if (strncasecmp(line, "profile_check_interval:", 23) == 0) { + char* value = line + 23; + while (*value == ' ') value++; + config->profile.check_interval_ms = atoi(value); + if (debug) printf("Config: profile_check_interval = %d ms\n", config->profile.check_interval_ms); + continue; + } + + // Parse key descriptions (description_0, description_1, etc.) + if (strncasecmp(line, "description_", 12) == 0) { + char* num_str = line + 12; + char* colon = strchr(num_str, ':'); + if (colon) { + *colon = '\0'; + int btn = atoi(num_str); + if (btn >= 0 && btn <= 18) { + 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]); + } + } + continue; + } + // Parse button if (strncasecmp(line, "button ", 7) == 0) { char* num_str = line + 7; @@ -396,5 +546,33 @@ void config_print(const config_t* config, int debug) { printf("\n=== Wheel Click Configuration ===\n"); printf("Multi-click timeout: %d ms\n", config->wheel_click_timeout_ms); printf("Wheel mode: %s\n", wheel_mode_to_string(config->wheel_mode)); + + printf("\n=== OSD Configuration ===\n"); + printf("OSD enabled: %s\n", config->osd.enabled ? "yes" : "no"); + printf("Start visible: %s\n", config->osd.start_visible ? "yes" : "no"); + printf("Position: %d, %d\n", config->osd.pos_x, config->osd.pos_y); + printf("Opacity: %.2f\n", config->osd.opacity); + printf("Display duration: %d ms\n", config->osd.display_duration_ms); + printf("Min size: %dx%d\n", config->osd.min_width, config->osd.min_height); + printf("Expanded size: %dx%d\n", config->osd.expanded_width, config->osd.expanded_height); + printf("Toggle button: %d\n", config->osd.osd_toggle_button); + + printf("\n=== Profile Configuration ===\n"); + printf("Profiles file: %s\n", config->profile.profiles_file ? config->profile.profiles_file : "(none)"); + printf("Auto switch: %s\n", config->profile.auto_switch ? "yes" : "no"); + printf("Check interval: %d ms\n", config->profile.check_interval_ms); + + // Print key descriptions if any are set + int has_descriptions = 0; + for (int i = 0; i < 19; i++) { + if (config->key_descriptions[i]) { + if (!has_descriptions) { + printf("\n=== Key Descriptions ===\n"); + has_descriptions = 1; + } + printf("Button %d: %s\n", i, config->key_descriptions[i]); + } + } + printf("\n"); } @@ -15,6 +15,28 @@ typedef struct { char* left; } wheel; +// OSD configuration +typedef struct { + int enabled; // Is OSD enabled + int start_visible; // Show OSD on startup + int pos_x; // Initial X position + int pos_y; // Initial Y position + float opacity; // Background opacity (0.0-1.0, default 0.67 = 33% transparent) + int display_duration_ms; // How long to show key actions (default 3000) + int min_width; // Minimal mode width + int min_height; // Minimal mode height + int expanded_width; // Expanded mode width + int expanded_height; // Expanded mode height + int osd_toggle_button; // Button to toggle OSD (-1 = disabled) +} osd_config_t; + +// Profile configuration (for referencing profile files) +typedef struct { + char* profiles_file; // Path to profiles configuration file + int auto_switch; // Enable automatic profile switching + int check_interval_ms; // How often to check active window (default 500ms) +} profile_config_t; + // Configuration structure typedef struct { event* events; @@ -25,6 +47,9 @@ typedef struct { int enable_uclogic; int wheel_click_timeout_ms; // Multi-click detection timeout (20-990ms) wheel_mode_t wheel_mode; // Wheel toggle mode (sequential or sets) + osd_config_t osd; // OSD settings + profile_config_t profile; // Profile settings + char* key_descriptions[19]; // Per-button descriptions (for default profile) } config_t; // Configuration functions @@ -4,6 +4,9 @@ #include "handler.h" #include "utils.h" #include "compat.h" +#include "osd.h" +#include "profiles.h" +#include "window.h" #include <libusb-1.0/libusb.h> #include <stdio.h> #include <stdlib.h> @@ -26,6 +29,12 @@ void device_run(libusb_context* ctx, config_t* config, int debug, int accept, in int wheel_current_set = 0; // Current set: 0 (functions 0-1), 1 (functions 2-3), 2 (functions 4-5) int wheel_position_in_set = 0; // Position within set: 0 or 1 + // OSD and profile manager state + osd_state_t* osd = NULL; + profile_manager_t* profile_manager = NULL; + struct timeval last_profile_check = {0, 0}; + config_t* active_config = config; // Currently active configuration + system("clear"); if (debug > 0) { @@ -38,6 +47,70 @@ void device_run(libusb_context* ctx, config_t* config, int debug, int accept, in // Print configuration if debug config_print(config, debug); + // Initialize OSD if enabled + if (config->osd.enabled) { + osd = osd_create(config); + if (osd != NULL) { + // Apply OSD settings from config + osd->pos_x = config->osd.pos_x; + osd->pos_y = config->osd.pos_y; + osd->opacity = config->osd.opacity; + osd->display_duration_ms = config->osd.display_duration_ms; + osd->min_width = config->osd.min_width; + osd->min_height = config->osd.min_height; + osd->expanded_width = config->osd.expanded_width; + osd->expanded_height = config->osd.expanded_height; + + // Initialize X11 display + if (osd_init_display(osd) == 0) { + // Set initial key descriptions from config + for (int i = 0; i < 19; i++) { + if (config->key_descriptions[i]) { + osd_set_key_description(osd, i, config->key_descriptions[i]); + } + } + + // Show OSD if start_visible is set + if (config->osd.start_visible) { + osd_show(osd); + } + printf("OSD: Initialized successfully\n"); + } else { + printf("OSD: Failed to initialize X11 display\n"); + osd_destroy(osd); + osd = NULL; + } + } else { + printf("OSD: Failed to create OSD state\n"); + } + } + + // Initialize profile manager if profiles file is configured + if (config->profile.profiles_file) { + profile_manager = profile_manager_create(config); + if (profile_manager != NULL) { + profile_manager_set_debug(profile_manager, debug); + + // Initialize with shared display if OSD is enabled + void* shared_display = osd ? osd->display : NULL; + if (profile_manager_init(profile_manager, shared_display, osd) == 0) { + // Load profiles from file + if (profile_manager_load(profile_manager, config->profile.profiles_file) == 0) { + printf("Profiles: Loaded from %s\n", config->profile.profiles_file); + if (debug) { + profile_manager_print(profile_manager); + } + } else { + printf("Profiles: Failed to load from %s\n", config->profile.profiles_file); + } + } else { + printf("Profiles: Failed to initialize manager\n"); + profile_manager_destroy(profile_manager); + profile_manager = NULL; + } + } + } + // Check module state int uclogic_loaded = is_module_loaded("hid_uclogic"); @@ -280,6 +353,36 @@ void device_run(libusb_context* ctx, config_t* config, int debug, int accept, in while (err >= 0) { unsigned char data[40]; int keycode = 0; + + // Update OSD (process X11 events) + if (osd && osd->mode != OSD_MODE_HIDDEN) { + osd_update(osd); + } + + // Check for profile switches periodically + if (profile_manager && config->profile.auto_switch) { + struct timeval now; + gettimeofday(&now, NULL); + long time_since_check = + (now.tv_sec - last_profile_check.tv_sec) * 1000 + + (now.tv_usec - last_profile_check.tv_usec) / 1000; + + if (time_since_check >= config->profile.check_interval_ms) { + last_profile_check = now; + int profile_changed = profile_manager_update(profile_manager); + if (profile_changed > 0) { + // Get new active config + config_t* new_config = profile_manager_get_config(profile_manager); + if (new_config != NULL) { + active_config = new_config; + if (debug) { + printf("Switched to profile config\n"); + } + } + } + } + } + err = libusb_interrupt_transfer(handle, 0x81, data, sizeof(data), NULL, 0); if (err == LIBUSB_ERROR_TIMEOUT) @@ -398,18 +501,34 @@ void device_run(libusb_context* ctx, config_t* config, int debug, int accept, in int button_index = find_button_index(keycode); if (button_index != -1) { + // Check for OSD toggle button + if (config->osd.enabled && button_index == config->osd.osd_toggle_button && osd) { + osd_toggle_mode(osd); + if (debug) { + printf("OSD mode toggled\n"); + } + } + + // Record action to OSD + if (osd && active_config && button_index < active_config->totalButtons) { + const char* action = active_config->events[button_index].function; + if (action && strcmp(action, "NULL") != 0) { + osd_record_action(osd, button_index, action); + } + } + // Process button press with leader system - process_leader_combination(&config->leader, config->events, button_index, debug); + process_leader_combination(&active_config->leader, active_config->events, button_index, debug); // Also handle legacy single-button events for compatibility - if (config->events[button_index].function != NULL) { - if (strcmp(config->events[button_index].function, "NULL") == 0) { + if (active_config->events[button_index].function != NULL) { + if (strcmp(active_config->events[button_index].function, "NULL") == 0) { if (prevEvent.type != 0) { Handler(prevEvent.function, prevEvent.type, debug); prevEvent.type = 0; prevEvent.function = ""; } - } else if (strcmp(config->events[button_index].function, "swap") == 0) { + } else if (strcmp(active_config->events[button_index].function, "swap") == 0) { // Check wheel mode if (config->wheel_mode == WHEEL_MODE_SEQUENTIAL) { // Sequential mode: simple cycling through all functions @@ -454,19 +573,19 @@ void device_run(libusb_context* ctx, config_t* config, int debug, int accept, in last_button18_time = now; // Don't process yet - wait for timeout (checked at top of loop) } - } else if (strcmp(config->events[button_index].function, "mouse1") == 0 || - strcmp(config->events[button_index].function, "mouse2") == 0 || - strcmp(config->events[button_index].function, "mouse3") == 0 || - strcmp(config->events[button_index].function, "mouse4") == 0 || - strcmp(config->events[button_index].function, "mouse5") == 0) { - if (strcmp(config->events[button_index].function, prevEvent.function)) { + } else if (strcmp(active_config->events[button_index].function, "mouse1") == 0 || + strcmp(active_config->events[button_index].function, "mouse2") == 0 || + strcmp(active_config->events[button_index].function, "mouse3") == 0 || + strcmp(active_config->events[button_index].function, "mouse4") == 0 || + strcmp(active_config->events[button_index].function, "mouse5") == 0) { + if (strcmp(active_config->events[button_index].function, prevEvent.function)) { if (prevEvent.type != 0) { Handler(prevEvent.function, prevEvent.type, debug); } - prevEvent.function = config->events[button_index].function; + prevEvent.function = active_config->events[button_index].function; prevEvent.type = 3; } - Handler(config->events[button_index].function, 2, debug); + Handler(active_config->events[button_index].function, 2, debug); } } } @@ -479,14 +598,14 @@ void device_run(libusb_context* ctx, config_t* config, int debug, int accept, in } printf("]\n"); - if (config->leader.toggle_state) { - printf("Leader toggle: ON (mode: %s)\n", leader_mode_to_string(config->leader.mode)); - } else if (config->leader.leader_active) { + if (active_config->leader.toggle_state) { + printf("Leader toggle: ON (mode: %s)\n", leader_mode_to_string(active_config->leader.mode)); + } else if (active_config->leader.leader_active) { struct timeval now; gettimeofday(&now, NULL); - long elapsed = time_diff_ms(config->leader.leader_press_time, now); + long elapsed = time_diff_ms(active_config->leader.leader_press_time, now); printf("Leader active: YES (%ld ms elapsed, mode: %s)\n", - elapsed, leader_mode_to_string(config->leader.mode)); + elapsed, leader_mode_to_string(active_config->leader.mode)); } else { printf("Leader active: NO\n"); } @@ -511,4 +630,12 @@ void device_run(libusb_context* ctx, config_t* config, int debug, int accept, in } libusb_free_device_list(devs, 1); } + + // Cleanup OSD and profile manager + if (osd) { + osd_destroy(osd); + } + if (profile_manager) { + profile_manager_destroy(profile_manager); + } } @@ -1,12 +1,16 @@ /* - V1.5.0 - Modular Architecture + V1.6.0 - OSD & Profile System https://github.com/mckset/KD100.git KD100 Linux driver for X11 desktops Features: + - On-screen display (OSD) for key actions + - Profile system with window title matching + - Collapsible OSD (minimal/expanded modes) + - Draggable OSD overlay + - Wildcard pattern matching for profiles - Modular code structure for better maintainability - Configurable leader modes (one_shot, sticky, toggle) - Per-button leader eligibility - - Fixed timing system */ #include <libusb-1.0/libusb.h> @@ -165,26 +169,23 @@ int main(int args, char *in[]) { printf("\t-d [-d]\t\tEnable debug outputs (use twice to view data sent by the device)\n"); printf("\t-dry \t\tDisplay data sent by the device without sending events\n"); printf("\t-h\t\tDisplays this message\n"); - printf("\nNew in v1.5.0 - MODULAR ARCHITECTURE:\n"); - printf("\t• Separated code into focused modules for better maintainability\n"); - printf("\t• Improved code organization and testability\n"); - printf("\t• Retained all v1.4.9 features:\n"); - printf("\t - Configurable leader modes (one_shot, sticky, toggle)\n"); - printf("\t - Per-button eligibility control\n"); - printf("\t - Enhanced timing system\n"); - printf("\t• Example config in default.cfg:\n"); - printf("\t leader_button: 16\n"); - printf("\t leader_function: shift\n"); - printf("\t leader_timeout: 1000\n"); - printf("\t leader_mode: sticky\n"); - printf("\t button 0\n"); - printf("\t type: 0\n"); - printf("\t function: b\n"); - printf("\t leader_eligible: true\n"); - printf("\t button 18\n"); - printf("\t type: 1\n"); - printf("\t function: swap\n"); - printf("\t leader_eligible: false\n"); + printf("\nNew in v1.6.0 - OSD & PROFILE SYSTEM:\n"); + printf("\t• On-Screen Display (OSD) for key actions\n"); + printf("\t - Semi-transparent overlay (configurable opacity)\n"); + printf("\t - Collapsible: minimal mode (recent actions) / expanded (full layout)\n"); + printf("\t - Draggable by title bar\n"); + printf("\t - Shows what each key does in expanded mode\n"); + printf("\t• Profile System with window title matching\n"); + printf("\t - Automatic profile switching based on active window\n"); + printf("\t - Wildcard patterns: krita*, *photoshop*, gimp*\n"); + printf("\t - Case-insensitive matching\n"); + printf("\t - Per-profile key descriptions\n"); + printf("\t• Example OSD config in default.cfg:\n"); + printf("\t osd_enabled: true\n"); + printf("\t osd_opacity: 0.67\n"); + printf("\t osd_position: 50,50\n"); + printf("\t profiles_file: profiles.cfg\n"); + printf("\t profile_auto_switch: true\n"); printf("\nConfiguration:\n"); printf("\tAdd 'enable_uclogic: true' to config to work with hid_uclogic loaded\n"); printf("\tDefault: enable_uclogic: false (compatible with OpenTabletDriver)\n\n"); @@ -256,8 +257,8 @@ int main(int args, char *in[]) { } } - printf("\nKD100 Driver v1.5.0 - Modular Architecture\n"); - printf("Features: Configurable modes | Per-button eligibility | Modular design\n\n"); + printf("\nKD100 Driver v1.6.0 - OSD & Profile System\n"); + printf("Features: OSD overlay | Profile switching | Configurable modes\n\n"); // Run device handler device_run(ctx, config, debug, accept, dry); @@ -0,0 +1,652 @@ +#include "osd.h" +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/time.h> +#include <X11/Xlib.h> +#include <X11/Xutil.h> +#include <X11/Xatom.h> +#include <X11/extensions/Xrender.h> +#include <X11/extensions/shape.h> + +// Button layout for the KD100 (matches the physical layout) +// Row 0: Button 18 (wheel toggle) spans the top +// Rows 1-4: Main button grid +static const int BUTTON_LAYOUT[5][4] = { + {18, -1, -1, -1}, // Top row: wheel button (spans width) + { 0, 1, 2, 3}, // Row 1 + { 4, 5, 6, 7}, // Row 2 + { 8, 9, 10, 11}, // Row 3 + {12, 13, 14, 15} // Row 4 (16 and 17 are special - bottom left) +}; + +// Button names for display +static const char* BUTTON_NAMES[] = { + "B0", "B1", "B2", "B3", + "B4", "B5", "B6", "B7", + "B8", "B9", "B10", "B11", + "B12", "B13", "B14", "B15", + "B16", "B17", "WHEEL" +}; + +// Helper: get current time in milliseconds (osd-local version) +static long osd_get_time_ms(void) { + struct timeval tv; + gettimeofday(&tv, NULL); + return tv.tv_sec * 1000 + tv.tv_usec / 1000; +} + +// Helper: find ARGB visual for transparency +static Visual* find_argb_visual(Display* display, int screen, int* depth) { + XVisualInfo vinfo_template; + XVisualInfo* vinfo_list; + int num_visuals; + + vinfo_template.screen = screen; + vinfo_template.depth = 32; + vinfo_template.class = TrueColor; + + vinfo_list = XGetVisualInfo(display, VisualScreenMask | VisualDepthMask | VisualClassMask, + &vinfo_template, &num_visuals); + + if (vinfo_list != NULL && num_visuals > 0) { + for (int i = 0; i < num_visuals; i++) { + XRenderPictFormat* format = XRenderFindVisualFormat(display, vinfo_list[i].visual); + if (format && format->type == PictTypeDirect && format->direct.alphaMask) { + Visual* visual = vinfo_list[i].visual; + *depth = 32; + XFree(vinfo_list); + return visual; + } + } + XFree(vinfo_list); + } + + // Fallback to default visual + *depth = DefaultDepth(display, screen); + return DefaultVisual(display, screen); +} + +// Create OSD state +osd_state_t* osd_create(config_t* config) { + osd_state_t* osd = calloc(1, sizeof(osd_state_t)); + if (osd == NULL) return NULL; + + osd->mode = OSD_MODE_HIDDEN; + osd->enabled = 0; + osd->pos_x = 50; + osd->pos_y = 50; + osd->min_width = 200; + osd->min_height = 100; + osd->expanded_width = 400; + osd->expanded_height = 350; + osd->width = osd->min_width; + osd->height = osd->min_height; + osd->opacity = 0.67f; // 33% transparent = 67% opaque + osd->display_duration_ms = 3000; // Show actions for 3 seconds + osd->recent_count = 0; + osd->recent_head = 0; + osd->config = config; + osd->dragging = 0; + + // Initialize key descriptions to NULL + for (int i = 0; i < 19; i++) { + osd->key_descriptions[i] = NULL; + } + + return osd; +} + +// Destroy OSD state +void osd_destroy(osd_state_t* osd) { + if (osd == NULL) return; + + // Free recent actions + for (int i = 0; i < 10; i++) { + if (osd->recent_actions[i].key_name) free(osd->recent_actions[i].key_name); + if (osd->recent_actions[i].action) free(osd->recent_actions[i].action); + } + + // Free key descriptions + for (int i = 0; i < 19; i++) { + if (osd->key_descriptions[i]) free(osd->key_descriptions[i]); + } + + // Close X11 resources + if (osd->display) { + Display* dpy = (Display*)osd->display; + if (osd->window) { + XDestroyWindow(dpy, (Window)osd->window); + } + if (osd->gc) { + XFreeGC(dpy, (GC)osd->gc); + } + XCloseDisplay(dpy); + } + + free(osd); +} + +// Initialize X11 display +int osd_init_display(osd_state_t* osd) { + if (osd == NULL) return -1; + + Display* dpy = XOpenDisplay(NULL); + if (dpy == NULL) { + fprintf(stderr, "OSD: Cannot open X display\n"); + return -1; + } + + osd->display = dpy; + osd->screen = DefaultScreen(dpy); + + // Find ARGB visual for transparency + int depth; + Visual* visual = find_argb_visual(dpy, osd->screen, &depth); + osd->visual = visual; + + // Create colormap for the visual + Colormap colormap = XCreateColormap(dpy, RootWindow(dpy, osd->screen), visual, AllocNone); + osd->colormap = colormap; + + // Window attributes for transparency + XSetWindowAttributes attrs; + attrs.override_redirect = True; // No window manager decorations + attrs.colormap = colormap; + attrs.background_pixel = 0; // Transparent background + attrs.border_pixel = 0; + attrs.event_mask = ExposureMask | ButtonPressMask | ButtonReleaseMask | + PointerMotionMask | StructureNotifyMask; + + // Create the window + Window win = XCreateWindow(dpy, RootWindow(dpy, osd->screen), + osd->pos_x, osd->pos_y, + osd->width, osd->height, + 0, depth, InputOutput, visual, + CWOverrideRedirect | CWColormap | CWBackPixel | + CWBorderPixel | CWEventMask, + &attrs); + + osd->window = (void*)win; + + // Set window type to overlay/utility + Atom wm_type = XInternAtom(dpy, "_NET_WM_WINDOW_TYPE", False); + Atom wm_type_utility = XInternAtom(dpy, "_NET_WM_WINDOW_TYPE_UTILITY", False); + XChangeProperty(dpy, win, wm_type, XA_ATOM, 32, PropModeReplace, + (unsigned char*)&wm_type_utility, 1); + + // Set window to stay on top + Atom wm_state = XInternAtom(dpy, "_NET_WM_STATE", False); + Atom wm_state_above = XInternAtom(dpy, "_NET_WM_STATE_ABOVE", False); + Atom wm_state_sticky = XInternAtom(dpy, "_NET_WM_STATE_STICKY", False); + Atom states[2] = {wm_state_above, wm_state_sticky}; + XChangeProperty(dpy, win, wm_state, XA_ATOM, 32, PropModeReplace, + (unsigned char*)states, 2); + + // Make window click-through for most of the area (will update on redraw) + // Skip input for the main area, but allow dragging from edges + + // Create graphics context + GC gc = XCreateGC(dpy, win, 0, NULL); + osd->gc = gc; + + // Set window name + XStoreName(dpy, win, "KD100 OSD"); + + printf("OSD: Display initialized (ARGB visual: %s)\n", depth == 32 ? "yes" : "no"); + osd->enabled = 1; + + return 0; +} + +// Show OSD window +void osd_show(osd_state_t* osd) { + if (osd == NULL || osd->display == NULL) return; + + Display* dpy = (Display*)osd->display; + Window win = (Window)osd->window; + + XMapWindow(dpy, win); + XRaiseWindow(dpy, win); + XFlush(dpy); + + if (osd->mode == OSD_MODE_HIDDEN) { + osd->mode = OSD_MODE_MINIMAL; + } + + osd_redraw(osd); +} + +// Hide OSD window +void osd_hide(osd_state_t* osd) { + if (osd == NULL || osd->display == NULL) return; + + Display* dpy = (Display*)osd->display; + Window win = (Window)osd->window; + + XUnmapWindow(dpy, win); + XFlush(dpy); + + osd->mode = OSD_MODE_HIDDEN; +} + +// Toggle between minimal and expanded modes +void osd_toggle_mode(osd_state_t* osd) { + if (osd == NULL) return; + + switch (osd->mode) { + case OSD_MODE_HIDDEN: + osd_set_mode(osd, OSD_MODE_MINIMAL); + break; + case OSD_MODE_MINIMAL: + osd_set_mode(osd, OSD_MODE_EXPANDED); + break; + case OSD_MODE_EXPANDED: + osd_set_mode(osd, OSD_MODE_MINIMAL); + break; + } +} + +// Set specific mode +void osd_set_mode(osd_state_t* osd, osd_mode_t mode) { + if (osd == NULL || osd->display == NULL) return; + + Display* dpy = (Display*)osd->display; + Window win = (Window)osd->window; + + osd->mode = mode; + + if (mode == OSD_MODE_HIDDEN) { + osd_hide(osd); + return; + } + + // Update window size based on mode + if (mode == OSD_MODE_MINIMAL) { + osd->width = osd->min_width; + osd->height = osd->min_height; + } else { + osd->width = osd->expanded_width; + osd->height = osd->expanded_height; + } + + XResizeWindow(dpy, win, osd->width, osd->height); + XMapWindow(dpy, win); + XRaiseWindow(dpy, win); + + osd_redraw(osd); +} + +// Record a key action +void osd_record_action(osd_state_t* osd, int button_index, const char* action) { + if (osd == NULL || button_index < 0 || button_index > 18) return; + + // Get next slot in circular buffer + int slot = osd->recent_head; + osd->recent_head = (osd->recent_head + 1) % 10; + if (osd->recent_count < 10) osd->recent_count++; + + // Free old data + if (osd->recent_actions[slot].key_name) free(osd->recent_actions[slot].key_name); + if (osd->recent_actions[slot].action) free(osd->recent_actions[slot].action); + + // Store new action + osd->recent_actions[slot].key_name = strdup(BUTTON_NAMES[button_index]); + osd->recent_actions[slot].action = action ? strdup(action) : strdup("(unknown)"); + osd->recent_actions[slot].timestamp_ms = osd_get_time_ms(); + + // Trigger redraw if visible + if (osd->mode != OSD_MODE_HIDDEN) { + osd_redraw(osd); + } +} + +// Redraw the OSD +void osd_redraw(osd_state_t* osd) { + if (osd == NULL || osd->display == NULL || osd->mode == OSD_MODE_HIDDEN) return; + + Display* dpy = (Display*)osd->display; + Window win = (Window)osd->window; + GC gc = (GC)osd->gc; + + // Calculate alpha value (0-255) + unsigned char alpha = (unsigned char)(osd->opacity * 255); + + // 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 + + // Clear window + XSetForeground(dpy, gc, bg_color); + XFillRectangle(dpy, win, gc, 0, 0, osd->width, osd->height); + + // Draw border + XSetForeground(dpy, gc, accent_color); + XDrawRectangle(dpy, win, gc, 0, 0, osd->width - 1, osd->height - 1); + + // Load font + XFontStruct* font = XLoadQueryFont(dpy, "-misc-fixed-medium-r-*-*-13-*-*-*-*-*-*-*"); + if (font == NULL) { + font = XLoadQueryFont(dpy, "fixed"); + } + if (font) { + XSetFont(dpy, gc, font->fid); + } + + int line_height = 16; + int padding = 10; + int y_offset = padding + line_height; + + // Draw title bar with mode indicator + XSetForeground(dpy, gc, accent_color); + XFillRectangle(dpy, win, gc, 0, 0, osd->width, 25); + + XSetForeground(dpy, gc, fg_color); + const char* title = (osd->mode == OSD_MODE_MINIMAL) ? "KD100 [+]" : "KD100 [-]"; + XDrawString(dpy, win, gc, padding, 17, title, strlen(title)); + + // Draw drag hint + const char* hint = "drag to move"; + XDrawString(dpy, win, gc, osd->width - 85, 17, hint, strlen(hint)); + + y_offset = 35; + + 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 + 5; + + long now = osd_get_time_ms(); + int shown = 0; + + // Show up to 4 recent actions + for (int i = 0; i < osd->recent_count && shown < 4; 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; + + // Format: "B0: ctrl+z" + char line[128]; + snprintf(line, sizeof(line), "%s: %s", action->key_name, action->action); + + // 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); + } + + XDrawString(dpy, win, gc, padding, y_offset, line, strlen(line)); + y_offset += line_height; + shown++; + } + + if (shown == 0) { + XSetForeground(dpy, gc, highlight_color); + XDrawString(dpy, win, gc, padding, y_offset, "(no recent actions)", 19); + } + + } else { + // Expanded mode: show full keyboard layout + XSetForeground(dpy, gc, fg_color); + XDrawString(dpy, win, gc, padding, y_offset, "Keyboard Layout:", 16); + y_offset += line_height + 10; + + // Draw keyboard grid + int key_width = 85; + int key_height = 45; + int grid_padding = 5; + int start_x = padding; + int start_y = y_offset; + + // Draw wheel button (top, spanning) + int wheel_y = start_y; + XSetForeground(dpy, gc, highlight_color); + XFillRectangle(dpy, win, gc, start_x, wheel_y, osd->width - 2 * padding, key_height - 10); + XSetForeground(dpy, gc, fg_color); + XDrawRectangle(dpy, win, gc, start_x, wheel_y, osd->width - 2 * padding - 1, key_height - 11); + + // Wheel button label + const char* wheel_desc = osd->key_descriptions[18] ? osd->key_descriptions[18] : "Wheel Toggle"; + XDrawString(dpy, win, gc, start_x + 5, wheel_y + 22, wheel_desc, strlen(wheel_desc)); + + start_y += key_height; + + // Draw main button grid (4 rows x 4 columns) + for (int row = 1; row < 5; row++) { + for (int col = 0; col < 4; col++) { + int btn = BUTTON_LAYOUT[row][col]; + if (btn < 0) 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 + 5, y + 15, 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 + 5, y + 35, desc, strlen(desc)); + } + } + } + + // Draw bottom row (buttons 16 and 17 have special layout) + int bottom_y = start_y + 4 * (key_height + grid_padding); + + // 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 + 5, bottom_y + 15, "16", 2); + if (osd->key_descriptions[16]) { + XSetForeground(dpy, gc, accent_color); + XDrawString(dpy, win, gc, start_x + 5, bottom_y + 35, osd->key_descriptions[16], + strlen(osd->key_descriptions[16]) > 15 ? 15 : strlen(osd->key_descriptions[16])); + } + + // 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); + XSetForeground(dpy, gc, fg_color); + XDrawRectangle(dpy, win, gc, x17, bottom_y, key_width - 1, key_height - 1); + XDrawString(dpy, win, gc, x17 + 5, bottom_y + 15, "17", 2); + if (osd->key_descriptions[17]) { + XSetForeground(dpy, gc, accent_color); + XDrawString(dpy, win, gc, x17 + 5, bottom_y + 35, osd->key_descriptions[17], + strlen(osd->key_descriptions[17]) > 10 ? 10 : strlen(osd->key_descriptions[17])); + } + } + + if (font) { + XFreeFont(dpy, font); + } + + XFlush(dpy); +} + +// Process X11 events +void osd_update(osd_state_t* osd) { + if (osd == NULL || osd->display == NULL || osd->mode == OSD_MODE_HIDDEN) return; + + Display* dpy = (Display*)osd->display; + Window win = (Window)osd->window; + + while (XPending(dpy)) { + XEvent event; + XNextEvent(dpy, &event); + + switch (event.type) { + case Expose: + if (event.xexpose.count == 0) { + osd_redraw(osd); + } + break; + + case ButtonPress: + if (event.xbutton.button == Button1) { + // Check if click is in title bar (for dragging) + if (event.xbutton.y < 25) { + // Toggle mode on title bar click (except when starting drag) + // Will be determined by button release + osd->dragging = 1; + osd->drag_start_x = event.xbutton.x_root - osd->pos_x; + osd->drag_start_y = event.xbutton.y_root - osd->pos_y; + } + } + break; + + case ButtonRelease: + if (event.xbutton.button == Button1) { + if (osd->dragging) { + // If barely moved, treat as click to toggle + int dx = event.xbutton.x_root - osd->pos_x - osd->drag_start_x; + int dy = event.xbutton.y_root - osd->pos_y - osd->drag_start_y; + if (abs(dx) < 5 && abs(dy) < 5) { + osd_toggle_mode(osd); + } + osd->dragging = 0; + } + } + break; + + case MotionNotify: + if (osd->dragging) { + osd->pos_x = event.xmotion.x_root - osd->drag_start_x; + osd->pos_y = event.xmotion.y_root - osd->drag_start_y; + XMoveWindow(dpy, win, osd->pos_x, osd->pos_y); + } + break; + + case ConfigureNotify: + // Window was moved or resized + break; + } + } + + // Check if we need to clear old actions and redraw + static long last_cleanup = 0; + long now = osd_get_time_ms(); + if (now - last_cleanup > 500) { // Check every 500ms + last_cleanup = now; + + // Check if any visible actions have expired + int need_redraw = 0; + for (int i = 0; i < osd->recent_count; i++) { + int idx = (osd->recent_head - 1 - i + 10) % 10; + long age = now - osd->recent_actions[idx].timestamp_ms; + if (age > osd->display_duration_ms && age < osd->display_duration_ms + 1000) { + need_redraw = 1; + break; + } + } + if (need_redraw) { + osd_redraw(osd); + } + } +} + +// Set OSD position +void osd_set_position(osd_state_t* osd, int x, int y) { + if (osd == NULL) return; + + osd->pos_x = x; + osd->pos_y = y; + + if (osd->display && osd->window) { + Display* dpy = (Display*)osd->display; + Window win = (Window)osd->window; + XMoveWindow(dpy, win, x, y); + XFlush(dpy); + } +} + +// Move OSD by delta +void osd_move(osd_state_t* osd, int dx, int dy) { + osd_set_position(osd, osd->pos_x + dx, osd->pos_y + dy); +} + +// Set key description +void osd_set_key_description(osd_state_t* osd, int button_index, const char* description) { + if (osd == NULL || button_index < 0 || button_index > 18) return; + + if (osd->key_descriptions[button_index]) { + free(osd->key_descriptions[button_index]); + } + osd->key_descriptions[button_index] = description ? strdup(description) : NULL; +} + +// Get key description +const char* osd_get_key_description(osd_state_t* osd, int button_index) { + if (osd == NULL || button_index < 0 || button_index > 18) return NULL; + return osd->key_descriptions[button_index]; +} + +// Clear all descriptions +void osd_clear_descriptions(osd_state_t* osd) { + if (osd == NULL) return; + + for (int i = 0; i < 19; i++) { + if (osd->key_descriptions[i]) { + free(osd->key_descriptions[i]); + osd->key_descriptions[i] = NULL; + } + } +} + +// Set opacity +void osd_set_opacity(osd_state_t* osd, float opacity) { + if (osd == NULL) return; + + if (opacity < 0.0f) opacity = 0.0f; + if (opacity > 1.0f) opacity = 1.0f; + + osd->opacity = opacity; + + if (osd->mode != OSD_MODE_HIDDEN) { + osd_redraw(osd); + } +} + +// Set display duration for actions +void osd_set_display_duration(osd_state_t* osd, int duration_ms) { + if (osd == NULL) return; + osd->display_duration_ms = duration_ms; +} @@ -0,0 +1,89 @@ +#ifndef OSD_H +#define OSD_H + +#include "config.h" + +// OSD display modes +typedef enum { + OSD_MODE_HIDDEN, // OSD not visible + OSD_MODE_MINIMAL, // Small box showing recent commands + OSD_MODE_EXPANDED // Full keyboard layout view +} osd_mode_t; + +// Recent key action for display +typedef struct { + char* key_name; // Display name of the key + char* action; // What action was performed + long timestamp_ms; // When it was pressed +} recent_action_t; + +// OSD state structure +typedef struct { + osd_mode_t mode; // Current display mode + int enabled; // Is OSD feature enabled + int pos_x; // Window X position + int pos_y; // Window Y position + int width; // Current width + int height; // Current height + int min_width; // Minimal mode width + int min_height; // Minimal mode height + int expanded_width; // Expanded mode width + int expanded_height; // Expanded mode height + float opacity; // Background opacity (0.0-1.0) + int display_duration_ms; // How long to show key actions + + // Recent actions buffer (circular) + recent_action_t recent_actions[10]; + int recent_count; + int recent_head; + + // X11 handles (void* to avoid X11 header in .h) + void* display; + void* window; + void* gc; + void* visual; + int screen; + int colormap; + + // Drag state + int dragging; + int drag_start_x; + int drag_start_y; + + // Configuration reference for key names + config_t* config; + + // Key descriptions for the current profile + char* key_descriptions[19]; // Descriptions for buttons 0-18 +} osd_state_t; + +// OSD lifecycle functions +osd_state_t* osd_create(config_t* config); +void osd_destroy(osd_state_t* osd); + +// OSD control functions +int osd_init_display(osd_state_t* osd); +void osd_show(osd_state_t* osd); +void osd_hide(osd_state_t* osd); +void osd_toggle_mode(osd_state_t* osd); // Toggle between minimal and expanded +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_update(osd_state_t* osd); // Process X11 events and redraw if needed +void osd_redraw(osd_state_t* osd); // Force redraw + +// OSD position functions +void osd_set_position(osd_state_t* osd, int x, int y); +void osd_move(osd_state_t* osd, int dx, int dy); + +// Key description functions +void osd_set_key_description(osd_state_t* osd, int button_index, const char* description); +const char* osd_get_key_description(osd_state_t* osd, int button_index); +void osd_clear_descriptions(osd_state_t* osd); + +// Configuration +void osd_set_opacity(osd_state_t* osd, float opacity); +void osd_set_display_duration(osd_state_t* osd, int duration_ms); + +#endif // OSD_H @@ -0,0 +1,556 @@ +#include "profiles.h" +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <pwd.h> +#include <unistd.h> + +// Helper: free profile contents +static void free_profile(profile_t* profile) { + if (profile->name) { free(profile->name); profile->name = NULL; } + if (profile->window_pattern) { free(profile->window_pattern); profile->window_pattern = NULL; } + if (profile->config_file) { free(profile->config_file); profile->config_file = NULL; } + + // Free config if we own it (not the default) + if (profile->config) { + config_destroy(profile->config); + profile->config = NULL; + } + + // Free descriptions + for (int i = 0; i < 19; i++) { + if (profile->key_descriptions[i]) { + free(profile->key_descriptions[i]); + profile->key_descriptions[i] = NULL; + } + } +} + +// Create profile manager +profile_manager_t* profile_manager_create(config_t* default_config) { + profile_manager_t* manager = calloc(1, sizeof(profile_manager_t)); + if (manager == NULL) return NULL; + + manager->default_config = default_config; + manager->profile_count = 0; + manager->active_profile_index = -1; + manager->debug = 0; + manager->window_tracker = NULL; + manager->osd = NULL; + + // Initialize all profiles + for (int i = 0; i < MAX_PROFILES; i++) { + memset(&manager->profiles[i], 0, sizeof(profile_t)); + } + + return manager; +} + +// Destroy profile manager +void profile_manager_destroy(profile_manager_t* manager) { + if (manager == NULL) return; + + // Free all profiles + for (int i = 0; i < manager->profile_count; i++) { + free_profile(&manager->profiles[i]); + } + + // Free window tracker + if (manager->window_tracker) { + window_tracker_destroy(manager->window_tracker); + } + + free(manager); +} + +// Initialize with X11 display +int profile_manager_init(profile_manager_t* manager, void* display, osd_state_t* osd) { + if (manager == NULL) return -1; + + manager->osd = osd; + + // Create window tracker + manager->window_tracker = window_tracker_create(); + if (manager->window_tracker == NULL) { + fprintf(stderr, "Profile manager: Failed to create window tracker\n"); + return -1; + } + + if (window_tracker_init(manager->window_tracker, display) < 0) { + window_tracker_destroy(manager->window_tracker); + manager->window_tracker = NULL; + return -1; + } + + return 0; +} + +// Add a profile +int profile_add(profile_manager_t* manager, const char* name, const char* window_pattern, + const char* config_file, int priority) { + if (manager == NULL || name == NULL || window_pattern == NULL) return -1; + if (manager->profile_count >= MAX_PROFILES) return -1; + + // Check for duplicate name + for (int i = 0; i < manager->profile_count; i++) { + if (manager->profiles[i].name && strcmp(manager->profiles[i].name, name) == 0) { + return -1; // Already exists + } + } + + profile_t* profile = &manager->profiles[manager->profile_count]; + + profile->name = strdup(name); + profile->window_pattern = strdup(window_pattern); + profile->config_file = config_file ? strdup(config_file) : NULL; + profile->priority = priority; + profile->config = NULL; + profile->is_default = 0; + + // Initialize descriptions to NULL + for (int i = 0; i < 19; i++) { + profile->key_descriptions[i] = NULL; + } + + // Load config if specified + if (config_file) { + profile->config = config_create(); + if (profile->config) { + if (config_load(profile->config, config_file, manager->debug) < 0) { + if (manager->debug) { + printf("Profile '%s': Failed to load config '%s', using default\n", + name, config_file); + } + config_destroy(profile->config); + profile->config = NULL; + } + } + } + + manager->profile_count++; + + if (manager->debug) { + printf("Profile added: '%s' (pattern: '%s', priority: %d)\n", + name, window_pattern, priority); + } + + return 0; +} + +// Remove a profile +int profile_remove(profile_manager_t* manager, const char* name) { + if (manager == NULL || name == NULL) return -1; + + for (int i = 0; i < manager->profile_count; i++) { + if (manager->profiles[i].name && strcmp(manager->profiles[i].name, name) == 0) { + // Free this profile + free_profile(&manager->profiles[i]); + + // Shift remaining profiles + for (int j = i; j < manager->profile_count - 1; j++) { + manager->profiles[j] = manager->profiles[j + 1]; + } + + // Clear last slot + memset(&manager->profiles[manager->profile_count - 1], 0, sizeof(profile_t)); + + manager->profile_count--; + + // Adjust active index if needed + if (manager->active_profile_index == i) { + manager->active_profile_index = -1; + } else if (manager->active_profile_index > i) { + manager->active_profile_index--; + } + + return 0; + } + } + + return -1; // Not found +} + +// Get profile by name +profile_t* profile_get(profile_manager_t* manager, const char* name) { + if (manager == NULL || name == NULL) return NULL; + + for (int i = 0; i < manager->profile_count; i++) { + if (manager->profiles[i].name && strcmp(manager->profiles[i].name, name) == 0) { + return &manager->profiles[i]; + } + } + + return NULL; +} + +// Get profile by index +profile_t* profile_get_by_index(profile_manager_t* manager, int index) { + if (manager == NULL || index < 0 || index >= manager->profile_count) return NULL; + return &manager->profiles[index]; +} + +// Set key description for a profile +int profile_set_description(profile_manager_t* manager, const char* profile_name, + int button_index, const char* description) { + if (manager == NULL || profile_name == NULL) return -1; + if (button_index < 0 || button_index > 18) return -1; + + profile_t* profile = profile_get(manager, profile_name); + if (profile == NULL) return -1; + + if (profile->key_descriptions[button_index]) { + free(profile->key_descriptions[button_index]); + } + profile->key_descriptions[button_index] = description ? strdup(description) : NULL; + + return 0; +} + +// Get key description for a profile +const char* profile_get_description(profile_manager_t* manager, const char* profile_name, + int button_index) { + if (manager == NULL || profile_name == NULL) return NULL; + if (button_index < 0 || button_index > 18) return NULL; + + profile_t* profile = profile_get(manager, profile_name); + if (profile == NULL) return NULL; + + return profile->key_descriptions[button_index]; +} + +// Set default profile +int profile_set_default(profile_manager_t* manager, const char* name) { + if (manager == NULL || name == NULL) return -1; + + // Clear existing default + for (int i = 0; i < manager->profile_count; i++) { + manager->profiles[i].is_default = 0; + } + + // Set new default + profile_t* profile = profile_get(manager, name); + if (profile) { + profile->is_default = 1; + return 0; + } + + return -1; +} + +// Update profile manager - check active window and switch if needed +int profile_manager_update(profile_manager_t* manager) { + if (manager == NULL || manager->window_tracker == NULL) return -1; + + // Update window tracker + int window_changed = window_tracker_update(manager->window_tracker); + + if (window_changed <= 0 && manager->active_profile_index >= 0) { + return 0; // Same window, same profile + } + + const window_info_t* window = window_tracker_get_current(manager->window_tracker); + + if (manager->debug) { + printf("Window changed: title='%s' class='%s' instance='%s'\n", + window->title ? window->title : "(null)", + window->class_name ? window->class_name : "(null)", + window->instance_name ? window->instance_name : "(null)"); + } + + // Find matching profile (highest priority) + int best_index = -1; + int best_priority = -999999; + int default_index = -1; + + for (int i = 0; i < manager->profile_count; i++) { + profile_t* p = &manager->profiles[i]; + + if (p->is_default) { + default_index = i; + } + + if (p->window_pattern && window_matches(window, p->window_pattern)) { + if (p->priority > best_priority) { + best_priority = p->priority; + best_index = i; + } + } + } + + // Use default profile if no match + if (best_index < 0) { + best_index = default_index; + } + + // Check if profile changed + if (best_index == manager->active_profile_index) { + return 0; // Same profile + } + + // Switch to new profile + manager->active_profile_index = best_index; + + if (manager->debug) { + if (best_index >= 0) { + printf("Profile switched: '%s'\n", manager->profiles[best_index].name); + } else { + printf("Profile switched: (default config)\n"); + } + } + + // Update OSD with new profile's descriptions + if (manager->osd && best_index >= 0) { + profile_t* profile = &manager->profiles[best_index]; + osd_clear_descriptions(manager->osd); + for (int i = 0; i < 19; i++) { + if (profile->key_descriptions[i]) { + osd_set_key_description(manager->osd, i, profile->key_descriptions[i]); + } + } + } + + return 1; // Profile changed +} + +// Get active profile +profile_t* profile_manager_get_active(profile_manager_t* manager) { + if (manager == NULL || manager->active_profile_index < 0) return NULL; + return &manager->profiles[manager->active_profile_index]; +} + +// Get active config +config_t* profile_manager_get_config(profile_manager_t* manager) { + if (manager == NULL) return NULL; + + profile_t* active = profile_manager_get_active(manager); + if (active && active->config) { + return active->config; + } + + return manager->default_config; +} + +// Manual profile switch by name +int profile_manager_switch(profile_manager_t* manager, const char* name) { + if (manager == NULL || name == NULL) return -1; + + for (int i = 0; i < manager->profile_count; i++) { + if (manager->profiles[i].name && strcmp(manager->profiles[i].name, name) == 0) { + return profile_manager_switch_by_index(manager, i); + } + } + + return -1; +} + +// Manual profile switch by index +int profile_manager_switch_by_index(profile_manager_t* manager, int index) { + if (manager == NULL || index < 0 || index >= manager->profile_count) return -1; + + manager->active_profile_index = index; + + // Update OSD descriptions + if (manager->osd) { + profile_t* profile = &manager->profiles[index]; + osd_clear_descriptions(manager->osd); + for (int i = 0; i < 19; i++) { + if (profile->key_descriptions[i]) { + osd_set_key_description(manager->osd, i, profile->key_descriptions[i]); + } + } + } + + return 0; +} + +// Set debug level +void profile_manager_set_debug(profile_manager_t* manager, int level) { + if (manager) manager->debug = level; +} + +// Print profile information +void profile_manager_print(const profile_manager_t* manager) { + if (manager == NULL) return; + + printf("\n=== Profile Configuration ===\n"); + printf("Total profiles: %d\n", manager->profile_count); + printf("Active profile: %d", manager->active_profile_index); + if (manager->active_profile_index >= 0) { + printf(" ('%s')", manager->profiles[manager->active_profile_index].name); + } + printf("\n\n"); + + for (int i = 0; i < manager->profile_count; i++) { + const profile_t* p = &manager->profiles[i]; + printf("Profile %d: '%s'%s\n", i, p->name, p->is_default ? " [DEFAULT]" : ""); + printf(" Pattern: '%s'\n", p->window_pattern); + printf(" Priority: %d\n", p->priority); + printf(" Config: %s\n", p->config_file ? p->config_file : "(default)"); + + // Print non-null descriptions + int has_desc = 0; + for (int j = 0; j < 19; j++) { + if (p->key_descriptions[j]) { + if (!has_desc) { + printf(" Descriptions:\n"); + has_desc = 1; + } + printf(" Button %d: %s\n", j, p->key_descriptions[j]); + } + } + printf("\n"); + } +} + +// Load profiles from config file +int profile_manager_load(profile_manager_t* manager, const char* filename) { + if (manager == NULL || filename == NULL) return -1; + + FILE* f = fopen(filename, "r"); + if (f == NULL) { + // Try home directory + char* home = getpwuid(getuid())->pw_dir; + char* config_dir = "/.config/KD100/"; + char temp[strlen(home) + strlen(config_dir) + strlen(filename) + 1]; + + strcpy(temp, home); + strcat(temp, config_dir); + strcat(temp, filename); + + f = fopen(temp, "r"); + if (f == NULL) { + if (manager->debug) { + printf("Profile config not found: %s\n", filename); + } + return -1; + } + } + + char line[512]; + char* current_profile = NULL; + char* current_pattern = NULL; + char* current_config = NULL; + int current_priority = 0; + int current_is_default = 0; + char* current_descriptions[19] = {NULL}; + + while (fgets(line, sizeof(line), f) != NULL) { + // Remove newline + line[strcspn(line, "\n")] = 0; + + // Skip comments and empty lines + if (strstr(line, "//") == line || strlen(line) == 0) continue; + + // Skip leading whitespace + char* ptr = line; + while (*ptr == ' ' || *ptr == '\t') ptr++; + + // Parse profile name + if (strncasecmp(ptr, "profile:", 8) == 0) { + // Save previous profile if exists + if (current_profile && current_pattern) { + if (profile_add(manager, current_profile, current_pattern, + current_config, current_priority) == 0) { + if (current_is_default) { + profile_set_default(manager, current_profile); + } + for (int i = 0; i < 19; i++) { + if (current_descriptions[i]) { + profile_set_description(manager, current_profile, i, current_descriptions[i]); + free(current_descriptions[i]); + current_descriptions[i] = NULL; + } + } + } + } + + // Start new profile + if (current_profile) free(current_profile); + if (current_pattern) free(current_pattern); + if (current_config) free(current_config); + + char* value = ptr + 8; + while (*value == ' ') value++; + current_profile = strdup(value); + current_pattern = NULL; + current_config = NULL; + current_priority = 0; + current_is_default = 0; + continue; + } + + // Parse pattern + if (strncasecmp(ptr, "pattern:", 8) == 0 && current_profile) { + char* value = ptr + 8; + while (*value == ' ') value++; + if (current_pattern) free(current_pattern); + current_pattern = strdup(value); + continue; + } + + // Parse config file + if (strncasecmp(ptr, "config:", 7) == 0 && current_profile) { + char* value = ptr + 7; + while (*value == ' ') value++; + if (current_config) free(current_config); + current_config = strdup(value); + continue; + } + + // Parse priority + if (strncasecmp(ptr, "priority:", 9) == 0 && current_profile) { + char* value = ptr + 9; + while (*value == ' ') value++; + current_priority = atoi(value); + continue; + } + + // Parse default flag + if (strncasecmp(ptr, "default:", 8) == 0 && current_profile) { + char* value = ptr + 8; + while (*value == ' ') value++; + current_is_default = (strcasecmp(value, "true") == 0 || strcmp(value, "1") == 0); + continue; + } + + // Parse button descriptions (description_0, description_1, etc.) + if (strncasecmp(ptr, "description_", 12) == 0 && current_profile) { + char* num_str = ptr + 12; + char* colon = strchr(num_str, ':'); + if (colon) { + *colon = '\0'; + int btn = atoi(num_str); + if (btn >= 0 && btn <= 18) { + char* value = colon + 1; + while (*value == ' ') value++; + if (current_descriptions[btn]) free(current_descriptions[btn]); + current_descriptions[btn] = strdup(value); + } + } + continue; + } + } + + // Save last profile + if (current_profile && current_pattern) { + if (profile_add(manager, current_profile, current_pattern, + current_config, current_priority) == 0) { + if (current_is_default) { + profile_set_default(manager, current_profile); + } + for (int i = 0; i < 19; i++) { + if (current_descriptions[i]) { + profile_set_description(manager, current_profile, i, current_descriptions[i]); + free(current_descriptions[i]); + } + } + } + } + + if (current_profile) free(current_profile); + if (current_pattern) free(current_pattern); + if (current_config) free(current_config); + + fclose(f); + return 0; +} @@ -0,0 +1,85 @@ +#ifndef PROFILES_H +#define PROFILES_H + +#include "config.h" +#include "window.h" +#include "osd.h" + +// Maximum profiles supported +#define MAX_PROFILES 32 + +// Profile definition +typedef struct { + char* name; // Profile name + char* window_pattern; // Window title/class pattern (e.g., "krita*", "*photoshop*") + int priority; // Higher priority matched first (default: 0) + char* config_file; // Config file for this profile (NULL = use main config) + config_t* config; // Loaded configuration (or NULL to use default) + char* key_descriptions[19]; // Per-button descriptions for this profile + int is_default; // Is this the default/fallback profile? +} profile_t; + +// Profile manager state +typedef struct { + profile_t profiles[MAX_PROFILES]; + int profile_count; + int active_profile_index; // Currently active profile (-1 = none) + config_t* default_config; // Default configuration (fallback) + window_tracker_t* window_tracker; + osd_state_t* osd; // Reference to OSD for updating descriptions + int debug; // Debug output level +} profile_manager_t; + +// Lifecycle functions +profile_manager_t* profile_manager_create(config_t* default_config); +void profile_manager_destroy(profile_manager_t* manager); + +// Initialize with shared X11 display +int profile_manager_init(profile_manager_t* manager, void* display, osd_state_t* osd); + +// Profile management +int profile_add(profile_manager_t* manager, const char* name, const char* window_pattern, + const char* config_file, int priority); +int profile_remove(profile_manager_t* manager, const char* name); +profile_t* profile_get(profile_manager_t* manager, const char* name); +profile_t* profile_get_by_index(profile_manager_t* manager, int index); + +// Key descriptions for a profile +int profile_set_description(profile_manager_t* manager, const char* profile_name, + int button_index, const char* description); +const char* profile_get_description(profile_manager_t* manager, const char* profile_name, + int button_index); + +// Set default profile +int profile_set_default(profile_manager_t* manager, const char* name); + +// Update function - check active window and switch profile if needed +// Returns 1 if profile changed, 0 if same, -1 on error +int profile_manager_update(profile_manager_t* manager); + +// Get current active profile +profile_t* profile_manager_get_active(profile_manager_t* manager); + +// Get current active config (either from active profile or default) +config_t* profile_manager_get_config(profile_manager_t* manager); + +// Manual profile switching +int profile_manager_switch(profile_manager_t* manager, const char* name); +int profile_manager_switch_by_index(profile_manager_t* manager, int index); + +// Debug +void profile_manager_set_debug(profile_manager_t* manager, int level); +void profile_manager_print(const profile_manager_t* manager); + +// Load profiles from config file +// Format in config: +// profile: <name> +// pattern: <window_pattern> +// config: <config_file> (optional) +// priority: <number> (optional, default 0) +// description_0: <desc> (optional) +// description_1: <desc> +// ... +int profile_manager_load(profile_manager_t* manager, const char* filename); + +#endif // PROFILES_H @@ -0,0 +1,259 @@ +#include "window.h" +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <ctype.h> +#include <X11/Xlib.h> +#include <X11/Xatom.h> +#include <X11/Xutil.h> + +// Helper: lowercase string copy +static char* str_tolower(const char* str) { + if (str == NULL) return NULL; + + char* lower = strdup(str); + if (lower == NULL) return NULL; + + for (char* p = lower; *p; p++) { + *p = tolower((unsigned char)*p); + } + return lower; +} + +// Helper: free window info contents +static void free_window_info(window_info_t* info) { + if (info->title) { free(info->title); info->title = NULL; } + if (info->class_name) { free(info->class_name); info->class_name = NULL; } + if (info->instance_name) { free(info->instance_name); info->instance_name = NULL; } + info->window_id = 0; +} + +// Create window tracker +window_tracker_t* window_tracker_create(void) { + window_tracker_t* tracker = calloc(1, sizeof(window_tracker_t)); + if (tracker == NULL) return NULL; + + tracker->initialized = 0; + tracker->current.title = NULL; + tracker->current.class_name = NULL; + tracker->current.instance_name = NULL; + tracker->current.window_id = 0; + + return tracker; +} + +// Destroy window tracker +void window_tracker_destroy(window_tracker_t* tracker) { + if (tracker == NULL) return; + + free_window_info(&tracker->current); + + // Only close display if we created it (display != NULL and initialized by us) + // We'll track this by not closing if it was passed in externally + // For safety, we won't close it - the caller should manage the display + + free(tracker); +} + +// Initialize tracker +int window_tracker_init(window_tracker_t* tracker, void* existing_display) { + if (tracker == NULL) return -1; + + if (existing_display) { + tracker->display = existing_display; + } else { + tracker->display = XOpenDisplay(NULL); + if (tracker->display == NULL) { + fprintf(stderr, "Window tracker: Cannot open X display\n"); + return -1; + } + } + + tracker->initialized = 1; + return 0; +} + +// Get active window +static Window get_active_window(Display* dpy) { + Atom actual_type; + int actual_format; + unsigned long nitems, bytes_after; + unsigned char* data = NULL; + Window active_window = None; + + Atom net_active_window = XInternAtom(dpy, "_NET_ACTIVE_WINDOW", False); + + if (XGetWindowProperty(dpy, DefaultRootWindow(dpy), net_active_window, + 0, 1, False, XA_WINDOW, + &actual_type, &actual_format, &nitems, &bytes_after, &data) == Success) { + if (data && nitems > 0) { + active_window = *(Window*)data; + XFree(data); + } + } + + return active_window; +} + +// Get window title +static char* get_window_title(Display* dpy, Window win) { + Atom actual_type; + int actual_format; + unsigned long nitems, bytes_after; + unsigned char* data = NULL; + char* title = NULL; + + // Try _NET_WM_NAME first (UTF-8) + Atom net_wm_name = XInternAtom(dpy, "_NET_WM_NAME", False); + Atom utf8_string = XInternAtom(dpy, "UTF8_STRING", False); + + if (XGetWindowProperty(dpy, win, net_wm_name, 0, 1024, False, utf8_string, + &actual_type, &actual_format, &nitems, &bytes_after, &data) == Success) { + if (data && nitems > 0) { + title = strdup((char*)data); + XFree(data); + return title; + } + if (data) XFree(data); + } + + // Fall back to WM_NAME + XTextProperty text_prop; + if (XGetWMName(dpy, win, &text_prop) && text_prop.value) { + title = strdup((char*)text_prop.value); + XFree(text_prop.value); + } + + return title; +} + +// Get window class +static void get_window_class(Display* dpy, Window win, char** class_name, char** instance_name) { + XClassHint class_hint; + + *class_name = NULL; + *instance_name = NULL; + + if (XGetClassHint(dpy, win, &class_hint)) { + if (class_hint.res_class) { + *class_name = strdup(class_hint.res_class); + XFree(class_hint.res_class); + } + if (class_hint.res_name) { + *instance_name = strdup(class_hint.res_name); + XFree(class_hint.res_name); + } + } +} + +// Update window tracker +int window_tracker_update(window_tracker_t* tracker) { + if (tracker == NULL || !tracker->initialized || tracker->display == NULL) return -1; + + Display* dpy = (Display*)tracker->display; + Window active = get_active_window(dpy); + + if (active == None) { + // No active window + if (tracker->current.window_id != 0) { + free_window_info(&tracker->current); + return 1; // Changed (to nothing) + } + return 0; // Same (still nothing) + } + + // Check if window changed + if (active == tracker->current.window_id) { + return 0; // Same window + } + + // Window changed - get new info + free_window_info(&tracker->current); + + tracker->current.window_id = active; + tracker->current.title = get_window_title(dpy, active); + get_window_class(dpy, active, &tracker->current.class_name, &tracker->current.instance_name); + + return 1; // Changed +} + +// Get current window info +const window_info_t* window_tracker_get_current(const window_tracker_t* tracker) { + if (tracker == NULL) return NULL; + return &tracker->current; +} + +// Wildcard pattern matching (case-insensitive) +// Supports * (any sequence) and ? (single char) +int window_match_pattern(const char* pattern, const char* text) { + if (pattern == NULL || text == NULL) return 0; + + // Convert both to lowercase for case-insensitive matching + char* pattern_lower = str_tolower(pattern); + char* text_lower = str_tolower(text); + + if (pattern_lower == NULL || text_lower == NULL) { + if (pattern_lower) free(pattern_lower); + if (text_lower) free(text_lower); + return 0; + } + + const char* p = pattern_lower; + const char* t = text_lower; + const char* star_p = NULL; + const char* star_t = NULL; + + while (*t) { + if (*p == '?' || *p == *t) { + // Match single character + p++; + t++; + } else if (*p == '*') { + // Remember position for backtracking + star_p = p++; + star_t = t; + } else if (star_p) { + // Backtrack: try matching one more character with * + p = star_p + 1; + t = ++star_t; + } else { + // No match + free(pattern_lower); + free(text_lower); + return 0; + } + } + + // Skip trailing * + while (*p == '*') p++; + + int result = (*p == '\0'); + + free(pattern_lower); + free(text_lower); + + return result; +} + +// Match window against pattern +// Tries to match against title, class_name, and instance_name +int window_matches(const window_info_t* window, const char* pattern) { + if (window == NULL || pattern == NULL) return 0; + + // Try matching title + if (window->title && window_match_pattern(pattern, window->title)) { + return 1; + } + + // Try matching class name + if (window->class_name && window_match_pattern(pattern, window->class_name)) { + return 1; + } + + // Try matching instance name + if (window->instance_name && window_match_pattern(pattern, window->instance_name)) { + return 1; + } + + return 0; +} @@ -0,0 +1,43 @@ +#ifndef WINDOW_H +#define WINDOW_H + +// Active window information +typedef struct { + char* title; // Window title (WM_NAME or _NET_WM_NAME) + char* class_name; // Window class (WM_CLASS) + char* instance_name; // Window instance name + unsigned long window_id; // X11 window ID +} window_info_t; + +// Window tracking state +typedef struct { + void* display; // X11 Display* + window_info_t current; // Current active window + int initialized; // Is the tracker initialized +} window_tracker_t; + +// Lifecycle functions +window_tracker_t* window_tracker_create(void); +void window_tracker_destroy(window_tracker_t* tracker); + +// Initialize with existing display or create new one +int window_tracker_init(window_tracker_t* tracker, void* existing_display); + +// Get current active window info (updates internal state) +// Returns 1 if window changed, 0 if same, -1 on error +int window_tracker_update(window_tracker_t* tracker); + +// Get current window info (without updating) +const window_info_t* window_tracker_get_current(const window_tracker_t* tracker); + +// Wildcard matching function +// Pattern supports: +// * - matches any sequence of characters +// ? - matches any single character +// Case-insensitive by default +int window_match_pattern(const char* pattern, const char* text); + +// Match against window title or class +int window_matches(const window_info_t* window, const char* pattern); + +#endif // WINDOW_H