From 0c286f5b98c5845f2338082115cb48fd90ca01d5 Mon Sep 17 00:00:00 2001 From: 0xhenrique Date: Mon, 4 May 2026 22:52:00 +0100 Subject: add files --- Makefile | 21 ++++ back.xpm | 118 ++++++++++++++++++++++ wmgpumon | Bin 0 -> 27320 bytes wmgpumon.c | 337 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 476 insertions(+) create mode 100644 Makefile create mode 100644 back.xpm create mode 100755 wmgpumon create mode 100644 wmgpumon.c diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a0441c5 --- /dev/null +++ b/Makefile @@ -0,0 +1,21 @@ +PROG = wmgpumon +CC = gcc +CFLAGS = -Wall -O2 $(shell pkg-config --cflags xft) +LIBS = $(shell pkg-config --libs xft) -lX11 -lXext + +PREFIX = /usr/local +BINDIR = $(PREFIX)/bin + +$(PROG): wmgpumon.c + $(CC) $(CFLAGS) wmgpumon.c -o $(PROG) $(LIBS) + +install: $(PROG) + install -m 755 $(PROG) $(BINDIR) + +uninstall: + rm -f $(BINDIR)/$(PROG) + +clean: + rm -f $(PROG) + +.PHONY: install uninstall clean diff --git a/back.xpm b/back.xpm new file mode 100644 index 0000000..f8f02b9 --- /dev/null +++ b/back.xpm @@ -0,0 +1,118 @@ +/* XPM */ +static char *back_xpm[] = { +/* width height num_colors chars_per_pixel */ +" 64 64 47 1", +/* colors */ +". c #010103", +"# c #818183", +"a c #414143", +"b c #c1c1c3", +"c c #a1a1a3", +"d c #616163", +"e c #919193", +"f c #515153", +"g c #b1b1b3", +"h c #717173", +"i c #29292b", +"j c #fdfdfb", +"k c #cdcdcb", +"l c #353533", +"m c #89898b", +"n c #49494b", +"o c #a9a9ab", +"p c #69696b", +"q c #99999b", +"r c #59595b", +"s c #b9b9bb", +"t c #79797b", +"u c #bdbdbb", +"v c #c9c9cb", +"w c #3d3d3b", +"x c #313133", +"y c #7d7d7b", +"z c #d1d1d3", +"A c #39393b", +"B c #858583", +"C c #454543", +"D c #c5c5c3", +"E c #a5a5a3", +"F c #656563", +"G c #959593", +"H c #555553", +"I c #b5b5b3", +"J c #757573", +"K c #8d8d8b", +"L c #4d4d4b", +"M c #adadab", +"N c #6d6d6b", +"O c #9d9d9b", +"P c #5d5d5b", +"Q c #009f00", +"R c #00a600", +"S c #00b600", +/* pixels */ +"qqqqqqqqqqqqqqqOOOOOOccccccEEEEEEEooooooMMMMMMggggggIIIIIIIIsssN", +"OOOOOOOOOOOOOOOqOOOccOOcccEcEEEEEoEEoooMoMMgggMgggIIgIIIssubbDGC", +"qqxxxxxxxxxxxlAAAwwwaaaCnnLLLffffHrHrPPddddFpppNNNNhhJJJttyyy#AC", +"qqxxxlllllllAAAAwwaaaCCnnnLLfffHHrHPPPdddFpppNNNhhJJhJtttyy#BBwC", +"OqllxlllllAAwwwaaaCCCnnnLLfffHrrrPrPdddFpFpppNNhhhhtJttyy##BBBaC", +"OOxll.....................................................iBBman", +"OOxll.QRSiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiijmmman", +"Oqxll.QRSiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiijmmKCC", +"OOxxl.QRSiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiijKmKCn", +"OOxll.QRSiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiijKKKnn", +"OOxxl.QRSiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiijKeenn", +"OclAA.QRSiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiijeeeCL", +"OOxAA.QRSiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiijeeGnL", +"OOAAw.QRSiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiijeGGnn", +"ccAAw.QRSiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiijeGGnL", +"ccAww.QRSiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiijqGqLL", +"ccAwa.QRSiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiijGGGLL", +"EEwwa.QRSiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiijqqOnL", +"Ecwaa.QRSiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiijGOOLf", +"EEaaC.QRSiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiijOqOLL", +"EEwaC.QRSiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiijOOcff", +"ooaCC.QRSiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiijOccLf", +"EoaCn.QRSiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiijOcEff", +"ooCnn.QRSiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiijEccff", +"ooCnL.QRSiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiijcEEfH", +"oMnLL.QRSiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiijEEEHf", +"MMnLL.QRSiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiijEEEHH", +"MMLLH.QRSiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiijEoMfH", +"MMLLf.QRSiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiijooMHH", +"ggffH.QRSiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiijMMMfH", +"MgffH.QRSiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiijoMgHH", +"gMfHr.QRSiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiijgMgrr", +"ggHHr.QRSiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiijgggHr", +"ggHHr.QRSiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiijgggrr", +"IgHrP.QRSiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiijIIIHr", +"gIrrP.QRSiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiijIIIrr", +"IIrPd.QRSiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiijsIsrr", +"IIPPd.QRSiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiijIssrP", +"IIPdd.QRSiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiijusurP", +"ssddF.QRSiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiijsuuPP", +"ssPdF.QRSiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiijuuurP", +"ssFFp.QRSiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiijuubPP", +"usFFp.QRSiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiijbDDPd", +"suFpp.QRSiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiijbDDdP", +"uuFpN.QRSiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiijbDvdd", +"uupNN.QRSiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiijvvvdd", +"bbppN.QRSiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiijDvvdd", +"uuNhh.QRSiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiijvkzFF", +"buNNh.QRSiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiijkkkdd", +"bDNhh.QRSiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiijkzzdF", +"bbhhJ.QRSiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiijzkzdd", +"bbNJJ.QRSiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiijkzkdF", +"DDhJJ.QRSiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiijkzzdd", +"bDJJt.QRSiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiijzkkdF", +"bDJJt.QRSiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiijzzzdd", +"DDJtt.QRSiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiijkkkdF", +"DDtyy.QRSiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiijzzzdd", +"Dvty#.QRSiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiijzkkdF", +"vvy##ijjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjzzzdd", +"vvy#BBmmmmmKeeGGGGGGGqqOOccEoMMMgggMggIIIsubbbDDvkkkvvvvkkkkkkdF", +"vk#BBBmmKKeeGGGGGGOOOcEccEEEMMMMgIIIssuuuubDvvkkkzkzzzzzzzzzzzdd", +"kk##mmmKKKKeeGGGqOqOOccEEEoMoMMgggIIsssuuDDDDvvkzkzkkkkkkkkkkkdF", +"vGxxllllAAAAAAwAwawaaaaCCCCnnnnLLLfLfffHfHHrrPPPPPPPPddddddddddd", +"GxlxlllllAAAAwwwwwaaaCCCCCnCnnLnLLLLffffHHHrrrPrPPPddPPPPddddddF" +}; diff --git a/wmgpumon b/wmgpumon new file mode 100755 index 0000000..757f713 Binary files /dev/null and b/wmgpumon differ diff --git a/wmgpumon.c b/wmgpumon.c new file mode 100644 index 0000000..8af1ce8 --- /dev/null +++ b/wmgpumon.c @@ -0,0 +1,337 @@ +/* wmgpumon.c — NVIDIA GPU monitor dockapp for Window Maker + * + * Usage: wmgpumon [-display ] [-interval ] + */ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +/* ── content area ────────────────────────────────────────── */ +#define WIN_SIZE 64 +#define CONTENT_X 4 +#define CONTENT_Y 6 +#define CONTENT_W 56 +#define CONTENT_H 52 +#define N_ROWS 4 +#define ROW_H (CONTENT_H / N_ROWS) /* 13 px */ + +/* ── gpu data ────────────────────────────────────────────── */ +typedef struct { + int util; + int mem_pct; + int temp; + float power; + float power_limit; + int valid; +} GpuStats; + +static GpuStats gpu; + +/* ── x11 state ───────────────────────────────────────────── */ +static Display *dpy; +static int scr; +static Window win, iconwin; +static GC gc; +static Pixmap buf; +static Atom tile_atom; + +static XftFont *font; +static XftDraw *xdraw; +static int fnt_asc; +static int fnt_h; + +/* ── colour palette ──────────────────────────────────────── */ +typedef struct { const char *hex; XftColor xft; } Color; + +static Color co_fg = { "#e0e0e0" }; +static Color co_dim = { "#888899" }; +static Color co_warn = { "#e3b341" }; +static Color co_crit = { "#ff5555" }; + +/* ── nvidia-smi query ────────────────────────────────────── */ +#define NVML_CMD \ + "nvidia-smi --query-gpu=" \ + "utilization.gpu,temperature.gpu,power.draw," \ + "memory.used,memory.total,power.limit " \ + "--format=csv,noheader,nounits 2>/dev/null" + +static void fetch(void) +{ + FILE *fp = popen(NVML_CMD, "r"); + if (!fp) { gpu.valid = 0; return; } + + int u, t, mu, mt; + float pw, pl; + + if (fscanf(fp, "%d, %d, %f, %d, %d, %f", + &u, &t, &pw, &mu, &mt, &pl) == 6) { + gpu.util = u < 0 ? 0 : u > 100 ? 100 : u; + gpu.temp = t; + gpu.power = pw < 0.0f ? 0.0f : pw; + gpu.power_limit = pl > 0.0f ? pl : 300.0f; + gpu.mem_pct = mt > 0 ? mu * 100 / mt : 0; + if (gpu.mem_pct > 100) gpu.mem_pct = 100; + gpu.valid = 1; + } else { + gpu.valid = 0; + } + pclose(fp); +} + +/* ── helpers ─────────────────────────────────────────────── */ +static void xft_alloc(Color *co) +{ + XftColorAllocName(dpy, DefaultVisual(dpy, scr), + DefaultColormap(dpy, scr), co->hex, &co->xft); +} + +static int row_y(int row) +{ + int center = CONTENT_Y + row * ROW_H + ROW_H / 2; + return center + fnt_asc - fnt_h / 2; +} + +/* Read _WINDOWMAKER_ICON_TILE from the root window. */ +static Pixmap get_wm_tile(void) +{ + if (tile_atom == None) return None; + + Atom actual_type; + int actual_format; + unsigned long nitems, bytes_after; + unsigned char *data = NULL; + + if (XGetWindowProperty(dpy, RootWindow(dpy, scr), tile_atom, + 0, 1, False, XA_PIXMAP, + &actual_type, &actual_format, + &nitems, &bytes_after, &data) != Success || !data) + return None; + + Pixmap tile = *(Pixmap *)data; + XFree(data); + return tile; +} + +/* ── drawing ─────────────────────────────────────────────── */ +static void draw_row(int row, const char *label, + const char *value, XftColor *vcol) +{ + int y = row_y(row); + + XftDrawStringUtf8(xdraw, &co_dim.xft, font, + CONTENT_X, y, + (const FcChar8 *)label, strlen(label)); + + XGlyphInfo ext; + XftTextExtentsUtf8(dpy, font, + (const FcChar8 *)value, strlen(value), &ext); + int vx = CONTENT_X + CONTENT_W - ext.xOff; + if (vx < CONTENT_X) vx = CONTENT_X; + XftDrawStringUtf8(xdraw, vcol, font, + vx, y, (const FcChar8 *)value, strlen(value)); +} + +static void paint(void) +{ + char s[16]; + + /* background: WM dock tile if available, else solid black */ + Pixmap tile = get_wm_tile(); + if (tile != None) { + XCopyArea(dpy, tile, buf, gc, 0, 0, WIN_SIZE, WIN_SIZE, 0, 0); + } else { + XSetForeground(dpy, gc, BlackPixel(dpy, scr)); + XFillRectangle(dpy, buf, gc, 0, 0, WIN_SIZE, WIN_SIZE); + } + + if (!gpu.valid) { + XftDrawStringUtf8(xdraw, &co_dim.xft, font, + CONTENT_X, row_y(1), + (const FcChar8 *)"no GPU data", 11); + goto flush; + } + + snprintf(s, sizeof(s), "%3d%%", gpu.util); + draw_row(0, "UTIL", s, &co_fg.xft); + + snprintf(s, sizeof(s), "%3d%%", gpu.mem_pct); + draw_row(1, "VRAM", s, &co_fg.xft); + + snprintf(s, sizeof(s), "%3dC", gpu.temp); + { + XftColor *tc = gpu.temp >= 90 ? &co_crit.xft : + gpu.temp >= 75 ? &co_warn.xft : &co_fg.xft; + draw_row(2, "TEMP", s, tc); + } + + snprintf(s, sizeof(s), "%.0fW", gpu.power); + draw_row(3, "WATT", s, &co_fg.xft); + +flush: + XCopyArea(dpy, buf, win, gc, 0, 0, WIN_SIZE, WIN_SIZE, 0, 0); + XCopyArea(dpy, buf, iconwin, gc, 0, 0, WIN_SIZE, WIN_SIZE, 0, 0); + XSync(dpy, False); +} + +/* ── window + pixmap setup ───────────────────────────────── */ +static void make_window(int argc, char **argv) +{ + Window root = RootWindow(dpy, scr); + int depth = DefaultDepth(dpy, scr); + Visual *vis = DefaultVisual(dpy, scr); + + tile_atom = XInternAtom(dpy, "_WINDOWMAKER_ICON_TILE", True); + + /* subscribe to tile updates on the root window */ + XSelectInput(dpy, root, PropertyChangeMask); + + buf = XCreatePixmap(dpy, root, WIN_SIZE, WIN_SIZE, depth); + + XSetWindowAttributes a; + a.background_pixel = BlackPixel(dpy, scr); + a.border_pixel = BlackPixel(dpy, scr); + a.event_mask = ExposureMask | StructureNotifyMask; + unsigned long mask = CWBackPixel | CWBorderPixel | CWEventMask; + + win = XCreateWindow(dpy, root, 0, 0, WIN_SIZE, WIN_SIZE, 0, + depth, InputOutput, vis, mask, &a); + iconwin = XCreateWindow(dpy, root, 0, 0, WIN_SIZE, WIN_SIZE, 0, + depth, InputOutput, vis, mask, &a); + + XWMHints *wm = XAllocWMHints(); + wm->flags = StateHint | IconWindowHint | WindowGroupHint; + wm->initial_state = WithdrawnState; + wm->icon_window = iconwin; + wm->window_group = win; + XSetWMHints(dpy, win, wm); + XFree(wm); + + XSizeHints *sz = XAllocSizeHints(); + sz->flags = PMinSize | PMaxSize | PBaseSize; + sz->min_width = sz->max_width = sz->base_width = WIN_SIZE; + sz->min_height = sz->max_height = sz->base_height = WIN_SIZE; + XSetWMNormalHints(dpy, win, sz); + XFree(sz); + + XStoreName(dpy, win, "wmgpumon"); + XSetIconName(dpy, win, "wmgpumon"); + + XClassHint *ch = XAllocClassHint(); + ch->res_name = "wmgpumon"; + ch->res_class = "WMGpumon"; + XSetClassHint(dpy, win, ch); + XFree(ch); + + Atom wm_del = XInternAtom(dpy, "WM_DELETE_WINDOW", False); + XSetWMProtocols(dpy, win, &wm_del, 1); + + XGCValues gcv; + gcv.graphics_exposures = False; + gcv.function = GXcopy; + gc = XCreateGC(dpy, buf, GCGraphicsExposures | GCFunction, &gcv); + + XSetCommand(dpy, win, argv, argc); + XMapWindow(dpy, win); +} + +/* ── entry point ─────────────────────────────────────────── */ +int main(int argc, char **argv) +{ + const char *dpyname = NULL; + int interval = 2; + + for (int i = 1; i < argc; i++) { + if (!strcmp(argv[i], "-display") && i+1 < argc) dpyname = argv[++i]; + if (!strcmp(argv[i], "-interval") && i+1 < argc) interval = atoi(argv[++i]); + if (!strcmp(argv[i], "-h") || !strcmp(argv[i], "--help")) { + puts("usage: wmgpumon [-display ] [-interval ]"); + return 0; + } + } + if (interval < 1) interval = 1; + + dpy = XOpenDisplay(dpyname); + if (!dpy) { fputs("wmgpumon: cannot open display\n", stderr); return 1; } + scr = DefaultScreen(dpy); + + xft_alloc(&co_fg); + xft_alloc(&co_dim); + xft_alloc(&co_warn); + xft_alloc(&co_crit); + + make_window(argc, argv); + + static const char *font_names[] = { + "UnifontExMono:size=10:antialias=false", + "UnifontExMono:size=9:antialias=false", + "monospace:size=10:antialias=false", + "monospace:size=10", + NULL + }; + for (int i = 0; font_names[i]; i++) { + font = XftFontOpenName(dpy, scr, font_names[i]); + if (font) break; + } + if (!font) { fputs("wmgpumon: could not load any font\n", stderr); return 1; } + fnt_asc = font->ascent; + fnt_h = font->ascent + font->descent; + + xdraw = XftDrawCreate(dpy, buf, + DefaultVisual(dpy, scr), + DefaultColormap(dpy, scr)); + + fetch(); + paint(); + + int xfd = XConnectionNumber(dpy); + time_t last = time(NULL); + + for (;;) { + fd_set fds; + struct timeval tv; + time_t now = time(NULL); + long wait = interval - (long)(now - last); + + FD_ZERO(&fds); + FD_SET(xfd, &fds); + tv.tv_sec = wait > 0 ? wait : 0; + tv.tv_usec = 0; + + select(xfd + 1, &fds, NULL, NULL, &tv); + + while (XPending(dpy)) { + XEvent ev; + XNextEvent(dpy, &ev); + if (ev.type == Expose) paint(); + if (ev.type == DestroyNotify) goto done; + if (ev.type == ClientMessage) goto done; + if (ev.type == PropertyNotify && + ev.xproperty.atom == tile_atom) + paint(); + } + + now = time(NULL); + if (now - last >= interval) { + fetch(); + paint(); + last = now; + } + } +done: + XftDrawDestroy(xdraw); + XftFontClose(dpy, font); + XFreeGC(dpy, gc); + XFreePixmap(dpy, buf); + XDestroyWindow(dpy, iconwin); + XDestroyWindow(dpy, win); + XCloseDisplay(dpy); + return 0; +} -- cgit v1.3