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

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();
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) {
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) {

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