diff options
Diffstat (limited to 'ttym.c')
| -rw-r--r-- | ttym.c | 253 |
1 files changed, 183 insertions, 70 deletions
@@ -5,12 +5,13 @@ #include <ctype.h> #include <errno.h> #include <fcntl.h> +#include <locale.h> #include <poll.h> #include <signal.h> -#include <stdarg.h> #include <stdio.h> #include <stdlib.h> #include <string.h> +#include <strings.h> #include <sys/ioctl.h> #include <termios.h> #include <time.h> @@ -23,6 +24,9 @@ typedef struct { int quiet; /* -q: suppress completion bell */ int silent; /* -s: no bell, no extras */ + int alert_persist; /* alert loop until keypress; --no-persist disables */ + int flash; /* reverse-video flash during alert loop */ + char bar_style[32]; /* --bar STYLE */ } Config; static struct termios oldtios; @@ -39,17 +43,6 @@ 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 }; @@ -73,6 +66,13 @@ term_cols(void) return 80; } +static long +elapsed(long start_ms, long pause_accum, long pause_start, int paused) +{ + long ms = (paused ? pause_start : now_ms()) - start_ms - pause_accum; + return ms < 0 ? 0 : ms; +} + static void on_signal(int sig) { @@ -80,16 +80,23 @@ on_signal(int sig) interrupted = 1; } +static int +parse_bool_str(const char *v) +{ + return !strcasecmp(v, "true") || !strcasecmp(v, "yes") || + !strcmp(v, "1") || !strcasecmp(v, "on"); +} + 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); + if (out_tty) { + fputs("\033[?5l\033[?7h\033[?25h", stdout); /* clear reverse, re-enable wrap, show cursor */ + fflush(stdout); + } } /* ===== duration parsing ===== @@ -142,18 +149,15 @@ parse_duration(const char *s) 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) { + } else { 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; @@ -164,11 +168,10 @@ parse_duration(const char *s) case 'm': total += v * 60; break; case 's': total += v; break; } - have_any = 1; i++; start = i; } - if (!have_any || start != len) return -1; + if (start != len) return -1; return total > 0 ? total : -1; } @@ -186,7 +189,8 @@ fmt_clock(long s, char *buf, size_t bufsz) /* ===== drawing ===== */ static void -draw_countdown(long total, long elapsed_ms, int paused, int barw) +draw_countdown(long total, long elapsed_ms, int paused, int barw, + const char *fill_glyph, const char *empty_glyph, int minimal) { if (total <= 0) return; long total_ms = total * 1000L; @@ -207,8 +211,15 @@ draw_countdown(long total, long elapsed_ms, int paused, int barw) 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); + if (minimal) { + fputs("\033[7m", stdout); + for (int i = 0; i < fill; i++) fputc(' ', stdout); + fputs("\033[27m", stdout); + for (int i = 0; i < empty; i++) fputc(' ', stdout); + } else { + for (int i = 0; i < fill; i++) fputs(fill_glyph, stdout); + for (int i = 0; i < empty; i++) fputs(empty_glyph, stdout); + } printf("] %3ld%% %s", pct, status); fflush(stdout); } @@ -221,11 +232,104 @@ draw_stopwatch(long elapsed_ms, int paused) 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); + printf("\r%sT+%s\033[0m %s\033[K", color, el, status); + fflush(stdout); +} + +/* ===== alert loop ===== + * Bell + poll for keypress, ~1.5s per beat; 4-min safety cap. + */ + +static int +is_foreground(void) +{ + if (!isatty(STDIN_FILENO) || !isatty(STDOUT_FILENO)) return 0; + pid_t pgrp = tcgetpgrp(STDIN_FILENO); + if (pgrp < 0) return 0; + return pgrp == getpgrp(); +} + +static void +alert_loop(int flash) +{ + fputs("\033[?25h\033[?7h", stdout); + int ticks = 0; + while (!interrupted) { + fputc('\a', stdout); + if (flash) fputs("\033[?5h", stdout); + fflush(stdout); + sleep_ms(150); + if (flash) fputs("\033[?5l", stdout); + fflush(stdout); + + int acked = 0; + for (int i = 0; i < 15 && !interrupted; i++) { + if (ttyfd < 0) { sleep_ms(100); continue; } + struct pollfd pfd = { .fd = ttyfd, .events = POLLIN }; + int pr = poll(&pfd, 1, 100); + if (pr > 0 && (pfd.revents & POLLIN)) { + char ch; + ssize_t rd = read(ttyfd, &ch, 1); + if (rd >= 0) { acked = 1; break; } + } else if (pr < 0) { + sleep_ms(100); + } + } + if (acked) break; + if (++ticks > 160) break; + } fflush(stdout); } +/* ===== bar styles ===== */ + +typedef struct { const char *name, *fill, *empty; } BarStyle; + +static const BarStyle bar_styles[] = { + { "unicode", "\xe2\x96\x88", "\xe2\x96\x91" }, /* U+2588 U+2591 */ + { "ascii", "=", "-" }, + { "hash", "#", "." }, + { "dots", "\xe2\x80\xa2", "\xc2\xb7" }, /* U+2022 U+00B7 */ + { "line", "\xe2\x94\x81", "\xe2\x94\x80" }, /* U+2501 U+2500 */ + { "block", "\xe2\x96\x93", "\xe2\x96\x91" }, /* U+2593 U+2591 */ + { "arrow", "\xe2\x96\xb6", "\xe2\x96\xb7" }, /* U+25B6 U+25B7 */ + { "minimal", "", "" }, + { NULL, NULL, NULL } +}; + +static const BarStyle * +find_bar_style(const char *name) +{ + if (!name || !*name) return NULL; + for (int i = 0; bar_styles[i].name; i++) + if (!strcmp(bar_styles[i].name, name)) return &bar_styles[i]; + return NULL; +} + +static int +is_utf8(void) +{ + const char *l = setlocale(LC_CTYPE, ""); + if (!l) return 0; + return strstr(l, "UTF-8") || strstr(l, "utf8") || strstr(l, "utf-8"); +} + +static void +resolve_bar(const Config *c, const char **fill, const char **empty, int *minimal) +{ + *minimal = 0; + const char *name = (c->bar_style[0]) ? c->bar_style : BAR_STYLE; + const BarStyle *s = find_bar_style(name); + if (!s) s = find_bar_style(is_utf8() ? "unicode" : "ascii"); + if (s && !strcmp(s->name, "minimal")) { + *minimal = 1; + *fill = " "; *empty = " "; + return; + } + *fill = s ? s->fill : BAR_FILL; + *empty = s ? s->empty : BAR_EMPTY; +} + /* ===== usage ===== * Patches MUST update this in lockstep with new flags so -h always * reflects the build. @@ -243,6 +347,9 @@ usage(FILE *f) "FLAGS:\n" " -q no sound\n" " -s silent\n" +" --no-persist skip alert-until-keypress loop\n" +" --flash on|off terminal flash during alert loop\n" +" --bar STYLE bar style: unicode|ascii|hash|dots|line|block|arrow|minimal\n" " -- end of options\n" " -h help\n" "\n" @@ -259,6 +366,10 @@ run(long total, int stopwatch, const Config *cfg) int barw = cols - 40; if (barw < 10) barw = 10; + const char *fill_glyph, *empty_glyph; + int minimal = 0; + resolve_bar(cfg, &fill_glyph, &empty_glyph, &minimal); + ttyfd = open("/dev/tty", O_RDONLY); if (ttyfd >= 0 && isatty(ttyfd)) { if (tcgetattr(ttyfd, &oldtios) == 0) { @@ -276,6 +387,8 @@ run(long total, int stopwatch, const Config *cfg) sa.sa_handler = on_signal; sigaction(SIGINT, &sa, NULL); sigaction(SIGTERM, &sa, NULL); + sigaction(SIGQUIT, &sa, NULL); + sigaction(SIGHUP, &sa, NULL); if (out_tty) fputs("\033[?25l\033[?7l", stdout); @@ -289,18 +402,15 @@ run(long total, int stopwatch, const Config *cfg) 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; + long elapsed_ms = elapsed(start_ms, pause_accum, pause_start, paused); if (stopwatch) { if (out_tty) draw_stopwatch(elapsed_ms, paused); } else { if (out_tty) - draw_countdown(total, elapsed_ms, paused, barw); + draw_countdown(total, elapsed_ms, paused, barw, + fill_glyph, empty_glyph, minimal); if (!paused && elapsed_ms >= total * 1000L) break; } @@ -318,19 +428,17 @@ run(long total, int stopwatch, const Config *cfg) 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; - } + long t = now_ms(); + if (paused) + pause_accum += t - pause_start; + pause_start = t; + paused = !paused; } else if (c == 'q' || c == 'Q') { user_quit = 1; goto done; } } - } else if (pr == 0) { + } else if (pr == 0 || (pr < 0 && errno == EINTR)) { break; } else { sleep_ms((int)rem_frame); @@ -340,34 +448,27 @@ run(long total, int stopwatch, const Config *cfg) } 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; - } + elapsed_s = elapsed(start_ms, pause_accum, pause_start, paused) / 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, ""); + printf("\r\033[32mSTOP %s\033[0m\033[K\n", el); 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, ""); + printf("\r\033[33mQUIT at %s\033[0m\033[K\n", el); 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, ""); + printf("\r\033[32mDONE %s 100%%\033[0m\033[K\n", tb); else printf("DONE %s 100%%\n", tb); if (!cfg->silent && !cfg->quiet && out_tty) { @@ -375,9 +476,12 @@ done: fflush(stdout); sleep_ms(50); } + if (!cfg->silent && cfg->alert_persist && is_foreground()) { + interrupted = 0; + alert_loop(cfg->flash); + } } - (void)cfg; return user_quit ? 130 : 0; } @@ -387,33 +491,45 @@ int main(int argc, char **argv) { Config cfg = {0}; + cfg.alert_persist = ALERT_PERSIST; + cfg.flash = FLASH; /* 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; + int nargc = 0, past_dd = 0; for (int i = 0; i < argc; i++) { - if (!past_dd && !strcmp(argv[i], "--")) { + if (!past_dd && !strcmp(argv[i], "--")) past_dd = 1; - new_argv[new_argc++] = argv[i]; + if (!past_dd && !strcmp(argv[i], "--no-persist")) { + cfg.alert_persist = 0; + continue; + } + if (!past_dd && !strncmp(argv[i], "--flash=", 8)) { + cfg.flash = parse_bool_str(argv[i] + 8); + continue; + } + if (!past_dd && !strcmp(argv[i], "--flash") && i + 1 < argc) { + cfg.flash = parse_bool_str(argv[++i]); + continue; + } + if (!past_dd && !strcmp(argv[i], "--bar") && i + 1 < argc) { + snprintf(cfg.bar_style, sizeof(cfg.bar_style), "%s", argv[++i]); continue; } - new_argv[new_argc++] = argv[i]; + argv[nargc++] = argv[i]; } - new_argv[new_argc] = NULL; + argv[nargc] = NULL; int opt; - while ((opt = getopt(new_argc, new_argv, "qsh")) != -1) { + while ((opt = getopt(nargc, 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; + case 'h': usage(stdout); return 0; + default: usage(stderr); return 1; } } - int rem_argc = new_argc - optind; - char **rem_argv = new_argv + optind; + int rem_argc = nargc - optind; + char **rem_argv = argv + optind; int stopwatch = (rem_argc < 1); long total = 0; @@ -421,12 +537,9 @@ main(int argc, char **argv) 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; + return run(total, stopwatch, &cfg); } |
