From 1340aed49b4c3677e851fbe5857c23c3bab3d0a1 Mon Sep 17 00:00:00 2001 From: Tomek Wojcik Date: Thu, 22 Oct 2026 18:15:01 +0000 Subject: [PATCH] wmpwman v26.10.22 A Window Maker dockapp to help with power management. Co-authored-by: Tomek Wojcik Co-committed-by: Tomek Wojcik --- packages/wmpwman/.gitignore | 1 + packages/wmpwman/Makefile | 21 + packages/wmpwman/README.md | 35 ++ packages/wmpwman/screenshots/wmpwman.png | Bin 0 -> 1411 bytes packages/wmpwman/src/wmpwman.c | 508 +++++++++++++++++++++++ 5 files changed, 565 insertions(+) create mode 100644 packages/wmpwman/.gitignore create mode 100644 packages/wmpwman/Makefile create mode 100644 packages/wmpwman/README.md create mode 100644 packages/wmpwman/screenshots/wmpwman.png create mode 100644 packages/wmpwman/src/wmpwman.c diff --git a/packages/wmpwman/.gitignore b/packages/wmpwman/.gitignore new file mode 100644 index 0000000..10457de --- /dev/null +++ b/packages/wmpwman/.gitignore @@ -0,0 +1 @@ +/wmpwman diff --git a/packages/wmpwman/Makefile b/packages/wmpwman/Makefile new file mode 100644 index 0000000..51bdfc2 --- /dev/null +++ b/packages/wmpwman/Makefile @@ -0,0 +1,21 @@ +# Co-Authored by: Claude (Sonnet 5.5 Medium) + +CC ?= cc +CFLAGS ?= -O2 -Wall -Wextra -Wno-incompatible-pointer-types +CFLAGS += $(shell pkg-config --cflags x11 dockapp 2>/dev/null) +LDLIBS = $(shell pkg-config --libs x11 xext xpm dockapp 2>/dev/null) + +PREFIX ?= /usr/local + +SOURCES = src/wmpwman.c + +.PHONY: install clean + +wmpwman: $(SOURCES) + $(CC) $(CFLAGS) -o $@ $< $(LDFLAGS) $(LDLIBS) + +install: wmpwman + install -D -m 755 wmpwman $(DESTDIR)$(PREFIX)/bin/wmpwman + +clean: + rm -f wmpwman diff --git a/packages/wmpwman/README.md b/packages/wmpwman/README.md new file mode 100644 index 0000000..be2bebc --- /dev/null +++ b/packages/wmpwman/README.md @@ -0,0 +1,35 @@ +# wmpwman + +A Window Maker dockapp to help with power management. + +![OMG DOCKAPP](screenshots/wmpwman.png "Screenshot of the dockapp") + +## Requirements + +* Xorg-7, +* libXext, +* libXpm, +* [libdockapp v0.7.3](https://www.dockapps.net/libdockapp), +* a WM that supports Window Maker dockapps (tested with Window Maker 0.96.0). + +## Installation + +Build wmpwman by running the following command: + +``` +make +``` + +Now, as the _root_ user: + +``` +make install +``` + +The binary will be installed in `/usr/local/bin/wmpwman`. + +## Changelog + +### v26.10.22 + +* Initial release. diff --git a/packages/wmpwman/screenshots/wmpwman.png b/packages/wmpwman/screenshots/wmpwman.png new file mode 100644 index 0000000000000000000000000000000000000000..3035cb8511048c8f0b534e596ca0ee552483ead6 GIT binary patch literal 1411 zcmV-}1$_F6P)004R> z004l5008;`004mK004C`008P>0026e000+ooVrmw00060P)t-s_V)kv_5bwr|MT?! z^Yj1m^8fMj|MBtv@bLfd@c-}c|L*So?e6{U?fvZS{Oj!d>+AdK>-y>H`RVET=;`?9 z==bO6_vYvJ<>vL|<@Dp^^Wx+3;o|Y&;qc$!@7~|;-QVrq-tFAo?AzVz+uZBg+v?fd z>Dk)p*xBgU+2_~T=hoQf)z{_K*5%aJq3OHyNadWcU^Uw3(jPEcNU zc!o_-U3GYcO;25QcZEz&Ty%DXOHNyJc7sbzTXS`TN=#aEbb?7tS#flMNlRIAbAd=o zSa5QHM@m<3a(_ojS8s8DMoCs~aehTeRc&y6MMqU_Z+%2ZRBUg2Lq=0;ZhJ#TQ)_K| zLPb()ZF)gOQE6;>K|@h#Yk5FJP-ts-KSEDuYIiWWN|nJGBrY4U1~8kL0MgCF*HG0Txl>gKv-O8FEc+^TW2paKUZ33 zE;2q=T4pUVJyls{EigS*SY<3QJXBa@D=#}ME<2Fen#%wH00Cl4M?^vp*FE$A00Rd} zL_t(o!=={cSK~ks0Ps27T}mm%-KCU)!-BiJyF10*-QE4T|MEgol>v4Eiw1xQCQGUfu_e>041V&qR$fQ(E8V-t;lbeU{o zI3vi)Mldn`5|D+$0p#Q&n3{bD&=5ghK7yG!VG9L?2<8@?03*%aS4k`?MzF9X9Ail- zf~6G!C@V*>vL*l(l?c{0Z+H0f@wTk08o|bvP|h{A2)1?vpspUl&Yl1?G$PpZ2tZRa z0*^=NNG+`_;6OOWwsr&uJ^|?HMBr0^u5K1^R0kM&@knb*K5+m&y$Fs1H9)x!d@5}< zpsyc6AmE4~`6B}P#0d-xvVapu4wz)M2spPeG>qUR{O%Sw00|2Sg&LBmoIg!1O)|m) z&V*we9Yb(-Apqkn;NnUFCJIW)M6)3Bc?ef~OY&m}dbm zZ$d|!UqJBoAspl45`vE}0a#u^@bx1At7`~;{z`zN@?}>=D^htO2i7+b`~yA!7^xrGoAs2qW!4{8P&C1868Auvce2P%<%2Hs_5V0RB8DEQl3PzUx85Q0Ov zk|_I|yrpVN;^7fONGRbLk53Rn!wA6X8A4b%0XV-v2#+9iq{}OW2oa&2uWt}U6yWxb z1tJN+{R2Xzm;gLJA&4o!^9u_^$HvAdCZ?pNWn|^#6%>_}RaDi~H8i!fb#(Rg4Gc*n zqvPX~)6=u_^NY)?>zmuV`-jJ;=a<*F_m9tiUJ;^VV&W1Kl2TLCGqZE^3yVw3E30ek z8=G6(JG*=P2Zx78#>OV5re@~m7M51lHnw*54vtRFF0O9w9-dxa{~{o9`~xrU3l7(- R9;5&O002ovPDHLkV1jL6jHCbn literal 0 HcmV?d00001 diff --git a/packages/wmpwman/src/wmpwman.c b/packages/wmpwman/src/wmpwman.c new file mode 100644 index 0000000..59ad665 --- /dev/null +++ b/packages/wmpwman/src/wmpwman.c @@ -0,0 +1,508 @@ +/* + * wmpwman - a Window Maker dockapp to help with power management. + * Co-Authored by: Claude (Sonnet 5.5 Medium) + */ + +#define _GNU_SOURCE + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#define WMPWMAN_VERSION "26.10.22" +#define VERSION "wmpwman version v" WMPWMAN_VERSION "\nCreated by Tomek Wójcik | For teh lulz" + +#define DOCK_SIZE 59 +#define NUM_BUTTONS 3 + +#define BTN_X 4 +#define BTN_W 51 +#define BTN_H 14 +#define BTN_GAP 4 +#define BTN_TOP 4 + +#define NO_PRESSED_BUTTON -1 + +#define SHUTDOWN_DELAY_MS 5000 +#define TICK_MS 200 + +#pragma mark - Globals + +struct WMPWMan { + Pixmap pixmap; + Pixmap mask; + unsigned short width; + unsigned short height; +}; + +struct WMPWMan *app; + +static GC graphics_context; + +static unsigned long color_background, + color_button, + color_button_pressed, + color_light, + color_dark, + color_button_title, + color_button_shutdown_background; + +static XFontStruct *font; + +static const char *const button_titles[NUM_BUTTONS] = { + "LOCK", + "SLEEP", + "SHUTDOWN", +}; + +static const char *const font_candidates[] = { + "5x8", + "5x7", + "6x10", + "fixed", + NULL, +}; + +// button currently held down, or NO_PRESSED_BUTTON +static int pressed_button = NO_PRESSED_BUTTON; + +static int is_shutdown_armed; +static int last_shutdown_seconds; +static struct timespec shutdown_deadline; + +# pragma mark - Helpers + +static unsigned long alloc_color(const char *name) { + XColor color, exact; + Colormap cmap = DefaultColormap(DADisplay, DefaultScreen(DADisplay)); + + if (!XAllocNamedColor(DADisplay, cmap, name, &color, &exact)) { + DAWarning("can't allocate color %s", name); + return BlackPixel(DADisplay, DefaultScreen(DADisplay)); + } + return color.pixel; +} + +static XFontStruct *load_font(void) { + const char *const *name; + XFontStruct *result; + + for (name = font_candidates; *name; name++) { + result = XLoadQueryFont(DADisplay, *name); + if (result) { + DAWarning("selected font: %s", *name); + return result; + } + } + + DAWarning("no usable font found, labels disabled"); + return NULL; +} + +static int calculate_button_y(int i) { + return BTN_TOP + i * (BTN_H + BTN_GAP); +} + +// Returns button index at (x, y), or NO_PRESSED_BUTTON. +static int hit_test(int x, int y) { + int i; + for (i = 0; i < NUM_BUTTONS; i++) { + int button_y = calculate_button_y(i); + if (x >= BTN_X && x < BTN_X + BTN_W && y >= button_y && y < button_y + BTN_H) { + return i; + } + } + + return NO_PRESSED_BUTTON; +} + +static long current_shutdown_deadline_ms(void) { + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + + return (shutdown_deadline.tv_sec - now.tv_sec) * 1000L + + (shutdown_deadline.tv_nsec - shutdown_deadline.tv_nsec) / 1000000L; +} + +static int current_shutdown_deadline_seconds(void) { + return (int)((current_shutdown_deadline_ms() + 999) / 1000); +} + +static void shutdown_arm(void) { + clock_gettime(CLOCK_MONOTONIC, &shutdown_deadline); + shutdown_deadline.tv_sec += SHUTDOWN_DELAY_MS / 1000; + is_shutdown_armed = 1; + last_shutdown_seconds = current_shutdown_deadline_seconds(); + DASetTimeout(TICK_MS); +} + +static void shutdown_disarm(void) { + is_shutdown_armed = 0; + DASetTimeout(-1); +} + +static void draw_button(int i, int down) { + int button_x = BTN_X, + button_y = calculate_button_y(i), + width = BTN_W, + height = BTN_H; + + unsigned long top_left = down ? color_dark : color_light; + unsigned long bot_right = down ? color_light : color_dark; + + // face + if (i == 2 && is_shutdown_armed) { + XSetForeground( + DADisplay, + graphics_context, + color_button_shutdown_background + ); + } else { + XSetForeground( + DADisplay, + graphics_context, + down ? color_button_pressed : color_button + ); + } + XFillRectangle( + DADisplay, + app->pixmap, + graphics_context, + button_x, + button_y, + width, + height + ); + + // bevel: top & left + XSetForeground(DADisplay, graphics_context, top_left); + XDrawLine( + DADisplay, + app->pixmap, + graphics_context, + button_x, + button_y, + button_x + width - 1, + button_y + ); + XDrawLine( + DADisplay, + app->pixmap, + graphics_context, + button_x, + button_y, + button_x, + button_y + height - 1 + ); + + // bevel: bottom & right + XSetForeground(DADisplay, graphics_context, bot_right); + XDrawLine( + DADisplay, + app->pixmap, + graphics_context, + button_x, + button_y + height - 1, + button_x + width - 1, + button_y + height - 1 + ); + XDrawLine( + DADisplay, + app->pixmap, + graphics_context, + button_x + width - 1, + button_y, + button_x + width - 1, + button_y + height - 1 + ); + + if (font) { + const char *title = button_titles[i]; + int title_length = (int)strlen(title); + int direction, font_ascent, font_descent; + XCharStruct overall; + int title_x, title_y; + + XTextExtents( + font, + title, + title_length, + &direction, + &font_ascent, + &font_descent, + &overall + ); + + title_x = button_x + (width - (overall.rbearing - overall.lbearing)) / 2 - overall.lbearing; + title_y = button_y + (height + overall.ascent - overall.descent) / 2; + + XSetForeground(DADisplay, graphics_context, color_button_title); + XDrawString( + DADisplay, + app->pixmap, + graphics_context, + title_x, + title_y, + title, + title_length + ); + } +} + +static void redraw(void) { + int i; + + XSetForeground(DADisplay, graphics_context, color_background); + XFillRectangle( + DADisplay, + app->pixmap, + graphics_context, + 0, + 0, + DOCK_SIZE, + DOCK_SIZE + ); + + for (i = 0; i < NUM_BUTTONS; i++) { + draw_button(i, i == pressed_button); + } + + DASetPixmap(app->pixmap); +} + +static void spawn_detached(char const* argv[]) { + posix_spawn_file_actions_t file_actions; + posix_spawnattr_t attributes; + sigset_t sigdefault; + pid_t pid; + int return_code; + + posix_spawn_file_actions_init(&file_actions); + posix_spawn_file_actions_addopen( + &file_actions, 0, "/dev/null", O_RDONLY, 0 + ); + + sigemptyset(&sigdefault); + sigaddset(&sigdefault, SIGCHLD); + + posix_spawnattr_init(&attributes); + posix_spawnattr_setsigdefault(&attributes, &sigdefault); + posix_spawnattr_setflags(&attributes, POSIX_SPAWN_SETSIGDEF); + + return_code = posix_spawnp( + &pid, argv[0], &file_actions, &attributes, argv, environ + ); + if (return_code != 0) { + DAWarning("can't spawn %s: %s", argv[0], strerror(return_code)); + } + + posix_spawn_file_actions_destroy(&file_actions); + posix_spawnattr_destroy(&attributes); +} + +static void do_shutdown(void) { + DAWarning("do_shutdown()"); + + char const *argv[] = { + "sudo", + "/usr/sbin/poweroff", + NULL, + }; + spawn_detached(argv); +} + +# pragma mark - Button handlers + +static void on_button_lock(void) { + DAWarning("on_button_lock"); + + char const *argv[] = { + "xscreensaver-command", + "-display", DisplayString(DADisplay), + "-lock", + NULL, + }; + spawn_detached(argv); +} + +static void on_button_sleep(void) { + DAWarning("on_button_sleep"); + + char const *argv[] = { + "sudo", + "/usr/sbin/pm-suspend", + NULL, + }; + spawn_detached(argv); +} + +static void on_button_shutdown(void) { + DAWarning("on_button_shutdown"); + + if (is_shutdown_armed) { + shutdown_disarm(); + } else { + shutdown_arm(); + } +} + +static void (*const handlers[NUM_BUTTONS])(void) = { + on_button_lock, on_button_sleep, on_button_shutdown +}; + +#pragma mark - libdockapp handlers + +static void on_dockapp_button_press(int button, int state, int x, int y) { + (void)state; + + if (button != Button1) { + return; + } + + pressed_button = hit_test(x, y); + redraw(); +} + +static void on_dockapp_button_release(int button, int state, int x, int y) { + int released_on; + (void)state; + + if (button != Button1) { + return; + } + + released_on = hit_test(x, y); + + // Fire only if released over the same button that was pressed. + if (pressed_button >= 0 && pressed_button == released_on) { + handlers[pressed_button](); + } + + pressed_button = NO_PRESSED_BUTTON; + + redraw(); +} + +static void on_dockapp_mouse_leave(void) { + // Cancel a press if the pointer leaves the dockapp. + if (pressed_button != NO_PRESSED_BUTTON) { + pressed_button = NO_PRESSED_BUTTON; + + redraw(); + } +} + +static void on_dockapp_destroy(void) { + if (font) { + XFreeFont((DADisplay), font); + } + + if (graphics_context) { + XFreeGC(DADisplay, graphics_context); + } + + if (app->pixmap) { + XFreePixmap(DADisplay, app->pixmap); + } + + exit(0); +} + +static void on_dockapp_timeout(void) { + DAWarning("on_dockapp_timeout(): %d", current_shutdown_deadline_ms()); + if (!is_shutdown_armed) { + return; + } + + if (current_shutdown_deadline_ms() <= 0) { + shutdown_disarm(); + redraw(); + do_shutdown(); + return; + } + + if (current_shutdown_deadline_seconds() != last_shutdown_seconds) { + last_shutdown_seconds = current_shutdown_deadline_seconds(); + redraw(); + } +} + +#pragma mark - Main + +int main(int argc, char **argv) { + XSizeHints *hints; + + DASetExpectedVersion(20050716); + + DACallbacks callbacks = { + .destroy = on_dockapp_destroy, + .buttonPress = on_dockapp_button_press, + .buttonRelease = on_dockapp_button_release, + .leave = on_dockapp_mouse_leave, + .timeout = on_dockapp_timeout, + }; + + app = calloc(1, sizeof(struct WMPWMan)); + + DAParseArguments( + argc, + argv, + NULL, + 0, + "A Window Maker dockapp to help with power management", + VERSION + ); + + DAInitialize("", argv[0], DOCK_SIZE, DOCK_SIZE, argc, argv); + + app->pixmap = DAMakePixmap(); + + graphics_context = XCreateGC(DADisplay, app->pixmap, 0, NULL); + + color_background = alloc_color("#8f909f"); + color_button = alloc_color("#4b5263"); + color_button_pressed = alloc_color("#363b47"); + color_light = alloc_color("white"); + color_dark = alloc_color("black"); + color_button_title = alloc_color("white"); + color_button_shutdown_background = alloc_color("#dc3545"); + + DASetCallbacks(&callbacks); + + font = load_font(); + if (font) { + XSetFont(DADisplay, graphics_context, font->fid); + } + + redraw(); + + hints = XAllocSizeHints(); + if (hints) { + hints->flags |= PMinSize | PMaxSize; + hints->min_width = DOCK_SIZE; + hints->max_width = DOCK_SIZE; + hints->min_height = DOCK_SIZE; + hints->max_height = DOCK_SIZE; + XSetWMNormalHints(DADisplay, DAWindow, hints); + XFree(hints); + } + + signal(SIGCHLD, SIG_IGN); + + DAShow(); + + // Omitting this causes the event loop to spin? :dunno: + DASetTimeout(-1); + + DAEventLoop(); + + return 0; +}