feat: add show desktop and move window to workspace features

build: enhance security with stricter compiler flags and sanitizer build
refactor: replace standard memory functions with custom dwn_ variants
chore: remove unused wifi and dropdown menu code from systray
This commit is contained in:
retoor 2026-01-24 13:26:45 +01:00
parent 58a4a27c8c
commit 8d3dfcbc7e
33 changed files with 169 additions and 436 deletions

View File

@ -3,7 +3,8 @@
# Compiler settings
CC = gcc
CFLAGS = -Wall -Wextra -O2 -I./include
CFLAGS = -Wall -Wextra -Wpedantic -Wshadow -O2 -I./include
CFLAGS += -fstack-protector-strong -D_FORTIFY_SOURCE=2
LDFLAGS = -lX11 -lXext -lXinerama -lXrandr -lXft -lfontconfig -ldbus-1 -lcurl -lm -lpthread
# Directories
@ -42,7 +43,7 @@ endif
# MAIN TARGETS
# =============================================================================
.PHONY: all help clean install uninstall debug run test deps check-deps
.PHONY: all help clean install uninstall debug sanitize run test deps check-deps
# Default target - show help if first time, otherwise build
all: check-deps $(TARGET)
@ -69,6 +70,7 @@ help:
@echo "ALL COMMANDS:"
@echo " make - Build DWN (release version)"
@echo " make debug - Build with debug symbols"
@echo " make sanitize - Build with address/UB sanitizers"
@echo " make run - Test in Xephyr window (safe!)"
@echo " make install - Install to $(BINDIR)"
@echo " make uninstall- Remove from system"
@ -91,6 +93,13 @@ debug: CFLAGS += -g -DDEBUG
debug: clean $(TARGET)
@echo "Debug build complete!"
# Build with address and undefined behavior sanitizers
sanitize: CFLAGS += -g -fsanitize=address,undefined -fno-omit-frame-pointer
sanitize: LDFLAGS += -fsanitize=address,undefined
sanitize: clean $(TARGET)
@echo "Sanitizer build complete!"
@echo "Run with: ASAN_OPTIONS=detect_leaks=1 ./$(TARGET)"
# Link all object files into final binary
$(TARGET): $(OBJS) | $(BIN_DIR)
@echo "Linking..."

BIN
bin/dwn

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -110,6 +110,7 @@ extern ICCCMAtoms icccm;
extern MiscAtoms misc_atoms;
void atoms_init(Display *display);
void atoms_cleanup(void);
void atoms_setup_ewmh(void);
void atoms_update_client_list(void);

View File

@ -174,6 +174,10 @@ typedef struct {
bool is_alt_tabbing;
Client *alt_tab_client;
bool desktop_shown;
Window desktop_minimized[MAX_CLIENTS];
int desktop_minimized_count;
} DWNState;
extern DWNState *dwn;

View File

@ -86,6 +86,9 @@ void key_snap_right(void);
void key_snap_up(void);
void key_snap_down(void);
void key_start_demo(void);
void key_show_desktop(void);
void key_move_to_workspace_prev(void);
void key_move_to_workspace_next(void);
void tutorial_start(void);
void tutorial_stop(void);

View File

