File: timer/go.mod 1 module timer 2 3 go 1.18 File: timer/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: timer/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 }