← KD100

Add on-screen display (OSD) and profile system

11032fe8ebb45dfd3c5b18f476097dffec71b58e · Claude · Feb 2, 2026 20:05

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
diff --git a/Makefile b/Makefile
index c0cc5bc..34a730b 100644
--- a/Makefile
+++ b/Makefile
@@ -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
 
diff --git a/default.cfg b/default.cfg
index c9a51f9..d5b443d 100644
--- a/default.cfg
+++ b/default.cfg
@@ -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:
diff --git a/profiles.cfg b/profiles.cfg
new file mode 100644
index 0000000..8ffe19a
--- /dev/null
+++ b/profiles.cfg
@@ -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>
diff --git a/src/config.c b/src/config.c
index 8c06d10..cc3d4db 100644
--- a/src/config.c
+++ b/src/config.c
@@ -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");
 }
diff --git a/src/config.h b/src/config.h
index 91cc4f1..ec0d5fb 100644
--- a/src/config.h
+++ b/src/config.h
@@ -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
diff --git a/src/device.c b/src/device.c
index dc87f3f..9e92234 100644
--- a/src/device.c
+++ b/src/device.c
@@ -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);
+    }
 }
diff --git a/src/main.c b/src/main.c
index 5c08a48..41895a0 100644
--- a/src/main.c
+++ b/src/main.c
@@ -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);
diff --git a/src/osd.c b/src/osd.c
new file mode 100644
index 0000000..b1c143a
--- /dev/null
+++ b/src/osd.c
@@ -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;
+}
diff --git a/src/osd.h b/src/osd.h
new file mode 100644
index 0000000..88dfa22
--- /dev/null
+++ b/src/osd.h
@@ -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
diff --git a/src/profiles.c b/src/profiles.c
new file mode 100644
index 0000000..1c50820
--- /dev/null
+++ b/src/profiles.c
@@ -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;
+}
diff --git a/src/profiles.h b/src/profiles.h
new file mode 100644
index 0000000..af0931d
--- /dev/null
+++ b/src/profiles.h
@@ -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
diff --git a/src/window.c b/src/window.c
new file mode 100644
index 0000000..8e0aedf
--- /dev/null
+++ b/src/window.c
@@ -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;
+}
diff --git a/src/window.h b/src/window.h
new file mode 100644
index 0000000..fa47f6c
--- /dev/null
+++ b/src/window.h
@@ -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