style: remove comments from header files

This commit is contained in:
retoor 2025-12-28 05:01:46 +01:00
parent a6b9fa3469
commit c76a620012
38 changed files with 167 additions and 1481 deletions

View File

@ -10,7 +10,6 @@
#include "dwn.h"
#include <stdbool.h>
/* AI request states */
typedef enum {
AI_STATE_IDLE,
AI_STATE_PENDING,
@ -18,7 +17,6 @@ typedef enum {
AI_STATE_ERROR
} AIState;
/* AI request structure */
typedef struct AIRequest {
char *prompt;
char *response;
@ -28,7 +26,6 @@ typedef struct AIRequest {
struct AIRequest *next;
} AIRequest;
/* AI context for window analysis */
typedef struct {
char focused_window[256];
char focused_class[64];
@ -36,40 +33,32 @@ typedef struct {
int window_count;
} AIContext;
/* Initialization */
bool ai_init(void);
void ai_cleanup(void);
bool ai_is_available(void);
/* API calls */
AIRequest *ai_send_request(const char *prompt, void (*callback)(AIRequest *));
void ai_cancel_request(AIRequest *req);
void ai_process_pending(void);
/* Context analysis */
void ai_update_context(void);
const char *ai_analyze_task(void);
const char *ai_suggest_window(void);
const char *ai_suggest_app(void);
/* Command palette */
void ai_show_command_palette(void);
void ai_execute_command(const char *command);
/* Smart features */
void ai_auto_organize_workspace(void);
void ai_suggest_layout(void);
void ai_analyze_workflow(void);
/* Notification intelligence */
bool ai_should_show_notification(const char *app, const char *summary);
int ai_notification_priority(const char *app, const char *summary);
/* Performance monitoring */
void ai_monitor_performance(void);
const char *ai_performance_suggestion(void);
/* Exa semantic search */
typedef struct {
char title[256];
char url[512];
@ -81,7 +70,7 @@ typedef struct ExaRequest {
char *query;
ExaSearchResult results[10];
int result_count;
int state; /* Uses AIState enum */
int state;
void (*callback)(struct ExaRequest *req);
void *user_data;
struct ExaRequest *next;
@ -92,4 +81,4 @@ ExaRequest *exa_search(const char *query, void (*callback)(ExaRequest *));
void exa_process_pending(void);
void exa_show_app_launcher(void);
#endif /* DWN_AI_H */
#endif

View File

@ -9,41 +9,33 @@
#include <stdbool.h>
/* Maximum applications to track */
#define MAX_APPS 512
#define MAX_RECENT_APPS 10
/* Application entry from .desktop file */
typedef struct {
char name[128]; /* Display name */
char exec[512]; /* Command to execute */
char icon[128]; /* Icon name (unused for now) */
char desktop_id[256]; /* Desktop file basename for tracking */
bool terminal; /* Run in terminal */
bool hidden; /* Should be hidden */
char name[128];
char exec[512];
char icon[128];
char desktop_id[256];
bool terminal;
bool hidden;
} AppEntry;
/* Application launcher state */
typedef struct {
AppEntry apps[MAX_APPS];
int app_count;
char recent[MAX_RECENT_APPS][256]; /* Desktop IDs of recent apps */
char recent[MAX_RECENT_APPS][256];
int recent_count;
} AppLauncherState;
/* Initialize the app launcher (scans .desktop files) */
void applauncher_init(void);
/* Cleanup resources */
void applauncher_cleanup(void);
/* Rescan .desktop files */
void applauncher_refresh(void);
/* Show the application launcher (dmenu-based) */
void applauncher_show(void);
/* Launch an application by desktop_id */
void applauncher_launch(const char *desktop_id);
#endif /* DWN_APPLAUNCHER_H */
#endif

View File

@ -10,9 +10,7 @@
#include <X11/Xlib.h>
#include <stdbool.h>
/* EWMH (Extended Window Manager Hints) atoms */
typedef struct {
/* Root window properties */
Atom NET_SUPPORTED;
Atom NET_SUPPORTING_WM_CHECK;
Atom NET_CLIENT_LIST;
@ -25,7 +23,6 @@ typedef struct {
Atom NET_ACTIVE_WINDOW;
Atom NET_WORKAREA;
/* Client window properties */
Atom NET_WM_NAME;
Atom NET_WM_VISIBLE_NAME;
Atom NET_WM_DESKTOP;
@ -36,7 +33,6 @@ typedef struct {
Atom NET_WM_STRUT_PARTIAL;
Atom NET_WM_PID;
/* Window types */
Atom NET_WM_WINDOW_TYPE_DESKTOP;
Atom NET_WM_WINDOW_TYPE_DOCK;
Atom NET_WM_WINDOW_TYPE_TOOLBAR;
@ -47,7 +43,6 @@ typedef struct {
Atom NET_WM_WINDOW_TYPE_NORMAL;
Atom NET_WM_WINDOW_TYPE_NOTIFICATION;
/* Window states */
Atom NET_WM_STATE_MODAL;
Atom NET_WM_STATE_STICKY;
Atom NET_WM_STATE_MAXIMIZED_VERT;
@ -62,7 +57,6 @@ typedef struct {
Atom NET_WM_STATE_DEMANDS_ATTENTION;
Atom NET_WM_STATE_FOCUSED;
/* Actions */
Atom NET_WM_ACTION_MOVE;
Atom NET_WM_ACTION_RESIZE;
Atom NET_WM_ACTION_MINIMIZE;
@ -74,14 +68,12 @@ typedef struct {
Atom NET_WM_ACTION_CHANGE_DESKTOP;
Atom NET_WM_ACTION_CLOSE;
/* Client messages */
Atom NET_CLOSE_WINDOW;
Atom NET_MOVERESIZE_WINDOW;
Atom NET_WM_MOVERESIZE;
Atom NET_REQUEST_FRAME_EXTENTS;
Atom NET_FRAME_EXTENTS;
/* System tray */
Atom NET_SYSTEM_TRAY_OPCODE;
Atom NET_SYSTEM_TRAY_S0;
Atom MANAGER;
@ -89,7 +81,6 @@ typedef struct {
Atom XEMBED_INFO;
} EWMHAtoms;
/* ICCCM (Inter-Client Communication Conventions Manual) atoms */
typedef struct {
Atom WM_PROTOCOLS;
Atom WM_DELETE_WINDOW;
@ -103,7 +94,6 @@ typedef struct {
Atom WM_WINDOW_ROLE;
} ICCCMAtoms;
/* Other useful atoms */
typedef struct {
Atom UTF8_STRING;
Atom COMPOUND_TEXT;
@ -113,15 +103,12 @@ typedef struct {
Atom DWN_RESTART;
} MiscAtoms;
/* Global atom containers */
extern EWMHAtoms ewmh;
extern ICCCMAtoms icccm;
extern MiscAtoms misc_atoms;
/* Initialization */
void atoms_init(Display *display);
/* EWMH root window setup */
void atoms_setup_ewmh(void);
void atoms_update_client_list(void);
void atoms_update_desktop_names(void);
@ -129,7 +116,6 @@ void atoms_set_current_desktop(int desktop);
void atoms_set_active_window(Window window);
void atoms_set_number_of_desktops(int count);
/* Window property helpers */
bool atoms_get_window_type(Window window, Atom *type);
bool atoms_get_window_state(Window window, Atom **states, int *count);
bool atoms_set_window_state(Window window, Atom *states, int count);
@ -138,12 +124,10 @@ bool atoms_set_window_desktop(Window window, int desktop);
char *atoms_get_window_name(Window window);
bool atoms_get_wm_class(Window window, char *class_name, char *instance_name, size_t len);
/* Protocol helpers */
bool atoms_window_supports_protocol(Window window, Atom protocol);
void atoms_send_protocol(Window window, Atom protocol, Time timestamp);
/* Client message sending */
void atoms_send_client_message(Window window, Atom message_type,
long data0, long data1, long data2, long data3, long data4);
#endif /* DWN_ATOMS_H */
#endif

View File

@ -10,17 +10,14 @@
#include "dwn.h"
#include <stdbool.h>
/* Client creation and destruction */
Client *client_create(Window window);
void client_destroy(Client *client);
/* Client management */
Client *client_manage(Window window);
void client_unmanage(Client *client);
Client *client_find_by_window(Window window);
Client *client_find_by_frame(Window frame);
/* Client state */
void client_focus(Client *client);
void client_unfocus(Client *client);
void client_raise(Client *client);
@ -28,14 +25,12 @@ void client_lower(Client *client);
void client_minimize(Client *client);
void client_restore(Client *client);
/* Client geometry */
void client_move(Client *client, int x, int y);
void client_resize(Client *client, int width, int height);
void client_move_resize(Client *client, int x, int y, int width, int height);
void client_configure(Client *client);
void client_apply_size_hints(Client *client, int *width, int *height);
/* Client properties */
void client_update_title(Client *client);
void client_update_class(Client *client);
void client_set_fullscreen(Client *client, bool fullscreen);
@ -43,7 +38,6 @@ void client_toggle_fullscreen(Client *client);
void client_set_floating(Client *client, bool floating);
void client_toggle_floating(Client *client);
/* Window type checking */
bool client_is_floating(Client *client);
bool client_is_fullscreen(Client *client);
bool client_is_minimized(Client *client);
@ -51,31 +45,26 @@ bool client_is_dialog(Window window);
bool client_is_dock(Window window);
bool client_is_desktop(Window window);
/* Frame management */
void client_create_frame(Client *client);
void client_destroy_frame(Client *client);
void client_reparent_to_frame(Client *client);
void client_reparent_from_frame(Client *client);
/* Visibility */
void client_show(Client *client);
void client_hide(Client *client);
bool client_is_visible(Client *client);
/* Close handling */
void client_close(Client *client);
void client_kill(Client *client);
/* List operations */
void client_add_to_list(Client *client);
void client_remove_from_list(Client *client);
int client_count(void);
int client_count_on_workspace(int workspace);
/* Iteration */
Client *client_get_next(Client *client);
Client *client_get_prev(Client *client);
Client *client_get_first(void);
Client *client_get_last(void);
#endif /* DWN_CLIENT_H */
#endif

View File

@ -10,7 +10,6 @@
#include "dwn.h"
#include <stdbool.h>
/* Color configuration */
typedef struct {
unsigned long panel_bg;
unsigned long panel_fg;
@ -27,16 +26,13 @@ typedef struct {
unsigned long notification_fg;
} ColorScheme;
/* Configuration structure */
struct Config {
/* General */
char terminal[128];
char launcher[128];
char file_manager[128];
FocusMode focus_mode;
bool show_decorations;
/* Appearance */
int border_width;
int title_height;
int panel_height;
@ -44,34 +40,28 @@ struct Config {
char font_name[128];
ColorScheme colors;
/* Layout */
float default_master_ratio;
int default_master_count;
LayoutType default_layout;
/* Panels */
bool top_panel_enabled;
bool bottom_panel_enabled;
/* AI */
char openrouter_api_key[256];
char exa_api_key[256];
char ai_model[64];
bool ai_enabled;
/* Paths */
char config_path[512];
char log_path[512];
};
/* Configuration functions */
Config *config_create(void);
void config_destroy(Config *cfg);
bool config_load(Config *cfg, const char *path);
bool config_reload(Config *cfg);
void config_set_defaults(Config *cfg);
/* Getters for commonly used values */
const char *config_get_terminal(void);
const char *config_get_launcher(void);
int config_get_border_width(void);
@ -80,9 +70,8 @@ int config_get_panel_height(void);
int config_get_gap(void);
const ColorScheme *config_get_colors(void);
/* INI parsing helpers */
typedef void (*ConfigCallback)(const char *section, const char *key,
const char *value, void *user_data);
bool config_parse_ini(const char *path, ConfigCallback callback, void *user_data);
#endif /* DWN_CONFIG_H */
#endif

View File

@ -10,7 +10,6 @@
#include "dwn.h"
#include <stdbool.h>
/* Button types */
typedef enum {
BUTTON_CLOSE,
BUTTON_MAXIMIZE,
@ -18,32 +17,26 @@ typedef enum {
BUTTON_COUNT
} ButtonType;
/* Button areas for hit testing */
typedef struct {
int x, y;
int width, height;
} ButtonArea;
/* Decoration initialization */
void decorations_init(void);
void decorations_cleanup(void);
/* Rendering */
void decorations_render(Client *client, bool focused);
void decorations_render_title_bar(Client *client, bool focused);
void decorations_render_buttons(Client *client, bool focused);
void decorations_render_border(Client *client, bool focused);
/* Hit testing */
ButtonType decorations_hit_test_button(Client *client, int x, int y);
bool decorations_hit_test_title_bar(Client *client, int x, int y);
bool decorations_hit_test_resize_area(Client *client, int x, int y, int *direction);
/* Button actions */
void decorations_button_press(Client *client, ButtonType button);
/* Text rendering */
void decorations_draw_text(Window window, GC gc, int x, int y,
const char *text, unsigned long color);
#endif /* DWN_DECORATIONS_H */
#endif

View File

@ -16,24 +16,20 @@
#include <stdbool.h>
#include <stdint.h>
/* Version */
#define DWN_VERSION "1.0.0"
#define DWN_NAME "DWN"
/* Limits */
#define MAX_CLIENTS 256
#define MAX_WORKSPACES 9
#define MAX_MONITORS 8
#define MAX_NOTIFICATIONS 32
#define MAX_KEYBINDINGS 64
/* Default dimensions */
#define DEFAULT_BORDER_WIDTH 2
#define DEFAULT_TITLE_HEIGHT 24
#define DEFAULT_PANEL_HEIGHT 28
#define DEFAULT_GAP 4
/* Common error/status codes */
typedef enum {
DWN_OK = 0,
DWN_ERROR = -1,
@ -44,7 +40,6 @@ typedef enum {
DWN_ERROR_IO = -6
} DwnStatus;
/* Layout types */
typedef enum {
LAYOUT_TILING,
LAYOUT_FLOATING,
@ -52,13 +47,11 @@ typedef enum {
LAYOUT_COUNT
} LayoutType;
/* Focus modes */
typedef enum {
FOCUS_CLICK,
FOCUS_FOLLOW
} FocusMode;
/* Client state flags */
typedef enum {
CLIENT_NORMAL = 0,
CLIENT_FLOATING = (1 << 0),
@ -68,31 +61,28 @@ typedef enum {
CLIENT_STICKY = (1 << 4)
} ClientFlags;
/* Forward declarations */
typedef struct Client Client;
typedef struct Workspace Workspace;
typedef struct Monitor Monitor;
typedef struct Panel Panel;
typedef struct Config Config;
/* Client structure - represents a managed window */
struct Client {
Window window; /* Application window */
Window frame; /* Frame window (decoration) */
int x, y; /* Position */
int width, height; /* Size */
int old_x, old_y; /* Previous position (for floating restore) */
Window window;
Window frame;
int x, y;
int width, height;
int old_x, old_y;
int old_width, old_height;
int border_width;
uint32_t flags; /* ClientFlags bitmask */
unsigned int workspace; /* Current workspace (0-8) */
char title[256]; /* Window title */
char class[64]; /* Window class */
Client *next; /* Linked list */
uint32_t flags;
unsigned int workspace;
char title[256];
char class[64];
Client *next;
Client *prev;
};
/* Monitor structure - represents a physical display */
struct Monitor {
int x, y;
int width, height;
@ -100,17 +90,15 @@ struct Monitor {
bool primary;
};
/* Workspace structure */
struct Workspace {
Client *clients; /* Head of client list */
Client *focused; /* Currently focused client */
LayoutType layout; /* Current layout */
float master_ratio; /* Ratio for master area in tiling */
int master_count; /* Number of windows in master area */
char name[32]; /* Workspace name */
Client *clients;
Client *focused;
LayoutType layout;
float master_ratio;
int master_count;
char name[32];
};
/* Panel widget types */
typedef enum {
WIDGET_WORKSPACES,
WIDGET_TASKBAR,
@ -120,7 +108,6 @@ typedef enum {
WIDGET_SEPARATOR
} WidgetType;
/* Global state - singleton pattern */
typedef struct {
Display *display;
int screen;
@ -128,36 +115,28 @@ typedef struct {
int screen_width;
int screen_height;
/* Monitors */
Monitor monitors[MAX_MONITORS];
int monitor_count;
/* Workspaces */
Workspace workspaces[MAX_WORKSPACES];
int current_workspace;
/* Clients */
Client *client_list; /* All clients */
Client *client_list;
int client_count;
/* Panels */
Panel *top_panel;
Panel *bottom_panel;
/* Configuration */
Config *config;
/* State */
bool running;
bool ai_enabled;
/* Graphics contexts */
GC gc;
XFontStruct *font;
XftFont *xft_font; /* Xft font for UTF-8 rendering */
XftFont *xft_font;
Colormap colormap;
/* Drag state */
Client *drag_client;
int drag_start_x, drag_start_y;
int drag_orig_x, drag_orig_y;
@ -165,16 +144,13 @@ typedef struct {
bool resizing;
} DWNState;
/* Global state accessor */
extern DWNState *dwn;
/* Core functions */
int dwn_init(void);
void dwn_cleanup(void);
void dwn_run(void);
void dwn_quit(void);
/* Event handlers */
void dwn_handle_event(XEvent *ev);
#endif /* DWN_H */
#endif

View File

@ -11,16 +11,13 @@
#include <stdbool.h>
#include <X11/keysym.h>
/* Modifier masks */
#define MOD_ALT Mod1Mask
#define MOD_CTRL ControlMask
#define MOD_SHIFT ShiftMask
#define MOD_SUPER Mod4Mask
/* Key binding callback type */
typedef void (*KeyCallback)(void);
/* Key binding structure */
typedef struct {
unsigned int modifiers;
KeySym keysym;
@ -28,26 +25,21 @@ typedef struct {
const char *description;
} KeyBinding;
/* Initialization */
void keys_init(void);
void keys_cleanup(void);
void keys_grab_all(void);
void keys_ungrab_all(void);
/* Key binding registration */
void keys_bind(unsigned int modifiers, KeySym keysym,
KeyCallback callback, const char *description);
void keys_unbind(unsigned int modifiers, KeySym keysym);
void keys_clear_all(void);
/* Key event handling */
void keys_handle_press(XKeyEvent *ev);
void keys_handle_release(XKeyEvent *ev);
/* Default key bindings */
void keys_setup_defaults(void);
/* Key binding callbacks */
void key_spawn_terminal(void);
void key_spawn_launcher(void);
void key_spawn_file_manager(void);
@ -89,11 +81,10 @@ void key_ai_command(void);
void key_show_shortcuts(void);
void key_start_tutorial(void);
/* Tutorial system */
void tutorial_start(void);
void tutorial_stop(void);
void tutorial_next_step(void);
void tutorial_check_key(unsigned int modifiers, KeySym keysym);
bool tutorial_is_active(void);
#endif /* DWN_KEYS_H */
#endif

View File

@ -9,18 +9,15 @@
#include "dwn.h"
/* Layout arrangement */
void layout_arrange(int workspace);
void layout_arrange_tiling(int workspace);
void layout_arrange_floating(int workspace);
void layout_arrange_monocle(int workspace);
/* Layout helpers */
int layout_get_usable_area(int *x, int *y, int *width, int *height);
int layout_count_tiled_clients(int workspace);
/* Layout names */
const char *layout_get_name(LayoutType layout);
const char *layout_get_symbol(LayoutType layout);
#endif /* DWN_LAYOUT_H */
#endif

View File

@ -10,18 +10,15 @@
#include "dwn.h"
#include <stdbool.h>
/* Maximum articles to cache */
#define MAX_NEWS_ARTICLES 50
#define NEWS_API_URL "https://news.app.molodetz.nl/api"
/* Sentiment classification */
typedef enum {
SENTIMENT_NEUTRAL = 0,
SENTIMENT_POSITIVE,
SENTIMENT_NEGATIVE
} NewsSentiment;
/* News article */
typedef struct {
char title[256];
char content[1024];
@ -31,46 +28,39 @@ typedef struct {
float sentiment_score;
} NewsArticle;
/* News ticker state */
typedef struct {
NewsArticle articles[MAX_NEWS_ARTICLES];
int article_count;
int current_article; /* Currently displayed article index */
double scroll_offset; /* Sub-pixel offset for smooth scrolling */
bool fetching; /* Currently fetching from API */
bool has_error; /* Last fetch failed */
long last_fetch; /* Timestamp of last fetch */
long last_scroll_update; /* Timestamp of last scroll update */
bool interactive_mode; /* User is navigating with up/down */
int display_widths[MAX_NEWS_ARTICLES]; /* Cached text widths */
int total_width; /* Total scrollable width */
int render_x; /* X position where news starts rendering */
int render_width; /* Width of news render area */
bool widths_dirty; /* Need to recalculate widths */
int current_article;
double scroll_offset;
bool fetching;
bool has_error;
long last_fetch;
long last_scroll_update;
bool interactive_mode;
int display_widths[MAX_NEWS_ARTICLES];
int total_width;
int render_x;
int render_width;
bool widths_dirty;
} NewsState;
/* Global state */
extern NewsState news_state;
/* Initialization */
void news_init(void);
void news_cleanup(void);
/* Fetching */
void news_fetch_async(void);
void news_update(void); /* Called from main loop */
void news_update(void);
/* Navigation */
void news_next_article(void);
void news_prev_article(void);
void news_open_current(void);
/* Rendering */
void news_render(Panel *panel, int x, int max_width, int *used_width);
void news_handle_click(int x, int y);
/* Thread-safe access */
void news_lock(void);
void news_unlock(void);
#endif /* DWN_NEWS_H */
#endif

View File

@ -11,42 +11,36 @@
#include <stdbool.h>
#include <dbus/dbus.h>
/* Notification urgency levels */
typedef enum {
NOTIFY_URGENCY_LOW,
NOTIFY_URGENCY_NORMAL,
NOTIFY_URGENCY_CRITICAL
} NotifyUrgency;
/* Notification structure */
typedef struct Notification {
uint32_t id;
char app_name[64];
char summary[512]; /* Larger summary for AI responses */
char *body; /* Dynamically allocated - unlimited size */
size_t body_len; /* Length of body text */
char summary[512];
char *body;
size_t body_len;
char icon[256];
int timeout; /* -1 = default, 0 = never, >0 = milliseconds */
int timeout;
NotifyUrgency urgency;
long expire_time; /* Timestamp when notification should disappear */
Window window; /* X11 window for rendering */
int width; /* Dynamic width based on content */
int height; /* Dynamic height based on content */
long expire_time;
Window window;
int width;
int height;
struct Notification *next;
} Notification;
/* D-Bus connection */
extern DBusConnection *dbus_conn;
/* Initialization */
bool notifications_init(void);
void notifications_cleanup(void);
/* D-Bus handling */
void notifications_process_messages(void);
bool notifications_register_service(void);
/* Notification management */
uint32_t notification_show(const char *app_name, const char *summary,
const char *body, const char *icon, int timeout);
void notification_close(uint32_t id);
@ -54,21 +48,18 @@ void notification_close_all(void);
Notification *notification_find(uint32_t id);
Notification *notification_find_by_window(Window window);
/* Rendering */
void notification_render(Notification *notif);
void notifications_render_all(void);
void notifications_update(void);
void notifications_position(void);
void notifications_raise_all(void);
/* D-Bus method handlers */
DBusHandlerResult notifications_handle_message(DBusConnection *conn,
DBusMessage *msg,
void *user_data);
/* Server info */
void notifications_get_server_info(const char **name, const char **vendor,
const char **version, const char **spec_version);
void notifications_get_capabilities(const char ***caps, int *count);
#endif /* DWN_NOTIFICATIONS_H */
#endif

View File

@ -10,29 +10,25 @@
#include "dwn.h"
#include <stdbool.h>
/* Panel position */
typedef enum {
PANEL_TOP,
PANEL_BOTTOM
} PanelPosition;
/* Panel structure */
struct Panel {
Window window;
PanelPosition position;
int x, y;
int width, height;
bool visible;
Pixmap buffer; /* Double buffering */
Pixmap buffer;
};
/* Panel initialization */
Panel *panel_create(PanelPosition position);
void panel_destroy(Panel *panel);
void panels_init(void);
void panels_cleanup(void);
/* Panel rendering */
void panel_render(Panel *panel);
void panel_render_all(void);
void panel_render_workspaces(Panel *panel, int x, int *width);
@ -42,25 +38,20 @@ void panel_render_systray(Panel *panel, int x, int *width);
void panel_render_layout_indicator(Panel *panel, int x, int *width);
void panel_render_ai_status(Panel *panel, int x, int *width);
/* Panel interaction */
void panel_handle_click(Panel *panel, int x, int y, int button);
int panel_hit_test_workspace(Panel *panel, int x, int y);
Client *panel_hit_test_taskbar(Panel *panel, int x, int y);
/* Panel visibility */
void panel_show(Panel *panel);
void panel_hide(Panel *panel);
void panel_toggle(Panel *panel);
/* Clock updates */
void panel_update_clock(void);
/* System stats updates */
void panel_update_system_stats(void);
/* System tray */
void panel_init_systray(void);
void panel_add_systray_icon(Window icon);
void panel_remove_systray_icon(Window icon);
#endif /* DWN_PANEL_H */
#endif

View File

@ -10,18 +10,15 @@
#include "dwn.h"
#include <stdbool.h>
/* Maximum number of WiFi networks to show */
#define MAX_WIFI_NETWORKS 20
/* WiFi network info */
typedef struct {
char ssid[64];
int signal; /* Signal strength 0-100 */
char security[32]; /* Security type (WPA2, etc.) */
int signal;
char security[32];
bool connected;
} WifiNetwork;
/* WiFi state */
typedef struct {
bool enabled;
bool connected;
@ -29,24 +26,21 @@ typedef struct {
int signal_strength;
WifiNetwork networks[MAX_WIFI_NETWORKS];
int network_count;
long last_scan; /* Timestamp of last scan */
long last_scan;
} WifiState;
/* Audio state */
typedef struct {
int volume; /* 0-100 */
int volume;
bool muted;
} AudioState;
/* Battery state */
typedef struct {
bool present; /* Battery exists */
bool charging; /* Currently charging */
int percentage; /* 0-100 */
int time_remaining; /* Minutes remaining */
bool present;
bool charging;
int percentage;
int time_remaining;
} BatteryState;
/* Volume slider popup */
typedef struct {
Window window;
int x, y;
@ -55,7 +49,6 @@ typedef struct {
bool dragging;
} VolumeSlider;
/* Dropdown menu */
typedef struct {
Window window;
int x, y;
@ -66,35 +59,29 @@ typedef struct {
void (*on_select)(int index);
} DropdownMenu;
/* System tray state */
extern WifiState wifi_state;
extern AudioState audio_state;
extern BatteryState battery_state;
extern DropdownMenu *wifi_menu;
extern VolumeSlider *volume_slider;
/* Initialization */
void systray_init(void);
void systray_cleanup(void);
/* WiFi functions */
void wifi_update_state(void);
void wifi_scan_networks(void);
void wifi_connect(const char *ssid);
void wifi_disconnect(void);
const char *wifi_get_icon(void);
/* Audio functions */
void audio_update_state(void);
void audio_set_volume(int volume);
void audio_toggle_mute(void);
const char *audio_get_icon(void);
/* Battery functions */
void battery_update_state(void);
const char *battery_get_icon(void);
/* Volume slider functions */
VolumeSlider *volume_slider_create(int x, int y);
void volume_slider_destroy(VolumeSlider *slider);
void volume_slider_show(VolumeSlider *slider);
@ -104,7 +91,6 @@ void volume_slider_handle_click(VolumeSlider *slider, int x, int y);
void volume_slider_handle_motion(VolumeSlider *slider, int x, int y);
void volume_slider_handle_release(VolumeSlider *slider);
/* Dropdown menu functions */
DropdownMenu *dropdown_create(int x, int y, int width);
void dropdown_destroy(DropdownMenu *menu);
void dropdown_show(DropdownMenu *menu);
@ -115,21 +101,17 @@ int dropdown_hit_test(DropdownMenu *menu, int x, int y);
void dropdown_handle_click(DropdownMenu *menu, int x, int y);
void dropdown_handle_motion(DropdownMenu *menu, int x, int y);
/* Panel rendering for systray */
void systray_render(Panel *panel, int x, int *width);
int systray_get_width(void);
void systray_handle_click(int x, int y, int button);
int systray_hit_test(int x); /* Returns: 0=wifi, 1=audio, -1=none */
int systray_hit_test(int x);
/* Periodic update */
void systray_update(void);
/* Thread-safe state access */
void systray_lock(void);
void systray_unlock(void);
/* Thread-safe state snapshots (copies state under lock) */
BatteryState systray_get_battery_snapshot(void);
AudioState systray_get_audio_snapshot(void);
#endif /* DWN_SYSTRAY_H */
#endif

View File

@ -12,11 +12,9 @@
#include <stdarg.h>
#include <assert.h>
/* Contract assertion macro - use for programmer errors */
#define DWN_ASSERT(cond) assert(cond)
#define DWN_ASSERT_MSG(cond, msg) assert((cond) && (msg))
/* Logging levels */
typedef enum {
LOG_DEBUG,
LOG_INFO,
@ -24,51 +22,43 @@ typedef enum {
LOG_ERROR
} LogLevel;
/* Async Logging - non-blocking with max file size (5MB) and rotation */
void log_init(const char *log_file);
void log_close(void);
void log_set_level(LogLevel level);
void log_flush(void); /* Force flush pending logs (call before exit/crash) */
void log_flush(void);
void log_msg(LogLevel level, const char *fmt, ...);
/* Convenience macros */
#define LOG_DEBUG(...) log_msg(LOG_DEBUG, __VA_ARGS__)
#define LOG_INFO(...) log_msg(LOG_INFO, __VA_ARGS__)
#define LOG_WARN(...) log_msg(LOG_WARN, __VA_ARGS__)
#define LOG_ERROR(...) log_msg(LOG_ERROR, __VA_ARGS__)
/* Memory allocation with error checking */
void *dwn_malloc(size_t size);
void *dwn_calloc(size_t nmemb, size_t size);
void *dwn_realloc(void *ptr, size_t size);
char *dwn_strdup(const char *s);
void dwn_free(void *ptr);
void secure_wipe(void *ptr, size_t size); /* Securely wipe sensitive data */
void secure_wipe(void *ptr, size_t size);
/* String utilities */
char *str_trim(char *str);
bool str_starts_with(const char *str, const char *prefix);
bool str_ends_with(const char *str, const char *suffix);
int str_split(char *str, char delim, char **parts, int max_parts);
char *shell_escape(const char *str); /* Escape string for safe shell use */
char *shell_escape(const char *str);
/* File utilities */
bool file_exists(const char *path);
char *file_read_all(const char *path);
bool file_write_all(const char *path, const char *content);
char *expand_path(const char *path);
/* Color utilities */
unsigned long parse_color(const char *color_str);
void color_to_rgb(unsigned long color, int *r, int *g, int *b);
/* Time utilities */
long get_time_ms(void);
void sleep_ms(int ms);
/* Process utilities */
int spawn(const char *cmd);
int spawn_async(const char *cmd);
char *spawn_capture(const char *cmd);
#endif /* DWN_UTIL_H */
#endif

View File

@ -10,28 +10,23 @@
#include "dwn.h"
#include <stdbool.h>
/* Workspace initialization */
void workspace_init(void);
void workspace_cleanup(void);
/* Workspace access */
Workspace *workspace_get(int index);
Workspace *workspace_get_current(void);
int workspace_get_current_index(void);
/* Workspace switching */
void workspace_switch(int index);
void workspace_switch_next(void);
void workspace_switch_prev(void);
/* Client management within workspaces */
void workspace_add_client(int workspace, Client *client);
void workspace_remove_client(int workspace, Client *client);
void workspace_move_client(Client *client, int new_workspace);
Client *workspace_get_first_client(int workspace);
Client *workspace_get_focused_client(int workspace);
/* Layout */
void workspace_set_layout(int workspace, LayoutType layout);
LayoutType workspace_get_layout(int workspace);
void workspace_cycle_layout(int workspace);
@ -40,23 +35,19 @@ void workspace_adjust_master_ratio(int workspace, float delta);
void workspace_set_master_count(int workspace, int count);
void workspace_adjust_master_count(int workspace, int delta);
/* Arrangement */
void workspace_arrange(int workspace);
void workspace_arrange_current(void);
/* Visibility */
void workspace_show(int workspace);
void workspace_hide(int workspace);
/* Properties */
void workspace_set_name(int workspace, const char *name);
const char *workspace_get_name(int workspace);
int workspace_client_count(int workspace);
bool workspace_is_empty(int workspace);
/* Focus cycling within workspace */
void workspace_focus_next(void);
void workspace_focus_prev(void);
void workspace_focus_master(void);
#endif /* DWN_WORKSPACE_H */
#endif

View File

@ -37,7 +37,6 @@
</div>
</nav>
</header>
<main>
<section class="hero" style="padding: 8rem 0 4rem;">
<div class="container hero-content">
@ -47,7 +46,6 @@
</p>
</div>
</section>
<section class="section">
<div class="container">
<div class="alert alert-info" style="margin-bottom: 2rem;">
@ -55,7 +53,6 @@
<p style="margin: 0;">AI features are completely optional and require external API keys.
DWN works perfectly without them.</p>
</div>
<h2>Overview</h2>
<p>
DWN integrates with two AI services to provide intelligent desktop assistance:
@ -64,7 +61,6 @@
<li><strong>OpenRouter API</strong> - Powers the AI command palette and context analysis</li>
<li><strong>Exa API</strong> - Provides semantic web search capabilities</li>
</ul>
<div class="features-grid">
<div class="feature-card">
<div class="feature-icon">&#129302;</div>
@ -88,14 +84,11 @@
<p style="margin-top: 1rem;"><kbd>Super</kbd> + <kbd>Shift</kbd> + <kbd>E</kbd></p>
</div>
</div>
<!-- Setup OpenRouter -->
<h2 id="openrouter" style="margin-top: 4rem;">Setting Up OpenRouter</h2>
<p style="color: var(--text-muted); margin-bottom: 1.5rem;">
OpenRouter provides access to multiple AI models through a single API.
You can use free models or paid ones depending on your needs.
</p>
<div class="steps">
<div class="step">
<div class="step-number">1</div>
@ -105,7 +98,6 @@
and create a free account to get your API key.</p>
</div>
</div>
<div class="step">
<div class="step-number">2</div>
<div class="step-content">
@ -118,7 +110,6 @@
<pre><code>export OPENROUTER_API_KEY="sk-or-v1-your-key-here"</code></pre>
</div>
</div>
<div class="step">
<div class="step-number">3</div>
<div class="step-content">
@ -136,7 +127,6 @@ model = google/gemini-2.0-flash-exp:free</code></pre>
</div>
</div>
</div>
<div class="card" style="margin-top: 2rem;">
<h3>Recommended Free Models</h3>
<div class="table-wrapper">
@ -168,14 +158,11 @@ model = google/gemini-2.0-flash-exp:free</code></pre>
</table>
</div>
</div>
<!-- AI Command Palette -->
<h2 id="command-palette" style="margin-top: 4rem;">AI Command Palette</h2>
<p>
Press <kbd>Super</kbd> + <kbd>Shift</kbd> + <kbd>A</kbd> to open the command palette.
Type natural language commands and press Enter.
</p>
<h3>Supported Commands</h3>
<div class="table-wrapper">
<table>
@ -215,19 +202,15 @@ model = google/gemini-2.0-flash-exp:free</code></pre>
</tbody>
</table>
</div>
<div class="alert alert-success" style="margin-top: 1.5rem;">
<strong class="alert-title">Pro Tip</strong>
<p style="margin: 0;">The AI understands context. You can say "open browser" instead of
remembering the exact application name - it will figure out what you mean.</p>
</div>
<!-- Context Analysis -->
<h2 id="context" style="margin-top: 4rem;">Context Analysis</h2>
<p>
Press <kbd>Super</kbd> + <kbd>A</kbd> to see AI-powered analysis of your current workspace.
</p>
<div class="card">
<h3>What It Shows</h3>
<ul style="padding-left: 1.25rem;">
@ -237,13 +220,10 @@ model = google/gemini-2.0-flash-exp:free</code></pre>
<li><strong>Workspace Summary</strong> - Overview of open applications</li>
</ul>
</div>
<!-- Setup Exa -->
<h2 id="exa" style="margin-top: 4rem;">Setting Up Exa Search</h2>
<p style="color: var(--text-muted); margin-bottom: 1.5rem;">
Exa provides semantic search - finding content based on meaning rather than exact keywords.
</p>
<div class="steps">
<div class="step">
<div class="step-number">1</div>
@ -253,7 +233,6 @@ model = google/gemini-2.0-flash-exp:free</code></pre>
and create an account to get your API key.</p>
</div>
</div>
<div class="step">
<div class="step-number">2</div>
<div class="step-content">
@ -265,7 +244,6 @@ model = google/gemini-2.0-flash-exp:free</code></pre>
<pre><code>export EXA_API_KEY="your-exa-key-here"</code></pre>
</div>
</div>
<div class="step">
<div class="step-number">3</div>
<div class="step-content">
@ -274,13 +252,10 @@ model = google/gemini-2.0-flash-exp:free</code></pre>
</div>
</div>
</div>
<!-- Semantic Search -->
<h2 id="search" style="margin-top: 4rem;">Using Semantic Search</h2>
<p>
Unlike traditional search, Exa understands the meaning of your query.
</p>
<div class="comparison" style="grid-template-columns: repeat(2, 1fr);">
<div class="comparison-card">
<h3>Traditional Search</h3>
@ -307,7 +282,6 @@ model = google/gemini-2.0-flash-exp:free</code></pre>
</p>
</div>
</div>
<h3 style="margin-top: 2rem;">Search Tips</h3>
<ul>
<li>Use natural, conversational queries</li>
@ -315,14 +289,11 @@ model = google/gemini-2.0-flash-exp:free</code></pre>
<li>Results appear in a dmenu/rofi list - select to open in browser</li>
<li>Search includes articles, documentation, tutorials, and more</li>
</ul>
<!-- Privacy -->
<h2 id="privacy" style="margin-top: 4rem;">Privacy Considerations</h2>
<div class="alert alert-warning">
<strong class="alert-title">Data Sent to External Services</strong>
<p style="margin: 0;">When using AI features, the following data is sent to external APIs:</p>
</div>
<div class="table-wrapper" style="margin-top: 1rem;">
<table>
<thead>
@ -351,7 +322,6 @@ model = google/gemini-2.0-flash-exp:free</code></pre>
</tbody>
</table>
</div>
<p style="margin-top: 1rem; color: var(--text-muted);">
If you're concerned about privacy, you can:
</p>
@ -360,10 +330,7 @@ model = google/gemini-2.0-flash-exp:free</code></pre>
<li>Use OpenRouter with privacy-focused models</li>
<li>Only use AI features when needed</li>
</ul>
<!-- Troubleshooting -->
<h2 id="troubleshooting" style="margin-top: 4rem;">Troubleshooting</h2>
<div class="faq-item">
<button class="faq-question" onclick="toggleFaq(this)">
AI commands don't work - "API key not configured"
@ -380,7 +347,6 @@ model = google/gemini-2.0-flash-exp:free</code></pre>
</div>
</div>
</div>
<div class="faq-item">
<button class="faq-question" onclick="toggleFaq(this)">
Slow responses from AI
@ -392,7 +358,6 @@ model = google/gemini-2.0-flash-exp:free</code></pre>
</div>
</div>
</div>
<div class="faq-item">
<button class="faq-question" onclick="toggleFaq(this)">
Exa search returns no results
@ -404,11 +369,9 @@ model = google/gemini-2.0-flash-exp:free</code></pre>
</div>
</div>
</div>
</div>
</section>
</main>
<footer>
<div class="container">
<div class="footer-grid">
@ -452,7 +415,6 @@ model = google/gemini-2.0-flash-exp:free</code></pre>
</div>
</div>
</footer>
<script src="js/main.js"></script>
</body>
</html>

View File

@ -37,7 +37,6 @@
</div>
</nav>
</header>
<main>
<section class="hero" style="padding: 8rem 0 4rem;">
<div class="container hero-content">
@ -47,7 +46,6 @@
</p>
</div>
</section>
<section class="section">
<div class="container">
<h2>Overview</h2>
@ -56,7 +54,6 @@
A global <code>DWNState</code> singleton manages all state, and the main event loop
dispatches X11 events to specialized modules.
</p>
<div class="card" style="margin: 2rem 0;">
<h3>Project Statistics</h3>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1rem; text-align: center;">
@ -78,8 +75,6 @@
</div>
</div>
</div>
<!-- Directory Structure -->
<h2 id="structure" style="margin-top: 3rem;">Directory Structure</h2>
<div class="code-header">
<span>Project Layout</span>
@ -104,8 +99,6 @@
├── Makefile # Build system
├── CLAUDE.md # AI assistant context
└── README.md # Project readme</code></pre>
<!-- Core Modules -->
<h2 id="modules" style="margin-top: 3rem;">Core Modules</h2>
<div class="table-wrapper">
<table>
@ -185,8 +178,6 @@
</tbody>
</table>
</div>
<!-- Module Dependencies -->
<h2 id="dependencies" style="margin-top: 3rem;">Module Dependencies</h2>
<div class="card">
<pre style="margin: 0; background: transparent; border: none; padding: 0;"><code>main.c (orchestrator)
@ -208,14 +199,11 @@
└── keys.c
└── config.c</code></pre>
</div>
<!-- Global State -->
<h2 id="state" style="margin-top: 3rem;">Global State (DWNState)</h2>
<p>
All window manager state is centralized in a single <code>DWNState</code> structure.
This simplifies state management and makes the codebase easier to understand.
</p>
<div class="code-header">
<span>include/dwn.h (simplified)</span>
</div>
@ -223,32 +211,23 @@
Display *display; // X11 connection
Window root; // Root window
int screen; // Default screen
Client *clients[MAX_CLIENTS]; // All managed windows
int client_count;
Workspace workspaces[MAX_WORKSPACES]; // Virtual desktops
int current_workspace;
Panel top_panel;
Panel bottom_panel;
Config config; // User configuration
KeyBinding keys[MAX_KEYBINDINGS];
// EWMH atoms
Atom atoms[ATOM_COUNT];
} DWNState;
extern DWNState *dwn; // Global singleton</code></pre>
<!-- Event Loop -->
<h2 id="events" style="margin-top: 3rem;">Event Loop</h2>
<p>
DWN uses a traditional X11 event loop with XNextEvent. Events are dispatched
to appropriate handlers based on type.
</p>
<div class="code-header">
<span>main.c (simplified)</span>
</div>
@ -256,11 +235,9 @@ extern DWNState *dwn; // Global singleton</code></pre>
dwn_init(); // Initialize X11, atoms, config
setup_keybindings(); // Register keyboard shortcuts
setup_panels(); // Create panel windows
XEvent event;
while (running) {
XNextEvent(dwn->display, &event);
switch (event.type) {
case MapRequest:
handle_map_request(&event.xmaprequest);
@ -280,12 +257,9 @@ extern DWNState *dwn; // Global singleton</code></pre>
// ... more event types
}
}
dwn_cleanup();
return 0;
}</code></pre>
<!-- Key Constants -->
<h2 id="constants" style="margin-top: 3rem;">Key Constants</h2>
<div class="table-wrapper">
<table>
@ -325,8 +299,6 @@ extern DWNState *dwn; // Global singleton</code></pre>
</tbody>
</table>
</div>
<!-- Coding Conventions -->
<h2 id="conventions" style="margin-top: 3rem;">Coding Conventions</h2>
<div class="features-grid" style="grid-template-columns: repeat(2, 1fr);">
<div class="card">
@ -348,33 +320,26 @@ extern DWNState *dwn; // Global singleton</code></pre>
</ul>
</div>
</div>
<div class="code-header" style="margin-top: 1.5rem;">
<span>Example Function</span>
</div>
<pre><code>void client_focus(Client *c) {
if (!c) return;
// Unfocus previous
if (dwn->focused && dwn->focused != c) {
client_unfocus(dwn->focused);
}
dwn->focused = c;
XSetInputFocus(dwn->display, c->window, RevertToPointerRoot, CurrentTime);
XRaiseWindow(dwn->display, c->frame);
decorations_update(c);
atoms_set_active_window(c->window);
}</code></pre>
<!-- EWMH/ICCCM -->
<h2 id="protocols" style="margin-top: 3rem;">EWMH/ICCCM Support</h2>
<p>
DWN implements key Extended Window Manager Hints and ICCCM protocols
for compatibility with modern applications.
</p>
<div class="features-grid" style="grid-template-columns: repeat(2, 1fr);">
<div class="card">
<h3>EWMH Atoms</h3>
@ -407,13 +372,10 @@ extern DWNState *dwn; // Global singleton</code></pre>
</ul>
</div>
</div>
<!-- Build System -->
<h2 id="build" style="margin-top: 3rem;">Build System</h2>
<p>
DWN uses a simple Makefile-based build system with pkg-config for dependency detection.
</p>
<div class="table-wrapper">
<table>
<thead>
@ -467,13 +429,10 @@ extern DWNState *dwn; // Global singleton</code></pre>
</tbody>
</table>
</div>
<!-- Contributing -->
<h2 id="contributing" style="margin-top: 3rem;">Contributing</h2>
<p>
Contributions are welcome! Here's how to get started:
</p>
<div class="steps">
<div class="step">
<div class="step-number">1</div>
@ -511,11 +470,9 @@ extern DWNState *dwn; // Global singleton</code></pre>
</div>
</div>
</div>
</div>
</section>
</main>
<footer>
<div class="container">
<div class="footer-grid">
@ -559,7 +516,6 @@ extern DWNState *dwn; // Global singleton</code></pre>
</div>
</div>
</footer>
<script src="js/main.js"></script>
</body>
</html>

View File

@ -37,7 +37,6 @@
</div>
</nav>
</header>
<main>
<section class="hero" style="padding: 8rem 0 4rem;">
<div class="container hero-content">
@ -47,7 +46,6 @@
</p>
</div>
</section>
<section class="section">
<div class="container">
<h2>Configuration File</h2>
@ -55,19 +53,15 @@
DWN reads its configuration from <code>~/.config/dwn/config</code> using an INI-style format.
Changes take effect on restart (or you can reload in a future version).
</p>
<div class="alert alert-info">
<strong class="alert-title">First Run</strong>
<p style="margin: 0;">DWN creates a default configuration file on first run if one doesn't exist.
You can also copy the example config from the source repository.</p>
</div>
<!-- General Section -->
<h2 id="general" style="margin-top: 3rem;">[general] - Core Settings</h2>
<p style="color: var(--text-muted); margin-bottom: 1.5rem;">
Basic behavior settings for applications and focus handling.
</p>
<div class="table-wrapper">
<table>
<thead>
@ -106,7 +100,6 @@
</tbody>
</table>
</div>
<div class="code-header">
<span>Example</span>
<button class="copy-btn" onclick="copyCode(this)">Copy</button>
@ -117,13 +110,10 @@ launcher = rofi -show run
file_manager = nautilus
focus_mode = click
decorations = true</code></pre>
<!-- Appearance Section -->
<h2 id="appearance" style="margin-top: 3rem;">[appearance] - Visual Settings</h2>
<p style="color: var(--text-muted); margin-bottom: 1.5rem;">
Control the visual appearance of windows, panels, and gaps.
</p>
<div class="table-wrapper">
<table>
<thead>
@ -168,7 +158,6 @@ decorations = true</code></pre>
</tbody>
</table>
</div>
<div class="code-header">
<span>Example</span>
<button class="copy-btn" onclick="copyCode(this)">Copy</button>
@ -179,13 +168,10 @@ title_height = 28
panel_height = 32
gap = 8
font = DejaVu Sans-10</code></pre>
<!-- Layout Section -->
<h2 id="layout" style="margin-top: 3rem;">[layout] - Layout Behavior</h2>
<p style="color: var(--text-muted); margin-bottom: 1.5rem;">
Configure the default layout mode and tiling parameters.
</p>
<div class="table-wrapper">
<table>
<thead>
@ -218,7 +204,6 @@ font = DejaVu Sans-10</code></pre>
</tbody>
</table>
</div>
<div class="code-header">
<span>Example</span>
<button class="copy-btn" onclick="copyCode(this)">Copy</button>
@ -227,13 +212,10 @@ font = DejaVu Sans-10</code></pre>
default = tiling
master_ratio = 0.60
master_count = 1</code></pre>
<!-- Panels Section -->
<h2 id="panels" style="margin-top: 3rem;">[panels] - Panel Visibility</h2>
<p style="color: var(--text-muted); margin-bottom: 1.5rem;">
Control which panels are displayed.
</p>
<div class="table-wrapper">
<table>
<thead>
@ -257,7 +239,6 @@ master_count = 1</code></pre>
</tbody>
</table>
</div>
<div class="code-header">
<span>Example - Minimal Setup</span>
<button class="copy-btn" onclick="copyCode(this)">Copy</button>
@ -265,13 +246,10 @@ master_count = 1</code></pre>
<pre><code>[panels]
top = true
bottom = false</code></pre>
<!-- Colors Section -->
<h2 id="colors" style="margin-top: 3rem;">[colors] - Color Scheme</h2>
<p style="color: var(--text-muted); margin-bottom: 1.5rem;">
Customize all colors using hex format (#RRGGBB).
</p>
<div class="table-wrapper">
<table>
<thead>
@ -350,7 +328,6 @@ bottom = false</code></pre>
</tbody>
</table>
</div>
<div class="code-header">
<span>Example - Nord Theme</span>
<button class="copy-btn" onclick="copyCode(this)">Copy</button>
@ -369,13 +346,10 @@ border_focused = #88c0d0
border_unfocused = #3b4252
notification_bg = #3b4252
notification_fg = #eceff4</code></pre>
<!-- AI Section -->
<h2 id="ai" style="margin-top: 3rem;">[ai] - AI Integration</h2>
<p style="color: var(--text-muted); margin-bottom: 1.5rem;">
Configure AI features. See <a href="ai-features.html">AI Features</a> for full setup instructions.
</p>
<div class="table-wrapper">
<table>
<thead>
@ -400,7 +374,6 @@ notification_fg = #eceff4</code></pre>
</tbody>
</table>
</div>
<div class="code-header">
<span>Example</span>
<button class="copy-btn" onclick="copyCode(this)">Copy</button>
@ -409,14 +382,11 @@ notification_fg = #eceff4</code></pre>
model = google/gemini-2.0-flash-exp:free
openrouter_api_key = sk-or-v1-your-key-here
exa_api_key = your-exa-key-here</code></pre>
<div class="alert alert-warning" style="margin-top: 1rem;">
<strong class="alert-title">Security Note</strong>
<p style="margin: 0;">For better security, use environment variables instead of storing API keys in the config file:
<code>export OPENROUTER_API_KEY=sk-or-v1-...</code></p>
</div>
<!-- Complete Example -->
<h2 style="margin-top: 3rem;">Complete Configuration Example</h2>
<div class="code-header">
<span>~/.config/dwn/config</span>
@ -424,30 +394,25 @@ exa_api_key = your-exa-key-here</code></pre>
</div>
<pre><code># DWN Window Manager Configuration
# https://dwn.github.io
[general]
terminal = alacritty
launcher = rofi -show drun
file_manager = thunar
focus_mode = click
decorations = true
[appearance]
border_width = 2
title_height = 24
panel_height = 28
gap = 6
font = DejaVu Sans-10
[layout]
default = tiling
master_ratio = 0.55
master_count = 1
[panels]
top = true
bottom = true
[colors]
panel_bg = #1a1a2e
panel_fg = #e0e0e0
@ -462,15 +427,12 @@ border_focused = #4a90d9
border_unfocused = #333333
notification_bg = #2a2a3e
notification_fg = #ffffff
[ai]
model = google/gemini-2.0-flash-exp:free
# API keys via environment variables recommended</code></pre>
</div>
</section>
</main>
<footer>
<div class="container">
<div class="footer-grid">
@ -514,7 +476,6 @@ model = google/gemini-2.0-flash-exp:free
</div>
</div>
</footer>
<script src="js/main.js"></script>
</body>
</html>

View File

@ -37,7 +37,6 @@
</div>
</nav>
</header>
<div class="docs-layout container">
<aside class="docs-sidebar">
<ul>
@ -70,23 +69,19 @@
</li>
</ul>
</aside>
<main class="docs-content">
<h1 id="introduction">Getting Started with DWN</h1>
<p class="lead">
Learn the fundamentals of DWN and become productive in minutes.
</p>
<h2 id="first-steps">First Steps</h2>
<p>
After <a href="installation.html">installing DWN</a> and starting your session,
you'll see a clean desktop with two panels: a top panel with workspace indicators,
taskbar, and system tray, and a bottom panel showing the clock.
</p>
<h3>Opening Your First Application</h3>
<p>Start by launching a terminal and application launcher:</p>
<div class="table-wrapper">
<table>
<thead>
@ -115,28 +110,23 @@
</tbody>
</table>
</div>
<div class="alert alert-info">
<strong class="alert-title">Tip: Run the Tutorial</strong>
<p style="margin: 0;">Press <kbd>Super</kbd> + <kbd>T</kbd> to start an interactive
tutorial that will guide you through all essential shortcuts.</p>
</div>
<h2 id="basic-concepts">Basic Concepts</h2>
<h3>The Super Key</h3>
<p>
Most DWN shortcuts use the <kbd>Super</kbd> key (often the Windows key or Command key).
This keeps shortcuts separate from application shortcuts that typically use
<kbd>Ctrl</kbd> or <kbd>Alt</kbd>.
</p>
<h3>Focus Model</h3>
<p>
By default, DWN uses "click to focus" - you click on a window to focus it.
You can change this to "focus follows mouse" (sloppy focus) in the configuration.
</p>
<h3>Window Decorations</h3>
<p>
Each window has a title bar showing its name. The title bar color indicates focus:
@ -145,7 +135,6 @@
<li><strong>Blue title bar</strong> - Focused window</li>
<li><strong>Gray title bar</strong> - Unfocused window</li>
</ul>
<h2 id="tutorial">Interactive Tutorial</h2>
<p>
DWN includes a built-in interactive tutorial that teaches you essential shortcuts
@ -157,14 +146,11 @@
<li>Automatically advances when you complete each step</li>
<li>Covers all essential shortcuts from basic to advanced</li>
</ul>
<div class="card">
<h3>Start the Tutorial</h3>
<p>Press <kbd>Super</kbd> + <kbd>T</kbd> at any time to start or resume the tutorial.</p>
</div>
<h2 id="windows">Managing Windows</h2>
<h3>Window Operations</h3>
<div class="table-wrapper">
<table>
@ -202,20 +188,17 @@
</tbody>
</table>
</div>
<h3>Moving and Resizing</h3>
<p>In floating mode, you can move and resize windows with the mouse:</p>
<ul>
<li><strong>Move</strong> - Click and drag the title bar</li>
<li><strong>Resize</strong> - Drag any window edge or corner</li>
</ul>
<h2 id="workspaces">Using Workspaces</h2>
<p>
DWN provides 9 virtual workspaces to organize your windows. You can see which
workspaces are active in the top panel.
</p>
<h3>Workspace Navigation</h3>
<div class="table-wrapper">
<table>
@ -245,7 +228,6 @@
</tbody>
</table>
</div>
<h3>Workspace Organization Tips</h3>
<ul>
<li><strong>Workspace 1</strong> - Main work (editor, terminal)</li>
@ -253,13 +235,11 @@
<li><strong>Workspace 3</strong> - Communication (email, chat)</li>
<li><strong>Workspace 4-9</strong> - Project-specific contexts</li>
</ul>
<h2 id="layouts">Layout Modes</h2>
<p>
DWN supports three layout modes. Press <kbd>Super</kbd> + <kbd>Space</kbd> to cycle
through them.
</p>
<div class="features-grid" style="grid-template-columns: repeat(3, 1fr); gap: 1rem; margin: 1.5rem 0;">
<div class="card" style="padding: 1.5rem;">
<h4>Tiling</h4>
@ -283,7 +263,6 @@
</p>
</div>
</div>
<h3>Tiling Layout Controls</h3>
<div class="table-wrapper">
<table>
@ -313,9 +292,7 @@
</tbody>
</table>
</div>
<h2 id="panels">Panels & System Tray</h2>
<h3>Top Panel</h3>
<p>The top panel contains:</p>
<ul>
@ -323,7 +300,6 @@
<li><strong>Taskbar</strong> - Shows windows on current workspace</li>
<li><strong>System tray</strong> - Battery, volume, WiFi (see below)</li>
</ul>
<h3>System Tray</h3>
<div class="table-wrapper">
<table>
@ -357,7 +333,6 @@
</tbody>
</table>
</div>
<h3>Bottom Panel</h3>
<p>
The bottom panel shows the current time and a scrolling news ticker. Navigate
@ -365,7 +340,6 @@
the current article with <kbd>Super</kbd> + <kbd>Return</kbd>. Both panels can
be hidden in the configuration if you prefer a minimal setup.
</p>
<h2>Next Steps</h2>
<p>Now that you know the basics, explore these topics:</p>
<ul>
@ -375,7 +349,6 @@
</ul>
</main>
</div>
<footer>
<div class="container">
<div class="footer-grid">
@ -419,7 +392,6 @@
</div>
</div>
</footer>
<script src="js/main.js"></script>
</body>
</html>

View File

@ -37,7 +37,6 @@
</div>
</nav>
</header>
<main>
<section class="hero" style="padding: 8rem 0 4rem;">
<div class="container hero-content">
@ -47,8 +46,6 @@
</p>
</div>
</section>
<!-- Window Management -->
<section class="section">
<div class="container">
<h2>Window Management</h2>
@ -56,7 +53,6 @@
DWN provides flexible window management that adapts to your workflow, whether you prefer
the precision of tiling or the freedom of floating windows.
</p>
<div class="features-grid">
<div class="card">
<h3><span class="card-icon">&#9783;</span> Tiling Layout</h3>
@ -92,7 +88,6 @@
</ul>
</div>
</div>
<div class="alert alert-info" style="margin-top: 2rem;">
<strong class="alert-title">Pro Tip</strong>
<p style="margin: 0;">Switch layouts instantly with <kbd>Super</kbd> + <kbd>Space</kbd>.
@ -100,8 +95,6 @@
</div>
</div>
</section>
<!-- Workspaces -->
<section class="section section-alt">
<div class="container">
<h2>Virtual Workspaces</h2>
@ -109,7 +102,6 @@
Nine virtual desktops give you unlimited room to organize your work.
Each workspace maintains its own window state and layout preferences.
</p>
<div class="features-grid" style="grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));">
<div class="feature-card">
<div class="feature-icon">1-9</div>
@ -135,8 +127,6 @@
</div>
</div>
</section>
<!-- Panel & System Tray -->
<section class="section">
<div class="container">
<h2>Panels & System Tray</h2>
@ -144,7 +134,6 @@
Built-in panels provide essential information and quick access to common functions
without needing external tools or status bars.
</p>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 2rem;">
<div class="card">
<h3>Top Panel</h3>
@ -165,7 +154,6 @@
</ul>
</div>
</div>
<h3 style="margin-top: 3rem; margin-bottom: 1.5rem;">News Ticker</h3>
<div class="card">
<p>The bottom panel includes a scrolling news ticker that displays headlines from a news feed.
@ -180,7 +168,6 @@
at 80 pixels per second keeps you informed without distraction.
</p>
</div>
<h3 style="margin-top: 3rem; margin-bottom: 1.5rem;">System Tray Features</h3>
<div class="features-grid">
<div class="card">
@ -213,8 +200,6 @@
</div>
</div>
</section>
<!-- Notifications -->
<section class="section section-alt">
<div class="container">
<h2>Notification System</h2>
@ -222,7 +207,6 @@
Built-in D-Bus notification daemon following freedesktop.org standards.
No need for external notification tools like dunst or notify-osd.
</p>
<div class="features-grid" style="grid-template-columns: repeat(2, 1fr);">
<div class="card">
<h3>Standards Compliant</h3>
@ -235,7 +219,6 @@
Notifications match your overall color scheme automatically.</p>
</div>
</div>
<div class="alert alert-success" style="margin-top: 2rem;">
<strong class="alert-title">Capacity</strong>
<p style="margin: 0;">DWN can display up to 32 notifications simultaneously,
@ -243,8 +226,6 @@
</div>
</div>
</section>
<!-- AI Features Preview -->
<section class="section">
<div class="container">
<h2>AI Integration</h2>
@ -252,7 +233,6 @@
Optional AI features powered by OpenRouter API and Exa semantic search.
Control your desktop with natural language and get intelligent assistance.
</p>
<div class="features-grid">
<div class="card">
<h3>&#129302; AI Command Palette</h3>
@ -275,15 +255,12 @@
</div>
</div>
</section>
<!-- EWMH/ICCCM -->
<section class="section section-alt">
<div class="container">
<h2>Standards Compliance</h2>
<p style="color: var(--text-muted); max-width: 700px; margin-bottom: 2rem;">
DWN implements EWMH and ICCCM protocols for maximum compatibility with X11 applications.
</p>
<div class="features-grid" style="grid-template-columns: repeat(2, 1fr);">
<div class="card">
<h3>EWMH Support</h3>
@ -310,8 +287,6 @@
</div>
</div>
</section>
<!-- Technical Specs -->
<section class="section">
<div class="container">
<h2>Technical Specifications</h2>
@ -361,8 +336,6 @@
</div>
</div>
</section>
<!-- CTA -->
<section class="section section-alt">
<div class="container" style="text-align: center;">
<h2>Ready to Try DWN?</h2>
@ -376,7 +349,6 @@
</div>
</section>
</main>
<footer>
<div class="container">
<div class="footer-grid">
@ -420,7 +392,6 @@
</div>
</div>
</footer>
<script src="js/main.js"></script>
</body>
</html>

View File

@ -38,9 +38,7 @@
</div>
</nav>
</header>
<main>
<!-- Hero Section -->
<section class="hero">
<div class="container hero-content">
<h1>Modern Window Management for X11</h1>
@ -54,8 +52,6 @@
</div>
</div>
</section>
<!-- Key Features Overview -->
<section class="section">
<div class="container">
<h2 style="text-align: center; margin-bottom: 1rem;">Why Choose DWN?</h2>
@ -103,8 +99,6 @@
</div>
</div>
</section>
<!-- Stats -->
<section class="section section-alt">
<div class="container">
<div class="stats">
@ -127,8 +121,6 @@
</div>
</div>
</section>
<!-- Quick Start -->
<section class="section">
<div class="container">
<h2 style="text-align: center;">Get Up and Running in Minutes</h2>
@ -136,7 +128,6 @@
DWN is designed for easy installation and immediate productivity.
Build from source or use your distribution's package manager.
</p>
<div class="card" style="max-width: 700px; margin: 0 auto;">
<div class="code-header">
<span>Terminal</span>
@ -145,25 +136,19 @@
<pre><code># Clone the repository
git clone https://retoor.molodetz.nl/retoor/dwn.git
cd dwn
# Install dependencies (auto-detects your distro)
make deps
# Build and install
make
sudo make install
# Add to your .xinitrc
echo "exec dwn" >> ~/.xinitrc</code></pre>
</div>
<p style="text-align: center; margin-top: 2rem;">
<a href="installation.html" class="btn btn-primary">Full Installation Guide</a>
</p>
</div>
</section>
<!-- Screenshots Preview -->
<section class="section section-alt">
<div class="container">
<h2 style="text-align: center;">See DWN in Action</h2>
@ -192,8 +177,6 @@ echo "exec dwn" >> ~/.xinitrc</code></pre>
</div>
</div>
</section>
<!-- Comparison -->
<section class="section">
<div class="container">
<h2 style="text-align: center;">How DWN Compares</h2>
@ -239,62 +222,9 @@ echo "exec dwn" >> ~/.xinitrc</code></pre>
</div>
</div>
</section>
<!-- Testimonials -->
<section class="section section-alt">
<div class="container">
<h2 style="text-align: center;">What Users Say</h2>
<div class="testimonials">
<div class="testimonial">
<p class="testimonial-text">
"Finally, a window manager that doesn't make me choose between
productivity and usability. DWN just works."
</p>
<div class="testimonial-author">
<div class="testimonial-avatar">JD</div>
<div class="testimonial-info">
<strong>John Developer</strong>
<span>Senior Software Engineer</span>
</div>
</div>
</div>
<div class="testimonial">
<p class="testimonial-text">
"The AI command palette changed how I interact with my desktop.
It's like having an assistant for window management."
</p>
<div class="testimonial-author">
<div class="testimonial-avatar">SA</div>
<div class="testimonial-info">
<strong>Sarah Admin</strong>
<span>System Administrator</span>
</div>
</div>
</div>
<div class="testimonial">
<p class="testimonial-text">
"Coming from i3, the learning curve was minimal. The built-in
tutorial had me productive in under 10 minutes."
</p>
<div class="testimonial-author">
<div class="testimonial-avatar">ML</div>
<div class="testimonial-info">
<strong>Mike Linux</strong>
<span>DevOps Engineer</span>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- CTA Section -->
<section class="section">
<div class="container" style="text-align: center;">
<h2>Ready to Transform Your Desktop?</h2>
<p style="color: var(--text-muted); max-width: 500px; margin: 0 auto 2rem;">
Join thousands of developers and power users who have made DWN their window manager of choice.
</p>
<h2>Ready to Try DWN?</h2>
<div class="hero-buttons" style="justify-content: center;">
<a href="installation.html" class="btn btn-primary btn-lg">Install DWN</a>
<a href="documentation.html" class="btn btn-secondary btn-lg">Read the Docs</a>
@ -302,7 +232,6 @@ echo "exec dwn" >> ~/.xinitrc</code></pre>
</div>
</section>
</main>
<footer>
<div class="container">
<div class="footer-grid">
@ -346,7 +275,6 @@ echo "exec dwn" >> ~/.xinitrc</code></pre>
</div>
</div>
</footer>
<script src="js/main.js"></script>
</body>
</html>

View File

@ -37,7 +37,6 @@
</div>
</nav>
</header>
<main>
<section class="hero" style="padding: 8rem 0 4rem;">
<div class="container hero-content">
@ -47,15 +46,12 @@
</p>
</div>
</section>
<!-- Requirements -->
<section class="section">
<div class="container">
<h2>Requirements</h2>
<p style="color: var(--text-muted); margin-bottom: 2rem;">
DWN requires X11 and a few common libraries. Most Linux distributions include these by default.
</p>
<div class="card">
<h3>Required Dependencies</h3>
<div class="table-wrapper" style="margin-top: 1rem;">
@ -114,15 +110,12 @@
</div>
</div>
</section>
<!-- Quick Install -->
<section class="section section-alt">
<div class="container">
<h2>Quick Installation</h2>
<p style="color: var(--text-muted); margin-bottom: 2rem;">
The fastest way to get started. Our build system auto-detects your distribution.
</p>
<div class="steps">
<div class="step">
<div class="step-number">1</div>
@ -137,7 +130,6 @@
cd dwn</code></pre>
</div>
</div>
<div class="step">
<div class="step-number">2</div>
<div class="step-content">
@ -153,7 +145,6 @@ cd dwn</code></pre>
</p>
</div>
</div>
<div class="step">
<div class="step-number">3</div>
<div class="step-content">
@ -166,7 +157,6 @@ cd dwn</code></pre>
<pre><code>make</code></pre>
</div>
</div>
<div class="step">
<div class="step-number">4</div>
<div class="step-content">
@ -182,7 +172,6 @@ cd dwn</code></pre>
</p>
</div>
</div>
<div class="step">
<div class="step-number">5</div>
<div class="step-content">
@ -198,19 +187,15 @@ cd dwn</code></pre>
</div>
</div>
</section>
<!-- Distribution-specific -->
<section class="section">
<div class="container">
<h2>Distribution-Specific Instructions</h2>
<div class="tabs">
<button class="tab active" onclick="showTab('debian')">Debian/Ubuntu</button>
<button class="tab" onclick="showTab('fedora')">Fedora</button>
<button class="tab" onclick="showTab('arch')">Arch Linux</button>
<button class="tab" onclick="showTab('void')">Void Linux</button>
</div>
<div id="debian" class="tab-content active">
<h3>Debian / Ubuntu / Linux Mint</h3>
<div class="code-header">
@ -230,14 +215,12 @@ sudo apt install -y \
libdbus-1-dev \
libcurl4-openssl-dev \
pkg-config
# Build and install
git clone https://retoor.molodetz.nl/retoor/dwn.git
cd dwn
make
sudo make install</code></pre>
</div>
<div id="fedora" class="tab-content">
<h3>Fedora / RHEL / CentOS</h3>
<div class="code-header">
@ -257,14 +240,12 @@ sudo dnf install -y \
dbus-devel \
libcurl-devel \
pkg-config
# Build and install
git clone https://retoor.molodetz.nl/retoor/dwn.git
cd dwn
make
sudo make install</code></pre>
</div>
<div id="arch" class="tab-content">
<h3>Arch Linux / Manjaro</h3>
<div class="code-header">
@ -283,7 +264,6 @@ sudo pacman -S --needed \
dbus \
curl \
pkg-config
# Build and install
git clone https://retoor.molodetz.nl/retoor/dwn.git
cd dwn
@ -295,7 +275,6 @@ sudo make install</code></pre>
<code>yay -S dwn-git</code></p>
</div>
</div>
<div id="void" class="tab-content">
<h3>Void Linux</h3>
<div class="code-header">
@ -314,7 +293,6 @@ sudo xbps-install -S \
dbus-devel \
libcurl-devel \
pkg-config
# Build and install
git clone https://retoor.molodetz.nl/retoor/dwn.git
cd dwn
@ -323,15 +301,12 @@ sudo make install</code></pre>
</div>
</div>
</section>
<!-- Session Setup -->
<section class="section section-alt">
<div class="container">
<h2>Session Setup</h2>
<p style="color: var(--text-muted); margin-bottom: 2rem;">
Configure your display manager or xinit to start DWN.
</p>
<div class="features-grid" style="grid-template-columns: repeat(2, 1fr);">
<div class="card">
<h3>Using xinit / startx</h3>
@ -342,10 +317,8 @@ sudo make install</code></pre>
</div>
<pre><code># Optional: set display settings
xrandr --output DP-1 --mode 2560x1440
# Optional: set wallpaper
feh --bg-fill ~/wallpaper.jpg
# Start DWN
exec dwn</code></pre>
<p style="margin-top: 1rem; font-size: 0.875rem; color: var(--text-muted);">
@ -372,15 +345,12 @@ DesktopNames=DWN</code></pre>
</div>
</div>
</section>
<!-- Testing -->
<section class="section">
<div class="container">
<h2>Testing in a Nested X Server</h2>
<p style="color: var(--text-muted); margin-bottom: 2rem;">
Test DWN without leaving your current session using Xephyr.
</p>
<div class="card">
<h3>Using make run</h3>
<p>The easiest way to test DWN safely:</p>
@ -392,14 +362,12 @@ DesktopNames=DWN</code></pre>
# Debian/Ubuntu: sudo apt install xserver-xephyr
# Fedora: sudo dnf install xorg-x11-server-Xephyr
# Arch: sudo pacman -S xorg-server-xephyr
# Run DWN in a nested window
make run</code></pre>
<p style="margin-top: 1rem; color: var(--text-muted);">
This opens a 1280x720 window running DWN. Perfect for experimenting with configuration changes.
</p>
</div>
<div class="card" style="margin-top: 1.5rem;">
<h3>Manual Xephyr Setup</h3>
<p>For more control over the test environment:</p>
@ -409,21 +377,16 @@ make run</code></pre>
</div>
<pre><code># Start Xephyr on display :1
Xephyr :1 -screen 1920x1080 &
# Run DWN on that display
DISPLAY=:1 ./dwn
# Open a terminal in the test environment
DISPLAY=:1 xterm &</code></pre>
</div>
</div>
</section>
<!-- Post-install -->
<section class="section section-alt">
<div class="container">
<h2>Post-Installation</h2>
<div class="steps">
<div class="step">
<div class="step-number">1</div>
@ -437,7 +400,6 @@ DISPLAY=:1 xterm &</code></pre>
<pre><code>mkdir -p ~/.config/dwn</code></pre>
</div>
</div>
<div class="step">
<div class="step-number">2</div>
<div class="step-content">
@ -446,7 +408,6 @@ DISPLAY=:1 xterm &</code></pre>
that will teach you all the essential shortcuts.</p>
</div>
</div>
<div class="step">
<div class="step-number">3</div>
<div class="step-content">
@ -454,7 +415,6 @@ DISPLAY=:1 xterm &</code></pre>
<p>Press <kbd>Super</kbd> + <kbd>S</kbd> to see a complete list of keyboard shortcuts.</p>
</div>
</div>
<div class="step">
<div class="step-number">4</div>
<div class="step-content">
@ -465,12 +425,9 @@ DISPLAY=:1 xterm &</code></pre>
</div>
</div>
</section>
<!-- Troubleshooting -->
<section class="section">
<div class="container">
<h2>Troubleshooting</h2>
<div class="faq-item">
<button class="faq-question" onclick="toggleFaq(this)">
DWN doesn't start - "cannot open display"
@ -486,7 +443,6 @@ DISPLAY=:1 xterm &</code></pre>
</div>
</div>
</div>
<div class="faq-item">
<button class="faq-question" onclick="toggleFaq(this)">
Build fails - "pkg-config: command not found"
@ -500,7 +456,6 @@ sudo pacman -S pkg-config # Arch</code></pre>
</div>
</div>
</div>
<div class="faq-item">
<button class="faq-question" onclick="toggleFaq(this)">
Missing header files during build
@ -513,7 +468,6 @@ sudo pacman -S pkg-config # Arch</code></pre>
</div>
</div>
</div>
<div class="faq-item">
<button class="faq-question" onclick="toggleFaq(this)">
Keyboard shortcuts don't work
@ -529,7 +483,6 @@ sudo pacman -S pkg-config # Arch</code></pre>
</div>
</div>
</div>
<div class="faq-item">
<button class="faq-question" onclick="toggleFaq(this)">
Fonts look bad or missing
@ -545,8 +498,6 @@ sudo dnf install dejavu-fonts-all liberation-fonts # Fedora</code></pre>
</div>
</div>
</section>
<!-- CTA -->
<section class="section section-alt">
<div class="container" style="text-align: center;">
<h2>Installation Complete?</h2>
@ -560,7 +511,6 @@ sudo dnf install dejavu-fonts-all liberation-fonts # Fedora</code></pre>
</div>
</section>
</main>
<footer>
<div class="container">
<div class="footer-grid">
@ -604,7 +554,6 @@ sudo dnf install dejavu-fonts-all liberation-fonts # Fedora</code></pre>
</div>
</div>
</footer>
<script src="js/main.js"></script>
</body>
</html>

View File

@ -37,7 +37,6 @@
</div>
</nav>
</header>
<main>
<section class="hero" style="padding: 8rem 0 4rem;">
<div class="container hero-content">
@ -48,14 +47,11 @@
</p>
</div>
</section>
<section class="section">
<div class="container">
<div class="search-box">
<input type="text" id="shortcut-search" placeholder="Search shortcuts..." onkeyup="filterShortcuts()">
</div>
<!-- Application Launchers -->
<h2 id="launchers">Application Launchers</h2>
<div class="table-wrapper">
<table class="shortcuts-table">
@ -89,8 +85,6 @@
</tbody>
</table>
</div>
<!-- Window Management -->
<h2 id="windows">Window Management</h2>
<div class="table-wrapper">
<table class="shortcuts-table">
@ -128,8 +122,6 @@
</tbody>
</table>
</div>
<!-- Workspace Navigation -->
<h2 id="workspaces">Workspace Navigation</h2>
<div class="table-wrapper">
<table class="shortcuts-table">
@ -223,8 +215,6 @@
</tbody>
</table>
</div>
<!-- Layout Control -->
<h2 id="layouts">Layout Control</h2>
<div class="table-wrapper">
<table class="shortcuts-table">
@ -258,8 +248,6 @@
</tbody>
</table>
</div>
<!-- AI Features -->
<h2 id="ai">AI Features</h2>
<p style="color: var(--text-muted); margin-bottom: 1rem;">
These shortcuts require API keys to be configured. See <a href="ai-features.html">AI Features</a> for setup.
@ -288,8 +276,6 @@
</tbody>
</table>
</div>
<!-- News Ticker -->
<h2 id="news">News Ticker</h2>
<p style="color: var(--text-muted); margin-bottom: 1rem;">
Navigate the scrolling news ticker displayed in the bottom panel.
@ -318,8 +304,6 @@
</tbody>
</table>
</div>
<!-- Help & System -->
<h2 id="system">Help & System</h2>
<div class="table-wrapper">
<table class="shortcuts-table">
@ -345,12 +329,9 @@
</tbody>
</table>
</div>
<!-- Printable Reference -->
<div class="card" style="margin-top: 3rem;">
<h3>Printable Quick Reference</h3>
<p>Essential shortcuts to memorize when starting with DWN:</p>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1.5rem; margin-top: 1.5rem;">
<div>
<h4 style="color: var(--primary); margin-bottom: 0.75rem;">Must Know</h4>
@ -397,7 +378,6 @@
</div>
</section>
</main>
<footer>
<div class="container">
<div class="footer-grid">
@ -441,7 +421,6 @@
</div>
</div>
</footer>
<script src="js/main.js"></script>
</body>
</html>

View File

@ -17,21 +17,17 @@
#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)
{
@ -51,7 +47,6 @@ static size_t write_callback(void *contents, size_t size, size_t nmemb, void *us
return realsize;
}
/* ========== Initialization ========== */
bool ai_init(void)
{
@ -62,10 +57,9 @@ bool ai_init(void)
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 */
return true;
}
/* Initialize curl */
curl_global_init(CURL_GLOBAL_DEFAULT);
curl_multi = curl_multi_init();
@ -82,7 +76,6 @@ bool ai_init(void)
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);
@ -104,7 +97,6 @@ bool ai_is_available(void)
return dwn != NULL && dwn->ai_enabled;
}
/* ========== API calls ========== */
AIRequest *ai_send_request(const char *prompt, void (*callback)(AIRequest *))
{
@ -117,11 +109,9 @@ AIRequest *ai_send_request(const char *prompt, void (*callback)(AIRequest *))
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) {
@ -144,7 +134,6 @@ AIRequest *ai_send_request(const char *prompt, void (*callback)(AIRequest *))
dwn_free(escaped_prompt);
/* Create curl easy handle */
CURL *easy = curl_easy_init();
if (easy == NULL) {
dwn_free(json_prompt);
@ -153,10 +142,8 @@ AIRequest *ai_send_request(const char *prompt, void (*callback)(AIRequest *))
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",
@ -175,19 +162,15 @@ AIRequest *ai_send_request(const char *prompt, void (*callback)(AIRequest *))
curl_easy_setopt(easy, CURLOPT_SSL_VERIFYPEER, 1L);
curl_easy_setopt(easy, CURLOPT_SSL_VERIFYHOST, 2L);
/* 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;
}
@ -198,7 +181,6 @@ void ai_cancel_request(AIRequest *req)
return;
}
/* Remove from queue */
AIRequest **pp = &request_queue;
while (*pp != NULL) {
if (*pp == req) {
@ -229,7 +211,6 @@ void ai_process_pending(void)
int running_handles;
curl_multi_perform(curl_multi, &running_handles);
/* Check for completed requests */
CURLMsg *msg;
int msgs_left;
@ -244,8 +225,6 @@ void ai_process_pending(void)
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");
@ -260,7 +239,6 @@ void ai_process_pending(void)
}
}
}
/* Check for error in response */
if (req->state != AI_STATE_COMPLETED) {
cJSON *error = cJSON_GetObjectItemCaseSensitive(root, "error");
if (error != NULL) {
@ -276,7 +254,6 @@ void ai_process_pending(void)
}
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");
@ -286,12 +263,10 @@ void ai_process_pending(void)
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);
@ -304,7 +279,6 @@ void ai_process_pending(void)
}
}
/* ========== Context analysis ========== */
void ai_update_context(void)
{
@ -318,7 +292,6 @@ void ai_update_context(void)
"%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) {
@ -335,7 +308,6 @@ void ai_update_context(void)
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") ||
@ -359,7 +331,6 @@ const char *ai_analyze_task(void)
const char *ai_suggest_window(void)
{
/* Simple heuristic suggestion */
const char *task = ai_analyze_task();
if (strcmp(task, "coding") == 0) {
@ -374,12 +345,10 @@ const char *ai_suggest_window(void)
const char *ai_suggest_app(void)
{
return NULL; /* Would require more context */
return NULL;
}
/* ========== Command palette ========== */
/* Callback for AI command response */
static void ai_command_response_callback(AIRequest *req)
{
if (req == NULL) {
@ -387,15 +356,12 @@ static void ai_command_response_callback(AIRequest *req)
}
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++;
@ -408,7 +374,6 @@ static void ai_command_response_callback(AIRequest *req)
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';
}
@ -420,15 +385,12 @@ static void ai_command_response_callback(AIRequest *req)
}
}
} 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)
@ -440,10 +402,8 @@ void ai_show_command_palette(void)
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) {
@ -466,10 +426,8 @@ void ai_show_command_palette(void)
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();
@ -492,7 +450,6 @@ void ai_show_command_palette(void)
dwn_free(input);
/* Send request */
ai_send_request(prompt, ai_command_response_callback);
}
@ -504,7 +461,6 @@ void ai_execute_command(const char *command)
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.",
@ -513,7 +469,6 @@ void ai_execute_command(const char *command)
ai_send_request(prompt, NULL);
}
/* ========== Smart features ========== */
void ai_auto_organize_workspace(void)
{
@ -530,19 +485,16 @@ 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 */
return true;
}
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;
@ -554,11 +506,9 @@ int ai_notification_priority(const char *app, const char *summary)
return 1;
}
/* ========== Performance monitoring ========== */
void ai_monitor_performance(void)
{
/* Read from /proc for basic metrics */
LOG_DEBUG("AI performance monitoring (placeholder)");
}
@ -567,7 +517,6 @@ const char *ai_performance_suggestion(void)
return NULL;
}
/* ========== Exa Semantic Search ========== */
#define EXA_API_URL "https://api.exa.ai/search"
@ -579,7 +528,6 @@ bool exa_is_available(void)
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) {
@ -607,21 +555,18 @@ static void exa_parse_response(ExaRequest *req, const char *json)
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);
@ -647,11 +592,9 @@ ExaRequest *exa_search(const char *query, void (*callback)(ExaRequest *))
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) {
@ -667,7 +610,6 @@ ExaRequest *exa_search(const char *query, void (*callback)(ExaRequest *))
escaped);
dwn_free(escaped);
/* Create curl handle */
CURL *easy = curl_easy_init();
if (easy == NULL) {
dwn_free(json_query);
@ -676,10 +618,8 @@ ExaRequest *exa_search(const char *query, void (*callback)(ExaRequest *))
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",
@ -698,13 +638,11 @@ ExaRequest *exa_search(const char *query, void (*callback)(ExaRequest *))
curl_easy_setopt(easy, CURLOPT_SSL_VERIFYPEER, 1L);
curl_easy_setopt(easy, CURLOPT_SSL_VERIFYHOST, 2L);
/* 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;
@ -733,7 +671,6 @@ void exa_process_pending(void)
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) {
@ -757,13 +694,11 @@ void exa_process_pending(void)
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) {
@ -780,7 +715,6 @@ void exa_process_pending(void)
}
}
/* Callback for app launcher search */
static void exa_launcher_callback(ExaRequest *req)
{
if (req == NULL || req->state != AI_STATE_COMPLETED) {
@ -793,7 +727,6 @@ static void exa_launcher_callback(ExaRequest *req)
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;
@ -807,7 +740,6 @@ static void exa_launcher_callback(ExaRequest *req)
}
}
/* 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);
@ -817,10 +749,8 @@ static void exa_launcher_callback(ExaRequest *req)
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);
@ -847,7 +777,6 @@ void exa_show_app_launcher(void)
return;
}
/* Get search query from user */
char *query = NULL;
if (spawn("command -v dmenu >/dev/null 2>&1") == 0) {
@ -865,7 +794,6 @@ void exa_show_app_launcher(void)
return;
}
/* Remove trailing newline */
query[strcspn(query, "\n")] = '\0';
notification_show("Exa", "Searching...", query, NULL, 2000);

View File

@ -17,13 +17,10 @@
#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);
@ -32,13 +29,11 @@ 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());
@ -47,15 +42,12 @@ void applauncher_init(void)
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);
@ -70,10 +62,8 @@ 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];
@ -81,14 +71,12 @@ void applauncher_refresh(void)
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)
{
@ -103,17 +91,14 @@ static void scan_desktop_dir(const char *dir)
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) {
@ -125,7 +110,6 @@ static void scan_desktop_dir(const char *dir)
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);
@ -136,7 +120,6 @@ static void scan_desktop_dir(const char *dir)
closedir(d);
}
/* ========== Desktop File Parsing ========== */
static int parse_desktop_file(const char *path, AppEntry *entry)
{
@ -154,14 +137,12 @@ static int parse_desktop_file(const char *path, AppEntry *entry)
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;
@ -171,7 +152,6 @@ static int parse_desktop_file(const char *path, AppEntry *entry)
continue;
}
/* Parse key=value */
char *eq = strchr(line, '=');
if (eq == NULL) {
continue;
@ -181,7 +161,6 @@ static int parse_desktop_file(const char *path, AppEntry *entry)
char *key = line;
char *value = eq + 1;
/* Skip localized entries (Name[en]=...) */
if (strchr(key, '[') != NULL) {
continue;
}
@ -197,7 +176,6 @@ static int parse_desktop_file(const char *path, AppEntry *entry)
} 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;
@ -211,7 +189,6 @@ static int parse_desktop_file(const char *path, AppEntry *entry)
fclose(f);
/* Must have name and exec, and not be hidden */
if (!has_name || !has_exec || entry->hidden) {
return -1;
}
@ -219,7 +196,6 @@ static int parse_desktop_file(const char *path, AppEntry *entry)
return 0;
}
/* ========== Recent Apps Management ========== */
static void load_recent_apps(void)
{
@ -233,7 +209,6 @@ static void load_recent_apps(void)
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';
@ -266,10 +241,8 @@ static void save_recent_apps(void)
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]);
}
@ -279,7 +252,6 @@ static void add_to_recent(const char *desktop_id)
}
}
/* Add to front, shift others */
if (launcher_state.recent_count < MAX_RECENT_APPS) {
launcher_state.recent_count++;
}
@ -292,7 +264,6 @@ static void add_to_recent(const char *desktop_id)
save_recent_apps();
}
/* ========== Helper Functions ========== */
static char *strip_field_codes(const char *exec)
{
@ -301,13 +272,11 @@ static char *strip_field_codes(const char *exec)
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 */
i++;
if (exec[i+1] == ' ') {
i++;
}
@ -318,7 +287,6 @@ static char *strip_field_codes(const char *exec)
}
result[j] = '\0';
/* Trim trailing whitespace */
while (j > 0 && (result[j-1] == ' ' || result[j-1] == '\t')) {
result[--j] = '\0';
}
@ -333,7 +301,6 @@ static int compare_apps(const void *a, const void *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++) {
@ -344,7 +311,6 @@ static AppEntry *find_app_by_id(const char *desktop_id)
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++) {
@ -355,7 +321,6 @@ static AppEntry *find_app_by_name(const char *name)
return NULL;
}
/* ========== Launcher Display ========== */
void applauncher_show(void)
{
@ -364,10 +329,6 @@ void applauncher_show(void)
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) {
@ -376,7 +337,6 @@ void applauncher_show(void)
}
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);
@ -385,11 +345,9 @@ void applauncher_show(void)
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;
@ -405,7 +363,6 @@ void applauncher_show(void)
}
}
/* Add remaining apps */
for (int i = 0; i < launcher_state.app_count; i++) {
if (added[i]) {
continue;
@ -419,12 +376,11 @@ void applauncher_show(void)
}
if (pos > 0) {
choices[pos-1] = '\0'; /* Remove trailing newline */
choices[pos-1] = '\0';
}
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) {
@ -443,7 +399,6 @@ void applauncher_show(void)
return;
}
/* Run dmenu */
char cmd[1024];
snprintf(cmd, sizeof(cmd),
"cat '%s' | dmenu -i -l 10 -p 'Run:'; rm -f '%s'",
@ -458,7 +413,6 @@ void applauncher_show(void)
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';
@ -467,15 +421,13 @@ void applauncher_show(void)
pclose(p);
if (selected[0] == '\0') {
return; /* User cancelled */
return;
}
/* 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);
}
}
@ -488,10 +440,8 @@ void applauncher_launch(const char *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();
@ -503,6 +453,5 @@ void applauncher_launch(const char *desktop_id)
LOG_INFO("Launching: %s (%s)", app->name, cmd);
spawn_async(cmd);
/* Add to recent apps */
add_to_recent(desktop_id);
}

View File

@ -12,19 +12,16 @@
#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");
@ -37,7 +34,6 @@ void atoms_init(Display *display)
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");
@ -48,7 +44,6 @@ void atoms_init(Display *display)
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");
@ -59,7 +54,6 @@ void atoms_init(Display *display)
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");
@ -74,7 +68,6 @@ void atoms_init(Display *display)
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");
@ -86,21 +79,18 @@ void atoms_init(Display *display)
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");
@ -112,7 +102,6 @@ void atoms_init(Display *display)
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");
@ -123,7 +112,6 @@ void atoms_init(Display *display)
LOG_DEBUG("X11 atoms initialized");
}
/* ========== EWMH Setup ========== */
void atoms_setup_ewmh(void)
{
@ -134,10 +122,8 @@ void atoms_setup_ewmh(void)
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);
@ -145,12 +131,10 @@ void atoms_setup_ewmh(void)
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,
@ -176,13 +160,10 @@ void atoms_setup_ewmh(void)
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");
@ -216,7 +197,6 @@ void atoms_update_desktop_names(void)
return;
}
/* Build null-separated list of workspace names */
char names[MAX_WORKSPACES * 32];
int offset = 0;
@ -270,7 +250,6 @@ void atoms_set_number_of_desktops(int count)
(unsigned char *)&data, 1);
}
/* ========== Window property helpers ========== */
bool atoms_get_window_type(Window window, Atom *type)
{
@ -343,7 +322,6 @@ char *atoms_get_window_name(Window window)
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,
@ -353,7 +331,6 @@ char *atoms_get_window_name(Window window)
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,
@ -363,7 +340,6 @@ char *atoms_get_window_name(Window window)
return name;
}
/* Last resort: XFetchName */
char *name = NULL;
if (XFetchName(dpy, window, &name) && name != NULL) {
char *result = dwn_strdup(name);
@ -398,7 +374,6 @@ bool atoms_get_wm_class(Window window, char *class_name, char *instance_name, si
return false;
}
/* ========== Protocol helpers ========== */
bool atoms_window_supports_protocol(Window window, Atom protocol)
{

View File

@ -17,11 +17,9 @@
#include <unistd.h>
#include <X11/Xresource.h>
/* ========== Client creation/destruction ========== */
Client *client_create(Window window)
{
/* Defensive: validate global state before proceeding */
if (dwn == NULL || dwn->display == NULL) {
LOG_ERROR("client_create: dwn or display is NULL");
return NULL;
@ -46,7 +44,6 @@ Client *client_create(Window window)
client->next = NULL;
client->prev = NULL;
/* Get initial geometry from window */
XWindowAttributes wa;
int orig_width = 640, orig_height = 480;
if (XGetWindowAttributes(dwn->display, window, &wa)) {
@ -54,7 +51,6 @@ Client *client_create(Window window)
orig_height = wa.height;
}
/* Calculate work area (account for panels) */
int work_x = 0;
int work_y = 0;
int work_width = dwn->screen_width;
@ -70,28 +66,23 @@ Client *client_create(Window window)
}
}
/* Set size to 75% of work area or original size, whichever is larger */
int target_width = (work_width * 75) / 100;
int target_height = (work_height * 75) / 100;
client->width = (orig_width > target_width) ? orig_width : target_width;
client->height = (orig_height > target_height) ? orig_height : target_height;
/* Clamp to work area */
if (client->width > work_width - 20) client->width = work_width - 20;
if (client->height > work_height - 20) client->height = work_height - 20;
/* Center on screen */
client->x = work_x + (work_width - client->width) / 2;
client->y = work_y + (work_height - client->height) / 2;
/* Save original geometry for floating restore */
client->old_x = client->x;
client->old_y = client->y;
client->old_width = client->width;
client->old_height = client->height;
/* Update properties */
client_update_title(client);
client_update_class(client);
@ -111,13 +102,11 @@ void client_destroy(Client *client)
dwn_free(client);
}
/* Sync log for debugging - disabled in production */
static inline void client_sync_log(const char *msg)
{
(void)msg; /* No-op - enable for debugging */
(void)msg;
}
/* ========== Client management ========== */
Client *client_manage(Window window)
{
@ -128,14 +117,12 @@ Client *client_manage(Window window)
return NULL;
}
/* Check if already managed */
Client *existing = client_find_by_window(window);
if (existing != NULL) {
client_sync_log("client_manage: already managed");
return existing;
}
/* Check window type - don't manage docks, desktops */
if (client_is_dock(window) || client_is_desktop(window)) {
LOG_DEBUG("Skipping dock/desktop window: %lu", window);
client_sync_log("client_manage: skip dock/desktop");
@ -145,7 +132,6 @@ Client *client_manage(Window window)
LOG_DEBUG("Managing window: %lu", window);
client_sync_log("client_manage: creating client");
/* Create client */
Client *client = client_create(window);
if (client == NULL) {
LOG_ERROR("client_manage: failed to create client for window %lu", window);
@ -155,19 +141,16 @@ Client *client_manage(Window window)
client_sync_log("client_manage: checking dialog");
/* Check if it should be floating (dialogs, etc.) */
if (client_is_dialog(window)) {
client->flags |= CLIENT_FLOATING;
}
client_sync_log("client_manage: creating frame");
/* Create frame with decorations */
client_create_frame(client);
client_sync_log("client_manage: verifying window");
/* Verify window still exists before reparenting */
XWindowAttributes wa;
if (!XGetWindowAttributes(dwn->display, window, &wa)) {
LOG_WARN("client_manage: window %lu disappeared before reparenting", window);
@ -179,9 +162,8 @@ Client *client_manage(Window window)
client_sync_log("client_manage: reparenting");
client_reparent_to_frame(client);
/* Verify both window AND frame still exist after reparent */
client_sync_log("client_manage: verifying after reparent");
XSync(dwn->display, False); /* Flush any pending errors */
XSync(dwn->display, False);
if (client->frame == None) {
client_sync_log("client_manage: frame is None after reparent");
@ -207,24 +189,19 @@ Client *client_manage(Window window)
}
client_sync_log("client_manage: configuring");
/* Resize client window to match frame dimensions */
client_configure(client);
client_sync_log("client_manage: adding to list");
/* Add to client list */
client_add_to_list(client);
client_sync_log("client_manage: adding to workspace");
/* Add to current workspace */
workspace_add_client(dwn->current_workspace, client);
client_sync_log("client_manage: setting EWMH");
/* Set EWMH properties */
atoms_set_window_desktop(window, client->workspace);
atoms_update_client_list();
client_sync_log("client_manage: verifying window again");
/* Verify window still exists before subscribing to events */
if (!XGetWindowAttributes(dwn->display, window, &wa)) {
LOG_WARN("client_manage: window %lu disappeared before event setup", window);
client_sync_log("client_manage: window gone before events");
@ -236,22 +213,18 @@ Client *client_manage(Window window)
}
client_sync_log("client_manage: selecting input");
/* Subscribe to window events */
XSelectInput(dwn->display, window,
EnterWindowMask | FocusChangeMask | PropertyChangeMask |
StructureNotifyMask);
client_sync_log("client_manage: grabbing button");
/* Grab button for click-to-focus (will replay event to app after focusing) */
XGrabButton(dwn->display, Button1, AnyModifier, window, False,
ButtonPressMask, GrabModeSync, GrabModeAsync, None, None);
client_sync_log("client_manage: syncing X");
/* Sync to ensure all operations completed */
XSync(dwn->display, False);
client_sync_log("client_manage: showing");
/* Map and focus */
client_show(client);
client_sync_log("client_manage: focusing");
@ -273,27 +246,21 @@ void client_unmanage(Client *client)
LOG_DEBUG("Unmanaging window: %lu", client->window);
client_sync_log("client_unmanage: remove from workspace");
/* Remove from workspace */
workspace_remove_client(client->workspace, client);
client_sync_log("client_unmanage: remove from list");
/* Remove from client list */
client_remove_from_list(client);
client_sync_log("client_unmanage: reparent from frame");
/* Reparent back to root */
client_reparent_from_frame(client);
client_sync_log("client_unmanage: update EWMH");
/* Update EWMH */
atoms_update_client_list();
client_sync_log("client_unmanage: destroy client");
/* Destroy client */
client_destroy(client);
client_sync_log("client_unmanage: focus next");
/* Focus next client if needed */
Workspace *ws = workspace_get_current();
if (ws != NULL && ws->focused == NULL) {
Client *next = workspace_get_first_client(dwn->current_workspace);
@ -333,7 +300,6 @@ Client *client_find_by_frame(Window frame)
return NULL;
}
/* ========== Client state ========== */
void client_focus(Client *client)
{
@ -344,14 +310,12 @@ void client_focus(Client *client)
return;
}
/* Defensive: verify window is still valid */
if (client->window == None) {
LOG_WARN("client_focus: client has invalid window (None)");
client_sync_log("client_focus: window is None");
return;
}
/* Verify window still exists before focusing */
XWindowAttributes wa;
if (!XGetWindowAttributes(dwn->display, client->window, &wa)) {
LOG_WARN("client_focus: window %lu no longer exists", client->window);
@ -361,7 +325,6 @@ void client_focus(Client *client)
client_sync_log("client_focus: unfocusing previous");
/* Unfocus previous */
Workspace *ws = workspace_get(client->workspace);
if (ws != NULL && ws->focused != NULL && ws->focused != client) {
client_unfocus(ws->focused);
@ -369,30 +332,25 @@ void client_focus(Client *client)
client_sync_log("client_focus: XSetInputFocus");
/* Set focus with error handling */
XSetInputFocus(dwn->display, client->window, RevertToPointerRoot, CurrentTime);
XSync(dwn->display, False); /* Catch any X errors immediately */
XSync(dwn->display, False);
client_sync_log("client_focus: updating workspace");
/* Update workspace */
if (ws != NULL) {
ws->focused = client;
}
client_sync_log("client_focus: raising");
/* Raise window */
client_raise(client);
client_sync_log("client_focus: decorations");
/* Update decorations */
decorations_render(client, true);
client_sync_log("client_focus: EWMH");
/* Update EWMH */
atoms_set_active_window(client->window);
client_sync_log("client_focus: DONE");
@ -405,7 +363,6 @@ void client_unfocus(Client *client)
return;
}
/* Update decorations */
decorations_render(client, false);
}
@ -426,7 +383,6 @@ void client_raise(Client *client)
XRaiseWindow(dwn->display, win);
/* Keep notifications on top */
notifications_raise_all();
}
@ -468,7 +424,6 @@ void client_restore(Client *client)
client_focus(client);
}
/* ========== Client geometry ========== */
void client_move(Client *client, int x, int y)
{
@ -517,7 +472,6 @@ void client_configure(Client *client)
return;
}
/* Verify window still exists before configuring */
XWindowAttributes wa;
if (!XGetWindowAttributes(dwn->display, client->window, &wa)) {
LOG_DEBUG("client_configure: window no longer exists");
@ -529,14 +483,12 @@ void client_configure(Client *client)
int border = client->border_width;
if (client->frame != None) {
/* Move/resize frame */
XMoveResizeWindow(dwn->display, client->frame,
client->x - border,
client->y - title_height - border,
client->width + 2 * border,
client->height + title_height + 2 * border);
/* Move/resize client window within frame */
XMoveResizeWindow(dwn->display, client->window,
border, title_height + border,
client->width, client->height);
@ -546,9 +498,8 @@ void client_configure(Client *client)
client->width, client->height);
}
/* Send configure notify to client */
XConfigureEvent ce;
memset(&ce, 0, sizeof(ce)); /* Zero-initialize */
memset(&ce, 0, sizeof(ce));
ce.type = ConfigureNotify;
ce.event = client->window;
ce.window = client->window;
@ -576,19 +527,16 @@ void client_apply_size_hints(Client *client, int *width, int *height)
return;
}
/* Minimum size */
if (hints.flags & PMinSize) {
if (*width < hints.min_width) *width = hints.min_width;
if (*height < hints.min_height) *height = hints.min_height;
}
/* Maximum size */
if (hints.flags & PMaxSize) {
if (*width > hints.max_width) *width = hints.max_width;
if (*height > hints.max_height) *height = hints.max_height;
}
/* Size increments */
if (hints.flags & PResizeInc) {
int base_w = (hints.flags & PBaseSize) ? hints.base_width : 0;
int base_h = (hints.flags & PBaseSize) ? hints.base_height : 0;
@ -598,7 +546,6 @@ void client_apply_size_hints(Client *client, int *width, int *height)
}
}
/* ========== Client properties ========== */
void client_update_title(Client *client)
{
@ -615,7 +562,6 @@ void client_update_title(Client *client)
strncpy(client->title, "Untitled", sizeof(client->title) - 1);
}
/* Redraw decorations */
if (client->frame != None) {
Workspace *ws = workspace_get(client->workspace);
bool focused = (ws != NULL && ws->focused == client);
@ -638,7 +584,6 @@ void client_set_fullscreen(Client *client, bool fullscreen)
return;
}
/* Verify window still exists */
if (client->window == None) {
return;
}
@ -650,7 +595,6 @@ void client_set_fullscreen(Client *client, bool fullscreen)
}
if (fullscreen) {
/* Save current geometry */
if (!(client->flags & CLIENT_FULLSCREEN)) {
client->old_x = client->x;
client->old_y = client->y;
@ -660,13 +604,11 @@ void client_set_fullscreen(Client *client, bool fullscreen)
client->flags |= CLIENT_FULLSCREEN;
/* Get monitor dimensions */
client->x = 0;
client->y = 0;
client->width = dwn->screen_width;
client->height = dwn->screen_height;
/* Reparent window to root and hide frame */
if (client->frame != None) {
XUnmapWindow(dwn->display, client->frame);
XSync(dwn->display, False);
@ -680,13 +622,11 @@ void client_set_fullscreen(Client *client, bool fullscreen)
} else {
client->flags &= ~CLIENT_FULLSCREEN;
/* Restore geometry */
client->x = client->old_x;
client->y = client->old_y;
client->width = client->old_width;
client->height = client->old_height;
/* Reparent back to frame and show it */
if (client->frame != None) {
int title_height = config_get_title_height();
int border = client->border_width;
@ -729,7 +669,6 @@ void client_toggle_floating(Client *client)
client_set_floating(client, !(client->flags & CLIENT_FLOATING));
}
/* ========== Window type checking ========== */
bool client_is_floating(Client *client)
{
@ -753,7 +692,6 @@ bool client_is_dialog(Window window)
return type == ewmh.NET_WM_WINDOW_TYPE_DIALOG;
}
/* Check transient hint */
Window transient_for = None;
if (XGetTransientForHint(dwn->display, window, &transient_for)) {
return transient_for != None;
@ -780,7 +718,6 @@ bool client_is_desktop(Window window)
return false;
}
/* ========== Frame management ========== */
void client_create_frame(Client *client)
{
@ -793,7 +730,6 @@ void client_create_frame(Client *client)
return;
}
/* Defensive: verify client window is valid */
if (client->window == None) {
LOG_ERROR("client_create_frame: client has no valid window");
return;
@ -802,7 +738,6 @@ void client_create_frame(Client *client)
int title_height = config_get_title_height();
int border = client->border_width;
/* Ensure reasonable dimensions */
int frame_width = client->width + 2 * border;
int frame_height = client->height + title_height + 2 * border;
if (frame_width <= 0 || frame_height <= 0) {
@ -810,9 +745,8 @@ void client_create_frame(Client *client)
return;
}
/* Create frame window */
XSetWindowAttributes swa;
memset(&swa, 0, sizeof(swa)); /* Defensive: zero-initialize */
memset(&swa, 0, sizeof(swa));
swa.override_redirect = True;
swa.background_pixel = dwn->config->colors.title_unfocused_bg;
swa.border_pixel = dwn->config->colors.border_unfocused;
@ -830,14 +764,13 @@ void client_create_frame(Client *client)
CWOverrideRedirect | CWBackPixel | CWBorderPixel | CWEventMask,
&swa);
/* Verify frame was created successfully */
if (client->frame == None) {
LOG_ERROR("client_create_frame: XCreateWindow failed for window %lu", client->window);
return;
}
XSetWindowBorderWidth(dwn->display, client->frame, border);
XSync(dwn->display, False); /* Catch any X errors */
XSync(dwn->display, False);
LOG_DEBUG("Created frame %lu for window %lu", client->frame, client->window);
}
@ -862,7 +795,6 @@ void client_reparent_to_frame(Client *client)
return;
}
/* Verify window still exists before reparenting */
if (client->window == None) {
LOG_WARN("client_reparent_to_frame: client has no valid window");
return;
@ -877,16 +809,13 @@ void client_reparent_to_frame(Client *client)
int title_height = config_get_title_height();
int border = client->border_width;
/* Sync before reparent to catch any pending errors */
XSync(dwn->display, False);
XReparentWindow(dwn->display, client->window, client->frame,
border, title_height + border);
/* Sync after reparent to catch errors immediately */
XSync(dwn->display, False);
/* Save the frame window association */
XSaveContext(dwn->display, client->window, XUniqueContext(), (XPointer)client);
}
@ -900,24 +829,20 @@ void client_reparent_from_frame(Client *client)
return;
}
/* Verify window still exists before reparenting */
XWindowAttributes wa;
if (!XGetWindowAttributes(dwn->display, client->window, &wa)) {
LOG_WARN("client_reparent_from_frame: window %lu no longer exists", client->window);
return;
}
/* Sync before reparent to catch any pending errors */
XSync(dwn->display, False);
XReparentWindow(dwn->display, client->window, dwn->root,
client->x, client->y);
/* Sync after reparent to catch errors immediately */
XSync(dwn->display, False);
}
/* ========== Visibility ========== */
void client_show(Client *client)
{
@ -929,7 +854,6 @@ void client_show(Client *client)
return;
}
/* Verify window still exists before mapping */
XWindowAttributes wa;
if (!XGetWindowAttributes(dwn->display, client->window, &wa)) {
LOG_DEBUG("client_show: window no longer exists");
@ -952,7 +876,6 @@ void client_hide(Client *client)
return;
}
/* Verify window still exists before unmapping */
XWindowAttributes wa;
if (!XGetWindowAttributes(dwn->display, client->window, &wa)) {
LOG_DEBUG("client_hide: window no longer exists");
@ -971,12 +894,10 @@ bool client_is_visible(Client *client)
return false;
}
/* Hidden windows are not visible */
if (client->flags & CLIENT_MINIMIZED) {
return false;
}
/* Only visible if on current workspace (or sticky) */
if (!(client->flags & CLIENT_STICKY) &&
client->workspace != (unsigned int)dwn->current_workspace) {
return false;
@ -985,7 +906,6 @@ bool client_is_visible(Client *client)
return true;
}
/* ========== Close handling ========== */
void client_close(Client *client)
{
@ -997,18 +917,15 @@ void client_close(Client *client)
return;
}
/* Verify window still exists */
XWindowAttributes wa;
if (!XGetWindowAttributes(dwn->display, client->window, &wa)) {
LOG_DEBUG("client_close: window no longer exists");
return;
}
/* Try WM_DELETE_WINDOW first */
if (atoms_window_supports_protocol(client->window, icccm.WM_DELETE_WINDOW)) {
atoms_send_protocol(client->window, icccm.WM_DELETE_WINDOW, CurrentTime);
} else {
/* Force kill */
client_kill(client);
}
}
@ -1026,7 +943,6 @@ void client_kill(Client *client)
XKillClient(dwn->display, client->window);
}
/* ========== List operations ========== */
void client_add_to_list(Client *client)
{
@ -1080,7 +996,6 @@ int client_count_on_workspace(int workspace)
return count;
}
/* ========== Iteration ========== */
Client *client_get_next(Client *client)
{

View File

@ -12,7 +12,6 @@
#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"
@ -27,7 +26,6 @@
#define DEFAULT_NOTIFICATION_BG "#2a2a3e"
#define DEFAULT_NOTIFICATION_FG "#ffffff"
/* ========== Configuration creation/destruction ========== */
Config *config_create(void)
{
@ -39,7 +37,6 @@ Config *config_create(void)
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);
@ -52,41 +49,34 @@ void config_set_defaults(Config *cfg)
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;
@ -181,12 +171,10 @@ bool config_parse_ini(const char *path, ConfigCallback callback, void *user_data
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) {
@ -196,14 +184,12 @@ bool config_parse_ini(const char *path, ConfigCallback callback, void *user_data
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] == '\''))) {
@ -219,7 +205,6 @@ bool config_parse_ini(const char *path, ConfigCallback callback, void *user_data
return true;
}
/* ========== Load configuration ========== */
bool config_load(Config *cfg, const char *path)
{
@ -227,16 +212,13 @@ bool config_load(Config *cfg, const char *path)
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);
@ -249,7 +231,6 @@ bool config_load(Config *cfg, const char *path)
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);
@ -262,7 +243,6 @@ bool config_load(Config *cfg, const char *path)
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");
}
@ -275,13 +255,11 @@ 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)
{
@ -339,7 +317,6 @@ const ColorScheme *config_get_colors(void)
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) {

View File

@ -13,14 +13,11 @@
#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)
{
@ -29,10 +26,8 @@ void decorations_init(void)
void decorations_cleanup(void)
{
/* Nothing to clean up */
}
/* ========== Rendering ========== */
void decorations_render(Client *client, bool focused)
{
@ -64,25 +59,21 @@ void decorations_render_title_bar(Client *client, bool focused)
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 "..." */
strncpy(display_title, client->title, sizeof(display_title) - 4);
display_title[sizeof(display_title) - 4] = '\0';
XGlyphInfo extents;
@ -90,16 +81,14 @@ void decorations_render_title_bar(Client *client, bool focused)
(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 */
cut--;
}
if (cut > 0) cut--; /* Remove one more character for ellipsis space */
if (cut > 0) cut--;
while (cut > 0 && (display_title[cut] & 0xC0) == 0x80) {
cut--;
}
@ -114,7 +103,6 @@ void decorations_render_title_bar(Client *client, bool focused)
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);
@ -155,7 +143,6 @@ void decorations_render_buttons(Client *client, bool focused)
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;
@ -163,10 +150,9 @@ void decorations_render_buttons(Client *client, bool focused)
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 */
XSetForeground(dpy, dwn->gc, 0xcc4444);
XDrawLine(dpy, client->frame, dwn->gc,
close_x + 3, button_y + 3,
close_x + BUTTON_SIZE - 4, button_y + BUTTON_SIZE - 4);
@ -174,18 +160,16 @@ void decorations_render_buttons(Client *client, bool focused)
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 */
XSetForeground(dpy, dwn->gc, 0x44cc44);
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 */
XSetForeground(dpy, dwn->gc, 0xcccc44);
XDrawLine(dpy, client->frame, dwn->gc,
min_x + 3, button_y + BUTTON_SIZE - 5,
min_x + BUTTON_SIZE - 4, button_y + BUTTON_SIZE - 5);
@ -207,18 +191,16 @@ void decorations_render_border(Client *client, bool focused)
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 */
return BUTTON_COUNT;
}
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;
}
@ -228,19 +210,16 @@ ButtonType decorations_hit_test_button(Client *client, int x, int y)
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;
@ -258,7 +237,6 @@ bool decorations_hit_test_title_bar(Client *client, int x, int y)
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;
@ -280,7 +258,6 @@ bool decorations_hit_test_resize_area(Client *client, int x, int y, int *directi
*direction = 0;
/* Check edges */
bool left = (x < RESIZE_EDGE);
bool right = (x > frame_width - RESIZE_EDGE);
bool top = (y < RESIZE_EDGE);
@ -290,7 +267,6 @@ bool decorations_hit_test_resize_area(Client *client, int x, int y, int *directi
return false;
}
/* Encode direction as bitmask */
if (left) *direction |= 1;
if (right) *direction |= 2;
if (top) *direction |= 4;
@ -299,7 +275,6 @@ bool decorations_hit_test_resize_area(Client *client, int x, int y, int *directi
return true;
}
/* ========== Button actions ========== */
void decorations_button_press(Client *client, ButtonType button)
{
@ -328,7 +303,6 @@ void decorations_button_press(Client *client, ButtonType button)
}
}
/* ========== Text rendering ========== */
void decorations_draw_text(Window window, GC gc, int x, int y,
const char *text, unsigned long color)
@ -337,7 +311,6 @@ void decorations_draw_text(Window window, GC gc, int x, int y,
return;
}
/* Use Xft for UTF-8 support */
if (dwn->xft_font != NULL) {
XftDraw *xft_draw = XftDrawCreate(dwn->display, window,
DefaultVisual(dwn->display, dwn->screen),
@ -363,7 +336,6 @@ void decorations_draw_text(Window window, GC gc, int x, int y,
}
}
/* Fallback to legacy X11 text */
XSetForeground(dwn->display, gc, color);
XDrawString(dwn->display, window, gc, x, y, text, strlen(text));
}

