#include "deskbar.h" #define TRAY_REQUEST_DOCK 0 #define XEMBED_EMBEDDED_NOTIFY 0 #define XEMBED_VERSION 0 /* ── layout ───────────────────────────────────────────────────────────── */ int db_apps_y(void) { return DB_TOP_H + 1; } int db_ws_y(Deskbar *db) { return db_apps_y() + db->napps * DB_ITEM_H + 1; } static int db_tray_h(Deskbar *db) { return db->n_tray_icons > 0 ? DB_TRAY_H : 0; } static int db_tray_y(Deskbar *db) { return db_ws_y(db) + DB_WS_H + 1; } static int db_status_y(Deskbar *db) { int th = db_tray_h(db); return db_tray_y(db) + th + (th > 0 ? 1 : 0); } void db_resize(Deskbar *db) { db->bar_h = db_status_y(db) + DB_STATUS_H + 2; int sw = DisplayWidth(db->dpy, db->screen); XMoveResizeWindow(db->dpy, db->win, sw - DB_W, 0, DB_W, db->bar_h); } /* ── colors ───────────────────────────────────────────────────────────── */ unsigned long db_color(Deskbar *db, unsigned long rgb) { XColor c; c.red = ((rgb >> 16) & 0xFF) * 257; c.green = ((rgb >> 8) & 0xFF) * 257; c.blue = ((rgb) & 0xFF) * 257; c.flags = DoRed | DoGreen | DoBlue; XAllocColor(db->dpy, db->cmap, &c); return c.pixel; } void db_xft_color(Deskbar *db, unsigned long rgb, XftColor *out) { XRenderColor rc; rc.red = ((rgb >> 16) & 0xFF) * 257; rc.green = ((rgb >> 8) & 0xFF) * 257; rc.blue = ((rgb) & 0xFF) * 257; rc.alpha = 0xFFFF; XftColorAllocValue(db->dpy, db->visual, db->cmap, &rc, out); } static unsigned long db_app_icon_color(const char *title) { unsigned int h = 0; for (const char *p = title; *p; p++) h = h * 31 + (unsigned char)*p; return DB_ICON_COLORS[h % DB_ICON_NCOLORS]; } /* ── EWMH helpers ─────────────────────────────────────────────────────── */ static long ewmh_get_long(Deskbar *db, Window win, Atom prop) { Atom type; int fmt; unsigned long n, after; unsigned char *data = NULL; long val = -1; if (XGetWindowProperty(db->dpy, win, prop, 0, 1, False, XA_CARDINAL, &type, &fmt, &n, &after, &data) == Success && data) { val = *(long *)data; XFree(data); } return val; } static void ewmh_get_title(Deskbar *db, Window win, char *buf, int bufsz) { Atom type; int fmt; unsigned long n, after; unsigned char *data = NULL; buf[0] = '\0'; if (XGetWindowProperty(db->dpy, win, db->net_wm_name, 0, 256, False, db->utf8_string, &type, &fmt, &n, &after, &data) == Success && data && n > 0) { snprintf(buf, bufsz, "%.*s", (int)n, data); XFree(data); return; } if (data) XFree(data); char *name = NULL; if (XFetchName(db->dpy, win, &name) && name) { snprintf(buf, bufsz, "%s", name); XFree(name); } } void db_update_apps(Deskbar *db) { Atom type; int fmt; unsigned long n, after; unsigned char *data = NULL; db->napps = 0; if (XGetWindowProperty(db->dpy, db->root, db->net_client_list, 0, DB_MAX_APPS, False, XA_WINDOW, &type, &fmt, &n, &after, &data) != Success || !data) return; Window *wins = (Window *)data; long active_win = -1; { unsigned char *aw = NULL; unsigned long an, aa; Atom at; int af; if (XGetWindowProperty(db->dpy, db->root, db->net_active_window, 0, 1, False, XA_WINDOW, &at, &af, &an, &aa, &aw) == Success && aw) { active_win = *(long *)aw; XFree(aw); } } Atom net_wm_state = XInternAtom(db->dpy, "_NET_WM_STATE", False); Atom net_wm_state_hidden = XInternAtom(db->dpy, "_NET_WM_STATE_HIDDEN", False); for (unsigned long i = 0; i < n && db->napps < DB_MAX_APPS; i++) { db->apps[db->napps].win = wins[i]; db->apps[db->napps].focused = (wins[i] == (Window)active_win); db->apps[db->napps].minimized = 0; { Atom atype; int afmt; unsigned long an, aa; unsigned char *adata = NULL; if (XGetWindowProperty(db->dpy, wins[i], net_wm_state, 0, 32, False, XA_ATOM, &atype, &afmt, &an, &aa, &adata) == Success && adata) { Atom *atoms = (Atom *)adata; for (unsigned long j = 0; j < an; j++) if (atoms[j] == net_wm_state_hidden) db->apps[db->napps].minimized = 1; XFree(adata); } } ewmh_get_title(db, wins[i], db->apps[db->napps].title, sizeof(db->apps[db->napps].title)); if (db->apps[db->napps].title[0] != '\0') db->napps++; } XFree(data); } void db_update_workspace(Deskbar *db) { long cws = ewmh_get_long(db, db->root, db->net_current_desktop); long nws = ewmh_get_long(db, db->root, db->net_number_of_desktops); db->cur_ws = (cws >= 0) ? (int)cws : 0; db->num_ws = (nws > 0) ? (int)nws : 4; } /* ── system tray (XEmbed) ─────────────────────────────────────────────── */ static void db_tray_reposition(Deskbar *db) { int ty = db_tray_y(db); int iy = (DB_TRAY_H - DB_TRAY_ICON_SZ) / 2; for (int i = 0; i < db->n_tray_icons; i++) { int ix = 4 + i * (DB_TRAY_ICON_SZ + 4); XMoveWindow(db->dpy, db->tray_icons[i], ix, ty + iy); } } static void db_tray_dock(Deskbar *db, Window icon_win) { if (db->n_tray_icons >= DB_TRAY_MAX) return; for (int i = 0; i < db->n_tray_icons; i++) if (db->tray_icons[i] == icon_win) return; int ty = db_tray_y(db); int iy = (DB_TRAY_H - DB_TRAY_ICON_SZ) / 2; int ix = 4 + db->n_tray_icons * (DB_TRAY_ICON_SZ + 4); int was_empty = (db->n_tray_icons == 0); XSelectInput(db->dpy, icon_win, StructureNotifyMask); XReparentWindow(db->dpy, icon_win, db->win, ix, ty + iy); XResizeWindow(db->dpy, icon_win, DB_TRAY_ICON_SZ, DB_TRAY_ICON_SZ); XMapWindow(db->dpy, icon_win); /* XEMBED_EMBEDDED_NOTIFY message to the icon */ XEvent ev = {0}; ev.xclient.type = ClientMessage; ev.xclient.window = icon_win; ev.xclient.message_type = db->atom_xembed; ev.xclient.format = 32; ev.xclient.data.l[0] = CurrentTime; ev.xclient.data.l[1] = XEMBED_EMBEDDED_NOTIFY; ev.xclient.data.l[2] = 0; ev.xclient.data.l[3] = db->win; ev.xclient.data.l[4] = XEMBED_VERSION; XSendEvent(db->dpy, icon_win, False, NoEventMask, &ev); db->tray_icons[db->n_tray_icons++] = icon_win; if (was_empty) db_resize(db); db_draw(db); } static void db_tray_remove(Deskbar *db, Window icon_win) { int pos = -1; for (int i = 0; i < db->n_tray_icons; i++) if (db->tray_icons[i] == icon_win) { pos = i; break; } if (pos < 0) return; memmove(&db->tray_icons[pos], &db->tray_icons[pos + 1], (db->n_tray_icons - pos - 1) * sizeof(Window)); db->n_tray_icons--; db_tray_reposition(db); if (db->n_tray_icons == 0) db_resize(db); db_draw(db); } /* ── system status ────────────────────────────────────────────────────── */ static int read_cpu_percent(void) { static long prev_idle = 0, prev_total = 0; FILE *f = fopen("/proc/stat", "r"); if (!f) return -1; long u, n, s, idle, iow, irq, sirq; int ok = fscanf(f, "cpu %ld %ld %ld %ld %ld %ld %ld", &u, &n, &s, &idle, &iow, &irq, &sirq); fclose(f); if (ok < 7) return -1; long total = u + n + s + idle + iow + irq + sirq; long d_idle = idle - prev_idle; long d_total = total - prev_total; prev_idle = idle; prev_total = total; if (d_total == 0) return 0; return (int)(100 * (d_total - d_idle) / d_total); } static int read_volume_percent(void) { static int cached = -1; static int ticks = 0; if (ticks-- > 0) return cached; ticks = 4; /* re-poll every 5 seconds */ FILE *f = popen("pactl get-sink-volume @DEFAULT_SINK@ 2>/dev/null", "r"); if (!f) return cached; char line[256]; cached = -1; if (fgets(line, sizeof(line), f)) { char *p = strstr(line, "/ "); if (p) cached = atoi(p + 2); } pclose(f); return cached; } static int read_battery_percent(void) { FILE *f; char path[80]; for (int i = 0; i <= 1; i++) { snprintf(path, sizeof(path), "/sys/class/power_supply/BAT%d/capacity", i); f = fopen(path, "r"); if (f) { int v = -1; fscanf(f, "%d", &v); fclose(f); return v; } } return -1; } static char read_battery_status(void) { FILE *f; char path[80], status[16]; for (int i = 0; i <= 1; i++) { snprintf(path, sizeof(path), "/sys/class/power_supply/BAT%d/status", i); f = fopen(path, "r"); if (f) { status[0] = '\0'; fscanf(f, "%15s", status); fclose(f); return status[0]; /* 'C'harging, 'D'ischarging, 'F'ull */ } } return 0; } static int read_mem_percent(void) { FILE *f = fopen("/proc/meminfo", "r"); if (!f) return -1; long total = 0, available = 0; char line[128]; while (fgets(line, sizeof(line), f)) { long v; if (sscanf(line, "MemTotal: %ld", &v) == 1) total = v; if (sscanf(line, "MemAvailable: %ld", &v) == 1) available = v; if (total && available) break; } fclose(f); if (!total) return -1; return (int)(100 * (total - available) / total); } /* ── drawing ──────────────────────────────────────────────────────────── */ static void draw_str(Deskbar *db, XftDraw *xd, XftFont *font, unsigned long rgb, int x, int y, const char *str, int clip_x, int clip_w) { XftColor col; db_xft_color(db, rgb, &col); if (clip_w > 0) { XRectangle r = { (short)clip_x, 0, (unsigned short)clip_w, (unsigned short)db->bar_h }; XftDrawSetClipRectangles(xd, 0, 0, &r, 1); } XGlyphInfo ext; XftTextExtentsUtf8(db->dpy, font, (const FcChar8 *)str, strlen(str), &ext); XftDrawStringUtf8(xd, &col, font, x, y + ext.height - ext.y, (const FcChar8 *)str, strlen(str)); if (clip_w > 0) XftDrawSetClip(xd, None); XftColorFree(db->dpy, db->visual, db->cmap, &col); } static void draw_str_centered(Deskbar *db, XftDraw *xd, XftFont *font, unsigned long rgb, int strip_x, int strip_y, int strip_w, int strip_h, const char *str) { XGlyphInfo ext; XftTextExtentsUtf8(db->dpy, font, (const FcChar8 *)str, strlen(str), &ext); int tx = strip_x + (strip_w - (int)ext.width) / 2; int ty = strip_y + (strip_h - (int)ext.height) / 2; draw_str(db, xd, font, rgb, tx, ty, str, 0, 0); } /* Draw a letter icon (first char of title) */ static void draw_letter_icon(Deskbar *db, XftDraw *xd, int ix, int iy, const char *title, unsigned long bg, int mini) { /* background square */ unsigned long icon_col = mini ? db_color(db, 0x2A2E3A) : db_color(db, bg); XSetForeground(db->dpy, db->gc, icon_col); XFillRectangle(db->dpy, db->win, db->gc, ix, iy, DB_ICON_SZ, DB_ICON_SZ); /* subtle border */ unsigned long border_col = mini ? db_color(db, 0x383C4A) : db_color(db, bg + 0x101010); XSetForeground(db->dpy, db->gc, border_col); XDrawRectangle(db->dpy, db->win, db->gc, ix, iy, DB_ICON_SZ - 1, DB_ICON_SZ - 1); /* letter */ char letter[2] = { title[0] ? (char)((title[0] & 0x5F) | 0x40) : '?', '\0' }; /* convert to uppercase */ if (letter[0] >= 'a' && letter[0] <= 'z') letter[0] -= 32; unsigned long txt_col = mini ? DB_COL_ITEM_MINI : DB_COL_ITEM_HTXT; XGlyphInfo ext; XftTextExtentsUtf8(db->dpy, db->font_icon, (const FcChar8 *)letter, 1, &ext); int lx = ix + (DB_ICON_SZ - ext.width) / 2; int ly = iy + (DB_ICON_SZ - ext.height) / 2; draw_str(db, xd, db->font_icon, txt_col, lx, ly, letter, 0, 0); } void db_draw(Deskbar *db) { XftDraw *xd = XftDrawCreate(db->dpy, db->win, db->visual, db->cmap); /* ── TOP STRIP ── */ XSetForeground(db->dpy, db->gc, db_color(db, DB_COL_BG)); XFillRectangle(db->dpy, db->win, db->gc, 0, 0, DB_W, DB_TOP_H); /* accent line at very top */ XSetForeground(db->dpy, db->gc, db_color(db, DB_COL_ACCENT)); XFillRectangle(db->dpy, db->win, db->gc, 0, 0, DB_W, 2); /* Orion menu button — highlight when menu is open */ unsigned long sky_bg = db->menu_win ? 0x1A3868 : DB_COL_MENU_BG; XSetForeground(db->dpy, db->gc, db_color(db, sky_bg)); XFillRectangle(db->dpy, db->win, db->gc, 0, 2, DB_MENU_W, DB_TOP_H - 2); /* bottom indicator line when open */ if (db->menu_win) { XSetForeground(db->dpy, db->gc, db_color(db, DB_COL_ACCENT)); XFillRectangle(db->dpy, db->win, db->gc, 0, DB_TOP_H - 2, DB_MENU_W, 2); } /* "Orion ▾" */ draw_str_centered(db, xd, db->font_menu, DB_COL_LOGO_TEXT, 0, 2, DB_MENU_W, DB_TOP_H - 2, "Orion \xe2\x96\xbe"); /* vertical divider */ XSetForeground(db->dpy, db->gc, db_color(db, DB_COL_BORDER_D)); XDrawLine(db->dpy, db->win, db->gc, DB_MENU_W, 2, DB_MENU_W, DB_TOP_H - 2); /* clock */ { time_t t = time(NULL); struct tm *tm_info = localtime(&t); char tstr[16], dstr[24]; strftime(tstr, sizeof(tstr), "%I:%M %p", tm_info); strftime(dstr, sizeof(dstr), "%a %b %d", tm_info); int cw = DB_W - DB_MENU_W - 1; int cx = DB_MENU_W + 1; /* time on upper part */ draw_str_centered(db, xd, db->font_clock, DB_COL_CLOCK_TXT, cx, 4, cw, DB_TOP_H / 2 - 2, tstr); /* date on lower part */ draw_str_centered(db, xd, db->font_date, DB_COL_DATE_TXT, cx, DB_TOP_H / 2 + 2, cw, DB_TOP_H / 2 - 4, dstr); } /* separator below top strip */ XSetForeground(db->dpy, db->gc, db_color(db, DB_COL_BORDER_D)); XDrawLine(db->dpy, db->win, db->gc, 0, DB_TOP_H, DB_W, DB_TOP_H); /* ── APP LIST ── */ int ay = db_apps_y(); for (int i = 0; i < db->napps; i++) { int iy = ay + i * DB_ITEM_H; int hover = (db->hovered == i); int focus = db->apps[i].focused; int mini = db->apps[i].minimized; /* row background — alternate for readability */ unsigned long row_bg = hover ? db_color(db, DB_COL_ITEM_HOV) : focus ? db_color(db, DB_COL_ITEM_FOCUS) : (i % 2 == 0) ? db_color(db, DB_COL_ITEM_BG) : db_color(db, DB_COL_ITEM_ALT); XSetForeground(db->dpy, db->gc, row_bg); XFillRectangle(db->dpy, db->win, db->gc, 0, iy, DB_W, DB_ITEM_H); /* letter icon */ unsigned long icon_bg = db_app_icon_color(db->apps[i].title); int icon_x = 5; int icon_y = iy + (DB_ITEM_H - DB_ICON_SZ) / 2; draw_letter_icon(db, xd, icon_x, icon_y, db->apps[i].title, icon_bg, mini); /* app name */ int name_x = icon_x + DB_ICON_SZ + 6; int name_maxw = DB_W - name_x - 14; int name_y = iy + (DB_ITEM_H - 10) / 2; unsigned long tc = (hover || focus) ? DB_COL_ITEM_HTXT : mini ? DB_COL_ITEM_MINI : DB_COL_ITEM_TEXT; if (mini && !hover) { char bracketed[70]; snprintf(bracketed, sizeof(bracketed), "[%s]", db->apps[i].title); draw_str(db, xd, db->font_ui, tc, name_x, name_y, bracketed, name_x, name_maxw); } else { draw_str(db, xd, db->font_ui, tc, name_x, name_y, db->apps[i].title, name_x, name_maxw); } /* expand arrow ▸ */ unsigned long arrow_col = (hover || focus) ? DB_COL_ITEM_HTXT : 0x303848; draw_str(db, xd, db->font_ui, arrow_col, DB_W - 13, name_y, "\xe2\x96\xb8", 0, 0); /* row divider */ XSetForeground(db->dpy, db->gc, db_color(db, DB_COL_DIVIDER)); XDrawLine(db->dpy, db->win, db->gc, 0, iy + DB_ITEM_H - 1, DB_W, iy + DB_ITEM_H - 1); } /* separator below app list */ XSetForeground(db->dpy, db->gc, db_color(db, DB_COL_BORDER_D)); XDrawLine(db->dpy, db->win, db->gc, 0, ay + db->napps * DB_ITEM_H, DB_W, ay + db->napps * DB_ITEM_H); /* ── WORKSPACE STRIP ── */ int wy = db_ws_y(db); XSetForeground(db->dpy, db->gc, db_color(db, DB_COL_BG)); XFillRectangle(db->dpy, db->win, db->gc, 0, wy, DB_W, DB_WS_H); int total = db->num_ws < 1 ? 4 : (db->num_ws > 8 ? 8 : db->num_ws); int btnw = (DB_W - 8) / total; int bth = DB_WS_H - 6; for (int i = 0; i < total; i++) { int bx = 4 + i * btnw; int by = wy + 3; int active = (i == db->cur_ws); XSetForeground(db->dpy, db->gc, db_color(db, active ? DB_COL_WS_ACTIVE : DB_COL_WS_INACT)); XFillRectangle(db->dpy, db->win, db->gc, bx, by, btnw - 2, bth); /* active indicator: accent top edge */ if (active) { XSetForeground(db->dpy, db->gc, db_color(db, DB_COL_ACCENT)); XFillRectangle(db->dpy, db->win, db->gc, bx, by, btnw - 2, 2); } char label[4]; snprintf(label, sizeof(label), "%d", i + 1); draw_str_centered(db, xd, db->font_ui, active ? DB_COL_ITEM_HTXT : DB_COL_WS_TEXT, bx, by, btnw - 2, bth, label); } /* separator below workspace strip */ XSetForeground(db->dpy, db->gc, db_color(db, DB_COL_BORDER_D)); XDrawLine(db->dpy, db->win, db->gc, 0, wy + DB_WS_H, DB_W, wy + DB_WS_H); /* ── SYSTEM TRAY ── */ if (db->n_tray_icons > 0) { int ty = db_tray_y(db); XSetForeground(db->dpy, db->gc, db_color(db, DB_COL_BG)); XFillRectangle(db->dpy, db->win, db->gc, 0, ty, DB_W, DB_TRAY_H); /* separator below tray */ XSetForeground(db->dpy, db->gc, db_color(db, DB_COL_BORDER_D)); XDrawLine(db->dpy, db->win, db->gc, 0, ty + DB_TRAY_H, DB_W, ty + DB_TRAY_H); } /* ── STATUS BAR ── */ int sy = db_status_y(db); XSetForeground(db->dpy, db->gc, db_color(db, DB_COL_BG)); XFillRectangle(db->dpy, db->win, db->gc, 0, sy, DB_W, DB_STATUS_H); { int cpu = read_cpu_percent(); int mem = read_mem_percent(); int bat = read_battery_percent(); char bst = read_battery_status(); int vol = read_volume_percent(); char status[80]; int pos = 0; if (vol >= 0) pos += snprintf(status + pos, sizeof(status) - pos, "VOL %d%%", vol); if (cpu >= 0) pos += snprintf(status + pos, sizeof(status) - pos, "%sCPU %d%%", pos ? " " : "", cpu); if (mem >= 0) pos += snprintf(status + pos, sizeof(status) - pos, " MEM %d%%", mem); if (bat >= 0) { const char *arrow = (bst == 'C') ? "+" : (bst == 'F') ? "=" : "-"; pos += snprintf(status + pos, sizeof(status) - pos, " BAT %d%%%s", bat, arrow); } if (pos == 0) snprintf(status, sizeof(status), "OrionOS"); draw_str_centered(db, xd, db->font_date, DB_COL_STATUS_TXT, 0, sy, DB_W, DB_STATUS_H, status); } XftDrawDestroy(xd); } /* ── BeOS-style fly-out menu ─────────────────────────────────────────── */ typedef struct { const char *lbl; const char *cmd; int sep; int has_sub; int sub_i; } MI; /* sub-menu 0: Applications */ static const char *S0_LBL[] = { "Firefox","Terminal","Files","Text Editor", "Media Player","Image Viewer","OnlyOffice","PDF Viewer" }; static const char *S0_CMD[] = { "firefox &","alacritty &","thunar &","mousepad &", "mpv --player-operation-mode=pseudo-gui &","viewnior &", "onlyoffice-desktopeditors &","evince &" }; #define NS0 8 /* sub-menu 1: Settings */ static const char *S1_LBL[] = { "Display","Audio","Network","Bluetooth","Power" }; static const char *S1_CMD[] = { "arandr &","pavucontrol &", "nm-connection-editor &","blueman-manager &", "xfce4-power-manager-settings &" }; #define NS1 5 static const MI MENU[] = { { "Install OrionOS...", "alacritty -e bash -c 'sudo skyinstall; exec bash' &", 0, 0,-1 }, { NULL, NULL, 1, 0,-1 }, { "Applications", NULL, 0, 1, 0 }, { "Settings", NULL, 0, 1, 1 }, { NULL, NULL, 1, 0,-1 }, { "About OrionOS", "alacritty -e bash -c 'fastfetch; read' &", 0, 0,-1 }, { "Lock Screen", "xscreensaver-command -lock &", 0, 0,-1 }, { NULL, NULL, 1, 0,-1 }, { "Restart", "systemctl reboot", 0, 0,-1 }, { "Shut Down", "systemctl poweroff", 0, 0,-1 }, }; #define N_MENU ((int)(sizeof(MENU)/sizeof(MENU[0]))) static int mi_y(int i) { int y = DB_FLYOUT_PAD; for (int j = 0; j < i; j++) y += MENU[j].sep ? DB_FLYOUT_SH : DB_FLYOUT_IH; return y; } static int menu_win_h(void) { return mi_y(N_MENU) + DB_FLYOUT_PAD; } static int sub_win_h(int n) { return n * DB_FLYOUT_IH + DB_FLYOUT_PAD * 2; } static int menu_item_at(int y) { for (int i = 0; i < N_MENU; i++) { if (MENU[i].sep) continue; int iy = mi_y(i); if (y >= iy && y < iy + DB_FLYOUT_IH) return i; } return -1; } static int sub_item_at(int y, int n) { int iy = DB_FLYOUT_PAD; for (int i = 0; i < n; i++) { if (y >= iy && y < iy + DB_FLYOUT_IH) return i; iy += DB_FLYOUT_IH; } return -1; } static void menu_draw_panel(Deskbar *db, Window win, int is_sub, const char **lbls, int n, int hov) { int ww = DB_FLYOUT_W; int wh = is_sub ? sub_win_h(n) : menu_win_h(); XftDraw *xd = XftDrawCreate(db->dpy, win, db->visual, db->cmap); /* background */ XSetForeground(db->dpy, db->gc, db_color(db, 0x0C1120)); XFillRectangle(db->dpy, win, db->gc, 0, 0, ww, wh); /* left accent bar (BeOS-style colored stripe) */ XSetForeground(db->dpy, db->gc, db_color(db, DB_COL_ACCENT)); XFillRectangle(db->dpy, win, db->gc, 0, 0, 3, wh); /* subtle border */ XSetForeground(db->dpy, db->gc, db_color(db, 0x1A2A48)); XDrawRectangle(db->dpy, win, db->gc, 0, 0, ww - 1, wh - 1); if (is_sub) { for (int i = 0; i < n; i++) { int iy = DB_FLYOUT_PAD + i * DB_FLYOUT_IH; if (i == hov) { XSetForeground(db->dpy, db->gc, db_color(db, DB_COL_ITEM_HOV)); XFillRectangle(db->dpy, win, db->gc, 3, iy, ww - 4, DB_FLYOUT_IH); } unsigned long tc = (i == hov) ? DB_COL_ITEM_HTXT : DB_COL_ITEM_TEXT; draw_str(db, xd, db->font_ui, tc, DB_FLYOUT_LPD, iy + (DB_FLYOUT_IH - 10) / 2, lbls[i], 0, 0); } } else { for (int i = 0; i < N_MENU; i++) { if (MENU[i].sep) { int sy = mi_y(i) + DB_FLYOUT_SH / 2; XSetForeground(db->dpy, db->gc, db_color(db, 0x1C2A44)); XDrawLine(db->dpy, win, db->gc, 8, sy, ww - 9, sy); continue; } int iy = mi_y(i); if (i == hov) { XSetForeground(db->dpy, db->gc, db_color(db, DB_COL_ITEM_HOV)); XFillRectangle(db->dpy, win, db->gc, 3, iy, ww - 4, DB_FLYOUT_IH); } unsigned long tc = (i == hov) ? DB_COL_ITEM_HTXT : DB_COL_ITEM_TEXT; draw_str(db, xd, db->font_ui, tc, DB_FLYOUT_LPD, iy + (DB_FLYOUT_IH - 10) / 2, MENU[i].lbl, 0, 0); if (MENU[i].has_sub) { unsigned long ac = (i == hov) ? DB_COL_ITEM_HTXT : 0x30486A; /* ▶ U+25B6 */ draw_str(db, xd, db->font_ui, ac, ww - 15, iy + (DB_FLYOUT_IH - 10) / 2, "\xe2\x96\xb6", 0, 0); } } } XftDrawDestroy(xd); XFlush(db->dpy); } static void db_sub_close(Deskbar *db) { if (!db->sub_win) return; XDestroyWindow(db->dpy, db->sub_win); db->sub_win = None; db->sub_active = -1; db->sub_hov = -1; } static void db_sub_open(Deskbar *db, int sub_i) { if (db->sub_active == sub_i && db->sub_win) return; db_sub_close(db); const char **lbls = (sub_i == 0) ? S0_LBL : S1_LBL; int n = (sub_i == 0) ? NS0 : NS1; int sx = db->sw_cache - DB_W - DB_FLYOUT_W - DB_FLYOUT_W; int sy = mi_y(db->menu_hov); XSetWindowAttributes swa; swa.override_redirect = True; swa.background_pixel = db_color(db, 0x0C1120); swa.event_mask = ExposureMask; db->sub_win = XCreateWindow( db->dpy, db->root, sx, sy, DB_FLYOUT_W, sub_win_h(n), 0, db->depth, InputOutput, db->visual, CWOverrideRedirect | CWBackPixel | CWEventMask, &swa); db->sub_active = sub_i; db->sub_hov = -1; XMapRaised(db->dpy, db->sub_win); menu_draw_panel(db, db->sub_win, 1, lbls, n, -1); } void db_menu_open(Deskbar *db) { if (db->menu_win) return; db->sw_cache = DisplayWidth(db->dpy, db->screen); int wx = db->sw_cache - DB_W - DB_FLYOUT_W; XSetWindowAttributes swa; swa.override_redirect = True; swa.background_pixel = db_color(db, 0x0C1120); swa.event_mask = ExposureMask; db->menu_win = XCreateWindow( db->dpy, db->root, wx, 0, DB_FLYOUT_W, menu_win_h(), 0, db->depth, InputOutput, db->visual, CWOverrideRedirect | CWBackPixel | CWEventMask, &swa); db->menu_hov = -1; XMapRaised(db->dpy, db->menu_win); menu_draw_panel(db, db->menu_win, 0, NULL, 0, -1); /* grab pointer so clicks outside are detected */ XGrabPointer(db->dpy, db->menu_win, True, ButtonPressMask | PointerMotionMask | ButtonReleaseMask, GrabModeAsync, GrabModeAsync, None, None, CurrentTime); /* redraw deskbar to show Sky button as active */ db_draw(db); } void db_menu_close(Deskbar *db) { if (!db->menu_win) return; XUngrabPointer(db->dpy, CurrentTime); db_sub_close(db); XDestroyWindow(db->dpy, db->menu_win); db->menu_win = None; db->menu_hov = -1; db_draw(db); } static void handle_menu_motion(Deskbar *db, int rx, int ry) { /* is pointer in sub-menu? */ if (db->sub_win) { int sx, sy; unsigned sw, sh, bw, dep; Window sr; XGetGeometry(db->dpy, db->sub_win, &sr, &sx, &sy, &sw, &sh, &bw, &dep); if (rx >= sx && ry >= sy && rx < sx + (int)sw && ry < sy + (int)sh) { int n = (db->sub_active == 0) ? NS0 : NS1; const char **lbls = (db->sub_active == 0) ? S0_LBL : S1_LBL; int idx = sub_item_at(ry - sy, n); if (idx != db->sub_hov) { db->sub_hov = idx; menu_draw_panel(db, db->sub_win, 1, lbls, n, idx); } return; } } /* is pointer in main menu? */ if (db->menu_win) { int mx, my; unsigned mw, mh, bw, dep; Window mr; XGetGeometry(db->dpy, db->menu_win, &mr, &mx, &my, &mw, &mh, &bw, &dep); if (rx >= mx && ry >= my && rx < mx + (int)mw && ry < my + (int)mh) { int idx = menu_item_at(ry - my); if (idx != db->menu_hov) { db->menu_hov = idx; menu_draw_panel(db, db->menu_win, 0, NULL, 0, idx); if (idx >= 0 && MENU[idx].has_sub) db_sub_open(db, MENU[idx].sub_i); else db_sub_close(db); } return; } } } static void handle_menu_press(Deskbar *db, int rx, int ry) { /* click in sub-menu? */ if (db->sub_win) { int sx, sy; unsigned sw, sh, bw, dep; Window sr; XGetGeometry(db->dpy, db->sub_win, &sr, &sx, &sy, &sw, &sh, &bw, &dep); if (rx >= sx && ry >= sy && rx < sx + (int)sw && ry < sy + (int)sh) { int n = (db->sub_active == 0) ? NS0 : NS1; const char **cmds = (db->sub_active == 0) ? S0_CMD : S1_CMD; int idx = sub_item_at(ry - sy, n); if (idx >= 0) system(cmds[idx]); db_menu_close(db); return; } } /* click in main menu? */ if (db->menu_win) { int mx, my; unsigned mw, mh, bw, dep; Window mr; XGetGeometry(db->dpy, db->menu_win, &mr, &mx, &my, &mw, &mh, &bw, &dep); if (rx >= mx && ry >= my && rx < mx + (int)mw && ry < my + (int)mh) { int idx = menu_item_at(ry - my); if (idx >= 0 && !MENU[idx].has_sub && MENU[idx].cmd) system(MENU[idx].cmd); if (idx >= 0 && !MENU[idx].has_sub) db_menu_close(db); /* clicking a has_sub item just keeps the sub-menu open */ return; } } /* click outside all menu windows → close */ db_menu_close(db); } /* ── input ────────────────────────────────────────────────────────────── */ void db_hover(Deskbar *db, int x, int y) { (void)x; int prev = db->hovered; int ay = db_apps_y(); db->hovered = -1; if (y >= ay && y < ay + db->napps * DB_ITEM_H) db->hovered = (y - ay) / DB_ITEM_H; if (db->hovered != prev) db_draw(db); } void db_click(Deskbar *db, int x, int y) { int ay = db_apps_y(); int wy = db_ws_y(db); /* Orion menu button → toggle fly-out menu */ if (y < DB_TOP_H && x < DB_MENU_W) { if (db->menu_win) db_menu_close(db); else db_menu_open(db); return; } /* app row click → focus or restore */ if (y >= ay && y < ay + db->napps * DB_ITEM_H) { int idx = (y - ay) / DB_ITEM_H; if (idx < db->napps) { XEvent ev = {0}; ev.xclient.type = ClientMessage; ev.xclient.window = db->apps[idx].win; ev.xclient.message_type = XInternAtom(db->dpy, "_NET_ACTIVE_WINDOW", False); ev.xclient.format = 32; ev.xclient.data.l[0] = 2; ev.xclient.data.l[1] = CurrentTime; XSendEvent(db->dpy, db->root, False, SubstructureRedirectMask | SubstructureNotifyMask, &ev); XRaiseWindow(db->dpy, db->apps[idx].win); } return; } /* workspace button click → switch desktop */ if (y >= wy && y < wy + DB_WS_H) { int total = db->num_ws < 1 ? 4 : (db->num_ws > 8 ? 8 : db->num_ws); int btnw = (DB_W - 8) / total; int idx = (x - 4) / btnw; if (idx >= 0 && idx < db->num_ws) { XEvent ev = {0}; ev.xclient.type = ClientMessage; ev.xclient.window = db->root; ev.xclient.message_type = XInternAtom(db->dpy, "_NET_CURRENT_DESKTOP", False); ev.xclient.format = 32; ev.xclient.data.l[0] = idx; ev.xclient.data.l[1] = CurrentTime; XSendEvent(db->dpy, db->root, False, SubstructureRedirectMask | SubstructureNotifyMask, &ev); } } } /* ── init & run ───────────────────────────────────────────────────────── */ void db_init(Deskbar *db) { db->dpy = XOpenDisplay(NULL); if (!db->dpy) { fprintf(stderr, "oriondeskbar: cannot open display\n"); exit(1); } db->screen = DefaultScreen(db->dpy); db->root = RootWindow(db->dpy, db->screen); db->cmap = DefaultColormap(db->dpy, db->screen); db->visual = DefaultVisual(db->dpy, db->screen); db->depth = DefaultDepth(db->dpy, db->screen); db->hovered = -1; db->menu_win = None; db->sub_win = None; db->menu_hov = -1; db->sub_active = -1; db->sub_hov = -1; db->sw_cache = 0; db->n_tray_icons = 0; db->net_client_list = XInternAtom(db->dpy, "_NET_CLIENT_LIST", False); db->net_active_window = XInternAtom(db->dpy, "_NET_ACTIVE_WINDOW", False); db->net_current_desktop = XInternAtom(db->dpy, "_NET_CURRENT_DESKTOP", False); db->net_number_of_desktops = XInternAtom(db->dpy, "_NET_NUMBER_OF_DESKTOPS", False); db->net_wm_name = XInternAtom(db->dpy, "_NET_WM_NAME", False); db->utf8_string = XInternAtom(db->dpy, "UTF8_STRING", False); { char sel_name[32]; snprintf(sel_name, sizeof(sel_name), "_NET_SYSTEM_TRAY_S%d", db->screen); db->atom_tray_s0 = XInternAtom(db->dpy, sel_name, False); } db->atom_tray_opcode = XInternAtom(db->dpy, "_NET_SYSTEM_TRAY_OPCODE", False); db->atom_xembed = XInternAtom(db->dpy, "_XEMBED", False); db->atom_manager = XInternAtom(db->dpy, "MANAGER", False); db->font_ui = XftFontOpenName(db->dpy, db->screen, DB_FONT_UI); db->font_menu = XftFontOpenName(db->dpy, db->screen, DB_FONT_MENU); db->font_clock = XftFontOpenName(db->dpy, db->screen, DB_FONT_CLOCK); db->font_date = XftFontOpenName(db->dpy, db->screen, DB_FONT_DATE); db->font_icon = XftFontOpenName(db->dpy, db->screen, DB_FONT_ICON); if (!db->font_ui) db->font_ui = XftFontOpenName(db->dpy, db->screen, "fixed:size=9"); if (!db->font_menu) db->font_menu = db->font_ui; if (!db->font_clock) db->font_clock = db->font_ui; if (!db->font_date) db->font_date = db->font_ui; if (!db->font_icon) db->font_icon = db->font_ui; db_update_apps(db); db_update_workspace(db); db->bar_h = db_status_y(db) + DB_STATUS_H + 2; int sw = DisplayWidth(db->dpy, db->screen); XSetWindowAttributes swa; swa.override_redirect = True; swa.background_pixel = db_color(db, DB_COL_BG); swa.event_mask = ExposureMask | ButtonPressMask | PointerMotionMask | LeaveWindowMask | VisibilityChangeMask; db->win = XCreateWindow( db->dpy, db->root, sw - DB_W, 0, DB_W, db->bar_h, 0, db->depth, InputOutput, db->visual, CWOverrideRedirect | CWBackPixel | CWEventMask, &swa); db->gc = XCreateGC(db->dpy, db->win, 0, NULL); /* Claim _NET_SYSTEM_TRAY_S selection and announce ourselves */ XSetSelectionOwner(db->dpy, db->atom_tray_s0, db->win, CurrentTime); if (XGetSelectionOwner(db->dpy, db->atom_tray_s0) == db->win) { XEvent sev = {0}; sev.xclient.type = ClientMessage; sev.xclient.window = db->root; sev.xclient.message_type = db->atom_manager; sev.xclient.format = 32; sev.xclient.data.l[0] = CurrentTime; sev.xclient.data.l[1] = db->atom_tray_s0; sev.xclient.data.l[2] = db->win; XSendEvent(db->dpy, db->root, False, StructureNotifyMask, &sev); } Atom net_wm_window_type = XInternAtom(db->dpy, "_NET_WM_WINDOW_TYPE", False); Atom net_wm_type_dock = XInternAtom(db->dpy, "_NET_WM_WINDOW_TYPE_DOCK", False); XChangeProperty(db->dpy, db->win, net_wm_window_type, XA_ATOM, 32, PropModeReplace, (unsigned char *)&net_wm_type_dock, 1); /* strut: reserve right edge for deskbar */ Atom net_strut_partial = XInternAtom(db->dpy, "_NET_WM_STRUT_PARTIAL", False); long strut[12] = { 0, DB_W, 0, 0, 0,0, 0,0, 0,0, 0,0 }; /* right strut: strut[1]=DB_W, right_start[8]=0, right_end[9]=screen_height */ int sh = DisplayHeight(db->dpy, db->screen); strut[9] = sh; XChangeProperty(db->dpy, db->win, net_strut_partial, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)strut, 12); XSelectInput(db->dpy, db->root, PropertyChangeMask); db_resize(db); XMapRaised(db->dpy, db->win); XSync(db->dpy, False); } void db_run(Deskbar *db) { time_t last_tick = 0; int status_tick = 0; for (;;) { while (XPending(db->dpy)) { XEvent e; XNextEvent(db->dpy, &e); /* While the fly-out menu is open, the pointer is grabbed. Route all motion and button events through the menu handlers (they use root-relative coords to determine which panel was hit). */ if (db->menu_win) { if (e.type == MotionNotify) { handle_menu_motion(db, e.xmotion.x_root, e.xmotion.y_root); continue; } if (e.type == ButtonPress) { handle_menu_press(db, e.xbutton.x_root, e.xbutton.y_root); continue; } if (e.type == ButtonRelease) continue; if (e.type == Expose && e.xexpose.count == 0) { if (e.xany.window == db->menu_win) menu_draw_panel(db, db->menu_win, 0, NULL, 0, db->menu_hov); else if (db->sub_win && e.xany.window == db->sub_win) { int n = (db->sub_active == 0) ? NS0 : NS1; const char **lbls = (db->sub_active == 0) ? S0_LBL : S1_LBL; menu_draw_panel(db, db->sub_win, 1, lbls, n, db->sub_hov); } else if (e.xany.window == db->win) db_draw(db); continue; } } switch (e.type) { case Expose: if (e.xexpose.count == 0) db_draw(db); break; case ButtonPress: if (e.xany.window == db->win) { int sy = db_status_y(db); if (e.xbutton.button == Button4) { /* scroll up on status bar = volume up */ if (e.xbutton.y >= sy) system("pactl set-sink-volume @DEFAULT_SINK@ +5% 2>/dev/null &"); break; } if (e.xbutton.button == Button5) { if (e.xbutton.y >= sy) system("pactl set-sink-volume @DEFAULT_SINK@ -5% 2>/dev/null &"); break; } if (e.xbutton.button == Button1 && e.xbutton.y >= sy) system("pactl set-sink-mute @DEFAULT_SINK@ toggle 2>/dev/null &"); else db_click(db, e.xbutton.x, e.xbutton.y); } break; case MotionNotify: if (e.xany.window == db->win) db_hover(db, e.xmotion.x, e.xmotion.y); break; case LeaveNotify: if (e.xany.window == db->win && db->hovered != -1) { db->hovered = -1; db_draw(db); } break; case VisibilityNotify: /* Another window covered us — fight back to the top */ if (e.xany.window == db->win && e.xvisibility.state != VisibilityUnobscured) XRaiseWindow(db->dpy, db->win); break; case ClientMessage: if (e.xclient.message_type == db->atom_tray_opcode && e.xclient.data.l[1] == TRAY_REQUEST_DOCK) db_tray_dock(db, (Window)e.xclient.data.l[2]); break; case DestroyNotify: db_tray_remove(db, e.xdestroywindow.window); break; case ReparentNotify: if (e.xreparent.parent != db->win) db_tray_remove(db, e.xreparent.window); break; case SelectionClear: if (e.xselectionclear.selection == db->atom_tray_s0) { db->n_tray_icons = 0; db_resize(db); db_draw(db); } break; case PropertyNotify: if (e.xproperty.atom == db->net_client_list) { /* Window list changed — full refresh + resize */ db_update_apps(db); db_tray_reposition(db); db_resize(db); db_draw(db); } else if (e.xproperty.atom == db->net_active_window) { /* Focus changed — only update focused flag, no resize */ Atom at; int af; unsigned long an, aa; unsigned char *aw = NULL; long active_win = -1; if (XGetWindowProperty(db->dpy, db->root, db->net_active_window, 0, 1, False, XA_WINDOW, &at, &af, &an, &aa, &aw) == Success && aw) { active_win = *(long *)aw; XFree(aw); } for (int i = 0; i < db->napps; i++) db->apps[i].focused = (db->apps[i].win == (Window)active_win); db_draw(db); } else if (e.xproperty.atom == db->net_current_desktop) { db_update_workspace(db); db_draw(db); } break; default: break; } } time_t now = time(NULL); if (now != last_tick) { last_tick = now; status_tick++; if (status_tick >= 5) { status_tick = 0; /* Safety net: re-raise deskbar every 5s in case VisibilityNotify was missed while menu was open */ if (!db->menu_win) XRaiseWindow(db->dpy, db->win); } db_draw(db); } usleep(100000); } } int main(void) { Deskbar db = {0}; db_init(&db); db_run(&db); return 0; }