/* ttym - countdown / stopwatch in a single C file. * See LICENSE file for copyright and license details. */ #define _POSIX_C_SOURCE 200809L #include #include #include #include #include #include #include #include #include #include #include #include #include #include "config.h" /* ===== types & globals ===== */ typedef struct { int quiet; /* -q: suppress completion bell */ int silent; /* -s: no bell, no extras */ } Config; static struct termios oldtios; static int ttyfd = -1; static int rawset = 0; static volatile sig_atomic_t interrupted = 0; /* exposed for patches that read the final elapsed seconds at exit */ static long elapsed_s = 0; /* nonzero once stdout is confirmed a tty; gates cursor/redraw escapes */ static int out_tty = 0; /* ===== helpers ===== */ static void die(const char *fmt, ...) { va_list ap; va_start(ap, fmt); vfprintf(stderr, fmt, ap); va_end(ap); fputc('\n', stderr); exit(1); } static void sleep_ms(int ms) { struct timespec ts = { ms / 1000, (long)(ms % 1000) * 1000000L }; nanosleep(&ts, NULL); } static long now_ms(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return ts.tv_sec * 1000L + ts.tv_nsec / 1000000L; } static int term_cols(void) { struct winsize ws; if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_col > 0) return ws.ws_col; return 80; } static void on_signal(int sig) { (void)sig; interrupted = 1; } static void restore_tty(void) { if (rawset && ttyfd >= 0) tcsetattr(ttyfd, TCSANOW, &oldtios); rawset = 0; if (out_tty) fputs("\033[?7h\033[?25h", stdout); /* re-enable wrap, show cursor */ fputc('\n', stdout); fflush(stdout); } /* ===== duration parsing ===== * accepts: 25m | 90s | 2h | 1h30m | 01:30:00 | 25:00 */ static int parse_uint(const char *s, size_t n, long *out) { long v = 0; if (n == 0) return -1; for (size_t i = 0; i < n; i++) { if (!isdigit((unsigned char)s[i])) return -1; v = v * 10 + (s[i] - '0'); if (v > 1000000) return -1; } *out = v; return 0; } static long parse_duration(const char *s) { size_t len = strlen(s); if (len == 0) return -1; if (strchr(s, ':')) { /* Split on ':' into 2 or 3 fields, then parse each with * parse_uint. That rejects empty fields, signs and * whitespace, and bounds every component -- so unlike a * raw sscanf("%ld") this cannot overflow on huge input. */ const char *fld[3]; size_t flen[3]; int nf = 0; size_t fstart = 0; for (size_t i = 0; i <= len; i++) { if (i != len && s[i] != ':') continue; if (nf == 3) return -1; fld[nf] = s + fstart; flen[nf] = i - fstart; nf++; fstart = i + 1; } long v[3] = { 0, 0, 0 }; for (int k = 0; k < nf; k++) if (parse_uint(fld[k], flen[k], &v[k]) < 0) return -1; long total; if (nf == 3) { if (v[1] >= 60 || v[2] >= 60) return -1; total = v[0] * 3600 + v[1] * 60 + v[2]; } else if (nf == 2) { if (v[1] >= 60) return -1; total = v[0] * 60 + v[1]; } else { return -1; } return total > 0 ? total : -1; } long total = 0; size_t i = 0, start = 0; int have_any = 0; while (i < len) { if (isdigit((unsigned char)s[i])) { i++; continue; } if (s[i] != 'h' && s[i] != 'm' && s[i] != 's') return -1; long v; if (parse_uint(s + start, i - start, &v) < 0) return -1; switch (s[i]) { case 'h': total += v * 3600; break; case 'm': total += v * 60; break; case 's': total += v; break; } have_any = 1; i++; start = i; } if (!have_any || start != len) return -1; return total > 0 ? total : -1; } /* ===== formatting ===== */ static void fmt_clock(long s, char *buf, size_t bufsz) { long h = s / 3600; s %= 3600; long m = s / 60; long sec = s % 60; if (h > 0) snprintf(buf, bufsz, "%02ld:%02ld:%02ld", h, m, sec); else snprintf(buf, bufsz, "%02ld:%02ld", m, sec); } /* ===== drawing ===== */ static void draw_countdown(long total, long elapsed_ms, int paused, int barw) { if (total <= 0) return; long total_ms = total * 1000L; if (elapsed_ms < 0) elapsed_ms = 0; if (elapsed_ms > total_ms) elapsed_ms = total_ms; long es = elapsed_ms / 1000; long remaining = total - es; long pct = (elapsed_ms * 100) / total_ms; int fill = (int)((long)barw * elapsed_ms / total_ms); if (fill > barw) fill = barw; int empty = barw - fill; char rem[24], el[24]; fmt_clock(remaining, rem, sizeof(rem)); fmt_clock(es, el, sizeof(el)); const char *color = paused ? "\033[36m" : "\033[33m"; const char *status = paused ? "PAUSED " : " "; printf("\r%sT-%s\033[0m \033[2m+%s\033[0m [", color, rem, el); for (int i = 0; i < fill; i++) fputs(BAR_FILL, stdout); for (int i = 0; i < empty; i++) fputs(BAR_EMPTY, stdout); printf("] %3ld%% %s", pct, status); fflush(stdout); } static void draw_stopwatch(long elapsed_ms, int paused) { long es = elapsed_ms / 1000; char el[24]; fmt_clock(es, el, sizeof(el)); const char *color = paused ? "\033[36m" : "\033[32m"; const char *status = paused ? "PAUSED " : " "; printf("\r%sT+%s\033[0m %s ", color, el, status); fflush(stdout); } /* ===== usage ===== * Patches MUST update this in lockstep with new flags so -h always * reflects the build. */ static void usage(FILE *f) { fprintf(f, "usage:\n" " timer [FLAGS] DURATION countdown\n" " timer [FLAGS] stopwatch\n" "\n" "DURATION: 25m | 90s | 2h | 1h30m | 01:30:00 | 25:00\n" "FLAGS:\n" " -q no sound\n" " -s silent\n" " -- end of options\n" " -h help\n" "\n" "controls: [space] pause/resume [q | Ctrl+C] quit\n"); } /* ===== run loop ===== */ static int run(long total, int stopwatch, const Config *cfg) { out_tty = isatty(STDOUT_FILENO); int cols = term_cols(); int barw = cols - 40; if (barw < 10) barw = 10; ttyfd = open("/dev/tty", O_RDONLY); if (ttyfd >= 0 && isatty(ttyfd)) { if (tcgetattr(ttyfd, &oldtios) == 0) { struct termios raw = oldtios; raw.c_lflag &= ~(ICANON | ECHO); raw.c_cc[VMIN] = 0; raw.c_cc[VTIME] = 0; if (tcsetattr(ttyfd, TCSANOW, &raw) == 0) rawset = 1; } } atexit(restore_tty); struct sigaction sa = {0}; sa.sa_handler = on_signal; sigaction(SIGINT, &sa, NULL); sigaction(SIGTERM, &sa, NULL); if (out_tty) fputs("\033[?25l\033[?7l", stdout); long start_ms = now_ms(); long pause_accum = 0; long pause_start = 0; int paused = 0; int user_quit = 0; for (;;) { if (interrupted) { user_quit = 1; break; } long now = now_ms(); long elapsed_ms = paused ? pause_start - start_ms - pause_accum : now - start_ms - pause_accum; if (elapsed_ms < 0) elapsed_ms = 0; if (stopwatch) { if (out_tty) draw_stopwatch(elapsed_ms, paused); } else { if (out_tty) draw_countdown(total, elapsed_ms, paused, barw); if (!paused && elapsed_ms >= total * 1000L) break; } long frame_end = now_ms() + 100; for (;;) { if (interrupted) break; long rem_frame = frame_end - now_ms(); if (rem_frame <= 0) break; if (ttyfd < 0) { sleep_ms((int)rem_frame); break; } struct pollfd pfd = { .fd = ttyfd, .events = POLLIN }; int pr = poll(&pfd, 1, (int)rem_frame); if (pr > 0 && (pfd.revents & POLLIN)) { char c; ssize_t n = read(ttyfd, &c, 1); if (n == 1) { if (c == ' ') { if (paused) { pause_accum += now_ms() - pause_start; paused = 0; } else { pause_start = now_ms(); paused = 1; } } else if (c == 'q' || c == 'Q') { user_quit = 1; goto done; } } } else if (pr == 0) { break; } else { sleep_ms((int)rem_frame); break; } } } done: { long final_now = now_ms(); long final_ms = paused ? pause_start - start_ms - pause_accum : final_now - start_ms - pause_accum; if (final_ms < 0) final_ms = 0; elapsed_s = final_ms / 1000; } if (stopwatch) { char el[24]; fmt_clock(elapsed_s, el, sizeof(el)); if (out_tty) printf("\r\033[32mSTOP %s%*s\033[0m\n", el, 30, ""); else printf("STOP %s\n", el); } else if (user_quit) { char el[24]; fmt_clock(elapsed_s, el, sizeof(el)); if (out_tty) printf("\r\033[33mQUIT at %s%*s\033[0m\n", el, 30, ""); else printf("QUIT at %s\n", el); } else { char tb[24]; fmt_clock(total, tb, sizeof(tb)); if (out_tty) printf("\r\033[32mDONE %s 100%%%*s\033[0m\n", tb, 20, ""); else printf("DONE %s 100%%\n", tb); if (!cfg->silent && !cfg->quiet && out_tty) { fputc('\a', stdout); fflush(stdout); sleep_ms(50); } } (void)cfg; return user_quit ? 130 : 0; } /* ===== main ===== */ int main(int argc, char **argv) { Config cfg = {0}; /* Pre-strip long-form flags before getopt. Stop at "--". */ int new_argc = 0; char **new_argv = malloc(sizeof(char *) * (argc + 1)); if (!new_argv) die("oom"); int past_dd = 0; for (int i = 0; i < argc; i++) { if (!past_dd && !strcmp(argv[i], "--")) { past_dd = 1; new_argv[new_argc++] = argv[i]; continue; } new_argv[new_argc++] = argv[i]; } new_argv[new_argc] = NULL; int opt; while ((opt = getopt(new_argc, new_argv, "qsh")) != -1) { switch (opt) { case 'q': cfg.quiet = 1; break; case 's': cfg.silent = 1; break; case 'h': usage(stdout); free(new_argv); return 0; default: usage(stderr); free(new_argv); return 1; } } int rem_argc = new_argc - optind; char **rem_argv = new_argv + optind; int stopwatch = (rem_argc < 1); long total = 0; if (!stopwatch) { total = parse_duration(rem_argv[0]); if (total < 0) { fprintf(stderr, "bad duration: %s\n", rem_argv[0]); free(new_argv); return 1; } } int rc = run(total, stopwatch, &cfg); free(new_argv); return rc; }