/* * DWN - Desktop Window Manager * retoor * System tray widgets implementation with UTF-8 support */ #include "systray.h" #include "panel.h" #include "config.h" #include "util.h" #include "notifications.h" #include #include #include #include #include #include #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" #define ASCII_WIFI_ON "W" #define ASCII_WIFI_OFF "-" #define ASCII_VOL_HIGH "V" #define ASCII_VOL_MUTE "M" #define ASCII_BATTERY "B" #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; static pthread_t update_thread; static pthread_mutex_t state_mutex = PTHREAD_MUTEX_INITIALIZER; static volatile int thread_running = 0; static int systray_x = 0; static int systray_width = 0; static int wifi_icon_x = 0; static int audio_icon_x = 0; static void wifi_update_state_internal(void); static void audio_update_state_internal(void); static void draw_utf8_text(Drawable d, int x, int y, const char *text, unsigned long color) { if (dwn == NULL || dwn->display == NULL || text == NULL) { return; } if (dwn->xft_font != NULL) { XftDraw *xft_draw = XftDrawCreate(dwn->display, d, DefaultVisual(dwn->display, dwn->screen), dwn->colormap); if (xft_draw != NULL) { XftColor xft_color; XRenderColor render_color; render_color.red = ((color >> 16) & 0xFF) * 257; render_color.green = ((color >> 8) & 0xFF) * 257; render_color.blue = (color & 0xFF) * 257; render_color.alpha = 0xFFFF; XftColorAllocValue(dwn->display, DefaultVisual(dwn->display, dwn->screen), dwn->colormap, &render_color, &xft_color); XftDrawStringUtf8(xft_draw, &xft_color, dwn->xft_font, x, y, (const FcChar8 *)text, strlen(text)); XftColorFree(dwn->display, DefaultVisual(dwn->display, dwn->screen), dwn->colormap, &xft_color); XftDrawDestroy(xft_draw); return; } } XSetForeground(dwn->display, dwn->gc, color); XDrawString(dwn->display, d, dwn->gc, x, y, text, strlen(text)); } static int get_text_width(const char *text) { if (dwn == NULL || text == NULL) { return 0; } if (dwn->xft_font != NULL) { XGlyphInfo extents; XftTextExtentsUtf8(dwn->display, dwn->xft_font, (const FcChar8 *)text, strlen(text), &extents); return extents.xOff; } if (dwn->font != NULL) { return XTextWidth(dwn->font, text, strlen(text)); } return strlen(text) * 8; } static void *systray_update_thread(void *arg) { (void)arg; while (thread_running) { wifi_update_state_internal(); audio_update_state_internal(); pthread_mutex_lock(&state_mutex); battery_update_state(); pthread_mutex_unlock(&state_mutex); for (int i = 0; i < 20 && thread_running; i++) { usleep(100000); } } return NULL; } void systray_init(void) { memset(&wifi_state, 0, sizeof(wifi_state)); memset(&audio_state, 0, sizeof(audio_state)); memset(&battery_state, 0, sizeof(battery_state)); audio_state.volume = 50; wifi_state.enabled = true; thread_running = 1; if (pthread_create(&update_thread, NULL, systray_update_thread, NULL) != 0) { LOG_WARN("Failed to create systray update thread"); thread_running = 0; } LOG_INFO("System tray initialized"); } void systray_cleanup(void) { if (thread_running) { thread_running = 0; 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; } } void battery_update_state(void) { DIR *dir; struct dirent *entry; char path[512]; char value[64]; FILE *fp; battery_state.present = false; battery_state.charging = false; battery_state.percentage = 0; dir = opendir("/sys/class/power_supply"); if (dir == NULL) { return; } while ((entry = readdir(dir)) != NULL) { if (entry->d_name[0] == '.') { continue; } snprintf(path, sizeof(path), "/sys/class/power_supply/%s/type", entry->d_name); fp = fopen(path, "r"); if (fp != NULL) { if (fgets(value, sizeof(value), fp) != NULL) { value[strcspn(value, "\n")] = '\0'; if (strcmp(value, "Battery") == 0) { battery_state.present = true; snprintf(path, sizeof(path), "/sys/class/power_supply/%s/capacity", entry->d_name); FILE *cap_fp = fopen(path, "r"); if (cap_fp != NULL) { if (fgets(value, sizeof(value), cap_fp) != NULL) { battery_state.percentage = atoi(value); } fclose(cap_fp); } snprintf(path, sizeof(path), "/sys/class/power_supply/%s/status", entry->d_name); FILE *status_fp = fopen(path, "r"); if (status_fp != NULL) { if (fgets(value, sizeof(value), status_fp) != NULL) { value[strcspn(value, "\n")] = '\0'; battery_state.charging = (strcmp(value, "Charging") == 0 || strcmp(value, "Full") == 0); } fclose(status_fp); } fclose(fp); break; } } fclose(fp); } } closedir(dir); } const char *battery_get_icon(void) { if (!battery_state.present) { return ""; } if (battery_state.charging) { return ASCII_CHARGING; } return ASCII_BATTERY; } static void wifi_update_state_internal(void) { FILE *fp; char line[256]; WifiState temp_state = {0}; 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) { line[strcspn(line, "\n")] = '\0'; if (strstr(line, "GENERAL.STATE:") != NULL) { if (strstr(line, "100 (connected)") != NULL) { temp_state.connected = true; temp_state.enabled = true; } } if (strstr(line, "GENERAL.CONNECTION:") != NULL) { char *ssid = strchr(line, ':'); if (ssid != NULL && strlen(ssid) > 1) { ssid++; strncpy(temp_state.current_ssid, ssid, sizeof(temp_state.current_ssid) - 1); } } } pclose(fp); } 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) { if (fgets(line, sizeof(line), fp) != NULL) { temp_state.signal_strength = atoi(line); } pclose(fp); } } pthread_mutex_lock(&state_mutex); wifi_state.connected = temp_state.connected; wifi_state.enabled = temp_state.enabled; wifi_state.signal_strength = temp_state.signal_strength; memcpy(wifi_state.current_ssid, temp_state.current_ssid, sizeof(wifi_state.current_ssid)); pthread_mutex_unlock(&state_mutex); } 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) { return ASCII_WIFI_OFF; } return ASCII_WIFI_ON; } static void audio_update_state_internal(void) { FILE *fp; char line[256]; int volume = 50; bool muted = false; fp = popen("amixer get Master 2>/dev/null | grep -o '[0-9]*%\\|\\[on\\]\\|\\[off\\]' | head -2", "r"); if (fp != NULL) { while (fgets(line, sizeof(line), fp) != NULL) { line[strcspn(line, "\n")] = '\0'; if (strchr(line, '%') != NULL) { volume = atoi(line); } else if (strcmp(line, "[off]") == 0) { muted = true; } else if (strcmp(line, "[on]") == 0) { muted = false; } } pclose(fp); } pthread_mutex_lock(&state_mutex); audio_state.volume = volume; audio_state.muted = muted; pthread_mutex_unlock(&state_mutex); } void audio_update_state(void) { } void audio_set_volume(int volume) { if (volume < 0) volume = 0; if (volume > 100) volume = 100; char cmd[64]; snprintf(cmd, sizeof(cmd), "amixer set Master %d%% >/dev/null 2>&1 &", volume); spawn_async(cmd); pthread_mutex_lock(&state_mutex); audio_state.volume = volume; audio_state.muted = false; pthread_mutex_unlock(&state_mutex); } void audio_toggle_mute(void) { spawn_async("amixer set Master toggle >/dev/null 2>&1 &"); pthread_mutex_lock(&state_mutex); audio_state.muted = !audio_state.muted; pthread_mutex_unlock(&state_mutex); } const char *audio_get_icon(void) { if (audio_state.muted || audio_state.volume == 0) { return ASCII_VOL_MUTE; } return ASCII_VOL_HIGH; } VolumeSlider *volume_slider_create(int x, int y) { if (dwn == NULL || dwn->display == NULL) { return NULL; } VolumeSlider *slider = dwn_calloc(1, sizeof(VolumeSlider)); slider->x = x; slider->y = y; slider->width = SLIDER_WIDTH; slider->height = SLIDER_HEIGHT; slider->visible = false; slider->dragging = false; return slider; } void volume_slider_destroy(VolumeSlider *slider) { if (slider == NULL) { return; } if (slider->window != None && dwn != NULL && dwn->display != NULL) { XDestroyWindow(dwn->display, slider->window); } dwn_free(slider); } void volume_slider_show(VolumeSlider *slider) { if (slider == NULL || dwn == NULL || dwn->display == NULL) { return; } if (slider->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 | ButtonReleaseMask | PointerMotionMask | LeaveWindowMask; slider->window = XCreateWindow(dwn->display, dwn->root, slider->x, slider->y, slider->width, slider->height, 1, CopyFromParent, InputOutput, CopyFromParent, CWOverrideRedirect | CWBackPixel | CWBorderPixel | CWEventMask, &swa); } slider->visible = true; XMapRaised(dwn->display, slider->window); volume_slider_render(slider); } void volume_slider_hide(VolumeSlider *slider) { if (slider == NULL || slider->window == None) { return; } slider->visible = false; slider->dragging = false; XUnmapWindow(dwn->display, slider->window); } void volume_slider_render(VolumeSlider *slider) { if (slider == NULL || slider->window == None || !slider->visible) { return; } Display *dpy = dwn->display; const ColorScheme *colors = config_get_colors(); XSetForeground(dpy, dwn->gc, colors->panel_bg); XFillRectangle(dpy, slider->window, dwn->gc, 0, 0, slider->width, slider->height); int track_x = slider->width / 2 - 2; int track_y = SLIDER_PADDING; int track_height = slider->height - 2 * SLIDER_PADDING; XSetForeground(dpy, dwn->gc, colors->workspace_inactive); XFillRectangle(dpy, slider->window, dwn->gc, track_x, track_y, 4, track_height); 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); 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) { knob_y = track_y + track_height - SLIDER_KNOB_HEIGHT; } XSetForeground(dpy, dwn->gc, colors->panel_fg); XFillRectangle(dpy, slider->window, dwn->gc, track_x - 4, knob_y, 12, SLIDER_KNOB_HEIGHT); char vol_text[8]; snprintf(vol_text, sizeof(vol_text), "%d%%", audio_state.volume); int text_width = get_text_width(vol_text); int text_x = (slider->width - text_width) / 2; draw_utf8_text(slider->window, text_x, slider->height - 4, vol_text, colors->panel_fg); XFlush(dpy); } void volume_slider_handle_click(VolumeSlider *slider, int x, int y) { (void)x; if (slider == NULL || !slider->visible) { return; } slider->dragging = true; int track_y = SLIDER_PADDING; int track_height = slider->height - 2 * SLIDER_PADDING; int relative_y = y - track_y; if (relative_y < 0) relative_y = 0; if (relative_y > track_height) relative_y = track_height; int new_volume = 100 - (relative_y * 100 / track_height); audio_set_volume(new_volume); volume_slider_render(slider); panel_render_all(); } void volume_slider_handle_motion(VolumeSlider *slider, int x, int y) { if (slider == NULL || !slider->visible || !slider->dragging) { return; } volume_slider_handle_click(slider, x, y); } void volume_slider_handle_release(VolumeSlider *slider) { if (slider == NULL) { return; } slider->dragging = false; } 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, 1, 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); AudioState audio_snap = audio_state; WifiState wifi_snap; wifi_snap.connected = wifi_state.connected; wifi_snap.enabled = wifi_state.enabled; wifi_snap.signal_strength = wifi_state.signal_strength; memcpy(wifi_snap.current_ssid, wifi_state.current_ssid, sizeof(wifi_snap.current_ssid)); pthread_mutex_unlock(&state_mutex); char audio_label[32]; const char *audio_icon = (audio_snap.muted || audio_snap.volume == 0) ? ASCII_VOL_MUTE : ASCII_VOL_HIGH; snprintf(audio_label, sizeof(audio_label), "%s%d%%", audio_icon, audio_snap.volume); char wifi_label[128]; const char *wifi_icon_str = (!wifi_snap.enabled || !wifi_snap.connected) ? ASCII_WIFI_OFF : ASCII_WIFI_ON; if (wifi_snap.connected && wifi_snap.current_ssid[0] != '\0') { snprintf(wifi_label, sizeof(wifi_label), "%s %s %d%%", wifi_icon_str, wifi_snap.current_ssid, wifi_snap.signal_strength); } else { snprintf(wifi_label, sizeof(wifi_label), "%s", wifi_icon_str); } return get_text_width(audio_label) + SYSTRAY_SPACING + get_text_width(wifi_label); } void systray_render(Panel *panel, int x, int *width) { if (panel == NULL || width == NULL) { return; } const ColorScheme *colors = config_get_colors(); pthread_mutex_lock(&state_mutex); AudioState audio_snap = audio_state; WifiState wifi_snap; wifi_snap.connected = wifi_state.connected; wifi_snap.enabled = wifi_state.enabled; wifi_snap.signal_strength = wifi_state.signal_strength; memcpy(wifi_snap.current_ssid, wifi_state.current_ssid, sizeof(wifi_snap.current_ssid)); pthread_mutex_unlock(&state_mutex); 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 = (panel->height + font_height) / 2; systray_x = x; int current_x = x; 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; snprintf(audio_label, sizeof(audio_label), "%s%d%%", audio_icon, audio_snap.volume); unsigned long audio_color = audio_snap.muted ? colors->workspace_inactive : colors->panel_fg; draw_utf8_text(panel->buffer, current_x, text_y, audio_label, audio_color); current_x += get_text_width(audio_label) + SYSTRAY_SPACING; 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; if (wifi_snap.connected && wifi_snap.current_ssid[0] != '\0') { snprintf(wifi_label, sizeof(wifi_label), "%s %s %d%%", wifi_icon_str, wifi_snap.current_ssid, wifi_snap.signal_strength); } else { snprintf(wifi_label, sizeof(wifi_label), "%s", wifi_icon_str); } unsigned long wifi_color = wifi_snap.connected ? colors->panel_fg : colors->workspace_inactive; draw_utf8_text(panel->buffer, current_x, text_y, wifi_label, wifi_color); current_x += get_text_width(wifi_label); systray_width = current_x - x; *width = systray_width; } int systray_hit_test(int x) { if (x >= wifi_icon_x) { return 0; } if (x >= audio_icon_x && x < wifi_icon_x) { return 1; } return -1; } void systray_handle_click(int x, int y, int button) { int widget = systray_hit_test(x); 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 (button == 1) { if (volume_slider == NULL) { int panel_height = config_get_panel_height(); volume_slider = volume_slider_create(audio_icon_x, panel_height); } if (volume_slider->visible) { volume_slider_hide(volume_slider); } else { volume_slider->x = audio_icon_x; volume_slider->y = config_get_panel_height(); volume_slider_show(volume_slider); } } else if (button == 3) { audio_toggle_mute(); panel_render_all(); } else if (button == 4) { 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) { audio_set_volume(audio_state.volume - 5); if (volume_slider != NULL && volume_slider->visible) { volume_slider_render(volume_slider); } panel_render_all(); } } (void)y; } 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); } } } void systray_lock(void) { pthread_mutex_lock(&state_mutex); } void systray_unlock(void) { pthread_mutex_unlock(&state_mutex); } BatteryState systray_get_battery_snapshot(void) { BatteryState snapshot; pthread_mutex_lock(&state_mutex); snapshot = battery_state; pthread_mutex_unlock(&state_mutex); return snapshot; } AudioState systray_get_audio_snapshot(void) { AudioState snapshot; pthread_mutex_lock(&state_mutex); snapshot = audio_state; pthread_mutex_unlock(&state_mutex); return snapshot; }