chore: add MIT license, Makefile, and initial build artifacts for DWN project

This commit is contained in:
2025-12-28 02:14:31 +00:00
commit 21e8bcc88a
80 changed files with 21222 additions and 0 deletions
+872
View File
@@ -0,0 +1,872 @@
/*
* DWN - Desktop Window Manager
* AI Integration implementation
*/
#include "ai.h"
#include "config.h"
#include "client.h"
#include "workspace.h"
#include "notifications.h"
#include "util.h"
#include "cJSON.h"
#include <curl/curl.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
/* API endpoints */
#define OPENROUTER_URL "https://openrouter.ai/api/v1/chat/completions"
/* Request queue */
static AIRequest *request_queue = NULL;
static CURLM *curl_multi = NULL;
static AIContext current_context;
/* Response buffer for curl */
typedef struct {
char *data;
size_t size;
} ResponseBuffer;
/* ========== CURL callbacks ========== */
static size_t write_callback(void *contents, size_t size, size_t nmemb, void *userp)
{
size_t realsize = size * nmemb;
ResponseBuffer *buf = (ResponseBuffer *)userp;
char *ptr = realloc(buf->data, buf->size + realsize + 1);
if (ptr == NULL) {
return 0;
}
buf->data = ptr;
memcpy(&(buf->data[buf->size]), contents, realsize);
buf->size += realsize;
buf->data[buf->size] = '\0';
return realsize;
}
/* ========== Initialization ========== */
bool ai_init(void)
{
if (dwn == NULL || dwn->config == NULL) {
return false;
}
if (dwn->config->openrouter_api_key[0] == '\0') {
LOG_INFO("AI features disabled (no OPENROUTER_API_KEY)");
dwn->ai_enabled = false;
return true; /* Not an error, just disabled */
}
/* Initialize curl */
curl_global_init(CURL_GLOBAL_DEFAULT);
curl_multi = curl_multi_init();
if (curl_multi == NULL) {
LOG_ERROR("Failed to initialize curl multi handle");
return false;
}
dwn->ai_enabled = true;
LOG_INFO("AI features enabled");
return true;
}
void ai_cleanup(void)
{
/* Cancel all pending requests */
while (request_queue != NULL) {
AIRequest *next = request_queue->next;
if (request_queue->prompt) free(request_queue->prompt);
if (request_queue->response) free(request_queue->response);
free(request_queue);
request_queue = next;
}
if (curl_multi != NULL) {
curl_multi_cleanup(curl_multi);
curl_multi = NULL;
}
curl_global_cleanup();
}
bool ai_is_available(void)
{
return dwn != NULL && dwn->ai_enabled;
}
/* ========== API calls ========== */
AIRequest *ai_send_request(const char *prompt, void (*callback)(AIRequest *))
{
if (!ai_is_available() || prompt == NULL) {
return NULL;
}
AIRequest *req = dwn_calloc(1, sizeof(AIRequest));
req->prompt = dwn_strdup(prompt);
req->state = AI_STATE_PENDING;
req->callback = callback;
/* Build JSON request body */
char *json_prompt = dwn_malloc(strlen(prompt) * 2 + 256);
char *escaped_prompt = dwn_malloc(strlen(prompt) * 2 + 1);
/* Escape special characters in prompt */
const char *src = prompt;
char *dst = escaped_prompt;
while (*src) {
if (*src == '"' || *src == '\\' || *src == '\n' || *src == '\r' || *src == '\t') {
*dst++ = '\\';
if (*src == '\n') *dst++ = 'n';
else if (*src == '\r') *dst++ = 'r';
else if (*src == '\t') *dst++ = 't';
else *dst++ = *src;
} else {
*dst++ = *src;
}
src++;
}
*dst = '\0';
snprintf(json_prompt, strlen(prompt) * 2 + 256,
"{\"model\":\"%s\",\"messages\":[{\"role\":\"user\",\"content\":\"%s\"}]}",
dwn->config->ai_model, escaped_prompt);
dwn_free(escaped_prompt);
/* Create curl easy handle */
CURL *easy = curl_easy_init();
if (easy == NULL) {
dwn_free(json_prompt);
dwn_free(req->prompt);
dwn_free(req);
return NULL;
}
/* Response buffer */
ResponseBuffer *response = dwn_calloc(1, sizeof(ResponseBuffer));
/* Set curl options */
struct curl_slist *headers = NULL;
char auth_header[300];
snprintf(auth_header, sizeof(auth_header), "Authorization: Bearer %s",
dwn->config->openrouter_api_key);
headers = curl_slist_append(headers, "Content-Type: application/json");
headers = curl_slist_append(headers, auth_header);
curl_easy_setopt(easy, CURLOPT_URL, OPENROUTER_URL);
curl_easy_setopt(easy, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(easy, CURLOPT_POSTFIELDS, json_prompt);
curl_easy_setopt(easy, CURLOPT_WRITEFUNCTION, write_callback);
curl_easy_setopt(easy, CURLOPT_WRITEDATA, response);
curl_easy_setopt(easy, CURLOPT_TIMEOUT, 30L);
curl_easy_setopt(easy, CURLOPT_PRIVATE, req);
curl_easy_setopt(easy, CURLOPT_SSL_VERIFYPEER, 1L);
curl_easy_setopt(easy, CURLOPT_SSL_VERIFYHOST, 2L);
/* Add to multi handle */
curl_multi_add_handle(curl_multi, easy);
/* Add to queue */
req->next = request_queue;
request_queue = req;
/* Store response buffer pointer for cleanup */
req->user_data = response;
LOG_DEBUG("AI request sent: %.50s...", prompt);
/* Note: json_prompt and headers will be freed after request completes */
return req;
}
void ai_cancel_request(AIRequest *req)
{
if (req == NULL) {
return;
}
/* Remove from queue */
AIRequest **pp = &request_queue;
while (*pp != NULL) {
if (*pp == req) {
*pp = req->next;
break;
}
pp = &(*pp)->next;
}
req->state = AI_STATE_ERROR;
if (req->prompt) dwn_free(req->prompt);
if (req->response) dwn_free(req->response);
if (req->user_data) {
ResponseBuffer *buf = (ResponseBuffer *)req->user_data;
if (buf->data) free(buf->data);
dwn_free(buf);
}
dwn_free(req);
}
void ai_process_pending(void)
{
if (curl_multi == NULL) {
return;
}
int running_handles;
curl_multi_perform(curl_multi, &running_handles);
/* Check for completed requests */
CURLMsg *msg;
int msgs_left;
while ((msg = curl_multi_info_read(curl_multi, &msgs_left)) != NULL) {
if (msg->msg == CURLMSG_DONE) {
CURL *easy = msg->easy_handle;
AIRequest *req = NULL;
curl_easy_getinfo(easy, CURLINFO_PRIVATE, &req);
if (req != NULL) {
ResponseBuffer *buf = (ResponseBuffer *)req->user_data;
if (msg->data.result == CURLE_OK && buf != NULL && buf->data != NULL) {
/* Parse response using cJSON */
/* OpenRouter format: {"choices":[{"message":{"content":"..."}}]} */
cJSON *root = cJSON_Parse(buf->data);
if (root != NULL) {
cJSON *choices = cJSON_GetObjectItemCaseSensitive(root, "choices");
if (cJSON_IsArray(choices) && cJSON_GetArraySize(choices) > 0) {
cJSON *first_choice = cJSON_GetArrayItem(choices, 0);
cJSON *message = cJSON_GetObjectItemCaseSensitive(first_choice, "message");
if (message != NULL) {
cJSON *content = cJSON_GetObjectItemCaseSensitive(message, "content");
if (cJSON_IsString(content) && content->valuestring != NULL) {
req->response = dwn_strdup(content->valuestring);
req->state = AI_STATE_COMPLETED;
}
}
}
/* Check for error in response */
if (req->state != AI_STATE_COMPLETED) {
cJSON *error = cJSON_GetObjectItemCaseSensitive(root, "error");
if (error != NULL) {
cJSON *err_msg = cJSON_GetObjectItemCaseSensitive(error, "message");
if (cJSON_IsString(err_msg)) {
req->response = dwn_strdup(err_msg->valuestring);
req->state = AI_STATE_ERROR;
LOG_ERROR("AI API error: %s", err_msg->valuestring);
}
}
}
cJSON_Delete(root);
}
if (req->state != AI_STATE_COMPLETED && req->state != AI_STATE_ERROR) {
/* Fallback: return raw response for debugging */
req->response = dwn_strdup(buf->data);
req->state = AI_STATE_COMPLETED;
LOG_WARN("Could not parse AI response, returning raw");
}
} else {
req->state = AI_STATE_ERROR;
LOG_ERROR("AI request failed: %s", curl_easy_strerror(msg->data.result));
}
/* Call callback */
if (req->callback != NULL) {
req->callback(req);
}
/* Cleanup */
if (buf != NULL) {
if (buf->data) free(buf->data);
dwn_free(buf);
}
}
curl_multi_remove_handle(curl_multi, easy);
curl_easy_cleanup(easy);
}
}
}
/* ========== Context analysis ========== */
void ai_update_context(void)
{
memset(&current_context, 0, sizeof(current_context));
Workspace *ws = workspace_get_current();
if (ws != NULL && ws->focused != NULL) {
snprintf(current_context.focused_window, sizeof(current_context.focused_window),
"%s", ws->focused->title);
snprintf(current_context.focused_class, sizeof(current_context.focused_class),
"%s", ws->focused->class);
}
/* Build list of windows on current workspace */
int offset = 0;
for (Client *c = dwn->client_list; c != NULL; c = c->next) {
if (c->workspace == (unsigned int)dwn->current_workspace) {
int written = snprintf(current_context.workspace_windows + offset,
sizeof(current_context.workspace_windows) - offset,
"%s%s", offset > 0 ? ", " : "", c->title);
if (written > 0) {
offset += written;
}
current_context.window_count++;
}
}
}
const char *ai_analyze_task(void)
{
/* Analyze based on focused window class */
const char *class = current_context.focused_class;
if (strstr(class, "code") || strstr(class, "Code") ||
strstr(class, "vim") || strstr(class, "emacs")) {
return "coding";
}
if (strstr(class, "firefox") || strstr(class, "chrome") ||
strstr(class, "Firefox") || strstr(class, "Chrome")) {
return "browsing";
}
if (strstr(class, "slack") || strstr(class, "discord") ||
strstr(class, "Slack") || strstr(class, "Discord")) {
return "communication";
}
if (strstr(class, "terminal") || strstr(class, "Terminal")) {
return "terminal";
}
return "general";
}
const char *ai_suggest_window(void)
{
/* Simple heuristic suggestion */
const char *task = ai_analyze_task();
if (strcmp(task, "coding") == 0) {
return "Consider opening a terminal for testing";
}
if (strcmp(task, "browsing") == 0) {
return "Documentation or reference material available?";
}
return NULL;
}
const char *ai_suggest_app(void)
{
return NULL; /* Would require more context */
}
/* ========== Command palette ========== */
/* Callback for AI command response */
static void ai_command_response_callback(AIRequest *req)
{
if (req == NULL) {
return;
}
if (req->state == AI_STATE_COMPLETED && req->response != NULL) {
/* Check if response contains a command to execute */
/* Format: [RUN: command] or [EXEC: command] */
char *run_cmd = strstr(req->response, "[RUN:");
if (run_cmd == NULL) {
run_cmd = strstr(req->response, "[EXEC:");
}
if (run_cmd != NULL) {
/* Extract command */
char *cmd_start = strchr(run_cmd, ':');
if (cmd_start != NULL) {
cmd_start++;
while (*cmd_start == ' ') cmd_start++;
char *cmd_end = strchr(cmd_start, ']');
if (cmd_end != NULL) {
size_t cmd_len = cmd_end - cmd_start;
char *cmd = dwn_malloc(cmd_len + 1);
strncpy(cmd, cmd_start, cmd_len);
cmd[cmd_len] = '\0';
/* Trim trailing spaces */
while (cmd_len > 0 && cmd[cmd_len - 1] == ' ') {
cmd[--cmd_len] = '\0';
}
LOG_INFO("AI executing command: %s", cmd);
notification_show("DWN AI", "Running", cmd, NULL, 2000);
spawn_async(cmd);
dwn_free(cmd);
}
}
} else {
/* No command, just show response */
notification_show("DWN AI", "Response", req->response, NULL, 8000);
}
} else {
notification_show("DWN AI", "Error", "Failed to get AI response", NULL, 3000);
}
/* Cleanup - don't free req itself, it's managed by the queue */
/* The queue will be cleaned up separately */
}
void ai_show_command_palette(void)
{
if (!ai_is_available()) {
notification_show("DWN", "AI Unavailable",
"Set OPENROUTER_API_KEY to enable AI features",
NULL, 3000);
return;
}
/* Check if dmenu or rofi is available */
char *input = NULL;
/* Try dmenu first, then rofi */
if (spawn("command -v dmenu >/dev/null 2>&1") == 0) {
input = spawn_capture("echo '' | dmenu -p 'Ask AI:'");
} else if (spawn("command -v rofi >/dev/null 2>&1") == 0) {
input = spawn_capture("rofi -dmenu -p 'Ask AI:'");
} else {
notification_show("DWN AI", "Missing Dependency",
"Install dmenu or rofi for AI command palette:\n"
"sudo apt install dmenu",
NULL, 5000);
return;
}
if (input == NULL || input[0] == '\0') {
if (input != NULL) {
dwn_free(input);
}
LOG_DEBUG("AI command palette cancelled");
return;
}
LOG_DEBUG("AI command palette input: %s", input);
/* Show "thinking" notification */
notification_show("DWN AI", "Processing...", input, NULL, 2000);
/* Build context-aware prompt */
ai_update_context();
const char *task = ai_analyze_task();
char prompt[2048];
snprintf(prompt, sizeof(prompt),
"You are an AI assistant integrated into a Linux window manager called DWN. "
"You can execute shell commands for the user.\n\n"
"IMPORTANT: When the user asks you to run, open, launch, or start an application, "
"respond with the command in this exact format: [RUN: command]\n"
"Examples:\n"
"- User: 'open chrome' -> [RUN: google-chrome]\n"
"- User: 'run firefox' -> [RUN: firefox]\n"
"- User: 'open file manager' -> [RUN: thunar]\n"
"- User: 'launch terminal' -> [RUN: xfce4-terminal]\n"
"- User: 'open vs code' -> [RUN: code]\n\n"
"For questions or non-command requests, respond briefly (1-2 sentences) without the [RUN:] format.\n\n"
"User's current task: %s\n"
"User's request: %s",
task, input);
dwn_free(input);
/* Send request */
ai_send_request(prompt, ai_command_response_callback);
}
void ai_execute_command(const char *command)
{
if (!ai_is_available() || command == NULL) {
return;
}
LOG_DEBUG("AI executing command: %s", command);
/* Send to AI for interpretation */
char prompt[512];
snprintf(prompt, sizeof(prompt),
"User command: %s\nCurrent task: %s\nRespond with a single action to take.",
command, ai_analyze_task());
ai_send_request(prompt, NULL);
}
/* ========== Smart features ========== */
void ai_auto_organize_workspace(void)
{
LOG_DEBUG("AI auto-organize (placeholder)");
}
void ai_suggest_layout(void)
{
LOG_DEBUG("AI layout suggestion (placeholder)");
}
void ai_analyze_workflow(void)
{
LOG_DEBUG("AI workflow analysis (placeholder)");
}
/* ========== Notification intelligence ========== */
bool ai_should_show_notification(const char *app, const char *summary)
{
/* Simple filtering - could be enhanced with AI */
(void)app;
(void)summary;
return true; /* Show all by default */
}
int ai_notification_priority(const char *app, const char *summary)
{
/* Simple priority assignment */
if (strstr(summary, "urgent") || strstr(summary, "Urgent") ||
strstr(summary, "error") || strstr(summary, "Error")) {
return 3;
}
if (strstr(app, "slack") || strstr(app, "Slack") ||
strstr(app, "discord") || strstr(app, "Discord")) {
return 2;
}
return 1;
}
/* ========== Performance monitoring ========== */
void ai_monitor_performance(void)
{
/* Read from /proc for basic metrics */
LOG_DEBUG("AI performance monitoring (placeholder)");
}
const char *ai_performance_suggestion(void)
{
return NULL;
}
/* ========== Exa Semantic Search ========== */
#define EXA_API_URL "https://api.exa.ai/search"
static ExaRequest *exa_queue = NULL;
bool exa_is_available(void)
{
return dwn != NULL && dwn->config != NULL &&
dwn->config->exa_api_key[0] != '\0';
}
/* Parse Exa JSON response using cJSON */
static void exa_parse_response(ExaRequest *req, const char *json)
{
if (req == NULL || json == NULL) {
return;
}
req->result_count = 0;
cJSON *root = cJSON_Parse(json);
if (root == NULL) {
LOG_WARN("Failed to parse Exa response JSON");
return;
}
cJSON *results = cJSON_GetObjectItemCaseSensitive(root, "results");
if (!cJSON_IsArray(results)) {
cJSON_Delete(root);
return;
}
int array_size = cJSON_GetArraySize(results);
for (int i = 0; i < array_size && req->result_count < 10; i++) {
cJSON *item = cJSON_GetArrayItem(results, i);
if (!cJSON_IsObject(item)) continue;
ExaSearchResult *res = &req->results[req->result_count];
/* Extract title */
cJSON *title = cJSON_GetObjectItemCaseSensitive(item, "title");
if (cJSON_IsString(title) && title->valuestring != NULL) {
strncpy(res->title, title->valuestring, sizeof(res->title) - 1);
res->title[sizeof(res->title) - 1] = '\0';
}
/* Extract URL */
cJSON *url = cJSON_GetObjectItemCaseSensitive(item, "url");
if (cJSON_IsString(url) && url->valuestring != NULL) {
strncpy(res->url, url->valuestring, sizeof(res->url) - 1);
res->url[sizeof(res->url) - 1] = '\0';
}
/* Extract text/snippet if available */
cJSON *text = cJSON_GetObjectItemCaseSensitive(item, "text");
if (cJSON_IsString(text) && text->valuestring != NULL) {
strncpy(res->snippet, text->valuestring, sizeof(res->snippet) - 1);
res->snippet[sizeof(res->snippet) - 1] = '\0';
}
req->result_count++;
}
cJSON_Delete(root);
LOG_DEBUG("Parsed %d Exa results", req->result_count);
}
ExaRequest *exa_search(const char *query, void (*callback)(ExaRequest *))
{
if (!exa_is_available() || query == NULL) {
return NULL;
}
ExaRequest *req = dwn_calloc(1, sizeof(ExaRequest));
req->query = dwn_strdup(query);
req->state = AI_STATE_PENDING;
req->callback = callback;
req->result_count = 0;
/* Build JSON request */
char *json_query = dwn_malloc(strlen(query) * 2 + 256);
char *escaped = dwn_malloc(strlen(query) * 2 + 1);
/* Escape query string */
const char *src = query;
char *dst = escaped;
while (*src) {
if (*src == '"' || *src == '\\') {
*dst++ = '\\';
}
*dst++ = *src++;
}
*dst = '\0';
snprintf(json_query, strlen(query) * 2 + 256,
"{\"query\":\"%s\",\"type\":\"auto\",\"numResults\":10,\"contents\":{\"text\":true}}",
escaped);
dwn_free(escaped);
/* Create curl handle */
CURL *easy = curl_easy_init();
if (easy == NULL) {
dwn_free(json_query);
dwn_free(req->query);
dwn_free(req);
return NULL;
}
/* Response buffer */
ResponseBuffer *response = dwn_calloc(1, sizeof(ResponseBuffer));
/* Set headers */
struct curl_slist *headers = NULL;
char api_header[300];
snprintf(api_header, sizeof(api_header), "x-api-key: %s",
dwn->config->exa_api_key);
headers = curl_slist_append(headers, "Content-Type: application/json");
headers = curl_slist_append(headers, api_header);
curl_easy_setopt(easy, CURLOPT_URL, EXA_API_URL);
curl_easy_setopt(easy, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(easy, CURLOPT_POSTFIELDS, json_query);
curl_easy_setopt(easy, CURLOPT_WRITEFUNCTION, write_callback);
curl_easy_setopt(easy, CURLOPT_WRITEDATA, response);
curl_easy_setopt(easy, CURLOPT_TIMEOUT, 15L);
curl_easy_setopt(easy, CURLOPT_PRIVATE, req);
curl_easy_setopt(easy, CURLOPT_SSL_VERIFYPEER, 1L);
curl_easy_setopt(easy, CURLOPT_SSL_VERIFYHOST, 2L);
/* Add to multi handle */
if (curl_multi == NULL) {
curl_multi = curl_multi_init();
}
curl_multi_add_handle(curl_multi, easy);
/* Add to queue */
req->next = exa_queue;
exa_queue = req;
req->user_data = response;
LOG_DEBUG("Exa search sent: %s", query);
return req;
}
void exa_process_pending(void)
{
if (curl_multi == NULL || exa_queue == NULL) {
return;
}
int running_handles;
curl_multi_perform(curl_multi, &running_handles);
CURLMsg *msg;
int msgs_left;
while ((msg = curl_multi_info_read(curl_multi, &msgs_left)) != NULL) {
if (msg->msg == CURLMSG_DONE) {
CURL *easy = msg->easy_handle;
ExaRequest *req = NULL;
curl_easy_getinfo(easy, CURLINFO_PRIVATE, &req);
/* Check if this is an Exa request (in exa_queue) */
bool is_exa = false;
for (ExaRequest *r = exa_queue; r != NULL; r = r->next) {
if (r == req) {
is_exa = true;
break;
}
}
if (is_exa && req != NULL) {
ResponseBuffer *buf = (ResponseBuffer *)req->user_data;
if (msg->data.result == CURLE_OK && buf != NULL && buf->data != NULL) {
exa_parse_response(req, buf->data);
req->state = AI_STATE_COMPLETED;
} else {
req->state = AI_STATE_ERROR;
LOG_ERROR("Exa request failed: %s", curl_easy_strerror(msg->data.result));
}
if (req->callback != NULL) {
req->callback(req);
}
/* Cleanup buffer */
if (buf != NULL) {
if (buf->data) free(buf->data);
dwn_free(buf);
}
/* Remove from queue */
ExaRequest **pp = &exa_queue;
while (*pp != NULL) {
if (*pp == req) {
*pp = req->next;
break;
}
pp = &(*pp)->next;
}
}
curl_multi_remove_handle(curl_multi, easy);
curl_easy_cleanup(easy);
}
}
}
/* Callback for app launcher search */
static void exa_launcher_callback(ExaRequest *req)
{
if (req == NULL || req->state != AI_STATE_COMPLETED) {
notification_show("Exa Search", "Error", "Search failed", NULL, 3000);
return;
}
if (req->result_count == 0) {
notification_show("Exa Search", "No Results", req->query, NULL, 3000);
return;
}
/* Show results via dmenu/rofi - use bounded string operations */
size_t choices_size = req->result_count * 300;
char *choices = dwn_malloc(choices_size);
size_t offset = 0;
choices[0] = '\0';
for (int i = 0; i < req->result_count; i++) {
int written = snprintf(choices + offset, choices_size - offset,
"%s%s", offset > 0 ? "\n" : "", req->results[i].title);
if (written > 0 && (size_t)written < choices_size - offset) {
offset += written;
}
}
/* Show in dmenu - escape choices to prevent command injection */
char *escaped_choices = shell_escape(choices);
char *cmd = dwn_malloc(strlen(escaped_choices) + 64);
snprintf(cmd, strlen(escaped_choices) + 64, "echo %s | dmenu -l 10 -p 'Results:'", escaped_choices);
char *selected = spawn_capture(cmd);
dwn_free(cmd);
dwn_free(escaped_choices);
if (selected != NULL && selected[0] != '\0') {
/* Find which result was selected and open URL */
for (int i = 0; i < req->result_count; i++) {
if (strncmp(selected, req->results[i].title, strlen(req->results[i].title)) == 0) {
/* Escape URL to prevent command injection */
char *escaped_url = shell_escape(req->results[i].url);
char *open_cmd = dwn_malloc(strlen(escaped_url) + 32);
snprintf(open_cmd, strlen(escaped_url) + 32, "xdg-open %s &", escaped_url);
spawn_async(open_cmd);
dwn_free(open_cmd);
dwn_free(escaped_url);
break;
}
}
dwn_free(selected);
}
dwn_free(choices);
if (req->query) dwn_free(req->query);
dwn_free(req);
}
void exa_show_app_launcher(void)
{
if (!exa_is_available()) {
notification_show("Exa", "Unavailable",
"Set EXA_API_KEY in config to enable semantic search",
NULL, 3000);
return;
}
/* Get search query from user */
char *query = NULL;
if (spawn("command -v dmenu >/dev/null 2>&1") == 0) {
query = spawn_capture("echo '' | dmenu -p 'Exa Search:'");
} else if (spawn("command -v rofi >/dev/null 2>&1") == 0) {
query = spawn_capture("rofi -dmenu -p 'Exa Search:'");
} else {
notification_show("Exa", "Missing Dependency",
"Install dmenu or rofi", NULL, 3000);
return;
}
if (query == NULL || query[0] == '\0') {
if (query != NULL) dwn_free(query);
return;
}
/* Remove trailing newline */
query[strcspn(query, "\n")] = '\0';
notification_show("Exa", "Searching...", query, NULL, 2000);
exa_search(query, exa_launcher_callback);
}
+507
View File
@@ -0,0 +1,507 @@
/*
* DWN - Desktop Window Manager
* Application launcher with .desktop file support
*/
#include "applauncher.h"
#include "config.h"
#include "util.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dirent.h>
#include <sys/stat.h>
#include <unistd.h>
#include <ctype.h>
#include <pwd.h>
/* Global state */
static AppLauncherState launcher_state = {0};
/* Path to recent apps file */
static char recent_file_path[512] = {0};
/* Forward declarations */
static void scan_desktop_dir(const char *dir);
static int parse_desktop_file(const char *path, AppEntry *entry);
static void load_recent_apps(void);
static void save_recent_apps(void);
static void add_to_recent(const char *desktop_id);
static char *strip_field_codes(const char *exec);
static int compare_apps(const void *a, const void *b);
/* ========== Initialization ========== */
void applauncher_init(void)
{
memset(&launcher_state, 0, sizeof(launcher_state));
/* Set up recent file path */
const char *home = getenv("HOME");
if (home == NULL) {
struct passwd *pw = getpwuid(getuid());
home = pw ? pw->pw_dir : "/tmp";
}
snprintf(recent_file_path, sizeof(recent_file_path),
"%s/.local/share/dwn/recent_apps", home);
/* Ensure directory exists */
char dir_path[512];
snprintf(dir_path, sizeof(dir_path), "%s/.local/share/dwn", home);
mkdir(dir_path, 0755);
/* Load recent apps first */
load_recent_apps();
/* Scan application directories */
applauncher_refresh();
LOG_INFO("App launcher initialized with %d applications", launcher_state.app_count);
}
void applauncher_cleanup(void)
{
save_recent_apps();
}
void applauncher_refresh(void)
{
launcher_state.app_count = 0;
/* Scan system applications */
scan_desktop_dir("/usr/share/applications");
/* Scan user applications (takes precedence) */
const char *home = getenv("HOME");
if (home) {
char user_apps[512];
snprintf(user_apps, sizeof(user_apps), "%s/.local/share/applications", home);
scan_desktop_dir(user_apps);
}
/* Sort apps alphabetically by name */
qsort(launcher_state.apps, launcher_state.app_count,
sizeof(AppEntry), compare_apps);
LOG_DEBUG("Refreshed app list: %d applications found", launcher_state.app_count);
}
/* ========== Directory Scanning ========== */
static void scan_desktop_dir(const char *dir)
{
DIR *d = opendir(dir);
if (d == NULL) {
return;
}
struct dirent *entry;
while ((entry = readdir(d)) != NULL) {
if (launcher_state.app_count >= MAX_APPS) {
break;
}
/* Only process .desktop files */
size_t len = strlen(entry->d_name);
if (len < 9 || strcmp(entry->d_name + len - 8, ".desktop") != 0) {
continue;
}
/* Build full path */
char path[1024];
snprintf(path, sizeof(path), "%s/%s", dir, entry->d_name);
/* Check if already exists (user apps override system) */
bool exists = false;
for (int i = 0; i < launcher_state.app_count; i++) {
if (strcmp(launcher_state.apps[i].desktop_id, entry->d_name) == 0) {
exists = true;
break;
}
}
if (exists) {
continue;
}
/* Parse the desktop file */
AppEntry app = {0};
if (parse_desktop_file(path, &app) == 0) {
snprintf(app.desktop_id, sizeof(app.desktop_id), "%s", entry->d_name);
launcher_state.apps[launcher_state.app_count++] = app;
}
}
closedir(d);
}
/* ========== Desktop File Parsing ========== */
static int parse_desktop_file(const char *path, AppEntry *entry)
{
FILE *f = fopen(path, "r");
if (f == NULL) {
return -1;
}
char line[1024];
bool in_desktop_entry = false;
bool has_name = false;
bool has_exec = false;
entry->hidden = false;
entry->terminal = false;
while (fgets(line, sizeof(line), f) != NULL) {
/* Remove trailing whitespace/newline */
size_t len = strlen(line);
while (len > 0 && (line[len-1] == '\n' || line[len-1] == '\r' ||
line[len-1] == ' ' || line[len-1] == '\t')) {
line[--len] = '\0';
}
/* Check for section header */
if (line[0] == '[') {
in_desktop_entry = (strcmp(line, "[Desktop Entry]") == 0);
continue;
}
if (!in_desktop_entry) {
continue;
}
/* Parse key=value */
char *eq = strchr(line, '=');
if (eq == NULL) {
continue;
}
*eq = '\0';
char *key = line;
char *value = eq + 1;
/* Skip localized entries (Name[en]=...) */
if (strchr(key, '[') != NULL) {
continue;
}
if (strcmp(key, "Name") == 0 && !has_name) {
strncpy(entry->name, value, sizeof(entry->name) - 1);
has_name = true;
} else if (strcmp(key, "Exec") == 0 && !has_exec) {
strncpy(entry->exec, value, sizeof(entry->exec) - 1);
has_exec = true;
} else if (strcmp(key, "Icon") == 0) {
strncpy(entry->icon, value, sizeof(entry->icon) - 1);
} else if (strcmp(key, "Terminal") == 0) {
entry->terminal = (strcasecmp(value, "true") == 0);
} else if (strcmp(key, "Type") == 0) {
/* Skip non-Application types */
if (strcmp(value, "Application") != 0) {
fclose(f);
return -1;
}
} else if (strcmp(key, "Hidden") == 0 || strcmp(key, "NoDisplay") == 0) {
if (strcasecmp(value, "true") == 0) {
entry->hidden = true;
}
}
}
fclose(f);
/* Must have name and exec, and not be hidden */
if (!has_name || !has_exec || entry->hidden) {
return -1;
}
return 0;
}
/* ========== Recent Apps Management ========== */
static void load_recent_apps(void)
{
launcher_state.recent_count = 0;
FILE *f = fopen(recent_file_path, "r");
if (f == NULL) {
return;
}
char line[256];
while (fgets(line, sizeof(line), f) != NULL &&
launcher_state.recent_count < MAX_RECENT_APPS) {
/* Remove newline */
size_t len = strlen(line);
if (len > 0 && line[len-1] == '\n') {
line[len-1] = '\0';
}
if (line[0] != '\0') {
snprintf(launcher_state.recent[launcher_state.recent_count],
sizeof(launcher_state.recent[0]), "%s", line);
launcher_state.recent_count++;
}
}
fclose(f);
LOG_DEBUG("Loaded %d recent apps", launcher_state.recent_count);
}
static void save_recent_apps(void)
{
FILE *f = fopen(recent_file_path, "w");
if (f == NULL) {
LOG_WARN("Could not save recent apps to %s", recent_file_path);
return;
}
for (int i = 0; i < launcher_state.recent_count; i++) {
fprintf(f, "%s\n", launcher_state.recent[i]);
}
fclose(f);
}
static void add_to_recent(const char *desktop_id)
{
/* Remove if already in list */
for (int i = 0; i < launcher_state.recent_count; i++) {
if (strcmp(launcher_state.recent[i], desktop_id) == 0) {
/* Shift everything down */
for (int j = i; j > 0; j--) {
strcpy(launcher_state.recent[j], launcher_state.recent[j-1]);
}
strcpy(launcher_state.recent[0], desktop_id);
save_recent_apps();
return;
}
}
/* Add to front, shift others */
if (launcher_state.recent_count < MAX_RECENT_APPS) {
launcher_state.recent_count++;
}
for (int i = launcher_state.recent_count - 1; i > 0; i--) {
strcpy(launcher_state.recent[i], launcher_state.recent[i-1]);
}
strncpy(launcher_state.recent[0], desktop_id, sizeof(launcher_state.recent[0]) - 1);
save_recent_apps();
}
/* ========== Helper Functions ========== */
static char *strip_field_codes(const char *exec)
{
static char result[512];
size_t j = 0;
for (size_t i = 0; exec[i] && j < sizeof(result) - 1; i++) {
if (exec[i] == '%' && exec[i+1]) {
/* Skip field codes: %f, %F, %u, %U, %d, %D, %n, %N, %i, %c, %k */
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') {
i++; /* Skip the code character */
/* Also skip trailing space if any */
if (exec[i+1] == ' ') {
i++;
}
continue;
}
}
result[j++] = exec[i];
}
result[j] = '\0';
/* Trim trailing whitespace */
while (j > 0 && (result[j-1] == ' ' || result[j-1] == '\t')) {
result[--j] = '\0';
}
return result;
}
static int compare_apps(const void *a, const void *b)
{
const AppEntry *app_a = (const AppEntry *)a;
const AppEntry *app_b = (const AppEntry *)b;
return strcasecmp(app_a->name, app_b->name);
}
/* Find app by desktop_id */
static AppEntry *find_app_by_id(const char *desktop_id)
{
for (int i = 0; i < launcher_state.app_count; i++) {
if (strcmp(launcher_state.apps[i].desktop_id, desktop_id) == 0) {
return &launcher_state.apps[i];
}
}
return NULL;
}
/* Find app by name */
static AppEntry *find_app_by_name(const char *name)
{
for (int i = 0; i < launcher_state.app_count; i++) {
if (strcmp(launcher_state.apps[i].name, name) == 0) {
return &launcher_state.apps[i];
}
}
return NULL;
}
/* ========== Launcher Display ========== */
void applauncher_show(void)
{
if (launcher_state.app_count == 0) {
LOG_WARN("No applications found");
return;
}
/* Build the list for dmenu:
* - Recent apps first (if any)
* - Then all apps alphabetically
*/
size_t buf_size = launcher_state.app_count * 140;
char *choices = malloc(buf_size);
if (choices == NULL) {
LOG_ERROR("Failed to allocate memory for app list");
return;
}
choices[0] = '\0';
/* Track which apps we've added (to avoid duplicates) */
bool *added = calloc(launcher_state.app_count, sizeof(bool));
if (added == NULL) {
free(choices);
return;
}
size_t pos = 0;
/* Add recent apps first */
for (int i = 0; i < launcher_state.recent_count; i++) {
AppEntry *app = find_app_by_id(launcher_state.recent[i]);
if (app != NULL) {
/* Mark as added */
for (int j = 0; j < launcher_state.app_count; j++) {
if (&launcher_state.apps[j] == app) {
added[j] = true;
break;
}
}
size_t name_len = strlen(app->name);
if (pos + name_len + 2 < buf_size) {
memcpy(choices + pos, app->name, name_len);
pos += name_len;
choices[pos++] = '\n';
}
}
}
/* Add remaining apps */
for (int i = 0; i < launcher_state.app_count; i++) {
if (added[i]) {
continue;
}
size_t name_len = strlen(launcher_state.apps[i].name);
if (pos + name_len + 2 < buf_size) {
memcpy(choices + pos, launcher_state.apps[i].name, name_len);
pos += name_len;
choices[pos++] = '\n';
}
}
if (pos > 0) {
choices[pos-1] = '\0'; /* Remove trailing newline */
}
free(added);
/* Write choices to a temp file to avoid shell escaping issues */
char tmp_path[] = "/tmp/dwn_apps_XXXXXX";
int tmp_fd = mkstemp(tmp_path);
if (tmp_fd < 0) {
free(choices);
LOG_ERROR("Failed to create temp file for app list");
return;
}
ssize_t written = write(tmp_fd, choices, strlen(choices));
close(tmp_fd);
free(choices);
if (written < 0) {
unlink(tmp_path);
LOG_ERROR("Failed to write app list to temp file");
return;
}
/* Run dmenu */
char cmd[1024];
snprintf(cmd, sizeof(cmd),
"cat '%s' | dmenu -i -l 10 -p 'Run:'; rm -f '%s'",
tmp_path, tmp_path);
FILE *p = popen(cmd, "r");
if (p == NULL) {
LOG_ERROR("Failed to run dmenu");
return;
}
char selected[256] = {0};
if (fgets(selected, sizeof(selected), p) != NULL) {
/* Remove trailing newline */
size_t len = strlen(selected);
if (len > 0 && selected[len-1] == '\n') {
selected[len-1] = '\0';
}
}
pclose(p);
if (selected[0] == '\0') {
return; /* User cancelled */
}
/* Find and launch the selected app */
AppEntry *app = find_app_by_name(selected);
if (app != NULL) {
applauncher_launch(app->desktop_id);
} else {
/* User typed a custom command */
spawn_async(selected);
}
}
void applauncher_launch(const char *desktop_id)
{
AppEntry *app = find_app_by_id(desktop_id);
if (app == NULL) {
LOG_WARN("App not found: %s", desktop_id);
return;
}
/* Strip field codes from exec */
char *exec_cmd = strip_field_codes(app->exec);
/* Build command */
char cmd[1024];
if (app->terminal) {
const char *terminal = config_get_terminal();
snprintf(cmd, sizeof(cmd), "%s -e %s", terminal, exec_cmd);
} else {
snprintf(cmd, sizeof(cmd), "%s", exec_cmd);
}
LOG_INFO("Launching: %s (%s)", app->name, cmd);
spawn_async(cmd);
/* Add to recent apps */
add_to_recent(desktop_id);
}
+463
View File
@@ -0,0 +1,463 @@
/*
* DWN - Desktop Window Manager
* X11 Atoms management implementation
*/
#include "atoms.h"
#include "dwn.h"
#include "util.h"
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
/* Global atom containers */
EWMHAtoms ewmh;
ICCCMAtoms icccm;
MiscAtoms misc_atoms;
/* Helper macro for atom initialization */
#define ATOM(name) XInternAtom(display, name, False)
void atoms_init(Display *display)
{
LOG_DEBUG("Initializing X11 atoms");
/* EWMH root window properties */
ewmh.NET_SUPPORTED = ATOM("_NET_SUPPORTED");
ewmh.NET_SUPPORTING_WM_CHECK = ATOM("_NET_SUPPORTING_WM_CHECK");
ewmh.NET_CLIENT_LIST = ATOM("_NET_CLIENT_LIST");
ewmh.NET_CLIENT_LIST_STACKING = ATOM("_NET_CLIENT_LIST_STACKING");
ewmh.NET_NUMBER_OF_DESKTOPS = ATOM("_NET_NUMBER_OF_DESKTOPS");
ewmh.NET_DESKTOP_GEOMETRY = ATOM("_NET_DESKTOP_GEOMETRY");
ewmh.NET_DESKTOP_VIEWPORT = ATOM("_NET_DESKTOP_VIEWPORT");
ewmh.NET_CURRENT_DESKTOP = ATOM("_NET_CURRENT_DESKTOP");
ewmh.NET_DESKTOP_NAMES = ATOM("_NET_DESKTOP_NAMES");
ewmh.NET_ACTIVE_WINDOW = ATOM("_NET_ACTIVE_WINDOW");
ewmh.NET_WORKAREA = ATOM("_NET_WORKAREA");
/* EWMH client window properties */
ewmh.NET_WM_NAME = ATOM("_NET_WM_NAME");
ewmh.NET_WM_VISIBLE_NAME = ATOM("_NET_WM_VISIBLE_NAME");
ewmh.NET_WM_DESKTOP = ATOM("_NET_WM_DESKTOP");
ewmh.NET_WM_WINDOW_TYPE = ATOM("_NET_WM_WINDOW_TYPE");
ewmh.NET_WM_STATE = ATOM("_NET_WM_STATE");
ewmh.NET_WM_ALLOWED_ACTIONS = ATOM("_NET_WM_ALLOWED_ACTIONS");
ewmh.NET_WM_STRUT = ATOM("_NET_WM_STRUT");
ewmh.NET_WM_STRUT_PARTIAL = ATOM("_NET_WM_STRUT_PARTIAL");
ewmh.NET_WM_PID = ATOM("_NET_WM_PID");
/* Window types */
ewmh.NET_WM_WINDOW_TYPE_DESKTOP = ATOM("_NET_WM_WINDOW_TYPE_DESKTOP");
ewmh.NET_WM_WINDOW_TYPE_DOCK = ATOM("_NET_WM_WINDOW_TYPE_DOCK");
ewmh.NET_WM_WINDOW_TYPE_TOOLBAR = ATOM("_NET_WM_WINDOW_TYPE_TOOLBAR");
ewmh.NET_WM_WINDOW_TYPE_MENU = ATOM("_NET_WM_WINDOW_TYPE_MENU");
ewmh.NET_WM_WINDOW_TYPE_UTILITY = ATOM("_NET_WM_WINDOW_TYPE_UTILITY");
ewmh.NET_WM_WINDOW_TYPE_SPLASH = ATOM("_NET_WM_WINDOW_TYPE_SPLASH");
ewmh.NET_WM_WINDOW_TYPE_DIALOG = ATOM("_NET_WM_WINDOW_TYPE_DIALOG");
ewmh.NET_WM_WINDOW_TYPE_NORMAL = ATOM("_NET_WM_WINDOW_TYPE_NORMAL");
ewmh.NET_WM_WINDOW_TYPE_NOTIFICATION = ATOM("_NET_WM_WINDOW_TYPE_NOTIFICATION");
/* Window states */
ewmh.NET_WM_STATE_MODAL = ATOM("_NET_WM_STATE_MODAL");
ewmh.NET_WM_STATE_STICKY = ATOM("_NET_WM_STATE_STICKY");
ewmh.NET_WM_STATE_MAXIMIZED_VERT = ATOM("_NET_WM_STATE_MAXIMIZED_VERT");
ewmh.NET_WM_STATE_MAXIMIZED_HORZ = ATOM("_NET_WM_STATE_MAXIMIZED_HORZ");
ewmh.NET_WM_STATE_SHADED = ATOM("_NET_WM_STATE_SHADED");
ewmh.NET_WM_STATE_SKIP_TASKBAR = ATOM("_NET_WM_STATE_SKIP_TASKBAR");
ewmh.NET_WM_STATE_SKIP_PAGER = ATOM("_NET_WM_STATE_SKIP_PAGER");
ewmh.NET_WM_STATE_HIDDEN = ATOM("_NET_WM_STATE_HIDDEN");
ewmh.NET_WM_STATE_FULLSCREEN = ATOM("_NET_WM_STATE_FULLSCREEN");
ewmh.NET_WM_STATE_ABOVE = ATOM("_NET_WM_STATE_ABOVE");
ewmh.NET_WM_STATE_BELOW = ATOM("_NET_WM_STATE_BELOW");
ewmh.NET_WM_STATE_DEMANDS_ATTENTION = ATOM("_NET_WM_STATE_DEMANDS_ATTENTION");
ewmh.NET_WM_STATE_FOCUSED = ATOM("_NET_WM_STATE_FOCUSED");
/* Actions */
ewmh.NET_WM_ACTION_MOVE = ATOM("_NET_WM_ACTION_MOVE");
ewmh.NET_WM_ACTION_RESIZE = ATOM("_NET_WM_ACTION_RESIZE");
ewmh.NET_WM_ACTION_MINIMIZE = ATOM("_NET_WM_ACTION_MINIMIZE");
ewmh.NET_WM_ACTION_SHADE = ATOM("_NET_WM_ACTION_SHADE");
ewmh.NET_WM_ACTION_STICK = ATOM("_NET_WM_ACTION_STICK");
ewmh.NET_WM_ACTION_MAXIMIZE_HORZ = ATOM("_NET_WM_ACTION_MAXIMIZE_HORZ");
ewmh.NET_WM_ACTION_MAXIMIZE_VERT = ATOM("_NET_WM_ACTION_MAXIMIZE_VERT");
ewmh.NET_WM_ACTION_FULLSCREEN = ATOM("_NET_WM_ACTION_FULLSCREEN");
ewmh.NET_WM_ACTION_CHANGE_DESKTOP = ATOM("_NET_WM_ACTION_CHANGE_DESKTOP");
ewmh.NET_WM_ACTION_CLOSE = ATOM("_NET_WM_ACTION_CLOSE");
/* Client messages */
ewmh.NET_CLOSE_WINDOW = ATOM("_NET_CLOSE_WINDOW");
ewmh.NET_MOVERESIZE_WINDOW = ATOM("_NET_MOVERESIZE_WINDOW");
ewmh.NET_WM_MOVERESIZE = ATOM("_NET_WM_MOVERESIZE");
ewmh.NET_REQUEST_FRAME_EXTENTS = ATOM("_NET_REQUEST_FRAME_EXTENTS");
ewmh.NET_FRAME_EXTENTS = ATOM("_NET_FRAME_EXTENTS");
/* System tray */
ewmh.NET_SYSTEM_TRAY_OPCODE = ATOM("_NET_SYSTEM_TRAY_OPCODE");
ewmh.NET_SYSTEM_TRAY_S0 = ATOM("_NET_SYSTEM_TRAY_S0");
ewmh.MANAGER = ATOM("MANAGER");
ewmh.XEMBED = ATOM("_XEMBED");
ewmh.XEMBED_INFO = ATOM("_XEMBED_INFO");
/* ICCCM atoms */
icccm.WM_PROTOCOLS = ATOM("WM_PROTOCOLS");
icccm.WM_DELETE_WINDOW = ATOM("WM_DELETE_WINDOW");
icccm.WM_TAKE_FOCUS = ATOM("WM_TAKE_FOCUS");
icccm.WM_STATE = ATOM("WM_STATE");
icccm.WM_CHANGE_STATE = ATOM("WM_CHANGE_STATE");
icccm.WM_CLASS = ATOM("WM_CLASS");
icccm.WM_NAME = ATOM("WM_NAME");
icccm.WM_TRANSIENT_FOR = ATOM("WM_TRANSIENT_FOR");
icccm.WM_CLIENT_LEADER = ATOM("WM_CLIENT_LEADER");
icccm.WM_WINDOW_ROLE = ATOM("WM_WINDOW_ROLE");
/* Misc atoms */
misc_atoms.UTF8_STRING = ATOM("UTF8_STRING");
misc_atoms.COMPOUND_TEXT = ATOM("COMPOUND_TEXT");
misc_atoms.MOTIF_WM_HINTS = ATOM("_MOTIF_WM_HINTS");
misc_atoms.CLIPBOARD = ATOM("CLIPBOARD");
misc_atoms.PRIMARY = ATOM("PRIMARY");
misc_atoms.DWN_RESTART = ATOM("_DWN_RESTART");
LOG_DEBUG("X11 atoms initialized");
}
/* ========== EWMH Setup ========== */
void atoms_setup_ewmh(void)
{
if (dwn == NULL || dwn->display == NULL) {
return;
}
Display *dpy = dwn->display;
Window root = dwn->root;
/* Create a check window for _NET_SUPPORTING_WM_CHECK */
Window check = XCreateSimpleWindow(dpy, root, 0, 0, 1, 1, 0, 0, 0);
/* Set _NET_SUPPORTING_WM_CHECK on root and check window */
XChangeProperty(dpy, root, ewmh.NET_SUPPORTING_WM_CHECK,
XA_WINDOW, 32, PropModeReplace,
(unsigned char *)&check, 1);
XChangeProperty(dpy, check, ewmh.NET_SUPPORTING_WM_CHECK,
XA_WINDOW, 32, PropModeReplace,
(unsigned char *)&check, 1);
/* Set _NET_WM_NAME on check window */
XChangeProperty(dpy, check, ewmh.NET_WM_NAME,
misc_atoms.UTF8_STRING, 8, PropModeReplace,
(unsigned char *)DWN_NAME, strlen(DWN_NAME));
/* Set supported atoms */
Atom supported[] = {
ewmh.NET_SUPPORTED,
ewmh.NET_SUPPORTING_WM_CHECK,
ewmh.NET_CLIENT_LIST,
ewmh.NET_CLIENT_LIST_STACKING,
ewmh.NET_NUMBER_OF_DESKTOPS,
ewmh.NET_CURRENT_DESKTOP,
ewmh.NET_DESKTOP_NAMES,
ewmh.NET_ACTIVE_WINDOW,
ewmh.NET_WM_NAME,
ewmh.NET_WM_DESKTOP,
ewmh.NET_WM_WINDOW_TYPE,
ewmh.NET_WM_STATE,
ewmh.NET_WM_STATE_FULLSCREEN,
ewmh.NET_WM_STATE_HIDDEN,
ewmh.NET_WM_STATE_DEMANDS_ATTENTION,
ewmh.NET_CLOSE_WINDOW,
ewmh.NET_WM_STRUT_PARTIAL,
ewmh.NET_FRAME_EXTENTS
};
XChangeProperty(dpy, root, ewmh.NET_SUPPORTED,
XA_ATOM, 32, PropModeReplace,
(unsigned char *)supported, sizeof(supported) / sizeof(Atom));
/* Set number of desktops */
atoms_set_number_of_desktops(MAX_WORKSPACES);
/* Set current desktop */
atoms_set_current_desktop(0);
/* Update desktop names */
atoms_update_desktop_names();
LOG_INFO("EWMH compliance initialized");
}
void atoms_update_client_list(void)
{
if (dwn == NULL || dwn->display == NULL) {
return;
}
Window windows[MAX_CLIENTS];
int count = 0;
for (Client *c = dwn->client_list; c != NULL && count < MAX_CLIENTS; c = c->next) {
windows[count++] = c->window;
}
XChangeProperty(dwn->display, dwn->root, ewmh.NET_CLIENT_LIST,
XA_WINDOW, 32, PropModeReplace,
(unsigned char *)windows, count);
XChangeProperty(dwn->display, dwn->root, ewmh.NET_CLIENT_LIST_STACKING,
XA_WINDOW, 32, PropModeReplace,
(unsigned char *)windows, count);
}
void atoms_update_desktop_names(void)
{
if (dwn == NULL || dwn->display == NULL) {
return;
}
/* Build null-separated list of workspace names */
char names[MAX_WORKSPACES * 32];
int offset = 0;
for (int i = 0; i < MAX_WORKSPACES; i++) {
const char *name = dwn->workspaces[i].name;
if (name[0] == '\0') {
offset += snprintf(names + offset, sizeof(names) - offset, "%d", i + 1);
} else {
offset += snprintf(names + offset, sizeof(names) - offset, "%s", name);
}
names[offset++] = '\0';
}
XChangeProperty(dwn->display, dwn->root, ewmh.NET_DESKTOP_NAMES,
misc_atoms.UTF8_STRING, 8, PropModeReplace,
(unsigned char *)names, offset);
}
void atoms_set_current_desktop(int desktop)
{
if (dwn == NULL || dwn->display == NULL) {
return;
}
long data = desktop;
XChangeProperty(dwn->display, dwn->root, ewmh.NET_CURRENT_DESKTOP,
XA_CARDINAL, 32, PropModeReplace,
(unsigned char *)&data, 1);
}
void atoms_set_active_window(Window window)
{
if (dwn == NULL || dwn->display == NULL) {
return;
}
XChangeProperty(dwn->display, dwn->root, ewmh.NET_ACTIVE_WINDOW,
XA_WINDOW, 32, PropModeReplace,
(unsigned char *)&window, 1);
}
void atoms_set_number_of_desktops(int count)
{
if (dwn == NULL || dwn->display == NULL) {
return;
}
long data = count;
XChangeProperty(dwn->display, dwn->root, ewmh.NET_NUMBER_OF_DESKTOPS,
XA_CARDINAL, 32, PropModeReplace,
(unsigned char *)&data, 1);
}
/* ========== Window property helpers ========== */
bool atoms_get_window_type(Window window, Atom *type)
{
if (dwn == NULL || dwn->display == NULL || type == NULL) {
return false;
}
Atom actual_type;
int actual_format;
unsigned long nitems, bytes_after;
unsigned char *data = NULL;
if (XGetWindowProperty(dwn->display, window, ewmh.NET_WM_WINDOW_TYPE,
0, 1, False, XA_ATOM,
&actual_type, &actual_format, &nitems, &bytes_after,
&data) == Success && data != NULL) {
*type = *(Atom *)data;
XFree(data);
return true;
}
return false;
}
bool atoms_get_window_desktop(Window window, int *desktop)
{
if (dwn == NULL || dwn->display == NULL || desktop == NULL) {
return false;
}
Atom actual_type;
int actual_format;
unsigned long nitems, bytes_after;
unsigned char *data = NULL;
if (XGetWindowProperty(dwn->display, window, ewmh.NET_WM_DESKTOP,
0, 1, False, XA_CARDINAL,
&actual_type, &actual_format, &nitems, &bytes_after,
&data) == Success && data != NULL) {
*desktop = *(int *)data;
XFree(data);
return true;
}
return false;
}
bool atoms_set_window_desktop(Window window, int desktop)
{
if (dwn == NULL || dwn->display == NULL) {
return false;
}
long data = desktop;
XChangeProperty(dwn->display, window, ewmh.NET_WM_DESKTOP,
XA_CARDINAL, 32, PropModeReplace,
(unsigned char *)&data, 1);
return true;
}
char *atoms_get_window_name(Window window)
{
if (dwn == NULL || dwn->display == NULL) {
return NULL;
}
Display *dpy = dwn->display;
Atom actual_type;
int actual_format;
unsigned long nitems, bytes_after;
unsigned char *data = NULL;
/* Try _NET_WM_NAME first (UTF-8) */
if (XGetWindowProperty(dpy, window, ewmh.NET_WM_NAME,
0, 256, False, misc_atoms.UTF8_STRING,
&actual_type, &actual_format, &nitems, &bytes_after,
&data) == Success && data != NULL) {
char *name = dwn_strdup((char *)data);
XFree(data);
return name;
}
/* Fall back to WM_NAME */
if (XGetWindowProperty(dpy, window, icccm.WM_NAME,
0, 256, False, XA_STRING,
&actual_type, &actual_format, &nitems, &bytes_after,
&data) == Success && data != NULL) {
char *name = dwn_strdup((char *)data);
XFree(data);
return name;
}
/* Last resort: XFetchName */
char *name = NULL;
if (XFetchName(dpy, window, &name) && name != NULL) {
char *result = dwn_strdup(name);
XFree(name);
return result;
}
return dwn_strdup("Untitled");
}
bool atoms_get_wm_class(Window window, char *class_name, char *instance_name, size_t len)
{
if (dwn == NULL || dwn->display == NULL) {
return false;
}
XClassHint hint;
if (XGetClassHint(dwn->display, window, &hint)) {
if (class_name != NULL && hint.res_class != NULL) {
strncpy(class_name, hint.res_class, len - 1);
class_name[len - 1] = '\0';
}
if (instance_name != NULL && hint.res_name != NULL) {
strncpy(instance_name, hint.res_name, len - 1);
instance_name[len - 1] = '\0';
}
if (hint.res_class) XFree(hint.res_class);
if (hint.res_name) XFree(hint.res_name);
return true;
}
return false;
}
/* ========== Protocol helpers ========== */
bool atoms_window_supports_protocol(Window window, Atom protocol)
{
if (dwn == NULL || dwn->display == NULL) {
return false;
}
Atom *protocols = NULL;
int count = 0;
if (XGetWMProtocols(dwn->display, window, &protocols, &count)) {
for (int i = 0; i < count; i++) {
if (protocols[i] == protocol) {
XFree(protocols);
return true;
}
}
XFree(protocols);
}
return false;
}
void atoms_send_protocol(Window window, Atom protocol, Time timestamp)
{
if (dwn == NULL || dwn->display == NULL) {
return;
}
XEvent ev;
memset(&ev, 0, sizeof(ev));
ev.type = ClientMessage;
ev.xclient.window = window;
ev.xclient.message_type = icccm.WM_PROTOCOLS;
ev.xclient.format = 32;
ev.xclient.data.l[0] = protocol;
ev.xclient.data.l[1] = timestamp;
XSendEvent(dwn->display, window, False, NoEventMask, &ev);
}
void atoms_send_client_message(Window window, Atom message_type,
long data0, long data1, long data2, long data3, long data4)
{
if (dwn == NULL || dwn->display == NULL) {
return;
}
XEvent ev;
memset(&ev, 0, sizeof(ev));
ev.type = ClientMessage;
ev.xclient.window = window;
ev.xclient.message_type = message_type;
ev.xclient.format = 32;
ev.xclient.data.l[0] = data0;
ev.xclient.data.l[1] = data1;
ev.xclient.data.l[2] = data2;
ev.xclient.data.l[3] = data3;
ev.xclient.data.l[4] = data4;
XSendEvent(dwn->display, dwn->root, False,
SubstructureNotifyMask | SubstructureRedirectMask, &ev);
}
+3191
View File
File diff suppressed because it is too large Load Diff
+1110
View File
File diff suppressed because it is too large Load Diff
+361
View File
@@ -0,0 +1,361 @@
/*
* DWN - Desktop Window Manager
* Configuration system implementation
*/
#include "config.h"
#include "util.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
/* Default color values (dark modern theme) */
#define DEFAULT_PANEL_BG "#1a1a2e"
#define DEFAULT_PANEL_FG "#e0e0e0"
#define DEFAULT_WS_ACTIVE "#4a90d9"
#define DEFAULT_WS_INACTIVE "#3a3a4e"
#define DEFAULT_WS_URGENT "#d94a4a"
#define DEFAULT_TITLE_FOCUSED_BG "#2d3a4a"
#define DEFAULT_TITLE_FOCUSED_FG "#ffffff"
#define DEFAULT_TITLE_UNFOCUSED_BG "#1a1a1a"
#define DEFAULT_TITLE_UNFOCUSED_FG "#808080"
#define DEFAULT_BORDER_FOCUSED "#4a90d9"
#define DEFAULT_BORDER_UNFOCUSED "#333333"
#define DEFAULT_NOTIFICATION_BG "#2a2a3e"
#define DEFAULT_NOTIFICATION_FG "#ffffff"
/* ========== Configuration creation/destruction ========== */
Config *config_create(void)
{
Config *cfg = dwn_calloc(1, sizeof(Config));
config_set_defaults(cfg);
return cfg;
}
void config_destroy(Config *cfg)
{
if (cfg != NULL) {
/* Securely wipe API keys before freeing */
secure_wipe(cfg->openrouter_api_key, sizeof(cfg->openrouter_api_key));
secure_wipe(cfg->exa_api_key, sizeof(cfg->exa_api_key));
dwn_free(cfg);
}
}
void config_set_defaults(Config *cfg)
{
if (cfg == NULL) {
return;
}
/* General */
strncpy(cfg->terminal, "xfce4-terminal", sizeof(cfg->terminal) - 1);
strncpy(cfg->launcher, "dmenu_run", sizeof(cfg->launcher) - 1);
strncpy(cfg->file_manager, "thunar", sizeof(cfg->file_manager) - 1);
cfg->focus_mode = FOCUS_CLICK;
cfg->show_decorations = true;
/* Appearance */
cfg->border_width = DEFAULT_BORDER_WIDTH;
cfg->title_height = DEFAULT_TITLE_HEIGHT;
cfg->panel_height = DEFAULT_PANEL_HEIGHT;
cfg->gap = DEFAULT_GAP;
strncpy(cfg->font_name, "fixed", sizeof(cfg->font_name) - 1);
/* Layout */
cfg->default_master_ratio = 0.55f;
cfg->default_master_count = 1;
cfg->default_layout = LAYOUT_TILING;
/* Panels */
cfg->top_panel_enabled = true;
cfg->bottom_panel_enabled = true;
/* AI (disabled by default, enabled if API key found) */
cfg->openrouter_api_key[0] = '\0';
cfg->exa_api_key[0] = '\0';
strncpy(cfg->ai_model, "google/gemini-2.0-flash-exp:free", sizeof(cfg->ai_model) - 1);
cfg->ai_enabled = false;
/* Paths */
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);
}
/* ========== INI Parser ========== */
typedef struct {
Config *cfg;
char current_section[64];
} ParseContext;
static void handle_config_entry(const char *section, const char *key,
const char *value, void *user_data)
{
ParseContext *ctx = (ParseContext *)user_data;
Config *cfg = ctx->cfg;
if (strcmp(section, "general") == 0) {
if (strcmp(key, "terminal") == 0) {
strncpy(cfg->terminal, value, sizeof(cfg->terminal) - 1);
} else if (strcmp(key, "launcher") == 0) {
strncpy(cfg->launcher, value, sizeof(cfg->launcher) - 1);
} else if (strcmp(key, "file_manager") == 0) {
strncpy(cfg->file_manager, value, sizeof(cfg->file_manager) - 1);
} else if (strcmp(key, "focus_mode") == 0) {
if (strcmp(value, "follow") == 0) {
cfg->focus_mode = FOCUS_FOLLOW;
} else {
cfg->focus_mode = FOCUS_CLICK;
}
} else if (strcmp(key, "decorations") == 0) {
cfg->show_decorations = (strcmp(value, "true") == 0 || strcmp(value, "1") == 0);
}
} else if (strcmp(section, "appearance") == 0) {
if (strcmp(key, "border_width") == 0) {
int val = atoi(value);
cfg->border_width = (val >= 0 && val <= 50) ? val : DEFAULT_BORDER_WIDTH;
} else if (strcmp(key, "title_height") == 0) {
int val = atoi(value);
cfg->title_height = (val >= 0 && val <= 100) ? val : DEFAULT_TITLE_HEIGHT;
} else if (strcmp(key, "panel_height") == 0) {
int val = atoi(value);
cfg->panel_height = (val >= 0 && val <= 100) ? val : DEFAULT_PANEL_HEIGHT;
} else if (strcmp(key, "gap") == 0) {
int val = atoi(value);
cfg->gap = (val >= 0 && val <= 100) ? val : DEFAULT_GAP;
} else if (strcmp(key, "font") == 0) {
strncpy(cfg->font_name, value, sizeof(cfg->font_name) - 1);
}
} else if (strcmp(section, "layout") == 0) {
if (strcmp(key, "master_ratio") == 0) {
float val = atof(value);
cfg->default_master_ratio = (val >= 0.1f && val <= 0.9f) ? val : 0.55f;
} else if (strcmp(key, "master_count") == 0) {
int val = atoi(value);
cfg->default_master_count = (val >= 1 && val <= 10) ? val : 1;
} else if (strcmp(key, "default") == 0) {
if (strcmp(value, "floating") == 0) {
cfg->default_layout = LAYOUT_FLOATING;
} else if (strcmp(value, "monocle") == 0) {
cfg->default_layout = LAYOUT_MONOCLE;
} else {
cfg->default_layout = LAYOUT_TILING;
}
}
} else if (strcmp(section, "panels") == 0) {
if (strcmp(key, "top") == 0) {
cfg->top_panel_enabled = (strcmp(value, "true") == 0 || strcmp(value, "1") == 0);
} else if (strcmp(key, "bottom") == 0) {
cfg->bottom_panel_enabled = (strcmp(value, "true") == 0 || strcmp(value, "1") == 0);
}
} else if (strcmp(section, "ai") == 0) {
if (strcmp(key, "model") == 0) {
strncpy(cfg->ai_model, value, sizeof(cfg->ai_model) - 1);
} else if (strcmp(key, "openrouter_api_key") == 0) {
strncpy(cfg->openrouter_api_key, value, sizeof(cfg->openrouter_api_key) - 1);
cfg->ai_enabled = true;
} else if (strcmp(key, "exa_api_key") == 0) {
strncpy(cfg->exa_api_key, value, sizeof(cfg->exa_api_key) - 1);
}
}
}
bool config_parse_ini(const char *path, ConfigCallback callback, void *user_data)
{
char *expanded = expand_path(path);
FILE *f = fopen(expanded, "r");
dwn_free(expanded);
if (f == NULL) {
return false;
}
char line[1024];
char section[64] = "";
while (fgets(line, sizeof(line), f) != NULL) {
char *trimmed = str_trim(line);
/* Skip empty lines and comments */
if (trimmed[0] == '\0' || trimmed[0] == '#' || trimmed[0] == ';') {
continue;
}
/* Section header */
if (trimmed[0] == '[') {
char *end = strchr(trimmed, ']');
if (end != NULL) {
*end = '\0';
strncpy(section, trimmed + 1, sizeof(section) - 1);
}
continue;
}
/* Key = value */
char *equals = strchr(trimmed, '=');
if (equals != NULL) {
*equals = '\0';
char *key = str_trim(trimmed);
char *value = str_trim(equals + 1);
/* Remove quotes from value */
size_t vlen = strlen(value);
if (vlen >= 2 && ((value[0] == '"' && value[vlen-1] == '"') ||
(value[0] == '\'' && value[vlen-1] == '\''))) {
value[vlen-1] = '\0';
value++;
}
callback(section, key, value, user_data);
}
}
fclose(f);
return true;
}
/* ========== Load configuration ========== */
bool config_load(Config *cfg, const char *path)
{
if (cfg == NULL) {
return false;
}
/* Set defaults first */
config_set_defaults(cfg);
/* Determine config path */
const char *config_path = path;
if (config_path == NULL) {
config_path = cfg->config_path;
}
/* Parse config file first (environment variables override below) */
ParseContext ctx = { .cfg = cfg };
char *expanded = expand_path(config_path);
if (file_exists(expanded)) {
LOG_INFO("Loading configuration from %s", expanded);
config_parse_ini(expanded, handle_config_entry, &ctx);
} else {
LOG_INFO("No config file found at %s, using defaults", expanded);
}
dwn_free(expanded);
/* Check for API keys in environment (override config file) */
const char *openrouter_key = getenv("OPENROUTER_API_KEY");
if (openrouter_key != NULL && openrouter_key[0] != '\0') {
strncpy(cfg->openrouter_api_key, openrouter_key, sizeof(cfg->openrouter_api_key) - 1);
cfg->ai_enabled = true;
LOG_INFO("OpenRouter API key found in environment");
}
const char *exa_key = getenv("EXA_API_KEY");
if (exa_key != NULL && exa_key[0] != '\0') {
strncpy(cfg->exa_api_key, exa_key, sizeof(cfg->exa_api_key) - 1);
}
/* Log AI status */
if (cfg->ai_enabled) {
LOG_INFO("AI features enabled");
}
return true;
}
bool config_reload(Config *cfg)
{
if (cfg == NULL) {
return false;
}
/* Securely wipe existing API keys before reloading */
secure_wipe(cfg->openrouter_api_key, sizeof(cfg->openrouter_api_key));
secure_wipe(cfg->exa_api_key, sizeof(cfg->exa_api_key));
return config_load(cfg, cfg->config_path);
}
/* ========== Getters ========== */
const char *config_get_terminal(void)
{
if (dwn != NULL && dwn->config != NULL) {
return dwn->config->terminal;
}
return "xterm";
}
const char *config_get_launcher(void)
{
if (dwn != NULL && dwn->config != NULL) {
return dwn->config->launcher;
}
return "dmenu_run";
}
int config_get_border_width(void)
{
if (dwn != NULL && dwn->config != NULL) {
return dwn->config->border_width;
}
return DEFAULT_BORDER_WIDTH;
}
int config_get_title_height(void)
{
if (dwn != NULL && dwn->config != NULL) {
return dwn->config->title_height;
}
return DEFAULT_TITLE_HEIGHT;
}
int config_get_panel_height(void)
{
if (dwn != NULL && dwn->config != NULL) {
return dwn->config->panel_height;
}
return DEFAULT_PANEL_HEIGHT;
}
int config_get_gap(void)
{
if (dwn != NULL && dwn->config != NULL) {
return dwn->config->gap;
}
return DEFAULT_GAP;
}
const ColorScheme *config_get_colors(void)
{
if (dwn != NULL && dwn->config != NULL) {
return &dwn->config->colors;
}
return NULL;
}
/* Initialize colors (must be called after display is open) */
void config_init_colors(Config *cfg)
{
if (cfg == NULL || dwn == NULL || dwn->display == NULL) {
return;
}
cfg->colors.panel_bg = parse_color(DEFAULT_PANEL_BG);
cfg->colors.panel_fg = parse_color(DEFAULT_PANEL_FG);
cfg->colors.workspace_active = parse_color(DEFAULT_WS_ACTIVE);
cfg->colors.workspace_inactive = parse_color(DEFAULT_WS_INACTIVE);
cfg->colors.workspace_urgent = parse_color(DEFAULT_WS_URGENT);
cfg->colors.title_focused_bg = parse_color(DEFAULT_TITLE_FOCUSED_BG);
cfg->colors.title_focused_fg = parse_color(DEFAULT_TITLE_FOCUSED_FG);
cfg->colors.title_unfocused_bg = parse_color(DEFAULT_TITLE_UNFOCUSED_BG);
cfg->colors.title_unfocused_fg = parse_color(DEFAULT_TITLE_UNFOCUSED_FG);
cfg->colors.border_focused = parse_color(DEFAULT_BORDER_FOCUSED);
cfg->colors.border_unfocused = parse_color(DEFAULT_BORDER_UNFOCUSED);
cfg->colors.notification_bg = parse_color(DEFAULT_NOTIFICATION_BG);
cfg->colors.notification_fg = parse_color(DEFAULT_NOTIFICATION_FG);
}
+368
View File
@@ -0,0 +1,368 @@
/*
* DWN - Desktop Window Manager
* Window decorations implementation
*/
#include "decorations.h"
#include "client.h"
#include "config.h"
#include "util.h"
#include <string.h>
#include <stdbool.h>
#include <X11/Xft/Xft.h>
/* Button dimensions */
#define BUTTON_SIZE 16
#define BUTTON_PADDING 4
/* Resize edge size */
#define RESIZE_EDGE 8
/* ========== Initialization ========== */
void decorations_init(void)
{
LOG_DEBUG("Decorations system initialized");
}
void decorations_cleanup(void)
{
/* Nothing to clean up */
}
/* ========== Rendering ========== */
void decorations_render(Client *client, bool focused)
{
if (client == NULL || client->frame == None) {
return;
}
if (dwn == NULL || dwn->display == NULL || dwn->config == NULL) {
return;
}
decorations_render_title_bar(client, focused);
decorations_render_buttons(client, focused);
decorations_render_border(client, focused);
}
void decorations_render_title_bar(Client *client, bool focused)
{
if (client == NULL || client->frame == None) {
return;
}
if (dwn == NULL || dwn->display == NULL || dwn->gc == None || dwn->config == NULL) {
return;
}
Display *dpy = dwn->display;
const ColorScheme *colors = config_get_colors();
int title_height = config_get_title_height();
int border = client->border_width;
/* Set colors based on focus */
unsigned long bg_color = focused ? colors->title_focused_bg : colors->title_unfocused_bg;
unsigned long fg_color = focused ? colors->title_focused_fg : colors->title_unfocused_fg;
/* Draw title bar background */
XSetForeground(dpy, dwn->gc, bg_color);
XFillRectangle(dpy, client->frame, dwn->gc,
border, border,
client->width, title_height);
/* Draw title text */
if (client->title[0] != '\0' && dwn->xft_font != NULL) {
int text_y = border + (title_height + dwn->xft_font->ascent) / 2;
int text_x = border + BUTTON_PADDING;
/* Truncate title if too long */
int max_width = client->width - 3 * (BUTTON_SIZE + BUTTON_PADDING) - 2 * BUTTON_PADDING;
char display_title[256];
strncpy(display_title, client->title, sizeof(display_title) - 4); /* Leave room for "..." */
display_title[sizeof(display_title) - 4] = '\0';
XGlyphInfo extents;
XftTextExtentsUtf8(dpy, dwn->xft_font,
(const FcChar8 *)display_title, strlen(display_title), &extents);
int text_width = extents.xOff;
/* Truncate UTF-8 aware: find valid UTF-8 boundary */
bool title_truncated = false;
while (text_width > max_width && strlen(display_title) > 3) {
size_t len = strlen(display_title);
/* Move back to find UTF-8 character boundary */
size_t cut = len - 1;
while (cut > 0 && (display_title[cut] & 0xC0) == 0x80) {
cut--; /* Skip continuation bytes */
}
if (cut > 0) cut--; /* Remove one more character for ellipsis space */
while (cut > 0 && (display_title[cut] & 0xC0) == 0x80) {
cut--;
}
display_title[cut] = '\0';
title_truncated = true;
XftTextExtentsUtf8(dpy, dwn->xft_font,
(const FcChar8 *)display_title, strlen(display_title), &extents);
text_width = extents.xOff;
}
if (title_truncated) {
strncat(display_title, "...", sizeof(display_title) - strlen(display_title) - 1);
}
/* Draw with Xft for UTF-8 support */
XftDraw *xft_draw = XftDrawCreate(dpy, client->frame,
DefaultVisual(dpy, dwn->screen),
dwn->colormap);
if (xft_draw != NULL) {
XftColor xft_color;
XRenderColor render_color;
render_color.red = ((fg_color >> 16) & 0xFF) * 257;
render_color.green = ((fg_color >> 8) & 0xFF) * 257;
render_color.blue = (fg_color & 0xFF) * 257;
render_color.alpha = 0xFFFF;
XftColorAllocValue(dpy, DefaultVisual(dpy, dwn->screen),
dwn->colormap, &render_color, &xft_color);
XftDrawStringUtf8(xft_draw, &xft_color, dwn->xft_font,
text_x, text_y,
(const FcChar8 *)display_title, strlen(display_title));
XftColorFree(dpy, DefaultVisual(dpy, dwn->screen),
dwn->colormap, &xft_color);
XftDrawDestroy(xft_draw);
}
}
}
void decorations_render_buttons(Client *client, bool focused)
{
if (client == NULL || client->frame == None) {
return;
}
if (dwn == NULL || dwn->display == NULL || dwn->gc == None || dwn->config == NULL) {
return;
}
Display *dpy = dwn->display;
const ColorScheme *colors = config_get_colors();
int title_height = config_get_title_height();
int border = client->border_width;
/* Button positions (right-aligned) */
int button_y = border + (title_height - BUTTON_SIZE) / 2;
int close_x = border + client->width - BUTTON_SIZE - BUTTON_PADDING;
int max_x = close_x - BUTTON_SIZE - BUTTON_PADDING;
int min_x = max_x - BUTTON_SIZE - BUTTON_PADDING;
unsigned long bg_color = focused ? colors->title_focused_bg : colors->title_unfocused_bg;
/* Close button (red X) */
XSetForeground(dpy, dwn->gc, bg_color);
XFillRectangle(dpy, client->frame, dwn->gc, close_x, button_y, BUTTON_SIZE, BUTTON_SIZE);
XSetForeground(dpy, dwn->gc, 0xcc4444); /* Red */
XDrawLine(dpy, client->frame, dwn->gc,
close_x + 3, button_y + 3,
close_x + BUTTON_SIZE - 4, button_y + BUTTON_SIZE - 4);
XDrawLine(dpy, client->frame, dwn->gc,
close_x + BUTTON_SIZE - 4, button_y + 3,
close_x + 3, button_y + BUTTON_SIZE - 4);
/* Maximize button (square) */
XSetForeground(dpy, dwn->gc, bg_color);
XFillRectangle(dpy, client->frame, dwn->gc, max_x, button_y, BUTTON_SIZE, BUTTON_SIZE);
XSetForeground(dpy, dwn->gc, 0x44cc44); /* Green */
XDrawRectangle(dpy, client->frame, dwn->gc,
max_x + 3, button_y + 3,
BUTTON_SIZE - 7, BUTTON_SIZE - 7);
/* Minimize button (line) */
XSetForeground(dpy, dwn->gc, bg_color);
XFillRectangle(dpy, client->frame, dwn->gc, min_x, button_y, BUTTON_SIZE, BUTTON_SIZE);
XSetForeground(dpy, dwn->gc, 0xcccc44); /* Yellow */
XDrawLine(dpy, client->frame, dwn->gc,
min_x + 3, button_y + BUTTON_SIZE - 5,
min_x + BUTTON_SIZE - 4, button_y + BUTTON_SIZE - 5);
}
void decorations_render_border(Client *client, bool focused)
{
if (client == NULL || client->frame == None) {
return;
}
if (dwn == NULL || dwn->display == NULL || dwn->config == NULL) {
return;
}
const ColorScheme *colors = config_get_colors();
unsigned long border_color = focused ? colors->border_focused : colors->border_unfocused;
XSetWindowBorder(dwn->display, client->frame, border_color);
}
/* ========== Hit testing ========== */
ButtonType decorations_hit_test_button(Client *client, int x, int y)
{
if (client == NULL) {
return BUTTON_COUNT; /* No button hit */
}
int title_height = config_get_title_height();
int border = client->border_width;
/* Check if in title bar area */
if (y < border || y > border + title_height) {
return BUTTON_COUNT;
}
int button_y = border + (title_height - BUTTON_SIZE) / 2;
int close_x = border + client->width - BUTTON_SIZE - BUTTON_PADDING;
int max_x = close_x - BUTTON_SIZE - BUTTON_PADDING;
int min_x = max_x - BUTTON_SIZE - BUTTON_PADDING;
/* Check close button */
if (x >= close_x && x < close_x + BUTTON_SIZE &&
y >= button_y && y < button_y + BUTTON_SIZE) {
return BUTTON_CLOSE;
}
/* Check maximize button */
if (x >= max_x && x < max_x + BUTTON_SIZE &&
y >= button_y && y < button_y + BUTTON_SIZE) {
return BUTTON_MAXIMIZE;
}
/* Check minimize button */
if (x >= min_x && x < min_x + BUTTON_SIZE &&
y >= button_y && y < button_y + BUTTON_SIZE) {
return BUTTON_MINIMIZE;
}
return BUTTON_COUNT;
}
bool decorations_hit_test_title_bar(Client *client, int x, int y)
{
if (client == NULL) {
return false;
}
int title_height = config_get_title_height();
int border = client->border_width;
/* In title bar but not on a button */
if (y >= border && y < border + title_height) {
ButtonType btn = decorations_hit_test_button(client, x, y);
return btn == BUTTON_COUNT;
}
return false;
}
bool decorations_hit_test_resize_area(Client *client, int x, int y, int *direction)
{
if (client == NULL || direction == NULL) {
return false;
}
int title_height = config_get_title_height();
int border = client->border_width;
int frame_width = client->width + 2 * border;
int frame_height = client->height + title_height + 2 * border;
*direction = 0;
/* Check edges */
bool left = (x < RESIZE_EDGE);
bool right = (x > frame_width - RESIZE_EDGE);
bool top = (y < RESIZE_EDGE);
bool bottom = (y > frame_height - RESIZE_EDGE);
if (!left && !right && !top && !bottom) {
return false;
}
/* Encode direction as bitmask */
if (left) *direction |= 1;
if (right) *direction |= 2;
if (top) *direction |= 4;
if (bottom) *direction |= 8;
return true;
}
/* ========== Button actions ========== */
void decorations_button_press(Client *client, ButtonType button)
{
if (client == NULL) {
return;
}
switch (button) {
case BUTTON_CLOSE:
LOG_DEBUG("Close button pressed for %s", client->title);
client_close(client);
break;
case BUTTON_MAXIMIZE:
LOG_DEBUG("Maximize button pressed for %s", client->title);
client_toggle_fullscreen(client);
break;
case BUTTON_MINIMIZE:
LOG_DEBUG("Minimize button pressed for %s", client->title);
client_minimize(client);
break;
default:
break;
}
}
/* ========== Text rendering ========== */
void decorations_draw_text(Window window, GC gc, int x, int y,
const char *text, unsigned long color)
{
if (text == NULL || dwn == NULL || dwn->display == NULL) {
return;
}
/* Use Xft for UTF-8 support */
if (dwn->xft_font != NULL) {
XftDraw *xft_draw = XftDrawCreate(dwn->display, window,
DefaultVisual(dwn->display, dwn->screen),
dwn->colormap);
if (xft_draw != NULL) {
XftColor xft_color;
XRenderColor render_color;
render_color.red = ((color >> 16) & 0xFF) * 257;
render_color.green = ((color >> 8) & 0xFF) * 257;
render_color.blue = (color & 0xFF) * 257;
render_color.alpha = 0xFFFF;
XftColorAllocValue(dwn->display, DefaultVisual(dwn->display, dwn->screen),
dwn->colormap, &render_color, &xft_color);
XftDrawStringUtf8(xft_draw, &xft_color, dwn->xft_font,
x, y, (const FcChar8 *)text, strlen(text));
XftColorFree(dwn->display, DefaultVisual(dwn->display, dwn->screen),
dwn->colormap, &xft_color);
XftDrawDestroy(xft_draw);
return;
}
}
/* Fallback to legacy X11 text */
XSetForeground(dwn->display, gc, color);
XDrawString(dwn->display, window, gc, x, y, text, strlen(text));
}
+842
View File
@@ -0,0 +1,842 @@
/*
* DWN - Desktop Window Manager
* Keyboard shortcut handling implementation
*/
#include "keys.h"
#include "client.h"
#include "workspace.h"
#include "config.h"
#include "util.h"
#include "ai.h"
#include "notifications.h"
#include "news.h"
#include "applauncher.h"
#include <stdio.h>
#include <string.h>
/* Forward declarations for key callbacks */
void key_spawn_terminal(void);
void key_spawn_launcher(void);
void key_spawn_file_manager(void);
void key_spawn_browser(void);
void key_close_window(void);
void key_quit_dwn(void);
void key_cycle_layout(void);
void key_toggle_floating(void);
void key_toggle_fullscreen(void);
void key_toggle_maximize(void);
void key_focus_next(void);
void key_focus_prev(void);
void key_workspace_next(void);
void key_workspace_prev(void);
void key_workspace_1(void);
void key_workspace_2(void);
void key_workspace_3(void);
void key_workspace_4(void);
void key_workspace_5(void);
void key_workspace_6(void);
void key_workspace_7(void);
void key_workspace_8(void);
void key_workspace_9(void);
void key_move_to_workspace_1(void);
void key_move_to_workspace_2(void);
void key_move_to_workspace_3(void);
void key_move_to_workspace_4(void);
void key_move_to_workspace_5(void);
void key_move_to_workspace_6(void);
void key_move_to_workspace_7(void);
void key_move_to_workspace_8(void);
void key_move_to_workspace_9(void);
void key_increase_master(void);
void key_decrease_master(void);
void key_increase_master_count(void);
void key_decrease_master_count(void);
void key_toggle_ai(void);
void key_ai_command(void);
void key_exa_search(void);
void key_news_next(void);
void key_news_prev(void);
void key_news_open(void);
void key_show_shortcuts(void);
void key_start_tutorial(void);
void key_screenshot(void);
/* Key bindings storage */
static KeyBinding bindings[MAX_KEYBINDINGS];
static int binding_count = 0;
/* ========== Tutorial System ========== */
typedef struct {
const char *title;
const char *instruction;
const char *hint;
unsigned int modifiers;
KeySym keysym;
} TutorialStep;
static const TutorialStep tutorial_steps[] = {
{
"Welcome to DWN!",
"This tutorial teaches all keyboard shortcuts.\n\n"
"Press Super+T to continue...",
"(Super = Windows/Meta key)",
MOD_SUPER, XK_t
},
/* === Applications === */
{
"1/20: Open Terminal",
"The terminal is your command center.\n\n"
"Press: Ctrl + Alt + T",
"Hold Ctrl and Alt, then press T",
MOD_CTRL | MOD_ALT, XK_t
},
{
"2/20: App Launcher",
"Launch any application by name.\n\n"
"Press: Alt + F2",
"Type app name and press Enter",
MOD_ALT, XK_F2
},
{
"3/20: File Manager",
"Open the file manager.\n\n"
"Press: Super + E",
"",
MOD_SUPER, XK_e
},
{
"4/20: Web Browser",
"Open your default web browser.\n\n"
"Press: Super + B",
"",
MOD_SUPER, XK_b
},
/* === Window Management === */
{
"5/20: Switch Windows",
"Cycle through open windows.\n\n"
"Press: Alt + Tab",
"Hold Alt, press Tab repeatedly",
MOD_ALT, XK_Tab
},
{
"6/20: Close Window",
"Close the focused window.\n\n"
"Press: Alt + F4",
"",
MOD_ALT, XK_F4
},
{
"7/20: Toggle Maximize",
"Maximize or restore a window.\n\n"
"Press: Alt + F10",
"",
MOD_ALT, XK_F10
},
{
"8/20: Toggle Fullscreen",
"Make a window fullscreen.\n\n"
"Press: Alt + F11",
"Press again to exit",
MOD_ALT, XK_F11
},
{
"9/20: Toggle Floating",
"Make window float above tiled windows.\n\n"
"Press: Super + F9",
"",
MOD_SUPER, XK_F9
},
/* === Workspaces === */
{
"10/20: Next Workspace",
"Switch to the next virtual desktop.\n\n"
"Press: Ctrl + Alt + Right",
"",
MOD_CTRL | MOD_ALT, XK_Right
},
{
"11/20: Previous Workspace",
"Switch to the previous workspace.\n\n"
"Press: Ctrl + Alt + Left",
"",
MOD_CTRL | MOD_ALT, XK_Left
},
{
"12/20: Go to Workspace",
"Jump directly to workspace 1.\n\n"
"Press: F1",
"Use F1-F9 for workspaces 1-9",
0, XK_F1
},
/* === Layout === */
{
"13/20: Cycle Layout",
"Switch between tiling, floating, monocle.\n\n"
"Press: Super + Space",
"Try it multiple times!",
MOD_SUPER, XK_space
},
{
"14/20: Expand Master",
"Make the master area larger.\n\n"
"Press: Super + L",
"",
MOD_SUPER, XK_l
},
{
"15/20: Shrink Master",
"Make the master area smaller.\n\n"
"Press: Super + H",
"",
MOD_SUPER, XK_h
},
{
"16/20: Add to Master",
"Increase windows in master area.\n\n"
"Press: Super + I",
"",
MOD_SUPER, XK_i
},
/* === AI Features === */
{
"17/20: AI Context",
"Show AI analysis of your current task.\n\n"
"Press: Super + A",
"Requires OPENROUTER_API_KEY",
MOD_SUPER, XK_a
},
{
"18/20: AI Command",
"Ask AI to run commands for you.\n\n"
"Press: Super + Shift + A",
"Requires OPENROUTER_API_KEY",
MOD_SUPER | MOD_SHIFT, XK_a
},
{
"19/20: Exa Search",
"Semantic web search.\n\n"
"Press: Super + Shift + E",
"Requires EXA_API_KEY",
MOD_SUPER | MOD_SHIFT, XK_e
},
/* === Help & System === */
{
"20/20: Show Shortcuts",
"Display all keyboard shortcuts.\n\n"
"Press: Super + S",
"Shows complete reference",
MOD_SUPER, XK_s
},
{
"Tutorial Complete!",
"You've learned all DWN shortcuts!\n\n"
"Super+S: Show all shortcuts\n"
"Super+T: Restart this tutorial\n\n"
"Press: Super + Backspace to quit DWN",
"Congratulations!",
MOD_SUPER, XK_BackSpace
}
};
#define TUTORIAL_STEPS (sizeof(tutorial_steps) / sizeof(tutorial_steps[0]))
static bool tutorial_active = false;
static int tutorial_current_step = 0;
bool tutorial_is_active(void)
{
return tutorial_active;
}
void tutorial_start(void)
{
tutorial_active = true;
tutorial_current_step = 0;
const TutorialStep *step = &tutorial_steps[0];
char msg[512];
snprintf(msg, sizeof(msg), "%s\n\n%s", step->instruction, step->hint);
notification_show("DWN Tutorial", step->title, msg, NULL, 0); /* No timeout */
}
void tutorial_stop(void)
{
tutorial_active = false;
tutorial_current_step = 0;
notification_show("DWN Tutorial", "Tutorial Stopped",
"Press Super+T to restart anytime.", NULL, 3000);
}
void tutorial_next_step(void)
{
tutorial_current_step++;
if (tutorial_current_step >= (int)TUTORIAL_STEPS) {
tutorial_active = false;
tutorial_current_step = 0;
return;
}
const TutorialStep *step = &tutorial_steps[tutorial_current_step];
char msg[512];
snprintf(msg, sizeof(msg), "%s\n\n%s", step->instruction, step->hint);
notification_show("DWN Tutorial", step->title, msg, NULL, 0);
}
void tutorial_check_key(unsigned int modifiers, KeySym keysym)
{
if (!tutorial_active) {
return;
}
const TutorialStep *step = &tutorial_steps[tutorial_current_step];
/* Check if the pressed key matches the expected key */
if (modifiers == step->modifiers && keysym == step->keysym) {
/* Correct key! Move to next step */
tutorial_next_step();
}
}
/* ========== Initialization ========== */
void keys_init(void)
{
binding_count = 0;
memset(bindings, 0, sizeof(bindings));
keys_setup_defaults();
keys_grab_all();
LOG_INFO("Keyboard shortcuts initialized (%d bindings)", binding_count);
}
void keys_cleanup(void)
{
keys_ungrab_all();
keys_clear_all();
}
void keys_grab_all(void)
{
if (dwn == NULL || dwn->display == NULL) {
return;
}
Display *dpy = dwn->display;
Window root = dwn->root;
/* Ungrab first to avoid errors */
XUngrabKey(dpy, AnyKey, AnyModifier, root);
/* Grab all registered bindings */
for (int i = 0; i < binding_count; i++) {
KeyCode code = XKeysymToKeycode(dpy, bindings[i].keysym);
if (code == 0) {
LOG_WARN("Could not get keycode for keysym %lu", bindings[i].keysym);
continue;
}
/* Grab with and without NumLock/CapsLock */
unsigned int modifiers[] = {
bindings[i].modifiers,
bindings[i].modifiers | Mod2Mask, /* NumLock */
bindings[i].modifiers | LockMask, /* CapsLock */
bindings[i].modifiers | Mod2Mask | LockMask
};
for (size_t j = 0; j < sizeof(modifiers) / sizeof(modifiers[0]); j++) {
XGrabKey(dpy, code, modifiers[j], root, True,
GrabModeAsync, GrabModeAsync);
}
}
}
void keys_ungrab_all(void)
{
if (dwn == NULL || dwn->display == NULL) {
return;
}
XUngrabKey(dwn->display, AnyKey, AnyModifier, dwn->root);
}
/* ========== Key binding registration ========== */
void keys_bind(unsigned int modifiers, KeySym keysym,
KeyCallback callback, const char *description)
{
if (binding_count >= MAX_KEYBINDINGS) {
LOG_WARN("Maximum key bindings reached");
return;
}
bindings[binding_count].modifiers = modifiers;
bindings[binding_count].keysym = keysym;
bindings[binding_count].callback = callback;
bindings[binding_count].description = description;
binding_count++;
}
void keys_unbind(unsigned int modifiers, KeySym keysym)
{
for (int i = 0; i < binding_count; i++) {
if (bindings[i].modifiers == modifiers &&
bindings[i].keysym == keysym) {
/* Shift remaining bindings */
memmove(&bindings[i], &bindings[i + 1],
(binding_count - i - 1) * sizeof(KeyBinding));
binding_count--;
return;
}
}
}
void keys_clear_all(void)
{
binding_count = 0;
memset(bindings, 0, sizeof(bindings));
}
/* ========== Key event handling ========== */
void keys_handle_press(XKeyEvent *ev)
{
if (ev == NULL || dwn == NULL || dwn->display == NULL) {
return;
}
KeySym keysym = XLookupKeysym(ev, 0);
/* Clean modifiers (remove NumLock, CapsLock) */
unsigned int clean_mask = ev->state & ~(Mod2Mask | LockMask);
/* Check tutorial progress */
if (tutorial_is_active()) {
tutorial_check_key(clean_mask, keysym);
}
for (int i = 0; i < binding_count; i++) {
if (bindings[i].keysym == keysym &&
bindings[i].modifiers == clean_mask) {
if (bindings[i].callback != NULL) {
LOG_DEBUG("Key binding triggered: %s", bindings[i].description);
bindings[i].callback();
}
return;
}
}
}
void keys_handle_release(XKeyEvent *ev)
{
/* Currently no release handling needed */
(void)ev;
}
/* ========== Default key bindings (XFCE-style) ========== */
void keys_setup_defaults(void)
{
/* ===== XFCE-style shortcuts ===== */
/* Terminal: Ctrl+Alt+T (XFCE default) */
keys_bind(MOD_CTRL | MOD_ALT, XK_t, key_spawn_terminal, "Spawn terminal");
/* Application finder: Alt+F2 (XFCE default) */
keys_bind(MOD_ALT, XK_F2, key_spawn_launcher, "Application finder");
/* File manager: Super+E (XFCE default) */
keys_bind(MOD_SUPER, XK_e, key_spawn_file_manager, "File manager");
/* Browser: Super+B */
keys_bind(MOD_SUPER, XK_b, key_spawn_browser, "Web browser");
/* Close window: Alt+F4 (XFCE default) */
keys_bind(MOD_ALT, XK_F4, key_close_window, "Close window");
/* Maximize toggle: Alt+F10 (XFCE default) */
keys_bind(MOD_ALT, XK_F10, key_toggle_maximize, "Toggle maximize");
/* Fullscreen: Alt+F11 (XFCE default) */
keys_bind(MOD_ALT, XK_F11, key_toggle_fullscreen, "Toggle fullscreen");
/* Cycle windows: Alt+Tab (XFCE default) */
keys_bind(MOD_ALT, XK_Tab, key_focus_next, "Cycle windows");
keys_bind(MOD_ALT | MOD_SHIFT, XK_Tab, key_focus_prev, "Cycle windows reverse");
/* Toggle floating: Super+F9 */
keys_bind(MOD_SUPER, XK_F9, key_toggle_floating, "Toggle floating");
/* Next/Previous workspace: Ctrl+Alt+Right/Left (XFCE default) */
keys_bind(MOD_CTRL | MOD_ALT, XK_Right, key_workspace_next, "Next workspace");
keys_bind(MOD_CTRL | MOD_ALT, XK_Left, key_workspace_prev, "Previous workspace");
/* Switch to workspace: F1-F9 */
keys_bind(0, XK_F1, key_workspace_1, "Switch to workspace 1");
keys_bind(0, XK_F2, key_workspace_2, "Switch to workspace 2");
keys_bind(0, XK_F3, key_workspace_3, "Switch to workspace 3");
keys_bind(0, XK_F4, key_workspace_4, "Switch to workspace 4");
keys_bind(0, XK_F5, key_workspace_5, "Switch to workspace 5");
keys_bind(0, XK_F6, key_workspace_6, "Switch to workspace 6");
keys_bind(0, XK_F7, key_workspace_7, "Switch to workspace 7");
keys_bind(0, XK_F8, key_workspace_8, "Switch to workspace 8");
keys_bind(0, XK_F9, key_workspace_9, "Switch to workspace 9");
/* Move to workspace: Shift+F1-F9 */
keys_bind(MOD_SHIFT, XK_F1, key_move_to_workspace_1, "Move to workspace 1");
keys_bind(MOD_SHIFT, XK_F2, key_move_to_workspace_2, "Move to workspace 2");
keys_bind(MOD_SHIFT, XK_F3, key_move_to_workspace_3, "Move to workspace 3");
keys_bind(MOD_SHIFT, XK_F4, key_move_to_workspace_4, "Move to workspace 4");
keys_bind(MOD_SHIFT, XK_F5, key_move_to_workspace_5, "Move to workspace 5");
keys_bind(MOD_SHIFT, XK_F6, key_move_to_workspace_6, "Move to workspace 6");
keys_bind(MOD_SHIFT, XK_F7, key_move_to_workspace_7, "Move to workspace 7");
keys_bind(MOD_SHIFT, XK_F8, key_move_to_workspace_8, "Move to workspace 8");
keys_bind(MOD_SHIFT, XK_F9, key_move_to_workspace_9, "Move to workspace 9");
/* ===== DWN-specific shortcuts (all use Super key) ===== */
/* Quit DWN: Super+BackSpace */
keys_bind(MOD_SUPER, XK_BackSpace, key_quit_dwn, "Quit DWN");
/* Cycle layout mode: Super+Space */
keys_bind(MOD_SUPER, XK_space, key_cycle_layout, "Cycle layout");
/* Master area adjustments: Super+H/L/I/D */
keys_bind(MOD_SUPER, XK_l, key_increase_master, "Increase master ratio");
keys_bind(MOD_SUPER, XK_h, key_decrease_master, "Decrease master ratio");
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");
/* AI: Super+A */
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");
/* Exa semantic search: Super+Shift+E */
keys_bind(MOD_SUPER | MOD_SHIFT, XK_e, key_exa_search, "Exa web search");
/* Show shortcuts: Super+S */
keys_bind(MOD_SUPER, XK_s, key_show_shortcuts, "Show shortcuts");
/* Tutorial: Super+T */
keys_bind(MOD_SUPER, XK_t, key_start_tutorial, "Start tutorial");
/* ===== News ticker navigation ===== */
/* Super+Down: Next news article */
keys_bind(MOD_SUPER, XK_Down, key_news_next, "Next news article");
/* Super+Up: Previous news article */
keys_bind(MOD_SUPER, XK_Up, key_news_prev, "Previous news article");
/* Super+Return: Open current news article */
keys_bind(MOD_SUPER, XK_Return, key_news_open, "Open news article");
/* Screenshot: Print Screen (XFCE default) */
keys_bind(0, XK_Print, key_screenshot, "Take screenshot");
}
/* ========== Key binding callbacks ========== */
void key_spawn_terminal(void)
{
const char *terminal = config_get_terminal();
spawn_async(terminal);
}
void key_spawn_launcher(void)
{
applauncher_show();
}
void key_spawn_file_manager(void)
{
if (dwn != NULL && dwn->config != NULL) {
spawn_async(dwn->config->file_manager);
} else {
spawn_async("thunar");
}
}
void key_spawn_browser(void)
{
spawn_async("xdg-open http://");
}
void key_close_window(void)
{
Workspace *ws = workspace_get_current();
if (ws != NULL && ws->focused != NULL) {
client_close(ws->focused);
}
}
void key_quit_dwn(void)
{
LOG_INFO("Quit requested via keyboard shortcut");
dwn_quit();
}
void key_cycle_layout(void)
{
if (dwn == NULL) {
return;
}
workspace_cycle_layout(dwn->current_workspace);
}
void key_toggle_floating(void)
{
Workspace *ws = workspace_get_current();
if (ws != NULL && ws->focused != NULL) {
client_toggle_floating(ws->focused);
workspace_arrange_current();
}
}
void key_toggle_fullscreen(void)
{
Workspace *ws = workspace_get_current();
if (ws != NULL && ws->focused != NULL) {
client_toggle_fullscreen(ws->focused);
}
}
void key_toggle_maximize(void)
{
Workspace *ws = workspace_get_current();
if (ws != NULL && ws->focused != NULL) {
/* Toggle between maximized and normal state */
/* For now, use fullscreen as maximize equivalent */
client_toggle_fullscreen(ws->focused);
}
}
void key_focus_next(void)
{
workspace_focus_next();
}
void key_focus_prev(void)
{
workspace_focus_prev();
}
void key_workspace_next(void)
{
if (dwn == NULL) {
return;
}
int next = dwn->current_workspace + 1;
if (next >= MAX_WORKSPACES) {
next = 0; /* Wrap around */
}
workspace_switch(next);
}
void key_workspace_prev(void)
{
if (dwn == NULL) {
return;
}
int prev = dwn->current_workspace - 1;
if (prev < 0) {
prev = MAX_WORKSPACES - 1; /* Wrap around */
}
workspace_switch(prev);
}
/* Workspace switching */
void key_workspace_1(void) { workspace_switch(0); }
void key_workspace_2(void) { workspace_switch(1); }
void key_workspace_3(void) { workspace_switch(2); }
void key_workspace_4(void) { workspace_switch(3); }
void key_workspace_5(void) { workspace_switch(4); }
void key_workspace_6(void) { workspace_switch(5); }
void key_workspace_7(void) { workspace_switch(6); }
void key_workspace_8(void) { workspace_switch(7); }
void key_workspace_9(void) { workspace_switch(8); }
/* Move to workspace */
static void move_focused_to_workspace(int ws)
{
Workspace *current = workspace_get_current();
if (current != NULL && current->focused != NULL) {
workspace_move_client(current->focused, ws);
}
}
void key_move_to_workspace_1(void) { move_focused_to_workspace(0); }
void key_move_to_workspace_2(void) { move_focused_to_workspace(1); }
void key_move_to_workspace_3(void) { move_focused_to_workspace(2); }
void key_move_to_workspace_4(void) { move_focused_to_workspace(3); }
void key_move_to_workspace_5(void) { move_focused_to_workspace(4); }
void key_move_to_workspace_6(void) { move_focused_to_workspace(5); }
void key_move_to_workspace_7(void) { move_focused_to_workspace(6); }
void key_move_to_workspace_8(void) { move_focused_to_workspace(7); }
void key_move_to_workspace_9(void) { move_focused_to_workspace(8); }
/* Master area adjustments */
void key_increase_master(void)
{
if (dwn == NULL) {
return;
}
workspace_adjust_master_ratio(dwn->current_workspace, 0.05f);
}
void key_decrease_master(void)
{
if (dwn == NULL) {
return;
}
workspace_adjust_master_ratio(dwn->current_workspace, -0.05f);
}
void key_increase_master_count(void)
{
if (dwn == NULL) {
return;
}
workspace_adjust_master_count(dwn->current_workspace, 1);
}
void key_decrease_master_count(void)
{
if (dwn == NULL) {
return;
}
workspace_adjust_master_count(dwn->current_workspace, -1);
}
/* AI functions */
void key_toggle_ai(void)
{
if (dwn == NULL) {
return;
}
if (!dwn->ai_enabled) {
notification_show("DWN AI", "AI Disabled",
"Set OPENROUTER_API_KEY to enable AI features",
NULL, 3000);
return;
}
/* Update and show AI context */
ai_update_context();
const char *task = ai_analyze_task();
const char *suggestion = ai_suggest_window();
char body[512];
Workspace *ws = workspace_get_current();
if (ws != NULL && ws->focused != NULL) {
snprintf(body, sizeof(body),
"Current task: %s\nFocused: %s\n%s",
task ? task : "Unknown",
ws->focused->title[0] ? ws->focused->title : "(unnamed)",
suggestion ? suggestion : "Press Alt+A to ask AI for help");
} else {
snprintf(body, sizeof(body),
"No window focused.\nPress Alt+A to ask AI for help");
}
notification_show("DWN AI", "Context Analysis", body, NULL, 4000);
}
void key_ai_command(void)
{
if (dwn == NULL) {
return;
}
if (dwn->ai_enabled) {
ai_show_command_palette();
} else {
LOG_INFO("AI not enabled (no API key)");
}
}
void key_exa_search(void)
{
exa_show_app_launcher();
}
void key_show_shortcuts(void)
{
const char *shortcuts =
"=== Applications ===\n"
"Ctrl+Alt+T Terminal\n"
"Alt+F2 App launcher\n"
"Super+E File manager\n"
"Super+B Web browser\n"
"Print Screenshot\n"
"\n"
"=== Window Management ===\n"
"Alt+F4 Close window\n"
"Alt+Tab Next window\n"
"Alt+Shift+Tab Previous window\n"
"Alt+F10 Toggle maximize\n"
"Alt+F11 Toggle fullscreen\n"
"Super+F9 Toggle floating\n"
"\n"
"=== Workspaces ===\n"
"F1-F9 Switch to workspace\n"
"Shift+F1-F9 Move window to workspace\n"
"Ctrl+Alt+Right Next workspace\n"
"Ctrl+Alt+Left Previous workspace\n"
"\n"
"=== Layout Control ===\n"
"Super+Space Cycle layout (tile/float/mono)\n"
"Super+H Shrink master area\n"
"Super+L Expand master area\n"
"Super+I Add to master\n"
"Super+D Remove from master\n"
"\n"
"=== AI Features ===\n"
"Super+A Show AI context\n"
"Super+Shift+A AI command palette\n"
"Super+Shift+E Exa semantic search\n"
"\n"
"=== Help & System ===\n"
"Super+S Show shortcuts (this)\n"
"Super+T Interactive tutorial\n"
"Super+Backspace Quit DWN";
notification_show("DWN Shortcuts", "Complete Reference", shortcuts, NULL, 0);
}
void key_start_tutorial(void)
{
if (tutorial_is_active()) {
/* If already in tutorial, this acts as "next" */
tutorial_next_step();
} else {
tutorial_start();
}
}
/* ========== News ticker callbacks ========== */
void key_news_next(void)
{
news_next_article();
}
void key_news_prev(void)
{
news_prev_article();
}
void key_news_open(void)
{
news_open_current();
}
void key_screenshot(void)
{
spawn_async("xfce4-screenshooter");
}
+263
View File
@@ -0,0 +1,263 @@
/*
* DWN - Desktop Window Manager
* Layout algorithms implementation
*/
#include "layout.h"
#include "client.h"
#include "workspace.h"
#include "config.h"
#include "util.h"
/* Layout names and symbols */
static const char *layout_names[] = {
"Tiling",
"Floating",
"Monocle"
};
static const char *layout_symbols[] = {
"[]=",
"><>",
"[M]"
};
/* ========== Main arrangement function ========== */
void layout_arrange(int workspace)
{
Workspace *ws = workspace_get(workspace);
if (ws == NULL) {
return;
}
switch (ws->layout) {
case LAYOUT_TILING:
layout_arrange_tiling(workspace);
break;
case LAYOUT_FLOATING:
layout_arrange_floating(workspace);
break;
case LAYOUT_MONOCLE:
layout_arrange_monocle(workspace);
break;
default:
layout_arrange_tiling(workspace);
break;
}
}
/* ========== Tiling layout ========== */
void layout_arrange_tiling(int workspace)
{
Workspace *ws = workspace_get(workspace);
if (ws == NULL) {
return;
}
/* Get usable screen area (excluding panels) */
int area_x, area_y, area_width, area_height;
layout_get_usable_area(&area_x, &area_y, &area_width, &area_height);
int gap = config_get_gap();
int title_height = config_get_title_height();
int border = config_get_border_width();
/* Count tiled (non-floating) clients */
int n = layout_count_tiled_clients(workspace);
if (n == 0) {
return;
}
int master_count = ws->master_count;
if (master_count > n) {
master_count = n;
}
/* Calculate master area width */
int master_width;
if (n <= master_count) {
master_width = area_width - 2 * gap;
} else {
master_width = (int)((area_width - 3 * gap) * ws->master_ratio);
}
int stack_width = area_width - master_width - 3 * gap;
/* Arrange windows */
int i = 0;
int master_y = area_y + gap;
int stack_y = area_y + gap;
for (Client *c = dwn->client_list; c != NULL; c = c->next) {
if (c->workspace != (unsigned int)workspace) {
continue;
}
if (client_is_floating(c) || client_is_fullscreen(c) || client_is_minimized(c)) {
continue;
}
int x, y, w, h;
if (i < master_count) {
/* Master area */
int master_h = (area_height - 2 * gap - (master_count - 1) * gap) / master_count;
x = area_x + gap;
y = master_y;
w = master_width;
h = master_h;
master_y += h + gap;
} else {
/* Stack area */
int stack_count = n - master_count;
int stack_h = (area_height - 2 * gap - (stack_count - 1) * gap) / stack_count;
x = area_x + gap + master_width + gap;
y = stack_y;
w = stack_width;
h = stack_h;
stack_y += h + gap;
}
/* Account for decorations */
int actual_h = h - title_height - 2 * border;
int actual_w = w - 2 * border;
if (actual_h < 50) actual_h = 50;
if (actual_w < 50) actual_w = 50;
client_move_resize(c, x + border, y + title_height + border,
actual_w, actual_h);
i++;
}
}
/* ========== Floating layout ========== */
void layout_arrange_floating(int workspace)
{
/* In floating mode, we don't rearrange windows automatically.
Just make sure all clients are configured properly. */
for (Client *c = dwn->client_list; c != NULL; c = c->next) {
if (c->workspace != (unsigned int)workspace) {
continue;
}
if (client_is_minimized(c)) {
continue;
}
/* Ensure window is within screen bounds */
int area_x, area_y, area_width, area_height;
layout_get_usable_area(&area_x, &area_y, &area_width, &area_height);
if (c->x < area_x) c->x = area_x;
if (c->y < area_y) c->y = area_y;
if (c->x + c->width > area_x + area_width) {
c->x = area_x + area_width - c->width;
}
if (c->y + c->height > area_y + area_height) {
c->y = area_y + area_height - c->height;
}
client_configure(c);
}
}
/* ========== Monocle layout ========== */
void layout_arrange_monocle(int workspace)
{
/* Get usable screen area */
int area_x, area_y, area_width, area_height;
layout_get_usable_area(&area_x, &area_y, &area_width, &area_height);
int gap = config_get_gap();
int title_height = config_get_title_height();
int border = config_get_border_width();
for (Client *c = dwn->client_list; c != NULL; c = c->next) {
if (c->workspace != (unsigned int)workspace) {
continue;
}
if (client_is_floating(c) || client_is_fullscreen(c) || client_is_minimized(c)) {
continue;
}
/* All tiled windows take the full usable area */
int x = area_x + gap;
int y = area_y + gap;
int w = area_width - 2 * gap - 2 * border;
int h = area_height - 2 * gap - title_height - 2 * border;
if (h < 50) h = 50;
if (w < 50) w = 50;
client_move_resize(c, x + border, y + title_height + border, w, h);
}
}
/* ========== Helpers ========== */
int layout_get_usable_area(int *x, int *y, int *width, int *height)
{
if (dwn == NULL) {
return -1;
}
int panel_height = config_get_panel_height();
bool top_panel = (dwn->config != NULL) ? dwn->config->top_panel_enabled : true;
bool bottom_panel = (dwn->config != NULL) ? dwn->config->bottom_panel_enabled : true;
*x = 0;
*y = top_panel ? panel_height : 0;
*width = dwn->screen_width;
*height = dwn->screen_height;
if (top_panel) {
*height -= panel_height;
}
if (bottom_panel) {
*height -= panel_height;
}
return 0;
}
int layout_count_tiled_clients(int workspace)
{
int count = 0;
for (Client *c = dwn->client_list; c != NULL; c = c->next) {
if (c->workspace != (unsigned int)workspace) {
continue;
}
if (client_is_floating(c) || client_is_fullscreen(c) || client_is_minimized(c)) {
continue;
}
count++;
}
return count;
}
const char *layout_get_name(LayoutType layout)
{
if (layout >= 0 && layout < LAYOUT_COUNT) {
return layout_names[layout];
}
return "Unknown";
}
const char *layout_get_symbol(LayoutType layout)
{
if (layout >= 0 && layout < LAYOUT_COUNT) {
return layout_symbols[layout];
}
return "???";
}
+1016
View File
File diff suppressed because it is too large Load Diff
+697
View File
@@ -0,0 +1,697 @@
/*
* DWN - Desktop Window Manager
* News ticker implementation
*/
#include "news.h"
#include "panel.h"
#include "config.h"
#include "util.h"
#include "cJSON.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <pthread.h>
#include <curl/curl.h>
#include <X11/Xft/Xft.h>
/* Scroll speed in pixels per second */
#define SCROLL_SPEED_PPS 80
/* Fetch interval in milliseconds (5 minutes) */
#define FETCH_INTERVAL 300000
/* Global state */
NewsState news_state = {0};
/* Thread safety */
static pthread_mutex_t news_mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_t fetch_thread;
static volatile int fetch_running = 0;
/* CURL response buffer */
typedef struct {
char *data;
size_t size;
} CurlBuffer;
static size_t news_curl_write_cb(void *contents, size_t size, size_t nmemb, void *userp)
{
size_t realsize = size * nmemb;
CurlBuffer *buf = (CurlBuffer *)userp;
char *ptr = realloc(buf->data, buf->size + realsize + 1);
if (ptr == NULL) {
return 0;
}
buf->data = ptr;
memcpy(&(buf->data[buf->size]), contents, realsize);
buf->size += realsize;
buf->data[buf->size] = '\0';
return realsize;
}
/* Decode numeric HTML entity (&#123; or &#x1F;) */
static int decode_numeric_entity(const char *src, char *out, size_t max_out)
{
if (src[0] != '&' || src[1] != '#') return 0;
const char *p = src + 2;
int base = 10;
if (*p == 'x' || *p == 'X') {
base = 16;
p++;
}
char *end;
long code = strtol(p, &end, base);
if (*end != ';' || code <= 0) return 0;
int consumed = (end - src) + 1;
if (code < 128 && max_out >= 1) {
out[0] = (char)code;
return consumed;
} else if (code < 0x800 && max_out >= 2) {
out[0] = 0xC0 | (code >> 6);
out[1] = 0x80 | (code & 0x3F);
return consumed;
} else if (code < 0x10000 && max_out >= 3) {
out[0] = 0xE0 | (code >> 12);
out[1] = 0x80 | ((code >> 6) & 0x3F);
out[2] = 0x80 | (code & 0x3F);
return consumed;
}
return 0;
}
/* Strip HTML tags and decode entities */
static void strip_html(char *dst, const char *src, size_t max_len)
{
size_t j = 0;
bool in_tag = false;
for (size_t i = 0; src[i] && j < max_len - 1; i++) {
if (src[i] == '<') {
in_tag = true;
} else if (src[i] == '>') {
in_tag = false;
} else if (!in_tag) {
if (src[i] == '&') {
/* Named entities */
if (strncmp(&src[i], "&amp;", 5) == 0) {
dst[j++] = '&'; i += 4;
} else if (strncmp(&src[i], "&lt;", 4) == 0) {
dst[j++] = '<'; i += 3;
} else if (strncmp(&src[i], "&gt;", 4) == 0) {
dst[j++] = '>'; i += 3;
} else if (strncmp(&src[i], "&quot;", 6) == 0) {
dst[j++] = '"'; i += 5;
} else if (strncmp(&src[i], "&apos;", 6) == 0) {
dst[j++] = '\''; i += 5;
} else if (strncmp(&src[i], "&nbsp;", 6) == 0) {
dst[j++] = ' '; i += 5;
} else if (strncmp(&src[i], "&ndash;", 7) == 0) {
dst[j++] = '-'; i += 6;
} else if (strncmp(&src[i], "&mdash;", 7) == 0) {
dst[j++] = '-'; dst[j++] = '-'; i += 6;
} else if (strncmp(&src[i], "&hellip;", 8) == 0) {
dst[j++] = '.'; dst[j++] = '.'; dst[j++] = '.'; i += 7;
} else if (strncmp(&src[i], "&lsquo;", 7) == 0 || strncmp(&src[i], "&rsquo;", 7) == 0) {
dst[j++] = '\''; i += 6;
} else if (strncmp(&src[i], "&ldquo;", 7) == 0 || strncmp(&src[i], "&rdquo;", 7) == 0) {
dst[j++] = '"'; i += 6;
} else if (src[i+1] == '#') {
/* Numeric entity */
char decoded[4] = {0};
int consumed = decode_numeric_entity(&src[i], decoded, sizeof(decoded));
if (consumed > 0) {
for (int k = 0; decoded[k] && j < max_len - 1; k++) {
dst[j++] = decoded[k];
}
i += consumed - 1;
} else {
dst[j++] = src[i];
}
} else {
dst[j++] = src[i];
}
} else if (src[i] == '\n' || src[i] == '\r' || src[i] == '\t') {
dst[j++] = ' ';
} else {
dst[j++] = src[i];
}
}
}
dst[j] = '\0';
/* Collapse multiple spaces */
char *read = dst, *write = dst;
bool last_space = false;
while (*read) {
if (*read == ' ') {
if (!last_space) {
*write++ = *read;
}
last_space = true;
} else {
*write++ = *read;
last_space = false;
}
read++;
}
*write = '\0';
}
/* Parse JSON response and populate articles */
static int parse_news_json(const char *json_str)
{
cJSON *root = cJSON_Parse(json_str);
if (root == NULL) {
LOG_WARN("Failed to parse news JSON");
return -1;
}
cJSON *articles = cJSON_GetObjectItemCaseSensitive(root, "articles");
if (!cJSON_IsArray(articles)) {
LOG_WARN("No articles array in news JSON");
cJSON_Delete(root);
return -1;
}
int count = 0;
cJSON *article;
cJSON_ArrayForEach(article, articles) {
if (count >= MAX_NEWS_ARTICLES) break;
cJSON *title = cJSON_GetObjectItemCaseSensitive(article, "title");
cJSON *content = cJSON_GetObjectItemCaseSensitive(article, "content");
cJSON *description = cJSON_GetObjectItemCaseSensitive(article, "description");
cJSON *link = cJSON_GetObjectItemCaseSensitive(article, "link");
cJSON *author = cJSON_GetObjectItemCaseSensitive(article, "author");
NewsArticle *art = &news_state.articles[count];
memset(art, 0, sizeof(NewsArticle));
if (cJSON_IsString(title) && title->valuestring) {
strip_html(art->title, title->valuestring, sizeof(art->title));
}
/* Prefer content, fallback to description */
const char *text = NULL;
if (cJSON_IsString(content) && content->valuestring && strlen(content->valuestring) > 0) {
text = content->valuestring;
} else if (cJSON_IsString(description) && description->valuestring) {
text = description->valuestring;
}
if (text) {
strip_html(art->content, text, sizeof(art->content));
} else {
/* Use title as fallback */
strncpy(art->content, art->title, sizeof(art->content) - 1);
}
if (cJSON_IsString(link) && link->valuestring) {
strncpy(art->link, link->valuestring, sizeof(art->link) - 1);
}
if (cJSON_IsString(author) && author->valuestring) {
strncpy(art->author, author->valuestring, sizeof(art->author) - 1);
}
count++;
}
cJSON_Delete(root);
return count;
}
/* Separator between articles */
#define NEWS_SEPARATOR " • "
#define NEWS_SEPARATOR_LEN 5
/* Forward declarations */
static int news_find_article_at_x(int click_x);
/* Get UTF-8 text width using Xft */
static int news_text_width(const char *text)
{
if (dwn == NULL || text == NULL) return 0;
if (dwn->xft_font != NULL) {
XGlyphInfo extents;
XftTextExtentsUtf8(dwn->display, dwn->xft_font,
(const FcChar8 *)text, strlen(text), &extents);
return extents.xOff;
}
if (dwn->font != NULL) {
return XTextWidth(dwn->font, text, strlen(text));
}
return strlen(text) * 8;
}
/* Draw UTF-8 text using Xft with clipping */
static void news_draw_text_clipped(Drawable d, int x, int y, const char *text,
unsigned long color, XRectangle *clip)
{
if (dwn == NULL || dwn->display == NULL || text == NULL) return;
if (dwn->xft_font != NULL) {
XftDraw *xft_draw = XftDrawCreate(dwn->display, d,
DefaultVisual(dwn->display, dwn->screen),
dwn->colormap);
if (xft_draw != NULL) {
if (clip != NULL) {
Region region = XCreateRegion();
XUnionRectWithRegion(clip, region, region);
XftDrawSetClip(xft_draw, region);
XDestroyRegion(region);
}
XftColor xft_color;
XRenderColor render_color;
render_color.red = ((color >> 16) & 0xFF) * 257;
render_color.green = ((color >> 8) & 0xFF) * 257;
render_color.blue = (color & 0xFF) * 257;
render_color.alpha = 0xFFFF;
XftColorAllocValue(dwn->display,
DefaultVisual(dwn->display, dwn->screen),
dwn->colormap, &render_color, &xft_color);
XftDrawStringUtf8(xft_draw, &xft_color, dwn->xft_font,
x, y, (const FcChar8 *)text, strlen(text));
XftColorFree(dwn->display,
DefaultVisual(dwn->display, dwn->screen),
dwn->colormap, &xft_color);
XftDrawDestroy(xft_draw);
return;
}
}
if (clip != NULL) {
XSetClipRectangles(dwn->display, dwn->gc, 0, 0, clip, 1, Unsorted);
}
XSetForeground(dwn->display, dwn->gc, color);
XDrawString(dwn->display, d, dwn->gc, x, y, text, strlen(text));
if (clip != NULL) {
XSetClipMask(dwn->display, dwn->gc, None);
}
}
/* Get display text for an article */
static void get_article_display_text(int index, char *buf, size_t buf_size)
{
if (index < 0 || index >= news_state.article_count) {
buf[0] = '\0';
return;
}
NewsArticle *art = &news_state.articles[index];
if (art->content[0] && strcmp(art->content, art->title) != 0) {
snprintf(buf, buf_size, "%s: %s", art->title, art->content);
} else {
snprintf(buf, buf_size, "%s", art->title);
}
}
/* Recalculate cached widths after fetching */
static void news_recalc_widths(void)
{
if (dwn == NULL) {
return;
}
int separator_width = news_text_width(NEWS_SEPARATOR);
news_state.total_width = 0;
for (int i = 0; i < news_state.article_count; i++) {
char display_text[2048];
get_article_display_text(i, display_text, sizeof(display_text));
news_state.display_widths[i] = news_text_width(display_text);
news_state.total_width += news_state.display_widths[i] + separator_width;
}
}
/* Background fetch thread */
static void *news_fetch_thread(void *arg)
{
(void)arg;
CURL *curl = curl_easy_init();
if (curl == NULL) {
pthread_mutex_lock(&news_mutex);
news_state.fetching = false;
news_state.has_error = true;
pthread_mutex_unlock(&news_mutex);
return NULL;
}
CurlBuffer buf = {0};
buf.data = malloc(1);
buf.data[0] = '\0';
curl_easy_setopt(curl, CURLOPT_URL, NEWS_API_URL);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, news_curl_write_cb);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buf);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L);
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 5L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
curl_easy_setopt(curl, CURLOPT_USERAGENT, "DWN/1.0");
CURLcode res = curl_easy_perform(curl);
pthread_mutex_lock(&news_mutex);
if (res == CURLE_OK && buf.data && buf.size > 0) {
int count = parse_news_json(buf.data);
if (count > 0) {
news_state.article_count = count;
news_state.has_error = false;
news_state.current_article = 0;
news_state.scroll_offset = 0.0;
news_state.last_scroll_update = 0;
news_state.widths_dirty = true;
LOG_INFO("Fetched %d news articles", count);
} else {
news_state.has_error = true;
}
} else {
news_state.has_error = true;
LOG_DEBUG("News fetch failed: %s", curl_easy_strerror(res));
}
news_state.fetching = false;
news_state.last_fetch = get_time_ms();
pthread_mutex_unlock(&news_mutex);
free(buf.data);
curl_easy_cleanup(curl);
return NULL;
}
/* ========== Public API ========== */
void news_init(void)
{
memset(&news_state, 0, sizeof(news_state));
news_state.last_fetch = 0;
/* Start initial fetch */
news_fetch_async();
LOG_INFO("News ticker initialized");
}
void news_cleanup(void)
{
/* Wait for any pending fetch */
if (fetch_running) {
pthread_join(fetch_thread, NULL);
fetch_running = 0;
}
}
void news_fetch_async(void)
{
pthread_mutex_lock(&news_mutex);
if (news_state.fetching) {
pthread_mutex_unlock(&news_mutex);
return;
}
news_state.fetching = true;
pthread_mutex_unlock(&news_mutex);
/* Wait for previous thread if still running */
if (fetch_running) {
pthread_join(fetch_thread, NULL);
}
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;
}
}
void news_update(void)
{
long now = get_time_ms();
pthread_mutex_lock(&news_mutex);
/* Auto-refresh if interval passed */
if (!news_state.fetching && (now - news_state.last_fetch) >= FETCH_INTERVAL) {
pthread_mutex_unlock(&news_mutex);
news_fetch_async();
pthread_mutex_lock(&news_mutex);
}
/* Time-based smooth scrolling */
if (!news_state.interactive_mode && news_state.article_count > 0) {
if (news_state.last_scroll_update > 0) {
long delta_ms = now - news_state.last_scroll_update;
/* Calculate smooth sub-pixel scroll amount based on elapsed time */
double scroll_amount = (SCROLL_SPEED_PPS * delta_ms) / 1000.0;
news_state.scroll_offset += scroll_amount;
}
news_state.last_scroll_update = now;
}
pthread_mutex_unlock(&news_mutex);
}
void news_next_article(void)
{
pthread_mutex_lock(&news_mutex);
if (news_state.article_count > 0) {
news_state.current_article = (news_state.current_article + 1) % news_state.article_count;
news_state.scroll_offset = 0.0;
news_state.last_scroll_update = 0;
news_state.interactive_mode = true;
}
pthread_mutex_unlock(&news_mutex);
}
void news_prev_article(void)
{
pthread_mutex_lock(&news_mutex);
if (news_state.article_count > 0) {
news_state.current_article--;
if (news_state.current_article < 0) {
news_state.current_article = news_state.article_count - 1;
}
news_state.scroll_offset = 0.0;
news_state.last_scroll_update = 0;
news_state.interactive_mode = true;
}
pthread_mutex_unlock(&news_mutex);
}
void news_open_current(void)
{
pthread_mutex_lock(&news_mutex);
if (news_state.article_count > 0 && news_state.total_width > 0) {
int article_index = news_find_article_at_x(news_state.render_x);
if (article_index < 0) article_index = 0;
char url[512];
strncpy(url, news_state.articles[article_index].link, sizeof(url) - 1);
url[sizeof(url) - 1] = '\0';
pthread_mutex_unlock(&news_mutex);
pid_t pid = fork();
if (pid == 0) {
setsid();
pid_t pid2 = fork();
if (pid2 == 0) {
execlp("xdg-open", "xdg-open", url, NULL);
_exit(1);
}
_exit(0);
} else if (pid > 0) {
int status;
waitpid(pid, &status, 0);
}
} else {
pthread_mutex_unlock(&news_mutex);
}
}
/* Find article at given x position within news area */
static int news_find_article_at_x(int click_x)
{
if (news_state.article_count == 0 || news_state.total_width == 0) {
return -1;
}
int separator_width = news_text_width(NEWS_SEPARATOR);
int scroll_offset = (int)news_state.scroll_offset % news_state.total_width;
int rel_x = click_x - news_state.render_x;
int content_x = scroll_offset + rel_x;
if (content_x < 0) {
content_x += news_state.total_width;
}
content_x = content_x % news_state.total_width;
int pos = 0;
for (int i = 0; i < news_state.article_count; i++) {
int article_total = news_state.display_widths[i] + separator_width;
if (content_x < pos + article_total) {
return i;
}
pos += article_total;
}
return 0;
}
void news_render(Panel *panel, int x, int max_width, int *used_width)
{
if (panel == NULL || used_width == NULL || dwn == NULL) {
*used_width = 0;
return;
}
if (dwn->xft_font == NULL && dwn->font == NULL) {
*used_width = 0;
return;
}
pthread_mutex_lock(&news_mutex);
if (news_state.article_count == 0) {
pthread_mutex_unlock(&news_mutex);
*used_width = 0;
return;
}
if (news_state.widths_dirty) {
news_recalc_widths();
news_state.widths_dirty = false;
}
if (news_state.total_width == 0) {
pthread_mutex_unlock(&news_mutex);
*used_width = 0;
return;
}
news_state.render_x = x;
news_state.render_width = max_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;
int pos = 0;
for (int i = 0; i < news_state.article_count; i++) {
int article_total = news_state.display_widths[i] + separator_width;
if (pos + article_total > scroll_offset) {
start_article = i;
draw_x = x + (pos - scroll_offset);
break;
}
pos += article_total;
}
int current_article = start_article;
while (draw_x < x + max_width && articles_drawn < news_state.article_count * 2) {
char display_text[2048];
get_article_display_text(current_article, display_text, sizeof(display_text));
if (draw_x + news_state.display_widths[current_article] > x) {
news_draw_text_clipped(panel->buffer, draw_x, text_y, display_text,
colors->panel_fg, &clip_rect);
}
draw_x += news_state.display_widths[current_article];
if (draw_x < x + max_width) {
news_draw_text_clipped(panel->buffer, draw_x, text_y, NEWS_SEPARATOR,
colors->panel_fg, &clip_rect);
}
draw_x += separator_width;
current_article = (current_article + 1) % news_state.article_count;
articles_drawn++;
}
pthread_mutex_unlock(&news_mutex);
*used_width = max_width;
}
void news_handle_click(int x, int y)
{
(void)y;
pthread_mutex_lock(&news_mutex);
if (news_state.article_count == 0) {
pthread_mutex_unlock(&news_mutex);
return;
}
int article_index = news_find_article_at_x(x);
if (article_index >= 0 && article_index < news_state.article_count) {
char url[512];
strncpy(url, news_state.articles[article_index].link, sizeof(url) - 1);
url[sizeof(url) - 1] = '\0';
pthread_mutex_unlock(&news_mutex);
pid_t pid = fork();
if (pid == 0) {
setsid();
pid_t pid2 = fork();
if (pid2 == 0) {
execlp("xdg-open", "xdg-open", url, NULL);
_exit(1);
}
_exit(0);
} else if (pid > 0) {
int status;
waitpid(pid, &status, 0);
}
} else {
pthread_mutex_unlock(&news_mutex);
}
}
void news_lock(void)
{
pthread_mutex_lock(&news_mutex);
}
void news_unlock(void)
{
pthread_mutex_unlock(&news_mutex);
}
+856
View File
@@ -0,0 +1,856 @@
/*
* DWN - Desktop Window Manager
* D-Bus Notification daemon implementation
*/
#include "notifications.h"
#include "config.h"
#include "util.h"
#include <string.h>
#include <stdlib.h>
#include <X11/Xft/Xft.h>
/* D-Bus connection */
DBusConnection *dbus_conn = NULL;
/* Notification list */
static Notification *notification_list = NULL;
static uint32_t next_notification_id = 1;
/* Notification dimensions - dynamic based on screen size */
#define NOTIFICATION_MIN_WIDTH 280
#define NOTIFICATION_PADDING 12
#define NOTIFICATION_MARGIN 10
/* Get max width: 33% of screen width */
static int notification_max_width(void)
{
if (dwn == NULL) return 500;
return dwn->screen_width / 3;
}
/* Get max height: 50% of screen height */
static int notification_max_height(void)
{
if (dwn == NULL) return 600;
return dwn->screen_height / 2;
}
/* Default timeout */
#define DEFAULT_TIMEOUT 5000
/* ========== UTF-8 text helpers ========== */
/* Get text width using Xft (UTF-8 aware) */
static int notif_text_width(const char *text, int len)
{
if (text == NULL || dwn == NULL) return len * 7; /* Fallback estimate */
if (dwn->xft_font != NULL) {
XGlyphInfo extents;
XftTextExtentsUtf8(dwn->display, dwn->xft_font,
(const FcChar8 *)text, len, &extents);
return extents.xOff;
}
if (dwn->font != NULL) {
return XTextWidth(dwn->font, text, len);
}
return len * 7; /* Fallback estimate */
}
/* Get line height */
static int notif_line_height(void)
{
if (dwn == NULL) return 14;
if (dwn->xft_font != NULL) {
return dwn->xft_font->ascent + dwn->xft_font->descent + 2;
}
if (dwn->font != NULL) {
return dwn->font->ascent + dwn->font->descent + 2;
}
return 14;
}
/* Get font ascent */
static int notif_font_ascent(void)
{
if (dwn == NULL) return 12;
if (dwn->xft_font != NULL) {
return dwn->xft_font->ascent;
}
if (dwn->font != NULL) {
return dwn->font->ascent;
}
return 12;
}
/* Draw text using Xft (UTF-8 aware) */
static void notif_draw_text(Window win, int x, int y, const char *text,
int len, unsigned long color)
{
if (text == NULL || dwn == NULL || dwn->display == NULL) return;
/* Use Xft for UTF-8 support */
if (dwn->xft_font != NULL) {
XftDraw *xft_draw = XftDrawCreate(dwn->display, win,
DefaultVisual(dwn->display, dwn->screen),
dwn->colormap);
if (xft_draw != NULL) {
XftColor xft_color;
XRenderColor render_color;
render_color.red = ((color >> 16) & 0xFF) * 257;
render_color.green = ((color >> 8) & 0xFF) * 257;
render_color.blue = (color & 0xFF) * 257;
render_color.alpha = 0xFFFF;
XftColorAllocValue(dwn->display, DefaultVisual(dwn->display, dwn->screen),
dwn->colormap, &render_color, &xft_color);
XftDrawStringUtf8(xft_draw, &xft_color, dwn->xft_font,
x, y, (const FcChar8 *)text, len);
XftColorFree(dwn->display, DefaultVisual(dwn->display, dwn->screen),
dwn->colormap, &xft_color);
XftDrawDestroy(xft_draw);
return;
}
}
/* Fallback to legacy X11 text */
XSetForeground(dwn->display, dwn->gc, color);
XDrawString(dwn->display, win, dwn->gc, x, y, text, len);
}
/* ========== Initialization ========== */
bool notifications_init(void)
{
DBusError err;
dbus_error_init(&err);
/* Connect to session bus */
dbus_conn = dbus_bus_get(DBUS_BUS_SESSION, &err);
if (dbus_error_is_set(&err)) {
LOG_ERROR("D-Bus connection error: %s", err.message);
dbus_error_free(&err);
return false;
}
if (dbus_conn == NULL) {
LOG_ERROR("Failed to connect to D-Bus session bus");
return false;
}
/* Register notification service */
if (!notifications_register_service()) {
LOG_WARN("Could not register notification service (another daemon running?)");
/* Don't fail - we can still function as a WM */
}
LOG_INFO("Notification daemon initialized");
return true;
}
void notifications_cleanup(void)
{
/* Close all notifications */
notification_close_all();
/* D-Bus connection is managed by libdbus */
dbus_conn = NULL;
}
/* ========== D-Bus handling ========== */
bool notifications_register_service(void)
{
if (dbus_conn == NULL) {
return false;
}
DBusError err;
dbus_error_init(&err);
/* Request the notification service name */
int result = dbus_bus_request_name(dbus_conn, "org.freedesktop.Notifications",
DBUS_NAME_FLAG_REPLACE_EXISTING, &err);
if (dbus_error_is_set(&err)) {
LOG_ERROR("D-Bus name request error: %s", err.message);
dbus_error_free(&err);
return false;
}
if (result != DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER) {
LOG_WARN("Could not become primary owner of notification service");
return false;
}
/* Add message filter */
dbus_connection_add_filter(dbus_conn, notifications_handle_message, NULL, NULL);
LOG_INFO("Registered as org.freedesktop.Notifications");
return true;
}
void notifications_process_messages(void)
{
if (dbus_conn == NULL) {
return;
}
/* Process pending D-Bus messages (non-blocking) */
dbus_connection_read_write(dbus_conn, 0);
while (dbus_connection_dispatch(dbus_conn) == DBUS_DISPATCH_DATA_REMAINS) {
/* Keep dispatching */
}
}
DBusHandlerResult notifications_handle_message(DBusConnection *conn,
DBusMessage *msg,
void *user_data)
{
(void)user_data;
const char *interface = dbus_message_get_interface(msg);
const char *member = dbus_message_get_member(msg);
if (interface == NULL || member == NULL) {
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
}
if (strcmp(interface, "org.freedesktop.Notifications") != 0) {
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
}
/* Handle Notify method */
if (strcmp(member, "Notify") == 0) {
DBusMessageIter args;
if (!dbus_message_iter_init(msg, &args)) {
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
}
char *app_name = "";
uint32_t replaces_id = 0;
char *icon = "";
char *summary = "";
char *body = "";
int32_t timeout = -1;
/* Parse arguments */
if (dbus_message_iter_get_arg_type(&args) == DBUS_TYPE_STRING) {
dbus_message_iter_get_basic(&args, &app_name);
dbus_message_iter_next(&args);
}
if (dbus_message_iter_get_arg_type(&args) == DBUS_TYPE_UINT32) {
dbus_message_iter_get_basic(&args, &replaces_id);
dbus_message_iter_next(&args);
}
if (dbus_message_iter_get_arg_type(&args) == DBUS_TYPE_STRING) {
dbus_message_iter_get_basic(&args, &icon);
dbus_message_iter_next(&args);
}
if (dbus_message_iter_get_arg_type(&args) == DBUS_TYPE_STRING) {
dbus_message_iter_get_basic(&args, &summary);
dbus_message_iter_next(&args);
}
if (dbus_message_iter_get_arg_type(&args) == DBUS_TYPE_STRING) {
dbus_message_iter_get_basic(&args, &body);
dbus_message_iter_next(&args);
}
/* Skip actions array */
if (dbus_message_iter_get_arg_type(&args) == DBUS_TYPE_ARRAY) {
dbus_message_iter_next(&args);
}
/* Skip hints dict */
if (dbus_message_iter_get_arg_type(&args) == DBUS_TYPE_ARRAY) {
dbus_message_iter_next(&args);
}
if (dbus_message_iter_get_arg_type(&args) == DBUS_TYPE_INT32) {
dbus_message_iter_get_basic(&args, &timeout);
}
/* Show notification */
uint32_t id = notification_show(app_name, summary, body, icon, timeout);
/* Send reply */
DBusMessage *reply = dbus_message_new_method_return(msg);
dbus_message_append_args(reply, DBUS_TYPE_UINT32, &id, DBUS_TYPE_INVALID);
dbus_connection_send(conn, reply, NULL);
dbus_message_unref(reply);
return DBUS_HANDLER_RESULT_HANDLED;
}
/* Handle CloseNotification method */
if (strcmp(member, "CloseNotification") == 0) {
DBusMessageIter args;
if (dbus_message_iter_init(msg, &args) &&
dbus_message_iter_get_arg_type(&args) == DBUS_TYPE_UINT32) {
uint32_t id;
dbus_message_iter_get_basic(&args, &id);
notification_close(id);
}
DBusMessage *reply = dbus_message_new_method_return(msg);
dbus_connection_send(conn, reply, NULL);
dbus_message_unref(reply);
return DBUS_HANDLER_RESULT_HANDLED;
}
/* Handle GetCapabilities method */
if (strcmp(member, "GetCapabilities") == 0) {
DBusMessage *reply = dbus_message_new_method_return(msg);
DBusMessageIter args_iter, array_iter;
dbus_message_iter_init_append(reply, &args_iter);
dbus_message_iter_open_container(&args_iter, DBUS_TYPE_ARRAY, "s", &array_iter);
const char *caps[] = { "body", "body-markup" };
for (size_t i = 0; i < sizeof(caps) / sizeof(caps[0]); i++) {
dbus_message_iter_append_basic(&array_iter, DBUS_TYPE_STRING, &caps[i]);
}
dbus_message_iter_close_container(&args_iter, &array_iter);
dbus_connection_send(conn, reply, NULL);
dbus_message_unref(reply);
return DBUS_HANDLER_RESULT_HANDLED;
}
/* Handle GetServerInformation method */
if (strcmp(member, "GetServerInformation") == 0) {
DBusMessage *reply = dbus_message_new_method_return(msg);
const char *name = "DWN";
const char *vendor = "DWN Project";
const char *version = DWN_VERSION;
const char *spec_version = "1.2";
dbus_message_append_args(reply,
DBUS_TYPE_STRING, &name,
DBUS_TYPE_STRING, &vendor,
DBUS_TYPE_STRING, &version,
DBUS_TYPE_STRING, &spec_version,
DBUS_TYPE_INVALID);
dbus_connection_send(conn, reply, NULL);
dbus_message_unref(reply);
return DBUS_HANDLER_RESULT_HANDLED;
}
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
}
/* ========== Notification management ========== */
#define MAX_VISIBLE_NOTIFICATIONS 3
/* Calculate notification size based on content - accounts for word wrapping */
static void notification_calculate_size(const char *summary, const char *body,
int *out_width, int *out_height)
{
int max_width = notification_max_width();
int max_height = notification_max_height();
int min_height = 80;
int content_max_width = max_width - 2 * NOTIFICATION_PADDING;
int width = NOTIFICATION_MIN_WIDTH;
int height = NOTIFICATION_PADDING * 2;
int line_height = notif_line_height();
/* Add space for summary */
if (summary != NULL && summary[0] != '\0') {
int summary_width = NOTIFICATION_PADDING * 2;
summary_width += notif_text_width(summary, strlen(summary));
if (summary_width > width) width = summary_width;
height += line_height + 4;
}
/* Count wrapped lines in body */
if (body != NULL && body[0] != '\0') {
const char *p = body;
int wrapped_line_count = 0;
while (*p != '\0') {
/* Find end of this logical line (newline) */
const char *line_start = p;
while (*p != '\0' && *p != '\n') p++;
size_t logical_line_len = p - line_start;
if (logical_line_len == 0) {
/* Empty line */
wrapped_line_count++;
} else {
/* Count how many wrapped lines this logical line needs */
const char *lp = line_start;
while (lp < line_start + logical_line_len) {
size_t remaining = (line_start + logical_line_len) - lp;
size_t fit_len = 0;
/* Find how much fits on one line */
for (size_t i = 0; i < remaining; i++) {
if ((lp[i] & 0xC0) == 0x80) continue; /* Skip continuation bytes */
int w = notif_text_width(lp, i + 1);
if (w > content_max_width) break;
fit_len = i + 1;
}
/* Include trailing UTF-8 continuation bytes */
while (fit_len < remaining && (lp[fit_len] & 0xC0) == 0x80) {
fit_len++;
}
/* Force at least one character */
if (fit_len == 0 && remaining > 0) {
fit_len = 1;
while (fit_len < remaining && (lp[fit_len] & 0xC0) == 0x80) {
fit_len++;
}
}
if (fit_len == 0) break;
lp += fit_len;
/* Skip leading spaces on continuation */
while (lp < line_start + logical_line_len && *lp == ' ') lp++;
wrapped_line_count++;
/* Stop if we'd exceed max height */
if (height + (wrapped_line_count * line_height) > max_height - line_height - NOTIFICATION_PADDING) {
goto done_counting;
}
}
}
if (*p == '\n') p++;
}
done_counting:
height += wrapped_line_count * line_height;
}
/* Add space for app name at bottom */
height += line_height + NOTIFICATION_PADDING;
/* Use full max_width since we're wrapping, not truncating */
width = max_width;
/* Clamp to min/max */
if (width < NOTIFICATION_MIN_WIDTH) width = NOTIFICATION_MIN_WIDTH;
if (width > max_width) width = max_width;
if (height < min_height) height = min_height;
if (height > max_height) height = max_height;
*out_width = width;
*out_height = height;
}
uint32_t notification_show(const char *app_name, const char *summary,
const char *body, const char *icon, int timeout)
{
/* Count existing notifications and close oldest if we have too many */
int count = 0;
Notification *oldest = NULL;
for (Notification *n = notification_list; n != NULL; n = n->next) {
count++;
oldest = n; /* Last in list is oldest (we prepend new ones) */
}
/* Close oldest notification if we're at the limit */
if (count >= MAX_VISIBLE_NOTIFICATIONS && oldest != NULL) {
notification_close(oldest->id);
}
Notification *notif = dwn_calloc(1, sizeof(Notification));
notif->id = next_notification_id++;
if (app_name) strncpy(notif->app_name, app_name, sizeof(notif->app_name) - 1);
if (summary) strncpy(notif->summary, summary, sizeof(notif->summary) - 1);
/* Dynamically allocate body - unlimited size */
if (body != NULL && body[0] != '\0') {
notif->body_len = strlen(body);
notif->body = dwn_malloc(notif->body_len + 1);
if (notif->body != NULL) {
memcpy(notif->body, body, notif->body_len + 1);
}
} else {
notif->body = NULL;
notif->body_len = 0;
}
if (icon) strncpy(notif->icon, icon, sizeof(notif->icon) - 1);
notif->timeout = (timeout < 0) ? DEFAULT_TIMEOUT : timeout;
notif->urgency = NOTIFY_URGENCY_NORMAL;
notif->expire_time = (notif->timeout > 0) ?
get_time_ms() + notif->timeout : 0;
/* Calculate size based on content */
int notif_width, notif_height;
notification_calculate_size(summary, body, &notif_width, &notif_height);
notif->width = notif_width;
notif->height = notif_height;
/* Create notification window */
if (dwn != NULL && dwn->display != NULL) {
XSetWindowAttributes swa;
swa.override_redirect = True;
swa.background_pixel = dwn->config->colors.notification_bg;
swa.event_mask = ExposureMask | ButtonPressMask;
notif->window = XCreateWindow(dwn->display, dwn->root,
0, 0,
notif->width, notif->height,
1,
CopyFromParent, InputOutput, CopyFromParent,
CWOverrideRedirect | CWBackPixel | CWEventMask,
&swa);
XSetWindowBorder(dwn->display, notif->window,
dwn->config->colors.border_focused);
}
/* Add to list */
notif->next = notification_list;
notification_list = notif;
/* Position and show */
notifications_position();
notification_render(notif);
if (notif->window != None) {
XMapRaised(dwn->display, notif->window);
}
LOG_DEBUG("Notification %u (%dx%d): %s - %s", notif->id,
notif->width, notif->height, notif->summary, notif->body);
return notif->id;
}
void notification_close(uint32_t id)
{
Notification *prev = NULL;
Notification *notif = notification_list;
while (notif != NULL) {
if (notif->id == id) {
/* Remove from list */
if (prev != NULL) {
prev->next = notif->next;
} else {
notification_list = notif->next;
}
/* Destroy window */
if (notif->window != None && dwn != NULL && dwn->display != NULL) {
XDestroyWindow(dwn->display, notif->window);
}
/* Free dynamically allocated body */
if (notif->body != NULL) {
dwn_free(notif->body);
notif->body = NULL;
}
dwn_free(notif);
/* Reposition remaining notifications */
notifications_position();
return;
}
prev = notif;
notif = notif->next;
}
}
void notification_close_all(void)
{
while (notification_list != NULL) {
notification_close(notification_list->id);
}
}
Notification *notification_find(uint32_t id)
{
for (Notification *n = notification_list; n != NULL; n = n->next) {
if (n->id == id) {
return n;
}
}
return NULL;
}
Notification *notification_find_by_window(Window window)
{
for (Notification *n = notification_list; n != NULL; n = n->next) {
if (n->window == window) {
return n;
}
}
return NULL;
}
/* ========== Rendering ========== */
void notification_render(Notification *notif)
{
if (notif == NULL || notif->window == None) {
LOG_DEBUG("notification_render: notif or window is NULL");
return;
}
if (dwn == NULL || dwn->display == NULL) {
LOG_DEBUG("notification_render: dwn or display is NULL");
return;
}
if (dwn->xft_font == NULL && dwn->font == NULL) {
LOG_ERROR("notification_render: no font available - cannot render text");
return;
}
Display *dpy = dwn->display;
const ColorScheme *colors = config_get_colors();
if (colors == NULL) {
LOG_ERROR("notification_render: colors is NULL");
return;
}
LOG_DEBUG("Rendering notification: summary='%s', body_len=%zu",
notif->summary, notif->body_len);
/* Set legacy font in GC if available (for fallback) */
if (dwn->font != NULL) {
XSetFont(dpy, dwn->gc, dwn->font->fid);
}
/* Clear background using dynamic size */
XSetForeground(dpy, dwn->gc, colors->notification_bg);
XFillRectangle(dpy, notif->window, dwn->gc, 0, 0,
notif->width, notif->height);
/* Draw summary (title) */
int line_height = notif_line_height();
int y = NOTIFICATION_PADDING + notif_font_ascent();
notif_draw_text(notif->window, NOTIFICATION_PADDING, y,
notif->summary, strlen(notif->summary),
colors->notification_fg);
/* Draw body - handle multiple lines */
y += line_height + 4;
int max_width = notif->width - 2 * NOTIFICATION_PADDING;
int max_y = notif->height - line_height - NOTIFICATION_PADDING;
/* Only process body if it exists */
if (notif->body != NULL && notif->body_len > 0) {
/* Dynamically allocate body copy for tokenization */
char *body_copy = dwn_malloc(notif->body_len + 1);
if (body_copy != NULL) {
memcpy(body_copy, notif->body, notif->body_len + 1);
/* Split by newlines and draw each line */
char *line = body_copy;
char *next;
while (line != NULL && *line != '\0' && y < max_y) {
/* Find next newline */
next = strchr(line, '\n');
if (next != NULL) {
*next = '\0';
next++;
}
/* Skip empty lines but count them for spacing */
if (*line == '\0') {
y += line_height / 2; /* Half-height for empty lines */
line = next;
continue;
}
/* Word wrap long lines instead of truncating */
size_t line_len = strlen(line);
const char *p = line;
while (*p != '\0' && y < max_y) {
/* Find how much text fits on this line */
size_t fit_len = 0;
size_t last_space = 0;
for (size_t i = 0; p[i] != '\0'; i++) {
/* Skip UTF-8 continuation bytes for character counting */
if ((p[i] & 0xC0) == 0x80) continue;
int width = notif_text_width(p, i + 1);
if (width > max_width) {
break;
}
fit_len = i + 1;
/* Track last space for word wrapping */
if (p[i] == ' ') {
last_space = i;
}
}
/* Adjust to include full UTF-8 characters */
while (fit_len < line_len && (p[fit_len] & 0xC0) == 0x80) {
fit_len++;
}
/* If we couldn't fit anything, force at least one character */
if (fit_len == 0 && *p != '\0') {
fit_len = 1;
while (fit_len < line_len && (p[fit_len] & 0xC0) == 0x80) {
fit_len++;
}
}
/* Prefer breaking at word boundary if possible */
if (fit_len < strlen(p) && last_space > 0 && last_space > fit_len / 2) {
fit_len = last_space + 1; /* Include the space */
}
/* Draw this segment */
if (fit_len > 0) {
char *segment = dwn_malloc(fit_len + 1);
if (segment != NULL) {
memcpy(segment, p, fit_len);
segment[fit_len] = '\0';
notif_draw_text(notif->window, NOTIFICATION_PADDING, y,
segment, strlen(segment), colors->panel_fg);
dwn_free(segment);
}
p += fit_len;
/* Skip leading spaces on new line */
while (*p == ' ') p++;
y += line_height;
} else {
break;
}
}
line = next;
}
dwn_free(body_copy);
}
}
/* Draw app name at bottom */
if (notif->app_name[0] != '\0') {
y = notif->height - NOTIFICATION_PADDING;
notif_draw_text(notif->window, NOTIFICATION_PADDING, y,
notif->app_name, strlen(notif->app_name),
colors->workspace_inactive);
}
/* Force the drawing to be sent to X server */
XFlush(dpy);
}
void notifications_render_all(void)
{
for (Notification *n = notification_list; n != NULL; n = n->next) {
notification_render(n);
}
}
void notifications_update(void)
{
long now = get_time_ms();
/* Check for expired notifications */
Notification *notif = notification_list;
while (notif != NULL) {
Notification *next = notif->next;
if (notif->expire_time > 0 && now >= notif->expire_time) {
notification_close(notif->id);
}
notif = next;
}
}
void notifications_position(void)
{
if (dwn == NULL || dwn->display == NULL) {
return;
}
int y = NOTIFICATION_MARGIN;
/* Account for top panel */
if (dwn->config && dwn->config->top_panel_enabled) {
y += config_get_panel_height();
}
for (Notification *n = notification_list; n != NULL; n = n->next) {
if (n->window != None) {
/* Position from right edge using notification's own width */
int x = dwn->screen_width - n->width - NOTIFICATION_MARGIN;
XMoveWindow(dwn->display, n->window, x, y);
}
/* Stack using notification's own height */
y += n->height + NOTIFICATION_MARGIN;
}
}
void notifications_raise_all(void)
{
if (dwn == NULL || dwn->display == NULL) {
return;
}
/* Raise all notification windows to the top */
for (Notification *n = notification_list; n != NULL; n = n->next) {
if (n->window != None) {
XRaiseWindow(dwn->display, n->window);
}
}
}
/* ========== Server info ========== */
void notifications_get_server_info(const char **name, const char **vendor,
const char **version, const char **spec_version)
{
if (name) *name = "DWN";
if (vendor) *vendor = "DWN Project";
if (version) *version = DWN_VERSION;
if (spec_version) *spec_version = "1.2";
}
void notifications_get_capabilities(const char ***caps, int *count)
{
static const char *capabilities[] = { "body", "body-markup" };
if (caps) *caps = capabilities;
if (count) *count = 2;
}
+892
View File
@@ -0,0 +1,892 @@
/*
* DWN - Desktop Window Manager
* Panel system implementation
*/
#include "panel.h"
#include "workspace.h"
#include "layout.h"
#include "client.h"
#include "config.h"
#include "util.h"
#include "atoms.h"
#include "systray.h"
#include "news.h"
#include <string.h>
#include <stdbool.h>
#include <time.h>
#include <stdio.h>
#include <stdlib.h>
#include <X11/Xft/Xft.h>
/* Panel padding and spacing */
#define PANEL_PADDING 8
#define WIDGET_SPACING 12
#define WORKSPACE_WIDTH 28
#define TASKBAR_ITEM_WIDTH 150
/* Clock format */
#define CLOCK_FORMAT "%H:%M:%S"
#define DATE_FORMAT "%Y-%m-%d"
/* Static clock buffer */
static char clock_buffer[32] = "";
static char date_buffer[32] = "";
/* System stats */
typedef struct {
int cpu_percent; /* CPU usage percentage */
int mem_percent; /* Memory usage percentage */
int mem_used_mb; /* Memory used in MB */
int mem_total_mb; /* Total memory in MB */
float load_1min; /* 1-minute load average */
float load_5min; /* 5-minute load average */
float load_15min; /* 15-minute load average */
/* CPU calculation state */
unsigned long long prev_idle;
unsigned long long prev_total;
} SystemStats;
static SystemStats sys_stats = {0};
/* Forward declarations */
static void panel_render_system_stats(Panel *panel, int x, int *width);
static int panel_calculate_stats_width(void);
/* ========== UTF-8 text helpers ========== */
/* Get text width using Xft (UTF-8 aware) */
static int panel_text_width(const char *text, int len)
{
if (text == NULL || dwn == NULL) return 0;
if (dwn->xft_font != NULL) {
XGlyphInfo extents;
XftTextExtentsUtf8(dwn->display, dwn->xft_font,
(const FcChar8 *)text, len, &extents);
return extents.xOff;
}
/* Fallback to legacy font */
if (dwn->font != NULL) {
return XTextWidth(dwn->font, text, len);
}
return 0;
}
/* Draw text using Xft (UTF-8 aware) */
static void panel_draw_text(Drawable d, int x, int y, const char *text,
int len, unsigned long color)
{
if (text == NULL || dwn == NULL || dwn->display == NULL) return;
/* Use Xft for UTF-8 support */
if (dwn->xft_font != NULL) {
XftDraw *xft_draw = XftDrawCreate(dwn->display, d,
DefaultVisual(dwn->display, dwn->screen),
dwn->colormap);
if (xft_draw != NULL) {
XftColor xft_color;
XRenderColor render_color;
render_color.red = ((color >> 16) & 0xFF) * 257;
render_color.green = ((color >> 8) & 0xFF) * 257;
render_color.blue = (color & 0xFF) * 257;
render_color.alpha = 0xFFFF;
XftColorAllocValue(dwn->display, DefaultVisual(dwn->display, dwn->screen),
dwn->colormap, &render_color, &xft_color);
XftDrawStringUtf8(xft_draw, &xft_color, dwn->xft_font,
x, y, (const FcChar8 *)text, len);
XftColorFree(dwn->display, DefaultVisual(dwn->display, dwn->screen),
dwn->colormap, &xft_color);
XftDrawDestroy(xft_draw);
return;
}
}
/* Fallback to legacy X11 text */
XSetForeground(dwn->display, dwn->gc, color);
XDrawString(dwn->display, d, dwn->gc, x, y, text, len);
}
/* Get text Y position for vertical centering */
static int panel_text_y(int panel_height)
{
if (dwn->xft_font != NULL) {
return (panel_height + dwn->xft_font->ascent) / 2;
}
if (dwn->font != NULL) {
return (panel_height + dwn->font->ascent - dwn->font->descent) / 2;
}
return panel_height / 2;
}
/* ========== Panel creation/destruction ========== */
Panel *panel_create(PanelPosition position)
{
if (dwn == NULL || dwn->display == NULL) {
return NULL;
}
Panel *panel = dwn_calloc(1, sizeof(Panel));
panel->position = position;
panel->width = dwn->screen_width;
panel->height = config_get_panel_height();
panel->x = 0;
panel->visible = true;
if (position == PANEL_TOP) {
panel->y = 0;
} else {
panel->y = dwn->screen_height - panel->height;
}
/* Create panel window */
XSetWindowAttributes swa;
swa.override_redirect = True;
swa.background_pixel = dwn->config->colors.panel_bg;
swa.event_mask = ExposureMask | ButtonPressMask | ButtonReleaseMask;
panel->window = XCreateWindow(dwn->display, dwn->root,
panel->x, panel->y,
panel->width, panel->height,
0,
CopyFromParent, InputOutput, CopyFromParent,
CWOverrideRedirect | CWBackPixel | CWEventMask,
&swa);
/* Create double buffer */
panel->buffer = XCreatePixmap(dwn->display, panel->window,
panel->width, panel->height,
DefaultDepth(dwn->display, dwn->screen));
/* Set EWMH strut to reserve space */
long strut[4] = { 0, 0, 0, 0 };
if (position == PANEL_TOP) {
strut[2] = panel->height;
} else {
strut[3] = panel->height;
}
XChangeProperty(dwn->display, panel->window, ewmh.NET_WM_STRUT,
XA_CARDINAL, 32, PropModeReplace,
(unsigned char *)strut, 4);
LOG_DEBUG("Created %s panel at y=%d",
position == PANEL_TOP ? "top" : "bottom", panel->y);
return panel;
}
void panel_destroy(Panel *panel)
{
if (panel == NULL) {
return;
}
if (panel->buffer != None) {
XFreePixmap(dwn->display, panel->buffer);
}
if (panel->window != None) {
XDestroyWindow(dwn->display, panel->window);
}
dwn_free(panel);
}
void panels_init(void)
{
if (dwn == NULL || dwn->config == NULL) {
return;
}
if (dwn->config->top_panel_enabled) {
dwn->top_panel = panel_create(PANEL_TOP);
if (dwn->top_panel != NULL) {
XMapRaised(dwn->display, dwn->top_panel->window);
}
}
if (dwn->config->bottom_panel_enabled) {
dwn->bottom_panel = panel_create(PANEL_BOTTOM);
if (dwn->bottom_panel != NULL) {
XMapRaised(dwn->display, dwn->bottom_panel->window);
}
}
/* Initial clock and stats update */
panel_update_clock();
panel_update_system_stats();
LOG_INFO("Panels initialized");
}
void panels_cleanup(void)
{
if (dwn->top_panel != NULL) {
panel_destroy(dwn->top_panel);
dwn->top_panel = NULL;
}
if (dwn->bottom_panel != NULL) {
panel_destroy(dwn->bottom_panel);
dwn->bottom_panel = NULL;
}
}
/* ========== Panel rendering ========== */
void panel_render(Panel *panel)
{
if (panel == NULL || !panel->visible) {
return;
}
Display *dpy = dwn->display;
const ColorScheme *colors = config_get_colors();
/* Clear buffer with background color */
XSetForeground(dpy, dwn->gc, colors->panel_bg);
XFillRectangle(dpy, panel->buffer, dwn->gc, 0, 0, panel->width, panel->height);
int x = PANEL_PADDING;
int width;
if (panel->position == PANEL_TOP) {
/* Top panel: workspaces, layout indicator, taskbar, systray */
/* Workspace indicators */
panel_render_workspaces(panel, x, &width);
x += width + WIDGET_SPACING;
/* Layout indicator */
panel_render_layout_indicator(panel, x, &width);
x += width + WIDGET_SPACING;
/* Taskbar (takes remaining space, but leave room for systray) */
panel_render_taskbar(panel, x, &width);
/* System tray (right side) - WiFi, Audio, etc. */
int systray_actual_width = systray_get_width();
int systray_x = panel->width - systray_actual_width - PANEL_PADDING;
systray_render(panel, systray_x, &width);
/* AI status (left of systray) */
if (dwn->ai_enabled) {
int ai_x = systray_x - 60;
panel_render_ai_status(panel, ai_x, &width);
}
} else {
/* Bottom panel: date (left), news ticker (center), system stats + clock (right) */
/* Date (left side) */
int date_width = 0;
if (dwn->xft_font != NULL || dwn->font != NULL) {
int text_y = panel_text_y(panel->height);
panel_draw_text(panel->buffer, x, text_y, date_buffer,
strlen(date_buffer), colors->panel_fg);
date_width = panel_text_width(date_buffer, strlen(date_buffer));
}
/* Calculate positions from right edge */
int clock_width = panel_text_width(clock_buffer, strlen(clock_buffer));
int stats_width = panel_calculate_stats_width();
/* Clock at rightmost position */
int clock_x = panel->width - clock_width - PANEL_PADDING;
panel_render_clock(panel, clock_x, &width);
/* Stats immediately left of clock */
int stats_x = clock_x - stats_width - WIDGET_SPACING;
panel_render_system_stats(panel, stats_x, &width);
/* News ticker between date and stats */
int news_start = PANEL_PADDING + date_width + WIDGET_SPACING * 2;
int news_max_width = stats_x - news_start - WIDGET_SPACING;
if (news_max_width > 100) { /* Only show if there's reasonable space */
int news_width = 0;
news_render(panel, news_start, news_max_width, &news_width);
}
}
/* Copy buffer to window */
XCopyArea(dpy, panel->buffer, panel->window, dwn->gc,
0, 0, panel->width, panel->height, 0, 0);
XFlush(dpy);
}
void panel_render_all(void)
{
if (dwn->top_panel != NULL) {
panel_render(dwn->top_panel);
}
if (dwn->bottom_panel != NULL) {
panel_render(dwn->bottom_panel);
}
}
void panel_render_workspaces(Panel *panel, int x, int *width)
{
if (panel == NULL || width == NULL) {
return;
}
Display *dpy = dwn->display;
const ColorScheme *colors = config_get_colors();
int text_y = panel_text_y(panel->height);
*width = 0;
for (int i = 0; i < MAX_WORKSPACES; i++) {
bool active = (i == dwn->current_workspace);
bool has_clients = !workspace_is_empty(i);
/* Background */
unsigned long bg = active ? colors->workspace_active : colors->panel_bg;
XSetForeground(dpy, dwn->gc, bg);
XFillRectangle(dpy, panel->buffer, dwn->gc,
x + i * WORKSPACE_WIDTH, 2,
WORKSPACE_WIDTH - 2, panel->height - 4);
/* Workspace number */
char num[4];
snprintf(num, sizeof(num), "%d", i + 1);
unsigned long fg = active ? colors->panel_bg :
(has_clients ? colors->panel_fg : colors->workspace_inactive);
int text_x = x + i * WORKSPACE_WIDTH + (WORKSPACE_WIDTH - panel_text_width(num, strlen(num))) / 2;
panel_draw_text(panel->buffer, text_x, text_y, num, strlen(num), fg);
}
*width = MAX_WORKSPACES * WORKSPACE_WIDTH;
}
void panel_render_taskbar(Panel *panel, int x, int *width)
{
if (panel == NULL || width == NULL) {
return;
}
Display *dpy = dwn->display;
const ColorScheme *colors = config_get_colors();
int text_y = panel_text_y(panel->height);
int current_x = x;
int available_width = panel->width - x - 100 - PANEL_PADDING;
int item_count = 0;
/* Count visible clients */
for (Client *c = dwn->client_list; c != NULL; c = c->next) {
if (c->workspace == (unsigned int)dwn->current_workspace && !client_is_minimized(c)) {
item_count++;
}
}
if (item_count == 0) {
*width = 0;
return;
}
int item_width = available_width / item_count;
if (item_width > TASKBAR_ITEM_WIDTH) {
item_width = TASKBAR_ITEM_WIDTH;
}
/* Render each client */
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)) {
continue;
}
bool focused = (ws != NULL && ws->focused == c);
/* Background */
unsigned long bg = focused ? colors->workspace_active : colors->panel_bg;
XSetForeground(dpy, dwn->gc, bg);
XFillRectangle(dpy, panel->buffer, dwn->gc,
current_x, 2, item_width - 2, panel->height - 4);
/* Title - leave room for "..." (4 bytes including null) */
char title[64];
strncpy(title, c->title, sizeof(title) - 4); /* Leave room for "..." */
title[sizeof(title) - 4] = '\0';
/* Truncate UTF-8 aware if necessary */
int max_text_width = item_width - 8;
bool truncated = false;
while (panel_text_width(title, strlen(title)) > max_text_width && strlen(title) > 3) {
size_t len = strlen(title);
/* Move back to find UTF-8 character boundary */
size_t cut = len - 1;
while (cut > 0 && (title[cut] & 0xC0) == 0x80) {
cut--; /* Skip continuation bytes */
}
if (cut > 0) cut--;
while (cut > 0 && (title[cut] & 0xC0) == 0x80) {
cut--;
}
/* Ensure we have room for "..." */
if (cut > sizeof(title) - 4) {
cut = sizeof(title) - 4;
}
title[cut] = '\0';
truncated = true;
}
if (truncated) {
strncat(title, "...", sizeof(title) - strlen(title) - 1);
}
unsigned long fg = focused ? colors->panel_bg : colors->panel_fg;
panel_draw_text(panel->buffer, current_x + 4, text_y, title, strlen(title), fg);
current_x += item_width;
}
*width = current_x - x;
}
void panel_render_clock(Panel *panel, int x, int *width)
{
if (panel == NULL || width == NULL) {
return;
}
if (dwn->xft_font == NULL && dwn->font == NULL) {
return;
}
const ColorScheme *colors = config_get_colors();
int text_y = panel_text_y(panel->height);
panel_draw_text(panel->buffer, x, text_y, clock_buffer,
strlen(clock_buffer), colors->panel_fg);
*width = panel_text_width(clock_buffer, strlen(clock_buffer));
}
void panel_render_systray(Panel *panel, int x, int *width)
{
/* System tray placeholder - actual implementation requires
handling _NET_SYSTEM_TRAY protocol */
(void)panel;
(void)x;
*width = 0;
}
void panel_render_layout_indicator(Panel *panel, int x, int *width)
{
if (panel == NULL || width == NULL) {
return;
}
if (dwn->xft_font == NULL && dwn->font == NULL) {
return;
}
const ColorScheme *colors = config_get_colors();
int text_y = panel_text_y(panel->height);
Workspace *ws = workspace_get_current();
const char *symbol = layout_get_symbol(ws != NULL ? ws->layout : LAYOUT_TILING);
panel_draw_text(panel->buffer, x, text_y, symbol, strlen(symbol),
colors->workspace_active);
*width = panel_text_width(symbol, strlen(symbol));
}
void panel_render_ai_status(Panel *panel, int x, int *width)
{
if (panel == NULL || width == NULL) {
return;
}
if (dwn->xft_font == NULL && dwn->font == NULL) {
return;
}
const ColorScheme *colors = config_get_colors();
int text_y = panel_text_y(panel->height);
const char *status = dwn->ai_enabled ? "[AI]" : "";
panel_draw_text(panel->buffer, x, text_y, status, strlen(status),
colors->workspace_active);
*width = panel_text_width(status, strlen(status));
}
/* ========== Panel interaction ========== */
void panel_handle_click(Panel *panel, int x, int y, int button)
{
if (panel == NULL) {
return;
}
if (panel->position == PANEL_TOP) {
/* Check systray click first (right side) */
int systray_actual_width = systray_get_width();
int systray_start = panel->width - systray_actual_width - PANEL_PADDING;
if (x >= systray_start) {
systray_handle_click(x, y, button);
return;
}
/* Check workspace click */
int ws = panel_hit_test_workspace(panel, x, y);
if (ws >= 0 && ws < MAX_WORKSPACES) {
if (button == 1) { /* Left click */
workspace_switch(ws);
}
return;
}
/* Check taskbar click */
Client *c = panel_hit_test_taskbar(panel, x, y);
if (c != NULL) {
if (button == 1) {
client_focus(c);
} else if (button == 3) { /* Right click */
client_close(c);
}
return;
}
} else if (panel->position == PANEL_BOTTOM) {
/* Bottom panel - click on news opens article in browser */
if (button == 1) {
news_handle_click(x, y);
}
}
}
int panel_hit_test_workspace(Panel *panel, int x, int y)
{
(void)y;
if (panel == NULL || panel->position != PANEL_TOP) {
return -1;
}
if (x < PANEL_PADDING || x >= PANEL_PADDING + MAX_WORKSPACES * WORKSPACE_WIDTH) {
return -1;
}
return (x - PANEL_PADDING) / WORKSPACE_WIDTH;
}
Client *panel_hit_test_taskbar(Panel *panel, int x, int y)
{
(void)y;
if (panel == NULL || panel->position != PANEL_TOP) {
return NULL;
}
int taskbar_start = PANEL_PADDING + MAX_WORKSPACES * WORKSPACE_WIDTH + WIDGET_SPACING + 50;
if (x < taskbar_start) {
return NULL;
}
int available_width = panel->width - taskbar_start - 100 - PANEL_PADDING;
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)) {
item_count++;
}
}
if (item_count == 0) {
return NULL;
}
int item_width = available_width / item_count;
if (item_width > TASKBAR_ITEM_WIDTH) {
item_width = TASKBAR_ITEM_WIDTH;
}
int index = (x - taskbar_start) / item_width;
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 (i == index) {
return c;
}
i++;
}
}
return NULL;
}
/* ========== Panel visibility ========== */
void panel_show(Panel *panel)
{
if (panel == NULL) {
return;
}
panel->visible = true;
XMapRaised(dwn->display, panel->window);
panel_render(panel);
}
void panel_hide(Panel *panel)
{
if (panel == NULL) {
return;
}
panel->visible = false;
XUnmapWindow(dwn->display, panel->window);
}
void panel_toggle(Panel *panel)
{
if (panel == NULL) {
return;
}
if (panel->visible) {
panel_hide(panel);
} else {
panel_show(panel);
}
}
/* ========== Clock updates ========== */
void panel_update_clock(void)
{
time_t now = time(NULL);
struct tm *tm_info = localtime(&now);
strftime(clock_buffer, sizeof(clock_buffer), CLOCK_FORMAT, tm_info);
strftime(date_buffer, sizeof(date_buffer), DATE_FORMAT, tm_info);
}
/* ========== System stats ========== */
void panel_update_system_stats(void)
{
FILE *fp;
char line[256];
/* Read CPU stats from /proc/stat */
fp = fopen("/proc/stat", "r");
if (fp != NULL) {
if (fgets(line, sizeof(line), fp) != NULL) {
unsigned long long user, nice, system, idle, iowait, irq, softirq;
if (sscanf(line, "cpu %llu %llu %llu %llu %llu %llu %llu",
&user, &nice, &system, &idle, &iowait, &irq, &softirq) >= 4) {
unsigned long long total = user + nice + system + idle + iowait + irq + softirq;
unsigned long long idle_time = idle + iowait;
if (sys_stats.prev_total > 0) {
unsigned long long total_diff = total - sys_stats.prev_total;
unsigned long long idle_diff = idle_time - sys_stats.prev_idle;
if (total_diff > 0) {
sys_stats.cpu_percent = (int)(100 * (total_diff - idle_diff) / total_diff);
}
}
sys_stats.prev_total = total;
sys_stats.prev_idle = idle_time;
}
}
fclose(fp);
}
/* Read memory stats from /proc/meminfo */
fp = fopen("/proc/meminfo", "r");
if (fp != NULL) {
unsigned long mem_total = 0, mem_free = 0, buffers = 0, cached = 0;
while (fgets(line, sizeof(line), fp) != NULL) {
if (strncmp(line, "MemTotal:", 9) == 0) {
sscanf(line + 9, " %lu", &mem_total);
} else if (strncmp(line, "MemFree:", 8) == 0) {
sscanf(line + 8, " %lu", &mem_free);
} else if (strncmp(line, "Buffers:", 8) == 0) {
sscanf(line + 8, " %lu", &buffers);
} else if (strncmp(line, "Cached:", 7) == 0) {
sscanf(line + 7, " %lu", &cached);
break; /* Got all we need */
}
}
fclose(fp);
if (mem_total > 0) {
unsigned long used = mem_total - mem_free - buffers - cached;
sys_stats.mem_total_mb = (int)(mem_total / 1024);
sys_stats.mem_used_mb = (int)(used / 1024);
sys_stats.mem_percent = (int)(100 * used / mem_total);
}
}
/* Read load average from /proc/loadavg */
fp = fopen("/proc/loadavg", "r");
if (fp != NULL) {
if (fscanf(fp, "%f %f %f",
&sys_stats.load_1min,
&sys_stats.load_5min,
&sys_stats.load_15min) != 3) {
sys_stats.load_1min = 0;
sys_stats.load_5min = 0;
sys_stats.load_15min = 0;
}
fclose(fp);
}
}
/* Calculate stats width without rendering */
static int panel_calculate_stats_width(void)
{
if (dwn->xft_font == NULL && dwn->font == NULL) return 0;
char buf[256];
int total = 0;
/* CPU */
int len = snprintf(buf, sizeof(buf), "CPU:%2d%%", sys_stats.cpu_percent);
total += panel_text_width(buf, len) + WIDGET_SPACING;
/* Memory */
if (sys_stats.mem_total_mb >= 1024) {
len = snprintf(buf, sizeof(buf), "MEM:%.1fG/%dG",
sys_stats.mem_used_mb / 1024.0f, sys_stats.mem_total_mb / 1024);
} else {
len = snprintf(buf, sizeof(buf), "MEM:%dM/%dM",
sys_stats.mem_used_mb, sys_stats.mem_total_mb);
}
total += panel_text_width(buf, len) + WIDGET_SPACING;
/* Load */
len = snprintf(buf, sizeof(buf), "Load:%.2f %.2f %.2f",
sys_stats.load_1min, sys_stats.load_5min, sys_stats.load_15min);
total += panel_text_width(buf, len) + WIDGET_SPACING;
/* Battery - use thread-safe snapshot */
BatteryState bat_snap = systray_get_battery_snapshot();
if (bat_snap.present) {
const char *bat_icon = bat_snap.charging ? "[+]" : "[=]";
len = snprintf(buf, sizeof(buf), "%s%d%%", bat_icon, bat_snap.percentage);
total += panel_text_width(buf, len) + WIDGET_SPACING;
}
return total;
}
static void panel_render_system_stats(Panel *panel, int x, int *width)
{
if (panel == NULL || width == NULL) {
*width = 0;
return;
}
if (dwn->xft_font == NULL && dwn->font == NULL) {
*width = 0;
return;
}
const ColorScheme *colors = config_get_colors();
int text_y = panel_text_y(panel->height);
int current_x = x;
/* Format: "CPU: 25% | MEM: 4.2G/16G | Load: 1.23 0.98 0.76 | BAT: 85%" */
char stats_buf[256];
int len;
/* CPU with color coding */
unsigned long cpu_color = colors->panel_fg;
if (sys_stats.cpu_percent >= 90) {
cpu_color = colors->workspace_urgent; /* Red for high CPU */
} else if (sys_stats.cpu_percent >= 70) {
cpu_color = 0xFFA500; /* Orange for medium-high */
}
len = snprintf(stats_buf, sizeof(stats_buf), "CPU:%2d%%", sys_stats.cpu_percent);
panel_draw_text(panel->buffer, current_x, text_y, stats_buf, len, cpu_color);
current_x += panel_text_width(stats_buf, len) + WIDGET_SPACING;
/* Memory */
unsigned long mem_color = colors->panel_fg;
if (sys_stats.mem_percent >= 90) {
mem_color = colors->workspace_urgent;
} else if (sys_stats.mem_percent >= 75) {
mem_color = 0xFFA500;
}
if (sys_stats.mem_total_mb >= 1024) {
len = snprintf(stats_buf, sizeof(stats_buf), "MEM:%.1fG/%dG",
sys_stats.mem_used_mb / 1024.0f, sys_stats.mem_total_mb / 1024);
} else {
len = snprintf(stats_buf, sizeof(stats_buf), "MEM:%dM/%dM",
sys_stats.mem_used_mb, sys_stats.mem_total_mb);
}
panel_draw_text(panel->buffer, current_x, text_y, stats_buf, len, mem_color);
current_x += panel_text_width(stats_buf, len) + WIDGET_SPACING;
/* Load average */
unsigned long load_color = colors->panel_fg;
if (sys_stats.load_1min >= 4.0f) {
load_color = colors->workspace_urgent;
} else if (sys_stats.load_1min >= 2.0f) {
load_color = 0xFFA500;
}
len = snprintf(stats_buf, sizeof(stats_buf), "Load:%.2f %.2f %.2f",
sys_stats.load_1min, sys_stats.load_5min, sys_stats.load_15min);
panel_draw_text(panel->buffer, current_x, text_y, stats_buf, len, load_color);
current_x += panel_text_width(stats_buf, len) + WIDGET_SPACING;
/* Battery (if present) - use thread-safe snapshot */
BatteryState bat_snap = systray_get_battery_snapshot();
if (bat_snap.present) {
unsigned long bat_color = colors->panel_fg;
if (bat_snap.percentage <= 20 && !bat_snap.charging) {
bat_color = colors->workspace_urgent; /* Red for low */
} else if (bat_snap.percentage <= 40 && !bat_snap.charging) {
bat_color = 0xFFA500; /* Orange for medium-low */
} else if (bat_snap.charging) {
bat_color = colors->workspace_active; /* Blue for charging */
}
const char *bat_icon = bat_snap.charging ? "[+]" : "[=]";
len = snprintf(stats_buf, sizeof(stats_buf), "%s%d%%", bat_icon, bat_snap.percentage);
panel_draw_text(panel->buffer, current_x, text_y, stats_buf, len, bat_color);
current_x += panel_text_width(stats_buf, len) + WIDGET_SPACING;
}
*width = current_x - x;
}
/* ========== System tray ========== */
void panel_init_systray(void)
{
/* System tray initialization - requires _NET_SYSTEM_TRAY protocol */
LOG_DEBUG("System tray initialization (placeholder)");
}
void panel_add_systray_icon(Window icon)
{
(void)icon;
LOG_DEBUG("Add systray icon (placeholder)");
}
void panel_remove_systray_icon(Window icon)
{
(void)icon;
LOG_DEBUG("Remove systray icon (placeholder)");
}
+1105
View File
File diff suppressed because it is too large Load Diff
+709
View File
@@ -0,0 +1,709 @@
/*
* DWN - Desktop Window Manager
* Utility functions implementation
*/
#include "util.h"
#include "dwn.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <time.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <errno.h>
#include <pwd.h>
#include <pthread.h>
#include <stdatomic.h>
#include <fcntl.h>
/* ========== Async Logging Configuration ========== */
#define LOG_RING_SIZE 256 /* Number of log entries in ring buffer */
#define LOG_MSG_MAX_LEN 512 /* Max length of a single log message */
#define LOG_MAX_FILE_SIZE (5 * 1024 * 1024) /* 5 MB max log file size */
#define LOG_FLUSH_INTERVAL_MS 100 /* Flush to disk every 100ms */
/* Log entry in ring buffer */
typedef struct {
char message[LOG_MSG_MAX_LEN];
LogLevel level;
_Atomic int ready; /* 0 = empty, 1 = ready to write */
} LogEntry;
/* Static state for async logging */
static LogEntry log_ring[LOG_RING_SIZE];
static _Atomic size_t log_write_idx = 0; /* Next slot to write to */
static _Atomic size_t log_read_idx = 0; /* Next slot to read from */
static _Atomic int log_running = 0; /* Log thread running flag */
static pthread_t log_thread;
static int log_fd = -1; /* File descriptor for log file */
static char *log_path = NULL; /* Path to log file */
static LogLevel min_level = LOG_INFO;
static _Atomic size_t log_file_size = 0; /* Current log file size */
static const char *level_names[] = {
"DEBUG",
"INFO",
"WARN",
"ERROR"
};
static const char *level_colors[] = {
"\033[36m", /* Cyan for DEBUG */
"\033[32m", /* Green for INFO */
"\033[33m", /* Yellow for WARN */
"\033[31m" /* Red for ERROR */
};
/* Forward declarations for internal log functions */
static void log_rotate_if_needed(void);
static void *log_writer_thread(void *arg);
/* ========== Async Logging Implementation ========== */
/* Rotate log file if it exceeds max size */
static void log_rotate_if_needed(void)
{
if (log_fd < 0 || log_path == NULL) {
return;
}
size_t current_size = atomic_load(&log_file_size);
if (current_size < LOG_MAX_FILE_SIZE) {
return;
}
/* Close current file */
close(log_fd);
log_fd = -1;
/* Create backup filename */
size_t path_len = strlen(log_path);
char *backup_path = malloc(path_len + 8);
if (backup_path != NULL) {
snprintf(backup_path, path_len + 8, "%s.old", log_path);
/* Remove old backup if exists, rename current to backup */
unlink(backup_path);
rename(log_path, backup_path);
free(backup_path);
}
/* Reopen fresh log file */
log_fd = open(log_path, O_WRONLY | O_CREAT | O_APPEND | O_CLOEXEC, 0644);
atomic_store(&log_file_size, 0);
}
/* Background thread that writes log entries to file */
static void *log_writer_thread(void *arg)
{
(void)arg;
while (atomic_load(&log_running)) {
size_t read_idx = atomic_load(&log_read_idx);
size_t write_idx = atomic_load(&log_write_idx);
/* Process all available entries */
while (read_idx != write_idx) {
size_t idx = read_idx % LOG_RING_SIZE;
LogEntry *entry = &log_ring[idx];
/* Wait for entry to be ready (spin briefly) */
int attempts = 0;
while (!atomic_load(&entry->ready) && attempts < 100) {
attempts++;
/* Brief yield */
struct timespec ts = {0, 1000}; /* 1 microsecond */
nanosleep(&ts, NULL);
}
if (atomic_load(&entry->ready)) {
/* Write to file if open */
if (log_fd >= 0) {
log_rotate_if_needed();
if (log_fd >= 0) {
ssize_t written = write(log_fd, entry->message, strlen(entry->message));
if (written > 0) {
atomic_fetch_add(&log_file_size, (size_t)written);
}
}
}
/* Mark entry as consumed */
atomic_store(&entry->ready, 0);
}
read_idx++;
atomic_store(&log_read_idx, read_idx);
}
/* Sleep before next check */
struct timespec ts = {0, LOG_FLUSH_INTERVAL_MS * 1000000L};
nanosleep(&ts, NULL);
}
return NULL;
}
void log_init(const char *path)
{
/* Initialize ring buffer */
memset(log_ring, 0, sizeof(log_ring));
atomic_store(&log_write_idx, 0);
atomic_store(&log_read_idx, 0);
if (path != NULL) {
char *expanded = expand_path(path);
if (expanded != NULL) {
/* Ensure directory exists */
char *dir = strdup(expanded);
if (dir != NULL) {
char *last_slash = strrchr(dir, '/');
if (last_slash != NULL) {
*last_slash = '\0';
/* Create directory recursively using mkdir */
struct stat st;
if (stat(dir, &st) != 0) {
/* Directory doesn't exist, try to create it */
char cmd[512];
snprintf(cmd, sizeof(cmd), "mkdir -p '%s' 2>/dev/null", dir);
int ret = system(cmd);
(void)ret; /* Ignore result - file open will fail if dir creation fails */
}
}
free(dir);
}
/* Open log file with O_APPEND for atomic writes */
log_fd = open(expanded, O_WRONLY | O_CREAT | O_APPEND | O_CLOEXEC, 0644);
if (log_fd < 0) {
fprintf(stderr, "Warning: Could not open log file: %s\n", expanded);
} else {
/* Get current file size */
struct stat st;
if (fstat(log_fd, &st) == 0) {
atomic_store(&log_file_size, (size_t)st.st_size);
}
}
log_path = expanded; /* Keep for rotation */
}
}
/* Start background writer thread */
atomic_store(&log_running, 1);
if (pthread_create(&log_thread, NULL, log_writer_thread, NULL) != 0) {
fprintf(stderr, "Warning: Could not create log writer thread\n");
atomic_store(&log_running, 0);
}
}
void log_close(void)
{
/* Signal thread to stop */
if (!atomic_load(&log_running)) {
goto cleanup;
}
atomic_store(&log_running, 0);
/* Wait for thread to finish - give it time to flush */
pthread_join(log_thread, NULL);
cleanup:
/* Close file */
if (log_fd >= 0) {
close(log_fd);
log_fd = -1;
}
/* Free path */
if (log_path != NULL) {
free(log_path);
log_path = NULL;
}
}
void log_set_level(LogLevel level)
{
min_level = level;
}
void log_flush(void)
{
/* Force flush all pending log entries synchronously */
if (!atomic_load(&log_running) || log_fd < 0) {
return;
}
size_t read_idx = atomic_load(&log_read_idx);
size_t write_idx = atomic_load(&log_write_idx);
while (read_idx != write_idx) {
size_t idx = read_idx % LOG_RING_SIZE;
LogEntry *entry = &log_ring[idx];
if (atomic_load(&entry->ready)) {
ssize_t written = write(log_fd, entry->message, strlen(entry->message));
if (written > 0) {
atomic_fetch_add(&log_file_size, (size_t)written);
}
atomic_store(&entry->ready, 0);
}
read_idx++;
atomic_store(&log_read_idx, read_idx);
}
/* Sync to disk */
fsync(log_fd);
}
void log_msg(LogLevel level, const char *fmt, ...)
{
if (level < min_level) {
return;
}
/* Get timestamp */
time_t now = time(NULL);
struct tm tm_info;
localtime_r(&now, &tm_info); /* Thread-safe version */
char time_buf[32];
strftime(time_buf, sizeof(time_buf), "%Y-%m-%d %H:%M:%S", &tm_info);
/* Format the log message */
char msg_buf[LOG_MSG_MAX_LEN - 64]; /* Leave room for prefix */
va_list args;
va_start(args, fmt);
vsnprintf(msg_buf, sizeof(msg_buf), fmt, args);
va_end(args);
/* Print to stderr (always, for immediate visibility) */
fprintf(stderr, "%s[%s] %s: %s%s\n",
level_colors[level], time_buf, level_names[level], "\033[0m", msg_buf);
/* Add to ring buffer for async file write (non-blocking) */
if (atomic_load(&log_running)) {
size_t write_idx = atomic_fetch_add(&log_write_idx, 1);
size_t idx = write_idx % LOG_RING_SIZE;
LogEntry *entry = &log_ring[idx];
/* Check if slot is available (not overwriting unread entry) */
/* If buffer is full, we drop the message rather than block */
if (!atomic_load(&entry->ready)) {
entry->level = level;
snprintf(entry->message, sizeof(entry->message),
"[%s] %s: %s\n", time_buf, level_names[level], msg_buf);
atomic_store(&entry->ready, 1);
}
/* If entry->ready is true, buffer is full - message is dropped (non-blocking) */
}
}
/* ========== Memory allocation ========== */
void *dwn_malloc(size_t size)
{
void *ptr = malloc(size);
if (ptr == NULL && size > 0) {
LOG_ERROR("Memory allocation failed for %zu bytes", size);
exit(EXIT_FAILURE);
}
return ptr;
}
void *dwn_calloc(size_t nmemb, size_t size)
{
void *ptr = calloc(nmemb, size);
if (ptr == NULL && nmemb > 0 && size > 0) {
LOG_ERROR("Memory allocation failed for %zu elements", nmemb);
exit(EXIT_FAILURE);
}
return ptr;
}
void *dwn_realloc(void *ptr, size_t size)
{
void *new_ptr = realloc(ptr, size);
if (new_ptr == NULL && size > 0) {
LOG_ERROR("Memory reallocation failed for %zu bytes", size);
exit(EXIT_FAILURE);
}
return new_ptr;
}
char *dwn_strdup(const char *s)
{
if (s == NULL) {
return NULL;
}
char *dup = strdup(s);
if (dup == NULL) {
LOG_ERROR("String duplication failed");
exit(EXIT_FAILURE);
}
return dup;
}
void dwn_free(void *ptr)
{
free(ptr);
}
void secure_wipe(void *ptr, size_t size)
{
if (ptr == NULL || size == 0) {
return;
}
#if defined(__GLIBC__) && (__GLIBC__ >= 2) && (__GLIBC_MINOR__ >= 25)
/* Use explicit_bzero if available (glibc 2.25+) */
explicit_bzero(ptr, size);
#else
/* Fallback: Use volatile to prevent compiler optimization */
volatile unsigned char *p = (volatile unsigned char *)ptr;
while (size--) {
*p++ = 0;
}
#endif
}
/* ========== String utilities ========== */
char *str_trim(char *str)
{
if (str == NULL) {
return NULL;
}
/* Trim leading whitespace */
while (isspace((unsigned char)*str)) {
str++;
}
if (*str == '\0') {
return str;
}
/* Trim trailing whitespace */
char *end = str + strlen(str) - 1;
while (end > str && isspace((unsigned char)*end)) {
end--;
}
end[1] = '\0';
return str;
}
bool str_starts_with(const char *str, const char *prefix)
{
if (str == NULL || prefix == NULL) {
return false;
}
return strncmp(str, prefix, strlen(prefix)) == 0;
}
bool str_ends_with(const char *str, const char *suffix)
{
if (str == NULL || suffix == NULL) {
return false;
}
size_t str_len = strlen(str);
size_t suffix_len = strlen(suffix);
if (suffix_len > str_len) {
return false;
}
return strcmp(str + str_len - suffix_len, suffix) == 0;
}
int str_split(char *str, char delim, char **parts, int max_parts)
{
if (str == NULL || parts == NULL || max_parts <= 0) {
return 0;
}
int count = 0;
char *start = str;
while (*str && count < max_parts) {
if (*str == delim) {
*str = '\0';
parts[count++] = start;
start = str + 1;
}
str++;
}
if (count < max_parts && *start) {
parts[count++] = start;
}
return count;
}
char *shell_escape(const char *str)
{
if (str == NULL) {
return dwn_strdup("");
}
/* Count single quotes to determine buffer size */
size_t quotes = 0;
for (const char *p = str; *p; p++) {
if (*p == '\'') quotes++;
}
/* Each single quote becomes '\'' (4 chars), plus 2 for surrounding quotes */
size_t len = strlen(str) + quotes * 3 + 3;
char *escaped = dwn_malloc(len);
char *dst = escaped;
*dst++ = '\'';
for (const char *src = str; *src; src++) {
if (*src == '\'') {
/* Close quote, add escaped quote, reopen quote: '\'' */
*dst++ = '\'';
*dst++ = '\\';
*dst++ = '\'';
*dst++ = '\'';
} else {
*dst++ = *src;
}
}
*dst++ = '\'';
*dst = '\0';
return escaped;
}
/* ========== File utilities ========== */
bool file_exists(const char *path)
{
if (path == NULL) {
return false;
}
struct stat st;
return stat(path, &st) == 0;
}
char *file_read_all(const char *path)
{
FILE *f = fopen(path, "r");
if (f == NULL) {
return NULL;
}
fseek(f, 0, SEEK_END);
long size = ftell(f);
fseek(f, 0, SEEK_SET);
char *content = dwn_malloc(size + 1);
size_t read = fread(content, 1, size, f);
content[read] = '\0';
fclose(f);
return content;
}
bool file_write_all(const char *path, const char *content)
{
FILE *f = fopen(path, "w");
if (f == NULL) {
return false;
}
size_t len = strlen(content);
size_t written = fwrite(content, 1, len, f);
fclose(f);
return written == len;
}
char *expand_path(const char *path)
{
if (path == NULL) {
return NULL;
}
if (path[0] == '~') {
const char *home = getenv("HOME");
if (home == NULL) {
struct passwd *pw = getpwuid(getuid());
if (pw != NULL) {
home = pw->pw_dir;
}
}
if (home != NULL) {
size_t len = strlen(home) + strlen(path);
char *expanded = dwn_malloc(len);
snprintf(expanded, len, "%s%s", home, path + 1);
return expanded;
}
}
return dwn_strdup(path);
}
/* ========== Color utilities ========== */
unsigned long parse_color(const char *color_str)
{
if (color_str == NULL || dwn == NULL || dwn->display == NULL) {
return 0;
}
XColor color, exact;
if (XAllocNamedColor(dwn->display, dwn->colormap, color_str, &color, &exact)) {
return color.pixel;
}
/* Try parsing as hex */
if (color_str[0] == '#') {
unsigned int r, g, b;
if (sscanf(color_str + 1, "%02x%02x%02x", &r, &g, &b) == 3) {
color.red = r << 8;
color.green = g << 8;
color.blue = b << 8;
color.flags = DoRed | DoGreen | DoBlue;
if (XAllocColor(dwn->display, dwn->colormap, &color)) {
return color.pixel;
}
}
}
LOG_WARN("Could not parse color: %s", color_str);
return 0;
}
void color_to_rgb(unsigned long color, int *r, int *g, int *b)
{
XColor xc;
xc.pixel = color;
XQueryColor(dwn->display, dwn->colormap, &xc);
*r = xc.red >> 8;
*g = xc.green >> 8;
*b = xc.blue >> 8;
}
/* ========== Time utilities ========== */
long get_time_ms(void)
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (long)(ts.tv_sec * 1000 + ts.tv_nsec / 1000000);
}
void sleep_ms(int ms)
{
struct timespec ts;
ts.tv_sec = ms / 1000;
ts.tv_nsec = (ms % 1000) * 1000000L;
nanosleep(&ts, NULL);
}
/* ========== Process utilities ========== */
int spawn(const char *cmd)
{
if (cmd == NULL) {
return -1;
}
LOG_DEBUG("Spawning: %s", cmd);
pid_t pid = fork();
if (pid == 0) {
/* Child process */
setsid();
execl("/bin/sh", "sh", "-c", cmd, NULL);
_exit(EXIT_FAILURE);
} else if (pid < 0) {
LOG_ERROR("Fork failed: %s", strerror(errno));
return -1;
}
/* Wait for child */
int status;
waitpid(pid, &status, 0);
return WEXITSTATUS(status);
}
int spawn_async(const char *cmd)
{
if (cmd == NULL) {
return -1;
}
LOG_DEBUG("Spawning async: %s", cmd);
pid_t pid = fork();
if (pid == 0) {
/* Child process */
setsid();
/* Double fork to avoid zombies */
pid_t pid2 = fork();
if (pid2 == 0) {
execl("/bin/sh", "sh", "-c", cmd, NULL);
_exit(EXIT_FAILURE);
}
_exit(EXIT_SUCCESS);
} else if (pid < 0) {
LOG_ERROR("Fork failed: %s", strerror(errno));
return -1;
}
/* Wait for first child (which exits immediately) */
int status;
waitpid(pid, &status, 0);
return 0;
}
char *spawn_capture(const char *cmd)
{
if (cmd == NULL) {
return NULL;
}
LOG_DEBUG("Spawning capture: %s", cmd);
FILE *fp = popen(cmd, "r");
if (fp == NULL) {
LOG_ERROR("popen failed: %s", strerror(errno));
return NULL;
}
/* Read output */
size_t buf_size = 1024;
size_t len = 0;
char *output = dwn_malloc(buf_size);
output[0] = '\0';
char line[256];
while (fgets(line, sizeof(line), fp) != NULL) {
size_t line_len = strlen(line);
if (len + line_len + 1 > buf_size) {
buf_size *= 2;
output = dwn_realloc(output, buf_size);
}
/* Use memcpy with explicit bounds instead of strcpy */
memcpy(output + len, line, line_len + 1);
len += line_len;
}
int status = pclose(fp);
if (status != 0) {
LOG_DEBUG("Command exited with status %d", status);
}
/* Trim trailing newline */
if (len > 0 && output[len - 1] == '\n') {
output[len - 1] = '\0';
}
return output;
}
+483
View File
@@ -0,0 +1,483 @@
/*
* DWN - Desktop Window Manager
* Workspace management implementation
*/
#include "workspace.h"
#include "client.h"
#include "layout.h"
#include "atoms.h"
#include "util.h"
#include "config.h"
#include <string.h>
#include <stdio.h>
/* ========== Initialization ========== */
void workspace_init(void)
{
if (dwn == NULL) {
return;
}
LOG_DEBUG("Initializing %d workspaces", MAX_WORKSPACES);
for (int i = 0; i < MAX_WORKSPACES; i++) {
Workspace *ws = &dwn->workspaces[i];
ws->clients = NULL;
ws->focused = NULL;
ws->layout = (dwn->config != NULL) ?
dwn->config->default_layout : LAYOUT_TILING;
ws->master_ratio = (dwn->config != NULL) ?
dwn->config->default_master_ratio : 0.55f;
ws->master_count = (dwn->config != NULL) ?
dwn->config->default_master_count : 1;
/* Default names: "1", "2", ..., "9" */
snprintf(ws->name, sizeof(ws->name), "%d", i + 1);
}
dwn->current_workspace = 0;
}
void workspace_cleanup(void)
{
/* Clients are cleaned up separately */
}
/* ========== Workspace access ========== */
Workspace *workspace_get(int index)
{
if (dwn == NULL || index < 0 || index >= MAX_WORKSPACES) {
return NULL;
}
return &dwn->workspaces[index];
}
Workspace *workspace_get_current(void)
{
return workspace_get(dwn->current_workspace);
}
int workspace_get_current_index(void)
{
return dwn != NULL ? dwn->current_workspace : 0;
}
/* ========== Workspace switching ========== */
void workspace_switch(int index)
{
if (dwn == NULL || index < 0 || index >= MAX_WORKSPACES) {
return;
}
if (index == dwn->current_workspace) {
return;
}
LOG_DEBUG("Switching from workspace %d to %d",
dwn->current_workspace + 1, index + 1);
/* Grab server to batch all X operations - prevents flickering and improves speed */
XGrabServer(dwn->display);
/* Hide current workspace windows */
workspace_hide(dwn->current_workspace);
/* Update current workspace */
int old_workspace = dwn->current_workspace;
dwn->current_workspace = index;
/* Show new workspace windows */
workspace_show(index);
/* Arrange windows on new workspace */
workspace_arrange(index);
/* Focus appropriate window */
Workspace *ws = workspace_get(index);
if (ws != NULL) {
if (ws->focused != NULL) {
client_focus(ws->focused);
} else {
Client *first = workspace_get_first_client(index);
if (first != NULL) {
client_focus(first);
} else {
/* No windows, focus root */
XSetInputFocus(dwn->display, dwn->root, RevertToPointerRoot, CurrentTime);
atoms_set_active_window(None);
}
}
}
/* Update EWMH */
atoms_set_current_desktop(index);
/* Release server and sync - all changes appear atomically */
XUngrabServer(dwn->display);
XSync(dwn->display, False);
(void)old_workspace;
}
void workspace_switch_next(void)
{
int next = (dwn->current_workspace + 1) % MAX_WORKSPACES;
workspace_switch(next);
}
void workspace_switch_prev(void)
{
int prev = (dwn->current_workspace - 1 + MAX_WORKSPACES) % MAX_WORKSPACES;
workspace_switch(prev);
}
/* ========== Client management ========== */
void workspace_add_client(int workspace, Client *client)
{
if (client == NULL || workspace < 0 || workspace >= MAX_WORKSPACES) {
return;
}
Workspace *ws = workspace_get(workspace);
if (ws == NULL) {
return;
}
/* Add to workspace client list (at head) */
client->workspace = workspace;
/* We don't maintain a separate linked list per workspace,
just use the client's workspace field */
}
void workspace_remove_client(int workspace, Client *client)
{
if (client == NULL || workspace < 0 || workspace >= MAX_WORKSPACES) {
return;
}
Workspace *ws = workspace_get(workspace);
if (ws == NULL) {
return;
}
/* If this was the focused client, clear it */
if (ws->focused == client) {
ws->focused = NULL;
}
}
void workspace_move_client(Client *client, int new_workspace)
{
if (client == NULL || new_workspace < 0 || new_workspace >= MAX_WORKSPACES) {
return;
}
if (client->workspace == (unsigned int)new_workspace) {
return;
}
int old_workspace = client->workspace;
LOG_DEBUG("Moving window '%s' from workspace %d to %d",
client->title, old_workspace + 1, new_workspace + 1);
/* Grab server to batch all X operations */
XGrabServer(dwn->display);
/* Remove from old workspace */
workspace_remove_client(old_workspace, client);
/* Add to new workspace */
workspace_add_client(new_workspace, client);
/* Update EWMH */
atoms_set_window_desktop(client->window, new_workspace);
/* Hide if moving to non-current workspace */
if (new_workspace != dwn->current_workspace) {
client_hide(client);
} else {
client_show(client);
}
/* Rearrange both workspaces */
workspace_arrange(old_workspace);
workspace_arrange(new_workspace);
/* Release server and sync */
XUngrabServer(dwn->display);
XSync(dwn->display, False);
}
Client *workspace_get_first_client(int workspace)
{
if (workspace < 0 || workspace >= MAX_WORKSPACES) {
return NULL;
}
for (Client *c = dwn->client_list; c != NULL; c = c->next) {
if (c->workspace == (unsigned int)workspace && !client_is_minimized(c)) {
return c;
}
}
return NULL;
}
Client *workspace_get_focused_client(int workspace)
{
Workspace *ws = workspace_get(workspace);
return ws != NULL ? ws->focused : NULL;
}
/* ========== Layout ========== */
void workspace_set_layout(int workspace, LayoutType layout)
{
Workspace *ws = workspace_get(workspace);
if (ws == NULL) {
return;
}
ws->layout = layout;
/* Batch the arrangement */
XGrabServer(dwn->display);
workspace_arrange(workspace);
XUngrabServer(dwn->display);
XSync(dwn->display, False);
LOG_DEBUG("Workspace %d layout set to %d", workspace + 1, layout);
}
LayoutType workspace_get_layout(int workspace)
{
Workspace *ws = workspace_get(workspace);
return ws != NULL ? ws->layout : LAYOUT_TILING;
}
void workspace_cycle_layout(int workspace)
{
Workspace *ws = workspace_get(workspace);
if (ws == NULL) {
return;
}
ws->layout = (ws->layout + 1) % LAYOUT_COUNT;
/* Batch the arrangement for smoother transition */
XGrabServer(dwn->display);
workspace_arrange(workspace);
XUngrabServer(dwn->display);
XSync(dwn->display, False);
const char *layout_names[] = { "Tiling", "Floating", "Monocle" };
LOG_INFO("Workspace %d: %s layout", workspace + 1, layout_names[ws->layout]);
}
void workspace_set_master_ratio(int workspace, float ratio)
{
Workspace *ws = workspace_get(workspace);
if (ws == NULL) {
return;
}
/* Clamp ratio */
if (ratio < 0.1f) ratio = 0.1f;
if (ratio > 0.9f) ratio = 0.9f;
ws->master_ratio = ratio;
/* Batch the arrangement */
XGrabServer(dwn->display);
workspace_arrange(workspace);
XUngrabServer(dwn->display);
XSync(dwn->display, False);
}
void workspace_adjust_master_ratio(int workspace, float delta)
{
Workspace *ws = workspace_get(workspace);
if (ws != NULL) {
workspace_set_master_ratio(workspace, ws->master_ratio + delta);
}
}
void workspace_set_master_count(int workspace, int count)
{
Workspace *ws = workspace_get(workspace);
if (ws == NULL) {
return;
}
if (count < 1) count = 1;
ws->master_count = count;
/* Batch the arrangement */
XGrabServer(dwn->display);
workspace_arrange(workspace);
XUngrabServer(dwn->display);
XSync(dwn->display, False);
}
void workspace_adjust_master_count(int workspace, int delta)
{
Workspace *ws = workspace_get(workspace);
if (ws != NULL) {
workspace_set_master_count(workspace, ws->master_count + delta);
}
}
/* ========== Arrangement ========== */
void workspace_arrange(int workspace)
{
Workspace *ws = workspace_get(workspace);
if (ws == NULL) {
return;
}
/* Only arrange visible workspace */
if (workspace != dwn->current_workspace) {
return;
}
layout_arrange(workspace);
}
void workspace_arrange_current(void)
{
/* Batch the arrangement */
XGrabServer(dwn->display);
workspace_arrange(dwn->current_workspace);
XUngrabServer(dwn->display);
XSync(dwn->display, False);
}
/* ========== Visibility ========== */
void workspace_show(int workspace)
{
if (workspace < 0 || workspace >= MAX_WORKSPACES) {
return;
}
for (Client *c = dwn->client_list; c != NULL; c = c->next) {
if (c->workspace == (unsigned int)workspace && !client_is_minimized(c)) {
client_show(c);
}
}
}
void workspace_hide(int workspace)
{
if (workspace < 0 || workspace >= MAX_WORKSPACES) {
return;
}
for (Client *c = dwn->client_list; c != NULL; c = c->next) {
if (c->workspace == (unsigned int)workspace) {
client_hide(c);
}
}
}
/* ========== Properties ========== */
void workspace_set_name(int workspace, const char *name)
{
Workspace *ws = workspace_get(workspace);
if (ws == NULL || name == NULL) {
return;
}
strncpy(ws->name, name, sizeof(ws->name) - 1);
ws->name[sizeof(ws->name) - 1] = '\0';
atoms_update_desktop_names();
}
const char *workspace_get_name(int workspace)
{
Workspace *ws = workspace_get(workspace);
return ws != NULL ? ws->name : "";
}
int workspace_client_count(int workspace)
{
return client_count_on_workspace(workspace);
}
bool workspace_is_empty(int workspace)
{
return workspace_client_count(workspace) == 0;
}
/* ========== Focus cycling ========== */
void workspace_focus_next(void)
{
Workspace *ws = workspace_get_current();
if (ws == NULL || ws->focused == NULL) {
return;
}
/* Find next client on same workspace */
Client *next = ws->focused->next;
while (next != NULL && next->workspace != (unsigned int)dwn->current_workspace) {
next = next->next;
}
/* Wrap around */
if (next == NULL) {
next = workspace_get_first_client(dwn->current_workspace);
}
if (next != NULL && next != ws->focused) {
client_focus(next);
}
}
void workspace_focus_prev(void)
{
Workspace *ws = workspace_get_current();
if (ws == NULL || ws->focused == NULL) {
return;
}
/* Find previous client on same workspace */
Client *prev = ws->focused->prev;
while (prev != NULL && prev->workspace != (unsigned int)dwn->current_workspace) {
prev = prev->prev;
}
/* Wrap around */
if (prev == NULL) {
for (Client *c = client_get_last(); c != NULL; c = c->prev) {
if (c->workspace == (unsigned int)dwn->current_workspace) {
prev = c;
break;
}
}
}
if (prev != NULL && prev != ws->focused) {
client_focus(prev);
}
}
void workspace_focus_master(void)
{
Client *master = workspace_get_first_client(dwn->current_workspace);
if (master != NULL) {
client_focus(master);
}
}