feat: add maximize window functionality and MRU-based focus management

feat: add focus follow delay configuration
feat: add demo mode timing configurations and wait conditions
docs: update configuration documentation for new options
This commit is contained in:
retoor 2025-12-28 10:23:03 +01:00
parent d0b1669cb4
commit 395583aea9
31 changed files with 896 additions and 208 deletions

BIN
bin/dwn

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -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:

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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;

View File

@ -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

View File

@ -93,6 +93,11 @@
<td><code>click</code></td>
<td><code>click</code> or <code>follow</code> (sloppy focus)</td>
</tr>
<tr>
<td><code>focus_follow_delay</code></td>
<td><code>100</code></td>
<td>Delay in ms before focus switches in follow mode (0-1000)</td>
</tr>
<tr>
<td><code>decorations</code></td>
<td><code>true</code></td>
@ -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</code></pre>
<h2 id="appearance" style="margin-top: 3rem;">[appearance] - Visual Settings</h2>
<p style="color: var(--text-muted); margin-bottom: 1.5rem;">
@ -432,6 +438,42 @@ path = ~/.config/dwn/autostart.d</code></pre>
<li><code>~/.config/autostart/</code> - User XDG autostart entries</li>
<li><code>~/.config/dwn/autostart.d/</code> - DWN-specific symlinks and scripts</li>
</ul>
<h2 id="demo" style="margin-top: 3rem;">[demo] - Demo Mode</h2>
<p style="color: var(--text-muted); margin-bottom: 1.5rem;">
Configure demo mode timing. The demo showcases DWN features including live AI and search functionality.
</p>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>Option</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>step_delay</code></td>
<td>Time between demo steps in milliseconds (1000-30000, default: 4000)</td>
</tr>
<tr>
<td><code>ai_timeout</code></td>
<td>Timeout for AI/Exa API responses in milliseconds (5000-60000, default: 15000)</td>
</tr>
<tr>
<td><code>window_timeout</code></td>
<td>Timeout for window spawn operations in milliseconds (1000-30000, default: 5000)</td>
</tr>
</tbody>
</table>
</div>
<div class="code-header">
<span>Example</span>
<button class="copy-btn" onclick="copyCode(this)">Copy</button>
</div>
<pre><code>[demo]
step_delay = 4000
ai_timeout = 15000
window_timeout = 5000</code></pre>
<h2 style="margin-top: 3rem;">Complete Configuration Example</h2>
<div class="code-header">
<span>~/.config/dwn/config</span>
@ -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</code></pre>
path = ~/.config/dwn/autostart.d
[demo]
step_delay = 4000
ai_timeout = 15000
window_timeout = 5000</code></pre>
</div>
</section>
</main>

View File

@ -11,6 +11,7 @@
#include "workspace.h"
#include "decorations.h"
#include "notifications.h"
#include "layout.h"
#include <string.h>
#include <stdlib.h>
@ -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)
{

View File

@ -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;
}
}
}

View File

@ -19,8 +19,6 @@
#include <string.h>
#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();
}
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,26 +344,24 @@ 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' &");
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_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;
@ -129,11 +372,10 @@ static void demo_phase_window_mgmt(void)
"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);
@ -141,23 +383,26 @@ static void demo_phase_window_mgmt(void)
"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.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,31 +412,28 @@ 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_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_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_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"
@ -199,13 +441,11 @@ static void demo_phase_workspaces(void)
"- 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();
}
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,10 +466,10 @@ 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();
@ -236,11 +480,10 @@ static void demo_phase_layouts(void)
"- 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();
@ -250,11 +493,10 @@ static void demo_phase_layouts(void)
"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();
@ -264,84 +506,144 @@ static void demo_phase_layouts(void)
"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;
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) {
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_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_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"
@ -350,34 +652,82 @@ static void demo_phase_ai(void)
"- 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);
}
"- 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_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_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_wait_for(DEMO_WAIT_TIME, 2000);
demo_next_step();
}
break;
case 4:
if (time_in_step() > 2000) {
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();
}
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;
}
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)

View File

@ -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);
}
}

View File

@ -369,7 +369,29 @@ static void handle_enter_notify(XCrossingEvent *ev)
}
if (c != NULL && c->workspace == (unsigned int)dwn->current_workspace) {
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) {

View File

@ -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;

View File

@ -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;
}