feat: add XDG autostart support for automatic application launching

feat: implement demo mode for automated feature showcase
feat: add XEmbed system tray protocol for external application icons
feat: add window snapping to left and right halves
feat: add minimize/restore toggle with Alt+F9 keybinding
docs: update README with new features and configuration details
build: update build system for new autostart and demo modules
This commit is contained in:
2025-12-28 09:26:53 +01:00
parent c76a620012
commit d0b1669cb4
53 changed files with 2921 additions and 56 deletions
+69 -5
View File
@@ -16,12 +16,17 @@
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <pthread.h>
#include <stdatomic.h>
#define OPENROUTER_URL "https://openrouter.ai/api/v1/chat/completions"
static AIRequest *request_queue = NULL;
static CURLM *curl_multi = NULL;
static AIContext current_context;
static pthread_mutex_t curl_mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_mutex_t queue_mutex = PTHREAD_MUTEX_INITIALIZER;
static atomic_int ai_shutting_down = 0;
typedef struct {
char *data;
@@ -54,6 +59,8 @@ bool ai_init(void)
return false;
}
atomic_store(&ai_shutting_down, 0);
if (dwn->config->openrouter_api_key[0] == '\0') {
LOG_INFO("AI features disabled (no OPENROUTER_API_KEY)");
dwn->ai_enabled = false;
@@ -61,12 +68,15 @@ bool ai_init(void)
}
curl_global_init(CURL_GLOBAL_DEFAULT);
curl_multi = curl_multi_init();
pthread_mutex_lock(&curl_mutex);
curl_multi = curl_multi_init();
if (curl_multi == NULL) {
pthread_mutex_unlock(&curl_mutex);
LOG_ERROR("Failed to initialize curl multi handle");
return false;
}
pthread_mutex_unlock(&curl_mutex);
dwn->ai_enabled = true;
LOG_INFO("AI features enabled");
@@ -76,6 +86,9 @@ bool ai_init(void)
void ai_cleanup(void)
{
atomic_store(&ai_shutting_down, 1);
pthread_mutex_lock(&queue_mutex);
while (request_queue != NULL) {
AIRequest *next = request_queue->next;
if (request_queue->prompt) free(request_queue->prompt);
@@ -83,11 +96,14 @@ void ai_cleanup(void)
free(request_queue);
request_queue = next;
}
pthread_mutex_unlock(&queue_mutex);
pthread_mutex_lock(&curl_mutex);
if (curl_multi != NULL) {
curl_multi_cleanup(curl_multi);
curl_multi = NULL;
}
pthread_mutex_unlock(&curl_mutex);
curl_global_cleanup();
}
@@ -104,6 +120,10 @@ AIRequest *ai_send_request(const char *prompt, void (*callback)(AIRequest *))
return NULL;
}
if (atomic_load(&ai_shutting_down)) {
return NULL;
}
AIRequest *req = dwn_calloc(1, sizeof(AIRequest));
req->prompt = dwn_strdup(prompt);
req->state = AI_STATE_PENDING;
@@ -162,16 +182,21 @@ AIRequest *ai_send_request(const char *prompt, void (*callback)(AIRequest *))
curl_easy_setopt(easy, CURLOPT_SSL_VERIFYPEER, 1L);
curl_easy_setopt(easy, CURLOPT_SSL_VERIFYHOST, 2L);
curl_multi_add_handle(curl_multi, easy);
pthread_mutex_lock(&curl_mutex);
if (curl_multi != NULL) {
curl_multi_add_handle(curl_multi, easy);
}
pthread_mutex_unlock(&curl_mutex);
pthread_mutex_lock(&queue_mutex);
req->next = request_queue;
request_queue = req;
pthread_mutex_unlock(&queue_mutex);
req->user_data = response;
LOG_DEBUG("AI request sent: %.50s...", prompt);
return req;
}
@@ -181,6 +206,7 @@ void ai_cancel_request(AIRequest *req)
return;
}
pthread_mutex_lock(&queue_mutex);
AIRequest **pp = &request_queue;
while (*pp != NULL) {
if (*pp == req) {
@@ -189,6 +215,7 @@ void ai_cancel_request(AIRequest *req)
}
pp = &(*pp)->next;
}
pthread_mutex_unlock(&queue_mutex);
req->state = AI_STATE_ERROR;
@@ -204,7 +231,13 @@ void ai_cancel_request(AIRequest *req)
void ai_process_pending(void)
{
if (atomic_load(&ai_shutting_down)) {
return;
}
pthread_mutex_lock(&curl_mutex);
if (curl_multi == NULL) {
pthread_mutex_unlock(&curl_mutex);
return;
}
@@ -277,6 +310,7 @@ void ai_process_pending(void)
curl_easy_cleanup(easy);
}
}
pthread_mutex_unlock(&curl_mutex);
}
@@ -586,6 +620,10 @@ ExaRequest *exa_search(const char *query, void (*callback)(ExaRequest *))
return NULL;
}
if (atomic_load(&ai_shutting_down)) {
return NULL;
}
ExaRequest *req = dwn_calloc(1, sizeof(ExaRequest));
req->query = dwn_strdup(query);
req->state = AI_STATE_PENDING;
@@ -638,13 +676,20 @@ ExaRequest *exa_search(const char *query, void (*callback)(ExaRequest *))
curl_easy_setopt(easy, CURLOPT_SSL_VERIFYPEER, 1L);
curl_easy_setopt(easy, CURLOPT_SSL_VERIFYHOST, 2L);
pthread_mutex_lock(&curl_mutex);
if (curl_multi == NULL) {
curl_multi = curl_multi_init();
}
curl_multi_add_handle(curl_multi, easy);
if (curl_multi != NULL) {
curl_multi_add_handle(curl_multi, easy);
}
pthread_mutex_unlock(&curl_mutex);
pthread_mutex_lock(&queue_mutex);
req->next = exa_queue;
exa_queue = req;
pthread_mutex_unlock(&queue_mutex);
req->user_data = response;
LOG_DEBUG("Exa search sent: %s", query);
@@ -654,10 +699,24 @@ ExaRequest *exa_search(const char *query, void (*callback)(ExaRequest *))
void exa_process_pending(void)
{
if (curl_multi == NULL || exa_queue == NULL) {
if (atomic_load(&ai_shutting_down)) {
return;
}
pthread_mutex_lock(&curl_mutex);
if (curl_multi == NULL) {
pthread_mutex_unlock(&curl_mutex);
return;
}
pthread_mutex_lock(&queue_mutex);
if (exa_queue == NULL) {
pthread_mutex_unlock(&queue_mutex);
pthread_mutex_unlock(&curl_mutex);
return;
}
pthread_mutex_unlock(&queue_mutex);
int running_handles;
curl_multi_perform(curl_multi, &running_handles);
@@ -671,6 +730,7 @@ void exa_process_pending(void)
curl_easy_getinfo(easy, CURLINFO_PRIVATE, &req);
pthread_mutex_lock(&queue_mutex);
bool is_exa = false;
for (ExaRequest *r = exa_queue; r != NULL; r = r->next) {
if (r == req) {
@@ -678,6 +738,7 @@ void exa_process_pending(void)
break;
}
}
pthread_mutex_unlock(&queue_mutex);
if (is_exa && req != NULL) {
ResponseBuffer *buf = (ResponseBuffer *)req->user_data;
@@ -699,6 +760,7 @@ void exa_process_pending(void)
dwn_free(buf);
}
pthread_mutex_lock(&queue_mutex);
ExaRequest **pp = &exa_queue;
while (*pp != NULL) {
if (*pp == req) {
@@ -707,12 +769,14 @@ void exa_process_pending(void)
}
pp = &(*pp)->next;
}
pthread_mutex_unlock(&queue_mutex);
}
curl_multi_remove_handle(curl_multi, easy);
curl_easy_cleanup(easy);
}
}
pthread_mutex_unlock(&curl_mutex);
}
static void exa_launcher_callback(ExaRequest *req)
+73
View File
@@ -87,6 +87,8 @@ void atoms_init(Display *display)
ewmh.NET_SYSTEM_TRAY_OPCODE = ATOM("_NET_SYSTEM_TRAY_OPCODE");
ewmh.NET_SYSTEM_TRAY_S0 = ATOM("_NET_SYSTEM_TRAY_S0");
ewmh.NET_SYSTEM_TRAY_ORIENTATION = ATOM("_NET_SYSTEM_TRAY_ORIENTATION");
ewmh.NET_SYSTEM_TRAY_VISUAL = ATOM("_NET_SYSTEM_TRAY_VISUAL");
ewmh.MANAGER = ATOM("MANAGER");
ewmh.XEMBED = ATOM("_XEMBED");
ewmh.XEMBED_INFO = ATOM("_XEMBED_INFO");
@@ -437,3 +439,74 @@ void atoms_send_client_message(Window window, Atom message_type,
XSendEvent(dwn->display, dwn->root, False,
SubstructureNotifyMask | SubstructureRedirectMask, &ev);
}
bool atoms_update_wm_state(Window window, Atom state, bool add)
{
if (dwn == NULL || dwn->display == NULL || window == None) {
return false;
}
Atom actual_type;
int actual_format;
unsigned long nitems, bytes_after;
unsigned char *data = NULL;
Atom *states = NULL;
int state_count = 0;
if (XGetWindowProperty(dwn->display, window, ewmh.NET_WM_STATE,
0, 32, False, XA_ATOM,
&actual_type, &actual_format, &nitems, &bytes_after,
&data) == Success && data != NULL) {
states = (Atom *)data;
state_count = (int)nitems;
}
bool found = false;
int found_index = -1;
for (int i = 0; i < state_count; i++) {
if (states[i] == state) {
found = true;
found_index = i;
break;
}
}
if (add && found) {
if (data) XFree(data);
return true;
}
if (!add && !found) {
if (data) XFree(data);
return true;
}
Atom new_states[32];
int new_count = 0;
if (add) {
for (int i = 0; i < state_count && new_count < 31; i++) {
new_states[new_count++] = states[i];
}
if (new_count < 32) {
new_states[new_count++] = state;
}
} else {
for (int i = 0; i < state_count && new_count < 32; i++) {
if (i != found_index) {
new_states[new_count++] = states[i];
}
}
}
if (data) XFree(data);
if (new_count > 0) {
XChangeProperty(dwn->display, window, ewmh.NET_WM_STATE,
XA_ATOM, 32, PropModeReplace,
(unsigned char *)new_states, new_count);
} else {
XDeleteProperty(dwn->display, window, ewmh.NET_WM_STATE);
}
return true;
}
+290
View File
@@ -0,0 +1,290 @@
/*
* DWN - Desktop Window Manager
* retoor <retoor@molodetz.nl>
* XDG Autostart support
*/
#include "autostart.h"
#include "dwn.h"
#include "config.h"
#include "util.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dirent.h>
#include <unistd.h>
#include <sys/stat.h>
#include <limits.h>
#include <errno.h>
static void scan_xdg_directory(const char *dir_path);
static void scan_symlink_directory(const char *dir_path);
static int parse_desktop_file(const char *path, char *exec_cmd, size_t exec_size);
static void strip_exec_field_codes(char *exec);
void autostart_init(void)
{
if (dwn == NULL || dwn->config == NULL) {
return;
}
if (!dwn->config->autostart_enabled) {
LOG_DEBUG("Autostart disabled in config");
return;
}
if (dwn->config->autostart_path[0] != '\0') {
struct stat st;
if (stat(dwn->config->autostart_path, &st) != 0) {
if (mkdir(dwn->config->autostart_path, 0755) == 0) {
LOG_INFO("Created autostart directory: %s", dwn->config->autostart_path);
}
}
}
LOG_INFO("Autostart initialized");
}
void autostart_run(void)
{
if (dwn == NULL || dwn->config == NULL) {
return;
}
if (!dwn->config->autostart_enabled) {
return;
}
LOG_INFO("Running autostart applications");
if (dwn->config->autostart_xdg) {
scan_xdg_directory("/etc/xdg/autostart");
char *user_autostart = expand_path("~/.config/autostart");
if (user_autostart != NULL) {
scan_xdg_directory(user_autostart);
free(user_autostart);
}
}
if (dwn->config->autostart_path[0] != '\0') {
scan_symlink_directory(dwn->config->autostart_path);
}
LOG_INFO("Autostart complete");
}
void autostart_cleanup(void)
{
LOG_DEBUG("Autostart cleanup");
}
static void scan_xdg_directory(const char *dir_path)
{
if (dir_path == NULL) {
return;
}
DIR *dir = opendir(dir_path);
if (dir == NULL) {
LOG_DEBUG("Cannot open autostart directory: %s", dir_path);
return;
}
LOG_DEBUG("Scanning XDG autostart directory: %s", dir_path);
struct dirent *entry;
while ((entry = readdir(dir)) != NULL) {
if (entry->d_name[0] == '.') {
continue;
}
if (!str_ends_with(entry->d_name, ".desktop")) {
continue;
}
char full_path[PATH_MAX];
int ret = snprintf(full_path, sizeof(full_path), "%s/%s", dir_path, entry->d_name);
if (ret < 0 || (size_t)ret >= sizeof(full_path)) {
continue;
}
char exec_cmd[512];
if (parse_desktop_file(full_path, exec_cmd, sizeof(exec_cmd)) == 0) {
LOG_INFO("Autostart: %s -> %s", entry->d_name, exec_cmd);
spawn_async(exec_cmd);
}
}
closedir(dir);
}
static void scan_symlink_directory(const char *dir_path)
{
if (dir_path == NULL) {
return;
}
DIR *dir = opendir(dir_path);
if (dir == NULL) {
LOG_DEBUG("Cannot open symlink directory: %s", dir_path);
return;
}
LOG_DEBUG("Scanning symlink directory: %s", dir_path);
struct dirent *entry;
while ((entry = readdir(dir)) != NULL) {
if (entry->d_name[0] == '.') {
continue;
}
char full_path[PATH_MAX];
int ret = snprintf(full_path, sizeof(full_path), "%s/%s", dir_path, entry->d_name);
if (ret < 0 || (size_t)ret >= sizeof(full_path)) {
continue;
}
if (access(full_path, X_OK) != 0) {
LOG_DEBUG("Skipping non-executable: %s", entry->d_name);
continue;
}
LOG_INFO("Autostart (symlink): %s", entry->d_name);
spawn_async(full_path);
}
closedir(dir);
}
static int parse_desktop_file(const char *path, char *exec_cmd, size_t exec_size)
{
if (path == NULL || exec_cmd == NULL || exec_size == 0) {
return -1;
}
FILE *f = fopen(path, "r");
if (f == NULL) {
return -1;
}
char line[1024];
int in_desktop_entry = 0;
int hidden = 0;
int enabled = 1;
char try_exec[256] = {0};
exec_cmd[0] = '\0';
while (fgets(line, sizeof(line), f) != NULL) {
size_t len = strlen(line);
while (len > 0 && (line[len-1] == '\n' || line[len-1] == '\r')) {
line[--len] = '\0';
}
char *trimmed = str_trim(line);
if (trimmed[0] == '\0' || trimmed[0] == '#') {
continue;
}
if (strcmp(trimmed, "[Desktop Entry]") == 0) {
in_desktop_entry = 1;
continue;
}
if (trimmed[0] == '[') {
in_desktop_entry = 0;
continue;
}
if (!in_desktop_entry) {
continue;
}
if (strcmp(trimmed, "Hidden=true") == 0) {
hidden = 1;
} else if (strcmp(trimmed, "X-GNOME-Autostart-enabled=false") == 0) {
enabled = 0;
} else if (strncmp(trimmed, "TryExec=", 8) == 0) {
strncpy(try_exec, trimmed + 8, sizeof(try_exec) - 1);
try_exec[sizeof(try_exec) - 1] = '\0';
} else if (strncmp(trimmed, "Exec=", 5) == 0) {
strncpy(exec_cmd, trimmed + 5, exec_size - 1);
exec_cmd[exec_size - 1] = '\0';
} else if (strncmp(trimmed, "NotShowIn=", 10) == 0) {
if (strstr(trimmed + 10, "DWN") != NULL) {
hidden = 1;
}
} else if (strncmp(trimmed, "OnlyShowIn=", 11) == 0) {
if (strstr(trimmed + 11, "DWN") == NULL) {
hidden = 1;
}
}
}
fclose(f);
if (hidden || !enabled || exec_cmd[0] == '\0') {
exec_cmd[0] = '\0';
return -1;
}
if (try_exec[0] != '\0') {
char *space = strchr(try_exec, ' ');
if (space != NULL) {
*space = '\0';
}
if (access(try_exec, X_OK) != 0) {
char which_cmd[512];
snprintf(which_cmd, sizeof(which_cmd), "which %s >/dev/null 2>&1", try_exec);
if (system(which_cmd) != 0) {
exec_cmd[0] = '\0';
return -1;
}
}
}
strip_exec_field_codes(exec_cmd);
return 0;
}
static void strip_exec_field_codes(char *exec)
{
if (exec == NULL) {
return;
}
char result[512];
size_t j = 0;
for (size_t i = 0; exec[i] != '\0' && j < sizeof(result) - 1; i++) {
if (exec[i] == '%' && exec[i+1] != '\0') {
char code = exec[i+1];
if (code == 'f' || code == 'F' || code == 'u' || code == 'U' ||
code == 'd' || code == 'D' || code == 'n' || code == 'N' ||
code == 'i' || code == 'c' || code == 'k' || code == 'v' ||
code == 'm') {
i++;
if (exec[i+1] == ' ') {
i++;
}
continue;
}
}
result[j++] = exec[i];
}
result[j] = '\0';
while (j > 0 && (result[j-1] == ' ' || result[j-1] == '\t')) {
result[--j] = '\0';
}
size_t result_len = strlen(result);
if (result_len >= 512) {
result_len = 511;
}
memcpy(exec, result, result_len);
exec[result_len] = '\0';
}
+8 -5
View File
@@ -66,11 +66,8 @@ Client *client_create(Window window)
}
}
int target_width = (work_width * 75) / 100;
int target_height = (work_height * 75) / 100;
client->width = (orig_width > target_width) ? orig_width : target_width;
client->height = (orig_height > target_height) ? orig_height : target_height;
client->width = orig_width;
client->height = orig_height;
if (client->width > work_width - 20) client->width = work_width - 20;
if (client->height > work_height - 20) client->height = work_height - 20;
@@ -603,6 +600,7 @@ void client_set_fullscreen(Client *client, bool fullscreen)
}
client->flags |= CLIENT_FULLSCREEN;
atoms_update_wm_state(client->window, ewmh.NET_WM_STATE_FULLSCREEN, true);
client->x = 0;
client->y = 0;
@@ -619,8 +617,10 @@ void client_set_fullscreen(Client *client, bool fullscreen)
0, 0, client->width, client->height);
XMapWindow(dwn->display, client->window);
XRaiseWindow(dwn->display, client->window);
XSync(dwn->display, False);
} else {
client->flags &= ~CLIENT_FULLSCREEN;
atoms_update_wm_state(client->window, ewmh.NET_WM_STATE_FULLSCREEN, false);
client->x = client->old_x;
client->y = client->old_y;
@@ -637,6 +637,9 @@ void client_set_fullscreen(Client *client, bool fullscreen)
XMapWindow(dwn->display, client->frame);
}
client_configure(client);
decorations_render(client, true);
client_raise(client);
XSync(dwn->display, False);
}
}
+20
View File
@@ -75,6 +75,14 @@ void config_set_defaults(Config *cfg)
strncpy(cfg->config_path, "~/.config/dwn/config", sizeof(cfg->config_path) - 1);
strncpy(cfg->log_path, "~/.local/share/dwn/dwn.log", sizeof(cfg->log_path) - 1);
cfg->autostart_enabled = true;
cfg->autostart_xdg = true;
char *autostart_path = expand_path("~/.config/dwn/autostart.d");
if (autostart_path != NULL) {
strncpy(cfg->autostart_path, autostart_path, sizeof(cfg->autostart_path) - 1);
dwn_free(autostart_path);
}
}
@@ -152,6 +160,18 @@ static void handle_config_entry(const char *section, const char *key,
} else if (strcmp(key, "exa_api_key") == 0) {
strncpy(cfg->exa_api_key, value, sizeof(cfg->exa_api_key) - 1);
}
} else if (strcmp(section, "autostart") == 0) {
if (strcmp(key, "enabled") == 0) {
cfg->autostart_enabled = (strcmp(value, "true") == 0 || strcmp(value, "1") == 0);
} else if (strcmp(key, "xdg_autostart") == 0) {
cfg->autostart_xdg = (strcmp(value, "true") == 0 || strcmp(value, "1") == 0);
} else if (strcmp(key, "path") == 0) {
char *expanded = expand_path(value);
if (expanded != NULL) {
strncpy(cfg->autostart_path, expanded, sizeof(cfg->autostart_path) - 1);
dwn_free(expanded);
}
}
}
}
+12
View File
@@ -72,6 +72,9 @@ void decorations_render_title_bar(Client *client, bool focused)
int text_x = border + BUTTON_PADDING;
int max_width = client->width - 3 * (BUTTON_SIZE + BUTTON_PADDING) - 2 * BUTTON_PADDING;
if (max_width < BUTTON_SIZE) {
return;
}
char display_title[256];
strncpy(display_title, client->title, sizeof(display_title) - 4);
display_title[sizeof(display_title) - 4] = '\0';
@@ -138,6 +141,11 @@ void decorations_render_buttons(Client *client, bool focused)
return;
}
int min_buttons_width = 3 * (BUTTON_SIZE + BUTTON_PADDING) + BUTTON_PADDING;
if (client->width < min_buttons_width) {
return;
}
Display *dpy = dwn->display;
const ColorScheme *colors = config_get_colors();
int title_height = config_get_title_height();
@@ -148,6 +156,10 @@ void decorations_render_buttons(Client *client, bool focused)
int max_x = close_x - BUTTON_SIZE - BUTTON_PADDING;
int min_x = max_x - BUTTON_SIZE - BUTTON_PADDING;
if (close_x < border || max_x < border || min_x < border) {
return;
}
unsigned long bg_color = focused ? colors->title_focused_bg : colors->title_unfocused_bg;
XSetForeground(dpy, dwn->gc, bg_color);
+610
View File
@@ -0,0 +1,610 @@
/*
* DWN - Desktop Window Manager
* retoor <retoor@molodetz.nl>
* Demo mode - automated feature showcase
*/
#include "demo.h"
#include "dwn.h"
#include "notifications.h"
#include "workspace.h"
#include "client.h"
#include "layout.h"
#include "keys.h"
#include "ai.h"
#include "news.h"
#include "util.h"
#include "panel.h"
#include "config.h"
#include <string.h>
#define DEMO_STEP_DELAY 3000
typedef struct {
bool active;
DemoPhase phase;
int step;
long phase_start_time;
long step_start_time;
uint32_t current_notification;
Window demo_window;
int demo_workspace_start;
} DemoState;
static DemoState demo = {0};
static void demo_notify(const char *title, const char *body, int timeout)
{
if (demo.current_notification > 0) {
notification_close(demo.current_notification);
}
demo.current_notification = notification_show("DWN Demo", title, body, NULL, timeout);
}
static void demo_next_phase(void)
{
demo.phase++;
demo.step = 0;
demo.phase_start_time = get_time_ms();
demo.step_start_time = demo.phase_start_time;
}
static void demo_next_step(void)
{
demo.step++;
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)
{
switch (demo.step) {
case 0:
demo_notify("Welcome to DWN",
"DWN is a modern X11 window manager with:\n\n"
"- Tiling, floating, and monocle layouts\n"
"- 9 virtual workspaces\n"
"- AI-powered command palette\n"
"- News ticker with sentiment analysis\n"
"- System tray and notifications\n\n"
"This demo will showcase all features.\n"
"Press Super+Shift+D to stop at any time.", 0);
demo_next_step();
break;
case 1:
if (time_in_step() > 5000) {
demo_next_phase();
}
break;
}
}
static void demo_phase_window_mgmt(void)
{
Client *c;
Workspace *ws;
switch (demo.step) {
case 0:
demo_notify("Window Management",
"DWN provides complete window management:\n\n"
"- Alt+F4: Close window\n"
"- Alt+F9: Minimize/restore\n"
"- Alt+F10: Maximize\n"
"- Alt+F11: Fullscreen\n"
"- Alt+Tab: Cycle windows\n\n"
"Let's open a terminal...", 0);
demo_next_step();
break;
case 1:
if (time_in_step() > 3000) {
spawn_async("xterm -title 'Demo Terminal' -e 'echo DWN Demo Terminal; sleep 30' &");
demo_next_step();
}
break;
case 2:
if (time_in_step() > 2000) {
demo_notify("Window Created",
"A terminal window was spawned.\n\n"
"Windows can be moved by dragging the title bar\n"
"or resized by dragging edges.\n\n"
"Let's try minimizing...", 0);
demo_next_step();
}
break;
case 3:
if (time_in_step() > 3000) {
ws = workspace_get_current();
if (ws != NULL && ws->focused != NULL) {
demo.demo_window = ws->focused->window;
client_minimize(ws->focused);
demo_notify("Window Minimized",
"The window is now minimized.\n\n"
"Look at the taskbar - minimized windows\n"
"appear in [brackets] with a gray background.\n\n"
"Click it or press Alt+F9 to restore.", 0);
}
demo_next_step();
}
break;
case 4:
if (time_in_step() > 4000) {
c = client_find_by_window(demo.demo_window);
if (c != NULL) {
client_restore(c);
demo_notify("Window Restored",
"The window is restored from the taskbar.\n\n"
"This is how minimize/restore works in DWN.", 0);
}
demo_next_step();
}
break;
case 5:
if (time_in_step() > 3000) {
c = client_find_by_window(demo.demo_window);
if (c != NULL) {
client_close(c);
}
demo_next_phase();
}
break;
}
}
static void demo_phase_workspaces(void)
{
switch (demo.step) {
case 0:
demo.demo_workspace_start = dwn->current_workspace;
demo_notify("Virtual Workspaces",
"DWN provides 9 virtual workspaces:\n\n"
"- F1 to F9: Switch workspace\n"
"- Shift+F1-F9: Move window to workspace\n"
"- Ctrl+Alt+Left/Right: Navigate\n\n"
"Watch the workspace indicators in the panel...", 0);
demo_next_step();
break;
case 1:
if (time_in_step() > 3000) {
workspace_switch(1);
demo_notify("Workspace 2", "Switched to workspace 2", 1500);
demo_next_step();
}
break;
case 2:
if (time_in_step() > 2000) {
workspace_switch(2);
demo_notify("Workspace 3", "Switched to workspace 3", 1500);
demo_next_step();
}
break;
case 3:
if (time_in_step() > 2000) {
workspace_switch(3);
demo_notify("Workspace 4", "Switched to workspace 4", 1500);
demo_next_step();
}
break;
case 4:
if (time_in_step() > 2000) {
workspace_switch(demo.demo_workspace_start);
demo_notify("Workspaces Complete",
"Returned to the original workspace.\n\n"
"Each workspace maintains its own:\n"
"- Window layout and positions\n"
"- Focused window\n"
"- Layout mode (tiling/floating/monocle)", 0);
demo_next_step();
}
break;
case 5:
if (time_in_step() > 4000) {
demo_next_phase();
}
break;
}
}
static void demo_phase_layouts(void)
{
Workspace *ws = workspace_get_current();
switch (demo.step) {
case 0:
demo_notify("Layout Modes",
"DWN supports 3 layout modes:\n\n"
"1. Tiling: Windows auto-arrange without overlap\n"
"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_next_step();
break;
case 1:
if (time_in_step() > 4000) {
if (ws != NULL) {
ws->layout = LAYOUT_TILING;
workspace_arrange_current();
}
demo_notify("Tiling Layout",
"In tiling mode, windows are automatically\n"
"arranged in a main + stack pattern.\n\n"
"- Super+H/L: Resize main area\n"
"- Super+I/D: Add/remove from main\n\n"
"No overlapping windows - efficient use of space.", 0);
demo_next_step();
}
break;
case 2:
if (time_in_step() > 4000) {
if (ws != NULL) {
ws->layout = LAYOUT_FLOATING;
workspace_arrange_current();
}
demo_notify("Floating Layout",
"In floating mode, windows can be freely\n"
"positioned and resized anywhere.\n\n"
"This is the traditional desktop behavior\n"
"familiar from Windows and macOS.", 0);
demo_next_step();
}
break;
case 3:
if (time_in_step() > 4000) {
if (ws != NULL) {
ws->layout = LAYOUT_MONOCLE;
workspace_arrange_current();
}
demo_notify("Monocle Layout",
"In monocle mode, each window takes\n"
"the full screen.\n\n"
"Use Alt+Tab to switch between windows.\n"
"Great for focused, single-task work.", 0);
demo_next_step();
}
break;
case 4:
if (time_in_step() > 4000) {
if (ws != NULL) {
ws->layout = LAYOUT_TILING;
workspace_arrange_current();
}
demo_next_phase();
}
break;
}
}
static void demo_phase_snapping(void)
{
switch (demo.step) {
case 0:
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);
demo_next_step();
break;
case 1:
if (time_in_step() > 4000) {
demo_next_phase();
}
break;
}
}
static void demo_phase_panels(void)
{
switch (demo.step) {
case 0:
demo_notify("Panel System",
"DWN has two panels with rich functionality:\n\n"
"TOP PANEL (left to right):\n"
"- Workspace indicators (click to switch)\n"
"- Layout mode indicator\n"
"- Taskbar (click to focus/restore)\n"
"- AI status indicator\n"
"- System tray icons\n\n"
"BOTTOM PANEL:\n"
"- Clock with date\n"
"- News ticker", 0);
demo_next_step();
break;
case 1:
if (time_in_step() > 5000) {
demo_notify("System Tray",
"The system tray shows:\n\n"
"- Battery level (color-coded)\n"
"- WiFi status (click for network list)\n"
"- Volume (scroll to adjust, click to mute)\n"
"- External app icons (Telegram, etc.)\n\n"
"DWN implements the XEmbed protocol for\n"
"full compatibility with tray applications.", 0);
demo_next_step();
}
break;
case 2:
if (time_in_step() > 5000) {
demo_next_phase();
}
break;
}
}
static void demo_phase_ai(void)
{
switch (demo.step) {
case 0:
if (dwn->ai_enabled) {
demo_notify("AI Integration",
"DWN includes AI-powered features:\n\n"
"COMMAND PALETTE (Super+Shift+A):\n"
"- Ask AI to launch apps: 'open firefox'\n"
"- Get answers: 'what time is it'\n"
"- Execute commands naturally\n\n"
"CONTEXT ANALYSIS (Super+A):\n"
"- See what you're working on\n"
"- Get AI suggestions based on context\n\n"
"EXA SEARCH (Super+Shift+E):\n"
"- Semantic web search\n"
"- Find relevant content by meaning", 0);
} else {
demo_notify("AI Integration",
"DWN includes AI-powered features:\n\n"
"AI is currently DISABLED.\n"
"To enable, set these environment variables:\n\n"
"OPENROUTER_API_KEY=sk-or-v1-your-key\n"
"EXA_API_KEY=your-exa-key\n\n"
"Then restart DWN to enable:\n"
"- AI Command Palette (Super+Shift+A)\n"
"- Context Analysis (Super+A)\n"
"- Exa Semantic Search (Super+Shift+E)", 0);
}
demo_next_step();
break;
case 1:
if (time_in_step() > 6000) {
demo_next_phase();
}
break;
}
}
static void demo_phase_news(void)
{
switch (demo.step) {
case 0:
demo_notify("News Ticker",
"The bottom panel includes a news ticker:\n\n"
"- Headlines scroll automatically\n"
"- Color-coded by sentiment:\n"
" Green = Positive\n"
" Red = Negative\n"
" White = Neutral\n\n"
"CONTROLS:\n"
"- Super+Down: Next article\n"
"- Super+Up: Previous article\n"
"- Super+Return: Open in browser", 0);
demo_next_step();
break;
case 1:
if (time_in_step() > 3000) {
news_next_article();
demo_notify("News Navigation", "Moved to next article", 1500);
demo_next_step();
}
break;
case 2:
if (time_in_step() > 2000) {
news_next_article();
demo_notify("News Navigation", "Moved to next article", 1500);
demo_next_step();
}
break;
case 3:
if (time_in_step() > 2000) {
news_prev_article();
demo_notify("News Navigation", "Moved to previous article", 1500);
demo_next_step();
}
break;
case 4:
if (time_in_step() > 2000) {
demo_next_phase();
}
break;
}
}
static void demo_phase_shortcuts(void)
{
switch (demo.step) {
case 0:
demo_notify("Quick Reference",
"ESSENTIAL SHORTCUTS:\n\n"
"Ctrl+Alt+T Open terminal\n"
"Super App launcher\n"
"Alt+F4 Close window\n"
"Alt+Tab Cycle windows\n"
"Super+Space Cycle layouts\n\n"
"WORKSPACES:\n"
"F1-F9 Switch workspace\n"
"Shift+F1-F9 Move window\n\n"
"WINDOW CONTROL:\n"
"Alt+F9 Minimize/restore\n"
"Alt+F10 Maximize\n"
"Alt+F11 Fullscreen\n"
"Super+Left Snap left\n"
"Super+Right Snap right\n\n"
"Press Super+S anytime to see all shortcuts.", 0);
demo_next_step();
break;
case 1:
if (time_in_step() > 8000) {
demo_next_phase();
}
break;
}
}
static void demo_phase_complete(void)
{
switch (demo.step) {
case 0:
demo_notify("Demo Complete",
"Thank you for watching the DWN demo!\n\n"
"NEXT STEPS:\n"
"- Super+T: Start interactive tutorial\n"
"- Super+S: View all shortcuts\n"
"- Super+Shift+D: Run this demo again\n\n"
"CONFIGURATION:\n"
"Edit ~/.config/dwn/config to customize\n"
"colors, fonts, keybindings, and more.\n\n"
"Enjoy using DWN!", 0);
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;
}
}
void demo_init(void)
{
memset(&demo, 0, sizeof(demo));
demo.phase = DEMO_IDLE;
}
void demo_cleanup(void)
{
if (demo.current_notification > 0) {
notification_close(demo.current_notification);
}
memset(&demo, 0, sizeof(demo));
}
void demo_start(void)
{
if (demo.active) {
demo_stop();
return;
}
if (tutorial_is_active()) {
demo_notify("Demo Unavailable",
"Cannot start demo while tutorial is running.\n"
"Please complete or stop the tutorial first.", 3000);
return;
}
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;
}
void demo_stop(void)
{
if (!demo.active) {
return;
}
if (demo.current_notification > 0) {
notification_close(demo.current_notification);
demo.current_notification = 0;
}
demo_notify("Demo Stopped", "Demo mode has been stopped.", 2000);
demo.active = false;
demo.phase = DEMO_IDLE;
demo.step = 0;
}
void demo_update(void)
{
if (!demo.active) {
return;
}
switch (demo.phase) {
case DEMO_IDLE:
break;
case DEMO_INTRO:
demo_phase_intro();
break;
case DEMO_WINDOW_MGMT:
demo_phase_window_mgmt();
break;
case DEMO_WORKSPACES:
demo_phase_workspaces();
break;
case DEMO_LAYOUTS:
demo_phase_layouts();
break;
case DEMO_SNAPPING:
demo_phase_snapping();
break;
case DEMO_PANELS:
demo_phase_panels();
break;
case DEMO_AI:
demo_phase_ai();
break;
case DEMO_NEWS:
demo_phase_news();
break;
case DEMO_SHORTCUTS:
demo_phase_shortcuts();
break;
case DEMO_COMPLETE:
demo_phase_complete();
break;
}
}
bool demo_is_active(void)
{
return demo.active;
}
DemoPhase demo_get_phase(void)
{
return demo.phase;
}
int demo_get_step(void)
{
return demo.step;
}
const char *demo_get_phase_name(DemoPhase phase)
{
switch (phase) {
case DEMO_IDLE: return "Idle";
case DEMO_INTRO: return "Introduction";
case DEMO_WINDOW_MGMT: return "Window Management";
case DEMO_WORKSPACES: return "Workspaces";
case DEMO_LAYOUTS: return "Layouts";
case DEMO_SNAPPING: return "Snapping";
case DEMO_PANELS: return "Panels";
case DEMO_AI: return "AI Features";
case DEMO_NEWS: return "News Ticker";
case DEMO_SHORTCUTS: return "Shortcuts";
case DEMO_COMPLETE: return "Complete";
default: return "Unknown";
}
}
+122
View File
@@ -13,6 +13,8 @@
#include "notifications.h"
#include "news.h"
#include "applauncher.h"
#include "decorations.h"
#include "demo.h"
#include <stdio.h>
#include <string.h>
@@ -470,6 +472,8 @@ void keys_setup_defaults(void)
keys_bind(MOD_ALT, XK_F4, key_close_window, "Close window");
keys_bind(MOD_ALT, XK_F9, key_toggle_minimize, "Toggle minimize");
keys_bind(MOD_ALT, XK_F10, key_toggle_maximize, "Toggle maximize");
keys_bind(MOD_ALT, XK_F11, key_toggle_fullscreen, "Toggle fullscreen");
@@ -512,11 +516,16 @@ void keys_setup_defaults(void)
keys_bind(MOD_SUPER, XK_i, key_increase_master_count, "Increase master count");
keys_bind(MOD_SUPER, XK_d, key_decrease_master_count, "Decrease master count");
keys_bind(MOD_SUPER, XK_Left, key_snap_left, "Snap window left");
keys_bind(MOD_SUPER, XK_Right, key_snap_right, "Snap window right");
keys_bind(MOD_SUPER, XK_a, key_toggle_ai, "Toggle AI context");
keys_bind(MOD_SUPER | MOD_SHIFT, XK_a, key_ai_command, "AI command");
keys_bind(MOD_SUPER | MOD_SHIFT, XK_e, key_exa_search, "Exa web search");
keys_bind(MOD_SUPER | MOD_SHIFT, XK_d, key_start_demo, "Start/stop demo mode");
keys_bind(MOD_SUPER, XK_s, key_show_shortcuts, "Show shortcuts");
keys_bind(MOD_SUPER, XK_t, key_start_tutorial, "Start tutorial");
@@ -603,6 +612,20 @@ void key_toggle_maximize(void)
}
}
void key_toggle_minimize(void)
{
Workspace *ws = workspace_get_current();
if (ws == NULL || ws->focused == NULL) {
return;
}
if (client_is_minimized(ws->focused)) {
client_restore(ws->focused);
} else {
client_minimize(ws->focused);
}
}
void key_focus_next(void)
{
workspace_focus_next();
@@ -762,6 +785,7 @@ void key_show_shortcuts(void)
"Alt+F4 Close window\n"
"Alt+Tab Next window\n"
"Alt+Shift+Tab Previous window\n"
"Alt+F9 Toggle minimize\n"
"Alt+F10 Toggle maximize\n"
"Alt+F11 Toggle fullscreen\n"
"Super+F9 Toggle floating\n"
@@ -778,6 +802,8 @@ void key_show_shortcuts(void)
"Super+L Expand master area\n"
"Super+I Add to master\n"
"Super+D Remove from master\n"
"Super+Left Snap window left (50%)\n"
"Super+Right Snap window right (50%)\n"
"\n"
"=== AI Features ===\n"
"Super+A Show AI context\n"
@@ -787,6 +813,7 @@ void key_show_shortcuts(void)
"=== Help & System ===\n"
"Super+S Show shortcuts (this)\n"
"Super+T Interactive tutorial\n"
"Super+Shift+D Demo mode\n"
"Super+Backspace Quit DWN";
notification_show("DWN Shortcuts", "Complete Reference", shortcuts, NULL, 0);
@@ -821,3 +848,98 @@ void key_screenshot(void)
{
spawn_async("xfce4-screenshooter");
}
void key_snap_left(void)
{
if (dwn == NULL) {
return;
}
Workspace *ws = workspace_get_current();
if (ws == NULL || ws->focused == NULL) {
return;
}
Client *c = ws->focused;
int work_x = 0;
int work_y = 0;
int work_width = dwn->screen_width;
int work_height = dwn->screen_height;
if (dwn->config != NULL) {
if (dwn->config->top_panel_enabled) {
work_y += config_get_panel_height();
work_height -= config_get_panel_height();
}
if (dwn->config->bottom_panel_enabled) {
work_height -= config_get_panel_height();
}
}
int gap = config_get_gap();
if (!client_is_floating(c)) {
client_set_floating(c, true);
}
c->x = work_x + gap;
c->y = work_y + gap;
c->width = (work_width / 2) - (gap * 2);
c->height = work_height - (gap * 2);
client_configure(c);
decorations_render(c, true);
}
void key_snap_right(void)
{
if (dwn == NULL) {
return;
}
Workspace *ws = workspace_get_current();
if (ws == NULL || ws->focused == NULL) {
return;
}
Client *c = ws->focused;
int work_x = 0;
int work_y = 0;
int work_width = dwn->screen_width;
int work_height = dwn->screen_height;
if (dwn->config != NULL) {
if (dwn->config->top_panel_enabled) {
work_y += config_get_panel_height();
work_height -= config_get_panel_height();
}
if (dwn->config->bottom_panel_enabled) {
work_height -= config_get_panel_height();
}
}
int gap = config_get_gap();
if (!client_is_floating(c)) {
client_set_floating(c, true);
}
c->x = work_x + (work_width / 2) + gap;
c->y = work_y + gap;
c->width = (work_width / 2) - (gap * 2);
c->height = work_height - (gap * 2);
client_configure(c);
decorations_render(c, true);
}
void key_start_demo(void)
{
if (demo_is_active()) {
demo_stop();
} else {
demo_start();
}
}
+32
View File
@@ -18,6 +18,8 @@
#include "news.h"
#include "applauncher.h"
#include "ai.h"
#include "autostart.h"
#include "demo.h"
#include "util.h"
#include <stdio.h>
@@ -251,6 +253,11 @@ static void handle_destroy_notify(XDestroyWindowEvent *ev)
return;
}
if (xembed_find_icon(ev->window) != NULL) {
xembed_remove_icon(ev->window);
return;
}
Client *c = client_find_by_window(ev->window);
if (c != NULL) {
client_unmanage(c);
@@ -295,6 +302,13 @@ static void handle_property_notify(XPropertyEvent *ev)
return;
}
if (ev->atom == ewmh.XEMBED_INFO) {
if (xembed_find_icon(ev->window) != NULL) {
xembed_update_icon_state(ev->window);
return;
}
}
Client *c = client_find_by_window(ev->window);
if (c == NULL) {
return;
@@ -560,6 +574,12 @@ static void handle_client_message(XClientMessageEvent *ev)
if (desktop >= 0 && desktop < MAX_WORKSPACES) {
workspace_switch(desktop);
}
} else if (ev->message_type == ewmh.NET_SYSTEM_TRAY_OPCODE) {
long opcode = ev->data.l[1];
if (opcode == SYSTEM_TRAY_REQUEST_DOCK) {
Window icon_win = ev->data.l[2];
xembed_dock_icon(icon_win);
}
}
}
@@ -708,16 +728,23 @@ int dwn_init(void)
systray_init();
xembed_init();
news_init();
applauncher_init();
autostart_init();
autostart_run();
keys_init();
notifications_init();
ai_init();
demo_init();
atoms_setup_ewmh();
XSelectInput(dwn->display, dwn->root,
@@ -745,11 +772,14 @@ void dwn_cleanup(void)
{
LOG_INFO("DWN shutting down");
demo_cleanup();
ai_cleanup();
notifications_cleanup();
news_cleanup();
applauncher_cleanup();
autostart_cleanup();
keys_cleanup();
xembed_cleanup();
systray_cleanup();
panels_cleanup();
decorations_cleanup();
@@ -806,6 +836,8 @@ void dwn_run(void)
exa_process_pending();
demo_update();
notifications_update();
long now = get_time_ms();
+16 -19
View File
@@ -17,6 +17,7 @@
#include <unistd.h>
#include <sys/wait.h>
#include <pthread.h>
#include <stdatomic.h>
#include <curl/curl.h>
#include <X11/Xft/Xft.h>
@@ -41,7 +42,7 @@ NewsState news_state = {0};
static pthread_mutex_t news_mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_t fetch_thread;
static volatile int fetch_running = 0;
static atomic_int fetch_running = 0;
typedef struct {
char *data;
@@ -428,9 +429,9 @@ void news_init(void)
void news_cleanup(void)
{
if (fetch_running) {
if (atomic_load(&fetch_running)) {
pthread_join(fetch_thread, NULL);
fetch_running = 0;
atomic_store(&fetch_running, 0);
}
}
@@ -444,17 +445,17 @@ void news_fetch_async(void)
news_state.fetching = true;
pthread_mutex_unlock(&news_mutex);
if (fetch_running) {
if (atomic_load(&fetch_running)) {
pthread_join(fetch_thread, NULL);
}
fetch_running = 1;
atomic_store(&fetch_running, 1);
if (pthread_create(&fetch_thread, NULL, news_fetch_thread, NULL) != 0) {
pthread_mutex_lock(&news_mutex);
news_state.fetching = false;
news_state.has_error = true;
pthread_mutex_unlock(&news_mutex);
fetch_running = 0;
atomic_store(&fetch_running, 0);
}
}
@@ -578,6 +579,15 @@ void news_render(Panel *panel, int x, int max_width, int *used_width)
return;
}
int text_y;
if (dwn->xft_font != NULL) {
text_y = (panel->height + dwn->xft_font->ascent - dwn->xft_font->descent) / 2;
} else {
text_y = (panel->height + dwn->font->ascent - dwn->font->descent) / 2;
}
const ColorScheme *colors = config_get_colors();
pthread_mutex_lock(&news_mutex);
if (news_state.article_count == 0) {
@@ -603,21 +613,8 @@ void news_render(Panel *panel, int x, int max_width, int *used_width)
int separator_width = news_text_width(NEWS_SEPARATOR);
int scroll_offset = (int)news_state.scroll_offset % news_state.total_width;
pthread_mutex_unlock(&news_mutex);
const ColorScheme *colors = config_get_colors();
int text_y;
if (dwn->xft_font != NULL) {
text_y = (panel->height + dwn->xft_font->ascent - dwn->xft_font->descent) / 2;
} else {
text_y = (panel->height + dwn->font->ascent - dwn->font->descent) / 2;
}
XRectangle clip_rect = { x, 0, max_width, panel->height };
pthread_mutex_lock(&news_mutex);
int draw_x = x - scroll_offset;
int articles_drawn = 0;
int start_article = 0;
+42
View File
@@ -10,8 +10,21 @@
#include <string.h>
#include <stdlib.h>
#include <signal.h>
#include <setjmp.h>
#include <unistd.h>
#include <X11/Xft/Xft.h>
static sigjmp_buf dbus_timeout_jmp;
static volatile sig_atomic_t dbus_timeout_occurred = 0;
static void dbus_timeout_handler(int sig)
{
(void)sig;
dbus_timeout_occurred = 1;
siglongjmp(dbus_timeout_jmp, 1);
}
DBusConnection *dbus_conn = NULL;
static Notification *notification_list = NULL;
@@ -124,7 +137,27 @@ bool notifications_init(void)
DBusError err;
dbus_error_init(&err);
dbus_timeout_occurred = 0;
struct sigaction sa, old_sa;
sa.sa_handler = dbus_timeout_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGALRM, &sa, &old_sa);
if (sigsetjmp(dbus_timeout_jmp, 1) != 0) {
sigaction(SIGALRM, &old_sa, NULL);
alarm(0);
LOG_WARN("D-Bus connection timed out (5s) - notifications disabled");
return false;
}
alarm(5);
dbus_conn = dbus_bus_get(DBUS_BUS_SESSION, &err);
alarm(0);
sigaction(SIGALRM, &old_sa, NULL);
if (dbus_error_is_set(&err)) {
LOG_ERROR("D-Bus connection error: %s", err.message);
dbus_error_free(&err);
@@ -731,10 +764,19 @@ void notifications_position(void)
y += config_get_panel_height();
}
int max_y = dwn->screen_height - NOTIFICATION_MARGIN;
for (Notification *n = notification_list; n != NULL; n = n->next) {
if (y + n->height > max_y) {
if (n->window != None) {
XUnmapWindow(dwn->display, n->window);
}
continue;
}
if (n->window != None) {
int x = dwn->screen_width - n->width - NOTIFICATION_MARGIN;
XMoveWindow(dwn->display, n->window, x, y);
XMapRaised(dwn->display, n->window);
}
y += n->height + NOTIFICATION_MARGIN;
}
+59 -12
View File
@@ -346,11 +346,15 @@ void panel_render_taskbar(Panel *panel, int x, int *width)
int text_y = panel_text_y(panel->height);
int current_x = x;
int available_width = panel->width - x - 100 - PANEL_PADDING;
int systray_actual_width = systray_get_width();
int ai_width = dwn->ai_enabled ? 60 : 0;
int right_reserve = systray_actual_width + ai_width + PANEL_PADDING * 2;
int available_width = panel->width - x - right_reserve;
if (available_width < 0) available_width = 0;
int item_count = 0;
for (Client *c = dwn->client_list; c != NULL; c = c->next) {
if (c->workspace == (unsigned int)dwn->current_workspace && !client_is_minimized(c)) {
if (c->workspace == (unsigned int)dwn->current_workspace) {
item_count++;
}
}
@@ -368,20 +372,32 @@ void panel_render_taskbar(Panel *panel, int x, int *width)
Workspace *ws = workspace_get_current();
for (Client *c = dwn->client_list; c != NULL; c = c->next) {
if (c->workspace != (unsigned int)dwn->current_workspace || client_is_minimized(c)) {
if (c->workspace != (unsigned int)dwn->current_workspace) {
continue;
}
bool focused = (ws != NULL && ws->focused == c);
bool minimized = client_is_minimized(c);
unsigned long bg = focused ? colors->workspace_active : colors->panel_bg;
unsigned long bg;
if (minimized) {
bg = colors->workspace_inactive;
} else if (focused) {
bg = colors->workspace_active;
} else {
bg = colors->panel_bg;
}
XSetForeground(dpy, dwn->gc, bg);
XFillRectangle(dpy, panel->buffer, dwn->gc,
current_x, 2, item_width - 2, panel->height - 4);
char title[64];
strncpy(title, c->title, sizeof(title) - 4);
title[sizeof(title) - 4] = '\0';
if (minimized) {
snprintf(title, sizeof(title), "[%s]", c->title);
} else {
strncpy(title, c->title, sizeof(title) - 1);
title[sizeof(title) - 1] = '\0';
}
int max_text_width = item_width - 8;
bool truncated = false;
@@ -405,7 +421,14 @@ void panel_render_taskbar(Panel *panel, int x, int *width)
strncat(title, "...", sizeof(title) - strlen(title) - 1);
}
unsigned long fg = focused ? colors->panel_bg : colors->panel_fg;
unsigned long fg;
if (minimized) {
fg = colors->panel_fg;
} else if (focused) {
fg = colors->panel_bg;
} else {
fg = colors->panel_fg;
}
panel_draw_text(panel->buffer, current_x + 4, text_y, title, strlen(title), fg);
current_x += item_width;
@@ -506,7 +529,11 @@ void panel_handle_click(Panel *panel, int x, int y, int button)
Client *c = panel_hit_test_taskbar(panel, x, y);
if (c != NULL) {
if (button == 1) {
client_focus(c);
if (client_is_minimized(c)) {
client_restore(c);
} else {
client_focus(c);
}
} else if (button == 3) {
client_close(c);
}
@@ -542,16 +569,23 @@ Client *panel_hit_test_taskbar(Panel *panel, int x, int y)
return NULL;
}
int taskbar_start = PANEL_PADDING + MAX_WORKSPACES * WORKSPACE_WIDTH + WIDGET_SPACING + 50;
Workspace *ws = workspace_get_current();
const char *layout_symbol = layout_get_symbol(ws != NULL ? ws->layout : LAYOUT_TILING);
int layout_width = panel_text_width(layout_symbol, strlen(layout_symbol));
int taskbar_start = PANEL_PADDING + MAX_WORKSPACES * WORKSPACE_WIDTH + WIDGET_SPACING + layout_width + WIDGET_SPACING;
if (x < taskbar_start) {
return NULL;
}
int available_width = panel->width - taskbar_start - 100 - PANEL_PADDING;
int systray_actual_width = systray_get_width();
int ai_width = dwn->ai_enabled ? 60 : 0;
int right_reserve = systray_actual_width + ai_width + PANEL_PADDING * 2;
int available_width = panel->width - taskbar_start - right_reserve;
if (available_width < 0) available_width = 0;
int item_count = 0;
for (Client *c = dwn->client_list; c != NULL; c = c->next) {
if (c->workspace == (unsigned int)dwn->current_workspace && !client_is_minimized(c)) {
if (c->workspace == (unsigned int)dwn->current_workspace) {
item_count++;
}
}
@@ -569,7 +603,7 @@ Client *panel_hit_test_taskbar(Panel *panel, int x, int y)
int i = 0;
for (Client *c = dwn->client_list; c != NULL; c = c->next) {
if (c->workspace == (unsigned int)dwn->current_workspace && !client_is_minimized(c)) {
if (c->workspace == (unsigned int)dwn->current_workspace) {
if (i == index) {
return c;
}
@@ -615,6 +649,19 @@ void panel_toggle(Panel *panel)
}
}
void panel_raise_all(void)
{
if (dwn == NULL || dwn->display == NULL) {
return;
}
if (dwn->top_panel != NULL && dwn->top_panel->visible) {
XRaiseWindow(dwn->display, dwn->top_panel->window);
}
if (dwn->bottom_panel != NULL && dwn->bottom_panel->visible) {
XRaiseWindow(dwn->display, dwn->bottom_panel->window);
}
}
void panel_update_clock(void)
{
+390 -1
View File
@@ -9,6 +9,7 @@
#include "config.h"
#include "util.h"
#include "notifications.h"
#include "atoms.h"
#include <stdio.h>
#include <stdlib.h>
@@ -50,6 +51,10 @@ BatteryState battery_state = {0};
DropdownMenu *wifi_menu = NULL;
VolumeSlider *volume_slider = NULL;
TrayIcon tray_icons[MAX_TRAY_ICONS];
int tray_icon_count = 0;
Window tray_selection_owner = None;
static pthread_t update_thread;
static pthread_mutex_t state_mutex = PTHREAD_MUTEX_INITIALIZER;
static volatile int thread_running = 0;
@@ -58,6 +63,7 @@ static int systray_x = 0;
static int systray_width = 0;
static int wifi_icon_x = 0;
static int audio_icon_x = 0;
static int xembed_icons_x = 0;
static void wifi_update_state_internal(void);
static void audio_update_state_internal(void);
@@ -854,7 +860,13 @@ int systray_get_width(void)
snprintf(wifi_label, sizeof(wifi_label), "%s", wifi_icon_str);
}
return get_text_width(audio_label) + SYSTRAY_SPACING + get_text_width(wifi_label);
int builtin_width = get_text_width(audio_label) + SYSTRAY_SPACING + get_text_width(wifi_label);
int xembed_width = xembed_get_icons_width();
if (xembed_width > 0) {
return xembed_width + SYSTRAY_SPACING + builtin_width;
}
return builtin_width;
}
void systray_render(Panel *panel, int x, int *width)
@@ -885,6 +897,12 @@ void systray_render(Panel *panel, int x, int *width)
systray_x = x;
int current_x = x;
int xembed_width = xembed_get_icons_width();
if (xembed_width > 0) {
xembed_render_icons(panel, current_x);
current_x += xembed_width + SYSTRAY_SPACING;
}
audio_icon_x = current_x;
char audio_label[32];
const char *audio_icon = (audio_snap.muted || audio_snap.volume == 0) ? ASCII_VOL_MUTE : ASCII_VOL_HIGH;
@@ -915,6 +933,10 @@ void systray_render(Panel *panel, int x, int *width)
int systray_hit_test(int x)
{
int xembed_idx = xembed_hit_test(x);
if (xembed_idx >= 0) {
return 2;
}
if (x >= wifi_icon_x) {
return 0;
}
@@ -928,6 +950,11 @@ void systray_handle_click(int x, int y, int button)
{
int widget = systray_hit_test(x);
if (widget == 2) {
xembed_handle_click(x, y, button);
return;
}
if (widget == 0) {
if (volume_slider != NULL && volume_slider->visible) {
volume_slider_hide(volume_slider);
@@ -1034,3 +1061,365 @@ AudioState systray_get_audio_snapshot(void)
pthread_mutex_unlock(&state_mutex);
return snapshot;
}
void xembed_init(void)
{
if (dwn == NULL || dwn->display == NULL) {
return;
}
Display *dpy = dwn->display;
memset(tray_icons, 0, sizeof(tray_icons));
tray_icon_count = 0;
tray_selection_owner = XCreateSimpleWindow(dpy, dwn->root, -1, -1, 1, 1, 0, 0, 0);
if (tray_selection_owner == None) {
LOG_WARN("Failed to create tray selection owner window");
return;
}
Window existing = XGetSelectionOwner(dpy, ewmh.NET_SYSTEM_TRAY_S0);
if (existing != None) {
LOG_WARN("Another system tray is already running");
XDestroyWindow(dpy, tray_selection_owner);
tray_selection_owner = None;
return;
}
XSetSelectionOwner(dpy, ewmh.NET_SYSTEM_TRAY_S0, tray_selection_owner, CurrentTime);
if (XGetSelectionOwner(dpy, ewmh.NET_SYSTEM_TRAY_S0) != tray_selection_owner) {
LOG_WARN("Failed to acquire system tray selection");
XDestroyWindow(dpy, tray_selection_owner);
tray_selection_owner = None;
return;
}
long orientation = 0;
XChangeProperty(dpy, tray_selection_owner, ewmh.NET_SYSTEM_TRAY_ORIENTATION,
XA_CARDINAL, 32, PropModeReplace,
(unsigned char *)&orientation, 1);
VisualID visual_id = XVisualIDFromVisual(DefaultVisual(dpy, dwn->screen));
XChangeProperty(dpy, tray_selection_owner, ewmh.NET_SYSTEM_TRAY_VISUAL,
XA_VISUALID, 32, PropModeReplace,
(unsigned char *)&visual_id, 1);
XEvent ev;
memset(&ev, 0, sizeof(ev));
ev.xclient.type = ClientMessage;
ev.xclient.window = dwn->root;
ev.xclient.message_type = ewmh.MANAGER;
ev.xclient.format = 32;
ev.xclient.data.l[0] = CurrentTime;
ev.xclient.data.l[1] = ewmh.NET_SYSTEM_TRAY_S0;
ev.xclient.data.l[2] = tray_selection_owner;
ev.xclient.data.l[3] = 0;
ev.xclient.data.l[4] = 0;
XSendEvent(dpy, dwn->root, False, StructureNotifyMask, &ev);
XFlush(dpy);
LOG_INFO("XEmbed system tray initialized");
}
void xembed_cleanup(void)
{
if (dwn == NULL || dwn->display == NULL) {
return;
}
for (int i = 0; i < tray_icon_count; i++) {
if (tray_icons[i].window != None) {
XReparentWindow(dwn->display, tray_icons[i].window, dwn->root, 0, 0);
}
}
tray_icon_count = 0;
if (tray_selection_owner != None) {
XDestroyWindow(dwn->display, tray_selection_owner);
tray_selection_owner = None;
}
LOG_DEBUG("XEmbed system tray cleaned up");
}
void xembed_send_message(Window icon, long message, long detail, long data1, long data2)
{
if (dwn == NULL || dwn->display == NULL || icon == None) {
return;
}
XEvent ev;
memset(&ev, 0, sizeof(ev));
ev.xclient.type = ClientMessage;
ev.xclient.window = icon;
ev.xclient.message_type = ewmh.XEMBED;
ev.xclient.format = 32;
ev.xclient.data.l[0] = CurrentTime;
ev.xclient.data.l[1] = message;
ev.xclient.data.l[2] = detail;
ev.xclient.data.l[3] = data1;
ev.xclient.data.l[4] = data2;
XSendEvent(dwn->display, icon, False, NoEventMask, &ev);
}
static bool xembed_get_info(Window icon, unsigned long *version, unsigned long *flags)
{
if (dwn == NULL || dwn->display == NULL || icon == None) {
return false;
}
Atom actual_type;
int actual_format;
unsigned long nitems, bytes_after;
unsigned char *data = NULL;
if (XGetWindowProperty(dwn->display, icon, ewmh.XEMBED_INFO,
0, 2, False, ewmh.XEMBED_INFO,
&actual_type, &actual_format, &nitems, &bytes_after,
&data) != Success || data == NULL) {
return false;
}
if (nitems >= 2 && actual_format == 32) {
unsigned long *info = (unsigned long *)data;
if (version != NULL) *version = info[0];
if (flags != NULL) *flags = info[1];
XFree(data);
return true;
}
XFree(data);
return false;
}
bool xembed_dock_icon(Window icon_win)
{
if (dwn == NULL || dwn->display == NULL || dwn->top_panel == NULL) {
return false;
}
if (icon_win == None) {
return false;
}
if (tray_icon_count >= MAX_TRAY_ICONS) {
LOG_WARN("Maximum tray icons reached");
return false;
}
for (int i = 0; i < tray_icon_count; i++) {
if (tray_icons[i].window == icon_win) {
return true;
}
}
XWindowAttributes wa;
if (!XGetWindowAttributes(dwn->display, icon_win, &wa)) {
LOG_WARN("Failed to get tray icon attributes");
return false;
}
Display *dpy = dwn->display;
XSelectInput(dpy, icon_win, StructureNotifyMask | PropertyChangeMask);
XReparentWindow(dpy, icon_win, dwn->top_panel->window, 0, 0);
XResizeWindow(dpy, icon_win, TRAY_ICON_SIZE, TRAY_ICON_SIZE);
unsigned long version = 0, flags = 0;
bool has_info = xembed_get_info(icon_win, &version, &flags);
TrayIcon *icon = &tray_icons[tray_icon_count];
icon->window = icon_win;
icon->width = TRAY_ICON_SIZE;
icon->height = TRAY_ICON_SIZE;
icon->mapped = !has_info || (flags & XEMBED_MAPPED);
icon->x = 0;
tray_icon_count++;
xembed_send_message(icon_win, XEMBED_EMBEDDED_NOTIFY, 0,
dwn->top_panel->window, XEMBED_PROTOCOL_VERSION);
if (icon->mapped) {
XMapWindow(dpy, icon_win);
}
XFlush(dpy);
LOG_INFO("Docked tray icon: window=%lu, mapped=%d", icon_win, icon->mapped);
panel_render_all();
return true;
}
void xembed_remove_icon(Window icon_win)
{
if (icon_win == None) {
return;
}
int found = -1;
for (int i = 0; i < tray_icon_count; i++) {
if (tray_icons[i].window == icon_win) {
found = i;
break;
}
}
if (found < 0) {
return;
}
LOG_DEBUG("Removing tray icon: window=%lu", icon_win);
for (int i = found; i < tray_icon_count - 1; i++) {
tray_icons[i] = tray_icons[i + 1];
}
tray_icon_count--;
memset(&tray_icons[tray_icon_count], 0, sizeof(TrayIcon));
panel_render_all();
}
TrayIcon *xembed_find_icon(Window icon_win)
{
if (icon_win == None) {
return NULL;
}
for (int i = 0; i < tray_icon_count; i++) {
if (tray_icons[i].window == icon_win) {
return &tray_icons[i];
}
}
return NULL;
}
int xembed_get_icons_width(void)
{
if (tray_icon_count == 0) {
return 0;
}
return tray_icon_count * (TRAY_ICON_SIZE + TRAY_ICON_SPACING);
}
void xembed_render_icons(Panel *panel, int x)
{
if (panel == NULL || dwn == NULL || dwn->display == NULL) {
return;
}
Display *dpy = dwn->display;
int current_x = x;
int y = (panel->height - TRAY_ICON_SIZE) / 2;
xembed_icons_x = x;
for (int i = 0; i < tray_icon_count; i++) {
TrayIcon *icon = &tray_icons[i];
if (icon->window == None) {
continue;
}
icon->x = current_x;
if (icon->mapped) {
XMoveResizeWindow(dpy, icon->window, current_x, y,
TRAY_ICON_SIZE, TRAY_ICON_SIZE);
}
current_x += TRAY_ICON_SIZE + TRAY_ICON_SPACING;
}
}
int xembed_hit_test(int x)
{
if (tray_icon_count == 0) {
return -1;
}
for (int i = 0; i < tray_icon_count; i++) {
TrayIcon *icon = &tray_icons[i];
if (x >= icon->x && x < icon->x + TRAY_ICON_SIZE) {
return i;
}
}
return -1;
}
void xembed_handle_click(int x, int y, int button)
{
if (dwn == NULL || dwn->display == NULL) {
return;
}
int index = xembed_hit_test(x);
if (index < 0 || index >= tray_icon_count) {
return;
}
TrayIcon *icon = &tray_icons[index];
if (icon->window == None || !icon->mapped) {
return;
}
int icon_x = x - icon->x;
int icon_y = y;
XEvent ev;
memset(&ev, 0, sizeof(ev));
ev.xbutton.type = ButtonPress;
ev.xbutton.window = icon->window;
ev.xbutton.root = dwn->root;
ev.xbutton.subwindow = None;
ev.xbutton.time = CurrentTime;
ev.xbutton.x = icon_x;
ev.xbutton.y = icon_y;
ev.xbutton.x_root = x;
ev.xbutton.y_root = y;
ev.xbutton.state = 0;
ev.xbutton.button = button;
ev.xbutton.same_screen = True;
XSendEvent(dwn->display, icon->window, False, ButtonPressMask, &ev);
ev.xbutton.type = ButtonRelease;
XSendEvent(dwn->display, icon->window, False, ButtonReleaseMask, &ev);
XFlush(dwn->display);
}
void xembed_update_icon_state(Window icon_win)
{
TrayIcon *icon = xembed_find_icon(icon_win);
if (icon == NULL) {
return;
}
unsigned long version = 0, flags = 0;
if (!xembed_get_info(icon_win, &version, &flags)) {
return;
}
bool should_map = (flags & XEMBED_MAPPED) != 0;
if (should_map != icon->mapped) {
icon->mapped = should_map;
if (should_map) {
XMapWindow(dwn->display, icon_win);
} else {
XUnmapWindow(dwn->display, icon_win);
}
panel_render_all();
}
}
+4
View File
@@ -10,6 +10,7 @@
#include "atoms.h"
#include "util.h"
#include "config.h"
#include "panel.h"
#include <string.h>
#include <stdio.h>
@@ -106,6 +107,8 @@ void workspace_switch(int index)
atoms_set_current_desktop(index);
panel_raise_all();
XUngrabServer(dwn->display);
XSync(dwn->display, False);
@@ -324,6 +327,7 @@ void workspace_arrange_current(void)
{
XGrabServer(dwn->display);
workspace_arrange(dwn->current_workspace);
panel_raise_all();
XUngrabServer(dwn->display);
XSync(dwn->display, False);
}