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 0000000..3035cb8 Binary files /dev/null and b/packages/wmpwman/screenshots/wmpwman.png differ 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; +}