style: remove comments from header files
This commit is contained in:
parent
a6b9fa3469
commit
c76a620012
15
include/ai.h
15
include/ai.h
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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">🤖</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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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">☷</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>🤖 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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
78
src/ai.c
78
src/ai.c
@ -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);
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
25
src/atoms.c
25
src/atoms.c
@ -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)
|
||||
{
|
||||
|
||||
97
src/client.c
97
src/client.c
@ -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)
|
||||
{
|
||||
|
||||
23
src/config.c
23
src/config.c
@ -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) {
|
||||
|
||||
@ -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));
|
||||
}
|
||||
|
||||
67
src/keys.c
67
src/keys.c
@ -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)
|
||||
{
|
||||
|
||||
18
src/layout.c
18
src/layout.c
@ -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)
|
||||
{
|
||||
|
||||
129
src/main.c
129
src/main.c
@ -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;
|
||||
|
||||
29
src/news.c
29
src/news.c
@ -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 ({ or ) */
|
||||
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], "&", 5) == 0) {
|
||||
dst[j++] = '&'; i += 4;
|
||||
} else if (strncmp(&src[i], "<", 4) == 0) {
|
||||
@ -143,7 +134,6 @@ static void strip_html(char *dst, const char *src, size_t max_len)
|
||||
} else if (strncmp(&src[i], "“", 7) == 0 || strncmp(&src[i], "”", 7) == 0) {
|
||||
dst[j++] = '"'; i += 6;
|
||||
} else if (src[i+1] == '#') {
|
||||
/* Numeric entity */
|
||||
char decoded[4] = {0};
|
||||
int consumed = decode_numeric_entity(&src[i], decoded, sizeof(decoded));
|
||||
if (consumed > 0) {
|
||||
@ -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) {
|
||||
|
||||
@ -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, ¬if_width, ¬if_height);
|
||||
notif->width = notif_width;
|
||||
notif->height = notif_height;
|
||||
|
||||
/* Create notification window */
|
||||
if (dwn != NULL && dwn->display != NULL) {
|
||||
XSetWindowAttributes swa;
|
||||
swa.override_redirect = True;
|
||||
@ -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)
|
||||
|
||||
103
src/panel.c
103
src/panel.c
@ -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)");
|
||||
}
|
||||
|
||||
|
||||
102
src/systray.c
102
src/systray.c
@ -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)
|
||||
{
|
||||
|
||||
99
src/util.c
99
src/util.c
@ -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';
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user