Compare commits
10 Commits
4e85f48d9e
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf9f50f583 | ||
|
|
d17993453e | ||
|
|
572f129ddb | ||
|
|
d05789b36b | ||
|
|
5f38a36027 | ||
|
|
d7d888453b | ||
|
|
7d2b9764c3 | ||
|
|
aeb575e621 | ||
|
|
f1846ed93c | ||
|
|
ca20b2a052 |
26
gcal.go
26
gcal.go
@@ -7,6 +7,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
@@ -26,6 +27,7 @@ func NewGCal() *GCal {
|
||||
type Event struct {
|
||||
Name string
|
||||
Time time.Time
|
||||
Duration time.Duration
|
||||
}
|
||||
|
||||
func (gcal *GCal) EventsToday(ctx context.Context) ([]Event, error) {
|
||||
@@ -36,6 +38,7 @@ func (gcal *GCal) EventsToday(ctx context.Context) ([]Event, error) {
|
||||
|
||||
today := time.Now().Add(-1 * time.Hour * time.Duration(time.Now().Hour()))
|
||||
tomorrow := today.Add(24 * time.Hour)
|
||||
|
||||
events, err := srv.Events.
|
||||
List("primary").
|
||||
ShowDeleted(false).
|
||||
@@ -53,16 +56,33 @@ func (gcal *GCal) EventsToday(ctx context.Context) ([]Event, error) {
|
||||
if events.Items[i].Start.DateTime == "" {
|
||||
continue
|
||||
}
|
||||
if slices.Contains([]string{"focusTime", "outOfOffice"}, events.Items[i].EventType) {
|
||||
continue
|
||||
}
|
||||
t, err := time.Parse(time.RFC3339, events.Items[i].Start.DateTime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if t.After(tomorrow) {
|
||||
continue
|
||||
}
|
||||
result = append(result, Event{
|
||||
Name: events.Items[i].Summary,
|
||||
|
||||
var d time.Duration
|
||||
if t2, err := time.Parse(time.RFC3339, events.Items[i].End.DateTime); err == nil {
|
||||
d = t2.Sub(t)
|
||||
}
|
||||
|
||||
result = append(result,
|
||||
Event{
|
||||
Name: "Soon: " + events.Items[i].Summary,
|
||||
Time: t.Add(-2 * time.Minute),
|
||||
Duration: 0,
|
||||
},
|
||||
Event{
|
||||
Name: "Now: " + events.Items[i].Summary,
|
||||
Time: t,
|
||||
})
|
||||
Duration: d,
|
||||
},
|
||||
)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -13,4 +13,13 @@ func TestGCal(t *testing.T) {
|
||||
if err := gcal.Login(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
today, err := gcal.EventsToday(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
for i := range today {
|
||||
t.Logf("[%d] %+v", i, today[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
144
main.go
144
main.go
@@ -9,12 +9,12 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"os/signal"
|
||||
"slices"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/gen2brain/beeep"
|
||||
"github.com/schollz/progressbar/v3"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -28,70 +28,148 @@ func main() {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if *gcal {
|
||||
if err := alertGCal(ctx, *ntfy); err != nil {
|
||||
alerts, err := alerts(ctx, *gcal, fs.Args())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
} else {
|
||||
if err := alertAfter(ctx, *ntfy, fs.Args()[0]); err != nil {
|
||||
|
||||
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 alertGCal(ctx context.Context, ntfy string) error {
|
||||
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
|
||||
}
|
||||
|
||||
events, err := gcal.EventsToday(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
if err := refresh(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, event := range events {
|
||||
until := event.Time.Add(-1 * time.Minute)
|
||||
if until.Sub(time.Now()) < 1 {
|
||||
continue
|
||||
}
|
||||
ch := make(chan string)
|
||||
go func() {
|
||||
defer close(ch)
|
||||
|
||||
log.Println("alerting about", event.Name, "at", until.Format("15:04"), "(", time.Until(until), ")")
|
||||
if err := alertAt(ctx, ntfy, until, event.Name, until.Format("15:04")); err != nil {
|
||||
return err
|
||||
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 nil
|
||||
}
|
||||
|
||||
func alertAfter(ctx context.Context, ntfy string, duration string) error {
|
||||
d, err := time.ParseDuration(duration)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return alertAt(ctx, ntfy, time.Now().Add(d), os.Args[0], "alerting after "+d.String())
|
||||
return ch, nil
|
||||
}
|
||||
|
||||
func alertAt(ctx context.Context, ntfy string, deadline time.Time, title, msg string) error {
|
||||
n := int64(time.Until(deadline) / time.Second)
|
||||
bar := progressbar.Default(n)
|
||||
c := time.NewTicker(time.Second)
|
||||
defer c.Stop()
|
||||
func() {
|
||||
ctx, can := context.WithDeadline(ctx, deadline)
|
||||
defer can()
|
||||
for {
|
||||
for ctx.Err() == nil {
|
||||
select {
|
||||
case <-c.C:
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
bar.Add(1)
|
||||
fmt.Printf("\r%s ", deadline.Sub(time.Now()))
|
||||
}
|
||||
}()
|
||||
bar.Finish()
|
||||
bar.Exit()
|
||||
fmt.Println()
|
||||
|
||||
if err := ctx.Err(); err == nil {
|
||||
beeep.Alert(title, msg, "/dev/null")
|
||||
|
||||
Reference in New Issue
Block a user