package main import ( "context" "flag" "fmt" "os" "os/exec" "os/signal" "runtime" "strings" "time" ) type Opt struct { Work time.Duration Short time.Duration Long time.Duration Day time.Duration Tick time.Duration Runs int run int } type stateFn func(context.Context) stateFn func (o *Opt) doWork(ctx context.Context) stateFn { select { case <-ctx.Done(): return nil default: ctx, cancel := context.WithTimeout(ctx, o.Work) defer cancel() display(ctx, o.Tick, "do work") } if o.run++; o.run%o.Runs == 0 { return o.longBreak } return o.shortBreak } func (o *Opt) shortBreak(ctx context.Context) stateFn { select { case <-ctx.Done(): return nil default: ctx, cancel := context.WithTimeout(ctx, o.Short) defer cancel() display(ctx, o.Tick, "short break") } return o.doWork } func (o *Opt) longBreak(ctx context.Context) stateFn { select { case <-ctx.Done(): return nil default: ctx, cancel := context.WithTimeout(ctx, o.Long) defer cancel() display(ctx, o.Tick, "long break") } return o.doWork } func display(ctx context.Context, tick time.Duration, s string) { go notify("Pomodoro timer", s) dl, ok := ctx.Deadline() if !ok { return } total := time.Until(dl) ticker := time.NewTicker(tick) defer ticker.Stop() defer fmt.Println("") for range ticker.C { fmt.Printf("\r%-20s %s", s, progress(total, time.Until(dl))) select { case <-ctx.Done(): return default: } } } func progress(total, left time.Duration) string { width := 40 if left < 0 { left = 0 } todo := width * int(left) / int(total) s := fmt.Sprintf("%3d%% |", 100-100*int(left)/int(total)) s += strings.Repeat("*", width-todo) + strings.Repeat(" ", todo) if left > 0 { s += fmt.Sprintf("| %9s", round(left)) } else { s += fmt.Sprintf("| %9s", "done") } return s } func notify(title, s string) error { s = time.Now().Format(time.Kitchen) + " " + s switch runtime.GOOS { case "darwin": msg := fmt.Sprintf("display notification %q with title %q", s, title) return exec.Command("osascript", "-e", msg).Run() case "linux": return exec.Command("notify-send", title, s).Run() default: // *BSD msg := fmt.Sprintf("%s\n\n%s", title, s) return exec.Command("xmessage", "-center", "-timeout", "5", msg).Run() } } func round(d time.Duration) time.Duration { return d - d%time.Second } func main() { var o Opt flag.DurationVar(&o.Work, "work", 25*time.Minute, "work time") flag.DurationVar(&o.Short, "short", 5*time.Minute, "short break") flag.DurationVar(&o.Long, "long", 15*time.Minute, "long break") flag.DurationVar(&o.Day, "day", 12*time.Hour, "work day") flag.DurationVar(&o.Tick, "tick", time.Second, "update interval") flag.IntVar(&o.Runs, "runs", 4, "work runs") flag.Parse() ctx, cancel := context.WithTimeout(context.Background(), o.Day) sig := make(chan os.Signal, 1) signal.Notify(sig, os.Interrupt) go func() { <-sig cancel() }() defer func(t time.Time) { fmt.Printf("total %v\n", round(time.Since(t))) }(time.Now()) for s := o.doWork; s != nil; s = s(ctx) { } }