/* * DWN - Desktop Window Manager * retoor * Keyboard shortcut handling implementation */ #include "keys.h" #include "client.h" #include "workspace.h" #include "config.h" #include "util.h" #include "ai.h" #include "notifications.h" #include "news.h" #include "applauncher.h" #include #include #include #include static bool super_pressed = false; static bool super_used_in_combo = false; /* Forward declarations for key callbacks */ void key_spawn_terminal(void); void key_spawn_launcher(void); void key_spawn_file_manager(void); void key_spawn_browser(void); void key_close_window(void); void key_quit_dwn(void); void key_cycle_layout(void); void key_toggle_floating(void); void key_toggle_fullscreen(void); void key_toggle_maximize(void); void key_focus_next(void); void key_focus_prev(void); void key_workspace_next(void); void key_workspace_prev(void); void key_workspace_1(void); void key_workspace_2(void); void key_workspace_3(void); void key_workspace_4(void); void key_workspace_5(void); void key_workspace_6(void); void key_workspace_7(void); void key_workspace_8(void); void key_workspace_9(void); void key_move_to_workspace_1(void); void key_move_to_workspace_2(void); void key_move_to_workspace_3(void); void key_move_to_workspace_4(void); void key_move_to_workspace_5(void); void key_move_to_workspace_6(void); void key_move_to_workspace_7(void); void key_move_to_workspace_8(void); void key_move_to_workspace_9(void); void key_increase_master(void); void key_decrease_master(void); void key_increase_master_count(void); void key_decrease_master_count(void); void key_toggle_ai(void); void key_ai_command(void); void key_exa_search(void); void key_news_next(void); void key_news_prev(void); void key_news_open(void); void key_show_shortcuts(void); void key_start_tutorial(void); void key_screenshot(void); /* Key bindings storage */ static KeyBinding bindings[MAX_KEYBINDINGS]; static int binding_count = 0; /* ========== Tutorial System ========== */ typedef struct { const char *title; const char *instruction; const char *hint; unsigned int modifiers; KeySym keysym; } TutorialStep; static const TutorialStep tutorial_steps[] = { { "Welcome to DWN!", "This tutorial teaches all keyboard shortcuts.\n\n" "Press Super+T to continue...", "(Super = Windows/Meta key)", MOD_SUPER, XK_t }, /* === Applications === */ { "1/20: Open Terminal", "The terminal is your command center.\n\n" "Press: Ctrl + Alt + T", "Hold Ctrl and Alt, then press T", MOD_CTRL | MOD_ALT, XK_t }, { "2/20: App Launcher", "Launch any application by name.\n\n" "Press: Alt + F2", "Type app name and press Enter", MOD_ALT, XK_F2 }, { "3/20: File Manager", "Open the file manager.\n\n" "Press: Super + E", "", MOD_SUPER, XK_e }, { "4/20: Web Browser", "Open your default web browser.\n\n" "Press: Super + B", "", MOD_SUPER, XK_b }, /* === Window Management === */ { "5/20: Switch Windows", "Cycle through open windows.\n\n" "Press: Alt + Tab", "Hold Alt, press Tab repeatedly", MOD_ALT, XK_Tab }, { "6/20: Close Window", "Close the focused window.\n\n" "Press: Alt + F4", "", MOD_ALT, XK_F4 }, { "7/20: Toggle Maximize", "Maximize or restore a window.\n\n" "Press: Alt + F10", "", MOD_ALT, XK_F10 }, { "8/20: Toggle Fullscreen", "Make a window fullscreen.\n\n" "Press: Alt + F11", "Press again to exit", MOD_ALT, XK_F11 }, { "9/20: Toggle Floating", "Make window float above tiled windows.\n\n" "Press: Super + F9", "", MOD_SUPER, XK_F9 }, /* === Workspaces === */ { "10/20: Next Workspace", "Switch to the next virtual desktop.\n\n" "Press: Ctrl + Alt + Right", "", MOD_CTRL | MOD_ALT, XK_Right }, { "11/20: Previous Workspace", "Switch to the previous workspace.\n\n" "Press: Ctrl + Alt + Left", "", MOD_CTRL | MOD_ALT, XK_Left }, { "12/20: Go to Workspace", "Jump directly to workspace 1.\n\n" "Press: F1", "Use F1-F9 for workspaces 1-9", 0, XK_F1 }, /* === Layout === */ { "13/20: Cycle Layout", "Switch between tiling, floating, monocle.\n\n" "Press: Super + Space", "Try it multiple times!", MOD_SUPER, XK_space }, { "14/20: Expand Master", "Make the master area larger.\n\n" "Press: Super + L", "", MOD_SUPER, XK_l }, { "15/20: Shrink Master", "Make the master area smaller.\n\n" "Press: Super + H", "", MOD_SUPER, XK_h }, { "16/20: Add to Master", "Increase windows in master area.\n\n" "Press: Super + I", "", MOD_SUPER, XK_i }, /* === AI Features === */ { "17/20: AI Context", "Show AI analysis of your current task.\n\n" "Press: Super + A", "Requires OPENROUTER_API_KEY", MOD_SUPER, XK_a }, { "18/20: AI Command", "Ask AI to run commands for you.\n\n" "Press: Super + Shift + A", "Requires OPENROUTER_API_KEY", MOD_SUPER | MOD_SHIFT, XK_a }, { "19/20: Exa Search", "Semantic web search.\n\n" "Press: Super + Shift + E", "Requires EXA_API_KEY", MOD_SUPER | MOD_SHIFT, XK_e }, /* === Help & System === */ { "20/20: Show Shortcuts", "Display all keyboard shortcuts.\n\n" "Press: Super + S", "Shows complete reference", MOD_SUPER, XK_s }, { "Tutorial Complete!", "You've learned all DWN shortcuts!\n\n" "Super+S: Show all shortcuts\n" "Super+T: Restart this tutorial\n\n" "Press: Super + Backspace to quit DWN", "Congratulations!", MOD_SUPER, XK_BackSpace } }; #define TUTORIAL_STEPS (sizeof(tutorial_steps) / sizeof(tutorial_steps[0])) static bool tutorial_active = false; static int tutorial_current_step = 0; bool tutorial_is_active(void) { return tutorial_active; } void tutorial_start(void) { tutorial_active = true; tutorial_current_step = 0; const TutorialStep *step = &tutorial_steps[0]; char msg[512]; snprintf(msg, sizeof(msg), "%s\n\n%s", step->instruction, step->hint); notification_show("DWN Tutorial", step->title, msg, NULL, 0); /* No timeout */ } void tutorial_stop(void) { tutorial_active = false; tutorial_current_step = 0; notification_show("DWN Tutorial", "Tutorial Stopped", "Press Super+T to restart anytime.", NULL, 3000); } void tutorial_next_step(void) { tutorial_current_step++; if (tutorial_current_step >= (int)TUTORIAL_STEPS) { tutorial_active = false; tutorial_current_step = 0; return; } const TutorialStep *step = &tutorial_steps[tutorial_current_step]; char msg[512]; snprintf(msg, sizeof(msg), "%s\n\n%s", step->instruction, step->hint); notification_show("DWN Tutorial", step->title, msg, NULL, 0); } void tutorial_check_key(unsigned int modifiers, KeySym keysym) { if (!tutorial_active) { return; } const TutorialStep *step = &tutorial_steps[tutorial_current_step]; /* Check if the pressed key matches the expected key */ if (modifiers == step->modifiers && keysym == step->keysym) { /* Correct key! Move to next step */ tutorial_next_step(); } } /* ========== Initialization ========== */ void keys_init(void) { binding_count = 0; memset(bindings, 0, sizeof(bindings)); keys_setup_defaults(); keys_grab_all(); LOG_INFO("Keyboard shortcuts initialized (%d bindings)", binding_count); } void keys_cleanup(void) { keys_ungrab_all(); keys_clear_all(); } void keys_grab_all(void) { if (dwn == NULL || dwn->display == NULL) { return; } Display *dpy = dwn->display; Window root = dwn->root; /* Ungrab first to avoid errors */ XUngrabKey(dpy, AnyKey, AnyModifier, root); /* Grab all registered bindings */ for (int i = 0; i < binding_count; i++) { KeyCode code = XKeysymToKeycode(dpy, bindings[i].keysym); if (code == 0) { LOG_WARN("Could not get keycode for keysym %lu", bindings[i].keysym); continue; } /* Grab with and without NumLock/CapsLock */ unsigned int modifiers[] = { bindings[i].modifiers, bindings[i].modifiers | Mod2Mask, /* NumLock */ bindings[i].modifiers | LockMask, /* CapsLock */ bindings[i].modifiers | Mod2Mask | LockMask }; for (size_t j = 0; j < sizeof(modifiers) / sizeof(modifiers[0]); j++) { XGrabKey(dpy, code, modifiers[j], root, True, GrabModeAsync, GrabModeAsync); } } KeyCode super_l = XKeysymToKeycode(dpy, XK_Super_L); KeyCode super_r = XKeysymToKeycode(dpy, XK_Super_R); unsigned int lock_mods[] = { 0, Mod2Mask, LockMask, Mod2Mask | LockMask }; for (size_t i = 0; i < sizeof(lock_mods) / sizeof(lock_mods[0]); i++) { if (super_l) XGrabKey(dpy, super_l, lock_mods[i], root, True, GrabModeAsync, GrabModeAsync); if (super_r) XGrabKey(dpy, super_r, lock_mods[i], root, True, GrabModeAsync, GrabModeAsync); } } void keys_ungrab_all(void) { if (dwn == NULL || dwn->display == NULL) { return; } XUngrabKey(dwn->display, AnyKey, AnyModifier, dwn->root); } /* ========== Key binding registration ========== */ void keys_bind(unsigned int modifiers, KeySym keysym, KeyCallback callback, const char *description) { if (binding_count >= MAX_KEYBINDINGS) { LOG_WARN("Maximum key bindings reached"); return; } bindings[binding_count].modifiers = modifiers; bindings[binding_count].keysym = keysym; bindings[binding_count].callback = callback; bindings[binding_count].description = description; binding_count++; } void keys_unbind(unsigned int modifiers, KeySym keysym) { for (int i = 0; i < binding_count; i++) { if (bindings[i].modifiers == modifiers && bindings[i].keysym == keysym) { /* Shift remaining bindings */ memmove(&bindings[i], &bindings[i + 1], (binding_count - i - 1) * sizeof(KeyBinding)); binding_count--; return; } } } void keys_clear_all(void) { binding_count = 0; memset(bindings, 0, sizeof(bindings)); } /* ========== Key event handling ========== */ void keys_handle_press(XKeyEvent *ev) { if (ev == NULL || dwn == NULL || dwn->display == NULL) { return; } KeySym keysym = XLookupKeysym(ev, 0); if (keysym == XK_Super_L || keysym == XK_Super_R) { super_pressed = true; super_used_in_combo = false; return; } if (super_pressed && (ev->state & Mod4Mask)) { super_used_in_combo = true; } /* Clean modifiers (remove NumLock, CapsLock) */ unsigned int clean_mask = ev->state & ~(Mod2Mask | LockMask); /* Check tutorial progress */ if (tutorial_is_active()) { tutorial_check_key(clean_mask, keysym); } for (int i = 0; i < binding_count; i++) { if (bindings[i].keysym == keysym && bindings[i].modifiers == clean_mask) { if (bindings[i].callback != NULL) { LOG_DEBUG("Key binding triggered: %s", bindings[i].description); bindings[i].callback(); } return; } } } void keys_handle_release(XKeyEvent *ev) { if (ev == NULL || dwn == NULL || dwn->display == NULL) { return; } KeySym keysym = XLookupKeysym(ev, 0); if (keysym == XK_Super_L || keysym == XK_Super_R) { if (super_pressed && !super_used_in_combo) { key_spawn_launcher(); } super_pressed = false; super_used_in_combo = false; } } /* ========== Default key bindings (XFCE-style) ========== */ void keys_setup_defaults(void) { /* ===== XFCE-style shortcuts ===== */ /* Terminal: Ctrl+Alt+T (XFCE default) */ keys_bind(MOD_CTRL | MOD_ALT, XK_t, key_spawn_terminal, "Spawn terminal"); /* Application finder: Alt+F2 (XFCE default) */ keys_bind(MOD_ALT, XK_F2, key_spawn_launcher, "Application finder"); /* File manager: Super+E (XFCE default) */ keys_bind(MOD_SUPER, XK_e, key_spawn_file_manager, "File manager"); /* Browser: Super+B */ keys_bind(MOD_SUPER, XK_b, key_spawn_browser, "Web browser"); /* Close window: Alt+F4 (XFCE default) */ keys_bind(MOD_ALT, XK_F4, key_close_window, "Close window"); /* Maximize toggle: Alt+F10 (XFCE default) */ keys_bind(MOD_ALT, XK_F10, key_toggle_maximize, "Toggle maximize"); /* Fullscreen: Alt+F11 (XFCE default) */ keys_bind(MOD_ALT, XK_F11, key_toggle_fullscreen, "Toggle fullscreen"); /* Cycle windows: Alt+Tab (XFCE default) */ keys_bind(MOD_ALT, XK_Tab, key_focus_next, "Cycle windows"); keys_bind(MOD_ALT | MOD_SHIFT, XK_Tab, key_focus_prev, "Cycle windows reverse"); /* Toggle floating: Super+F9 */ keys_bind(MOD_SUPER, XK_F9, key_toggle_floating, "Toggle floating"); /* Next/Previous workspace: Ctrl+Alt+Right/Left (XFCE default) */ keys_bind(MOD_CTRL | MOD_ALT, XK_Right, key_workspace_next, "Next workspace"); keys_bind(MOD_CTRL | MOD_ALT, XK_Left, key_workspace_prev, "Previous workspace"); /* Switch to workspace: F1-F9 */ keys_bind(0, XK_F1, key_workspace_1, "Switch to workspace 1"); keys_bind(0, XK_F2, key_workspace_2, "Switch to workspace 2"); keys_bind(0, XK_F3, key_workspace_3, "Switch to workspace 3"); keys_bind(0, XK_F4, key_workspace_4, "Switch to workspace 4"); keys_bind(0, XK_F5, key_workspace_5, "Switch to workspace 5"); keys_bind(0, XK_F6, key_workspace_6, "Switch to workspace 6"); keys_bind(0, XK_F7, key_workspace_7, "Switch to workspace 7"); keys_bind(0, XK_F8, key_workspace_8, "Switch to workspace 8"); keys_bind(0, XK_F9, key_workspace_9, "Switch to workspace 9"); /* Move to workspace: Shift+F1-F9 */ keys_bind(MOD_SHIFT, XK_F1, key_move_to_workspace_1, "Move to workspace 1"); keys_bind(MOD_SHIFT, XK_F2, key_move_to_workspace_2, "Move to workspace 2"); keys_bind(MOD_SHIFT, XK_F3, key_move_to_workspace_3, "Move to workspace 3"); keys_bind(MOD_SHIFT, XK_F4, key_move_to_workspace_4, "Move to workspace 4"); keys_bind(MOD_SHIFT, XK_F5, key_move_to_workspace_5, "Move to workspace 5"); keys_bind(MOD_SHIFT, XK_F6, key_move_to_workspace_6, "Move to workspace 6"); keys_bind(MOD_SHIFT, XK_F7, key_move_to_workspace_7, "Move to workspace 7"); keys_bind(MOD_SHIFT, XK_F8, key_move_to_workspace_8, "Move to workspace 8"); keys_bind(MOD_SHIFT, XK_F9, key_move_to_workspace_9, "Move to workspace 9"); /* ===== DWN-specific shortcuts (all use Super key) ===== */ /* Quit DWN: Super+BackSpace */ keys_bind(MOD_SUPER, XK_BackSpace, key_quit_dwn, "Quit DWN"); /* Cycle layout mode: Super+Space */ keys_bind(MOD_SUPER, XK_space, key_cycle_layout, "Cycle layout"); /* Master area adjustments: Super+H/L/I/D */ keys_bind(MOD_SUPER, XK_l, key_increase_master, "Increase master ratio"); keys_bind(MOD_SUPER, XK_h, key_decrease_master, "Decrease master ratio"); keys_bind(MOD_SUPER, XK_i, key_increase_master_count, "Increase master count"); keys_bind(MOD_SUPER, XK_d, key_decrease_master_count, "Decrease master count"); /* AI: Super+A */ keys_bind(MOD_SUPER, XK_a, key_toggle_ai, "Toggle AI context"); keys_bind(MOD_SUPER | MOD_SHIFT, XK_a, key_ai_command, "AI command"); /* Exa semantic search: Super+Shift+E */ keys_bind(MOD_SUPER | MOD_SHIFT, XK_e, key_exa_search, "Exa web search"); /* Show shortcuts: Super+S */ keys_bind(MOD_SUPER, XK_s, key_show_shortcuts, "Show shortcuts"); /* Tutorial: Super+T */ keys_bind(MOD_SUPER, XK_t, key_start_tutorial, "Start tutorial"); /* ===== News ticker navigation ===== */ /* Super+Down: Next news article */ keys_bind(MOD_SUPER, XK_Down, key_news_next, "Next news article"); /* Super+Up: Previous news article */ keys_bind(MOD_SUPER, XK_Up, key_news_prev, "Previous news article"); /* Super+Return: Open current news article */ keys_bind(MOD_SUPER, XK_Return, key_news_open, "Open news article"); /* Screenshot: Print Screen (XFCE default) */ keys_bind(0, XK_Print, key_screenshot, "Take screenshot"); } /* ========== Key binding callbacks ========== */ void key_spawn_terminal(void) { const char *terminal = config_get_terminal(); spawn_async(terminal); } void key_spawn_launcher(void) { applauncher_show(); } void key_spawn_file_manager(void) { if (dwn != NULL && dwn->config != NULL) { spawn_async(dwn->config->file_manager); } else { spawn_async("thunar"); } } void key_spawn_browser(void) { spawn_async("xdg-open http://"); } void key_close_window(void) { Workspace *ws = workspace_get_current(); if (ws != NULL && ws->focused != NULL) { client_close(ws->focused); } } void key_quit_dwn(void) { LOG_INFO("Quit requested via keyboard shortcut"); dwn_quit(); } void key_cycle_layout(void) { if (dwn == NULL) { return; } workspace_cycle_layout(dwn->current_workspace); } void key_toggle_floating(void) { Workspace *ws = workspace_get_current(); if (ws != NULL && ws->focused != NULL) { client_toggle_floating(ws->focused); workspace_arrange_current(); } } void key_toggle_fullscreen(void) { Workspace *ws = workspace_get_current(); if (ws != NULL && ws->focused != NULL) { client_toggle_fullscreen(ws->focused); } } void key_toggle_maximize(void) { Workspace *ws = workspace_get_current(); if (ws != NULL && ws->focused != NULL) { /* Toggle between maximized and normal state */ /* For now, use fullscreen as maximize equivalent */ client_toggle_fullscreen(ws->focused); } } void key_focus_next(void) { workspace_focus_next(); } void key_focus_prev(void) { workspace_focus_prev(); } void key_workspace_next(void) { if (dwn == NULL) { return; } int next = dwn->current_workspace + 1; if (next >= MAX_WORKSPACES) { next = 0; /* Wrap around */ } workspace_switch(next); } void key_workspace_prev(void) { if (dwn == NULL) { return; } int prev = dwn->current_workspace - 1; if (prev < 0) { prev = MAX_WORKSPACES - 1; /* Wrap around */ } workspace_switch(prev); } /* Workspace switching */ void key_workspace_1(void) { workspace_switch(0); } void key_workspace_2(void) { workspace_switch(1); } void key_workspace_3(void) { workspace_switch(2); } void key_workspace_4(void) { workspace_switch(3); } void key_workspace_5(void) { workspace_switch(4); } void key_workspace_6(void) { workspace_switch(5); } void key_workspace_7(void) { workspace_switch(6); } void key_workspace_8(void) { workspace_switch(7); } void key_workspace_9(void) { workspace_switch(8); } /* Move to workspace */ static void move_focused_to_workspace(int ws) { Workspace *current = workspace_get_current(); if (current != NULL && current->focused != NULL) { workspace_move_client(current->focused, ws); } } void key_move_to_workspace_1(void) { move_focused_to_workspace(0); } void key_move_to_workspace_2(void) { move_focused_to_workspace(1); } void key_move_to_workspace_3(void) { move_focused_to_workspace(2); } void key_move_to_workspace_4(void) { move_focused_to_workspace(3); } void key_move_to_workspace_5(void) { move_focused_to_workspace(4); } void key_move_to_workspace_6(void) { move_focused_to_workspace(5); } void key_move_to_workspace_7(void) { move_focused_to_workspace(6); } void key_move_to_workspace_8(void) { move_focused_to_workspace(7); } void key_move_to_workspace_9(void) { move_focused_to_workspace(8); } /* Master area adjustments */ void key_increase_master(void) { if (dwn == NULL) { return; } workspace_adjust_master_ratio(dwn->current_workspace, 0.05f); } void key_decrease_master(void) { if (dwn == NULL) { return; } workspace_adjust_master_ratio(dwn->current_workspace, -0.05f); } void key_increase_master_count(void) { if (dwn == NULL) { return; } workspace_adjust_master_count(dwn->current_workspace, 1); } void key_decrease_master_count(void) { if (dwn == NULL) { return; } workspace_adjust_master_count(dwn->current_workspace, -1); } /* AI functions */ void key_toggle_ai(void) { if (dwn == NULL) { return; } if (!dwn->ai_enabled) { notification_show("DWN AI", "AI Disabled", "Set OPENROUTER_API_KEY to enable AI features", NULL, 3000); return; } /* Update and show AI context */ ai_update_context(); const char *task = ai_analyze_task(); const char *suggestion = ai_suggest_window(); char body[512]; Workspace *ws = workspace_get_current(); if (ws != NULL && ws->focused != NULL) { snprintf(body, sizeof(body), "Current task: %s\nFocused: %s\n%s", task ? task : "Unknown", ws->focused->title[0] ? ws->focused->title : "(unnamed)", suggestion ? suggestion : "Press Alt+A to ask AI for help"); } else { snprintf(body, sizeof(body), "No window focused.\nPress Alt+A to ask AI for help"); } notification_show("DWN AI", "Context Analysis", body, NULL, 4000); } void key_ai_command(void) { if (dwn == NULL) { return; } if (dwn->ai_enabled) { ai_show_command_palette(); } else { LOG_INFO("AI not enabled (no API key)"); } } void key_exa_search(void) { exa_show_app_launcher(); } void key_show_shortcuts(void) { const char *shortcuts = "=== Applications ===\n" "Ctrl+Alt+T Terminal\n" "Super / Alt+F2 App launcher\n" "Super+E File manager\n" "Super+B Web browser\n" "Print Screenshot\n" "\n" "=== Window Management ===\n" "Alt+F4 Close window\n" "Alt+Tab Next window\n" "Alt+Shift+Tab Previous window\n" "Alt+F10 Toggle maximize\n" "Alt+F11 Toggle fullscreen\n" "Super+F9 Toggle floating\n" "\n" "=== Workspaces ===\n" "F1-F9 Switch to workspace\n" "Shift+F1-F9 Move window to workspace\n" "Ctrl+Alt+Right Next workspace\n" "Ctrl+Alt+Left Previous workspace\n" "\n" "=== Layout Control ===\n" "Super+Space Cycle layout (tile/float/mono)\n" "Super+H Shrink master area\n" "Super+L Expand master area\n" "Super+I Add to master\n" "Super+D Remove from master\n" "\n" "=== AI Features ===\n" "Super+A Show AI context\n" "Super+Shift+A AI command palette\n" "Super+Shift+E Exa semantic search\n" "\n" "=== Help & System ===\n" "Super+S Show shortcuts (this)\n" "Super+T Interactive tutorial\n" "Super+Backspace Quit DWN"; notification_show("DWN Shortcuts", "Complete Reference", shortcuts, NULL, 0); } void key_start_tutorial(void) { if (tutorial_is_active()) { /* If already in tutorial, this acts as "next" */ tutorial_next_step(); } else { tutorial_start(); } } /* ========== News ticker callbacks ========== */ void key_news_next(void) { news_next_article(); } void key_news_prev(void) { news_prev_article(); } void key_news_open(void) { news_open_current(); } void key_screenshot(void) { spawn_async("xfce4-screenshooter"); }