Break into multi files for more robust and less panicky

master
Bel LaPointe 2020-02-17 14:42:30 -07:00
parent 7a99f2af50
commit 7fe3934f07
10 changed files with 474 additions and 228 deletions

31
basicstopwatch.go Normal file
View File

@ -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(),
)
}

31
basictimer.go Normal file
View File

@ -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(),
)
}

58
config.go Normal file
View File

@ -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
}

37
keyboard.go Normal file
View File

@ -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)
}

45
keyboard_test.go Normal file
View File

@ -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() {}
}

252
main.go Executable file → Normal file
View File

@ -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
stop := make(chan bool)
pause := make(chan bool)
confirm := make(chan bool)
paused := false
delim := ':'
sigc := make(chan os.Signal)
signal.Notify(sigc, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGTERM)
log.Print("Quit with 'q', Pause with 'p', Reset with 'r'")
go listen(k, tp, timer, func() { close(sigc) })
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 <-sigc:
}
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)
}
}
}
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
fmt.Printf("\n")
tp.Flush()
case 'r':
timer.Reset()
case 'q':
stop()
return
}
ch <- by
}
}()
return ch
}

65
tickprinter.go Normal file
View File

@ -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)
}

43
tickprinter_test.go Normal file
View File

@ -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
}
}

91
timer.go Normal file
View File

@ -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)
}

39
with.go Normal file
View File

@ -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,
)
}
}