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

View File

@ -19,8 +19,6 @@
#include <string.h> #include <string.h>
#define DEMO_STEP_DELAY 3000
typedef struct { typedef struct {
bool active; bool active;
DemoPhase phase; DemoPhase phase;
@ -30,6 +28,18 @@ typedef struct {
uint32_t current_notification; uint32_t current_notification;
Window demo_window; Window demo_window;
int demo_workspace_start; 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; } DemoState;
static DemoState demo = {0}; 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); 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) static void demo_next_phase(void)
{ {
demo.phase++; demo.phase++;
@ -56,13 +299,12 @@ static void demo_next_step(void)
demo.step_start_time = get_time_ms(); 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) static void demo_phase_intro(void)
{ {
if (!demo_check_wait_complete()) {
return;
}
switch (demo.step) { switch (demo.step) {
case 0: case 0:
demo_notify("Welcome to DWN", demo_notify("Welcome to DWN",
@ -74,12 +316,11 @@ static void demo_phase_intro(void)
"- System tray and notifications\n\n" "- System tray and notifications\n\n"
"This demo will showcase all features.\n" "This demo will showcase all features.\n"
"Press Super+Shift+D to stop at any time.", 0); "Press Super+Shift+D to stop at any time.", 0);
demo_wait_for(DEMO_WAIT_TIME, demo_get_step_delay() + 2000);
demo_next_step(); demo_next_step();
break; break;
case 1: case 1:
if (time_in_step() > 5000) {
demo_next_phase(); demo_next_phase();
}
break; break;
} }
} }
@ -89,6 +330,10 @@ static void demo_phase_window_mgmt(void)
Client *c; Client *c;
Workspace *ws; Workspace *ws;
if (!demo_check_wait_complete()) {
return;
}
switch (demo.step) { switch (demo.step) {
case 0: case 0:
demo_notify("Window Management", demo_notify("Window Management",
@ -99,26 +344,24 @@ static void demo_phase_window_mgmt(void)
"- Alt+F11: Fullscreen\n" "- Alt+F11: Fullscreen\n"
"- Alt+Tab: Cycle windows\n\n" "- Alt+Tab: Cycle windows\n\n"
"Let's open a terminal...", 0); "Let's open a terminal...", 0);
demo_wait_for(DEMO_WAIT_TIME, demo_get_step_delay());
demo_next_step(); demo_next_step();
break; break;
case 1: case 1:
if (time_in_step() > 3000) { spawn_async("xterm -title 'Demo Terminal' -e 'echo DWN Demo Terminal; sleep 60' &");
spawn_async("xterm -title 'Demo Terminal' -e 'echo DWN Demo Terminal; sleep 30' &"); demo_wait_for(DEMO_WAIT_TIME, 2000);
demo_next_step(); demo_next_step();
}
break; break;
case 2: case 2:
if (time_in_step() > 2000) {
demo_notify("Window Created", demo_notify("Window Created",
"A terminal window was spawned.\n\n" "A terminal window was spawned.\n\n"
"Windows can be moved by dragging the title bar\n" "Windows can be moved by dragging the title bar\n"
"or resized by dragging edges.\n\n" "or resized by dragging edges.\n\n"
"Let's try minimizing...", 0); "Let's try minimizing...", 0);
demo_wait_for(DEMO_WAIT_TIME, demo_get_step_delay());
demo_next_step(); demo_next_step();
}
break; break;
case 3: case 3:
if (time_in_step() > 3000) {
ws = workspace_get_current(); ws = workspace_get_current();
if (ws != NULL && ws->focused != NULL) { if (ws != NULL && ws->focused != NULL) {
demo.demo_window = ws->focused->window; 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" "appear in [brackets] with a gray background.\n\n"
"Click it or press Alt+F9 to restore.", 0); "Click it or press Alt+F9 to restore.", 0);
} }
demo_wait_for(DEMO_WAIT_TIME, demo_get_step_delay());
demo_next_step(); demo_next_step();
}
break; break;
case 4: case 4:
if (time_in_step() > 4000) {
c = client_find_by_window(demo.demo_window); c = client_find_by_window(demo.demo_window);
if (c != NULL) { if (c != NULL) {
client_restore(c); client_restore(c);
@ -141,23 +383,26 @@ static void demo_phase_window_mgmt(void)
"The window is restored from the taskbar.\n\n" "The window is restored from the taskbar.\n\n"
"This is how minimize/restore works in DWN.", 0); "This is how minimize/restore works in DWN.", 0);
} }
demo_wait_for(DEMO_WAIT_TIME, demo_get_step_delay());
demo_next_step(); demo_next_step();
}
break; break;
case 5: case 5:
if (time_in_step() > 3000) {
c = client_find_by_window(demo.demo_window); c = client_find_by_window(demo.demo_window);
if (c != NULL) { if (c != NULL) {
client_close(c); client_close(c);
} }
demo.demo_window = None;
demo_next_phase(); demo_next_phase();
}
break; break;
} }
} }
static void demo_phase_workspaces(void) static void demo_phase_workspaces(void)
{ {
if (!demo_check_wait_complete()) {
return;
}
switch (demo.step) { switch (demo.step) {
case 0: case 0:
demo.demo_workspace_start = dwn->current_workspace; 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" "- Shift+F1-F9: Move window to workspace\n"
"- Ctrl+Alt+Left/Right: Navigate\n\n" "- Ctrl+Alt+Left/Right: Navigate\n\n"
"Watch the workspace indicators in the panel...", 0); "Watch the workspace indicators in the panel...", 0);
demo_wait_for(DEMO_WAIT_TIME, demo_get_step_delay());
demo_next_step(); demo_next_step();
break; break;
case 1: case 1:
if (time_in_step() > 3000) {
workspace_switch(1); workspace_switch(1);
demo_notify("Workspace 2", "Switched to workspace 2", 1500); demo_notify("Workspace 2", "Switched to workspace 2", 1500);
demo_wait_for(DEMO_WAIT_TIME, 2000);
demo_next_step(); demo_next_step();
}
break; break;
case 2: case 2:
if (time_in_step() > 2000) {
workspace_switch(2); workspace_switch(2);
demo_notify("Workspace 3", "Switched to workspace 3", 1500); demo_notify("Workspace 3", "Switched to workspace 3", 1500);
demo_wait_for(DEMO_WAIT_TIME, 2000);
demo_next_step(); demo_next_step();
}
break; break;
case 3: case 3:
if (time_in_step() > 2000) {
workspace_switch(3); workspace_switch(3);
demo_notify("Workspace 4", "Switched to workspace 4", 1500); demo_notify("Workspace 4", "Switched to workspace 4", 1500);
demo_wait_for(DEMO_WAIT_TIME, 2000);
demo_next_step(); demo_next_step();
}
break; break;
case 4: case 4:
if (time_in_step() > 2000) {
workspace_switch(demo.demo_workspace_start); workspace_switch(demo.demo_workspace_start);
demo_notify("Workspaces Complete", demo_notify("Workspaces Complete",
"Returned to the original workspace.\n\n" "Returned to the original workspace.\n\n"
@ -199,13 +441,11 @@ static void demo_phase_workspaces(void)
"- Window layout and positions\n" "- Window layout and positions\n"
"- Focused window\n" "- Focused window\n"
"- Layout mode (tiling/floating/monocle)", 0); "- Layout mode (tiling/floating/monocle)", 0);
demo_wait_for(DEMO_WAIT_TIME, demo_get_step_delay());
demo_next_step(); demo_next_step();
}
break; break;
case 5: case 5:
if (time_in_step() > 4000) {
demo_next_phase(); demo_next_phase();
}
break; break;
} }
} }
@ -214,6 +454,10 @@ static void demo_phase_layouts(void)
{ {
Workspace *ws = workspace_get_current(); Workspace *ws = workspace_get_current();
if (!demo_check_wait_complete()) {
return;
}
switch (demo.step) { switch (demo.step) {
case 0: case 0:
demo_notify("Layout Modes", demo_notify("Layout Modes",
@ -222,10 +466,10 @@ static void demo_phase_layouts(void)
"2. Floating: Free positioning like traditional WMs\n" "2. Floating: Free positioning like traditional WMs\n"
"3. Monocle: One window fullscreen at a time\n\n" "3. Monocle: One window fullscreen at a time\n\n"
"Press Super+Space to cycle layouts.", 0); "Press Super+Space to cycle layouts.", 0);
demo_wait_for(DEMO_WAIT_TIME, demo_get_step_delay());
demo_next_step(); demo_next_step();
break; break;
case 1: case 1:
if (time_in_step() > 4000) {
if (ws != NULL) { if (ws != NULL) {
ws->layout = LAYOUT_TILING; ws->layout = LAYOUT_TILING;
workspace_arrange_current(); workspace_arrange_current();
@ -236,11 +480,10 @@ static void demo_phase_layouts(void)
"- Super+H/L: Resize main area\n" "- Super+H/L: Resize main area\n"
"- Super+I/D: Add/remove from main\n\n" "- Super+I/D: Add/remove from main\n\n"
"No overlapping windows - efficient use of space.", 0); "No overlapping windows - efficient use of space.", 0);
demo_wait_for(DEMO_WAIT_TIME, demo_get_step_delay());
demo_next_step(); demo_next_step();
}
break; break;
case 2: case 2:
if (time_in_step() > 4000) {
if (ws != NULL) { if (ws != NULL) {
ws->layout = LAYOUT_FLOATING; ws->layout = LAYOUT_FLOATING;
workspace_arrange_current(); workspace_arrange_current();
@ -250,11 +493,10 @@ static void demo_phase_layouts(void)
"positioned and resized anywhere.\n\n" "positioned and resized anywhere.\n\n"
"This is the traditional desktop behavior\n" "This is the traditional desktop behavior\n"
"familiar from Windows and macOS.", 0); "familiar from Windows and macOS.", 0);
demo_wait_for(DEMO_WAIT_TIME, demo_get_step_delay());
demo_next_step(); demo_next_step();
}
break; break;
case 3: case 3:
if (time_in_step() > 4000) {
if (ws != NULL) { if (ws != NULL) {
ws->layout = LAYOUT_MONOCLE; ws->layout = LAYOUT_MONOCLE;
workspace_arrange_current(); workspace_arrange_current();
@ -264,84 +506,144 @@ static void demo_phase_layouts(void)
"the full screen.\n\n" "the full screen.\n\n"
"Use Alt+Tab to switch between windows.\n" "Use Alt+Tab to switch between windows.\n"
"Great for focused, single-task work.", 0); "Great for focused, single-task work.", 0);
demo_wait_for(DEMO_WAIT_TIME, demo_get_step_delay());
demo_next_step(); demo_next_step();
}
break; break;
case 4: case 4:
if (time_in_step() > 4000) {
if (ws != NULL) { if (ws != NULL) {
ws->layout = LAYOUT_TILING; ws->layout = demo.original_layout;
workspace_arrange_current(); workspace_arrange_current();
} }
demo_next_phase(); demo_next_phase();
}
break; break;
} }
} }
static void demo_phase_snapping(void) static void demo_phase_snapping(void)
{ {
Workspace *ws;
if (!demo_check_wait_complete()) {
return;
}
switch (demo.step) { switch (demo.step) {
case 0: 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", demo_notify("Window Snapping",
"Quickly arrange windows with snapping:\n\n" "Quickly arrange windows with snapping:\n\n"
"- Super+Left: Snap to left half (50%)\n" "- Super+Left: Snap to left half (50%)\n"
"- Super+Right: Snap to right half (50%)\n\n" "- Super+Right: Snap to right half (50%)\n\n"
"Perfect for side-by-side comparisons\n" "Watch the focused window snap to the left...", 0);
"or working with multiple documents.", 0); demo_wait_for(DEMO_WAIT_TIME, demo_get_step_delay());
demo_next_step(); demo_next_step();
break; break;
case 1: case 2:
if (time_in_step() > 4000) { 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(); demo_next_phase();
}
break; break;
} }
} }
static void demo_phase_panels(void) static void demo_phase_panels(void)
{ {
if (!demo_check_wait_complete()) {
return;
}
switch (demo.step) { switch (demo.step) {
case 0: case 0:
demo_notify("Panel System", 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" "TOP PANEL (left to right):\n"
"- Workspace indicators (click to switch)\n" "- Workspace indicators (click to switch)\n"
"- Layout mode indicator\n" "- Layout mode indicator (T/F/M)\n"
"- Taskbar (click to focus/restore)\n" "- Taskbar with window titles\n"
"- AI status indicator\n" "- AI status indicator\n"
"- System tray icons\n\n" "- System tray icons", 0);
"BOTTOM PANEL:\n" demo_wait_for(DEMO_WAIT_TIME, demo_get_step_delay() + 1000);
"- Clock with date\n"
"- News ticker", 0);
demo_next_step(); demo_next_step();
break; break;
case 1: case 1:
if (time_in_step() > 5000) { demo_notify("Taskbar Features",
demo_notify("System Tray", "The taskbar shows all windows on the workspace:\n\n"
"The system tray shows:\n\n" "- Click a window title to focus it\n"
"- Battery level (color-coded)\n" "- Minimized windows shown in [brackets]\n"
"- WiFi status (click for network list)\n" "- Focused window is highlighted\n"
"- Volume (scroll to adjust, click to mute)\n" "- Right-click for window menu", 0);
"- External app icons (Telegram, etc.)\n\n" demo_wait_for(DEMO_WAIT_TIME, demo_get_step_delay());
"DWN implements the XEmbed protocol for\n"
"full compatibility with tray applications.", 0);
demo_next_step(); demo_next_step();
}
break; break;
case 2: 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(); demo_next_phase();
}
break; break;
} }
} }
static void demo_phase_ai(void) static void demo_phase_ai(void)
{ {
if (!demo_check_wait_complete()) {
return;
}
switch (demo.step) { switch (demo.step) {
case 0: case 0:
if (dwn->ai_enabled) {
demo_notify("AI Integration", demo_notify("AI Integration",
"DWN includes AI-powered features:\n\n" "DWN includes AI-powered features:\n\n"
"COMMAND PALETTE (Super+Shift+A):\n" "COMMAND PALETTE (Super+Shift+A):\n"
@ -350,34 +652,82 @@ static void demo_phase_ai(void)
"- Execute commands naturally\n\n" "- Execute commands naturally\n\n"
"CONTEXT ANALYSIS (Super+A):\n" "CONTEXT ANALYSIS (Super+A):\n"
"- See what you're working on\n" "- See what you're working on\n"
"- Get AI suggestions based on context\n\n" "- Get AI suggestions based on context", 0);
"EXA SEARCH (Super+Shift+E):\n" demo_wait_for(DEMO_WAIT_TIME, demo_get_step_delay() + 1000);
"- 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_next_step(); demo_next_step();
break; break;
case 1: case 1:
if (time_in_step() > 6000) { demo_notify("Live AI Demo",
demo_next_phase(); "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; break;
} }
} }
static void demo_phase_news(void) static void demo_phase_news(void)
{ {
if (!demo_check_wait_complete()) {
return;
}
switch (demo.step) { switch (demo.step) {
case 0: case 0:
demo_notify("News Ticker", demo_notify("News Ticker",
@ -391,39 +741,39 @@ static void demo_phase_news(void)
"- Super+Down: Next article\n" "- Super+Down: Next article\n"
"- Super+Up: Previous article\n" "- Super+Up: Previous article\n"
"- Super+Return: Open in browser", 0); "- Super+Return: Open in browser", 0);
demo_wait_for(DEMO_WAIT_TIME, demo_get_step_delay());
demo_next_step(); demo_next_step();
break; break;
case 1: case 1:
if (time_in_step() > 3000) {
news_next_article(); news_next_article();
demo_notify("News Navigation", "Moved to next article", 1500); demo_notify("News Navigation", "Moved to next article", 1500);
demo_wait_for(DEMO_WAIT_TIME, 2000);
demo_next_step(); demo_next_step();
}
break; break;
case 2: case 2:
if (time_in_step() > 2000) {
news_next_article(); news_next_article();
demo_notify("News Navigation", "Moved to next article", 1500); demo_notify("News Navigation", "Moved to next article", 1500);
demo_wait_for(DEMO_WAIT_TIME, 2000);
demo_next_step(); demo_next_step();
}
break; break;
case 3: case 3:
if (time_in_step() > 2000) {
news_prev_article(); news_prev_article();
demo_notify("News Navigation", "Moved to previous article", 1500); demo_notify("News Navigation", "Moved to previous article", 1500);
demo_wait_for(DEMO_WAIT_TIME, 2000);
demo_next_step(); demo_next_step();
}
break; break;
case 4: case 4:
if (time_in_step() > 2000) {
demo_next_phase(); demo_next_phase();
}
break; break;
} }
} }
static void demo_phase_shortcuts(void) static void demo_phase_shortcuts(void)
{ {
if (!demo_check_wait_complete()) {
return;
}
switch (demo.step) { switch (demo.step) {
case 0: case 0:
demo_notify("Quick Reference", demo_notify("Quick Reference",
@ -443,18 +793,21 @@ static void demo_phase_shortcuts(void)
"Super+Left Snap left\n" "Super+Left Snap left\n"
"Super+Right Snap right\n\n" "Super+Right Snap right\n\n"
"Press Super+S anytime to see all shortcuts.", 0); "Press Super+S anytime to see all shortcuts.", 0);
demo_wait_for(DEMO_WAIT_TIME, demo_get_step_delay() * 2);
demo_next_step(); demo_next_step();
break; break;
case 1: case 1:
if (time_in_step() > 8000) {
demo_next_phase(); demo_next_phase();
}
break; break;
} }
} }
static void demo_phase_complete(void) static void demo_phase_complete(void)
{ {
if (!demo_check_wait_complete()) {
return;
}
switch (demo.step) { switch (demo.step) {
case 0: case 0:
demo_notify("Demo Complete", demo_notify("Demo Complete",
@ -467,15 +820,14 @@ static void demo_phase_complete(void)
"Edit ~/.config/dwn/config to customize\n" "Edit ~/.config/dwn/config to customize\n"
"colors, fonts, keybindings, and more.\n\n" "colors, fonts, keybindings, and more.\n\n"
"Enjoy using DWN!", 0); "Enjoy using DWN!", 0);
demo_wait_for(DEMO_WAIT_TIME, demo_get_step_delay() + 2000);
demo_next_step(); demo_next_step();
break; break;
case 1: case 1:
if (time_in_step() > 6000) {
notification_close(demo.current_notification); notification_close(demo.current_notification);
demo.current_notification = 0; demo.current_notification = 0;
demo.active = false; demo.active = false;
demo.phase = DEMO_IDLE; demo.phase = DEMO_IDLE;
}
break; break;
} }
} }
@ -508,12 +860,28 @@ void demo_start(void)
return; return;
} }
memset(&demo, 0, sizeof(demo));
demo.active = true; demo.active = true;
demo.phase = DEMO_INTRO; demo.phase = DEMO_INTRO;
demo.step = 0; demo.step = 0;
demo.phase_start_time = get_time_ms(); demo.phase_start_time = get_time_ms();
demo.step_start_time = demo.phase_start_time; demo.step_start_time = demo.phase_start_time;
demo.demo_workspace_start = dwn->current_workspace; 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) void demo_stop(void)
@ -522,16 +890,30 @@ void demo_stop(void)
return; 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) { if (demo.current_notification > 0) {
notification_close(demo.current_notification); notification_close(demo.current_notification);
demo.current_notification = 0; 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_notify("Demo Stopped", "Demo mode has been stopped.", 2000);
demo.active = false; demo.active = false;
demo.phase = DEMO_IDLE; demo.phase = DEMO_IDLE;
demo.step = 0; demo.step = 0;
demo.wait_condition = DEMO_WAIT_NONE;
} }
void demo_update(void) void demo_update(void)

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) {
int delay = dwn->config->focus_follow_delay_ms;
if (delay <= 0) {
client_focus(c); 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;
}