@ -10,7 +10,6 @@
#include "dwn.h"
#include <stdbool.h>
#define MAX_WIFI_NETWORKS 20
#define MAX_TRAY_ICONS 32
#define TRAY_ICON_SIZE 22
#define TRAY_ICON_SPACING 4
@ -41,21 +40,11 @@ extern TrayIcon tray_icons[MAX_TRAY_ICONS];
extern int tray_icon_count;
extern Window tray_selection_owner;
typedef struct {
char ssid[64];
int signal;
char security[32];
bool connected;
} WifiNetwork;
typedef struct {
bool enabled;
bool connected;
char current_ssid[64];
int signal_strength;
WifiNetwork networks[MAX_WIFI_NETWORKS];
int network_count;
long last_scan;
} WifiState;
typedef struct {
@ -78,29 +67,15 @@ typedef struct {
bool dragging;
} VolumeSlider;
typedef struct {
Window window;
int x, y;
int width, height;
int item_count;
int hovered_item;
bool visible;
void (*on_select)(int index);
} DropdownMenu;
extern WifiState wifi_state;
extern AudioState audio_state;
extern BatteryState battery_state;
extern DropdownMenu *wifi_menu;
extern VolumeSlider *volume_slider;
void systray_init(void);
void systray_cleanup(void);
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);
void audio_update_state(void);
@ -120,16 +95,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);
DropdownMenu *dropdown_create(int x, int y, int width);
void dropdown_destroy(DropdownMenu *menu);
void dropdown_show(DropdownMenu *menu);
void dropdown_hide(DropdownMenu *menu);
void dropdown_add_item(DropdownMenu *menu, const char *label);
void dropdown_render(DropdownMenu *menu);
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);
void systray_render(Panel *panel, int x, int *width);
int systray_get_width(void);
void systray_handle_click(int x, int y, int button);

View File

@ -39,7 +39,7 @@ static size_t write_callback(void *contents, size_t size, size_t nmemb, void *us
size_t realsize = size * nmemb;
ResponseBuffer *buf = (ResponseBuffer *)userp;
char *ptr = realloc(buf->data, buf->size + realsize + 1);
char *ptr = dwn_realloc(buf->data, buf->size + realsize + 1);
if (ptr == NULL) {
return 0;
}

View File

@ -244,9 +244,11 @@ static void add_to_recent(const char *desktop_id)
for (int i = 0; i < launcher_state.recent_count; i++) {
if (strcmp(launcher_state.recent[i], desktop_id) == 0) {
for (int j = i; j > 0; j--) {
strcpy(launcher_state.recent[j], launcher_state.recent[j-1]);
memcpy(launcher_state.recent[j], launcher_state.recent[j-1],
sizeof(launcher_state.recent[0]));
}
strcpy(launcher_state.recent[0], desktop_id);
snprintf(launcher_state.recent[0], sizeof(launcher_state.recent[0]),
"%s", desktop_id);
save_recent_apps();
return;
}
@ -257,9 +259,11 @@ static void add_to_recent(const char *desktop_id)
}
for (int i = launcher_state.recent_count - 1; i > 0; i--) {
strcpy(launcher_state.recent[i], launcher_state.recent[i-1]);
memcpy(launcher_state.recent[i], launcher_state.recent[i-1],
sizeof(launcher_state.recent[0]));
}
strncpy(launcher_state.recent[0], desktop_id, sizeof(launcher_state.recent[0]) - 1);
snprintf(launcher_state.recent[0], sizeof(launcher_state.recent[0]),
"%s", desktop_id);
save_recent_apps();
}
@ -330,16 +334,16 @@ void applauncher_show(void)
}
size_t buf_size = launcher_state.app_count * 140;
char *choices = malloc(buf_size);
char *choices = dwn_malloc(buf_size);
if (choices == NULL) {
LOG_ERROR("Failed to allocate memory for app list");
return;
}
choices[0] = '\0';
bool *added = calloc(launcher_state.app_count, sizeof(bool));
bool *added = dwn_calloc(launcher_state.app_count, sizeof(bool));
if (added == NULL) {
free(choices);
dwn_free(choices);
return;
}
@ -379,19 +383,19 @@ void applauncher_show(void)
choices[pos-1] = '\0';
}
free(added);
dwn_free(added);
char tmp_path[] = "/tmp/dwn_apps_XXXXXX";
int tmp_fd = mkstemp(tmp_path);
if (tmp_fd < 0) {
free(choices);
dwn_free(choices);
LOG_ERROR("Failed to create temp file for app list");
return;
}
ssize_t written = write(tmp_fd, choices, strlen(choices));
close(tmp_fd);
free(choices);
dwn_free(choices);
if (written < 0) {
unlink(tmp_path);

View File

@ -16,6 +16,8 @@ EWMHAtoms ewmh;
ICCCMAtoms icccm;
MiscAtoms misc_atoms;
static Window ewmh_check_window = None;
#define ATOM(name) XInternAtom(display, name, False)
void atoms_init(Display *display)
@ -124,7 +126,8 @@ void atoms_setup_ewmh(void)
Display *dpy = dwn->display;
Window root = dwn->root;
Window check = XCreateSimpleWindow(dpy, root, 0, 0, 1, 1, 0, 0, 0);
ewmh_check_window = XCreateSimpleWindow(dpy, root, 0, 0, 1, 1, 0, 0, 0);
Window check = ewmh_check_window;
XChangeProperty(dpy, root, ewmh.NET_SUPPORTING_WM_CHECK,
XA_WINDOW, 32, PropModeReplace,
@ -510,3 +513,16 @@ bool atoms_update_wm_state(Window window, Atom state, bool add)
return true;
}
void atoms_cleanup(void)
{
if (dwn == NULL || dwn->display == NULL) {
return;
}
if (ewmh_check_window != None) {
XDestroyWindow(dwn->display, ewmh_check_window);
ewmh_check_window = None;
LOG_DEBUG("EWMH check window destroyed");
}
}

View File

@ -410,7 +410,8 @@ static void demo_phase_workspaces(void)
"DWN provides 9 virtual workspaces:\n\n"
"- F1 to F9: Switch workspace\n"
"- Shift+F1-F9: Move window to workspace\n"
"- Ctrl+Alt+Left/Right: Navigate\n\n"
"- Ctrl+Alt+Left/Right: Navigate\n"
"- Super+Shift+Left/Right: Move window\n\n"
"Watch the workspace indicators in the panel...", 0);
demo_wait_for(DEMO_WAIT_TIME, demo_get_step_delay());
demo_next_step();

