Compare commits

...

10 Commits

Author SHA1 Message Date
Bel LaPointe
874b93f6e0 create pop ups 2020-02-18 08:43:42 -07:00
Bel LaPointe
4835c24868 clean up flushing 2020-02-17 15:19:38 -07:00
Bel LaPointe
6fa61b3d2f more white space 2020-02-17 15:14:23 -07:00
Bel LaPointe
e5a21a6063 Fix until with args changes 2020-02-17 15:09:11 -07:00
Bel LaPointe
aee3922767 fix big hours 2020-02-17 14:52:54 -07:00
Bel LaPointe
df3d56f677 Add add and sub 2020-02-17 14:45:11 -07:00
Bel LaPointe
7fe3934f07 Break into multi files for more robust and less panicky 2020-02-17 14:42:30 -07:00
Bel LaPointe
7a99f2af50 whatever 2019-05-02 09:33:21 -06:00
Bel LaPointe
48ef624154 shhh 2018-10-13 19:25:18 -06:00
Bel LaPointe
a4c734829f anti vendor 2018-06-28 19:08:17 -06:00
10 changed files with 508 additions and 225 deletions

31
basicstopwatch.go Executable 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())%60,
int(cur.Seconds())%60,
)
}
func (t *BasicStopwatch) String() string {
return fmt.Sprintf(
"%s",
t.left(),
)
}

31
basictimer.go Executable 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())%60,
int(cur.Seconds())%60,
)
}
func (t *BasicTimer) String() string {
return fmt.Sprintf(
"%s",
t.left(),
)
}

58
config.go Executable 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.After(time.Now()) {
config.Repeat = false
config.Duration = time.Until(config.Until) + config.Offset
}
return config, nil
}

37
keyboard.go Executable 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 Executable 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() {}
}

256
main.go
View File

@@ -1,250 +1,68 @@
// Package main has a comment
package main package main
import ( import (
"flag" "os"
"fmt" "os/signal"
"local/logger" "syscall"
"strconv"
"strings"
"time" "time"
"github.com/eiannone/keyboard"
"github.com/everdev/mack"
) )
var notified = false
var alertMessage string
var originalStart = time.Now()
func main() { func main() {
config, err := NewConfig()
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)
if err != nil { if err != nil {
panic(err) panic(err)
} }
monitor := time.NewTicker(tickerInterval) k, err := NewKeyboard()
defer monitor.Stop()
base, err := time.ParseDuration(duration)
if err != nil { if err != nil {
panic(err) panic(err)
} }
var cur time.Duration go k.Listen()
if invert { defer k.Close()
cur = time.Duration(0)
} else { tp := NewTickPrinter(config.Interval)
cur = base go tp.Start()
} defer tp.Stop()
skip, err := time.ParseDuration(offset)
timer, err := NewTimer(config)
if err != nil { if err != nil {
panic(err) panic(err)
} }
if invert { tp.Message <- timer.String
cur += skip
} else {
cur -= skip
}
stop := make(chan bool) sigc := make(chan os.Signal)
pause := make(chan bool) signal.Notify(sigc, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGTERM)
confirm := make(chan bool)
paused := false
delim := ':'
logger.Log("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 { select {
case <-monitor.C: case <-sigc:
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:
logger.Logf()
confirm <- true
return
}
printTime(&cur, base, &delim, invert, repeat, eta)
}
}()
func listen(k *Keyboard, tp *TickPrinter, timer *Timer, stop func()) {
for { for {
b := <-keych key := <-k.Events
switch b { switch key {
case 'q': case 'a':
stop <- true timer.Add(time.Second)
<-confirm case 'A':
return timer.Add(time.Minute)
case 's':
timer.Sub(time.Second)
case 'S':
timer.Sub(time.Minute)
case 'p': case 'p':
paused = !paused tp.Flush()
pause <- paused timer.TogglePause()
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)
case 'z': case 'z':
logger.Logf() tp.Flush()
printTime(&cur, base, &delim, invert, repeat, eta) case 'r':
} tp.Flush()
} timer.Reset()
} case 'q':
stop()
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)
}
}
logger.Logf("\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 return
} }
ch <- by
} }
}()
return ch
} }

66
tickprinter.go Executable file
View File

@@ -0,0 +1,66 @@
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:
fmt.Printf("\n")
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 Executable 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
}
}

115
timer.go Executable file
View File

@@ -0,0 +1,115 @@
package main
import (
"fmt"
"time"
"github.com/everdev/mack"
)
type Typed func(*Timer) string
type Timer struct {
config Config
From time.Time
Typed Typed
paused bool
offset time.Duration
displayed bool
}
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 {
remaining := time.Until(t.Deadline()) + time.Second
if remaining < 0 && !t.displayed {
t.displayed = true
go func() {
_, err := mack.Alert(fmt.Sprintf("Timer for %s done", t.config.Msg))
if err == nil {
t.Ack()
}
}()
}
return remaining
}
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)
}
t.displayed = false
}
func (t *Timer) Reset() {
t.Ack()
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)
}
func (t *Timer) Add(d time.Duration) {
t.From = t.From.Add(d)
}
func (t *Timer) Sub(d time.Duration) {
t.Add(time.Duration(-1) * d)
}

39
with.go Executable 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:\t%s",
time.Now().Format("15:04:05"),
s,
)
}
}