File: countdown.c
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright © 2020-2025 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the “Software”), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 /*
  26 You can build this command-line app by running
  27 
  28 cc -Wall -s -O3 -flto -o ./countdown ./countdown.c
  29 */
  30 
  31 #include <stdbool.h>
  32 #include <stddef.h>
  33 #include <stdint.h>
  34 #include <stdio.h>
  35 #include <stdlib.h>
  36 #include <string.h>
  37 #include <time.h>
  38 #include <unistd.h>
  39 
  40 #ifdef _WIN32
  41 #include <fcntl.h>
  42 #include <windows.h>
  43 #endif
  44 
  45 #ifdef RED_ERRORS
  46 #define ERROR_STYLE "\x1b[38;2;204;0;0m"
  47 #ifdef __APPLE__
  48 #define ERROR_STYLE "\x1b[31m"
  49 #endif
  50 #define RESET_STYLE "\x1b[0m"
  51 #else
  52 #define ERROR_STYLE
  53 #define RESET_STYLE
  54 #endif
  55 
  56 #define ERROR_LINE(MSG) (ERROR_STYLE MSG RESET_STYLE "\n")
  57 
  58 const char* info = ""
  59     "countdown [options...] [seconds/duration...]\n"
  60     "\n"
  61     "\n"
  62     "Count-down the number of seconds given, or 60 seconds by default.\n"
  63     "\n"
  64     "The duration can be either a number (of seconds) or a string with no\n"
  65     "spaces and numbered units `h`, `m`, `s`, which can be mixed together.\n"
  66     "\n"
  67     "Decimal dots and negative numbers aren't allowed."
  68     "\n"
  69     "\n"
  70     "Options, all of which can start with either 1 or 2 dashes:\n"
  71     "\n"
  72     "\n"
  73     "  -h          show this help message\n"
  74     "  -help       show this help message\n"
  75     "\n"
  76     "\n"
  77     "Examples"
  78     "\n"
  79     "\n"
  80     "  countdown 33 # 33 seconds\n"
  81     "  countdown 3h45m # 3 hours and 45 minutes\n"
  82     "  countdown 3h45s # 3 hours and 45 seconds\n"
  83     "  countdown 3h4m5s # 3 hours 4 minutes and 5 seconds\n"
  84     "";
  85 
  86 void countdown(int seconds) {
  87     struct timespec now;
  88     clock_gettime(CLOCK_REALTIME, &now);
  89     const int64_t ns2sec = 1e9;
  90     const int64_t end = ns2sec * (now.tv_sec + seconds) + now.tv_nsec;
  91 
  92     struct timespec delay;
  93     delay.tv_sec = 0;
  94     delay.tv_nsec = 100 * 1e6;
  95 
  96     int64_t t = ns2sec * now.tv_sec + now.tv_nsec;
  97     while (t < end) {
  98         const int64_t diff = end - t;
  99         const int64_t round_up = (diff % ns2sec) > (ns2sec / 5);
 100         const int64_t left = diff / ns2sec + round_up;
 101         const int h = left / 3600;
 102         const int m = (left / 60) % 60;
 103         const int s = left % 60;
 104         fprintf(stderr, "\r%02d:%02d:%02d", h, m, s);
 105 
 106         nanosleep(&delay, NULL);
 107         clock_gettime(CLOCK_REALTIME, &now);
 108         t = ns2sec * now.tv_sec + now.tv_nsec;
 109     }
 110 
 111     fputs("\r        \r", stderr);
 112 }
 113 
 114 // is_help_option simplifies control-flow for func main
 115 bool is_help_option(const char* s) {
 116     return (s[0] == '-') && (
 117         strcmp(s, "-h") == 0 ||
 118         strcmp(s, "-help") == 0 ||
 119         strcmp(s, "--h") == 0 ||
 120         strcmp(s, "--help") == 0
 121     );
 122 }
 123 
 124 // bool parse_duration(char* s, int* seconds) {
 125 //     char* end;
 126 //     int v = strtol(argv[1], &end, 10);
 127 //     if (*end == 0 && argv[1] != end) {
 128 //         *seconds = v;
 129 //         return true;
 130 //     }
 131 //     return false;
 132 // }
 133 
 134 bool parse_colon_style_duration(char* s, int* seconds) {
 135     int accum = 0;
 136     int digits = 0;
 137 
 138     int parts[4] = {0, 0, 0, 0};
 139     const int n = sizeof(parts) / sizeof(parts[0]);
 140     const int multipliers[4] = {1, 60, 60 * 60, 24 * 60 * 60};
 141     int which = 0;
 142 
 143     for (; *s != 0; s++) {
 144         const int b = *s;
 145 
 146         if (b == ':') {
 147             if (digits < 1) {
 148                 return false;
 149             }
 150 
 151             parts[which] = accum;
 152             accum = 0;
 153             digits = 0;
 154 
 155             which++;
 156             if (which >= n) {
 157                 return false;
 158             }
 159 
 160             continue;
 161         }
 162 
 163         if ('0' <= b && b <= '9') {
 164             digits++;
 165             accum *= 10;
 166             accum += b - '0';
 167             continue;
 168         }
 169 
 170         return false;
 171     }
 172 
 173     if (digits < 1) {
 174         return false;
 175     }
 176 
 177     parts[which] = accum;
 178 
 179     int total = 0;
 180     for (int i = which, j = 0; i >= 0; i--, j++) {
 181         total += multipliers[j] * parts[i];
 182     }
 183     *seconds = total;
 184     return true;
 185 }
 186 
 187 bool parse_duration(char* s, int* seconds) {
 188     int total = 0;
 189     int accum = 0;
 190 
 191     if (parse_colon_style_duration(s, seconds)) {
 192         return true;
 193     }
 194 
 195     for (; *s != 0; s++) {
 196         const int b = *s;
 197 
 198         if ('0' <= b && b <= '9') {
 199             accum *= 10;
 200             accum += b - '0';
 201             continue;
 202         }
 203 
 204         switch (b) {
 205             case 'h':
 206                 accum *= 3600;
 207                 total += accum;
 208                 accum = 0;
 209                 break;
 210 
 211             case 'm':
 212                 accum *= 60;
 213                 total += accum;
 214                 accum = 0;
 215                 break;
 216 
 217             case 's':
 218                 total += accum;
 219                 accum = 0;
 220                 break;
 221 
 222             default:
 223                 return false;
 224         }
 225     }
 226 
 227     total += accum;
 228     *seconds = total;
 229     return true;
 230 }
 231 
 232 int main(int argc, char** argv) {
 233 #ifdef _WIN32
 234     setmode(fileno(stdin), O_BINARY);
 235     // ensure output lines end in LF instead of CRLF on windows
 236     setmode(fileno(stdout), O_BINARY);
 237     setmode(fileno(stderr), O_BINARY);
 238 #endif
 239 
 240     // handle any of the help options, if given
 241     if (argc > 1 && is_help_option(argv[1])) {
 242         printf("%s", info);
 243         return 0;
 244     }
 245 
 246     int seconds = 60;
 247     if (argc > 1) {
 248         if (!parse_duration(argv[1], &seconds)) {
 249             const char* fmt = ERROR_LINE("invalid duration '%s'");
 250             fprintf(stderr, fmt, argv[1]);
 251             return 1;
 252         }
 253     }
 254 
 255     if (seconds > 0) {
 256         countdown(seconds);
 257     }
 258 
 259     printf("done waiting for %d seconds\n", seconds);
 260     return 0;
 261 }