893 lines
27 KiB
C
893 lines
27 KiB
C
|
|
/*
|
||
|
|
* 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)");
|
||
|
|
}
|