From 8106e2a2fdd2edc3929819890c85ecd5076cbdfc Mon Sep 17 00:00:00 2001 From: coast Date: Mon, 29 Dec 2025 14:44:54 +0330 Subject: piss wait, piss.1 --- makefile | 7 +- piss | Bin 0 -> 22448 bytes src/daemon.c | 387 +++++++++++++++++------------------------------------------ src/piss.1 | 68 +++++++++++ 4 files changed, 183 insertions(+), 279 deletions(-) create mode 100755 piss create mode 100644 src/piss.1 diff --git a/makefile b/makefile index 6fbd1c1..e857d33 100644 --- a/makefile +++ b/makefile @@ -1,4 +1,5 @@ PREFIX = /usr/local +MANPREFIX = $(PREFIX)/share/man X11INC = /usr/X11R6/include X11LIB = /usr/X11R6/lib @@ -6,7 +7,7 @@ X11LIB = /usr/X11R6/lib INCS = -I$(X11INC) LIBS = -L$(X11LIB) -lX11 -CPPFLAGS = -D_DEFAULT_SOURCE -D_BSD_SOURCE -D_POSIX_C_SOURCE=200809L +CPPFLAGS = -D_DEFAULT_SOURCE -D_BSD_SOURCE -D_POSIX_C_SOURCE=200809L -D_GNU_SOURCE CFLAGS = -std=c99 -pedantic -Wall -Wextra -Os $(INCS) $(CPPFLAGS) LDFLAGS = $(LIBS) @@ -24,8 +25,12 @@ install: all mkdir -p $(DESTDIR)$(PREFIX)/bin cp -f piss $(DESTDIR)$(PREFIX)/bin chmod 755 $(DESTDIR)$(PREFIX)/bin/piss + mkdir -p $(DESTDIR)$(MANPREFIX)/man1 + cp -f src/piss.1 $(DESTDIR)$(MANPREFIX)/man1/piss.1 + chmod 644 $(DESTDIR)$(MANPREFIX)/man1/piss.1 uninstall: rm -f $(DESTDIR)$(PREFIX)/bin/piss + rm -f $(DESTDIR)$(MANPREFIX)/man1/piss.1 .PHONY: all clean install uninstall diff --git a/piss b/piss new file mode 100755 index 0000000..1972912 Binary files /dev/null and b/piss differ diff --git a/src/daemon.c b/src/daemon.c index 40ed473..7be3e73 100644 --- a/src/daemon.c +++ b/src/daemon.c @@ -1,6 +1,8 @@ +#define _GNU_SOURCE #include #include #include +#include #include #include #include @@ -16,7 +18,7 @@ typedef struct { unsigned int mod; - KeySym keysym; + KeyCode keycode; char cmd[MAX_CMD_LEN]; } Binding; @@ -25,325 +27,154 @@ static Window root; static Binding bindings[MAX_BINDS]; static int bind_count = 0; -unsigned int parse_modifiers(const char *mod_str) { +unsigned int parse_modifiers(const char *str) { unsigned int mod = 0; - char *str = strdup(mod_str); - char *token = strtok(str, "+"); - - while (token) { - while (*token == ' ') - token++; - char *end = token + strlen(token) - 1; - while (end > token && *end == ' ') - end--; - *(end + 1) = '\0'; - - if (strcasecmp(token, "super") == 0 || strcasecmp(token, "mod4") == 0) - mod |= Mod4Mask; - else if (strcasecmp(token, "shift") == 0) - mod |= ShiftMask; - else if (strcasecmp(token, "ctrl") == 0 || - strcasecmp(token, "control") == 0) - mod |= ControlMask; - else if (strcasecmp(token, "alt") == 0 || strcasecmp(token, "mod1") == 0) - mod |= Mod1Mask; - - token = strtok(NULL, "+"); - } - - free(str); + if (strcasestr(str, "super") || strcasestr(str, "mod4")) + mod |= Mod4Mask; + if (strcasestr(str, "shift")) + mod |= ShiftMask; + if (strcasestr(str, "ctrl") || strcasestr(str, "control")) + mod |= ControlMask; + if (strcasestr(str, "alt") || strcasestr(str, "mod1")) + mod |= Mod1Mask; return mod; } -KeySym parse_key(const char *key_str) { - char *str = strdup(key_str); - char *s = str; - while (*s == ' ') - s++; - char *end = s + strlen(s) - 1; - while (end > s && *end == ' ') - end--; - *(end + 1) = '\0'; - - KeySym ks = XStringToKeysym(s); - free(str); - return ks; +void grab_key(unsigned int mod, KeyCode kc) { + unsigned int numlock = Mod2Mask, capslock = LockMask; + XGrabKey(dpy, kc, mod, root, True, GrabModeAsync, GrabModeAsync); + XGrabKey(dpy, kc, mod | numlock, root, True, GrabModeAsync, GrabModeAsync); + XGrabKey(dpy, kc, mod | capslock, root, True, GrabModeAsync, GrabModeAsync); + XGrabKey(dpy, kc, mod | numlock | capslock, root, True, GrabModeAsync, + GrabModeAsync); } -void add_binding(const char *keycombo, const char *cmd) { - if (bind_count >= MAX_BINDS) { - fprintf(stderr, "piss: max bindings reached\n"); +void add_binding(char *msg) { + if (bind_count >= MAX_BINDS) return; - } - - char *combo = strdup(keycombo); - char *end = combo + strlen(combo) - 1; - while (end > combo && (*end == ' ' || *end == '\t' || *end == '\n')) { - *end = '\0'; - end--; - } - - char *plus = strrchr(combo, '+'); - - if (!plus) { - fprintf(stderr, "piss: invalid key combo: %s\n", keycombo); - free(combo); + char *sep = strstr(msg, "|||"); + if (!sep) return; - } + *sep = '\0'; + char *combo = msg, *cmd = sep + 3; - *plus = '\0'; - char *mod_str = combo; - char *key_str = plus + 1; - - bindings[bind_count].mod = parse_modifiers(mod_str); - bindings[bind_count].keysym = parse_key(key_str); + char *last_plus = strrchr(combo, '+'); + if (!last_plus) + return; + *last_plus = '\0'; + char *key_name = last_plus + 1; + while (*key_name == ' ') + key_name++; - if (bindings[bind_count].keysym == NoSymbol) { - fprintf(stderr, "piss: invalid key: %s (from %s)\n", key_str, keycombo); - free(combo); + KeySym sym = XStringToKeysym(key_name); + if (sym == NoSymbol) return; - } + bindings[bind_count].mod = parse_modifiers(combo); + bindings[bind_count].keycode = XKeysymToKeycode(dpy, sym); strncpy(bindings[bind_count].cmd, cmd, MAX_CMD_LEN - 1); - bindings[bind_count].cmd[MAX_CMD_LEN - 1] = '\0'; - - KeyCode kc = XKeysymToKeycode(dpy, bindings[bind_count].keysym); - if (kc != 0) { - XSync(dpy, False); - - XGrabKey(dpy, kc, bindings[bind_count].mod, root, True, GrabModeAsync, - GrabModeAsync); - XGrabKey(dpy, kc, bindings[bind_count].mod | Mod2Mask, root, True, - GrabModeAsync, GrabModeAsync); - XGrabKey(dpy, kc, bindings[bind_count].mod | LockMask, root, True, - GrabModeAsync, GrabModeAsync); - XGrabKey(dpy, kc, bindings[bind_count].mod | Mod2Mask | LockMask, root, - True, GrabModeAsync, GrabModeAsync); - - XSync(dpy, False); - fprintf(stderr, "piss: bound [%s] keysym=%lu keycode=%u mod=%u -> %s\n", - keycombo, bindings[bind_count].keysym, kc, bindings[bind_count].mod, - cmd); - - bind_count++; - } else { - fprintf(stderr, "piss: couldn't get keycode for: %s\n", keycombo); - } - - free(combo); + grab_key(bindings[bind_count].mod, bindings[bind_count].keycode); + bind_count++; + XSync(dpy, False); } -void execute_command(const char *cmd) { - if (fork() == 0) { - setsid(); - execl("/bin/sh", "sh", "-c", cmd, NULL); - exit(0); - } -} - -void handle_keypress(XEvent *ev) { - XKeyEvent *e = &ev->xkey; - KeySym ks = XkbKeycodeToKeysym(dpy, e->keycode, 0, 0); - unsigned int cleanmask = e->state & ~(Mod2Mask | LockMask); - - fprintf(stderr, "piss: key pressed: keysym=%lu mod=%u\n", ks, cleanmask); - - for (int i = 0; i < bind_count; i++) { - if (bindings[i].keysym == ks && bindings[i].mod == cleanmask) { - fprintf(stderr, "piss: executing: %s\n", bindings[i].cmd); - execute_command(bindings[i].cmd); - break; - } - } -} - -char *get_default_config() { - static char path[512]; - const char *home = getenv("HOME"); - if (!home) { - struct passwd *pw = getpwuid(getuid()); - home = pw->pw_dir; - } - snprintf(path, sizeof(path), "%s/.config/piss/pissrc", home); - return path; -} - -int send_bind_to_daemon(const char *keycombo, const char *cmd) { - int sock; - struct sockaddr_un addr; - char msg[MAX_CMD_LEN * 2]; - - sock = socket(AF_UNIX, SOCK_STREAM, 0); - if (sock < 0) { - return 1; - } - - memset(&addr, 0, sizeof(addr)); - addr.sun_family = AF_UNIX; +int client_mode(int retry, char *combo, char *cmd) { + struct sockaddr_un addr = {.sun_family = AF_UNIX}; strncpy(addr.sun_path, SOCK_PATH, sizeof(addr.sun_path) - 1); - if (connect(sock, (struct sockaddr *)&addr, sizeof(addr)) < 0) { - close(sock); - return 1; - } - - snprintf(msg, sizeof(msg), "%s|||%s", keycombo, cmd); - write(sock, msg, strlen(msg)); - close(sock); - return 0; -} - -void handle_socket_connection(int client_sock) { - char buf[MAX_CMD_LEN * 2]; - int n = read(client_sock, buf, sizeof(buf) - 1); - if (n > 0) { - buf[n] = '\0'; - - char *sep = strstr(buf, "|||"); - if (sep) { - *sep = '\0'; - char *keycombo = buf; - char *cmd = sep + 3; - add_binding(keycombo, cmd); + while (1) { + int sock = socket(AF_UNIX, SOCK_STREAM, 0); + if (connect(sock, (struct sockaddr *)&addr, sizeof(addr)) == 0) { + if (combo && cmd) { + char buf[MAX_CMD_LEN * 2]; + snprintf(buf, sizeof(buf), "%s|||%s", combo, cmd); + write(sock, buf, strlen(buf)); + } + close(sock); + return 0; } + close(sock); + if (!retry) + return 1; + usleep(50000); } - close(client_sock); } int main(int argc, char *argv[]) { - if (argc >= 4 && strcmp(argv[1], "bind") == 0) { - return send_bind_to_daemon(argv[2], argv[3]); - } - - char *config = (argc >= 2) ? argv[1] : get_default_config(); + if (argc >= 2 && strcmp(argv[1], "wait") == 0) + return client_mode(1, NULL, NULL); + if (argc >= 4 && strcmp(argv[1], "bind") == 0) + return client_mode(0, argv[2], argv[3]); dpy = XOpenDisplay(NULL); - if (!dpy) { - fprintf(stderr, "piss: cannot open display\n"); + if (!dpy) return 1; - } - root = DefaultRootWindow(dpy); + XSelectInput(dpy, root, KeyPressMask); - int sock, client_sock; - struct sockaddr_un addr; - - unlink(SOCK_PATH); - sock = socket(AF_UNIX, SOCK_STREAM, 0); - if (sock < 0) { - perror("socket"); - return 1; - } - - memset(&addr, 0, sizeof(addr)); - addr.sun_family = AF_UNIX; + int serv = socket(AF_UNIX, SOCK_STREAM, 0); + struct sockaddr_un addr = {.sun_family = AF_UNIX}; strncpy(addr.sun_path, SOCK_PATH, sizeof(addr.sun_path) - 1); + unlink(SOCK_PATH); + bind(serv, (struct sockaddr *)&addr, sizeof(addr)); + listen(serv, 10); - if (bind(sock, (struct sockaddr *)&addr, sizeof(addr)) < 0) { - perror("bind"); - return 1; - } - - if (listen(sock, 5) < 0) { - perror("listen"); - return 1; - } - - char piss_path[512]; - ssize_t len = readlink("/proc/self/exe", piss_path, sizeof(piss_path) - 1); + char path[512]; + ssize_t len = readlink("/proc/self/exe", path, sizeof(path) - 1); if (len != -1) { - piss_path[len] = '\0'; - setenv("PISS_BIN", piss_path, 1); + path[len] = '\0'; + setenv("PISS_BIN", path, 1); } - pid_t config_pid = fork(); - if (config_pid == 0) { - usleep(50000); - - FILE *f = fopen(config, "r"); - if (f) { - char line[256]; - if (fgets(line, sizeof(line), f) && strncmp(line, "#!", 2) == 0) { - fclose(f); - execl(config, config, NULL); - } else { - fclose(f); - execl("/bin/sh", "sh", config, NULL); - } + if (fork() == 0) { + char *cfg = (argc > 1) ? argv[1] : NULL; + if (!cfg) { + static char def[512]; + snprintf(def, 511, "%s/.config/piss/pissrc", getpwuid(getuid())->pw_dir); + cfg = def; } - - perror("execl"); + // Use execvp so the shebang (#! /usr/bin/lua) is respected + char *args[] = {cfg, NULL}; + execvp(cfg, args); + perror("execvp"); exit(1); } - fd_set readfds; - struct timeval tv; - int status; - - while (waitpid(config_pid, &status, WNOHANG) == 0) { - FD_ZERO(&readfds); - FD_SET(sock, &readfds); - tv.tv_sec = 0; - tv.tv_usec = 50000; - - if (select(sock + 1, &readfds, NULL, NULL, &tv) > 0) { - client_sock = accept(sock, NULL, NULL); - if (client_sock >= 0) { - handle_socket_connection(client_sock); - } - } - } - - for (int i = 0; i < 5; i++) { - FD_ZERO(&readfds); - FD_SET(sock, &readfds); - tv.tv_sec = 0; - tv.tv_usec = 10000; - - if (select(sock + 1, &readfds, NULL, NULL, &tv) > 0) { - client_sock = accept(sock, NULL, NULL); - if (client_sock >= 0) { - handle_socket_connection(client_sock); - } - } - } - - XSelectInput(dpy, root, KeyPressMask); - - printf("piss: running with %d bindings\n", bind_count); - - int x11_fd = ConnectionNumber(dpy); - - fprintf(stderr, "piss: listening for keypresses (X11 fd=%d)...\n", x11_fd); - + int xfd = ConnectionNumber(dpy); while (1) { - FD_ZERO(&readfds); - FD_SET(x11_fd, &readfds); - FD_SET(sock, &readfds); - int max_fd = (x11_fd > sock) ? x11_fd : sock; - - if (select(max_fd + 1, &readfds, NULL, NULL, NULL) > 0) { - if (FD_ISSET(x11_fd, &readfds)) { - while (XPending(dpy)) { - XEvent ev; - XNextEvent(dpy, &ev); - fprintf(stderr, "piss: received X event type=%d\n", ev.type); - if (ev.type == KeyPress) - handle_keypress(&ev); - } + fd_set fds; + FD_ZERO(&fds); + FD_SET(xfd, &fds); + FD_SET(serv, &fds); + int max = (xfd > serv) ? xfd : serv; + + if (select(max + 1, &fds, NULL, NULL, NULL) > 0) { + if (FD_ISSET(serv, &fds)) { + int cli = accept(serv, NULL, NULL); + char buf[MAX_CMD_LEN * 2] = {0}; + if (read(cli, buf, sizeof(buf) - 1) > 0) + add_binding(buf); + close(cli); } - - if (FD_ISSET(sock, &readfds)) { - client_sock = accept(sock, NULL, NULL); - if (client_sock >= 0) { - handle_socket_connection(client_sock); + while (XPending(dpy)) { + XEvent ev; + XNextEvent(dpy, &ev); + if (ev.type == KeyPress) { + unsigned int state = ev.xkey.state & ~(Mod2Mask | LockMask); + for (int i = 0; i < bind_count; i++) { + if (bindings[i].keycode == ev.xkey.keycode && + bindings[i].mod == state) { + if (fork() == 0) { + setsid(); + execl("/bin/sh", "sh", "-c", bindings[i].cmd, NULL); + exit(0); + } + } + } } } } } - - unlink(SOCK_PATH); - XCloseDisplay(dpy); return 0; } diff --git a/src/piss.1 b/src/piss.1 new file mode 100644 index 0000000..5b5f0d6 --- /dev/null +++ b/src/piss.1 @@ -0,0 +1,68 @@ +.TH PISS 1 "DECEMBER 2025" "1.0" "PISS Manual" +.SH NAME +piss \- a lightweight socket-based X11 keybinding daemon +.SH SYNOPSIS +.B piss +[\fICONFIG_FILE\fR] +.br +.B piss bind +\fIKEYCOMBO\fR \fICOMMAND\fR +.br +.B piss wait +.SH DESCRIPTION +.B piss +is a minimal utility designed to manage X11 global hotkeys. It operates as a daemon that listens on a Unix domain socket for binding requests. +.PP +When started without a command, +.B piss +launches the daemon, creates a socket at \fB/tmp/piss.sock\fR, and executes the specified \fICONFIG_FILE\fR. This architecture allows for dynamic configuration using any scripting language. +.SH COMMANDS +.TP +.B (default) +Starts the daemon mode. If no \fICONFIG_FILE\fR is provided, it defaults to \fB~/.config/piss/pissrc\fR. The daemon exports the \fBPISS_BIN\fR environment variable to the child process. +.TP +.B bind \fIKEYCOMBO\fR \fICOMMAND\fR +Connects to the running daemon and registers a new binding. \fIKEYCOMBO\fR should follow the format "Modifier+Key" (e.g., "Super+Shift+Return"). +.TP +.B wait +Blocks execution until the daemon has successfully initialized its Unix socket. This is essential for preventing race conditions in configuration scripts. +.SH ENVIRONMENT +.TP +.B PISS_BIN +Set by the daemon to the absolute path of the current binary. Recommended for use within scripts to ensure the client calls the correct version of the tool. +.SH FILES +.TP +.I /tmp/piss.sock +The Unix domain socket used for IPC communication. +.TP +.I ~/.config/piss/pissrc +The default user configuration script (must be executable). +.SH EXAMPLES +.PP +Example of a Lua-based \fBpissrc\fR: +.IP +#!/usr/bin/lua +.br +local p = os.getenv("PISS_BIN") or "piss" +.br +os.execute(p .. " wait") +.br +os.execute(p .. ' bind "Super+Return" "alacritty"') +.SH TROUBLESHOOTING +.TP +.B Syntax error: "(" unexpected +This occurs if the daemon tries to run a Lua config using \fB/bin/sh\fR. Ensure your \fBpissrc\fR has a proper shebang (\fB#!/usr/bin/lua\fR) and that the daemon is using \fBexecvp\fR instead of \fBexecl\fR. +.TP +.B piss: cannot open display +The \fBDISPLAY\fR environment variable is not set. Ensure you are running \fBpiss\fR within an active X11 session. +.TP +.B Bindings not working +Check if another window manager or application has already "grabbed" the key. \fBpiss\fR cannot override existing global grabs from programs like \fBi3\fR or \fBbspwm\fR if they started first. +.TP +.B pissA: not found +This indicates a buffer overflow or null-termination issue in the \fBPISS_BIN\fR detection logic. Ensure \fBreadlink\fR is correctly null-terminating the path string in \fBdaemon.c\fR. +.TP +.B Connection refused +The \fBpiss wait\fR command timed out or the daemon crashed. Check if \fB/tmp/piss.sock\fR exists. If a stale socket remains after a crash, delete it manually. +.SH DIAGNOSTICS +If \fBpiss\fR fails to launch the config script, ensure the script is executable (\fBchmod +x\fR) and has a valid shebang line. -- cgit v1.3