You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
144 lines
3.7 KiB
144 lines
3.7 KiB
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) |
|
} |
|
} |
|
}
|
|
|