timer/main.go

186 lines
3.9 KiB
Go

package main
import (
"context"
"flag"
"fmt"
"log"
"net/http"
"net/url"
"os"
"os/signal"
"slices"
"strings"
"syscall"
"time"
"github.com/gen2brain/beeep"
)
func main() {
ctx, can := signal.NotifyContext(context.Background(), syscall.SIGINT)
defer can()
fs := flag.NewFlagSet(os.Args[0], flag.ContinueOnError)
gcal := fs.Bool("gcal", false, "wait for google cal events")
ntfy := fs.String("http", "", "https://squeaky2x3:dYbtypGkHXFtq1E00k2H42SPGUowi@ntfy.home.blapointe.com/alerts-render")
if err := fs.Parse(os.Args[1:]); err != nil {
panic(err)
}
alerts, err := alerts(ctx, *gcal, fs.Args())
if err != nil {
panic(err)
}
for alert := range alerts {
if ctx.Err() != nil {
break
}
if err := alertAt(ctx, *ntfy, time.Now(), alert, time.Now().Format("15:04")); err != nil && ctx.Err() == nil {
panic(err)
}
if ctx.Err() != nil {
break
}
}
if err := ctx.Err(); err != nil {
os.Exit(1)
}
}
func alerts(ctx context.Context, gcal bool, args []string) (chan string, error) {
if gcal {
return alertsGCal(ctx)
}
duration, err := time.ParseDuration(args[0])
if err != nil {
return nil, err
}
msg := "alerting after " + duration.String()
if len(args) > 1 {
msg = fmt.Sprintf("%s (%s)", args[1], duration.String())
}
return alertsAfter(ctx, duration, msg)
}
func alertsAfter(ctx context.Context, dur time.Duration, msg string) (chan string, error) {
ch := make(chan string)
deadline := time.Now().Add(dur)
go func() {
defer close(ch)
var prev string
for ctx.Err() == nil && time.Now().Before(deadline) {
seconds := int(time.Until(deadline).Seconds())
cur := fmt.Sprintf("%v", time.Duration(seconds)*time.Second)
if prev != "" {
fmt.Printf("\r%s\r", strings.Repeat(" ", len(prev)))
}
fmt.Printf("%s", cur)
prev = cur
select {
case <-ctx.Done():
case <-time.After(time.Second):
}
}
if ctx.Err() == nil {
fmt.Println(msg)
}
ch <- msg
}()
return ch, nil
}
func alertsGCal(ctx context.Context) (chan string, error) {
gcal := NewGCal()
if err := gcal.Login(ctx); err != nil {
return nil, err
}
var events []Event
refresh := func() error {
es, err := gcal.EventsToday(ctx)
es = slices.DeleteFunc(es, func(s Event) bool {
return time.Now().After(s.Time)
})
if !slices.Equal(es, events) {
events = es
if len(events) > 0 {
log.Println("alerting about", events[0].Name, "at", events[0].Time.Format("15:04"), "(", time.Until(events[0].Time), ")")
}
}
return err
}
if err := refresh(); err != nil {
return nil, err
}
ch := make(chan string)
go func() {
defer close(ch)
c := time.NewTicker(time.Minute * 13)
defer c.Stop()
for ctx.Err() == nil && len(events) > 0 {
select {
case <-c.C:
if err := refresh(); err != nil {
panic(err)
}
case <-time.After(time.Until(events[0].Time)):
select {
case <-ctx.Done():
case ch <- events[0].Name:
if events[0].Duration > 0 {
select {
case <-ctx.Done():
case <-time.After(events[0].Duration):
select {
case <-ctx.Done():
case ch <- "/" + events[0].Name:
}
}
}
}
events = events[1:]
case <-ctx.Done():
}
}
}()
return ch, nil
}
func alertAt(ctx context.Context, ntfy string, deadline time.Time, title, msg string) error {
c := time.NewTicker(time.Second)
defer c.Stop()
func() {
ctx, can := context.WithDeadline(ctx, deadline)
defer can()
for ctx.Err() == nil {
select {
case <-c.C:
case <-ctx.Done():
return
}
fmt.Printf("\r%s ", deadline.Sub(time.Now()))
}
}()
fmt.Println()
if err := ctx.Err(); err == nil {
beeep.Alert(title, msg, "/dev/null")
if ntfy != "" {
u, _ := url.Parse(ntfy)
req, _ := http.NewRequest(http.MethodPost, u.String(), strings.NewReader(fmt.Sprintf("%s: %s", title, msg)))
req = req.WithContext(ctx)
http.DefaultClient.Do(req)
}
}
return ctx.Err()
}