package main import ( "encoding/json" "flag" "fmt" "log" "net" "os" "os/signal" "path" "syscall" "time" ) const ( ConfigDir = ".config/gomodoro" LogFileName = "gomodoro.json" ) // Gomodoro holds the config for the current timer type TimerConfig struct { StartTime time.Time Duration time.Duration Interval time.Duration } // PomoStatus Represents the status of the Gomodor timer at a given moment type PomoStatus struct { Status string StartTime int64 DurationSeconds float64 RemainingSeconds float64 Done bool } // timeRemaining returns the remaining duration rounded to the nearest interval func timeRemaining(start time.Time, duration time.Duration, interval time.Duration) time.Duration { return (duration - time.Since(start)).Round(interval) } // fmtJSON returns a JSON string for a given interface and safely returns an err func fmtJSON(obj interface{}) (string, error) { bstring, err := json.Marshal(obj) if err != nil { return "", err } return string(bstring[:]), err } // logRemaining prints a JSON formatted string logging the time remaining in the timer func logRemaining(logFile *os.File, timerConfig TimerConfig) error { timeRemaining := timeRemaining(timerConfig.StartTime, timerConfig.Duration, timerConfig.Interval) err := logTextStatus(logFile, timerConfig, timeRemaining.String(), false) return err } // logTextStatus logs the status of a timer with a given string func logTextStatus(logFile *os.File, timerConfig TimerConfig, status string, done bool) error { // TODO: Find a better way than calculating this twice timeRemaining := timeRemaining(timerConfig.StartTime, timerConfig.Duration, timerConfig.Interval) jstring, err := fmtJSON(&PomoStatus{ Status: status, StartTime: timerConfig.StartTime.Unix(), DurationSeconds: timerConfig.Duration.Seconds(), RemainingSeconds: timeRemaining.Seconds(), Done: done, }) if err == nil { if logFile != nil { logFile.WriteString(jstring + "\n") } fmt.Printf("%s / %s\n", status, timerConfig.Duration.String()) } return err } func tryLock() (net.Listener, error) { return net.Listen("unix", "/tmp/gomodoro") } func openLogFile() (*os.File, error) { dir := path.Join(os.Getenv("HOME"), ConfigDir) os.MkdirAll(dir, os.ModePerm) logFile, err := os.OpenFile(path.Join(dir, LogFileName), os.O_APPEND|os.O_RDWR|os.O_CREATE, os.ModePerm) return logFile, err } func main() { var duration time.Duration flag.DurationVar(&duration, "duration", 25*time.Minute, "Gomodoro duration") var interval time.Duration flag.DurationVar(&interval, "interval", 1*time.Second, "Gomodoro tick interval") /* Haven't found a good way to tail a file in golang yet * var status bool * flag.BoolVar(&status, "status", false, "Only display current Gomodoro status") */ flag.Parse() // Ensure obtain lock lock, err := tryLock() if err != nil { // TODO: More helpful message log.Fatal(err) return } defer lock.Close() // Open file for logging logFile, err := openLogFile() if err != nil { lock.Close() log.Fatal(err) return } defer logFile.Close() // Start timer start := time.Now() timerConfig := TimerConfig{ StartTime: start, Duration: duration, Interval: interval, } tick := time.Tick(interval) end := time.After(duration) stop := make(chan os.Signal, 1) signal.Notify(stop, os.Interrupt, os.Kill, syscall.SIGINT, syscall.SIGTERM) for { select { case <-tick: logRemaining(logFile, timerConfig) case <-end: logTextStatus(logFile, timerConfig, "Complete", true) return case <-stop: logTextStatus(logFile, timerConfig, "Interrupted", true) return default: time.Sleep(50 * time.Millisecond) } } }