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-server.h \
/usr/include/dbus-1.0/dbus/dbus-signature.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-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/client.h:
include/dwn.h: include/dwn.h:
include/atoms.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-signature.h:
/usr/include/dbus-1.0/dbus/dbus-syntax.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:

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_update_class(Client *client);
void client_set_fullscreen(Client *client, bool fullscreen); void client_set_fullscreen(Client *client, bool fullscreen);
void client_toggle_fullscreen(Client *client); 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_set_floating(Client *client, bool floating);
void client_toggle_floating(Client *client); void client_toggle_floating(Client *client);
bool client_is_floating(Client *client); bool client_is_floating(Client *client);
bool client_is_fullscreen(Client *client); bool client_is_fullscreen(Client *client);
bool client_is_maximized(Client *client);
bool client_is_minimized(Client *client); bool client_is_minimized(Client *client);
bool client_is_dialog(Window window); bool client_is_dialog(Window window);
bool client_is_dock(Window window); bool client_is_dock(Window window);

View File

@ -31,6 +31,7 @@ struct Config {
char launcher[128]; char launcher[128];
char file_manager[128]; char file_manager[128];
FocusMode focus_mode; FocusMode focus_mode;
int focus_follow_delay_ms;
bool show_decorations; bool show_decorations;
int border_width; int border_width;
@ -58,6 +59,10 @@ struct Config {
bool autostart_enabled; bool autostart_enabled;
bool autostart_xdg; bool autostart_xdg;
char autostart_path[512]; char autostart_path[512];
int demo_step_delay_ms;
int demo_ai_timeout_ms;
int demo_window_timeout_ms;
}; };
Config *config_create(void); Config *config_create(void);

View File

@ -23,6 +23,14 @@ typedef enum {
DEMO_COMPLETE DEMO_COMPLETE
} DemoPhase; } 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_init(void);
void demo_cleanup(void); void demo_cleanup(void);
void demo_start(void); void demo_start(void);

View File

