diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-25 19:02:35 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-25 19:02:35 +0200 |
| commit | 8f99f1650e4a7363af1d83190b4082fb72a9f73f (patch) | |
| tree | 86ed3b714c376e95f98d33d7572e76d6b43f06d3 /patches/ttym-notify-2.0.diff | |
| parent | 2f6b29abd0d766ef477eb29c5a7bf6c1e88e9630 (diff) | |
| download | ttym-8f99f1650e4a7363af1d83190b4082fb72a9f73f.tar.gz ttym-8f99f1650e4a7363af1d83190b4082fb72a9f73f.zip | |
ttym 3.0: fix terminal and config bugs, fold terminal-only patches into base
Correctness fixes:
- restore the terminal on SIGQUIT and SIGHUP. atexit never runs on an
uncaught fatal signal, so Ctrl+\ left the cursor hidden and autowrap
disabled.
- drop the unconditional newline in restore_tty. Every run emitted a
stray blank line, including piped output.
- treat EINTR as an interruption rather than a poll() failure, which
delayed Ctrl+C by up to one 100ms frame.
config-file patch:
- make comment stripping quote-aware so a value may contain '#', and
strip one matched pair of quotes. bar_fill = "#" was silently dropped,
despite '#' being the glyph of the hash style this build ships.
- resolve bar_fill/bar_empty inside resolve_bar and clear them on --bar,
so command-line flags beat the config file as documented. Previously
config glyphs silently overrode --bar.
- report unknown keys, unparseable booleans and malformed lines on
stderr. flash = enabled silently meant off.
- never write to disk. --dump-config prints a commented template on
stdout instead of the binary creating ~/.config/ttym/config on first
run.
Patch set:
- fold persist-alert, flash and bar-styles into the base. The base is
now terminal-only: it opens /dev/tty, draws and exits, with no file
writes and no subprocesses. Everything still in patches/ changes what
the program touches.
- seven patches become four; config-file's prerequisites drop from six
patches to one (notify), so it is regenerable mechanically.
- regenerate every patch. All apply with zero fuzz and compile.
Source:
- rewrite argv in place rather than into a malloc'd copy (C99
5.1.2.2.1), removing die(), four free() calls and <stdarg.h>.
- fold the duplicated elapsed-time expression into elapsed().
- drop dead code: have_any, (void)cfg, and an unreachable branch in
parse_duration.
- replace hand-counted padding with \033[K.
Docs and build:
- timer.1: -c is the third hook argument, not the fourth. Add EXIT
STATUS (a stopwatch always exits 130 -- it has no natural end) and
ENVIRONMENT. Document config precedence and quoting.
- wire VERSION into a dist target; it was defined and never used.
- track .gitignore, which previously ignored itself, so a fresh clone
had no ignore rules.
Claude-Session: https://claude.ai/code/session_01APLBs8RB1FcUaC4viVkzbP
Diffstat (limited to 'patches/ttym-notify-2.0.diff')
| -rw-r--r-- | patches/ttym-notify-2.0.diff | 232 |
1 files changed, 0 insertions, 232 deletions
diff --git a/patches/ttym-notify-2.0.diff b/patches/ttym-notify-2.0.diff deleted file mode 100644 index a80eef3..0000000 --- a/patches/ttym-notify-2.0.diff +++ /dev/null @@ -1,232 +0,0 @@ -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); |