View File

@ -22,7 +22,6 @@
static bool super_pressed = false;
static bool super_used_in_combo = false;
/* Forward declarations for key callbacks */
void key_spawn_terminal(void);
void key_spawn_launcher(void);
void key_spawn_file_manager(void);
@ -69,11 +68,9 @@ 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;
@ -91,7 +88,6 @@ static const TutorialStep tutorial_steps[] = {
"(Super = Windows/Meta key)",
MOD_SUPER, XK_t
},
/* === Applications === */
{
"1/20: Open Terminal",
"The terminal is your command center.\n\n"
@ -120,7 +116,6 @@ static const TutorialStep tutorial_steps[] = {
"",
MOD_SUPER, XK_b
},
/* === Window Management === */
{
"5/20: Switch Windows",
"Cycle through open windows.\n\n"
@ -156,7 +151,6 @@ static const TutorialStep tutorial_steps[] = {
"",
MOD_SUPER, XK_F9
},
/* === Workspaces === */
{
"10/20: Next Workspace",
"Switch to the next virtual desktop.\n\n"
@ -178,7 +172,6 @@ static const TutorialStep tutorial_steps[] = {
"Use F1-F9 for workspaces 1-9",
0, XK_F1
},
/* === Layout === */
{
"13/20: Cycle Layout",
"Switch between tiling, floating, monocle.\n\n"
@ -207,7 +200,6 @@ static const TutorialStep tutorial_steps[] = {
"",
MOD_SUPER, XK_i
},
/* === AI Features === */
{
"17/20: AI Context",
"Show AI analysis of your current task.\n\n"
@ -229,7 +221,6 @@ static const TutorialStep tutorial_steps[] = {
"Requires EXA_API_KEY",
MOD_SUPER | MOD_SHIFT, XK_e
},
/* === Help & System === */
{
"20/20: Show Shortcuts",
"Display all keyboard shortcuts.\n\n"
@ -266,7 +257,7 @@ void tutorial_start(void)
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 */
notification_show("DWN Tutorial", step->title, msg, NULL, 0);
}
void tutorial_stop(void)
@ -301,14 +292,11 @@ void tutorial_check_key(unsigned int modifiers, KeySym keysym)
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)
{
@ -336,10 +324,8 @@ void keys_grab_all(void)
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) {
@ -347,11 +333,10 @@ void keys_grab_all(void)
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,
bindings[i].modifiers | LockMask,
bindings[i].modifiers | Mod2Mask | LockMask
};
@ -382,7 +367,6 @@ void keys_ungrab_all(void)
XUngrabKey(dwn->display, AnyKey, AnyModifier, dwn->root);
}
/* ========== Key binding registration ========== */
void keys_bind(unsigned int modifiers, KeySym keysym,
KeyCallback callback, const char *description)
@ -404,7 +388,6 @@ 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--;
@ -419,7 +402,6 @@ void keys_clear_all(void)
memset(bindings, 0, sizeof(bindings));
}
/* ========== Key event handling ========== */
void keys_handle_press(XKeyEvent *ev)
{
@ -439,10 +421,8 @@ void keys_handle_press(XKeyEvent *ev)
super_used_in_combo = true;
}
/* 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);
}
@ -476,45 +456,32 @@ void keys_handle_release(XKeyEvent *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");
@ -525,7 +492,6 @@ void keys_setup_defaults(void)
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");
@ -536,48 +502,34 @@ void keys_setup_defaults(void)
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)
{
@ -647,8 +599,6 @@ 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);
}
}
@ -670,7 +620,7 @@ void key_workspace_next(void)
}
int next = dwn->current_workspace + 1;
if (next >= MAX_WORKSPACES) {
next = 0; /* Wrap around */
next = 0;
}
workspace_switch(next);
}
@ -682,12 +632,11 @@ void key_workspace_prev(void)
}
int prev = dwn->current_workspace - 1;
if (prev < 0) {
prev = MAX_WORKSPACES - 1; /* Wrap around */
prev = MAX_WORKSPACES - 1;
}
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); }
@ -698,7 +647,6 @@ 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();
@ -717,7 +665,6 @@ 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) {
@ -750,7 +697,6 @@ void key_decrease_master_count(void)
workspace_adjust_master_count(dwn->current_workspace, -1);
}
/* AI functions */
void key_toggle_ai(void)
{
if (dwn == NULL) {
@ -764,7 +710,6 @@ void key_toggle_ai(void)
return;
}
/* Update and show AI context */
ai_update_context();
const char *task = ai_analyze_task();
const char *suggestion = ai_suggest_window();
@ -850,14 +795,12 @@ void key_show_shortcuts(void)
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)
{

View File

@ -10,7 +10,6 @@
#include "config.h"
#include "util.h"
/* Layout names and symbols */
static const char *layout_names[] = {
"Tiling",
"Floating",
@ -23,7 +22,6 @@ static const char *layout_symbols[] = {
"[M]"
};
/* ========== Main arrangement function ========== */
void layout_arrange(int workspace)
{
@ -48,7 +46,6 @@ void layout_arrange(int workspace)
}
}
/* ========== Tiling layout ========== */
void layout_arrange_tiling(int workspace)
{
@ -57,7 +54,6 @@ void layout_arrange_tiling(int workspace)
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);
@ -65,7 +61,6 @@ void layout_arrange_tiling(int workspace)
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;
@ -76,7 +71,6 @@ void layout_arrange_tiling(int workspace)
master_count = n;
}
/* Calculate master area width */
int master_width;
if (n <= master_count) {
master_width = area_width - 2 * gap;
@ -86,7 +80,6 @@ void layout_arrange_tiling(int workspace)
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;
@ -102,7 +95,6 @@ void layout_arrange_tiling(int workspace)
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;
@ -112,7 +104,6 @@ void layout_arrange_tiling(int workspace)
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;
@ -124,7 +115,6 @@ void layout_arrange_tiling(int workspace)
stack_y += h + gap;
}
/* Account for decorations */
int actual_h = h - title_height - 2 * border;
int actual_w = w - 2 * border;
@ -138,12 +128,9 @@ void layout_arrange_tiling(int workspace)
}
}
/* ========== 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) {
@ -153,7 +140,6 @@ void layout_arrange_floating(int workspace)
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);
@ -170,11 +156,9 @@ void layout_arrange_floating(int workspace)
}
}
/* ========== 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);
@ -190,7 +174,6 @@ void layout_arrange_monocle(int workspace)
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;
@ -203,7 +186,6 @@ void layout_arrange_monocle(int workspace)
}
}
/* ========== Helpers ========== */
int layout_get_usable_area(int *x, int *y, int *width, int *height)
{

View File

@ -30,11 +30,9 @@
#include <X11/cursorfont.h>
#include <X11/extensions/Xinerama.h>
/* Global state instance */
DWNState *dwn = NULL;
static DWNState dwn_state;
/* Signal handling */
static volatile sig_atomic_t received_signal = 0;
static void signal_handler(int sig)
@ -42,29 +40,23 @@ static void signal_handler(int sig)
received_signal = sig;
}
/* Crash signal handler - flush logs before dying */
static void crash_signal_handler(int sig)
{
/* Flush logs synchronously before crashing */
log_flush();
/* Re-raise the signal with default handler to get proper crash behavior */
signal(sig, SIG_DFL);
raise(sig);
}
/* X11 error handlers */
static int last_x_error = 0; /* Track last error for checking */
static int last_x_error = 0;
static int x_error_handler(Display *dpy, XErrorEvent *ev)
{
char error_text[256];
XGetErrorText(dpy, ev->error_code, error_text, sizeof(error_text));
/* Store last error code for functions that want to check */
last_x_error = ev->error_code;
/* Write all X errors to crash log for debugging */
FILE *crash = fopen("/tmp/dwn_crash.log", "a");
if (crash) {
fprintf(crash, "[X_ERROR] code=%d request=%d resource=%lu: %s\n",
@ -74,22 +66,19 @@ static int x_error_handler(Display *dpy, XErrorEvent *ev)
fclose(crash);
}
/* BadWindow errors are common and recoverable - just log and continue */
if (ev->error_code == BadWindow) {
LOG_DEBUG("X11 BadWindow error (request %d, resource %lu) - window was destroyed",
ev->request_code, ev->resourceid);
return 0; /* Continue - this is not fatal */
return 0;
}
/* BadMatch, BadValue, BadDrawable are also often recoverable */
if (ev->error_code == BadMatch || ev->error_code == BadValue ||
ev->error_code == BadDrawable || ev->error_code == BadPixmap) {
LOG_WARN("X11 error: %s (request %d, resource %lu) - continuing",
error_text, ev->request_code, ev->resourceid);
return 0; /* Continue - these are recoverable */
return 0;
}
/* Log other errors but don't crash */
LOG_WARN("X11 error: %s (request %d, resource %lu)",
error_text, ev->request_code, ev->resourceid);
return 0;
@ -99,7 +88,6 @@ static int x_io_error_handler(Display *dpy)
{
(void)dpy;
/* Write directly to crash log - do not rely on async logging */
FILE *crash = fopen("/tmp/dwn_crash.log", "a");
if (crash) {
fprintf(crash, "[IO_ERROR] Fatal X11 I/O error - X server connection lost\n");
@ -110,16 +98,13 @@ static int x_io_error_handler(Display *dpy)
fprintf(stderr, "[IO_ERROR] Fatal X11 I/O error - X server connection lost\n");
fflush(stderr);
/* I/O errors mean the X server connection is broken - we must exit */
/* But first, try to flush any pending logs */
log_flush();
LOG_ERROR("Fatal X11 I/O error - X server connection lost");
log_flush();
_exit(EXIT_FAILURE); /* Use _exit to avoid cleanup that might touch X */
return 0; /* Never reached */
_exit(EXIT_FAILURE);
return 0;
}
/* Check if another WM is running */
static int wm_detected = 0;
static int wm_detect_error_handler(Display *dpy, XErrorEvent *ev)
@ -143,11 +128,9 @@ static bool check_other_wm(void)
return wm_detected != 0;
}
/* Initialize multi-monitor support */
static void init_monitors(void)
{
if (!XineramaIsActive(dwn->display)) {
/* Single monitor */
dwn->monitors[0].x = 0;
dwn->monitors[0].y = 0;
dwn->monitors[0].width = dwn->screen_width;
@ -188,7 +171,6 @@ static void init_monitors(void)
LOG_INFO("Detected %d monitor(s)", dwn->monitor_count);
}
/* Scan for existing windows */
static void scan_existing_windows(void)
{
Window root_return, parent_return;
@ -213,11 +195,9 @@ static void scan_existing_windows(void)
LOG_INFO("Scanned %d existing window(s)", client_count());
}
/* ========== Event handlers ========== */
static void handle_map_request(XMapRequestEvent *ev)
{
/* Defensive: validate pointers */
if (ev == NULL || dwn == NULL || dwn->display == NULL) {
return;
}
@ -242,7 +222,6 @@ static void handle_map_request(XMapRequestEvent *ev)
static void handle_unmap_notify(XUnmapEvent *ev)
{
/* Defensive: validate pointers */
if (ev == NULL || dwn == NULL) {
return;
}
@ -252,15 +231,10 @@ static void handle_unmap_notify(XUnmapEvent *ev)
return;
}
/* Ignore synthetic events */
if (ev->send_event) {
return;
}
/* Don't unmanage windows that are intentionally hidden:
* - Windows on a different workspace (hidden during workspace switch)
* - Minimized windows
* These windows are still managed, just not visible */
if (c->workspace != (unsigned int)dwn->current_workspace) {
return;
}
@ -268,13 +242,11 @@ static void handle_unmap_notify(XUnmapEvent *ev)
return;
}
/* Window was actually closed/withdrawn - unmanage it */
client_unmanage(c);
}
static void handle_destroy_notify(XDestroyWindowEvent *ev)
{
/* Defensive: validate pointers */
if (ev == NULL) {
return;
}
@ -287,7 +259,6 @@ static void handle_destroy_notify(XDestroyWindowEvent *ev)
static void handle_configure_request(XConfigureRequestEvent *ev)
{
/* Defensive: validate pointers */
if (ev == NULL || dwn == NULL || dwn->display == NULL) {
return;
}
@ -295,7 +266,6 @@ static void handle_configure_request(XConfigureRequestEvent *ev)
Client *c = client_find_by_window(ev->window);
if (c != NULL) {
/* Managed window - respect some requests for floating windows */
if (client_is_floating(c) || client_is_fullscreen(c)) {
if (ev->value_mask & CWX) c->x = ev->x;
if (ev->value_mask & CWY) c->y = ev->y;
@ -303,11 +273,9 @@ static void handle_configure_request(XConfigureRequestEvent *ev)
if (ev->value_mask & CWHeight) c->height = ev->height;
client_configure(c);
} else {
/* Just send configure notify with current geometry */
client_configure(c);
}
} else {
/* Unmanaged window - pass through */
XWindowChanges wc;
wc.x = ev->x;
wc.y = ev->y;
@ -323,7 +291,6 @@ static void handle_configure_request(XConfigureRequestEvent *ev)
static void handle_property_notify(XPropertyEvent *ev)
{
/* Defensive: validate pointers */
if (ev == NULL || dwn == NULL) {
return;
}
@ -341,17 +308,14 @@ static void handle_property_notify(XPropertyEvent *ev)
static void handle_expose(XExposeEvent *ev)
{
/* Defensive: validate pointers */
if (ev == NULL || dwn == NULL) {
return;
}
/* Only handle final expose in a sequence */
if (ev->count != 0) {
return;
}
/* Check if it's a panel */
if (dwn->top_panel != NULL && ev->window == dwn->top_panel->window) {
panel_render(dwn->top_panel);
return;
@ -361,14 +325,12 @@ static void handle_expose(XExposeEvent *ev)
return;
}
/* Check if it's a notification */
Notification *notif = notification_find_by_window(ev->window);
if (notif != NULL) {
notification_render(notif);
return;
}
/* Check if it's a frame */
Client *c = client_find_by_frame(ev->window);
if (c != NULL) {
Workspace *ws = workspace_get(c->workspace);
@ -379,7 +341,6 @@ static void handle_expose(XExposeEvent *ev)
static void handle_enter_notify(XCrossingEvent *ev)
{
/* Defensive: validate all pointers */
if (ev == NULL || dwn == NULL || dwn->config == NULL) {
return;
}
@ -388,7 +349,6 @@ static void handle_enter_notify(XCrossingEvent *ev)
return;
}
/* Focus on enter for follow-mouse mode */
Client *c = client_find_by_frame(ev->window);
if (c == NULL) {
c = client_find_by_window(ev->window);
@ -401,41 +361,34 @@ static void handle_enter_notify(XCrossingEvent *ev)
static void handle_button_press(XButtonEvent *ev)
{
/* Defensive: validate all pointers */
if (ev == NULL || dwn == NULL || dwn->display == NULL) {
return;
}
/* Check volume slider first */
if (volume_slider != NULL && volume_slider->visible) {
if (ev->window == volume_slider->window) {
volume_slider_handle_click(volume_slider, ev->x, ev->y);
return;
} else {
/* Clicked outside slider - close it */
volume_slider_hide(volume_slider);
}
}
/* Check WiFi dropdown menu */
if (wifi_menu != NULL && wifi_menu->visible) {
if (ev->window == wifi_menu->window) {
dropdown_handle_click(wifi_menu, ev->x, ev->y);
return;
} else {
/* Clicked outside dropdown - close it */
dropdown_hide(wifi_menu);
}
}
/* Check notifications first - clicking dismisses them */
Notification *notif = notification_find_by_window(ev->window);
if (notif != NULL) {
notification_close(notif->id);
return;
}
/* Check panels first */
if (dwn->top_panel != NULL && ev->window == dwn->top_panel->window) {
panel_handle_click(dwn->top_panel, ev->x, ev->y, ev->button);
return;
@ -445,7 +398,6 @@ static void handle_button_press(XButtonEvent *ev)
return;
}
/* Find client */
Client *c = client_find_by_frame(ev->window);
bool is_client_window = false;
if (c == NULL) {
@ -457,20 +409,14 @@ static void handle_button_press(XButtonEvent *ev)
return;
}
/* If click was on client window content, replay the event to the application FIRST
* before any other X operations. The synchronous grab freezes pointer events until
* XAllowEvents is called. Calling XAllowEvents before client_focus ensures the
* click reaches the application (tabs, buttons, etc.) without timing issues. */
if (is_client_window) {
XAllowEvents(dwn->display, ReplayPointer, ev->time);
client_focus(c);
return;
}
/* Focus on click */
client_focus(c);
/* Check for button clicks in decorations */
if (c->frame != None && ev->window == c->frame) {
ButtonType btn = decorations_hit_test_button(c, ev->x, ev->y);
if (btn != BUTTON_COUNT) {
@ -478,10 +424,8 @@ static void handle_button_press(XButtonEvent *ev)
return;
}
/* Check for title bar drag */
if (decorations_hit_test_title_bar(c, ev->x, ev->y)) {
if (ev->button == 1) {
/* Start move */
dwn->drag_client = c;
dwn->drag_start_x = ev->x_root;
dwn->drag_start_y = ev->y_root;
@ -496,7 +440,6 @@ static void handle_button_press(XButtonEvent *ev)
return;
}
/* Check for resize */
int direction;
if (decorations_hit_test_resize_area(c, ev->x, ev->y, &direction)) {
if (ev->button == 1) {
@ -519,12 +462,10 @@ static void handle_button_press(XButtonEvent *ev)
static void handle_button_release(XButtonEvent *ev)
{
/* Defensive: validate pointers */
if (dwn == NULL || dwn->display == NULL) {
return;
}
/* Handle volume slider release */
if (volume_slider != NULL && volume_slider->visible && volume_slider->dragging) {
volume_slider_handle_release(volume_slider);
}
@ -539,18 +480,15 @@ static void handle_button_release(XButtonEvent *ev)
static void handle_motion_notify(XMotionEvent *ev)
{
/* Defensive: validate pointers */
if (ev == NULL || dwn == NULL || dwn->display == NULL) {
return;
}
/* Check volume slider drag */
if (volume_slider != NULL && volume_slider->visible && ev->window == volume_slider->window) {
volume_slider_handle_motion(volume_slider, ev->x, ev->y);
return;
}
/* Check WiFi dropdown hover */
if (wifi_menu != NULL && wifi_menu->visible && ev->window == wifi_menu->window) {
dropdown_handle_motion(wifi_menu, ev->x, ev->y);
return;
@ -562,7 +500,7 @@ static void handle_motion_notify(XMotionEvent *ev)
Client *c = dwn->drag_client;
if (c == NULL) {
dwn->drag_client = NULL; /* Reset invalid drag state */
dwn->drag_client = NULL;
return;
}
@ -582,7 +520,6 @@ static void handle_motion_notify(XMotionEvent *ev)
static void handle_client_message(XClientMessageEvent *ev)
{
/* Defensive: validate pointers */
if (ev == NULL || dwn == NULL || dwn->display == NULL) {
return;
}
@ -626,7 +563,6 @@ static void handle_client_message(XClientMessageEvent *ev)
}
}
/* Sync log - disabled in production (enable for debugging crashes) */
#if 0
static FILE *crash_log_file = NULL;
@ -645,16 +581,13 @@ static void sync_log(const char *msg)
}
#endif
/* Main event dispatcher */
void dwn_handle_event(XEvent *ev)
{
/* Defensive: validate event pointer */
if (ev == NULL) {
LOG_WARN("dwn_handle_event: received NULL event");
return;
}
/* Defensive: validate global state */
if (dwn == NULL || dwn->display == NULL) {
LOG_ERROR("dwn_handle_event: dwn or display is NULL");
return;
@ -705,15 +638,12 @@ void dwn_handle_event(XEvent *ev)
}
}
/* ========== Core functions ========== */
int dwn_init(void)
{
/* Initialize global state */
dwn = &dwn_state;
memset(dwn, 0, sizeof(DWNState));
/* Open display */
dwn->display = XOpenDisplay(NULL);
if (dwn->display == NULL) {
fprintf(stderr, "Cannot open X display\n");
@ -726,36 +656,29 @@ int dwn_init(void)
dwn->screen_height = DisplayHeight(dwn->display, dwn->screen);
dwn->colormap = DefaultColormap(dwn->display, dwn->screen);
/* Set error handlers */
XSetErrorHandler(x_error_handler);
XSetIOErrorHandler(x_io_error_handler);
/* Check for other WM */
if (check_other_wm()) {
fprintf(stderr, "Another window manager is already running\n");
XCloseDisplay(dwn->display);
return -1;
}
/* Initialize logging */
log_init("~/.local/share/dwn/dwn.log");
LOG_INFO("DWN %s starting", DWN_VERSION);
/* Load configuration */
dwn->config = config_create();
config_load(dwn->config, NULL);
/* Initialize colors (after display is open) */
extern void config_init_colors(Config *cfg);
config_init_colors(dwn->config);
/* Load font */
dwn->font = XLoadQueryFont(dwn->display, dwn->config->font_name);
if (dwn->font == NULL) {
dwn->font = XLoadQueryFont(dwn->display, "fixed");
}
/* Load Xft font for UTF-8 support */
dwn->xft_font = XftFontOpenName(dwn->display, dwn->screen,
"monospace:size=10:antialias=true");
if (dwn->xft_font == NULL) {
@ -766,7 +689,6 @@ int dwn_init(void)
LOG_INFO("Loaded Xft font for UTF-8 support");
}
/* Create GC */
XGCValues gcv;
gcv.foreground = dwn->config->colors.panel_fg;
gcv.background = dwn->config->colors.panel_bg;
@ -774,59 +696,42 @@ int dwn_init(void)
dwn->gc = XCreateGC(dwn->display, dwn->root,
GCForeground | GCBackground | (dwn->font ? GCFont : 0), &gcv);
/* Initialize atoms */
atoms_init(dwn->display);
/* Initialize monitors */
init_monitors();
/* Initialize workspaces */
workspace_init();
/* Initialize decorations */
decorations_init();
/* Initialize panels */
panels_init();
/* Initialize system tray (WiFi, Audio indicators) */
systray_init();
/* Initialize news ticker */
news_init();
/* Initialize app launcher */
applauncher_init();
/* Initialize keyboard shortcuts */
keys_init();
/* Initialize D-Bus notifications */
notifications_init();
/* Initialize AI */
ai_init();
/* Setup EWMH */
atoms_setup_ewmh();
/* Select events on root window */
XSelectInput(dwn->display, dwn->root,
SubstructureRedirectMask | SubstructureNotifyMask |
StructureNotifyMask | PropertyChangeMask |
ButtonPressMask | PointerMotionMask);
/* Set cursor */
Cursor cursor = XCreateFontCursor(dwn->display, XC_left_ptr);
XDefineCursor(dwn->display, dwn->root, cursor);
/* Scan existing windows */
scan_existing_windows();
/* Arrange initial workspace */
workspace_arrange_current();
/* Render panels */
panel_render_all();
dwn->running = true;
@ -840,7 +745,6 @@ void dwn_cleanup(void)
{
LOG_INFO("DWN shutting down");
/* Cleanup subsystems */
ai_cleanup();
notifications_cleanup();
news_cleanup();
@ -851,12 +755,10 @@ void dwn_cleanup(void)
decorations_cleanup();
workspace_cleanup();
/* Unmanage all clients */
while (dwn->client_list != NULL) {
client_unmanage(dwn->client_list);
}
/* Free resources */
if (dwn->gc != None) {
XFreeGC(dwn->display, dwn->gc);
}
@ -870,7 +772,6 @@ void dwn_cleanup(void)
config_destroy(dwn->config);
}
/* Close display */
if (dwn->display != NULL) {
XCloseDisplay(dwn->display);
}
@ -893,35 +794,28 @@ void dwn_run(void)
long last_news_update = 0;
while (dwn->running && received_signal == 0) {
/* Handle pending X events */
while (XPending(dwn->display)) {
XEvent ev;
XNextEvent(dwn->display, &ev);
dwn_handle_event(&ev);
}
/* Process D-Bus messages */
notifications_process_messages();
/* Process AI requests */
ai_process_pending();
/* Process Exa requests */
exa_process_pending();
/* Update notifications (check for expired) */
notifications_update();
long now = get_time_ms();
/* Update news ticker frequently for smooth scrolling (~60fps) */
if (now - last_news_update >= 16) {
news_update();
panel_render_all();
last_news_update = now;
}
/* Update clock and system stats every second */
if (now - last_clock_update >= 1000) {
panel_update_clock();
panel_update_system_stats();
@ -929,7 +823,6 @@ void dwn_run(void)
last_clock_update = now;
}
/* Wait for events with timeout - short for smooth animation */
fd_set fds;
FD_ZERO(&fds);
FD_SET(x11_fd, &fds);
@ -939,7 +832,7 @@ void dwn_run(void)
struct timeval tv;
tv.tv_sec = 0;
tv.tv_usec = 16000; /* ~16ms for 60fps smooth scrolling */
tv.tv_usec = 16000;
int max_fd = x11_fd;
if (dbus_fd > max_fd) max_fd = dbus_fd;
@ -947,7 +840,6 @@ void dwn_run(void)
select(max_fd + 1, &fds, NULL, NULL, &tv);
}
/* Handle signal */
if (received_signal != 0) {
LOG_INFO("Received signal %d", received_signal);
}
@ -958,7 +850,6 @@ void dwn_quit(void)
dwn->running = false;
}
/* ========== Main ========== */
static void print_usage(const char *program)
{
@ -982,7 +873,6 @@ static void print_version(void)
int main(int argc, char *argv[])
{
/* Parse arguments */
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) {
print_usage(argv[0]);
@ -994,27 +884,22 @@ int main(int argc, char *argv[])
}
}
/* Setup signal handlers */
signal(SIGTERM, signal_handler);
signal(SIGINT, signal_handler);
signal(SIGHUP, signal_handler);
/* Setup crash signal handlers to flush logs before dying */
signal(SIGSEGV, crash_signal_handler);
signal(SIGABRT, crash_signal_handler);
signal(SIGFPE, crash_signal_handler);
signal(SIGBUS, crash_signal_handler);
signal(SIGILL, crash_signal_handler);
/* Initialize */
if (dwn_init() != 0) {
return EXIT_FAILURE;
}
/* Run event loop */
dwn_run();
/* Cleanup */
dwn_cleanup();
return EXIT_SUCCESS;

View File

@ -20,13 +20,10 @@
#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
/* Sentiment colors (RGB hex values) */
#define SENTIMENT_COLOR_POSITIVE 0x81C784
#define SENTIMENT_COLOR_NEUTRAL 0xB0BEC5
#define SENTIMENT_COLOR_NEGATIVE 0xE57373
@ -40,15 +37,12 @@ static unsigned long news_sentiment_color(NewsSentiment sentiment)
}
}
/* 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;
@ -72,7 +66,6 @@ static size_t news_curl_write_cb(void *contents, size_t size, size_t nmemb, void
return realsize;
}
/* Decode numeric HTML entity (&#123; or &#x1F;) */
static int decode_numeric_entity(const char *src, char *out, size_t max_out)
{
if (src[0] != '&' || src[1] != '#') return 0;
@ -106,7 +99,6 @@ static int decode_numeric_entity(const char *src, char *out, size_t max_out)
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;
@ -119,7 +111,6 @@ static void strip_html(char *dst, const char *src, size_t max_len)
in_tag = false;
} else if (!in_tag) {
if (src[i] == '&') {
/* Named entities */
if (strncmp(&src[i], "&amp;", 5) == 0) {
dst[j++] = '&'; i += 4;
} else if (strncmp(&src[i], "&lt;", 4) == 0) {
@ -143,7 +134,6 @@ static void strip_html(char *dst, const char *src, size_t max_len)
} else if (strncmp(&src[i], "&ldquo;", 7) == 0 || strncmp(&src[i], "&rdquo;", 7) == 0) {
dst[j++] = '"'; i += 6;
} else if (src[i+1] == '#') {
/* Numeric entity */
char decoded[4] = {0};
int consumed = decode_numeric_entity(&src[i], decoded, sizeof(decoded));
if (consumed > 0) {
@ -166,7 +156,6 @@ static void strip_html(char *dst, const char *src, size_t max_len)
}
dst[j] = '\0';
/* Collapse multiple spaces */
char *read = dst, *write = dst;
bool last_space = false;
while (*read) {
@ -184,7 +173,6 @@ static void strip_html(char *dst, const char *src, size_t max_len)
*write = '\0';
}
/* Parse JSON response and populate articles */
static int parse_news_json(const char *json_str)
{
cJSON *root = cJSON_Parse(json_str);
@ -218,7 +206,6 @@ static int parse_news_json(const char *json_str)
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;
@ -229,7 +216,6 @@ static int parse_news_json(const char *json_str)
if (text) {
strip_html(art->content, text, sizeof(art->content));
} else {
/* Use title as fallback */
strncpy(art->content, art->title, sizeof(art->content) - 1);
}
@ -269,14 +255,11 @@ static int parse_news_json(const char *json_str)
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;
@ -295,7 +278,6 @@ static int news_text_width(const char *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)
{
@ -346,7 +328,6 @@ static void news_draw_text_clipped(Drawable d, int x, int y, const char *text,
}
}
/* 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) {
@ -361,7 +342,6 @@ static void get_article_display_text(int index, char *buf, size_t buf_size)
}
}
/* Recalculate cached widths after fetching */
static void news_recalc_widths(void)
{
if (dwn == NULL) {
@ -379,7 +359,6 @@ static void news_recalc_widths(void)
}
}
/* Background fetch thread */
static void *news_fetch_thread(void *arg)
{
(void)arg;
@ -436,14 +415,12 @@ static void *news_fetch_thread(void *arg)
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");
@ -451,7 +428,6 @@ void news_init(void)
void news_cleanup(void)
{
/* Wait for any pending fetch */
if (fetch_running) {
pthread_join(fetch_thread, NULL);
fetch_running = 0;
@ -468,7 +444,6 @@ void news_fetch_async(void)
news_state.fetching = true;
pthread_mutex_unlock(&news_mutex);
/* Wait for previous thread if still running */
if (fetch_running) {
pthread_join(fetch_thread, NULL);
}
@ -489,18 +464,15 @@ void news_update(void)
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;
}
@ -567,7 +539,6 @@ void news_open_current(void)
}
}
/* 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) {

View File

@ -12,41 +12,33 @@
#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 (text == NULL || dwn == NULL) return len * 7;
if (dwn->xft_font != NULL) {
XGlyphInfo extents;
@ -59,10 +51,9 @@ static int notif_text_width(const char *text, int len)
return XTextWidth(dwn->font, text, len);
}
return len * 7; /* Fallback estimate */
return len * 7;
}
/* Get line height */
static int notif_line_height(void)
{
if (dwn == NULL) return 14;
@ -78,7 +69,6 @@ static int notif_line_height(void)
return 14;
}
/* Get font ascent */
static int notif_font_ascent(void)
{
if (dwn == NULL) return 12;
@ -94,13 +84,11 @@ static int notif_font_ascent(void)
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),
@ -126,19 +114,16 @@ static void notif_draw_text(Window win, int x, int y, const char *text,
}
}
/* 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);
@ -151,10 +136,8 @@ bool notifications_init(void)
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");
@ -163,14 +146,11 @@ bool notifications_init(void)
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)
{
@ -181,7 +161,6 @@ bool notifications_register_service(void)
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);
@ -196,7 +175,6 @@ bool notifications_register_service(void)
return false;
}
/* Add message filter */
dbus_connection_add_filter(dbus_conn, notifications_handle_message, NULL, NULL);
LOG_INFO("Registered as org.freedesktop.Notifications");
@ -209,11 +187,9 @@ void notifications_process_messages(void)
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 */
}
}
@ -234,7 +210,6 @@ DBusHandlerResult notifications_handle_message(DBusConnection *conn,
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
}
/* Handle Notify method */
if (strcmp(member, "Notify") == 0) {
DBusMessageIter args;
if (!dbus_message_iter_init(msg, &args)) {
@ -248,7 +223,6 @@ DBusHandlerResult notifications_handle_message(DBusConnection *conn,
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);
@ -270,12 +244,10 @@ DBusHandlerResult notifications_handle_message(DBusConnection *conn,
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);
}
@ -284,10 +256,8 @@ DBusHandlerResult notifications_handle_message(DBusConnection *conn,
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);
@ -296,7 +266,6 @@ DBusHandlerResult notifications_handle_message(DBusConnection *conn,
return DBUS_HANDLER_RESULT_HANDLED;
}
/* Handle CloseNotification method */
if (strcmp(member, "CloseNotification") == 0) {
DBusMessageIter args;
if (dbus_message_iter_init(msg, &args) &&
@ -313,7 +282,6 @@ DBusHandlerResult notifications_handle_message(DBusConnection *conn,
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;
@ -333,7 +301,6 @@ DBusHandlerResult notifications_handle_message(DBusConnection *conn,
return DBUS_HANDLER_RESULT_HANDLED;
}
/* Handle GetServerInformation method */
if (strcmp(member, "GetServerInformation") == 0) {
DBusMessage *reply = dbus_message_new_method_return(msg);
@ -358,11 +325,9 @@ DBusHandlerResult notifications_handle_message(DBusConnection *conn,
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)
{
@ -375,7 +340,6 @@ static void notification_calculate_size(const char *summary, const char *body,
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));
@ -383,41 +347,34 @@ static void notification_calculate_size(const char *summary, const char *body,
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 */
if ((lp[i] & 0xC0) == 0x80) continue;
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) {
@ -428,12 +385,10 @@ static void notification_calculate_size(const char *summary, const char *body,
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;
}
@ -447,13 +402,10 @@ 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;
@ -466,15 +418,13 @@ done_counting:
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) */
oldest = n;
}
/* Close oldest notification if we're at the limit */
if (count >= MAX_VISIBLE_NOTIFICATIONS && oldest != NULL) {
notification_close(oldest->id);
}
@ -486,7 +436,6 @@ uint32_t notification_show(const char *app_name, const char *summary,
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);
@ -505,13 +454,11 @@ uint32_t notification_show(const char *app_name, const char *summary,
notif->expire_time = (notif->timeout > 0) ?
get_time_ms() + notif->timeout : 0;
/* Calculate size based on content */
int notif_width, notif_height;
notification_calculate_size(summary, body, &notif_width, &notif_height);
notif->width = notif_width;
notif->height = notif_height;
/* Create notification window */
if (dwn != NULL && dwn->display != NULL) {
XSetWindowAttributes swa;
swa.override_redirect = True;
@ -530,11 +477,9 @@ uint32_t notification_show(const char *app_name, const char *summary,
dwn->config->colors.border_focused);
}
/* Add to list */
notif->next = notification_list;
notification_list = notif;
/* Position and show */
notifications_position();
notification_render(notif);
@ -555,19 +500,16 @@ void notification_close(uint32_t id)
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;
@ -575,7 +517,6 @@ void notification_close(uint32_t id)
dwn_free(notif);
/* Reposition remaining notifications */
notifications_position();
return;
}
@ -612,7 +553,6 @@ Notification *notification_find_by_window(Window window)
return NULL;
}
/* ========== Rendering ========== */
void notification_render(Notification *notif)
{
@ -642,66 +582,54 @@ void notification_render(Notification *notif)
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 */
y += line_height / 2;
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);
@ -710,18 +638,15 @@ void notification_render(Notification *notif)
}
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) {
@ -729,12 +654,10 @@ void notification_render(Notification *notif)
}
}
/* 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 */
fit_len = last_space + 1;
}
/* Draw this segment */
if (fit_len > 0) {
char *segment = dwn_malloc(fit_len + 1);
if (segment != NULL) {
@ -748,7 +671,6 @@ void notification_render(Notification *notif)
}
p += fit_len;
/* Skip leading spaces on new line */
while (*p == ' ') p++;
y += line_height;
@ -764,7 +686,6 @@ void notification_render(Notification *notif)
}
}
/* Draw app name at bottom */
if (notif->app_name[0] != '\0') {
y = notif->height - NOTIFICATION_PADDING;
notif_draw_text(notif->window, NOTIFICATION_PADDING, y,
@ -772,7 +693,6 @@ void notification_render(Notification *notif)
colors->workspace_inactive);
}
/* Force the drawing to be sent to X server */
XFlush(dpy);
}
@ -787,7 +707,6 @@ void notifications_update(void)
{
long now = get_time_ms();
/* Check for expired notifications */
Notification *notif = notification_list;
while (notif != NULL) {
Notification *next = notif->next;
@ -808,18 +727,15 @@ void notifications_position(void)
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;
}
}
@ -830,7 +746,6 @@ void notifications_raise_all(void)
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);
@ -838,7 +753,6 @@ void notifications_raise_all(void)
}
}
/* ========== Server info ========== */
void notifications_get_server_info(const char **name, const char **vendor,
const char **version, const char **spec_version)

View File

@ -21,43 +21,35 @@
#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 */
int cpu_percent;
int mem_percent;
int mem_used_mb;
int mem_total_mb;
float load_1min;
float load_5min;
float load_15min;
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;
@ -69,7 +61,6 @@ static int panel_text_width(const char *text, int len)
return extents.xOff;
}
/* Fallback to legacy font */
if (dwn->font != NULL) {
return XTextWidth(dwn->font, text, len);
}
@ -77,13 +68,11 @@ static int panel_text_width(const char *text, int 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),
@ -109,12 +98,10 @@ static void panel_draw_text(Drawable d, int x, int y, const char *text,
}
}
/* 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) {
@ -126,7 +113,6 @@ static int panel_text_y(int panel_height)
return panel_height / 2;
}
/* ========== Panel creation/destruction ========== */
Panel *panel_create(PanelPosition position)
{
@ -147,7 +133,6 @@ Panel *panel_create(PanelPosition position)
panel->y = dwn->screen_height - panel->height;
}
/* Create panel window */
XSetWindowAttributes swa;
swa.override_redirect = True;
swa.background_pixel = dwn->config->colors.panel_bg;
@ -161,12 +146,10 @@ Panel *panel_create(PanelPosition position)
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;
@ -221,7 +204,6 @@ void panels_init(void)
}
}
/* Initial clock and stats update */
panel_update_clock();
panel_update_system_stats();
@ -241,7 +223,6 @@ void panels_cleanup(void)
}
}
/* ========== Panel rendering ========== */
void panel_render(Panel *panel)
{
@ -252,7 +233,6 @@ void panel_render(Panel *panel)
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);
@ -260,33 +240,25 @@ void panel_render(Panel *panel)
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);
@ -295,28 +267,23 @@ void panel_render(Panel *panel)
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 */
if (news_max_width > 100) {
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);
@ -349,14 +316,12 @@ void panel_render_workspaces(Panel *panel, int x, int *width)
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);
@ -384,7 +349,6 @@ void panel_render_taskbar(Panel *panel, int x, int *width)
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++;
@ -401,7 +365,6 @@ void panel_render_taskbar(Panel *panel, int x, int *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) {
@ -411,32 +374,27 @@ void panel_render_taskbar(Panel *panel, int x, int *width)
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 "..." */
strncpy(title, c->title, sizeof(title) - 4);
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 */
cut--;
}
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;
}
@ -476,8 +434,6 @@ void panel_render_clock(Panel *panel, int x, int *width)
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;
@ -524,7 +480,6 @@ void panel_render_ai_status(Panel *panel, int x, int *width)
*width = panel_text_width(status, strlen(status));
}
/* ========== Panel interaction ========== */
void panel_handle_click(Panel *panel, int x, int y, int button)
{
@ -533,7 +488,6 @@ void panel_handle_click(Panel *panel, int x, int y, int button)
}
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) {
@ -541,27 +495,24 @@ void panel_handle_click(Panel *panel, int x, int y, int 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 */
if (button == 1) {
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 */
} else if (button == 3) {
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);
}
@ -629,7 +580,6 @@ Client *panel_hit_test_taskbar(Panel *panel, int x, int y)
return NULL;
}
/* ========== Panel visibility ========== */
void panel_show(Panel *panel)
{
@ -665,7 +615,6 @@ void panel_toggle(Panel *panel)
}
}
/* ========== Clock updates ========== */
void panel_update_clock(void)
{
@ -676,14 +625,12 @@ void panel_update_clock(void)
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) {
@ -708,7 +655,6 @@ void panel_update_system_stats(void)
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;
@ -722,7 +668,7 @@ void panel_update_system_stats(void)
sscanf(line + 8, " %lu", &buffers);
} else if (strncmp(line, "Cached:", 7) == 0) {
sscanf(line + 7, " %lu", &cached);
break; /* Got all we need */
break;
}
}
fclose(fp);
@ -735,7 +681,6 @@ void panel_update_system_stats(void)
}
}
/* Read load average from /proc/loadavg */
fp = fopen("/proc/loadavg", "r");
if (fp != NULL) {
if (fscanf(fp, "%f %f %f",
@ -750,7 +695,6 @@ void panel_update_system_stats(void)
}
}
/* Calculate stats width without rendering */
static int panel_calculate_stats_width(void)
{
if (dwn->xft_font == NULL && dwn->font == NULL) return 0;
@ -758,11 +702,9 @@ static int panel_calculate_stats_width(void)
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);
@ -772,12 +714,10 @@ static int panel_calculate_stats_width(void)
}
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 ? "[+]" : "[=]";
@ -803,24 +743,21 @@ static void panel_render_system_stats(Panel *panel, int x, int *width)
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 */
cpu_color = colors->workspace_urgent;
} else if (sys_stats.cpu_percent >= 70) {
cpu_color = 0xFFA500; /* Orange for medium-high */
cpu_color = 0xFFA500;
}
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;
@ -838,7 +775,6 @@ static void panel_render_system_stats(Panel *panel, int x, int *width)
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;
@ -851,16 +787,15 @@ static void panel_render_system_stats(Panel *panel, int x, int *width)
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 */
bat_color = colors->workspace_urgent;
} else if (bat_snap.percentage <= 40 && !bat_snap.charging) {
bat_color = 0xFFA500; /* Orange for medium-low */
bat_color = 0xFFA500;
} else if (bat_snap.charging) {
bat_color = colors->workspace_active; /* Blue for charging */
bat_color = colors->workspace_active;
}
const char *bat_icon = bat_snap.charging ? "[+]" : "[=]";
@ -872,11 +807,9 @@ static void panel_render_system_stats(Panel *panel, int x, int *width)
*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)");
}

