File: ./go.mod
   1 module timer
   2 
   3 go 1.18

     File: ./info.txt
   1 timer
   2 
   3 
   4 Run a live timer/chronograph on stderr, always showing below all lines
   5 from stdin, which update stdout as they come.
   6 
   7 When stdin is over, it simply quits showing how long it ran, by extending
   8 its last visible timer line on stderr.
   9 
  10 You can also use this app without piping anything to its stdin, acting as
  11 a simple live clock until you force-quit it.

     File: ./main.go
   1 package main
   2 
   3 import (
   4     "bufio"
   5     "io"
   6     "os"
   7     "os/signal"
   8     "strconv"
   9     "time"
  10 
  11     _ "embed"
  12 )
  13 
  14 // note: the code is avoiding using the fmt package to save hundreds of
  15 // kilobytes on the resulting executable, which is a noticeable difference
  16 
  17 const (
  18     spaces = `                `
  19     clear  = "\r" + spaces + spaces + spaces + "\r"
  20 )
  21 
  22 //go:embed info.txt
  23 var info string
  24 
  25 func main() {
  26     if len(os.Args) > 1 {
  27         switch os.Args[1] {
  28         case `-h`, `--h`, `-help`, `--help`:
  29             os.Stderr.WriteString(info)
  30             os.Exit(0)
  31         }
  32     }
  33 
  34     chronograph(os.Stdout, os.Stdin)
  35 }
  36 
  37 // chronograph runs a live chronograph, showing the time elapsed
  38 func chronograph(w io.Writer, r io.Reader) error {
  39     start := time.Now()
  40     t := time.NewTicker(100 * time.Millisecond)
  41     startChronoLine(os.Stderr, start, start)
  42 
  43     stopped := make(chan os.Signal, 1)
  44     defer close(stopped)
  45     signal.Notify(stopped, os.Interrupt)
  46 
  47     lines := make(chan []byte)
  48     errors := make(chan error)
  49     defer close(errors)
  50     quitReader := make(chan struct{}, 1)
  51     defer close(quitReader)
  52 
  53     go func() {
  54         defer close(lines)
  55         const gb = 1024 * 1024 * 1024
  56         sc := bufio.NewScanner(r)
  57         sc.Buffer(nil, 8*gb)
  58 
  59         for sc.Scan() {
  60             select {
  61             case <-quitReader:
  62                 return
  63             default:
  64                 lines <- sc.Bytes()
  65             }
  66         }
  67 
  68         errors <- sc.Err()
  69     }()
  70 
  71     for {
  72         select {
  73         case now := <-t.C:
  74             os.Stderr.WriteString(clear)
  75             startChronoLine(os.Stderr, start, now)
  76 
  77         case line := <-lines:
  78             // write-order of the next 3 steps matters, to avoid mixing
  79             // up lines since stdout and stderr lines can show up together
  80             os.Stderr.WriteString(clear)
  81             w.Write(line)
  82             _, err := w.Write([]byte{'\n'})
  83             startChronoLine(os.Stderr, start, time.Now())
  84             if err != nil {
  85                 quitReader <- struct{}{}
  86                 endChronoLine(os.Stderr, start)
  87                 return err
  88             }
  89 
  90         case err := <-errors:
  91             t.Stop()
  92             os.Stderr.WriteString(clear)
  93             startChronoLine(os.Stderr, start, time.Now())
  94             endChronoLine(os.Stderr, start)
  95             return err
  96 
  97         case <-stopped:
  98             t.Stop()
  99             quitReader <- struct{}{}
 100             endChronoLine(os.Stderr, start)
 101             return nil
 102         }
 103     }
 104 }
 105 
 106 // startChronoLine is used by func chronograph
 107 func startChronoLine(w io.Writer, start, now time.Time) {
 108     v := now.Sub(start)
 109     var s []byte
 110     var buf [64]byte
 111     var t time.Time
 112 
 113     s = t.Add(v).AppendFormat(buf[:0], `15:04:05`)
 114     w.Write(s)
 115     w.Write([]byte(`    `))
 116     s = now.AppendFormat(buf[:0], `2006-01-02 15:04:05 Jan Mon`)
 117     w.Write(s)
 118 }
 119 
 120 // endChronoLine is used by func chronograph
 121 func endChronoLine(w io.Writer, start time.Time) {
 122     var buf [64]byte
 123     secs := time.Since(start).Seconds()
 124     s := strconv.AppendFloat(buf[:0], secs, 'f', 4, 64)
 125 
 126     w.Write([]byte(`    `))
 127     w.Write(s)
 128     w.Write([]byte(" seconds\n"))
 129 }