aboutsummaryrefslogtreecommitdiff
path: root/patches/ttym-notify-2.0.diff
diff options
context:
space:
mode:
Diffstat (limited to 'patches/ttym-notify-2.0.diff')
-rw-r--r--patches/ttym-notify-2.0.diff232
1 files changed, 232 insertions, 0 deletions
diff --git a/patches/ttym-notify-2.0.diff b/patches/ttym-notify-2.0.diff
new file mode 100644
index 0000000..a80eef3
--- /dev/null
+++ b/patches/ttym-notify-2.0.diff
@@ -0,0 +1,232 @@
+diff -ruN a/config.def.h b/config.def.h
+--- a/config.def.h 2026-05-20 12:03:30.597867999 +0200
++++ b/config.def.h 2026-05-20 12:03:30.597867999 +0200
+@@ -3,3 +3,16 @@
+ /* progress bar glyphs (ASCII default) */
+ static const char *const BAR_FILL = ">";
+ static const char *const BAR_EMPTY = "-";
++
++/* desktop notification on countdown completion.
++ * Tokens are split on whitespace; placeholders {title} and {msg} are expanded.
++ * Set NULL or "" to disable.
++ */
++static const char *const TITLE = "Timer";
++static const char *const NOTIFY_CMD = "notify-send -u critical {title} {msg}";
++
++/* sound on countdown completion. If SOUND_CMD is "" the binary autodetects
++ * a player (paplay/aplay/mpv/ffplay) and plays a default freedesktop sound.
++ * SOUND_CMD is split on whitespace; same token rules as NOTIFY_CMD.
++ */
++static const char *const SOUND_CMD = "";
+diff -ruN a/ttym.c b/ttym.c
+--- a/ttym.c 2026-05-20 12:03:30.597867999 +0200
++++ b/ttym.c 2026-05-20 12:03:53.438340036 +0200
+@@ -12,6 +12,7 @@
+ #include <stdlib.h>
+ #include <string.h>
+ #include <sys/ioctl.h>
++#include <sys/wait.h>
+ #include <termios.h>
+ #include <time.h>
+ #include <unistd.h>
+@@ -23,6 +24,10 @@
+ typedef struct {
+ int quiet; /* -q: suppress completion bell */
+ int silent; /* -s: no bell, no extras */
++ const char *title;
++ const char *notify_cmd;
++ const char *sound_cmd;
++ char msg[1024];
+ } Config;
+
+ static struct termios oldtios;
+@@ -226,6 +231,142 @@
+ fflush(stdout);
+ }
+
++/* ===== subprocess + placeholder expansion =====
++ * Tokens split on whitespace; {title} and {msg} expanded within each token.
++ * No shell involved — argv stays argv.
++ */
++
++static int
++build_argv(const char *cmd, const Config *c, char ***out)
++{
++ char buf[2048];
++ snprintf(buf, sizeof(buf), "%s", cmd);
++ char **argv = calloc(64, sizeof(char *));
++ if (!argv) return -1;
++ int argc = 0;
++ char *p = buf;
++ while (*p && argc < 63) {
++ while (*p && isspace((unsigned char)*p)) p++;
++ if (!*p) break;
++ char *start = p;
++ while (*p && !isspace((unsigned char)*p)) p++;
++ if (*p) *p++ = '\0';
++ char tok[2048]; size_t oi = 0;
++ for (char *q = start; *q && oi < sizeof(tok) - 1; ) {
++ const char *sub = NULL; size_t skip = 0;
++ if (!strncmp(q, "{msg}", 5)) { sub = c->msg; skip = 5; }
++ else if (!strncmp(q, "{title}", 7)) { sub = c->title; skip = 7; }
++ if (sub) {
++ size_t sl = strlen(sub);
++ if (oi + sl >= sizeof(tok)) sl = sizeof(tok) - 1 - oi;
++ memcpy(tok + oi, sub, sl);
++ oi += sl; q += skip;
++ } else { tok[oi++] = *q++; }
++ }
++ tok[oi] = '\0';
++ argv[argc++] = strdup(tok);
++ }
++ argv[argc] = NULL;
++ *out = argv;
++ return argc;
++}
++
++static void
++free_argv(char **argv)
++{
++ if (!argv) return;
++ for (int i = 0; argv[i]; i++) free(argv[i]);
++ free(argv);
++}
++
++static void
++spawn_bg(char *const argv[])
++{
++ pid_t pid = fork();
++ if (pid == 0) {
++ setsid();
++ int devnull = open("/dev/null", O_RDWR);
++ if (devnull >= 0) {
++ dup2(devnull, 0); dup2(devnull, 1); dup2(devnull, 2);
++ if (devnull > 2) close(devnull);
++ }
++ execvp(argv[0], argv);
++ _exit(127);
++ }
++ (void)pid;
++}
++
++static int
++command_exists(const char *name)
++{
++ const char *path = getenv("PATH");
++ if (!path) return 0;
++ char buf[512];
++ const char *p = path;
++ while (*p) {
++ const char *e = strchr(p, ':');
++ size_t len = e ? (size_t)(e - p) : strlen(p);
++ if (len > 0 && len + strlen(name) + 2 < sizeof(buf)) {
++ snprintf(buf, sizeof(buf), "%.*s/%s", (int)len, p, name);
++ if (access(buf, X_OK) == 0) return 1;
++ }
++ if (!e) break;
++ p = e + 1;
++ }
++ return 0;
++}
++
++static const char *
++detect_sound_player(void)
++{
++ static const char *cands[] = { "paplay", "aplay", "mpv", "ffplay", NULL };
++ for (int i = 0; cands[i]; i++)
++ if (command_exists(cands[i])) return cands[i];
++ return NULL;
++}
++
++static const char *
++default_sound_path(void)
++{
++ static const char *paths[] = {
++ "/usr/share/sounds/freedesktop/stereo/complete.oga",
++ "/usr/share/sounds/freedesktop/stereo/alarm-clock-elapsed.oga",
++ "/usr/share/sounds/alsa/Front_Center.wav",
++ NULL
++ };
++ for (int i = 0; paths[i]; i++)
++ if (access(paths[i], R_OK) == 0) return paths[i];
++ return NULL;
++}
++
++static void
++notify_and_sound(const Config *c)
++{
++ if (c->notify_cmd && c->notify_cmd[0] && command_exists("notify-send")) {
++ char **a;
++ if (build_argv(c->notify_cmd, c, &a) > 0) {
++ spawn_bg(a);
++ free_argv(a);
++ }
++ }
++ if (!c->quiet) {
++ if (c->sound_cmd && c->sound_cmd[0]) {
++ char **a;
++ if (build_argv(c->sound_cmd, c, &a) > 0) {
++ spawn_bg(a);
++ free_argv(a);
++ }
++ } else {
++ const char *pl = detect_sound_player();
++ const char *sp = default_sound_path();
++ if (pl && sp) {
++ char *a[] = { (char *)pl, (char *)sp, NULL };
++ spawn_bg(a);
++ }
++ }
++ }
++}
++
+ /* ===== usage =====
+ * Patches MUST update this in lockstep with new flags so -h always
+ * reflects the build.
+@@ -236,8 +377,8 @@
+ {
+ fprintf(f,
+ "usage:\n"
+-" timer [FLAGS] DURATION countdown\n"
+-" timer [FLAGS] stopwatch\n"
++" timer [FLAGS] DURATION [MESSAGE...] countdown\n"
++" timer [FLAGS] stopwatch\n"
+ "\n"
+ "DURATION: 25m | 90s | 2h | 1h30m | 01:30:00 | 25:00\n"
+ "FLAGS:\n"
+@@ -370,6 +511,7 @@
+ printf("\r\033[32mDONE %s 100%%%*s\033[0m\n", tb, 20, "");
+ else
+ printf("DONE %s 100%%\n", tb);
++ if (!cfg->silent) notify_and_sound(cfg);
+ if (!cfg->silent && !cfg->quiet && out_tty) {
+ fputc('\a', stdout);
+ fflush(stdout);
+@@ -387,6 +529,10 @@
+ main(int argc, char **argv)
+ {
+ Config cfg = {0};
++ cfg.title = TITLE;
++ cfg.notify_cmd = NOTIFY_CMD;
++ cfg.sound_cmd = SOUND_CMD;
++ snprintf(cfg.msg, sizeof(cfg.msg), "%s", "Time is up");
+
+ /* Pre-strip long-form flags before getopt. Stop at "--". */
+ int new_argc = 0;
+@@ -424,6 +570,16 @@
+ free(new_argv);
+ return 1;
+ }
++ if (rem_argc > 1) {
++ size_t off = 0;
++ cfg.msg[0] = '\0';
++ for (int i = 1; i < rem_argc && off < sizeof(cfg.msg) - 2; i++) {
++ int n = snprintf(cfg.msg + off, sizeof(cfg.msg) - off,
++ "%s%s", (i > 1 ? " " : ""), rem_argv[i]);
++ if (n < 0) break;
++ off += (size_t)n;
++ }
++ }
+ }
+
+ int rc = run(total, stopwatch, &cfg);