View File

@ -17,17 +17,15 @@
#include <dirent.h>
#include <pthread.h>
/* UTF-8 Icons for system tray */
#define ICON_WIFI_FULL "\xE2\x96\x82\xE2\x96\x84\xE2\x96\x86\xE2\x96\x88" /* Full signal bars */
#define ICON_WIFI_OFF "\xE2\x9C\x97" /* X mark */
#define ICON_VOLUME_HIGH "\xF0\x9F\x94\x8A" /* Speaker high */
#define ICON_VOLUME_MED "\xF0\x9F\x94\x89" /* Speaker medium */
#define ICON_VOLUME_LOW "\xF0\x9F\x94\x88" /* Speaker low */
#define ICON_VOLUME_MUTE "\xF0\x9F\x94\x87" /* Speaker muted */
#define ICON_BATTERY_FULL "\xF0\x9F\x94\x8B" /* Battery full */
#define ICON_BATTERY_CHARGE "\xE2\x9A\xA1" /* Lightning bolt */
#define ICON_WIFI_FULL "\xE2\x96\x82\xE2\x96\x84\xE2\x96\x86\xE2\x96\x88"
#define ICON_WIFI_OFF "\xE2\x9C\x97"
#define ICON_VOLUME_HIGH "\xF0\x9F\x94\x8A"
#define ICON_VOLUME_MED "\xF0\x9F\x94\x89"
#define ICON_VOLUME_LOW "\xF0\x9F\x94\x88"
#define ICON_VOLUME_MUTE "\xF0\x9F\x94\x87"
#define ICON_BATTERY_FULL "\xF0\x9F\x94\x8B"
#define ICON_BATTERY_CHARGE "\xE2\x9A\xA1"
/* Simple ASCII fallback icons */
#define ASCII_WIFI_ON "W"
#define ASCII_WIFI_OFF "-"
#define ASCII_VOL_HIGH "V"
@ -35,7 +33,6 @@
#define ASCII_BATTERY "B"
#define ASCII_CHARGING "+"
/* Widget dimensions */
#define SYSTRAY_SPACING 12
#define DROPDOWN_ITEM_HEIGHT 28
#define DROPDOWN_WIDTH 400
@ -45,32 +42,26 @@
#define SLIDER_PADDING 8
#define SLIDER_KNOB_HEIGHT 8
/* Scan interval in milliseconds */
#define WIFI_SCAN_INTERVAL 10000
/* Global state */
WifiState wifi_state = {0};
AudioState audio_state = {0};
BatteryState battery_state = {0};
DropdownMenu *wifi_menu = NULL;
VolumeSlider *volume_slider = NULL;
/* Async update thread */
static pthread_t update_thread;
static pthread_mutex_t state_mutex = PTHREAD_MUTEX_INITIALIZER;
static volatile int thread_running = 0;
/* Systray position tracking */
static int systray_x = 0;
static int systray_width = 0;
static int wifi_icon_x = 0;
static int audio_icon_x = 0;
/* Forward declarations for internal update functions */
static void wifi_update_state_internal(void);
static void audio_update_state_internal(void);
/* ========== UTF-8 Text Drawing ========== */
static void draw_utf8_text(Drawable d, int x, int y, const char *text, unsigned long color)
{
@ -79,7 +70,6 @@ static void draw_utf8_text(Drawable d, int x, int y, const char *text, unsigned
}
if (dwn->xft_font != NULL) {
/* Use Xft for proper UTF-8 rendering */
XftDraw *xft_draw = XftDrawCreate(dwn->display, d,
DefaultVisual(dwn->display, dwn->screen),
dwn->colormap);
@ -87,7 +77,6 @@ static void draw_utf8_text(Drawable d, int x, int y, const char *text, unsigned
XftColor xft_color;
XRenderColor render_color;
/* Convert X11 color to XRender color */
render_color.red = ((color >> 16) & 0xFF) * 257;
render_color.green = ((color >> 8) & 0xFF) * 257;
render_color.blue = (color & 0xFF) * 257;
@ -108,7 +97,6 @@ static void draw_utf8_text(Drawable d, int x, int y, const char *text, unsigned
}
}
/* Fallback to basic X11 drawing */
XSetForeground(dwn->display, dwn->gc, color);
XDrawString(dwn->display, d, dwn->gc, x, y, text, strlen(text));
}
@ -130,31 +118,25 @@ static int get_text_width(const char *text)
return XTextWidth(dwn->font, text, strlen(text));
}
return strlen(text) * 8; /* Rough estimate */
return strlen(text) * 8;
}
/* ========== Initialization ========== */
/* Background thread for async state updates */
static void *systray_update_thread(void *arg)
{
(void)arg;
while (thread_running) {
/* Update WiFi state (slow - uses popen) */
wifi_update_state_internal();
/* Update audio state (slow - uses popen) */
audio_update_state_internal();
/* Update battery state (fast - reads /sys files) */
pthread_mutex_lock(&state_mutex);
battery_update_state();
pthread_mutex_unlock(&state_mutex);
/* Sleep for 2 seconds between updates */
for (int i = 0; i < 20 && thread_running; i++) {
usleep(100000); /* 100ms chunks for responsive shutdown */
usleep(100000);
}
}
@ -167,11 +149,9 @@ void systray_init(void)
memset(&audio_state, 0, sizeof(audio_state));
memset(&battery_state, 0, sizeof(battery_state));
/* Set default states for instant display */
audio_state.volume = 50;
wifi_state.enabled = true;
/* Start background update thread - all updates happen async */
thread_running = 1;
if (pthread_create(&update_thread, NULL, systray_update_thread, NULL) != 0) {
LOG_WARN("Failed to create systray update thread");
@ -183,7 +163,6 @@ void systray_init(void)
void systray_cleanup(void)
{
/* Stop the update thread */
if (thread_running) {
thread_running = 0;
pthread_join(update_thread, NULL);
@ -199,7 +178,6 @@ void systray_cleanup(void)
}
}
/* ========== Battery Functions ========== */
void battery_update_state(void)
{
@ -213,7 +191,6 @@ void battery_update_state(void)
battery_state.charging = false;
battery_state.percentage = 0;
/* Look for battery in /sys/class/power_supply */
dir = opendir("/sys/class/power_supply");
if (dir == NULL) {
return;
@ -224,7 +201,6 @@ void battery_update_state(void)
continue;
}
/* Check if this is a battery (not AC adapter) */
snprintf(path, sizeof(path), "/sys/class/power_supply/%s/type", entry->d_name);
fp = fopen(path, "r");
if (fp != NULL) {
@ -233,7 +209,6 @@ void battery_update_state(void)
if (strcmp(value, "Battery") == 0) {
battery_state.present = true;
/* Get capacity */
snprintf(path, sizeof(path), "/sys/class/power_supply/%s/capacity", entry->d_name);
FILE *cap_fp = fopen(path, "r");
if (cap_fp != NULL) {
@ -243,7 +218,6 @@ void battery_update_state(void)
fclose(cap_fp);
}
/* Get status (charging/discharging) */
snprintf(path, sizeof(path), "/sys/class/power_supply/%s/status", entry->d_name);
FILE *status_fp = fopen(path, "r");
if (status_fp != NULL) {
@ -256,7 +230,7 @@ void battery_update_state(void)
}
fclose(fp);
break; /* Found a battery, stop searching */
break;
}
}
fclose(fp);
@ -268,7 +242,7 @@ void battery_update_state(void)
const char *battery_get_icon(void)
{
if (!battery_state.present) {
return ""; /* No battery = no icon */
return "";
}
if (battery_state.charging) {
@ -278,16 +252,13 @@ const char *battery_get_icon(void)
return ASCII_BATTERY;
}
/* ========== WiFi Functions ========== */
/* Internal update function - called from background thread */
static void wifi_update_state_internal(void)
{
FILE *fp;
char line[256];
WifiState temp_state = {0};
/* Check if WiFi is connected and get current SSID */
fp = popen("nmcli -t -f GENERAL.STATE,GENERAL.CONNECTION dev show 2>/dev/null | head -2", "r");
if (fp != NULL) {
while (fgets(line, sizeof(line), fp) != NULL) {
@ -310,7 +281,6 @@ static void wifi_update_state_internal(void)
pclose(fp);
}
/* Get signal strength if connected */
if (temp_state.connected) {
fp = popen("nmcli -t -f IN-USE,SIGNAL dev wifi list 2>/dev/null | grep '^\\*' | cut -d: -f2", "r");
if (fp != NULL) {
@ -321,7 +291,6 @@ static void wifi_update_state_internal(void)
}
}
/* Copy to global state with mutex protection */
pthread_mutex_lock(&state_mutex);
wifi_state.connected = temp_state.connected;
wifi_state.enabled = temp_state.enabled;
@ -330,11 +299,8 @@ static void wifi_update_state_internal(void)
pthread_mutex_unlock(&state_mutex);
}
/* Public function - now just reads cached state (non-blocking) */
void wifi_update_state(void)
{
/* State is updated by background thread - this is now a no-op */
/* Kept for API compatibility */
}
void wifi_scan_networks(void)
@ -342,7 +308,6 @@ void wifi_scan_networks(void)
FILE *fp;
char line[512];
/* Build network list in temporary storage first */
WifiNetwork temp_networks[MAX_WIFI_NETWORKS];
int temp_count = 0;
long scan_time = get_time_ms();
@ -374,7 +339,6 @@ void wifi_scan_networks(void)
}
pclose(fp);
/* Copy to global state under lock */
pthread_mutex_lock(&state_mutex);
memcpy(wifi_state.networks, temp_networks, sizeof(temp_networks));
wifi_state.network_count = temp_count;
@ -423,9 +387,7 @@ const char *wifi_get_icon(void)
return ASCII_WIFI_ON;
}
/* ========== Audio Functions ========== */
/* Internal update function - called from background thread */
static void audio_update_state_internal(void)
{
FILE *fp;
@ -449,18 +411,14 @@ static void audio_update_state_internal(void)
pclose(fp);
}
/* Copy to global state with mutex protection */
pthread_mutex_lock(&state_mutex);
audio_state.volume = volume;
audio_state.muted = muted;
pthread_mutex_unlock(&state_mutex);
}
/* Public function - now just reads cached state (non-blocking) */
void audio_update_state(void)
{
/* State is updated by background thread - this is now a no-op */
/* Kept for API compatibility */
}
void audio_set_volume(int volume)
@ -495,7 +453,6 @@ const char *audio_get_icon(void)
return ASCII_VOL_HIGH;
}
/* ========== Volume Slider ========== */
VolumeSlider *volume_slider_create(int x, int y)
{
@ -575,11 +532,9 @@ void volume_slider_render(VolumeSlider *slider)
Display *dpy = dwn->display;
const ColorScheme *colors = config_get_colors();
/* Clear background */
XSetForeground(dpy, dwn->gc, colors->panel_bg);
XFillRectangle(dpy, slider->window, dwn->gc, 0, 0, slider->width, slider->height);
/* Draw track */
int track_x = slider->width / 2 - 2;
int track_y = SLIDER_PADDING;
int track_height = slider->height - 2 * SLIDER_PADDING;
@ -587,14 +542,12 @@ void volume_slider_render(VolumeSlider *slider)
XSetForeground(dpy, dwn->gc, colors->workspace_inactive);
XFillRectangle(dpy, slider->window, dwn->gc, track_x, track_y, 4, track_height);
/* Draw filled portion */
int fill_height = (audio_state.volume * track_height) / 100;
int fill_y = track_y + track_height - fill_height;
XSetForeground(dpy, dwn->gc, colors->workspace_active);
XFillRectangle(dpy, slider->window, dwn->gc, track_x, fill_y, 4, fill_height);
/* Draw knob */
int knob_y = fill_y - SLIDER_KNOB_HEIGHT / 2;
if (knob_y < track_y) knob_y = track_y;
if (knob_y > track_y + track_height - SLIDER_KNOB_HEIGHT) {
@ -605,7 +558,6 @@ void volume_slider_render(VolumeSlider *slider)
XFillRectangle(dpy, slider->window, dwn->gc,
track_x - 4, knob_y, 12, SLIDER_KNOB_HEIGHT);
/* Draw percentage text at bottom */
char vol_text[8];
snprintf(vol_text, sizeof(vol_text), "%d%%", audio_state.volume);
int text_width = get_text_width(vol_text);
@ -626,7 +578,6 @@ void volume_slider_handle_click(VolumeSlider *slider, int x, int y)
slider->dragging = true;
/* Calculate volume from y position */
int track_y = SLIDER_PADDING;
int track_height = slider->height - 2 * SLIDER_PADDING;
@ -657,7 +608,6 @@ void volume_slider_handle_release(VolumeSlider *slider)
slider->dragging = false;
}
/* ========== Dropdown Menu ========== */
DropdownMenu *dropdown_create(int x, int y, int width)
{
@ -707,7 +657,6 @@ void dropdown_show(DropdownMenu *menu)
menu->item_count = 1;
}
/* Calculate auto-width based on longest network name + signal */
int max_text_width = 0;
for (int i = 0; i < wifi_state.network_count; i++) {
WifiNetwork *net = &wifi_state.networks[i];
@ -778,7 +727,6 @@ void dropdown_render(DropdownMenu *menu)
}
int text_y_offset = (DROPDOWN_ITEM_HEIGHT + font_height) / 2;
/* Clear background */
XSetForeground(dpy, dwn->gc, colors->panel_bg);
XFillRectangle(dpy, menu->window, dwn->gc, 0, 0, menu->width, menu->height);
@ -790,7 +738,6 @@ void dropdown_render(DropdownMenu *menu)
WifiNetwork *net = &wifi_state.networks[i];
int y = DROPDOWN_PADDING + i * DROPDOWN_ITEM_HEIGHT;
/* Highlight hovered item */
if (i == menu->hovered_item) {
XSetForeground(dpy, dwn->gc, colors->workspace_active);
XFillRectangle(dpy, menu->window, dwn->gc,
@ -799,7 +746,6 @@ void dropdown_render(DropdownMenu *menu)
unsigned long text_color = (i == menu->hovered_item) ? colors->panel_bg : colors->panel_fg;
/* Network name */
char label[80];
if (net->connected) {
snprintf(label, sizeof(label), "> %s", net->ssid);
@ -807,7 +753,6 @@ void dropdown_render(DropdownMenu *menu)
snprintf(label, sizeof(label), " %s", net->ssid);
}
/* Truncate if needed */
if (strlen(label) > 25) {
label[22] = '.';
label[23] = '.';
@ -817,7 +762,6 @@ void dropdown_render(DropdownMenu *menu)
draw_utf8_text(menu->window, DROPDOWN_PADDING, y + text_y_offset, label, text_color);
/* Signal strength */
char signal[8];
snprintf(signal, sizeof(signal), "%d%%", net->signal);
int signal_width = get_text_width(signal);
@ -885,7 +829,6 @@ void dropdown_handle_motion(DropdownMenu *menu, int x, int y)
}
}
/* ========== System Tray Rendering ========== */
int systray_get_width(void)
{
@ -922,7 +865,6 @@ void systray_render(Panel *panel, int x, int *width)
const ColorScheme *colors = config_get_colors();
/* Take thread-safe snapshots of state */
pthread_mutex_lock(&state_mutex);
AudioState audio_snap = audio_state;
WifiState wifi_snap;
@ -943,7 +885,6 @@ void systray_render(Panel *panel, int x, int *width)
systray_x = x;
int current_x = x;
/* Audio indicator */
audio_icon_x = current_x;
char audio_label[32];
const char *audio_icon = (audio_snap.muted || audio_snap.volume == 0) ? ASCII_VOL_MUTE : ASCII_VOL_HIGH;
@ -953,7 +894,6 @@ void systray_render(Panel *panel, int x, int *width)
draw_utf8_text(panel->buffer, current_x, text_y, audio_label, audio_color);
current_x += get_text_width(audio_label) + SYSTRAY_SPACING;
/* WiFi indicator - show SSID and signal strength */
wifi_icon_x = current_x;
char wifi_label[128];
const char *wifi_icon_str = (!wifi_snap.enabled || !wifi_snap.connected) ? ASCII_WIFI_OFF : ASCII_WIFI_ON;
@ -976,10 +916,10 @@ void systray_render(Panel *panel, int x, int *width)
int systray_hit_test(int x)
{
if (x >= wifi_icon_x) {
return 0; /* WiFi - extends to end of systray */
return 0;
}
if (x >= audio_icon_x && x < wifi_icon_x) {
return 1; /* Audio */
return 1;
}
return -1;
}
@ -988,8 +928,7 @@ void systray_handle_click(int x, int y, int button)
{
int widget = systray_hit_test(x);
if (widget == 0) { /* WiFi clicked */
/* Hide volume slider if open */
if (widget == 0) {
if (volume_slider != NULL && volume_slider->visible) {
volume_slider_hide(volume_slider);
}
@ -1012,14 +951,12 @@ void systray_handle_click(int x, int y, int button)
wifi_disconnect();
}
}
} else if (widget == 1) { /* Audio clicked */
/* Hide wifi menu if open */
} else if (widget == 1) {
if (wifi_menu != NULL && wifi_menu->visible) {
dropdown_hide(wifi_menu);
}
if (button == 1) {
/* Show volume slider */
if (volume_slider == NULL) {
int panel_height = config_get_panel_height();
volume_slider = volume_slider_create(audio_icon_x, panel_height);
@ -1033,18 +970,15 @@ void systray_handle_click(int x, int y, int button)
volume_slider_show(volume_slider);
}
} else if (button == 3) {
/* Right-click: toggle mute */
audio_toggle_mute();
panel_render_all();
} else if (button == 4) {
/* Scroll up: increase volume */
audio_set_volume(audio_state.volume + 5);
if (volume_slider != NULL && volume_slider->visible) {
volume_slider_render(volume_slider);
}
panel_render_all();
} else if (button == 5) {
/* Scroll down: decrease volume */
audio_set_volume(audio_state.volume - 5);
if (volume_slider != NULL && volume_slider->visible) {
volume_slider_render(volume_slider);
@ -1058,10 +992,7 @@ void systray_handle_click(int x, int y, int button)
void systray_update(void)
{
/* State is updated asynchronously by the background thread.
* This function now only handles the WiFi menu re-scan. */
/* Re-scan WiFi networks periodically if menu is open */
if (wifi_menu != NULL && wifi_menu->visible) {
long now = get_time_ms();
pthread_mutex_lock(&state_mutex);
@ -1075,7 +1006,6 @@ void systray_update(void)
}
}
/* ========== Thread-safe access functions ========== */
void systray_lock(void)
{

View File

@ -21,30 +21,27 @@
#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 */
#define LOG_RING_SIZE 256
#define LOG_MSG_MAX_LEN 512
#define LOG_MAX_FILE_SIZE (5 * 1024 * 1024)
#define LOG_FLUSH_INTERVAL_MS 100
/* Log entry in ring buffer */
typedef struct {
char message[LOG_MSG_MAX_LEN];
LogLevel level;
_Atomic int ready; /* 0 = empty, 1 = ready to write */
_Atomic int ready;
} 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 _Atomic size_t log_write_idx = 0;
static _Atomic size_t log_read_idx = 0;
static _Atomic int log_running = 0;
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 int log_fd = -1;
static char *log_path = NULL;
static LogLevel min_level = LOG_INFO;
static _Atomic size_t log_file_size = 0; /* Current log file size */
static _Atomic size_t log_file_size = 0;
static const char *level_names[] = {
"DEBUG",
@ -54,19 +51,16 @@ static const char *level_names[] = {
};
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 */
"\033[36m",
"\033[32m",
"\033[33m",
"\033[31m"
};
/* 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) {
@ -78,27 +72,22 @@ static void log_rotate_if_needed(void)
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;
@ -107,22 +96,18 @@ static void *log_writer_thread(void *arg)
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 */
struct timespec ts = {0, 1000};
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) {
@ -133,7 +118,6 @@ static void *log_writer_thread(void *arg)
}
}
/* Mark entry as consumed */
atomic_store(&entry->ready, 0);
}
@ -141,7 +125,6 @@ static void *log_writer_thread(void *arg)
atomic_store(&log_read_idx, read_idx);
}
/* Sleep before next check */
struct timespec ts = {0, LOG_FLUSH_INTERVAL_MS * 1000000L};
nanosleep(&ts, NULL);
}
@ -151,7 +134,6 @@ static void *log_writer_thread(void *arg)
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);
@ -159,42 +141,36 @@ void log_init(const char *path)
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 */
(void)ret;
}
}
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 */
log_path = expanded;
}
}
/* 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");
@ -204,23 +180,19 @@ void log_init(const char *path)
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;
@ -234,7 +206,6 @@ void log_set_level(LogLevel level)
void log_flush(void)
{
/* Force flush all pending log entries synchronously */
if (!atomic_load(&log_running) || log_fd < 0) {
return;
}
@ -258,7 +229,6 @@ void log_flush(void)
atomic_store(&log_read_idx, read_idx);
}
/* Sync to disk */
fsync(log_fd);
}
@ -268,43 +238,35 @@ void log_msg(LogLevel level, const char *fmt, ...)
return;
}
/* Get timestamp */
time_t now = time(NULL);
struct tm tm_info;
localtime_r(&now, &tm_info); /* Thread-safe version */
localtime_r(&now, &tm_info);
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 */
char msg_buf[LOG_MSG_MAX_LEN - 64];
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)
{
@ -360,10 +322,8 @@ void secure_wipe(void *ptr, size_t size)
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;
@ -371,7 +331,6 @@ void secure_wipe(void *ptr, size_t size)
#endif
}
/* ========== String utilities ========== */
char *str_trim(char *str)
{
@ -379,7 +338,6 @@ char *str_trim(char *str)
return NULL;
}
/* Trim leading whitespace */
while (isspace((unsigned char)*str)) {
str++;
}
@ -388,7 +346,6 @@ char *str_trim(char *str)
return str;
}
/* Trim trailing whitespace */
char *end = str + strlen(str) - 1;
while (end > str && isspace((unsigned char)*end)) {
end--;
@ -450,13 +407,11 @@ char *shell_escape(const char *str)
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;
@ -464,7 +419,6 @@ char *shell_escape(const char *str)
*dst++ = '\'';
for (const char *src = str; *src; src++) {
if (*src == '\'') {
/* Close quote, add escaped quote, reopen quote: '\'' */
*dst++ = '\'';
*dst++ = '\\';
*dst++ = '\'';
@ -479,7 +433,6 @@ char *shell_escape(const char *str)
return escaped;
}
/* ========== File utilities ========== */
bool file_exists(const char *path)
{
@ -548,7 +501,6 @@ char *expand_path(const char *path)
return dwn_strdup(path);
}
/* ========== Color utilities ========== */
unsigned long parse_color(const char *color_str)
{
@ -561,7 +513,6 @@ unsigned long parse_color(const char *color_str)
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) {
@ -589,7 +540,6 @@ void color_to_rgb(unsigned long color, int *r, int *g, int *b)
*b = xc.blue >> 8;
}
/* ========== Time utilities ========== */
long get_time_ms(void)
{
@ -606,7 +556,6 @@ void sleep_ms(int ms)
nanosleep(&ts, NULL);
}
/* ========== Process utilities ========== */
int spawn(const char *cmd)
{
@ -618,7 +567,6 @@ int spawn(const char *cmd)
pid_t pid = fork();
if (pid == 0) {
/* Child process */
setsid();
execl("/bin/sh", "sh", "-c", cmd, NULL);
_exit(EXIT_FAILURE);
@ -627,7 +575,6 @@ int spawn(const char *cmd)
return -1;
}
/* Wait for child */
int status;
waitpid(pid, &status, 0);
return WEXITSTATUS(status);
@ -643,10 +590,8 @@ int spawn_async(const char *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);
@ -658,7 +603,6 @@ int spawn_async(const char *cmd)
return -1;
}
/* Wait for first child (which exits immediately) */
int status;
waitpid(pid, &status, 0);
return 0;
@ -678,7 +622,6 @@ char *spawn_capture(const char *cmd)
return NULL;
}
/* Read output */
size_t buf_size = 1024;
size_t len = 0;
char *output = dwn_malloc(buf_size);
@ -691,7 +634,6 @@ char *spawn_capture(const char *cmd)
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;
}
@ -701,7 +643,6 @@ char *spawn_capture(const char *cmd)
LOG_DEBUG("Command exited with status %d", status);
}
/* Trim trailing newline */
if (len > 0 && output[len - 1] == '\n') {
output[len - 1] = '\0';
}

