gomodoro/main.go

145 lines
3.7 KiB
Go
Raw Normal View History

2018-01-19 01:42:41 +00:00
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 {
2018-05-11 01:08:10 +00:00
if logFile != nil {
logFile.WriteString(jstring + "\n")
}
2018-01-19 01:42:41 +00:00
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")
2018-05-11 01:08:10 +00:00
/* 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()
2018-01-19 01:42:41 +00:00
// 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)
}
}
}