chore: add MIT license, Makefile, and initial build artifacts for DWN project
This commit is contained in:
@@ -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(¤t_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);
|
||||
}
|
||||
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
+1110
File diff suppressed because it is too large
Load Diff
+361
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
+697
@@ -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 ({ or ) */
|
||||
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], "&", 5) == 0) {
|
||||
dst[j++] = '&'; i += 4;
|
||||
} else if (strncmp(&src[i], "<", 4) == 0) {
|
||||
dst[j++] = '<'; i += 3;
|
||||
} else if (strncmp(&src[i], ">", 4) == 0) {
|
||||
dst[j++] = '>'; i += 3;
|
||||
} else if (strncmp(&src[i], """, 6) == 0) {
|
||||
dst[j++] = '"'; i += 5;
|
||||
} else if (strncmp(&src[i], "'", 6) == 0) {
|
||||
dst[j++] = '\''; i += 5;
|
||||
} else if (strncmp(&src[i], " ", 6) == 0) {
|
||||
dst[j++] = ' '; i += 5;
|
||||
} else if (strncmp(&src[i], "–", 7) == 0) {
|
||||
dst[j++] = '-'; i += 6;
|
||||
} else if (strncmp(&src[i], "—", 7) == 0) {
|
||||
dst[j++] = '-'; dst[j++] = '-'; i += 6;
|
||||
} else if (strncmp(&src[i], "…", 8) == 0) {
|
||||
dst[j++] = '.'; dst[j++] = '.'; dst[j++] = '.'; i += 7;
|
||||
} else if (strncmp(&src[i], "‘", 7) == 0 || strncmp(&src[i], "’", 7) == 0) {
|
||||
dst[j++] = '\''; i += 6;
|
||||
} else if (strncmp(&src[i], "“", 7) == 0 || strncmp(&src[i], "”", 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);
|
||||
}
|
||||
@@ -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, ¬if_width, ¬if_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
@@ -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
File diff suppressed because it is too large
Load Diff
+709
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user