diff --git a/.gitignore b/.gitignore index d3beee5..ed6a967 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ _testmain.go *.test *.prof +.DS_Store diff --git a/README.md b/README.md index c96d696..2c7ae46 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,11 @@ # gomodoro -A simple command line pomodoro timer \ No newline at end of file +A simple command line pomodoro timer + +## Usage + +Run `gomodoro` to start the default timer. It will print the current status on the command line. + +You can customize the duration and the logging interval by using the `-duration` or `-interval` flags. Example: A 30 min timer logging every 1 min would be `gomodoro -duration 30m -interval 1m` (Not working right now) + +Only one gomodoro timer can be running at a time. If you are running one in the background on another thread, you can display the status by using `gomodoro -status` diff --git a/main.go b/main.go new file mode 100644 index 0000000..5755a85 --- /dev/null +++ b/main.go @@ -0,0 +1,139 @@ +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 { + 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") + var status bool + flag.BoolVar(&status, "status", false, "Only display current Gomodoro status") + + // 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) + } + } +}