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:
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user