View File

@ -14,7 +14,6 @@
#include <string.h>
#include <stdio.h>
/* ========== Initialization ========== */
void workspace_init(void)
{
@ -36,7 +35,6 @@ void workspace_init(void)
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);
}
@ -45,10 +43,8 @@ void workspace_init(void)
void workspace_cleanup(void)
{
/* Clients are cleaned up separately */
}
/* ========== Workspace access ========== */
Workspace *workspace_get(int index)
{
@ -68,7 +64,6 @@ int workspace_get_current_index(void)
return dwn != NULL ? dwn->current_workspace : 0;
}
/* ========== Workspace switching ========== */
void workspace_switch(int index)
{
@ -83,23 +78,17 @@ void workspace_switch(int index)
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) {
@ -109,17 +98,14 @@ void workspace_switch(int 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);
@ -138,7 +124,6 @@ void workspace_switch_prev(void)
workspace_switch(prev);
}
/* ========== Client management ========== */
void workspace_add_client(int workspace, Client *client)
{
@ -151,11 +136,8 @@ void workspace_add_client(int workspace, Client *client)
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)
@ -169,7 +151,6 @@ void workspace_remove_client(int workspace, Client *client)
return;
}
/* If this was the focused client, clear it */
if (ws->focused == client) {
ws->focused = NULL;
}
@ -190,30 +171,23 @@ void workspace_move_client(Client *client, int new_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);
}
@ -239,7 +213,6 @@ Client *workspace_get_focused_client(int workspace)
return ws != NULL ? ws->focused : NULL;
}
/* ========== Layout ========== */
void workspace_set_layout(int workspace, LayoutType layout)
{
@ -250,7 +223,6 @@ void workspace_set_layout(int workspace, LayoutType layout)
ws->layout = layout;
/* Batch the arrangement */
XGrabServer(dwn->display);
workspace_arrange(workspace);
XUngrabServer(dwn->display);
@ -274,7 +246,6 @@ void workspace_cycle_layout(int workspace)
ws->layout = (ws->layout + 1) % LAYOUT_COUNT;
/* Batch the arrangement for smoother transition */
XGrabServer(dwn->display);
workspace_arrange(workspace);
XUngrabServer(dwn->display);
@ -291,13 +262,11 @@ void workspace_set_master_ratio(int workspace, float ratio)
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);
@ -322,7 +291,6 @@ void workspace_set_master_count(int workspace, int count)
if (count < 1) count = 1;
ws->master_count = count;
/* Batch the arrangement */
XGrabServer(dwn->display);
workspace_arrange(workspace);
XUngrabServer(dwn->display);
@ -337,7 +305,6 @@ void workspace_adjust_master_count(int workspace, int delta)
}
}
/* ========== Arrangement ========== */
void workspace_arrange(int workspace)
{
@ -346,7 +313,6 @@ void workspace_arrange(int workspace)
return;
}
/* Only arrange visible workspace */
if (workspace != dwn->current_workspace) {
return;
}
@ -356,14 +322,12 @@ void workspace_arrange(int 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)
{
@ -391,7 +355,6 @@ void workspace_hide(int workspace)
}
}
/* ========== Properties ========== */
void workspace_set_name(int workspace, const char *name)
{
@ -422,7 +385,6 @@ bool workspace_is_empty(int workspace)
return workspace_client_count(workspace) == 0;
}
/* ========== Focus cycling ========== */
void workspace_focus_next(void)
{
@ -431,13 +393,11 @@ void workspace_focus_next(void)
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);
}
@ -454,13 +414,11 @@ void workspace_focus_prev(void)
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) {