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-logging-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-logging-2.0.diff')
| -rw-r--r-- | patches/ttym-logging-2.0.diff | 447 |
1 files changed, 0 insertions, 447 deletions
diff --git a/patches/ttym-logging-2.0.diff b/patches/ttym-logging-2.0.diff deleted file mode 100644 index d654d50..0000000 --- a/patches/ttym-logging-2.0.diff +++ /dev/null @@ -1,447 +0,0 @@ -diff -ruN a/config.def.h b/config.def.h ---- a/config.def.h 2026-05-20 11:57:26.722347891 +0200 -+++ b/config.def.h 2026-05-20 11:57:26.726347974 +0200 -@@ -3,3 +3,8 @@ - /* progress bar glyphs (ASCII default) */ - static const char *const BAR_FILL = ">"; - static const char *const BAR_EMPTY = "-"; -+ -+/* log location is ~/.config/ttym/ttym.log (honours $XDG_CONFIG_HOME). -+ * One-shot migration: ~/.config/timer/ -> ~/.config/ttym/ if the new -+ * directory doesn't already exist; legacy log file inside is renamed. -+ */ -diff -ruN a/ttym.c b/ttym.c ---- a/ttym.c 2026-05-20 11:57:26.722347891 +0200 -+++ b/ttym.c 2026-05-20 11:57:26.726347974 +0200 -@@ -12,6 +12,7 @@ - #include <stdlib.h> - #include <string.h> - #include <sys/ioctl.h> -+#include <sys/stat.h> - #include <termios.h> - #include <time.h> - #include <unistd.h> -@@ -23,6 +24,7 @@ - typedef struct { - int quiet; /* -q: suppress completion bell */ - int silent; /* -s: no bell, no extras */ -+ char comment[512]; /* -c TEXT: logged with each entry */ - } Config; - - static struct termios oldtios; -@@ -226,6 +228,367 @@ - fflush(stdout); - } - -+/* ===== log (TSV: ts<TAB>seconds<TAB>comment) ===== */ -+ -+static int -+build_config_path(char *buf, size_t bufsz, const char *suffix) -+{ -+ const char *home = getenv("HOME"); -+ const char *xdg = getenv("XDG_CONFIG_HOME"); -+ if (xdg && *xdg) snprintf(buf, bufsz, "%s/ttym/%s", xdg, suffix); -+ else if (home && *home) snprintf(buf, bufsz, "%s/.config/ttym/%s", home, suffix); -+ else return -1; -+ return 0; -+} -+ -+static void -+migrate_legacy_dir(void) -+{ -+ const char *home = getenv("HOME"); -+ const char *xdg = getenv("XDG_CONFIG_HOME"); -+ char base[512]; -+ if (xdg && *xdg) snprintf(base, sizeof(base), "%s", xdg); -+ else if (home && *home) snprintf(base, sizeof(base), "%s/.config", home); -+ else return; -+ char old_dir[600], new_dir[600]; -+ snprintf(old_dir, sizeof(old_dir), "%s/timer", base); -+ snprintf(new_dir, sizeof(new_dir), "%s/ttym", base); -+ struct stat sb; -+ if (stat(new_dir, &sb) == 0) return; -+ if (stat(old_dir, &sb) != 0 || !S_ISDIR(sb.st_mode)) return; -+ if (rename(old_dir, new_dir) != 0) return; -+ char old_log[700], new_log[700]; -+ snprintf(old_log, sizeof(old_log), "%s/timer.log", new_dir); -+ snprintf(new_log, sizeof(new_log), "%s/ttym.log", new_dir); -+ if (stat(old_log, &sb) == 0) rename(old_log, new_log); -+} -+ -+static int -+mkdir_p(const char *path, mode_t mode) -+{ -+ char buf[1024]; -+ snprintf(buf, sizeof(buf), "%s", path); -+ for (char *p = buf + 1; *p; p++) { -+ if (*p == '/') { -+ *p = '\0'; -+ if (mkdir(buf, mode) != 0 && errno != EEXIST) return -1; -+ *p = '/'; -+ } -+ } -+ if (mkdir(buf, mode) != 0 && errno != EEXIST) return -1; -+ return 0; -+} -+ -+static void -+iso_local(char *buf, size_t bufsz) -+{ -+ time_t t = time(NULL); -+ struct tm tm; -+ localtime_r(&t, &tm); -+ strftime(buf, bufsz, "%Y-%m-%d %H:%M:%S", &tm); -+} -+ -+static void -+sanitize_field(char *s) -+{ -+ for (; *s; s++) -+ if (*s == '\t' || *s == '\n' || *s == '\r') *s = ' '; -+} -+ -+static int -+is_old_format(const char *line) -+{ -+ return strlen(line) >= 11 && line[10] == 'T'; -+} -+ -+static void -+migrate_line(const char *line, char *out, size_t outsz) -+{ -+ if (strlen(line) < 19) { snprintf(out, outsz, "%s", line); return; } -+ char date[11], tpart[9]; -+ memcpy(date, line, 10); date[10] = '\0'; -+ memcpy(tpart, line + 11, 8); tpart[8] = '\0'; -+ const char *tab = strchr(line, '\t'); -+ if (!tab) { snprintf(out, outsz, "%s %s\n", date, tpart); return; } -+ snprintf(out, outsz, "%s %s%s", date, tpart, tab); -+} -+ -+static void -+migrate_log_if_needed(const char *path) -+{ -+ FILE *f = fopen(path, "r"); -+ if (!f) return; -+ char probe[2048]; -+ int needs = 0; -+ while (fgets(probe, sizeof(probe), f)) -+ if (is_old_format(probe)) { needs = 1; break; } -+ if (!needs) { fclose(f); return; } -+ rewind(f); -+ char tmp[1024]; -+ snprintf(tmp, sizeof(tmp), "%s.tmp", path); -+ FILE *out = fopen(tmp, "w"); -+ if (!out) { fclose(f); return; } -+ char line[2048]; -+ while (fgets(line, sizeof(line), f)) { -+ if (is_old_format(line)) { -+ char conv[2048]; -+ migrate_line(line, conv, sizeof(conv)); -+ fputs(conv, out); -+ } else fputs(line, out); -+ } -+ fclose(f); -+ fclose(out); -+ rename(tmp, path); -+} -+ -+static void -+write_log(long duration_s, const char *comment) -+{ -+ char path[512]; -+ if (build_config_path(path, sizeof(path), "ttym.log") < 0) return; -+ char dir[512]; -+ snprintf(dir, sizeof(dir), "%s", path); -+ char *slash = strrchr(dir, '/'); -+ if (slash) { *slash = '\0'; mkdir_p(dir, 0700); } -+ migrate_log_if_needed(path); -+ int fd = open(path, O_WRONLY | O_CREAT | O_APPEND, 0600); -+ if (fd < 0) return; -+ char ts[32]; iso_local(ts, sizeof(ts)); -+ char safe[512]; snprintf(safe, sizeof(safe), "%s", comment ? comment : ""); -+ sanitize_field(safe); -+ char line[1024]; -+ int n = snprintf(line, sizeof(line), "%s\t%ld\t%s\n", ts, duration_s, safe); -+ if (n > 0) { ssize_t w = write(fd, line, (size_t)n); (void)w; } -+ close(fd); -+} -+ -+static int -+read_log_lines(char ***out_lines, size_t *out_n) -+{ -+ char path[512]; -+ if (build_config_path(path, sizeof(path), "ttym.log") < 0) return -1; -+ migrate_log_if_needed(path); -+ FILE *f = fopen(path, "r"); -+ if (!f) { *out_lines = NULL; *out_n = 0; return 0; } -+ size_t cap = 64, n = 0; -+ char **lines = malloc(cap * sizeof(char *)); -+ if (!lines) { fclose(f); return -1; } -+ char buf[2048]; -+ while (fgets(buf, sizeof(buf), f)) { -+ size_t l = strlen(buf); -+ if (l && buf[l-1] == '\n') buf[l-1] = '\0'; -+ if (!*buf) continue; -+ if (n >= cap) { -+ cap *= 2; -+ char **nl = realloc(lines, cap * sizeof(char *)); -+ if (!nl) break; -+ lines = nl; -+ } -+ lines[n++] = strdup(buf); -+ } -+ fclose(f); -+ *out_lines = lines; -+ *out_n = n; -+ return 0; -+} -+ -+static void -+free_lines(char **lines, size_t n) -+{ -+ if (!lines) return; -+ for (size_t i = 0; i < n; i++) free(lines[i]); -+ free(lines); -+} -+ -+static int -+split_log_line(char *line, char **ts, long *secs, char **comment) -+{ -+ char *t1 = strchr(line, '\t'); if (!t1) return -1; -+ *t1 = '\0'; -+ char *t2 = strchr(t1 + 1, '\t'); if (!t2) return -1; -+ *t2 = '\0'; -+ char *end; -+ long v = strtol(t1 + 1, &end, 10); -+ if (end == t1 + 1) return -1; -+ *ts = line; *secs = v; *comment = t2 + 1; -+ return 0; -+} -+ -+static void -+fmt_long(long s, char *buf, size_t bufsz) -+{ -+ if (s < 0) s = 0; -+ long d = s / 86400; s %= 86400; -+ long h = s / 3600; s %= 3600; -+ long m = s / 60; -+ long sec = s % 60; -+ snprintf(buf, bufsz, "%02ld %02ld:%02ld:%02ld", d, h, m, sec); -+} -+ -+static int -+cmd_read(int argc, char **argv) -+{ -+ int n_tail = 0, raw = 0; -+ for (int i = 0; i < argc; i++) { -+ if (!strcmp(argv[i], "-n") && i + 1 < argc) { -+ n_tail = atoi(argv[++i]); -+ if (n_tail < 0) n_tail = 0; -+ } else if (!strcmp(argv[i], "--raw")) { -+ raw = 1; -+ } else { -+ fprintf(stderr, "timer -r: unknown arg: %s\n", argv[i]); -+ return 1; -+ } -+ } -+ char **lines = NULL; size_t n = 0; -+ if (read_log_lines(&lines, &n) < 0) return 1; -+ if (n == 0) { free_lines(lines, n); return 0; } -+ size_t start = 0; -+ if (n_tail > 0 && (size_t)n_tail < n) start = n - n_tail; -+ if (raw) { -+ for (size_t i = start; i < n; i++) puts(lines[i]); -+ } else { -+ printf("%-19s %-11s %s\n", "TIMESTAMP", "DURATION", "COMMENT"); -+ for (size_t i = start; i < n; i++) { -+ char *copy = strdup(lines[i]); -+ char *ts, *cmt; long secs; -+ if (split_log_line(copy, &ts, &secs, &cmt) == 0) { -+ char dur[32]; -+ fmt_long(secs, dur, sizeof(dur)); -+ printf("%-19s %-11s %s\n", ts, dur, cmt); -+ } else { -+ puts(lines[i]); -+ } -+ free(copy); -+ } -+ } -+ free_lines(lines, n); -+ return 0; -+} -+ -+static int -+parse_log_ts(const char *ts, struct tm *out) -+{ -+ memset(out, 0, sizeof(*out)); -+ if (sscanf(ts, "%d-%d-%d %d:%d:%d", -+ &out->tm_year, &out->tm_mon, &out->tm_mday, -+ &out->tm_hour, &out->tm_min, &out->tm_sec) != 6) return -1; -+ out->tm_year -= 1900; -+ out->tm_mon -= 1; -+ out->tm_isdst = -1; -+ return 0; -+} -+ -+typedef struct TagAcc { -+ char tag[128]; -+ long secs; -+ long count; -+ struct TagAcc *next; -+} TagAcc; -+ -+static void -+tag_add(TagAcc **head, const char *tag, long secs) -+{ -+ for (TagAcc *a = *head; a; a = a->next) -+ if (!strcmp(a->tag, tag)) { a->secs += secs; a->count++; return; } -+ TagAcc *n = calloc(1, sizeof(*n)); -+ if (!n) return; -+ snprintf(n->tag, sizeof(n->tag), "%s", tag); -+ n->secs = secs; -+ n->count = 1; -+ n->next = *head; -+ *head = n; -+} -+ -+static void -+extract_tags(const char *comment, long secs, TagAcc **head) -+{ -+ const char *p = comment; -+ int found = 0; -+ while (*p) { -+ int at_boundary = (p == comment) || isspace((unsigned char)*(p-1)); -+ if (*p == '+' && at_boundary) { -+ const char *s = p + 1, *e = s; -+ while (*e && !isspace((unsigned char)*e)) e++; -+ if (e > s) { -+ char buf[128]; -+ size_t len = (size_t)(e - s); -+ if (len >= sizeof(buf)) len = sizeof(buf) - 1; -+ memcpy(buf, s, len); buf[len] = '\0'; -+ tag_add(head, buf, secs); -+ found = 1; -+ } -+ p = e; -+ } else p++; -+ } -+ if (!found) tag_add(head, "(untagged)", secs); -+} -+ -+static int -+cmd_stats(int argc, char **argv) -+{ -+ const char *period = (argc > 0) ? argv[0] : "today"; -+ time_t now = time(NULL); -+ struct tm now_tm; localtime_r(&now, &now_tm); -+ time_t cutoff = 0; -+ if (!strcmp(period, "all")) cutoff = 0; -+ else if (!strcmp(period, "today")) { -+ struct tm t = now_tm; t.tm_hour = t.tm_min = t.tm_sec = 0; -+ cutoff = mktime(&t); -+ } else if (!strcmp(period, "week")) { -+ struct tm t = now_tm; t.tm_hour = t.tm_min = t.tm_sec = 0; -+ int back = (t.tm_wday == 0) ? 6 : t.tm_wday - 1; -+ t.tm_mday -= back; -+ cutoff = mktime(&t); -+ } else if (!strcmp(period, "month")) { -+ struct tm t = now_tm; t.tm_hour = t.tm_min = t.tm_sec = 0; t.tm_mday = 1; -+ cutoff = mktime(&t); -+ } else { -+ fprintf(stderr, "stats: unknown period '%s' (today|week|month|all)\n", period); -+ return 1; -+ } -+ char **lines = NULL; size_t n = 0; -+ if (read_log_lines(&lines, &n) < 0) return 1; -+ long total = 0, count = 0; -+ TagAcc *tags = NULL; -+ for (size_t i = 0; i < n; i++) { -+ char *copy = strdup(lines[i]); -+ char *ts, *cmt; long secs; -+ if (split_log_line(copy, &ts, &secs, &cmt) != 0) { free(copy); continue; } -+ struct tm tm; -+ if (parse_log_ts(ts, &tm) != 0) { free(copy); continue; } -+ time_t entry = mktime(&tm); -+ if (cutoff && entry < cutoff) { free(copy); continue; } -+ total += secs; count++; -+ extract_tags(cmt, secs, &tags); -+ free(copy); -+ } -+ free_lines(lines, n); -+ char tot[32]; fmt_long(total, tot, sizeof(tot)); -+ printf("\033[1m== timer stats: %s ==\033[0m\n", period); -+ printf("entries: %ld total: %s\n\n", count, tot); -+ int tag_n = 0; -+ for (TagAcc *a = tags; a; a = a->next) tag_n++; -+ if (tag_n > 0) { -+ TagAcc **arr = malloc(tag_n * sizeof(*arr)); -+ int i = 0; -+ for (TagAcc *a = tags; a; a = a->next) arr[i++] = a; -+ for (int x = 0; x < tag_n - 1; x++) -+ for (int y = x + 1; y < tag_n; y++) -+ if (arr[y]->secs > arr[x]->secs) { -+ TagAcc *t = arr[x]; arr[x] = arr[y]; arr[y] = t; -+ } -+ printf("%-24s %-11s %5s\n", "TAG", "DURATION", "COUNT"); -+ for (int z = 0; z < tag_n; z++) { -+ char d[32]; fmt_long(arr[z]->secs, d, sizeof(d)); -+ printf("%-24s %-11s %5ld\n", arr[z]->tag, d, arr[z]->count); -+ } -+ free(arr); -+ } -+ while (tags) { TagAcc *next = tags->next; free(tags); tags = next; } -+ return 0; -+} -+ - /* ===== usage ===== - * Patches MUST update this in lockstep with new flags so -h always - * reflects the build. -@@ -238,9 +601,12 @@ - "usage:\n" - " timer [FLAGS] DURATION countdown\n" - " timer [FLAGS] stopwatch\n" -+" timer -r [-n N] [--raw] read log\n" -+" timer --stats [today|week|month|all] stats (default: today)\n" - "\n" - "DURATION: 25m | 90s | 2h | 1h30m | 01:30:00 | 25:00\n" - "FLAGS:\n" -+" -c TEXT comment for log (+tag tokens are extracted)\n" - " -q no sound\n" - " -s silent\n" - " -- end of options\n" -@@ -378,6 +744,7 @@ - } - - (void)cfg; -+ write_log(elapsed_s, cfg->comment); - return user_quit ? 130 : 0; - } - -@@ -386,6 +753,13 @@ - int - main(int argc, char **argv) - { -+ migrate_legacy_dir(); -+ -+ if (argc >= 2 && !strcmp(argv[1], "-r")) -+ return cmd_read(argc - 2, argv + 2); -+ if (argc >= 2 && !strcmp(argv[1], "--stats")) -+ return cmd_stats(argc - 2, argv + 2); -+ - Config cfg = {0}; - - /* Pre-strip long-form flags before getopt. Stop at "--". */ -@@ -404,10 +778,11 @@ - new_argv[new_argc] = NULL; - - int opt; -- while ((opt = getopt(new_argc, new_argv, "qsh")) != -1) { -+ while ((opt = getopt(new_argc, new_argv, "qsc:h")) != -1) { - switch (opt) { - case 'q': cfg.quiet = 1; break; - case 's': cfg.silent = 1; break; -+ case 'c': snprintf(cfg.comment, sizeof(cfg.comment), "%s", optarg); break; - case 'h': usage(stdout); free(new_argv); return 0; - default: usage(stderr); free(new_argv); return 1; - } |
