From 7fe3934f07ab6232470a026c3c9acf838d129ad7 Mon Sep 17 00:00:00 2001 From: Bel LaPointe Date: Mon, 17 Feb 2020 14:42:30 -0700 Subject: [PATCH] Break into multi files for more robust and less panicky --- basicstopwatch.go | 31 ++++++ basictimer.go | 31 ++++++ config.go | 58 ++++++++++ keyboard.go | 37 +++++++ keyboard_test.go | 45 ++++++++ main.go | 262 ++++++-------------------------------------- tickprinter.go | 65 +++++++++++ tickprinter_test.go | 43 ++++++++ timer.go | 91 +++++++++++++++ with.go | 39 +++++++ 10 files changed, 474 insertions(+), 228 deletions(-) create mode 100644 basicstopwatch.go create mode 100644 basictimer.go create mode 100644 config.go create mode 100644 keyboard.go create mode 100644 keyboard_test.go mode change 100755 => 100644 main.go create mode 100644 tickprinter.go create mode 100644 tickprinter_test.go create mode 100644 timer.go create mode 100644 with.go diff --git a/basicstopwatch.go b/basicstopwatch.go new file mode 100644 index 0000000..b6cdc99 --- /dev/null +++ b/basicstopwatch.go @@ -0,0 +1,31 @@ +package main + +import ( + "fmt" +) + +type BasicStopwatch struct { + *Timer +} + +func NewBasicStopwatch(t *Timer) string { + b := &BasicStopwatch{Timer: t} + return b.String() +} + +func (t *BasicStopwatch) left() string { + cur := t.config.Duration - t.Remaining() + return fmt.Sprintf( + "%02d:%02d:%02d", + int(cur.Hours()), + int(cur.Minutes()), + int(cur.Seconds())%60, + ) +} + +func (t *BasicStopwatch) String() string { + return fmt.Sprintf( + "%s", + t.left(), + ) +} diff --git a/basictimer.go b/basictimer.go new file mode 100644 index 0000000..7b020b3 --- /dev/null +++ b/basictimer.go @@ -0,0 +1,31 @@ +package main + +import ( + "fmt" +) + +type BasicTimer struct { + *Timer +} + +func NewBasicTimer(t *Timer) string { + b := &BasicTimer{Timer: t} + return b.String() +} + +func (t *BasicTimer) left() string { + cur := t.Remaining() + return fmt.Sprintf( + "%02d:%02d:%02d", + int(cur.Hours()), + int(cur.Minutes()), + int(cur.Seconds())%60, + ) +} + +func (t *BasicTimer) String() string { + return fmt.Sprintf( + "%s", + t.left(), + ) +} diff --git a/config.go b/config.go new file mode 100644 index 0000000..855a3d9 --- /dev/null +++ b/config.go @@ -0,0 +1,58 @@ +package main + +import ( + "local/args" + "time" +) + +type Config struct { + Repeat bool + Stopwatch bool + ETA bool + TS bool + Done bool + Duration time.Duration + Offset time.Duration + Msg string + Interval time.Duration + Until time.Time +} + +func NewConfig() (Config, error) { + as := args.NewArgSet() + + as.Append(args.TIME, "until", "deadline", time.Time{}) + as.Append(args.BOOL, "repeat", "whether to loop", true) + as.Append(args.BOOL, "stopwatch", "count up to time instead of down", false) + as.Append(args.BOOL, "eta", "whether to display deadline", false) + as.Append(args.BOOL, "ts", "whether to display timestamp", false) + as.Append(args.BOOL, "done", "whether to display done or not", false) + as.Append(args.DURATION, "duration", "how long the base time is", time.Minute*30) + as.Append(args.DURATION, "offset", "offset from the base time", time.Second*0) + as.Append(args.STRING, "msg", "message to display", "Timer up") + as.Append(args.DURATION, "interval", "refresh interval", time.Millisecond*500) + + if err := as.Parse(); err != nil { + return Config{}, err + } + + config := Config{ + Repeat: as.Get("repeat").GetBool(), + Stopwatch: as.Get("stopwatch").GetBool(), + ETA: as.Get("eta").GetBool(), + TS: as.Get("ts").GetBool(), + Done: as.Get("done").GetBool(), + Duration: as.Get("duration").GetDuration(), + Offset: as.Get("offset").GetDuration(), + Msg: as.Get("msg").GetString(), + Interval: as.Get("interval").GetDuration(), + Until: as.Get("until").GetTime(), + } + + if !config.Until.IsZero() { + config.Repeat = false + config.Duration = time.Until(config.Until) + config.Offset + } + + return config, nil +} diff --git a/keyboard.go b/keyboard.go new file mode 100644 index 0000000..d06ecd7 --- /dev/null +++ b/keyboard.go @@ -0,0 +1,37 @@ +package main + +import "github.com/eiannone/keyboard" + +type Keyboard struct { + Events chan byte +} + +func NewKeyboard() (*Keyboard, error) { + err := keyboard.Open() + return &Keyboard{ + Events: make(chan byte), + }, err +} + +func (k *Keyboard) Listen() { + defer func() { + recover() + }() + for { + r, _, err := keyboard.GetKey() + if err != nil { + continue + } + b := byte(r) + k.Events <- b + } +} + +func (k *Keyboard) Next() byte { + return <-k.Events +} + +func (k *Keyboard) Close() { + keyboard.Close() + close(k.Events) +} diff --git a/keyboard_test.go b/keyboard_test.go new file mode 100644 index 0000000..258f614 --- /dev/null +++ b/keyboard_test.go @@ -0,0 +1,45 @@ +package main + +import ( + "fmt" + "os" + "strings" + "testing" +) + +func TestKeyboardNext(t *testing.T) { + defer fakeInput(t)() + k, err := NewKeyboard() + if err != nil { + t.Fatal(err) + } + go k.Listen() + for i := 0; i < 5; i++ { + t.Log(k.Next()) + } + k.Close() +} + +func TestKeyboardEvents(t *testing.T) { + defer fakeInput(t)() + k, err := NewKeyboard() + if err != nil { + t.Fatal(err) + } + go k.Listen() + i := 0 + for b := range k.Events { + i += 1 + t.Log(b) + if i == 5 { + k.Close() + } + } +} + +func fakeInput(t *testing.T) func() { + if !strings.Contains(fmt.Sprint(os.Args), "-test.v=true") { + t.Fatal("-v not passed, requries manual testing for input") + } + return func() {} +} diff --git a/main.go b/main.go old mode 100755 new mode 100644 index bde11c8..f4408c8 --- a/main.go +++ b/main.go @@ -1,253 +1,59 @@ -// Package main has a comment package main import ( - "flag" "fmt" - "log" - "strconv" - "strings" - "time" - - "github.com/eiannone/keyboard" - "github.com/everdev/mack" + "os" + "os/signal" + "syscall" ) -var notified = false -var alertMessage string - -var originalStart = time.Now() - func main() { - - var duration string - var offset string - var interval string - var repeat bool - var invert bool - var eta bool - var until string - - flag.BoolVar(&repeat, "repeat", true, "Whether to repeat the timer on complete") - flag.BoolVar(&invert, "stopwatch", false, "Use as a stopwatch") - flag.StringVar(&duration, "duration", "30m", "How long the timer should be") - flag.StringVar(&offset, "offset", "0m", "How much time the initial time should skip") - flag.StringVar(&alertMessage, "msg", "Timer up", "Message to display on timer expiration") - flag.StringVar(&interval, "interval", "500ms", "Interval duration") - flag.BoolVar(&eta, "eta", false, "Whether to display the ending time") - flag.StringVar(&until, "until", "", "Overload to make a non-repeating timer until given time as 23:59") - flag.Parse() - - if until != "" { - repeat = false - offset = "0m" - now := time.Now() - targetHourStr := strings.Split(until, ":")[0] - targetMinStr := strings.Split(until, ":")[1] - hour, err := strconv.Atoi(targetHourStr) - if err != nil { - panic(err) - } - min, err := strconv.Atoi(targetMinStr) - if err != nil { - panic(err) - } - if hour < now.Hour() || (hour == now.Hour() && min < now.Minute()) { - now = now.Add(time.Hour * 24) - } - thenTime := time.Date(now.Year(), now.Month(), now.Day(), hour, min, 0, 0, now.Location()) - duration = thenTime.Sub(time.Now()).String() - } - - tickerInterval, err := time.ParseDuration(interval) + config, err := NewConfig() if err != nil { panic(err) } - monitor := time.NewTicker(tickerInterval) - defer monitor.Stop() - base, err := time.ParseDuration(duration) + k, err := NewKeyboard() if err != nil { panic(err) } - var cur time.Duration - if invert { - cur = time.Duration(0) - } else { - cur = base - } - skip, err := time.ParseDuration(offset) + go k.Listen() + defer k.Close() + + tp := NewTickPrinter(config.Interval) + go tp.Start() + defer tp.Stop() + + timer, err := NewTimer(config) if err != nil { panic(err) } - if invert { - cur += skip - } else { - cur -= skip + tp.Message <- timer.String + + sigc := make(chan os.Signal) + signal.Notify(sigc, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGTERM) + + go listen(k, tp, timer, func() { close(sigc) }) + + select { + case <-sigc: } +} - stop := make(chan bool) - pause := make(chan bool) - confirm := make(chan bool) - paused := false - delim := ':' - - log.Print("Quit with 'q', Pause with 'p', Reset with 'r'") - - keych := keyChannel() - - go func() { - last := time.Now() - printTime(&cur, base, &delim, invert, repeat, eta) - for { - select { - case <-monitor.C: - difference := time.Duration(time.Now().UnixNano() - last.UnixNano()) - last = time.Now() - if difference > time.Hour*2 { - keych <- 'p' - continue - } else if invert { - cur += difference - } else { - cur -= difference - } - case state := <-pause: - if !state { - monitor = time.NewTicker(tickerInterval) - last = time.Now() - } else { - monitor.Stop() - } - case <-stop: - log.Print() - confirm <- true - return - } - printTime(&cur, base, &delim, invert, repeat, eta) - } - }() - +func listen(k *Keyboard, tp *TickPrinter, timer *Timer, stop func()) { for { - b := <-keych - switch b { - case 'q': - stop <- true - <-confirm - return + key := <-k.Events + switch key { case 'p': - paused = !paused - pause <- paused - if paused { - keych <- 'z' - } - case 'r': - skip = time.Duration(0) - originalStart = time.Now() - if invert { - cur = time.Duration(0) - } else { - cur = base - } - notified = false - printTime(&cur, base, &delim, invert, repeat, eta) + timer.TogglePause() case 'z': - log.Print() - printTime(&cur, base, &delim, invert, repeat, eta) + fmt.Printf("\n") + tp.Flush() + case 'r': + timer.Reset() + case 'q': + stop() + return } } } - -func printTime(pRemains *time.Duration, target time.Duration, delim *rune, reverse bool, repeat bool, eta bool) { - var final string - remains := *pRemains - sec := remains.Seconds() - min := int(sec / 60.0) - seconds := int(sec) % 60 - hrs := min / 60 - min = min % 60 - if *delim == ':' { - *delim = ' ' - } else { - *delim = ':' - } - if reverse { - remains := target - remains - rMin := int(remains.Seconds() / 60.0) - rSec := int(remains.Seconds()) % 60 - rHrs := rMin / 60 - rMin = rMin % 60 - tdelim := byte(*delim) - if remains < 0 { - rMin = 0 - rSec = 0 - rHrs = 0 - tdelim = ':' - go alertTime(pRemains, target, reverse, repeat) - } - at := fmt.Sprintf("%2d%c%02d%c%02d ", hrs, byte(*delim), min, byte(*delim), seconds) - rem := fmt.Sprintf("%2d%c%02d%c%02d ", rHrs, tdelim, rMin, tdelim, rSec) - rem = fmt.Sprintf("%s \tAt: %s ", rem, at) - if eta { - rem = fmt.Sprintf("%s \tETA: %s", rem, time.Unix(0, (time.Now().UnixNano()+target.Nanoseconds()-pRemains.Nanoseconds())).Format("3:04")) - } - final = rem - } else { - rem := fmt.Sprintf("%2d%c%02d%c%02d ", hrs, byte(*delim), min, byte(*delim), seconds) - final = rem + " " - if eta { - final = fmt.Sprintf("%s \tETA: %s", final, time.Unix(0, time.Now().UnixNano()+pRemains.Nanoseconds()).Format("3:04")) - } - if remains < 0 { - go alertTime(pRemains, target, reverse, repeat) - } - } - fmt.Printf("\r%s", final) -} - -func alertTime(pRemains *time.Duration, target time.Duration, reverse, repeat bool) { - if !notified { - notified = true - _, err := mack.Alert(fmt.Sprintf("%s\nTimer for %s done", alertMessage, target.String())) - if err != nil { - panic(err) - } - if repeat { - originalStart = time.Now() - if reverse { - for *pRemains > 0 { - *pRemains -= target - } - *pRemains += target - } else { - for *pRemains < 0 { - *pRemains += target - } - } - notified = false - } - } -} - -func keyChannel() chan rune { - ch := make(chan rune, 20) - go func() { - if err := keyboard.Open(); err != nil { - panic(err) - } - for { - b, _, err := keyboard.GetKey() - if err != nil { - panic(err) - } - by := rune(b) - if by == 'q' { - keyboard.Close() - ch <- by - return - } - ch <- by - } - }() - return ch -} diff --git a/tickprinter.go b/tickprinter.go new file mode 100644 index 0000000..e379f1a --- /dev/null +++ b/tickprinter.go @@ -0,0 +1,65 @@ +package main + +import ( + "fmt" + "strings" + "time" +) + +type TickPrinter struct { + Message chan func() string + msg func() string + lastLen int + Interval time.Duration + stop chan struct{} + flush chan struct{} +} + +func NewTickPrinter(interval time.Duration) *TickPrinter { + return &TickPrinter{ + Message: make(chan func() string), + msg: func() string { + return time.Now().Format("15:04:05") + }, + stop: make(chan struct{}), + flush: make(chan struct{}), + Interval: interval, + } +} + +func (t *TickPrinter) Start() { + ticker := time.NewTicker(t.Interval) + for { + select { + case <-t.flush: + t.Print() + case <-ticker.C: + t.Print() + case msg := <-t.Message: + t.msg = msg + case <-t.stop: + return + } + } +} + +func (t *TickPrinter) Stop() { + close(t.stop) + time.Sleep(t.Interval) + fmt.Printf("\n") +} + +func (t *TickPrinter) Flush() { + t.flush <- struct{}{} +} + +func (t *TickPrinter) Print() { + fmt.Printf("\r%s", strings.Repeat(" ", t.lastLen)) + msg := t.msg() + l := len(msg) + if len(msg) < t.lastLen { + msg += strings.Repeat(" ", t.lastLen-len(msg)) + } + t.lastLen = l + fmt.Printf("\r%s", msg) +} diff --git a/tickprinter_test.go b/tickprinter_test.go new file mode 100644 index 0000000..b51d61e --- /dev/null +++ b/tickprinter_test.go @@ -0,0 +1,43 @@ +package main + +import ( + "io/ioutil" + "os" + "strings" + "testing" + "time" +) + +func TestTickPrinter(t *testing.T) { + defer testOut(t)() + name := os.Stdout.Name() + + tp := NewTickPrinter(time.Millisecond * 250) + go tp.Start() + time.Sleep(time.Millisecond * 300) + tp.Stop() + b, err := ioutil.ReadFile(name) + if err != nil { + t.Fatal(err) + } + ts, err := time.Parse("15:04:05", strings.TrimSpace(string(b))) + if err != nil { + t.Fatal(err) + } + if (time.Now().Minute()*60+time.Now().Hour())-(ts.Minute()*60+ts.Hour()) > 1 { + t.Fatal(ts) + } +} + +func testOut(t *testing.T) func() { + wasOut := os.Stdout + + var err error + if os.Stdout, err = ioutil.TempFile(os.TempDir(), "testOut*"); err != nil { + t.Fatal(err) + } + + return func() { + os.Stdout = wasOut + } +} diff --git a/timer.go b/timer.go new file mode 100644 index 0000000..d047e52 --- /dev/null +++ b/timer.go @@ -0,0 +1,91 @@ +package main + +import ( + "time" +) + +type Typed func(*Timer) string + +type Timer struct { + config Config + From time.Time + Typed Typed + paused bool + offset time.Duration +} + +func NewTimer(config Config) (*Timer, error) { + t := &Timer{From: time.Now().Add(time.Duration(-1) * config.Offset), config: config} + return t, t.setTyped(config) +} + +func (t *Timer) TogglePause() { + if t.paused { + t.Resume() + } else { + t.Pause() + } +} + +func (t *Timer) Resume() { + t.paused = false + t.From = time.Now().Add(time.Duration(-1) * t.offset) + t.offset = 0 +} + +func (t *Timer) Pause() { + t.paused = true + t.offset = time.Since(t.From) +} + +func (t *Timer) setTyped(config Config) error { + if config.Stopwatch { + t.Typed = NewBasicStopwatch + } else { + t.Typed = NewBasicTimer + } + + if config.ETA { + t.Typed = WithETA(t.Typed) + } + + if config.Done { + t.Typed = WithDone(t.Typed) + } + + if config.TS { + t.Typed = WithTS(t.Typed) + } + + return nil +} + +func (t *Timer) Remaining() time.Duration { + return time.Until(t.Deadline()) + time.Second +} + +func (t *Timer) Deadline() time.Time { + from := t.From + if t.paused { + from = time.Now().Add(time.Duration(-1) * t.offset) + } + return from.Add(t.config.Duration) +} + +func (t *Timer) Ack() { + for t.Done() { + t.From = t.From.Add(t.config.Duration) + } +} + +func (t *Timer) Reset() { + t.From = time.Now() +} + +func (t *Timer) Done() bool { + return time.Now().After(t.From.Add(t.config.Duration)) +} + +func (t *Timer) String() string { + return t.Typed(t) +} diff --git a/with.go b/with.go new file mode 100644 index 0000000..fd03252 --- /dev/null +++ b/with.go @@ -0,0 +1,39 @@ +package main + +import ( + "fmt" + "time" +) + +func WithETA(typed Typed) Typed { + return func(t *Timer) string { + s := typed(t) + return fmt.Sprintf( + "%s\tETA: %s", + s, + t.Deadline().Format("15:04:05"), + ) + } +} + +func WithDone(typed Typed) Typed { + return func(t *Timer) string { + s := typed(t) + return fmt.Sprintf( + "%s\tDone: %v", + s, + t.Done(), + ) + } +} + +func WithTS(typed Typed) Typed { + return func(t *Timer) string { + s := typed(t) + return fmt.Sprintf( + "%s: %s", + time.Now().Format("15:04:05"), + s, + ) + } +}