File: countdown.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright © 2026 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 To compile a smaller-sized command-line app, you can use the `go` command as
  27 follows:
  28 
  29 go build -ldflags "-s -w" -trimpath countdown.go
  30 */
  31 
  32 package main
  33 
  34 import (
  35     "os"
  36     "os/signal"
  37     "strconv"
  38     "time"
  39 )
  40 
  41 // Note: the code is avoiding using the fmt package to save hundreds of
  42 // kilobytes on the resulting executable, which is a noticeable difference.
  43 
  44 const (
  45     spaces = `                `
  46 
  47     // clear has enough spaces in it to cover any chronograph output
  48     clear = "\r" + spaces + spaces + spaces + "\r"
  49 )
  50 
  51 const info = `
  52 countdown [period]
  53 
  54 Run a live countdown timer on stderr, until the time-period given ends,
  55 or the app is force-quit.
  56 
  57 The time-period is either a simple integer number (of seconds), or an
  58 integer followed by any of
  59   - "s" (for seconds)
  60   - "m" (for minutes)
  61   - "h" (for hours)
  62 without spaces, or a combination of those time-units without spaces.
  63 `
  64 
  65 func main() {
  66     if len(os.Args) == 1 {
  67         os.Stderr.WriteString(info[1:])
  68         return
  69     }
  70 
  71     if len(os.Args) != 2 {
  72         os.Stderr.WriteString(info[1:])
  73         os.Exit(1)
  74     }
  75 
  76     switch os.Args[1] {
  77     case `-h`, `--h`, `-help`, `--help`:
  78         os.Stdout.WriteString(info[1:])
  79         return
  80     }
  81 
  82     period, err := parseDuration(os.Args[1])
  83     if err != nil {
  84         os.Stderr.WriteString(err.Error())
  85         os.Stderr.WriteString("\n")
  86         os.Exit(1)
  87     }
  88 
  89     os.Stderr.WriteString("Countdown lasting ")
  90     os.Stderr.WriteString(time.Time{}.Add(period).Format(`15:04:05`))
  91     os.Stderr.WriteString(" started\n")
  92     countdown(period)
  93 }
  94 
  95 func parseDuration(s string) (time.Duration, error) {
  96     if n, err := strconv.Atoi(s); err == nil {
  97         return time.Duration(n) * time.Second, err
  98     }
  99     return time.ParseDuration(s)
 100 }
 101 
 102 func countdown(period time.Duration) {
 103     start := time.Now()
 104     end := start.Add(period)
 105     t := time.NewTicker(100 * time.Millisecond)
 106     startChronoLine(end, start)
 107 
 108     stopped := make(chan os.Signal, 1)
 109     defer close(stopped)
 110     signal.Notify(stopped, os.Interrupt)
 111 
 112     for {
 113         select {
 114         case now := <-t.C:
 115             if now.Sub(end) < 0 {
 116                 startChronoLine(end, now)
 117                 continue
 118             }
 119 
 120             t.Stop()
 121             startChronoLine(now, now)
 122             endChronoLine(start)
 123             return
 124 
 125         case <-stopped:
 126             t.Stop()
 127             endChronoLine(start)
 128             return
 129         }
 130     }
 131 }
 132 
 133 func startChronoLine(end, now time.Time) {
 134     var buf [128]byte
 135     dt := end.Sub(now)
 136 
 137     s := buf[:0]
 138     s = append(s, clear...)
 139     s = time.Time{}.Add(dt).AppendFormat(s, `15:04:05.0`)
 140     s = append(s, `    `...)
 141     s = now.AppendFormat(s, `2006-01-02 15:04:05 Jan Mon`)
 142     os.Stderr.Write(s)
 143 }
 144 
 145 func endChronoLine(start time.Time) {
 146     var buf [64]byte
 147     secs := time.Since(start).Seconds()
 148 
 149     s := buf[:0]
 150     s = append(s, `    `...)
 151     s = strconv.AppendFloat(s, secs, 'f', 4, 64)
 152     s = append(s, " seconds\n"...)
 153     os.Stderr.Write(s)
 154 }