aboutsummaryrefslogtreecommitdiff
path: root/patches/ttym-notify-3.0.diff
diff options
context:
space:
mode:
Diffstat (limited to 'patches/ttym-notify-3.0.diff')
-rw-r--r--patches/ttym-notify-3.0.diff259
1 files changed, 259 insertions, 0 deletions
diff --git a/patches/ttym-notify-3.0.diff b/patches/ttym-notify-3.0.diff
new file mode 100644
index 0000000..9ccfd5c
--- /dev/null
+++ b/patches/ttym-notify-3.0.diff
@@ -0,0 +1,259 @@
+diff -ruN a/config.def.h b/config.def.h
+--- a/config.def.h 2026-08-25 17:18:15.698123000 +0200
++++ b/config.def.h 2026-08-25 17:18:15.702123073 +0200
+@@ -4,6 +4,19 @@
+ 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 = "";
++
+ /* persistent alert: keep ringing the bell until any key is pressed.
+ * Foreground-only; backgrounded runs always fall through.
+ * Override per-run with --no-persist.
+diff -ruN a/config.h b/config.h
+--- a/config.h 2026-08-25 17:18:15.698123000 +0200
++++ b/config.h 2026-08-25 17:18:15.702123073 +0200
+@@ -4,6 +4,19 @@
+ 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 = "";
++
+ /* persistent alert: keep ringing the bell until any key is pressed.
+ * Foreground-only; backgrounded runs always fall through.
+ * Override per-run with --no-persist.
+Binary files a/timer and b/timer differ
+diff -ruN a/ttym.c b/ttym.c
+--- a/ttym.c 2026-08-25 17:18:15.698123000 +0200
++++ b/ttym.c 2026-08-25 17:18:15.702123073 +0200
+@@ -13,6 +13,7 @@
+ #include <string.h>
+ #include <strings.h>
+ #include <sys/ioctl.h>
++#include <sys/wait.h>
+ #include <termios.h>
+ #include <time.h>
+ #include <unistd.h>
+@@ -24,6 +25,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];
+ int alert_persist; /* alert loop until keypress; --no-persist disables */
+ int flash; /* reverse-video flash during alert loop */
+ char bar_style[32]; /* --bar STYLE */
+@@ -236,6 +241,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);
++ }
++ }
++ }
++}
++
+ /* ===== alert loop =====
+ * Bell + poll for keypress, ~1.5s per beat; 4-min safety cap.
+ */
+@@ -340,8 +481,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"
+@@ -471,6 +612,7 @@
+ printf("\r\033[32mDONE %s 100%%\033[0m\033[K\n", tb);
+ 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);
+@@ -491,6 +633,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");
+ cfg.alert_persist = ALERT_PERSIST;
+ cfg.flash = FLASH;
+
+@@ -539,6 +685,16 @@
+ fprintf(stderr, "bad duration: %s\n", rem_argv[0]);
+ 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;
++ }
++ }
+ }
+
+ return run(total, stopwatch, &cfg);