summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--makefile7
-rwxr-xr-xpissbin0 -> 22448 bytes
-rw-r--r--src/daemon.c385
-rw-r--r--src/piss.168
4 files changed, 182 insertions, 278 deletions
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
--- /dev/null
+++ b/piss
Binary files 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 <X11/XKBlib.h>
#include <X11/Xlib.h>
#include <X11/keysym.h>
+#include <errno.h>
#include <pwd.h>
#include <stdio.h>
#include <stdlib.h>
@@ -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);
-}
-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;
+ grab_key(bindings[bind_count].mod, bindings[bind_count].keycode);
+ bind_count++;
+ XSync(dpy, False);
}
-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;
+ fd_set fds;
+ FD_ZERO(&fds);
+ FD_SET(xfd, &fds);
+ FD_SET(serv, &fds);
+ int max = (xfd > serv) ? xfd : serv;
- 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);
- }
+ 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.