diff --git a/bin/dwn b/bin/dwn index 1a1b4c0..9f55182 100755 Binary files a/bin/dwn and b/bin/dwn differ diff --git a/build/ai.o b/build/ai.o index b176314..e1eb3b9 100644 Binary files a/build/ai.o and b/build/ai.o differ diff --git a/build/atoms.o b/build/atoms.o index 92f6d58..b600c15 100644 Binary files a/build/atoms.o and b/build/atoms.o differ diff --git a/build/autostart.o b/build/autostart.o index b31f73a..f49b273 100644 Binary files a/build/autostart.o and b/build/autostart.o differ diff --git a/build/client.d b/build/client.d index 45f07aa..5c1a493 100644 --- a/build/client.d +++ b/build/client.d @@ -18,7 +18,7 @@ build/client.o: src/client.c include/client.h include/dwn.h \ /usr/include/dbus-1.0/dbus/dbus-server.h \ /usr/include/dbus-1.0/dbus/dbus-signature.h \ /usr/include/dbus-1.0/dbus/dbus-syntax.h \ - /usr/include/dbus-1.0/dbus/dbus-threads.h + /usr/include/dbus-1.0/dbus/dbus-threads.h include/layout.h include/client.h: include/dwn.h: include/atoms.h: @@ -45,3 +45,4 @@ include/notifications.h: /usr/include/dbus-1.0/dbus/dbus-signature.h: /usr/include/dbus-1.0/dbus/dbus-syntax.h: /usr/include/dbus-1.0/dbus/dbus-threads.h: +include/layout.h: diff --git a/build/client.o b/build/client.o index 7f284f0..a1527ff 100644 Binary files a/build/client.o and b/build/client.o differ diff --git a/build/config.o b/build/config.o index ddc2281..ff378ee 100644 Binary files a/build/config.o and b/build/config.o differ diff --git a/build/decorations.o b/build/decorations.o index 47ec16a..ccfd53d 100644 Binary files a/build/decorations.o and b/build/decorations.o differ diff --git a/build/demo.o b/build/demo.o index 6ff6f6b..42c967e 100644 Binary files a/build/demo.o and b/build/demo.o differ diff --git a/build/keys.o b/build/keys.o index 4e9c12b..2b7143a 100644 Binary files a/build/keys.o and b/build/keys.o differ diff --git a/build/layout.o b/build/layout.o index f1dda5a..b04a777 100644 Binary files a/build/layout.o and b/build/layout.o differ diff --git a/build/main.o b/build/main.o index 261813d..cbd778c 100644 Binary files a/build/main.o and b/build/main.o differ diff --git a/build/news.o b/build/news.o index a414091..2b187e2 100644 Binary files a/build/news.o and b/build/news.o differ diff --git a/build/notifications.o b/build/notifications.o index 4dc1e10..8496e2f 100644 Binary files a/build/notifications.o and b/build/notifications.o differ diff --git a/build/panel.o b/build/panel.o index 6cc1a11..dcd7b5c 100644 Binary files a/build/panel.o and b/build/panel.o differ diff --git a/build/systray.o b/build/systray.o index 8b8e820..6c30bb1 100644 Binary files a/build/systray.o and b/build/systray.o differ diff --git a/build/util.o b/build/util.o index 5f6514e..36c03a1 100644 Binary files a/build/util.o and b/build/util.o differ diff --git a/build/workspace.o b/build/workspace.o index 3a772ae..b69a31a 100644 Binary files a/build/workspace.o and b/build/workspace.o differ diff --git a/include/client.h b/include/client.h index 6fa1a81..6e79f02 100644 --- a/include/client.h +++ b/include/client.h @@ -35,11 +35,14 @@ void client_update_title(Client *client); void client_update_class(Client *client); void client_set_fullscreen(Client *client, bool fullscreen); void client_toggle_fullscreen(Client *client); +void client_set_maximize(Client *client, bool maximized); +void client_toggle_maximize(Client *client); void client_set_floating(Client *client, bool floating); void client_toggle_floating(Client *client); bool client_is_floating(Client *client); bool client_is_fullscreen(Client *client); +bool client_is_maximized(Client *client); bool client_is_minimized(Client *client); bool client_is_dialog(Window window); bool client_is_dock(Window window); diff --git a/include/config.h b/include/config.h index b7d4ba6..2ae34a5 100644 --- a/include/config.h +++ b/include/config.h @@ -31,6 +31,7 @@ struct Config { char launcher[128]; char file_manager[128]; FocusMode focus_mode; + int focus_follow_delay_ms; bool show_decorations; int border_width; @@ -58,6 +59,10 @@ struct Config { bool autostart_enabled; bool autostart_xdg; char autostart_path[512]; + + int demo_step_delay_ms; + int demo_ai_timeout_ms; + int demo_window_timeout_ms; }; Config *config_create(void); diff --git a/include/demo.h b/include/demo.h index d2de378..faa5557 100644 --- a/include/demo.h +++ b/include/demo.h @@ -23,6 +23,14 @@ typedef enum { DEMO_COMPLETE } DemoPhase; +typedef enum { + DEMO_WAIT_NONE, + DEMO_WAIT_TIME, + DEMO_WAIT_WINDOW_SPAWN, + DEMO_WAIT_AI_RESPONSE, + DEMO_WAIT_EXA_RESPONSE +} DemoWaitCondition; + void demo_init(void); void demo_cleanup(void); void demo_start(void); diff --git a/include/dwn.h b/include/dwn.h index acddbda..df7983e 100644 --- a/include/dwn.h +++ b/include/dwn.h @@ -58,7 +58,8 @@ typedef enum { CLIENT_FULLSCREEN = (1 << 1), CLIENT_URGENT = (1 << 2), CLIENT_MINIMIZED = (1 << 3), - CLIENT_STICKY = (1 << 4) + CLIENT_STICKY = (1 << 4), + CLIENT_MAXIMIZED = (1 << 5) } ClientFlags; typedef struct Client Client; @@ -81,6 +82,8 @@ struct Client { char class[64]; Client *next; Client *prev; + Client *mru_next; + Client *mru_prev; }; struct Monitor { @@ -93,6 +96,8 @@ struct Monitor { struct Workspace { Client *clients; Client *focused; + Client *mru_head; + Client *mru_tail; LayoutType layout; float master_ratio; int master_count; @@ -142,6 +147,9 @@ typedef struct { int drag_orig_x, drag_orig_y; int drag_orig_w, drag_orig_h; bool resizing; + + Client *pending_focus_client; + long pending_focus_time; } DWNState; extern DWNState *dwn; diff --git a/include/workspace.h b/include/workspace.h index 8960908..80f8cd4 100644 --- a/include/workspace.h +++ b/include/workspace.h @@ -50,4 +50,8 @@ void workspace_focus_next(void); void workspace_focus_prev(void); void workspace_focus_master(void); +void workspace_mru_push(int workspace, Client *client); +void workspace_mru_remove(int workspace, Client *client); +Client *workspace_mru_get_previous(int workspace, Client *current); + #endif diff --git a/site/configuration.html b/site/configuration.html index 58daee2..b19b4f0 100644 --- a/site/configuration.html +++ b/site/configuration.html @@ -93,6 +93,11 @@ click click or follow (sloppy focus) + + focus_follow_delay + 100 + Delay in ms before focus switches in follow mode (0-1000) + decorations true @@ -109,7 +114,8 @@ terminal = alacritty launcher = rofi -show run file_manager = nautilus -focus_mode = click +focus_mode = follow +focus_follow_delay = 100 decorations = true

[appearance] - Visual Settings

@@ -432,6 +438,42 @@ path = ~/.config/dwn/autostart.d

  • ~/.config/autostart/ - User XDG autostart entries
  • ~/.config/dwn/autostart.d/ - DWN-specific symlinks and scripts
  • +

    [demo] - Demo Mode

    +

    + Configure demo mode timing. The demo showcases DWN features including live AI and search functionality. +

    +
    + + + + + + + + + + + + + + + + + + + + + +
    OptionDescription
    step_delayTime between demo steps in milliseconds (1000-30000, default: 4000)
    ai_timeoutTimeout for AI/Exa API responses in milliseconds (5000-60000, default: 15000)
    window_timeoutTimeout for window spawn operations in milliseconds (1000-30000, default: 5000)
    +
    +
    + Example + +
    +
    [demo]
    +step_delay = 4000
    +ai_timeout = 15000
    +window_timeout = 5000

    Complete Configuration Example

    ~/.config/dwn/config @@ -444,6 +486,7 @@ terminal = alacritty launcher = rofi -show drun file_manager = thunar focus_mode = click +focus_follow_delay = 100 decorations = true [appearance] border_width = 2 @@ -478,7 +521,11 @@ model = google/gemini-2.0-flash-exp:free [autostart] enabled = true xdg_autostart = true -path = ~/.config/dwn/autostart.d +path = ~/.config/dwn/autostart.d +[demo] +step_delay = 4000 +ai_timeout = 15000 +window_timeout = 5000
    diff --git a/src/client.c b/src/client.c index eeba94d..fde5c0d 100644 --- a/src/client.c +++ b/src/client.c @@ -11,6 +11,7 @@ #include "workspace.h" #include "decorations.h" #include "notifications.h" +#include "layout.h" #include #include @@ -43,6 +44,8 @@ Client *client_create(Window window) client->workspace = dwn->current_workspace; client->next = NULL; client->prev = NULL; + client->mru_next = NULL; + client->mru_prev = NULL; XWindowAttributes wa; int orig_width = 640, orig_height = 480; @@ -242,6 +245,9 @@ void client_unmanage(Client *client) LOG_DEBUG("Unmanaging window: %lu", client->window); + client_sync_log("client_unmanage: remove from MRU"); + workspace_mru_remove(client->workspace, client); + client_sync_log("client_unmanage: remove from workspace"); workspace_remove_client(client->workspace, client); @@ -258,14 +264,22 @@ void client_unmanage(Client *client) client_destroy(client); client_sync_log("client_unmanage: focus next"); + XGrabServer(dwn->display); + Workspace *ws = workspace_get_current(); if (ws != NULL && ws->focused == NULL) { - Client *next = workspace_get_first_client(dwn->current_workspace); + Client *next = workspace_mru_get_previous(dwn->current_workspace, NULL); if (next != NULL) { client_focus(next); + } else { + XSetInputFocus(dwn->display, dwn->root, RevertToPointerRoot, CurrentTime); + atoms_set_active_window(None); } } + XUngrabServer(dwn->display); + XSync(dwn->display, False); + client_sync_log("client_unmanage: DONE"); } @@ -336,6 +350,7 @@ void client_focus(Client *client) if (ws != NULL) { ws->focused = client; + workspace_mru_push(client->workspace, client); } client_sync_log("client_focus: raising"); @@ -672,6 +687,80 @@ void client_toggle_floating(Client *client) client_set_floating(client, !(client->flags & CLIENT_FLOATING)); } +bool client_is_maximized(Client *client) +{ + return client != NULL && (client->flags & CLIENT_MAXIMIZED); +} + +void client_set_maximize(Client *client, bool maximized) +{ + if (client == NULL || dwn == NULL || dwn->display == NULL) { + return; + } + + if (client->window == None) { + return; + } + + XWindowAttributes wa; + if (!XGetWindowAttributes(dwn->display, client->window, &wa)) { + LOG_WARN("client_set_maximize: window %lu no longer exists", client->window); + return; + } + + if (maximized) { + if (!(client->flags & CLIENT_MAXIMIZED)) { + client->old_x = client->x; + client->old_y = client->y; + client->old_width = client->width; + client->old_height = client->height; + } + + client->flags |= CLIENT_MAXIMIZED; + client->flags |= CLIENT_FLOATING; + + atoms_update_wm_state(client->window, ewmh.NET_WM_STATE_MAXIMIZED_VERT, true); + atoms_update_wm_state(client->window, ewmh.NET_WM_STATE_MAXIMIZED_HORZ, true); + + int area_x, area_y, area_width, area_height; + layout_get_usable_area(&area_x, &area_y, &area_width, &area_height); + + int gap = config_get_gap(); + int title_height = config_get_title_height(); + int border = client->border_width; + + client->x = area_x + gap + border; + client->y = area_y + gap + title_height + border; + client->width = area_width - 2 * gap - 2 * border; + client->height = area_height - 2 * gap - title_height - 2 * border; + + client_configure(client); + decorations_render(client, true); + client_raise(client); + } else { + client->flags &= ~CLIENT_MAXIMIZED; + + atoms_update_wm_state(client->window, ewmh.NET_WM_STATE_MAXIMIZED_VERT, false); + atoms_update_wm_state(client->window, ewmh.NET_WM_STATE_MAXIMIZED_HORZ, false); + + client->x = client->old_x; + client->y = client->old_y; + client->width = client->old_width; + client->height = client->old_height; + + client_configure(client); + decorations_render(client, true); + } +} + +void client_toggle_maximize(Client *client) +{ + if (client == NULL) { + return; + } + client_set_maximize(client, !(client->flags & CLIENT_MAXIMIZED)); +} + bool client_is_floating(Client *client) { diff --git a/src/config.c b/src/config.c index 22cedb4..71b46a6 100644 --- a/src/config.c +++ b/src/config.c @@ -53,6 +53,7 @@ void config_set_defaults(Config *cfg) strncpy(cfg->launcher, "dmenu_run", sizeof(cfg->launcher) - 1); strncpy(cfg->file_manager, "thunar", sizeof(cfg->file_manager) - 1); cfg->focus_mode = FOCUS_CLICK; + cfg->focus_follow_delay_ms = 100; cfg->show_decorations = true; cfg->border_width = DEFAULT_BORDER_WIDTH; @@ -83,6 +84,10 @@ void config_set_defaults(Config *cfg) strncpy(cfg->autostart_path, autostart_path, sizeof(cfg->autostart_path) - 1); dwn_free(autostart_path); } + + cfg->demo_step_delay_ms = 4000; + cfg->demo_ai_timeout_ms = 15000; + cfg->demo_window_timeout_ms = 5000; } @@ -110,6 +115,9 @@ static void handle_config_entry(const char *section, const char *key, } else { cfg->focus_mode = FOCUS_CLICK; } + } else if (strcmp(key, "focus_follow_delay") == 0) { + int val = atoi(value); + cfg->focus_follow_delay_ms = (val >= 0 && val <= 1000) ? val : 100; } else if (strcmp(key, "decorations") == 0) { cfg->show_decorations = (strcmp(value, "true") == 0 || strcmp(value, "1") == 0); } @@ -172,6 +180,17 @@ static void handle_config_entry(const char *section, const char *key, dwn_free(expanded); } } + } else if (strcmp(section, "demo") == 0) { + if (strcmp(key, "step_delay") == 0) { + int val = atoi(value); + cfg->demo_step_delay_ms = (val >= 1000 && val <= 30000) ? val : 4000; + } else if (strcmp(key, "ai_timeout") == 0) { + int val = atoi(value); + cfg->demo_ai_timeout_ms = (val >= 5000 && val <= 60000) ? val : 15000; + } else if (strcmp(key, "window_timeout") == 0) { + int val = atoi(value); + cfg->demo_window_timeout_ms = (val >= 1000 && val <= 30000) ? val : 5000; + } } } diff --git a/src/demo.c b/src/demo.c index ff54213..d881ddd 100644 --- a/src/demo.c +++ b/src/demo.c @@ -19,8 +19,6 @@ #include -#define DEMO_STEP_DELAY 3000 - typedef struct { bool active; DemoPhase phase; @@ -30,6 +28,18 @@ typedef struct { uint32_t current_notification; Window demo_window; int demo_workspace_start; + LayoutType original_layout; + + DemoWaitCondition wait_condition; + long wait_timeout; + long wait_started; + + AIRequest *pending_ai_request; + ExaRequest *pending_exa_request; + bool ai_demo_completed; + bool exa_demo_completed; + char ai_response_buffer[1024]; + char exa_response_buffer[1024]; } DemoState; static DemoState demo = {0}; @@ -42,6 +52,239 @@ static void demo_notify(const char *title, const char *body, int timeout) demo.current_notification = notification_show("DWN Demo", title, body, NULL, timeout); } +static int demo_get_step_delay(void) +{ + if (dwn != NULL && dwn->config != NULL) { + return dwn->config->demo_step_delay_ms; + } + return 4000; +} + +static int demo_get_ai_timeout(void) +{ + if (dwn != NULL && dwn->config != NULL) { + return dwn->config->demo_ai_timeout_ms; + } + return 15000; +} + +static void demo_wait_for(DemoWaitCondition condition, int timeout_ms) +{ + demo.wait_condition = condition; + demo.wait_timeout = timeout_ms; + demo.wait_started = get_time_ms(); +} + +static bool demo_check_wait_complete(void) +{ + if (demo.wait_condition == DEMO_WAIT_NONE) { + return true; + } + + long elapsed = get_time_ms() - demo.wait_started; + + switch (demo.wait_condition) { + case DEMO_WAIT_NONE: + return true; + + case DEMO_WAIT_TIME: + if (elapsed >= demo.wait_timeout) { + demo.wait_condition = DEMO_WAIT_NONE; + return true; + } + return false; + + case DEMO_WAIT_WINDOW_SPAWN: + if (demo.demo_window != None) { + Client *c = client_find_by_window(demo.demo_window); + if (c != NULL) { + demo.wait_condition = DEMO_WAIT_NONE; + return true; + } + } + if (elapsed >= demo.wait_timeout) { + demo.wait_condition = DEMO_WAIT_NONE; + return true; + } + return false; + + case DEMO_WAIT_AI_RESPONSE: + if (demo.ai_demo_completed) { + demo.wait_condition = DEMO_WAIT_NONE; + return true; + } + if (elapsed >= demo.wait_timeout) { + demo_notify("AI Timeout", "AI request timed out. Continuing demo...", 2000); + demo.wait_condition = DEMO_WAIT_NONE; + if (demo.pending_ai_request != NULL) { + ai_cancel_request(demo.pending_ai_request); + demo.pending_ai_request = NULL; + } + return true; + } + return false; + + case DEMO_WAIT_EXA_RESPONSE: + if (demo.exa_demo_completed) { + demo.wait_condition = DEMO_WAIT_NONE; + return true; + } + if (elapsed >= demo.wait_timeout) { + demo_notify("Search Timeout", "Exa search timed out. Continuing demo...", 2000); + demo.wait_condition = DEMO_WAIT_NONE; + demo.pending_exa_request = NULL; + return true; + } + return false; + } + + return true; +} + +static void demo_ai_callback(AIRequest *req) +{ + if (req == NULL || !demo.active) { + return; + } + + demo.pending_ai_request = NULL; + demo.ai_demo_completed = true; + + if (req->state == AI_STATE_COMPLETED && req->response != NULL) { + size_t resp_len = strlen(req->response); + snprintf(demo.ai_response_buffer, sizeof(demo.ai_response_buffer), + "%s", req->response); + + char display_buf[512]; + snprintf(display_buf, sizeof(display_buf), + "AI Response:\n\n%.400s%s", + demo.ai_response_buffer, + resp_len > 400 ? "..." : ""); + demo_notify("AI Query Complete", display_buf, 6000); + } else { + snprintf(demo.ai_response_buffer, sizeof(demo.ai_response_buffer), + "Error: Failed to get response"); + demo_notify("AI Error", "Failed to get AI response", 3000); + } +} + +static void demo_exa_callback(ExaRequest *req) +{ + if (req == NULL || !demo.active) { + return; + } + + demo.pending_exa_request = NULL; + demo.exa_demo_completed = true; + + if (req->state == AI_STATE_COMPLETED && req->result_count > 0) { + char results_buf[800]; + size_t offset = 0; + + offset += snprintf(results_buf + offset, sizeof(results_buf) - offset, + "Found %d results:\n\n", req->result_count); + + for (int i = 0; i < req->result_count && i < 3; i++) { + offset += snprintf(results_buf + offset, sizeof(results_buf) - offset, + "%d. %s\n", i + 1, req->results[i].title); + if (offset >= sizeof(results_buf) - 50) break; + } + + if (req->result_count > 3) { + snprintf(results_buf + offset, sizeof(results_buf) - offset, + "\n... and %d more", req->result_count - 3); + } + + snprintf(demo.exa_response_buffer, sizeof(demo.exa_response_buffer), + "%s", results_buf); + demo_notify("Exa Search Complete", results_buf, 6000); + } else { + snprintf(demo.exa_response_buffer, sizeof(demo.exa_response_buffer), + "No results found"); + demo_notify("Exa Search", "No results found for query", 3000); + } +} + +static void demo_execute_snap_left(void) +{ + Workspace *ws = workspace_get_current(); + if (ws != NULL && ws->focused != NULL) { + key_snap_left(); + demo_notify("Snap Left", "Window snapped to left half (50% width)", 2000); + } +} + +static void demo_execute_snap_right(void) +{ + Workspace *ws = workspace_get_current(); + if (ws != NULL && ws->focused != NULL) { + key_snap_right(); + demo_notify("Snap Right", "Window snapped to right half (50% width)", 2000); + } +} + +static void demo_execute_ai_query(void) +{ + if (!ai_is_available()) { + demo.ai_demo_completed = true; + demo_notify("AI Unavailable", + "AI is not configured.\n\n" + "Set OPENROUTER_API_KEY to enable.", 3000); + return; + } + + demo.ai_demo_completed = false; + demo.ai_response_buffer[0] = '\0'; + + ai_update_context(); + + Workspace *ws = workspace_get_current(); + char prompt[512]; + + if (ws != NULL && ws->focused != NULL) { + snprintf(prompt, sizeof(prompt), + "Describe what I'm working on based on this context: " + "Current window: %s, Window class: %s. " + "Keep the response brief (2-3 sentences).", + ws->focused->title, ws->focused->class); + } else { + snprintf(prompt, sizeof(prompt), + "I'm using a Linux window manager called DWN. " + "Describe what a typical desktop workspace looks like. " + "Keep the response brief (2-3 sentences)."); + } + + demo_notify("AI Query", "Asking AI: \"Describe my current workspace\"\n\nWaiting for response...", 0); + demo.pending_ai_request = ai_send_request(prompt, demo_ai_callback); + + if (demo.pending_ai_request == NULL) { + demo.ai_demo_completed = true; + demo_notify("AI Error", "Failed to send AI request", 3000); + } +} + +static void demo_execute_exa_search(void) +{ + if (!exa_is_available()) { + demo.exa_demo_completed = true; + demo_notify("Exa Unavailable", + "Exa search is not configured.\n\n" + "Set EXA_API_KEY to enable semantic search.", 3000); + return; + } + + demo.exa_demo_completed = false; + demo.exa_response_buffer[0] = '\0'; + + demo_notify("Exa Search", "Searching: \"What does molodetz mean?\"\n\nWaiting for results...", 0); + demo.pending_exa_request = exa_search("What does molodetz mean?", demo_exa_callback); + + if (demo.pending_exa_request == NULL) { + demo.exa_demo_completed = true; + demo_notify("Exa Error", "Failed to send search request", 3000); + } +} + static void demo_next_phase(void) { demo.phase++; @@ -56,13 +299,12 @@ static void demo_next_step(void) demo.step_start_time = get_time_ms(); } -static long time_in_step(void) -{ - return get_time_ms() - demo.step_start_time; -} - static void demo_phase_intro(void) { + if (!demo_check_wait_complete()) { + return; + } + switch (demo.step) { case 0: demo_notify("Welcome to DWN", @@ -74,12 +316,11 @@ static void demo_phase_intro(void) "- System tray and notifications\n\n" "This demo will showcase all features.\n" "Press Super+Shift+D to stop at any time.", 0); + demo_wait_for(DEMO_WAIT_TIME, demo_get_step_delay() + 2000); demo_next_step(); break; case 1: - if (time_in_step() > 5000) { - demo_next_phase(); - } + demo_next_phase(); break; } } @@ -89,6 +330,10 @@ static void demo_phase_window_mgmt(void) Client *c; Workspace *ws; + if (!demo_check_wait_complete()) { + return; + } + switch (demo.step) { case 0: demo_notify("Window Management", @@ -99,65 +344,65 @@ static void demo_phase_window_mgmt(void) "- Alt+F11: Fullscreen\n" "- Alt+Tab: Cycle windows\n\n" "Let's open a terminal...", 0); + demo_wait_for(DEMO_WAIT_TIME, demo_get_step_delay()); demo_next_step(); break; case 1: - if (time_in_step() > 3000) { - spawn_async("xterm -title 'Demo Terminal' -e 'echo DWN Demo Terminal; sleep 30' &"); - demo_next_step(); - } + spawn_async("xterm -title 'Demo Terminal' -e 'echo DWN Demo Terminal; sleep 60' &"); + demo_wait_for(DEMO_WAIT_TIME, 2000); + demo_next_step(); break; case 2: - if (time_in_step() > 2000) { - demo_notify("Window Created", - "A terminal window was spawned.\n\n" - "Windows can be moved by dragging the title bar\n" - "or resized by dragging edges.\n\n" - "Let's try minimizing...", 0); - demo_next_step(); - } + demo_notify("Window Created", + "A terminal window was spawned.\n\n" + "Windows can be moved by dragging the title bar\n" + "or resized by dragging edges.\n\n" + "Let's try minimizing...", 0); + demo_wait_for(DEMO_WAIT_TIME, demo_get_step_delay()); + demo_next_step(); break; case 3: - if (time_in_step() > 3000) { - ws = workspace_get_current(); - if (ws != NULL && ws->focused != NULL) { - demo.demo_window = ws->focused->window; - client_minimize(ws->focused); - demo_notify("Window Minimized", - "The window is now minimized.\n\n" - "Look at the taskbar - minimized windows\n" - "appear in [brackets] with a gray background.\n\n" - "Click it or press Alt+F9 to restore.", 0); - } - demo_next_step(); + ws = workspace_get_current(); + if (ws != NULL && ws->focused != NULL) { + demo.demo_window = ws->focused->window; + client_minimize(ws->focused); + demo_notify("Window Minimized", + "The window is now minimized.\n\n" + "Look at the taskbar - minimized windows\n" + "appear in [brackets] with a gray background.\n\n" + "Click it or press Alt+F9 to restore.", 0); } + demo_wait_for(DEMO_WAIT_TIME, demo_get_step_delay()); + demo_next_step(); break; case 4: - if (time_in_step() > 4000) { - c = client_find_by_window(demo.demo_window); - if (c != NULL) { - client_restore(c); - demo_notify("Window Restored", - "The window is restored from the taskbar.\n\n" - "This is how minimize/restore works in DWN.", 0); - } - demo_next_step(); + c = client_find_by_window(demo.demo_window); + if (c != NULL) { + client_restore(c); + demo_notify("Window Restored", + "The window is restored from the taskbar.\n\n" + "This is how minimize/restore works in DWN.", 0); } + demo_wait_for(DEMO_WAIT_TIME, demo_get_step_delay()); + demo_next_step(); break; case 5: - if (time_in_step() > 3000) { - c = client_find_by_window(demo.demo_window); - if (c != NULL) { - client_close(c); - } - demo_next_phase(); + c = client_find_by_window(demo.demo_window); + if (c != NULL) { + client_close(c); } + demo.demo_window = None; + demo_next_phase(); break; } } static void demo_phase_workspaces(void) { + if (!demo_check_wait_complete()) { + return; + } + switch (demo.step) { case 0: demo.demo_workspace_start = dwn->current_workspace; @@ -167,45 +412,40 @@ static void demo_phase_workspaces(void) "- Shift+F1-F9: Move window to workspace\n" "- Ctrl+Alt+Left/Right: Navigate\n\n" "Watch the workspace indicators in the panel...", 0); + demo_wait_for(DEMO_WAIT_TIME, demo_get_step_delay()); demo_next_step(); break; case 1: - if (time_in_step() > 3000) { - workspace_switch(1); - demo_notify("Workspace 2", "Switched to workspace 2", 1500); - demo_next_step(); - } + workspace_switch(1); + demo_notify("Workspace 2", "Switched to workspace 2", 1500); + demo_wait_for(DEMO_WAIT_TIME, 2000); + demo_next_step(); break; case 2: - if (time_in_step() > 2000) { - workspace_switch(2); - demo_notify("Workspace 3", "Switched to workspace 3", 1500); - demo_next_step(); - } + workspace_switch(2); + demo_notify("Workspace 3", "Switched to workspace 3", 1500); + demo_wait_for(DEMO_WAIT_TIME, 2000); + demo_next_step(); break; case 3: - if (time_in_step() > 2000) { - workspace_switch(3); - demo_notify("Workspace 4", "Switched to workspace 4", 1500); - demo_next_step(); - } + workspace_switch(3); + demo_notify("Workspace 4", "Switched to workspace 4", 1500); + demo_wait_for(DEMO_WAIT_TIME, 2000); + demo_next_step(); break; case 4: - if (time_in_step() > 2000) { - workspace_switch(demo.demo_workspace_start); - demo_notify("Workspaces Complete", - "Returned to the original workspace.\n\n" - "Each workspace maintains its own:\n" - "- Window layout and positions\n" - "- Focused window\n" - "- Layout mode (tiling/floating/monocle)", 0); - demo_next_step(); - } + workspace_switch(demo.demo_workspace_start); + demo_notify("Workspaces Complete", + "Returned to the original workspace.\n\n" + "Each workspace maintains its own:\n" + "- Window layout and positions\n" + "- Focused window\n" + "- Layout mode (tiling/floating/monocle)", 0); + demo_wait_for(DEMO_WAIT_TIME, demo_get_step_delay()); + demo_next_step(); break; case 5: - if (time_in_step() > 4000) { - demo_next_phase(); - } + demo_next_phase(); break; } } @@ -214,6 +454,10 @@ static void demo_phase_layouts(void) { Workspace *ws = workspace_get_current(); + if (!demo_check_wait_complete()) { + return; + } + switch (demo.step) { case 0: demo_notify("Layout Modes", @@ -222,162 +466,268 @@ static void demo_phase_layouts(void) "2. Floating: Free positioning like traditional WMs\n" "3. Monocle: One window fullscreen at a time\n\n" "Press Super+Space to cycle layouts.", 0); + demo_wait_for(DEMO_WAIT_TIME, demo_get_step_delay()); demo_next_step(); break; case 1: - if (time_in_step() > 4000) { - if (ws != NULL) { - ws->layout = LAYOUT_TILING; - workspace_arrange_current(); - } - demo_notify("Tiling Layout", - "In tiling mode, windows are automatically\n" - "arranged in a main + stack pattern.\n\n" - "- Super+H/L: Resize main area\n" - "- Super+I/D: Add/remove from main\n\n" - "No overlapping windows - efficient use of space.", 0); - demo_next_step(); + if (ws != NULL) { + ws->layout = LAYOUT_TILING; + workspace_arrange_current(); } + demo_notify("Tiling Layout", + "In tiling mode, windows are automatically\n" + "arranged in a main + stack pattern.\n\n" + "- Super+H/L: Resize main area\n" + "- Super+I/D: Add/remove from main\n\n" + "No overlapping windows - efficient use of space.", 0); + demo_wait_for(DEMO_WAIT_TIME, demo_get_step_delay()); + demo_next_step(); break; case 2: - if (time_in_step() > 4000) { - if (ws != NULL) { - ws->layout = LAYOUT_FLOATING; - workspace_arrange_current(); - } - demo_notify("Floating Layout", - "In floating mode, windows can be freely\n" - "positioned and resized anywhere.\n\n" - "This is the traditional desktop behavior\n" - "familiar from Windows and macOS.", 0); - demo_next_step(); + if (ws != NULL) { + ws->layout = LAYOUT_FLOATING; + workspace_arrange_current(); } + demo_notify("Floating Layout", + "In floating mode, windows can be freely\n" + "positioned and resized anywhere.\n\n" + "This is the traditional desktop behavior\n" + "familiar from Windows and macOS.", 0); + demo_wait_for(DEMO_WAIT_TIME, demo_get_step_delay()); + demo_next_step(); break; case 3: - if (time_in_step() > 4000) { - if (ws != NULL) { - ws->layout = LAYOUT_MONOCLE; - workspace_arrange_current(); - } - demo_notify("Monocle Layout", - "In monocle mode, each window takes\n" - "the full screen.\n\n" - "Use Alt+Tab to switch between windows.\n" - "Great for focused, single-task work.", 0); - demo_next_step(); + if (ws != NULL) { + ws->layout = LAYOUT_MONOCLE; + workspace_arrange_current(); } + demo_notify("Monocle Layout", + "In monocle mode, each window takes\n" + "the full screen.\n\n" + "Use Alt+Tab to switch between windows.\n" + "Great for focused, single-task work.", 0); + demo_wait_for(DEMO_WAIT_TIME, demo_get_step_delay()); + demo_next_step(); break; case 4: - if (time_in_step() > 4000) { - if (ws != NULL) { - ws->layout = LAYOUT_TILING; - workspace_arrange_current(); - } - demo_next_phase(); + if (ws != NULL) { + ws->layout = demo.original_layout; + workspace_arrange_current(); } + demo_next_phase(); break; } } static void demo_phase_snapping(void) { + Workspace *ws; + + if (!demo_check_wait_complete()) { + return; + } + switch (demo.step) { case 0: + ws = workspace_get_current(); + if (ws == NULL || ws->focused == NULL) { + demo_notify("Window Snapping", + "Snapping requires a window to demonstrate.\n\n" + "Spawning a demo terminal...", 0); + spawn_async("xterm -title 'Snap Demo' -e 'sleep 60' &"); + demo_wait_for(DEMO_WAIT_TIME, 2000); + } + demo_next_step(); + break; + case 1: demo_notify("Window Snapping", "Quickly arrange windows with snapping:\n\n" "- Super+Left: Snap to left half (50%)\n" "- Super+Right: Snap to right half (50%)\n\n" - "Perfect for side-by-side comparisons\n" - "or working with multiple documents.", 0); + "Watch the focused window snap to the left...", 0); + demo_wait_for(DEMO_WAIT_TIME, demo_get_step_delay()); demo_next_step(); break; - case 1: - if (time_in_step() > 4000) { - demo_next_phase(); - } + case 2: + demo_execute_snap_left(); + demo_wait_for(DEMO_WAIT_TIME, demo_get_step_delay()); + demo_next_step(); + break; + case 3: + demo_notify("Snap Right", + "Now snapping the window to the right half...", 0); + demo_wait_for(DEMO_WAIT_TIME, 1500); + demo_next_step(); + break; + case 4: + demo_execute_snap_right(); + demo_wait_for(DEMO_WAIT_TIME, demo_get_step_delay()); + demo_next_step(); + break; + case 5: + demo_notify("Snapping Complete", + "Window snapping allows quick side-by-side layouts.\n\n" + "Perfect for comparing documents, coding with\n" + "documentation open, or multitasking.", 0); + demo_wait_for(DEMO_WAIT_TIME, demo_get_step_delay()); + demo_next_step(); + break; + case 6: + demo_next_phase(); break; } } static void demo_phase_panels(void) { + if (!demo_check_wait_complete()) { + return; + } + switch (demo.step) { case 0: demo_notify("Panel System", - "DWN has two panels with rich functionality:\n\n" + "DWN has two configurable panels:\n\n" "TOP PANEL (left to right):\n" "- Workspace indicators (click to switch)\n" - "- Layout mode indicator\n" - "- Taskbar (click to focus/restore)\n" + "- Layout mode indicator (T/F/M)\n" + "- Taskbar with window titles\n" "- AI status indicator\n" - "- System tray icons\n\n" - "BOTTOM PANEL:\n" - "- Clock with date\n" - "- News ticker", 0); + "- System tray icons", 0); + demo_wait_for(DEMO_WAIT_TIME, demo_get_step_delay() + 1000); demo_next_step(); break; case 1: - if (time_in_step() > 5000) { - demo_notify("System Tray", - "The system tray shows:\n\n" - "- Battery level (color-coded)\n" - "- WiFi status (click for network list)\n" - "- Volume (scroll to adjust, click to mute)\n" - "- External app icons (Telegram, etc.)\n\n" - "DWN implements the XEmbed protocol for\n" - "full compatibility with tray applications.", 0); - demo_next_step(); - } + demo_notify("Taskbar Features", + "The taskbar shows all windows on the workspace:\n\n" + "- Click a window title to focus it\n" + "- Minimized windows shown in [brackets]\n" + "- Focused window is highlighted\n" + "- Right-click for window menu", 0); + demo_wait_for(DEMO_WAIT_TIME, demo_get_step_delay()); + demo_next_step(); break; case 2: - if (time_in_step() > 5000) { - demo_next_phase(); - } + demo_notify("System Tray", + "The system tray supports XEmbed protocol:\n\n" + "- Battery indicator (color-coded by level)\n" + "- WiFi status (click for network list)\n" + "- Volume control (scroll to adjust)\n" + "- External app icons (Telegram, nm-applet, etc.)\n\n" + "Applications can dock their status icons here.", 0); + demo_wait_for(DEMO_WAIT_TIME, demo_get_step_delay() + 1000); + demo_next_step(); + break; + case 3: + demo_notify("Bottom Panel", + "BOTTOM PANEL contains:\n\n" + "- Clock with current time and date\n" + "- News ticker with scrolling headlines\n\n" + "Headlines are color-coded by sentiment:\n" + " Green = Positive news\n" + " Red = Negative news\n" + " White = Neutral", 0); + demo_wait_for(DEMO_WAIT_TIME, demo_get_step_delay()); + demo_next_step(); + break; + case 4: + demo_next_phase(); break; } } static void demo_phase_ai(void) { + if (!demo_check_wait_complete()) { + return; + } + switch (demo.step) { case 0: - if (dwn->ai_enabled) { - demo_notify("AI Integration", - "DWN includes AI-powered features:\n\n" - "COMMAND PALETTE (Super+Shift+A):\n" - "- Ask AI to launch apps: 'open firefox'\n" - "- Get answers: 'what time is it'\n" - "- Execute commands naturally\n\n" - "CONTEXT ANALYSIS (Super+A):\n" - "- See what you're working on\n" - "- Get AI suggestions based on context\n\n" - "EXA SEARCH (Super+Shift+E):\n" - "- Semantic web search\n" - "- Find relevant content by meaning", 0); - } else { - demo_notify("AI Integration", - "DWN includes AI-powered features:\n\n" - "AI is currently DISABLED.\n" - "To enable, set these environment variables:\n\n" - "OPENROUTER_API_KEY=sk-or-v1-your-key\n" - "EXA_API_KEY=your-exa-key\n\n" - "Then restart DWN to enable:\n" - "- AI Command Palette (Super+Shift+A)\n" - "- Context Analysis (Super+A)\n" - "- Exa Semantic Search (Super+Shift+E)", 0); - } + demo_notify("AI Integration", + "DWN includes AI-powered features:\n\n" + "COMMAND PALETTE (Super+Shift+A):\n" + "- Ask AI to launch apps: 'open firefox'\n" + "- Get answers: 'what time is it'\n" + "- Execute commands naturally\n\n" + "CONTEXT ANALYSIS (Super+A):\n" + "- See what you're working on\n" + "- Get AI suggestions based on context", 0); + demo_wait_for(DEMO_WAIT_TIME, demo_get_step_delay() + 1000); demo_next_step(); break; case 1: - if (time_in_step() > 6000) { - demo_next_phase(); + demo_notify("Live AI Demo", + "Let's try a live AI query...\n\n" + "Asking: \"Describe my current workspace\"", 0); + demo_wait_for(DEMO_WAIT_TIME, 2000); + demo_next_step(); + break; + case 2: + demo_execute_ai_query(); + if (ai_is_available()) { + demo_wait_for(DEMO_WAIT_AI_RESPONSE, demo_get_ai_timeout()); + } else { + demo_wait_for(DEMO_WAIT_TIME, demo_get_step_delay()); } + demo_next_step(); + break; + case 3: + demo_notify("Exa Semantic Search", + "DWN also includes Exa semantic search:\n\n" + "EXA SEARCH (Super+Shift+E):\n" + "- Search the web by meaning, not just keywords\n" + "- Find relevant documentation and articles\n" + "- Results open in your default browser", 0); + demo_wait_for(DEMO_WAIT_TIME, demo_get_step_delay()); + demo_next_step(); + break; + case 4: + demo_notify("Live Exa Demo", + "Let's try a live Exa search...\n\n" + "Searching: \"What does molodetz mean?\"", 0); + demo_wait_for(DEMO_WAIT_TIME, 2000); + demo_next_step(); + break; + case 5: + demo_execute_exa_search(); + if (exa_is_available()) { + demo_wait_for(DEMO_WAIT_EXA_RESPONSE, demo_get_ai_timeout()); + } else { + demo_wait_for(DEMO_WAIT_TIME, demo_get_step_delay()); + } + demo_next_step(); + break; + case 6: + if (ai_is_available() || exa_is_available()) { + demo_notify("AI Features Complete", + "AI integration provides intelligent assistance\n" + "directly within your window manager.\n\n" + "No need to switch to a browser or separate app\n" + "to get quick answers or find information.", 0); + } else { + demo_notify("AI Features", + "To enable AI features, configure API keys:\n\n" + "OPENROUTER_API_KEY=sk-or-v1-your-key\n" + "EXA_API_KEY=your-exa-key\n\n" + "Add to ~/.config/dwn/config or set as\n" + "environment variables, then restart DWN.", 0); + } + demo_wait_for(DEMO_WAIT_TIME, demo_get_step_delay()); + demo_next_step(); + break; + case 7: + demo_next_phase(); break; } } static void demo_phase_news(void) { + if (!demo_check_wait_complete()) { + return; + } + switch (demo.step) { case 0: demo_notify("News Ticker", @@ -391,39 +741,39 @@ static void demo_phase_news(void) "- Super+Down: Next article\n" "- Super+Up: Previous article\n" "- Super+Return: Open in browser", 0); + demo_wait_for(DEMO_WAIT_TIME, demo_get_step_delay()); demo_next_step(); break; case 1: - if (time_in_step() > 3000) { - news_next_article(); - demo_notify("News Navigation", "Moved to next article", 1500); - demo_next_step(); - } + news_next_article(); + demo_notify("News Navigation", "Moved to next article", 1500); + demo_wait_for(DEMO_WAIT_TIME, 2000); + demo_next_step(); break; case 2: - if (time_in_step() > 2000) { - news_next_article(); - demo_notify("News Navigation", "Moved to next article", 1500); - demo_next_step(); - } + news_next_article(); + demo_notify("News Navigation", "Moved to next article", 1500); + demo_wait_for(DEMO_WAIT_TIME, 2000); + demo_next_step(); break; case 3: - if (time_in_step() > 2000) { - news_prev_article(); - demo_notify("News Navigation", "Moved to previous article", 1500); - demo_next_step(); - } + news_prev_article(); + demo_notify("News Navigation", "Moved to previous article", 1500); + demo_wait_for(DEMO_WAIT_TIME, 2000); + demo_next_step(); break; case 4: - if (time_in_step() > 2000) { - demo_next_phase(); - } + demo_next_phase(); break; } } static void demo_phase_shortcuts(void) { + if (!demo_check_wait_complete()) { + return; + } + switch (demo.step) { case 0: demo_notify("Quick Reference", @@ -443,18 +793,21 @@ static void demo_phase_shortcuts(void) "Super+Left Snap left\n" "Super+Right Snap right\n\n" "Press Super+S anytime to see all shortcuts.", 0); + demo_wait_for(DEMO_WAIT_TIME, demo_get_step_delay() * 2); demo_next_step(); break; case 1: - if (time_in_step() > 8000) { - demo_next_phase(); - } + demo_next_phase(); break; } } static void demo_phase_complete(void) { + if (!demo_check_wait_complete()) { + return; + } + switch (demo.step) { case 0: demo_notify("Demo Complete", @@ -467,15 +820,14 @@ static void demo_phase_complete(void) "Edit ~/.config/dwn/config to customize\n" "colors, fonts, keybindings, and more.\n\n" "Enjoy using DWN!", 0); + demo_wait_for(DEMO_WAIT_TIME, demo_get_step_delay() + 2000); demo_next_step(); break; case 1: - if (time_in_step() > 6000) { - notification_close(demo.current_notification); - demo.current_notification = 0; - demo.active = false; - demo.phase = DEMO_IDLE; - } + notification_close(demo.current_notification); + demo.current_notification = 0; + demo.active = false; + demo.phase = DEMO_IDLE; break; } } @@ -508,12 +860,28 @@ void demo_start(void) return; } + memset(&demo, 0, sizeof(demo)); + demo.active = true; demo.phase = DEMO_INTRO; demo.step = 0; demo.phase_start_time = get_time_ms(); demo.step_start_time = demo.phase_start_time; demo.demo_workspace_start = dwn->current_workspace; + demo.demo_window = None; + + Workspace *ws = workspace_get_current(); + if (ws != NULL) { + demo.original_layout = ws->layout; + } + + demo.wait_condition = DEMO_WAIT_NONE; + demo.pending_ai_request = NULL; + demo.pending_exa_request = NULL; + demo.ai_demo_completed = false; + demo.exa_demo_completed = false; + demo.ai_response_buffer[0] = '\0'; + demo.exa_response_buffer[0] = '\0'; } void demo_stop(void) @@ -522,16 +890,30 @@ void demo_stop(void) return; } + if (demo.pending_ai_request != NULL) { + ai_cancel_request(demo.pending_ai_request); + demo.pending_ai_request = NULL; + } + + demo.pending_exa_request = NULL; + if (demo.current_notification > 0) { notification_close(demo.current_notification); demo.current_notification = 0; } + Workspace *ws = workspace_get_current(); + if (ws != NULL && demo.original_layout != ws->layout) { + ws->layout = demo.original_layout; + workspace_arrange_current(); + } + demo_notify("Demo Stopped", "Demo mode has been stopped.", 2000); demo.active = false; demo.phase = DEMO_IDLE; demo.step = 0; + demo.wait_condition = DEMO_WAIT_NONE; } void demo_update(void) diff --git a/src/keys.c b/src/keys.c index c6acd77..6fcd568 100644 --- a/src/keys.c +++ b/src/keys.c @@ -608,7 +608,7 @@ void key_toggle_maximize(void) { Workspace *ws = workspace_get_current(); if (ws != NULL && ws->focused != NULL) { - client_toggle_fullscreen(ws->focused); + client_toggle_maximize(ws->focused); } } diff --git a/src/main.c b/src/main.c index 49448a1..6d1ad0a 100644 --- a/src/main.c +++ b/src/main.c @@ -369,7 +369,29 @@ static void handle_enter_notify(XCrossingEvent *ev) } if (c != NULL && c->workspace == (unsigned int)dwn->current_workspace) { - client_focus(c); + int delay = dwn->config->focus_follow_delay_ms; + if (delay <= 0) { + client_focus(c); + } else { + dwn->pending_focus_client = c; + dwn->pending_focus_time = get_time_ms() + delay; + } + } +} + +static void handle_leave_notify(XCrossingEvent *ev) +{ + if (ev == NULL || dwn == NULL) { + return; + } + + Client *c = client_find_by_frame(ev->window); + if (c == NULL) { + c = client_find_by_window(ev->window); + } + + if (c != NULL && dwn->pending_focus_client == c) { + dwn->pending_focus_client = NULL; } } @@ -568,6 +590,17 @@ static void handle_client_message(XClientMessageEvent *ev) client_set_fullscreen(c, set); } } + + if (prop1 == ewmh.NET_WM_STATE_MAXIMIZED_VERT || + prop1 == ewmh.NET_WM_STATE_MAXIMIZED_HORZ || + prop2 == ewmh.NET_WM_STATE_MAXIMIZED_VERT || + prop2 == ewmh.NET_WM_STATE_MAXIMIZED_HORZ) { + if (toggle) { + client_toggle_maximize(c); + } else { + client_set_maximize(c, set); + } + } } } else if (ev->message_type == ewmh.NET_CURRENT_DESKTOP) { int desktop = ev->data.l[0]; @@ -635,6 +668,9 @@ void dwn_handle_event(XEvent *ev) case EnterNotify: handle_enter_notify(&ev->xcrossing); break; + case LeaveNotify: + handle_leave_notify(&ev->xcrossing); + break; case ButtonPress: handle_button_press(&ev->xbutton); break; @@ -840,6 +876,19 @@ void dwn_run(void) notifications_update(); + if (dwn->pending_focus_client != NULL) { + long now_focus = get_time_ms(); + if (now_focus >= dwn->pending_focus_time) { + Workspace *ws = workspace_get_current(); + if (ws != NULL && ws->focused != dwn->pending_focus_client) { + if (dwn->pending_focus_client->workspace == (unsigned int)dwn->current_workspace) { + client_focus(dwn->pending_focus_client); + } + } + dwn->pending_focus_client = NULL; + } + } + long now = get_time_ms(); if (now - last_news_update >= 16) { diff --git a/src/panel.c b/src/panel.c index f59c9a7..281ca7e 100644 --- a/src/panel.c +++ b/src/panel.c @@ -391,12 +391,11 @@ void panel_render_taskbar(Panel *panel, int x, int *width) XFillRectangle(dpy, panel->buffer, dwn->gc, current_x, 2, item_width - 2, panel->height - 4); - char title[64]; + char title[260]; if (minimized) { snprintf(title, sizeof(title), "[%s]", c->title); } else { - strncpy(title, c->title, sizeof(title) - 1); - title[sizeof(title) - 1] = '\0'; + snprintf(title, sizeof(title), "%s", c->title); } int max_text_width = item_width - 8; diff --git a/src/workspace.c b/src/workspace.c index ac2aa70..67fcee9 100644 --- a/src/workspace.c +++ b/src/workspace.c @@ -29,6 +29,8 @@ void workspace_init(void) ws->clients = NULL; ws->focused = NULL; + ws->mru_head = NULL; + ws->mru_tail = NULL; ws->layout = (dwn->config != NULL) ? dwn->config->default_layout : LAYOUT_TILING; ws->master_ratio = (dwn->config != NULL) ? @@ -444,3 +446,75 @@ void workspace_focus_master(void) client_focus(master); } } + + +void workspace_mru_push(int workspace, Client *client) +{ + if (client == NULL || workspace < 0 || workspace >= MAX_WORKSPACES) { + return; + } + + Workspace *ws = workspace_get(workspace); + if (ws == NULL) { + return; + } + + workspace_mru_remove(workspace, client); + + client->mru_next = ws->mru_head; + client->mru_prev = NULL; + + if (ws->mru_head != NULL) { + ws->mru_head->mru_prev = client; + } + ws->mru_head = client; + + if (ws->mru_tail == NULL) { + ws->mru_tail = client; + } +} + +void workspace_mru_remove(int workspace, Client *client) +{ + if (client == NULL || workspace < 0 || workspace >= MAX_WORKSPACES) { + return; + } + + Workspace *ws = workspace_get(workspace); + if (ws == NULL) { + return; + } + + if (client->mru_prev != NULL) { + client->mru_prev->mru_next = client->mru_next; + } else if (ws->mru_head == client) { + ws->mru_head = client->mru_next; + } + + if (client->mru_next != NULL) { + client->mru_next->mru_prev = client->mru_prev; + } else if (ws->mru_tail == client) { + ws->mru_tail = client->mru_prev; + } + + client->mru_next = NULL; + client->mru_prev = NULL; +} + +Client *workspace_mru_get_previous(int workspace, Client *current) +{ + Workspace *ws = workspace_get(workspace); + if (ws == NULL) { + return NULL; + } + + Client *candidate = ws->mru_head; + while (candidate != NULL) { + if (candidate != current && !client_is_minimized(candidate)) { + return candidate; + } + candidate = candidate->mru_next; + } + + return NULL; +}