View File

@ -92,140 +92,161 @@ static const TutorialStep tutorial_steps[] = {
MOD_SUPER, XK_t
},
{
"1/20: Open Terminal",
"1/23: Open Terminal",
"The terminal is your command center.\n\n"
"Press: Ctrl + Alt + T",
"Hold Ctrl and Alt, then press T",
MOD_CTRL | MOD_ALT, XK_t
},
{
"2/20: App Launcher",
"2/23: App Launcher",
"Launch any application by name.\n\n"
"Press: Alt + F2",
"Type app name and press Enter",
MOD_ALT, XK_F2
},
{
"3/20: File Manager",
"3/23: File Manager",
"Open the file manager.\n\n"
"Press: Super + E",
"",
MOD_SUPER, XK_e
},
{
"4/20: Web Browser",
"4/23: Web Browser",
"Open your default web browser.\n\n"
"Press: Super + B",
"",
MOD_SUPER, XK_b
},
{
"5/20: Switch Windows",
"5/23: Switch Windows",
"Cycle through open windows.\n\n"
"Press: Alt + Tab",
"Hold Alt, press Tab repeatedly",
MOD_ALT, XK_Tab
},
{
"6/20: Close Window",
"6/23: Close Window",
"Close the focused window.\n\n"
"Press: Alt + F4",
"",
MOD_ALT, XK_F4
},
{
"7/20: Toggle Maximize",
"7/23: Show Desktop",
"Minimize all windows to show desktop.\n\n"
"Press: Super + D",
"Press again to restore windows",
MOD_SUPER, XK_d
},
{
"8/23: Toggle Maximize",
"Maximize or restore a window.\n\n"
"Press: Alt + F10",
"",
MOD_ALT, XK_F10
},
{
"8/20: Toggle Fullscreen",
"9/23: Toggle Fullscreen",
"Make a window fullscreen.\n\n"
"Press: Alt + F11",
"Press again to exit",
MOD_ALT, XK_F11
},
{
"9/20: Toggle Floating",
"10/23: Toggle Floating",
"Make window float above tiled windows.\n\n"
"Press: Super + F9",
"",
MOD_SUPER, XK_F9
},
{
"10/20: Next Workspace",
"11/23: Next Workspace",
"Switch to the next virtual desktop.\n\n"
"Press: Ctrl + Alt + Right",
"",
MOD_CTRL | MOD_ALT, XK_Right
},
{
"11/20: Previous Workspace",
"12/23: Previous Workspace",
"Switch to the previous workspace.\n\n"
"Press: Ctrl + Alt + Left",
"",
MOD_CTRL | MOD_ALT, XK_Left
},
{
"12/20: Go to Workspace",
"13/23: Go to Workspace",
"Jump directly to workspace 1.\n\n"
"Press: F1",
"Use F1-F9 for workspaces 1-9",
0, XK_F1
},
{
"13/20: Cycle Layout",
"14/23: Move Window to Prev Workspace",
"Send the focused window to previous workspace.\n\n"
"Press: Super + Shift + Left",
"Window moves, you stay",
MOD_SUPER | MOD_SHIFT, XK_Left
},
{
"15/23: Move Window to Next Workspace",
"Send the focused window to next workspace.\n\n"
"Press: Super + Shift + Right",
"Window moves, you stay",
MOD_SUPER | MOD_SHIFT, XK_Right
},
{
"16/23: Cycle Layout",
"Switch between tiling, floating, monocle.\n\n"
"Press: Super + Space",
"Try it multiple times!",
MOD_SUPER, XK_space
},
{
"14/20: Expand Master",
"17/23: Expand Master",
"Make the master area larger.\n\n"
"Press: Super + L",
"",
MOD_SUPER, XK_l
},
{
"15/20: Shrink Master",
"18/23: Shrink Master",
"Make the master area smaller.\n\n"
"Press: Super + H",
"",
MOD_SUPER, XK_h
},
{
"16/20: Add to Master",
"19/23: Add to Master",
"Increase windows in master area.\n\n"
"Press: Super + I",
"",
MOD_SUPER, XK_i
},
{
"17/20: AI Command",
"20/23: AI Command",
"Ask AI to run commands for you.\n\n"
"Press: Super + A",
"Requires OPENROUTER_API_KEY",
MOD_SUPER, XK_a
},
{
"18/20: AI Context",
"21/23: AI Context",
"Show AI analysis of your current task.\n\n"
"Press: Super + Shift + A",
"Requires OPENROUTER_API_KEY",
MOD_SUPER | MOD_SHIFT, XK_a
},
{
"19/20: Exa Search",
"22/23: Exa Search",
"Semantic web search.\n\n"
"Press: Super + Shift + E",
"Requires EXA_API_KEY",
MOD_SUPER | MOD_SHIFT, XK_e
},
{
"20/20: Show Shortcuts",
"23/23: Show Shortcuts",
"Display all keyboard shortcuts.\n\n"
"Press: Super + S",
"Shows complete reference",
@ -519,10 +540,14 @@ void keys_setup_defaults(void)
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");
keys_bind(MOD_SUPER, XK_minus, key_decrease_master_count, "Decrease master count");
keys_bind(MOD_SUPER, XK_d, key_show_desktop, "Show desktop");
keys_bind(MOD_SUPER, XK_Left, key_snap_left, "Snap window left");
keys_bind(MOD_SUPER, XK_Right, key_snap_right, "Snap window right");
keys_bind(MOD_SUPER | MOD_SHIFT, XK_Left, key_move_to_workspace_prev, "Move window to previous workspace");
keys_bind(MOD_SUPER | MOD_SHIFT, XK_Right, key_move_to_workspace_next, "Move window to next workspace");
keys_bind(MOD_SUPER, XK_a, key_ai_command, "AI command");
keys_bind(MOD_SUPER | MOD_SHIFT, XK_a, key_toggle_ai, "Toggle AI context");
@ -725,6 +750,63 @@ void key_decrease_master_count(void)
workspace_adjust_master_count(dwn->current_workspace, -1);
}
void key_show_desktop(void)
{
if (dwn == NULL) {
return;
}
if (!dwn->desktop_shown) {
dwn->desktop_minimized_count = 0;
for (Client *c = dwn->client_list; c != NULL; c = c->next) {
if (c->workspace == (unsigned int)dwn->current_workspace &&
!client_is_minimized(c) &&
dwn->desktop_minimized_count < MAX_CLIENTS) {
dwn->desktop_minimized[dwn->desktop_minimized_count++] = c->window;
client_minimize(c);
}
}
dwn->desktop_shown = true;
} else {
for (int i = 0; i < dwn->desktop_minimized_count; i++) {
Client *c = client_find_by_window(dwn->desktop_minimized[i]);
if (c != NULL && c->workspace == (unsigned int)dwn->current_workspace) {
client_restore(c);
}
}
dwn->desktop_minimized_count = 0;
dwn->desktop_shown = false;
}
}
void key_move_to_workspace_prev(void)
{
if (dwn == NULL) {
return;
}
int target = dwn->current_workspace - 1;
if (target < 0) {
target = MAX_WORKSPACES - 1;
}
move_focused_to_workspace(target);
}
void key_move_to_workspace_next(void)
{
if (dwn == NULL) {
return;
}
int target = dwn->current_workspace + 1;
if (target >= MAX_WORKSPACES) {
target = 0;
}
move_focused_to_workspace(target);
}
void key_toggle_ai(void)
{
if (dwn == NULL) {

View File

@ -416,15 +416,6 @@ static void handle_button_press(XButtonEvent *ev)
}
}
if (wifi_menu != NULL && wifi_menu->visible) {
if (ev->window == wifi_menu->window) {
dropdown_handle_click(wifi_menu, ev->x, ev->y);
return;
} else {
dropdown_hide(wifi_menu);
}
}
Notification *notif = notification_find_by_window(ev->window);
if (notif != NULL) {
notification_close(notif->id);
@ -531,11 +522,6 @@ static void handle_motion_notify(XMotionEvent *ev)
return;
}
if (wifi_menu != NULL && wifi_menu->visible && ev->window == wifi_menu->window) {
dropdown_handle_motion(wifi_menu, ev->x, ev->y);
return;
}
if (dwn->drag_client == NULL) {
return;
}