@ -58,7 +58,8 @@ typedef enum {
CLIENT_FULLSCREEN = (1 << 1), CLIENT_FULLSCREEN = (1 << 1),
CLIENT_URGENT = (1 << 2), CLIENT_URGENT = (1 << 2),
CLIENT_MINIMIZED = (1 << 3), CLIENT_MINIMIZED = (1 << 3),
CLIENT_STICKY = (1 << 4) CLIENT_STICKY = (1 << 4),
CLIENT_MAXIMIZED = (1 << 5)
} ClientFlags; } ClientFlags;
typedef struct Client Client; typedef struct Client Client;
@ -81,6 +82,8 @@ struct Client {
char class[64]; char class[64];
Client *next; Client *next;
Client *prev; Client *prev;
Client *mru_next;
Client *mru_prev;
}; };
struct Monitor { struct Monitor {
@ -93,6 +96,8 @@ struct Monitor {
struct Workspace { struct Workspace {
Client *clients; Client *clients;
Client *focused; Client *focused;
Client *mru_head;
Client *mru_tail;
LayoutType layout; LayoutType layout;
float master_ratio; float master_ratio;
int master_count; int master_count;
@ -142,6 +147,9 @@ typedef struct {
int drag_orig_x, drag_orig_y; int drag_orig_x, drag_orig_y;
int drag_orig_w, drag_orig_h; int drag_orig_w, drag_orig_h;
bool resizing; bool resizing;
Client *pending_focus_client;
long pending_focus_time;
} DWNState; } DWNState;
extern DWNState *dwn; extern DWNState *dwn;

View File

@ -50,4 +50,8 @@ void workspace_focus_next(void);
void workspace_focus_prev(void); void workspace_focus_prev(void);
void workspace_focus_master(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 #endif

View File

@ -93,6 +93,11 @@
<td><code>click</code></td> <td><code>click</code></td>
<td><code>click</code> or <code>follow</code> (sloppy focus)</td> <td><code>click</code> or <code>follow</code> (sloppy focus)</td>
</tr> </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> <tr>
<td><code>decorations</code></td> <td><code>decorations</code></td>
<td><code>true</code></td> <td><code>true</code></td>
@ -109,7 +114,8 @@
terminal = alacritty terminal = alacritty
launcher = rofi -show run launcher = rofi -show run
file_manager = nautilus file_manager = nautilus
focus_mode = click focus_mode = follow
focus_follow_delay = 100
decorations = true</code></pre> decorations = true</code></pre>
<h2 id="appearance" style="margin-top: 3rem;">[appearance] - Visual Settings</h2> <h2 id="appearance" style="margin-top: 3rem;">[appearance] - Visual Settings</h2>
<p style="color: var(--text-muted); margin-bottom: 1.5rem;"> <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/autostart/</code> - User XDG autostart entries</li>
<li><code>~/.config/dwn/autostart.d/</code> - DWN-specific symlinks and scripts</li> <li><code>~/.config/dwn/autostart.d/</code> - DWN-specific symlinks and scripts</li>
</ul> </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> <h2 style="margin-top: 3rem;">Complete Configuration Example</h2>
<div class="code-header"> <div class="code-header">
<span>~/.config/dwn/config</span> <span>~/.config/dwn/config</span>
@ -444,6 +486,7 @@ terminal = alacritty
launcher = rofi -show drun launcher = rofi -show drun
file_manager = thunar file_manager = thunar
focus_mode = click focus_mode = click
focus_follow_delay = 100
decorations = true decorations = true
[appearance] [appearance]
border_width = 2 border_width = 2
@ -478,7 +521,11 @@ model = google/gemini-2.0-flash-exp:free
[autostart] [autostart]
enabled = true enabled = true
xdg_autostart = 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> </div>
</section> </section>
</main> </main>

View File

@ -11,6 +11,7 @@
#include "workspace.h" #include "workspace.h"
#include "decorations.h" #include "decorations.h"
#include "notifications.h" #include "notifications.h"
#include "layout.h"
#include <string.h> #include <string.h>
#include <stdlib.h> #include <stdlib.h>
@ -43,6 +44,8 @@ Client *client_create(Window window)
client->workspace = dwn->current_workspace; client->workspace = dwn->current_workspace;
client->next = NULL; client->next = NULL;
client->prev = NULL; client->prev = NULL;
client->mru_next = NULL;
client->mru_prev = NULL;
XWindowAttributes wa; XWindowAttributes wa;
int orig_width = 640, orig_height = 480; int orig_width = 640, orig_height = 480;
@ -242,6 +245,9 @@ void client_unmanage(Client *client)
LOG_DEBUG("Unmanaging window: %lu", client->window); 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"); client_sync_log("client_unmanage: remove from workspace");
workspace_remove_client(client->workspace, client); workspace_remove_client(client->workspace, client);
@ -258,14 +264,22 @@ void client_unmanage(Client *client)
client_destroy(client); client_destroy(client);
client_sync_log("client_unmanage: focus next"); client_sync_log("client_unmanage: focus next");
XGrabServer(dwn->display);
Workspace *ws = workspace_get_current(); Workspace *ws = workspace_get_current();
if (ws != NULL && ws->focused == NULL) { 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) { if (next != NULL) {
client_focus(next); 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"); client_sync_log("client_unmanage: DONE");
} }
@ -336,6 +350,7 @@ void client_focus(Client *client)
if (ws != NULL) { if (ws != NULL) {
ws->focused = client; ws->focused = client;
workspace_mru_push(client->workspace, client);
} }
client_sync_log("client_focus: raising"); client_sync_log("client_focus: raising");
@ -672,6 +687,80 @@ void client_toggle_floating(Client *client)
client_set_floating(client, !(client->flags & CLIENT_FLOATING)); 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) 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->launcher, "dmenu_run", sizeof(cfg->launcher) - 1);
strncpy(cfg->file_manager, "thunar", sizeof(cfg->file_manager) - 1); strncpy(cfg->file_manager, "thunar", sizeof(cfg->file_manager) - 1);
cfg->focus_mode = FOCUS_CLICK; cfg->focus_mode = FOCUS_CLICK;
cfg->focus_follow_delay_ms = 100;
cfg->show_decorations = true; cfg->show_decorations = true;
cfg->border_width = DEFAULT_BORDER_WIDTH; 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); strncpy(cfg->autostart_path, autostart_path, sizeof(cfg->autostart_path) - 1);
dwn_free(autostart_path); 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 { } else {
cfg->focus_mode = FOCUS_CLICK; 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) { } else if (strcmp(key, "decorations") == 0) {
cfg->show_decorations = (strcmp(value, "true") == 0 || strcmp(value, "1") == 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); 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;
}
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -608,7 +608,7 @@ void key_toggle_maximize(void)
{ {
Workspace *ws = workspace_get_current(); Workspace *ws = workspace_get_current();
if (ws != NULL && ws->focused != NULL) { 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) { 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); 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) { } else if (ev->message_type == ewmh.NET_CURRENT_DESKTOP) {
int desktop = ev->data.l[0]; int desktop = ev->data.l[0];
@ -635,6 +668,9 @@ void dwn_handle_event(XEvent *ev)
case EnterNotify: case EnterNotify:
handle_enter_notify(&ev->xcrossing); handle_enter_notify(&ev->xcrossing);
break; break;
case LeaveNotify:
handle_leave_notify(&ev->xcrossing);
break;
case ButtonPress: case ButtonPress:
handle_button_press(&ev->xbutton); handle_button_press(&ev->xbutton);
break; break;
@ -840,6 +876,19 @@ void dwn_run(void)
notifications_update(); 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(); long now = get_time_ms();
if (now - last_news_update >= 16) { 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, XFillRectangle(dpy, panel->buffer, dwn->gc,
current_x, 2, item_width - 2, panel->height - 4); current_x, 2, item_width - 2, panel->height - 4);
char title[64]; char title[260];
if (minimized) { if (minimized) {
snprintf(title, sizeof(title), "[%s]", c->title); snprintf(title, sizeof(title), "[%s]", c->title);
} else { } else {
strncpy(title, c->title, sizeof(title) - 1); snprintf(title, sizeof(title), "%s", c->title);
title[sizeof(title) - 1] = '\0';
} }
int max_text_width = item_width - 8; int max_text_width = item_width - 8;

View File

@ -29,6 +29,8 @@ void workspace_init(void)
ws->clients = NULL; ws->clients = NULL;
ws->focused = NULL; ws->focused = NULL;
ws->mru_head = NULL;
ws->mru_tail = NULL;
ws->layout = (dwn->config != NULL) ? ws->layout = (dwn->config != NULL) ?
dwn->config->default_layout : LAYOUT_TILING; dwn->config->default_layout : LAYOUT_TILING;
ws->master_ratio = (dwn->config != NULL) ? ws->master_ratio = (dwn->config != NULL) ?
@ -444,3 +446,75 @@ void workspace_focus_master(void)
client_focus(master); 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;
}