827 lines
24 KiB
C
Raw Normal View History

2025-12-28 03:14:31 +01:00
/*
* DWN - Desktop Window Manager
* retoor <retoor@molodetz.nl>
2025-12-28 03:14:31 +01:00
* 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>
#define PANEL_PADDING 8
#define WIDGET_SPACING 12
#define WORKSPACE_WIDTH 28
#define TASKBAR_ITEM_WIDTH 150
#define CLOCK_FORMAT "%H:%M:%S"
#define DATE_FORMAT "%Y-%m-%d"
static char clock_buffer[32] = "";
static char date_buffer[32] = "";
typedef struct {
int cpu_percent;
int mem_percent;
int mem_used_mb;
int mem_total_mb;
float load_1min;
float load_5min;
float load_15min;
2025-12-28 03:14:31 +01:00
unsigned long long prev_idle;
unsigned long long prev_total;
} SystemStats;
static SystemStats sys_stats = {0};
static void panel_render_system_stats(Panel *panel, int x, int *width);
static int panel_calculate_stats_width(void);
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;
}
if (dwn->font != NULL) {
return XTextWidth(dwn->font, text, len);
}
return 0;
}
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;
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;
}
}
XSetForeground(dwn->display, dwn->gc, color);
XDrawString(dwn->display, d, dwn->gc, x, y, text, len);
}
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 *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;
}
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);
panel->buffer = XCreatePixmap(dwn->display, panel->window,
panel->width, panel->height,
DefaultDepth(dwn->display, dwn->screen));
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);
}
}
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;
}
}
void panel_render(Panel *panel)
{
if (panel == NULL || !panel->visible) {
return;
}
Display *dpy = dwn->display;
const ColorScheme *colors = config_get_colors();
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) {
panel_render_workspaces(panel, x, &width);
x += width + WIDGET_SPACING;
panel_render_layout_indicator(panel, x, &width);
x += width + WIDGET_SPACING;
panel_render_taskbar(panel, x, &width);
int systray_actual_width = systray_get_width();
int systray_x = panel->width - systray_actual_width - PANEL_PADDING;
systray_render(panel, systray_x, &width);
if (dwn->ai_enabled) {
int ai_x = systray_x - 60;
panel_render_ai_status(panel, ai_x, &width);
}
} else {
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));
}
int clock_width = panel_text_width(clock_buffer, strlen(clock_buffer));
int stats_width = panel_calculate_stats_width();
int clock_x = panel->width - clock_width - PANEL_PADDING;
panel_render_clock(panel, clock_x, &width);
int stats_x = clock_x - stats_width - WIDGET_SPACING;
panel_render_system_stats(panel, stats_x, &width);
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) {
2025-12-28 03:14:31 +01:00
int news_width = 0;
news_render(panel, news_start, news_max_width, &news_width);
}
}
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);
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);
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;
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;
}
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);
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);
char title[64];
strncpy(title, c->title, sizeof(title) - 4);
2025-12-28 03:14:31 +01:00
title[sizeof(title) - 4] = '\0';
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);
size_t cut = len - 1;
while (cut > 0 && (title[cut] & 0xC0) == 0x80) {
cut--;
2025-12-28 03:14:31 +01:00
}
if (cut > 0) cut--;
while (cut > 0 && (title[cut] & 0xC0) == 0x80) {
cut--;
}
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)
{
(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));
}
void panel_handle_click(Panel *panel, int x, int y, int button)
{
if (panel == NULL) {
return;
}
if (panel->position == PANEL_TOP) {
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;
}
int ws = panel_hit_test_workspace(panel, x, y);
if (ws >= 0 && ws < MAX_WORKSPACES) {
if (button == 1) {
2025-12-28 03:14:31 +01:00
workspace_switch(ws);
}
return;
}
Client *c = panel_hit_test_taskbar(panel, x, y);
if (c != NULL) {
if (button == 1) {
client_focus(c);
} else if (button == 3) {
2025-12-28 03:14:31 +01:00
client_close(c);
}
return;
}
} else if (panel->position == PANEL_BOTTOM) {
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;
}
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);
}
}
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);
}
void panel_update_system_stats(void)
{
FILE *fp;
char line[256];
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);
}
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;
2025-12-28 03:14:31 +01:00
}
}
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);
}
}
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);
}
}
static int panel_calculate_stats_width(void)
{
if (dwn->xft_font == NULL && dwn->font == NULL) return 0;
char buf[256];
int total = 0;
int len = snprintf(buf, sizeof(buf), "CPU:%2d%%", sys_stats.cpu_percent);
total += panel_text_width(buf, len) + WIDGET_SPACING;
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;
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;
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;
char stats_buf[256];
int len;
unsigned long cpu_color = colors->panel_fg;
if (sys_stats.cpu_percent >= 90) {
cpu_color = colors->workspace_urgent;
2025-12-28 03:14:31 +01:00
} else if (sys_stats.cpu_percent >= 70) {
cpu_color = 0xFFA500;
2025-12-28 03:14:31 +01:00
}
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;
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;
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;
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;
2025-12-28 03:14:31 +01:00
} else if (bat_snap.percentage <= 40 && !bat_snap.charging) {
bat_color = 0xFFA500;
2025-12-28 03:14:31 +01:00
} else if (bat_snap.charging) {
bat_color = colors->workspace_active;
2025-12-28 03:14:31 +01:00
}
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;
}
void panel_init_systray(void)
{
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)");
}