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, 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;
- }