diff options
Diffstat (limited to 'timer.c')
-rw-r--r-- | timer.c | 226 |
1 files changed, 226 insertions, 0 deletions
@@ -0,0 +1,226 @@ +// SPDX-FileCopyrightText: 2014-2017, 2025 Tomasz Kramkowski <tomasz@kramkow.ski> +// SPDX-License-Identifier: GPL-3.0-or-later + +#include <assert.h> +#include <errno.h> +#include <limits.h> +#include <signal.h> +#include <stdarg.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <stdnoreturn.h> +#include <string.h> +#include <sys/ioctl.h> +#include <time.h> +#include <unistd.h> + +static const unsigned long long SEC_SECOND = 1, SEC_MINUTE = 60, SEC_HOUR = 3600, + SEC_DAY = 86400, SEC_MONTH = 2629800, + SEC_YEAR = 31557600; +static const unsigned long INTERVAL_NSEC = 1000000000 / 4; + +volatile unsigned short term_width = 0; + +__attribute__((format(printf, 1, 2))) static noreturn void fatal(const char *fmt, ...) +{ + va_list ap; + int errno_save = errno; + + assert(fmt != NULL); + + fprintf(stderr, "fatal: "); + + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + + if (fmt[0] != '\0' && fmt[strlen(fmt) - 1] == ':') { + fprintf(stderr, " %s", strerror(errno_save)); + } + + fputc('\n', stderr); + + exit(EXIT_FAILURE); +} + +volatile sig_atomic_t got_sigwinch = 0; + +static void sigwinch(int sig) +{ + if (sig != SIGWINCH) return; + + got_sigwinch = 1; +} + +static void updatewidth(bool force) +{ + struct winsize ws; + + if (!got_sigwinch && !force) return; + + if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == -1) + fatal("Unable to get terminal width:"); + + term_width = ws.ws_col; + + got_sigwinch = 0; +} + +static void clear_line(void) +{ + fputs("\r\x1b[K", stdout); + + if (fflush(stdout) != 0) fatal("Unable to flush stdout:"); +} + +static void +format_dmy(char *dest, size_t size, const char *name, unsigned long long count) +{ + if (count == 1) + snprintf(dest, size, "%llu %s, ", count, name); + else if (count > 1) + snprintf(dest, size, "%llu %ss, ", count, name); + else + dest[0] = '\0'; +} + +static void print_time(unsigned long long total_sec, bool show_colon) +{ + unsigned long long count, hours, minutes, seconds; + char colon = show_colon ? ':' : ' ', syears[128], smonths[16], sdays[16]; + + count = total_sec / SEC_YEAR; + total_sec %= SEC_YEAR; + format_dmy(syears, sizeof syears, "year", count); + + count = total_sec / SEC_MONTH; + total_sec %= SEC_MONTH; + format_dmy(smonths, sizeof smonths, "month", count); + + count = total_sec / SEC_DAY; + total_sec %= SEC_DAY; + format_dmy(sdays, sizeof sdays, "day", count); + + hours = total_sec / SEC_HOUR; + total_sec %= SEC_HOUR; + + minutes = total_sec / SEC_MINUTE; + total_sec %= SEC_MINUTE; + + seconds = total_sec / SEC_SECOND; + total_sec %= SEC_SECOND; + + if (total_sec != 0) fatal("Unable to format time"); + + printf(" %s%s%s%.2llu%c%.2llu%c%.2llu\r", syears, smonths, sdays, hours, + colon, minutes, colon, seconds); + + if (fflush(stdout) != 0) fatal("Unable to flush stdout:"); +} + +static unsigned long long get_seconds(char *code) +{ + unsigned long long val, mul; + size_t len; + char *end; + + len = strlen(code); + + if (len < 2) return 0; + + switch (code[len - 1]) { + case 's': mul = SEC_SECOND; break; + case 'm': mul = SEC_MINUTE; break; + case 'h': mul = SEC_HOUR; break; + case 'D': mul = SEC_DAY; break; + case 'M': mul = SEC_MONTH; break; + case 'Y': mul = SEC_YEAR; break; + default: return 0; + } + + errno = 0; + val = strtoul(code, &end, 10); + if (val == ULONG_MAX && errno != 0) + fatal("Unable to convert time specifier %s:", code); + if (strlen(end) != 1) fatal("Invalid time specifier %s", code); + + // might wrap + return val * mul; +} + +int main(int argc, char **argv) +{ + int sig; + sigset_t sigset; + struct itimerspec its = {{0, INTERVAL_NSEC}, {0, INTERVAL_NSEC}}; + struct sigaction sigact = {.sa_handler = sigwinch}; + timer_t timerid; + struct sigevent sev = { + .sigev_notify = SIGEV_SIGNAL, + .sigev_signo = SIGRTMIN, + .sigev_value.sival_ptr = &timerid + }; + unsigned long long total_seconds = 0; + + if (!isatty(STDOUT_FILENO)) fatal("stdout is not a tty"); + + if (argc < 2) + fatal("Not enough arguments.\nUsage:\n" + "\t%s <n>{s,m,h,D,M,Y} ...", + argv[0]); + + for (int i = 1; i < argc; i++) total_seconds += get_seconds(argv[i]); + + if (sigaction(SIGWINCH, &sigact, NULL) != 0) + fatal("Unable to set SIGWINCH signal action:"); + + if (timer_create(CLOCK_MONOTONIC, &sev, &timerid) != 0) + fatal("Unable to create timer:"); + + if (sigemptyset(&sigset) != 0) fatal("Unable to empty signal set:"); + + if (sigaddset(&sigset, SIGRTMIN) != 0) + fatal("Unable to add SIGRTMIN to signal set:"); + + if (sigprocmask(SIG_BLOCK, &sigset, NULL) != 0) + fatal("Unable to block SIGRTMIN:"); + + if (timer_settime(timerid, 0, &its, NULL) != 0) + fatal("Unable to set timer:"); + + updatewidth(true); + + for (unsigned long long i = 0; i < total_seconds; i++) + for (int ii = 0; ii < 4; ii++) { + if (sigwait(&sigset, &sig), sig != SIGRTMIN) + fatal("sigwait returned unexpected signal %d", sig); + + updatewidth(false); + clear_line(); + print_time(total_seconds - i, ii < 2); + } + + if (sigaddset(&sigset, SIGINT) != 0) + fatal("Unable to add SIGINT to signal set:"); + + while (true) { + static bool blink = false; + + blink = !blink; + sigwait(&sigset, &sig); + if (sig != SIGRTMIN) break; + + clear_line(); + + if (blink) printf(" -- BEEP --\a\r"); + + if (fflush(stdout) != 0) fatal("Unable to flush stdout:"); + } + + timer_delete(timerid); + + clear_line(); + + return 0; +} |