commit c2143b5760024c3e3855f08abcd00c6279d482b5 from: Brett Fisher date: Sun Jun 7 03:23:07 2026 UTC bxbar.c, bxbar-ctl.lua, bxinfo.c: Implement workspace and active window monitoring. - Add bxinfo helper to query EWMH atoms directly from X server. - Refactor lua/manage-bxbar to bxbar-ctl.lua to orchestrate window/workspace state. - Update bxwm.c to clear active window state on empty workspaces. - Consolidate status bar logic into a single Lua loop. commit - a413da8b884f2883828d64a68564305f33065b77 commit + c2143b5760024c3e3855f08abcd00c6279d482b5 blob - ecfbc8c0d146d4a015d938ed753bcc5da73671d6 blob + f9f45a5721bc59e0f599af975047dd0ab1bc1571 --- bxbar.c +++ bxbar.c @@ -1,7 +1,6 @@ /* * bxbar - * - * A very basic X status bar, a.k.a. Brett's X bar. + * A very basic X status bar. * See LICENSE.md and README.md for details. */ blob - /dev/null blob + 49a72351d78295e0ec728fad37516674622b94c6 (mode 644) --- /dev/null +++ bxbar-ctl.lua @@ -0,0 +1,55 @@ +#!/usr/bin/env lua5.4 +-- bxbar-ctl + +-- Enforce Lua 5.4+ +if not _VERSION:match("5.4") then + io.stderr:write("Error: Lua 5.4 required (got " .. _VERSION .. ").\n") + os.exit(1) +end + +local function get_x11_info() + local h = io.popen("bxinfo", "r") + if not h then return "?", "" end + local total_s = h:read("*l") + local current_s = h:read("*l") + local occ_str = h:read("*l") + local win_title = h:read("*l") or "" + h:close() + + if not total_s or not current_s then return "?", "" end + + local total = tonumber(total_s) + local current = tonumber(current_s) + local occupied = {} + if occ_str then + for n in occ_str:gmatch("%d+") do + occupied[tonumber(n)] = true + end + end + + local ws_str = "" + for i = 0, total - 1 do + if i == current then + ws_str = ws_str .. "[" .. (i + 1) .. "*]" + elseif occupied[i] then + ws_str = ws_str .. "[" .. (i + 1) .. "]" + end + end + + ws_str = ws_str ~= "" and ws_str or "?" + return ws_str, win_title +end + +while true do + local ws, title = get_x11_info() + local dt = os.date("%Y-%m-%d %a %H:%M") + + -- Left: workspaces + active title | Right: datetime (battery later) + local left = ws .. " " .. title + local right = dt + + io.write(left .. " | " .. right .. "\n") + io.flush() + os.execute("sleep 2") +end + blob - /dev/null blob + 371bbfcc955ef87b04a186086283a40ff5c57f02 (mode 644) --- /dev/null +++ bxinfo.c @@ -0,0 +1,218 @@ +/* + * bxinfo - A basic X info helper + * Queries EWMH atoms and prints workspace state to stdout. + * Designed to be called from bxbar-ctl or other scripts. + * See LICENSE.md and README.md for details. + */ + +#include +#include +#include +#include +#include + +static long get_cardinal(Display *dpy, Window win, Atom prop); +static Window *get_window_list(Display *dpy, Window win, Atom prop, + unsigned long *count); +static char *get_window_name(Display *dpy, Window win); + +/* Read a single CARDINAL property from a window. + * Returns the value, or -1 on failure. */ +static long +get_cardinal(Display *dpy, Window win, Atom prop) +{ + Atom type; + int format; + unsigned long nitems, bytes_after; + unsigned char *data = NULL; + long val = -1; + + if (XGetWindowProperty(dpy, win, prop, 0, 1, False, XA_CARDINAL, + &type, &format, &nitems, &bytes_after, &data) == Success && data) { + if (type == XA_CARDINAL && nitems >= 1) + val = *(long *)data; + XFree(data); + } + return val; +} + +/* Read a WINDOW list property from a window. + * Returns dynamically allocated array; caller must free. + * Sets *count to number of items. Returns NULL on failure. */ +static Window * +get_window_list(Display *dpy, Window win, Atom prop, unsigned long *count) +{ + Atom type; + int format; + unsigned long nitems, bytes_after; + unsigned char *data = NULL; + Window *wins = NULL; + + *count = 0; + + if (XGetWindowProperty(dpy, win, prop, 0, 1024, False, XA_WINDOW, + &type, &format, &nitems, &bytes_after, &data) == Success && data) { + if (type == XA_WINDOW && nitems > 0) { + wins = malloc(nitems * sizeof(Window)); + if (wins) { + memcpy(wins, data, nitems * sizeof(Window)); + *count = nitems; + } + } + XFree(data); + return wins; + } + return NULL; +} + +/* Read the name of a window. + * Tries _NET_WM_NAME (UTF8_STRING), falls back to WM_NAME (XA_STRING). + * Returns malloc'd string or NULL. Caller must free(). */ +static char * +get_window_name(Display *dpy, Window win) +{ + Atom net_wm_name, utf8_string, type; + int format; + unsigned long nitems, bytes_after; + unsigned char *data = NULL; + char *name = NULL; + + net_wm_name = XInternAtom(dpy, "_NET_WM_NAME", False); + utf8_string = XInternAtom(dpy, "UTF8_STRING", False); + + /* Try _NET_WM_NAME first — covers UTF-8 titles like web pages */ + if (XGetWindowProperty(dpy, win, net_wm_name, 0, 1024, False, + utf8_string, &type, &format, &nitems, &bytes_after, + &data) == Success && data) { + if (type == utf8_string && nitems > 0) { + name = malloc(nitems + 1); + if (name) { + memcpy(name, data, nitems); + name[nitems] = '\0'; + } + XFree(data); + return name; + } + XFree(data); + } + + /* Fallback to WM_NAME — ICCCM legacy, Latin-1 */ + char *wm_name = NULL; + if (XFetchName(dpy, win, &wm_name) && wm_name) { + name = malloc(strlen(wm_name) + 1); + if (name) + strcpy(name, wm_name); + XFree(wm_name); + return name; + } + + return NULL; +} + +int +main(void) +{ + Display *dpy; + Window root; + Atom net_current_desktop, net_number_of_desktops; + Atom net_client_list, net_wm_desktop; + Atom net_active_window; + Atom type; + int format; + unsigned long nitems, bytes_after; + unsigned char *data = NULL; + long ndesktops, current; + Window *clients; + unsigned long nclients, i; + int *occupied; + int d, first; + + dpy = XOpenDisplay(NULL); + if (!dpy) { + fprintf(stderr, "bxinfo: cannot open display\n"); + return 1; + } + + root = DefaultRootWindow(dpy); + + /* Intern atoms */ + net_number_of_desktops = XInternAtom(dpy, "_NET_NUMBER_OF_DESKTOPS", False); + net_current_desktop = XInternAtom(dpy, "_NET_CURRENT_DESKTOP", False); + net_client_list = XInternAtom(dpy, "_NET_CLIENT_LIST", False); + net_wm_desktop = XInternAtom(dpy, "_NET_WM_DESKTOP", False); + net_active_window = XInternAtom(dpy, "_NET_ACTIVE_WINDOW", False); + + /* Read total and current desktop */ + ndesktops = get_cardinal(dpy, root, net_number_of_desktops); + current = get_cardinal(dpy, root, net_current_desktop); + + if (ndesktops < 1 || current < 0) { + fprintf(stderr, "bxinfo: cannot read desktop atoms\n"); + XCloseDisplay(dpy); + return 1; + } + + /* Track which desktops have at least one client */ + occupied = calloc(ndesktops, sizeof(int)); + if (!occupied) { + XCloseDisplay(dpy); + return 1; + } + + clients = get_window_list(dpy, root, net_client_list, &nclients); + if (clients) { + for (i = 0; i < nclients; i++) { + long ws = get_cardinal(dpy, clients[i], net_wm_desktop); + if (ws >= 0 && ws < ndesktops) + occupied[ws] = 1; + } + free(clients); + } + + /* Output three lines: + * line 1: total number of desktops + * line 2: current desktop (0-indexed) + * line 3: occupied desktops, comma-separated (0-indexed) + */ + printf("%ld\n", ndesktops); + printf("%ld\n", current); + + first = 1; + for (d = 0; d < ndesktops; d++) { + if (occupied[d]) { + if (!first) putchar(','); + printf("%d", d); + first = 0; + } + } + putchar('\n'); + + /* Output active window title (line 4) */ + if (XGetWindowProperty(dpy, root, net_active_window, 0, 1, False, + XA_WINDOW, &type, &format, &nitems, &bytes_after, + &data) == Success && data) { + Window active = None; + if (type == XA_WINDOW && nitems >= 1) + active = *(Window *)data; + XFree(data); + + if (active != None) { + char *name = get_window_name(dpy, active); + if (name) { + printf("%s\n", name); + free(name); + } else { + printf("\n"); + } + } else { + printf("\n"); + } + } else { + printf("\n"); + } + + free(occupied); + XCloseDisplay(dpy); + return 0; +} +