func (m *Module) CalendarID(id string) *Module {
	c := m.getConfig()
	c.calendarID = id
	m.config.Set(c)
	return m
}
CalendarID sets the ID of the calendar to fetch events for.
package calendar  
import (
	"net/http"
	"time"
	"barista.run/bar"
	"barista.run/base/value"
	"barista.run/oauth"
	"barista.run/outputs"
	"barista.run/timing"
	"golang.org/x/oauth2/google"
	calendar "google.golang.org/api/calendar/v3"
)
type Status string
const (
	StatusUnknown     = Status("")
	StatusConfirmed   = Status("confirmed")
	StatusTentative   = Status("tentative")
	StatusCancelled   = Status("cancelled")
	StatusDeclined    = Status("declined")
	StatusUnresponded = Status("needsAction")
)
type Event struct {
	Start       time.Time
	End         time.Time
	Alert       time.Time
	EventStatus Status
	Response    Status
	Location    string
	Summary     string
}
func (e Event) UntilStart() time.Duration {
	return e.Start.Sub(timing.Now())
}
func (e Event) UntilEnd() time.Duration {
	return e.End.Sub(timing.Now())
}
func (e Event) UntilAlert() time.Duration {
	return e.Alert.Sub(timing.Now())
}
type EventList struct {
	InProgress []Event
	Alerting []Event
	Upcoming []Event
}
type config struct {
	calendarID   string
	lookahead    time.Duration
	showDeclined bool
}
type Module struct {
	oauthConfig *oauth.Config
	config      value.Value  
	scheduler   *timing.Scheduler
	outputFunc  value.Value  
}
func New(clientConfig []byte) *Module {
	conf, err := google.ConfigFromJSON(clientConfig, calendar.CalendarReadonlyScope)
	if err != nil {
		panic("Bad client config: " + err.Error())
	}
	m := &Module{
		oauthConfig: oauth.Register(conf),
		scheduler:   timing.NewScheduler(),
	}
	m.config.Set(config{calendarID: "primary"})
	m.RefreshInterval(10 * time.Minute)
	m.TimeWindow(18 * time.Hour)
	m.Output(func(evts EventList) bar.Output {
		hasEvent := false
		out := outputs.Group()
		for _, e := range evts.InProgress {
			out.Append(outputs.Textf("ends %s: %s",
				e.End.Format("15:04"), e.Summary))
			hasEvent = true
		}
		for _, e := range evts.Alerting {
			out.Append(outputs.Textf("%s: %s",
				e.Start.Format("15:04"), e.Summary))
			hasEvent = true
		}
		if !hasEvent {
			for _, e := range evts.Upcoming {
				out.Append(outputs.Textf("%s: %s",
					e.Start.Format("15:04"), e.Summary))
				break
			}
		}
		return out
	})
	return m
}
var wrapForTest func(*http.Client)
func (m *Module) Stream(sink bar.Sink) {
	client, _ := m.oauthConfig.Client()
	if wrapForTest != nil {
		wrapForTest(client)
	}
	srv, _ := calendar.New(client)
	outf := m.outputFunc.Get().(func(EventList) bar.Output)
	nextOutputFunc, done := m.outputFunc.Subscribe()
	defer done()
	conf := m.getConfig()
	nextConfig, done := m.config.Subscribe()
	defer done()
	renderer := timing.NewScheduler()
	evts, err := fetch(srv, conf)
	for {
		if sink.Error(err) {
			return
		}
		list, refresh := makeEventList(evts)
		if !refresh.IsZero() {
			renderer.At(refresh.Add(time.Duration(1)))
		}
		sink.Output(outf(list))
		select {
		case <-nextOutputFunc:
			outf = m.outputFunc.Get().(func(EventList) bar.Output)
		case <-nextConfig:
			conf = m.getConfig()
			evts, err = fetch(srv, conf)
		case <-m.scheduler.C:
			evts, err = fetch(srv, conf)
		case <-renderer.C:
		}
	}
}
func fetch(srv *calendar.Service, conf config) ([]Event, error) {
	timeMin := timing.Now()
	timeMax := timeMin.Add(conf.lookahead)
	req := srv.Events.List(conf.calendarID)
	req.MaxAttendees(1)
	req.OrderBy("startTime")
	req.SingleEvents(true)
	req.TimeMin(timeMin.Format(time.RFC3339))
	req.TimeMax(timeMax.Format(time.RFC3339))
	req.Fields("items(end,location,start,status,summary,attendees,reminders),defaultReminders")
	res, err := req.Do()
	if err != nil {
		return nil, err
	}
	defaultAlert := getEarliestPopupReminder(res.DefaultReminders)
	events := []Event{}
	for _, e := range res.Items {
		if e.Start.DateTime == "" || e.End.DateTime == "" {
			continue
		}
		start, err := time.Parse(time.RFC3339, e.Start.DateTime)
		if err != nil {
			return nil, err
		}
		end, err := time.Parse(time.RFC3339, e.End.DateTime)
		if err != nil {
			return nil, err
		}
		if end.Before(timeMin) {
			continue
		}
		selfStatus := StatusUnknown
		for _, at := range e.Attendees {
			if at.Self {
				selfStatus = Status(at.ResponseStatus)
			}
		}
		if !conf.showDeclined && selfStatus == StatusDeclined {
			continue
		}
		alert := defaultAlert
		if e.Reminders != nil && !e.Reminders.UseDefault {
			alert = getEarliestPopupReminder(e.Reminders.Overrides)
		}
		eventStatus := Status(e.Status)
		if eventStatus == StatusCancelled {
			continue
		}
		events = append(events, Event{
			Start:       start,
			End:         end,
			Alert:       start.Add(-alert),
			EventStatus: eventStatus,
			Response:    selfStatus,
			Location:    e.Location,
			Summary:     e.Summary,
		})
	}
	return events, nil
}
func getEarliestPopupReminder(rs []*calendar.EventReminder) time.Duration {
	duration := time.Duration(0)
	for _, r := range rs {
		if r.Method != "popup" {
			continue
		}
		rDuration := time.Duration(r.Minutes) * time.Minute
		if rDuration > duration {
			duration = rDuration
		}
	}
	return duration
}
func makeEventList(events []Event) (EventList, time.Time) {
	now := timing.Now()
	var refresh time.Time
	list := EventList{}
	for _, e := range events {
		switch {
		case now.After(e.End):
			continue
		case now.After(e.Start) && e.End.After(now):
			setIfEarlier(&refresh, e.End)
			list.InProgress = append(list.InProgress, e)
		case now.After(e.Alert) && e.Start.After(now):
			setIfEarlier(&refresh, e.Start)
			list.Alerting = append(list.Alerting, e)
		default:
			setIfEarlier(&refresh, e.Alert)
			list.Upcoming = append(list.Upcoming, e)
		}
	}
	return list, refresh
}
func setIfEarlier(target *time.Time, source time.Time) {
	if target.IsZero() || source.Before(*target) {
		*target = source
	}
}
func (m *Module) Output(outputFunc func(EventList) bar.Output) *Module {
	m.outputFunc.Set(outputFunc)
	return m
}
func (m *Module) RefreshInterval(interval time.Duration) *Module {
	m.scheduler.Every(interval)
	return m
}
func (m *Module) getConfig() config {
	return m.config.Get().(config)
}