aboutsummaryrefslogtreecommitdiff
path: root/patches/ttym-logging-2.0.diff
diff options
context:
space:
mode:
Diffstat (limited to 'patches/ttym-logging-2.0.diff')
-rw-r--r--patches/ttym-logging-2.0.diff447
1 files changed, 447 insertions, 0 deletions
diff --git a/patches/ttym-logging-2.0.diff b/patches/ttym-logging-2.0.diff
new file mode 100644
index 0000000..d654d50
--- /dev/null
+++ b/patches/ttym-logging-2.0.diff
@@ -0,0 +1,447 @@
+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;
+ }