View File

@ -54,7 +54,7 @@ static size_t news_curl_write_cb(void *contents, size_t size, size_t nmemb, void
size_t realsize = size * nmemb;
CurlBuffer *buf = (CurlBuffer *)userp;
char *ptr = realloc(buf->data, buf->size + realsize + 1);
char *ptr = dwn_realloc(buf->data, buf->size + realsize + 1);
if (ptr == NULL) {
return 0;
}
@ -374,7 +374,15 @@ static void *news_fetch_thread(void *arg)
}
CurlBuffer buf = {0};
buf.data = malloc(1);
buf.data = dwn_malloc(1);
if (buf.data == NULL) {
curl_easy_cleanup(curl);
pthread_mutex_lock(&news_mutex);
news_state.fetching = false;
news_state.has_error = true;
pthread_mutex_unlock(&news_mutex);
return NULL;
}
buf.data[0] = '\0';
curl_easy_setopt(curl, CURLOPT_URL, NEWS_API_URL);

View File

@ -552,8 +552,8 @@ void panel_handle_click(Panel *panel, int x, int y, int button)
if (client_is_minimized(c)) {
client_restore(c);
} else {
Workspace *ws = workspace_get(dwn->current_workspace);
if (ws != NULL && ws->focused == c) {
Workspace *current = workspace_get(dwn->current_workspace);
if (current != NULL && current->focused == c) {
client_minimize(c);
} else {
client_focus(c, true);

View File

@ -35,20 +35,14 @@
#define ASCII_CHARGING "+"
#define SYSTRAY_SPACING 12
#define DROPDOWN_ITEM_HEIGHT 28
#define DROPDOWN_WIDTH 400
#define DROPDOWN_PADDING 8
#define SLIDER_WIDTH 30
#define SLIDER_HEIGHT 120
#define SLIDER_PADDING 8
#define SLIDER_KNOB_HEIGHT 8
#define WIFI_SCAN_INTERVAL 10000
WifiState wifi_state = {0};
AudioState audio_state = {0};
BatteryState battery_state = {0};
DropdownMenu *wifi_menu = NULL;
VolumeSlider *volume_slider = NULL;
TrayIcon tray_icons[MAX_TRAY_ICONS];
@ -174,10 +168,6 @@ void systray_cleanup(void)
pthread_join(update_thread, NULL);
}
if (wifi_menu != NULL) {
dropdown_destroy(wifi_menu);
wifi_menu = NULL;
}
if (volume_slider != NULL) {
volume_slider_destroy(volume_slider);
volume_slider = NULL;
@ -309,82 +299,6 @@ void wifi_update_state(void)
{
}
void wifi_scan_networks(void)
{
FILE *fp;
char line[512];
WifiNetwork temp_networks[MAX_WIFI_NETWORKS];
int temp_count = 0;
long scan_time = get_time_ms();
fp = popen("nmcli -t -f SSID,SIGNAL,SECURITY,IN-USE dev wifi list 2>/dev/null", "r");
if (fp == NULL) {
LOG_WARN("Failed to scan WiFi networks");
return;
}
while (fgets(line, sizeof(line), fp) != NULL && temp_count < MAX_WIFI_NETWORKS) {
line[strcspn(line, "\n")] = '\0';
char *ssid = strtok(line, ":");
char *signal = strtok(NULL, ":");
char *security = strtok(NULL, ":");
char *in_use = strtok(NULL, ":");
if (ssid != NULL && strlen(ssid) > 0) {
WifiNetwork *net = &temp_networks[temp_count];
strncpy(net->ssid, ssid, sizeof(net->ssid) - 1);
net->ssid[sizeof(net->ssid) - 1] = '\0';
net->signal = signal ? atoi(signal) : 0;
strncpy(net->security, security ? security : "", sizeof(net->security) - 1);
net->security[sizeof(net->security) - 1] = '\0';
net->connected = (in_use != NULL && in_use[0] == '*');
temp_count++;
}
}
pclose(fp);
pthread_mutex_lock(&state_mutex);
memcpy(wifi_state.networks, temp_networks, sizeof(temp_networks));
wifi_state.network_count = temp_count;
wifi_state.last_scan = scan_time;
pthread_mutex_unlock(&state_mutex);
LOG_DEBUG("Scanned %d WiFi networks", temp_count);
}
void wifi_connect(const char *ssid)
{
if (ssid == NULL || strlen(ssid) == 0) {
return;
}
char cmd[256];
snprintf(cmd, sizeof(cmd), "nmcli dev wifi connect \"%s\" &", ssid);
char msg[128];
snprintf(msg, sizeof(msg), "Connecting to %s...", ssid);
notification_show("WiFi", "Connecting", msg, NULL, 3000);
spawn_async(cmd);
pthread_mutex_lock(&state_mutex);
wifi_state.connected = false;
pthread_mutex_unlock(&state_mutex);
}
void wifi_disconnect(void)
{
spawn_async("nmcli dev disconnect wlan0 &");
pthread_mutex_lock(&state_mutex);
wifi_state.connected = false;
pthread_mutex_unlock(&state_mutex);
notification_show("WiFi", "Disconnected", "WiFi connection closed", NULL, 2000);
}
const char *wifi_get_icon(void)
{
if (!wifi_state.enabled || !wifi_state.connected) {
@ -615,227 +529,6 @@ void volume_slider_handle_release(VolumeSlider *slider)
}
DropdownMenu *dropdown_create(int x, int y, int width)
{
if (dwn == NULL || dwn->display == NULL) {
return NULL;
}
DropdownMenu *menu = dwn_calloc(1, sizeof(DropdownMenu));
menu->x = x;
menu->y = y;
menu->width = width;
menu->height = 0;
menu->item_count = 0;
menu->hovered_item = -1;
menu->visible = false;
menu->on_select = NULL;
return menu;
}
void dropdown_destroy(DropdownMenu *menu)
{
if (menu == NULL) {
return;
}
if (menu->window != None && dwn != NULL && dwn->display != NULL) {
XDestroyWindow(dwn->display, menu->window);
}
dwn_free(menu);
}
void dropdown_show(DropdownMenu *menu)
{
if (menu == NULL || dwn == NULL || dwn->display == NULL) {
return;
}
wifi_scan_networks();
menu->item_count = wifi_state.network_count;
menu->height = menu->item_count * DROPDOWN_ITEM_HEIGHT + 2 * DROPDOWN_PADDING;
if (menu->height < DROPDOWN_ITEM_HEIGHT + 2 * DROPDOWN_PADDING) {
menu->height = DROPDOWN_ITEM_HEIGHT + 2 * DROPDOWN_PADDING;
menu->item_count = 1;
}
int max_text_width = 0;
for (int i = 0; i < wifi_state.network_count; i++) {
WifiNetwork *net = &wifi_state.networks[i];
char label[128];
snprintf(label, sizeof(label), "%s %d%%", net->ssid, net->signal);
int text_width = get_text_width(label);
if (text_width > max_text_width) {
max_text_width = text_width;
}
}
int min_width = 250;
int calculated_width = max_text_width + 2 * DROPDOWN_PADDING + 30;
menu->width = (calculated_width > min_width) ? calculated_width : min_width;
if (menu->window == None) {
XSetWindowAttributes swa;
swa.override_redirect = True;
swa.background_pixel = dwn->config->colors.panel_bg;
swa.border_pixel = dwn->config->colors.border_focused;
swa.event_mask = ExposureMask | ButtonPressMask | PointerMotionMask |
LeaveWindowMask | EnterWindowMask;
menu->window = XCreateWindow(dwn->display, dwn->root,
menu->x, menu->y,
menu->width, menu->height,
0,
CopyFromParent, InputOutput, CopyFromParent,
CWOverrideRedirect | CWBackPixel | CWBorderPixel | CWEventMask,
&swa);
} else {
XMoveResizeWindow(dwn->display, menu->window,
menu->x, menu->y, menu->width, menu->height);
}
menu->visible = true;
XMapRaised(dwn->display, menu->window);
dropdown_render(menu);
}
void dropdown_hide(DropdownMenu *menu)
{
if (menu == NULL || menu->window == None) {
return;
}
menu->visible = false;
XUnmapWindow(dwn->display, menu->window);
}
void dropdown_render(DropdownMenu *menu)
{
if (menu == NULL || menu->window == None || !menu->visible) {
return;
}
if (dwn == NULL || dwn->display == NULL) {
return;
}
Display *dpy = dwn->display;
const ColorScheme *colors = config_get_colors();
int font_height = 12;
if (dwn->xft_font != NULL) {
font_height = dwn->xft_font->ascent;
} else if (dwn->font != NULL) {
font_height = dwn->font->ascent;
}
int text_y_offset = (DROPDOWN_ITEM_HEIGHT + font_height) / 2;
XSetForeground(dpy, dwn->gc, colors->panel_bg);
XFillRectangle(dpy, menu->window, dwn->gc, 0, 0, menu->width, menu->height);
if (wifi_state.network_count == 0) {
draw_utf8_text(menu->window, DROPDOWN_PADDING, DROPDOWN_PADDING + text_y_offset,
"No networks found", colors->workspace_inactive);
} else {
for (int i = 0; i < wifi_state.network_count && i < MAX_WIFI_NETWORKS; i++) {
WifiNetwork *net = &wifi_state.networks[i];
int y = DROPDOWN_PADDING + i * DROPDOWN_ITEM_HEIGHT;
if (i == menu->hovered_item) {
XSetForeground(dpy, dwn->gc, colors->workspace_active);
XFillRectangle(dpy, menu->window, dwn->gc,
2, y, menu->width - 4, DROPDOWN_ITEM_HEIGHT);
}
unsigned long text_color = (i == menu->hovered_item) ? colors->panel_bg : colors->panel_fg;
char label[80];
if (net->connected) {
snprintf(label, sizeof(label), "> %s", net->ssid);
} else {
snprintf(label, sizeof(label), " %s", net->ssid);
}
if (strlen(label) > 25) {
label[22] = '.';
label[23] = '.';
label[24] = '.';
label[25] = '\0';
}
draw_utf8_text(menu->window, DROPDOWN_PADDING, y + text_y_offset, label, text_color);
char signal[8];
snprintf(signal, sizeof(signal), "%d%%", net->signal);
int signal_width = get_text_width(signal);
int signal_x = menu->width - DROPDOWN_PADDING - signal_width;
unsigned long signal_color = (i == menu->hovered_item) ? colors->panel_bg : colors->workspace_inactive;
draw_utf8_text(menu->window, signal_x, y + text_y_offset, signal, signal_color);
}
}
XFlush(dpy);
}
int dropdown_hit_test(DropdownMenu *menu, int x, int y)
{
(void)x;
if (menu == NULL || !menu->visible) {
return -1;
}
if (y < DROPDOWN_PADDING || y >= menu->height - DROPDOWN_PADDING) {
return -1;
}
int item = (y - DROPDOWN_PADDING) / DROPDOWN_ITEM_HEIGHT;
if (item >= 0 && item < menu->item_count) {
return item;
}
return -1;
}
void dropdown_handle_click(DropdownMenu *menu, int x, int y)
{
if (menu == NULL || !menu->visible) {
return;
}
int item = dropdown_hit_test(menu, x, y);
if (item >= 0 && item < wifi_state.network_count) {
WifiNetwork *net = &wifi_state.networks[item];
if (net->connected) {
wifi_disconnect();
} else {
wifi_connect(net->ssid);
}
dropdown_hide(menu);
}
}
void dropdown_handle_motion(DropdownMenu *menu, int x, int y)
{
if (menu == NULL || !menu->visible) {
return;
}
int old_hovered = menu->hovered_item;
menu->hovered_item = dropdown_hit_test(menu, x, y);
if (menu->hovered_item != old_hovered) {
dropdown_render(menu);
}
}
int systray_get_width(void)
{
pthread_mutex_lock(&state_mutex);
@ -955,34 +648,7 @@ void systray_handle_click(int x, int y, int button)
return;
}
if (widget == 0) {
if (volume_slider != NULL && volume_slider->visible) {
volume_slider_hide(volume_slider);
}
if (button == 1) {
if (wifi_menu == NULL) {
int panel_height = config_get_panel_height();
wifi_menu = dropdown_create(wifi_icon_x, panel_height, DROPDOWN_WIDTH);
}
if (wifi_menu->visible) {
dropdown_hide(wifi_menu);
} else {
wifi_menu->x = wifi_icon_x;
wifi_menu->y = config_get_panel_height();
dropdown_show(wifi_menu);
}
} else if (button == 3) {
if (wifi_state.connected) {
wifi_disconnect();
}
}
} else if (widget == 1) {
if (wifi_menu != NULL && wifi_menu->visible) {
dropdown_hide(wifi_menu);
}
if (widget == 1) {
if (button == 1) {
if (volume_slider == NULL) {
int panel_height = config_get_panel_height();
@ -1019,18 +685,6 @@ void systray_handle_click(int x, int y, int button)
void systray_update(void)
{
if (wifi_menu != NULL && wifi_menu->visible) {
long now = get_time_ms();
pthread_mutex_lock(&state_mutex);
long last_scan = wifi_state.last_scan;
pthread_mutex_unlock(&state_mutex);
if (now - last_scan >= WIFI_SCAN_INTERVAL) {
wifi_scan_networks();
dropdown_render(wifi_menu);
}
}
}