Repository: deanishe/alfred-gcal Branch: master Commit: cd3271d165e0 Files: 31 Total size: 174.3 KB Directory structure: gitextract__en3xsmk/ ├── .gitignore ├── .golangci.toml ├── LICENCE.txt ├── README.md ├── TODO.taskpaper ├── account.go ├── auth.go ├── cmd_calendars.go ├── cmd_config.go ├── cmd_dates.go ├── cmd_dates_test.go ├── cmd_events.go ├── cmd_open.go ├── cmd_quickadd.go ├── cmd_reload.go ├── cmd_server.go ├── cmd_set.go ├── cmd_update.go ├── env.sh ├── events.go ├── go.mod ├── go.sum ├── icons.afdesign ├── icons.go ├── info.plist ├── magefile.go ├── magic.go ├── main.go ├── modd.conf ├── preview.html └── secret.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ /gcal /build /dist /vendor /.autoenv.zsh /.autoenv_leave.zsh /*_private.go /tags # vim turds [._]*.s[a-v][a-z] [._]*.sw[a-p] [._]s[a-v][a-z] [._]sw[a-p] *~ /tags ================================================ FILE: .golangci.toml ================================================ [run] deadline = "5m" [linters] disable-all = true enable = [ "deadcode", "goconst", "gocritic", "gofmt", "goimports", "gosimple", "ineffassign", "scopelint", "staticcheck", "stylecheck", "unconvert", "unused", "whitespace", ] [linter-settings] [linter-settings.errcheck] check-blank = true check-type-assertions = true [issues] max-same-issues = 50 max-issues-per-linter = 50 exclude = [ # scopelint chokes on filepath.Walk "Using the variable on range scope .* in function literal", # gocritic chokes on if t.Before(today) { "ifElseChain:", ] # vim: set ft=toml ts=4 sw=4 tw=0 et : ================================================ FILE: LICENCE.txt ================================================ The MIT License (MIT) Copyright (c) 2017 Dean Jackson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================
Google Calendar for Alfred ========================== View Google Calendar events in [Alfred][alfred]. Supports multiple accounts. - [Google Calendar for Alfred](#google-calendar-for-alfred) - [Download & installation](#download--installation) - [Usage](#usage) - [Date format](#date-format) - [Add event format](#add-event-format) - [Configuration](#configuration) - [Licensing & thanks](#licensing--thanks) - [Privacy](#privacy) Download & installation ----------------------- Grab the workflow from [GitHub releases][download]. Download the `Google-Calendar-View-X.X.alfredworkflow` file and double-click it to install. Usage ----- When run, the workflow will open Google Calendar in your browser and ask for permission to access your calendars. If you do not grant permission, it won't work. The workflow requests permission to edit your calendars, as this is needed for the "Add New Event" feature (keyword `gnew`). It does not otherwise alter your calendars or events in any way. You will also be prompted to activate some calendars (the workflow will show events from these calendars). You can alter the active calendars or add/remove Google accounts in the settings using keyword `gcalconf`. - `gcal` — Show upcoming events. - `` — Filter list of events. - `↩` — Open event in browser or day in workflow. - `⌘↩` — Open event in Google Maps or Apple Maps (if event has a location). - `⇧` / `⌘Y` — Quicklook event details. - `today` / `tomorrow` / `yesterday` — Show events for the given day. - `` / `↩` / `⌘↩` / `⇧` / `⌘Y` — As above. - `gdate []` — Show one or more dates. See below for query format. - `↩` — Show events for the given day. - `gnew []` — Add a new event in the one of active calendars. (example: Some meeting at Office at 5pm with Ian) - `↩` — Create event in selected calendar. - `gcalconf []` — Show workflow configuration. - `Active Calendars…` — Turn calendars on/off. - `↩` — Toggle calendar on/off. - `Add Account…` — Add a Google account. - `↩` — Open Google login in browser to authorise an account. - `your.email@gmail.com` — Your logged in Google account(s). - `↩` — Remove account. - `Open Locations in Google Maps/Apple Maps` — Choose app to open event locations. - `↩` — Toggle setting between Google Maps & Apple Maps. - `Workflow is up to Date` / `An Update is Available` — Whether a newer version of the workflow is available. - `↩` — Check for or install update. - `Open Locations in XYZ` — Open locations in Google Maps or Apple Maps. - `↩` — Toggle between applications. - `Open Documentation` — Open this page in your brower. - `Get Help` — Visit [the thread for this workflow][forumthread] on [AlfredForum.com][alfredforum]. - `Report Issue` — [Open an issue][issues] on GitHub. - `Clear Cached Calendars & Events` — Remove cached lists of calendars and events. ### Date format ### When viewing dates/events, you can specify and jump to a particular date using the following input format: - `YYYY-MM-DD` — e.g. `2017-12-01` - `YYYYMMDD` — e.g. `20180101` - `[+|-]N[d|w]` — e.g.: - `1`, `1d` or `+1d` for tomorrow - `-1` or `-1d` for yesterday - `3w` for 21 days from now - `-4w` for 4 weeks ago ### Add event format ### The "Add New Event" feature (keyword `gnew`) creates an event using Google Calendar's natural language syntax. This doesn't appear to be properly documented anywhere, but it is pretty powerful. You can specify event title, location, time & duration and repetition. Some examples: - `Wash pants` — creates an event titled "Wash pants" starting now using your default event duration - `Clean pants party tomorrow` — creates an all-day event for tomorrow title "Clean pants party" - `Drink beer every day 2000-2200` — creates an event titled "Drink beer" starting at 8pm, finishing at 10pm, and repeating every day. Configuration ------------- There are a couple of options in the workflow's configuration sheet (the `[x]` button in Alfred Preferences): | Setting | Description | |---------|-------------| | `CALENDAR_APP` | Name of application to open Google Calendar URLs (not map URLs) in. If blank, your default browser is used. | | `EVENT_CACHE_MINS` | Number of minutes to cache event lists before updating from the server. | | `SCHEDULE_DAYS` | The number of days' events to show with the `gcal` keyword. | | `APPLE_MAPS` | Set to `1` to open map links in Apple Maps instead of Google Maps. This option can be toggled from within the workflow's configuration with keyword `gcalconf`. | Licensing & thanks ------------------ This workflow is released under the [MIT Licence][mit]. It is heavily based on the [Google API libraries for Go][google-libs] ([BSD 3-clause licence][google-licence]) and [AwGo][awgo] libraries ([MIT][mit]), and of course, [Google Calendar][gcal]. The icons are from or based on [Font Awesome][awesome] and [Weather Icons][weather] (both [SIL][sil]). Special thanks to [@diffmike][diffmike] for adding the "Add New Event" feature. Privacy ------- The data used and accessed by this workflow are stored exclusively on your own Mac. Nothing is shared with anyone. When you authorise this workflow to access your Google Calendars, the only person you are enabling to read that data is you. [gcal]: https://calendar.google.com/calendar/ [google-libs]: https://github.com/google/google-api-go-client [google-licence]: https://github.com/google/google-api-go-client/blob/master/LICENSE [alfred]: https://alfredapp.com/ [alfredforum]: https://www.alfredforum.com/ [awgo]: https://github.com/deanishe/awgo [forumthread]: https://www.alfredforum.com/topic/11016-google-calendar-view/ [download]: https://github.com/deanishe/alfred-gcal/releases/latest [issues]: https://github.com/deanishe/alfred-gcal/issues [sil]: http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL [mit]: https://opensource.org/licenses/MIT [awesome]: http://fortawesome.github.io/Font-Awesome/ [weather]: https://erikflowers.github.io/weather-icons/ [diffmike]: https://github.com/diffmike ================================================ FILE: TODO.taskpaper ================================================ Bugs: - Clear cache after selected calendars change Currently, workflow shows events from inactive calendars and no events from active ones. Improvements: - Change Keywords to Script Filters Lets magic arguments work, too! New Features: - Integration with Stuart's Google Maps workflow ================================================ FILE: account.go ================================================ // Copyright (c) 2019 Dean Jackson // MIT Licence applies http://opensource.org/licenses/MIT package main import ( "encoding/json" "fmt" "io/ioutil" "log" "net/http" "net/url" "os" "path/filepath" "sort" "strings" "time" aw "github.com/deanishe/awgo" "github.com/deanishe/awgo/util" "github.com/pkg/errors" "golang.org/x/net/context" "golang.org/x/oauth2" "google.golang.org/api/calendar/v3" "google.golang.org/api/option" ) // Account is a Google account. It contains user's email, avatar URL and OAuth2 // token. type Account struct { Name string // Directory account data is stored in Email string // User's email address AvatarURL string // URL of user's Google avatar // Whether account has write permissions. // Early versions of the workflow only requested "read" // permission from the Google Calendar API, and this flag // is used to tell users to re-authenticate with the new // read-write permissions if they try to use the // "Add New Event" feature with an old, read-only access token. ReadWrite bool // Calendars contained by account Calendars []*Calendar // OAuth2 Token *oauth2.Token auth *Authenticator } // NewAccount creates a new account or loads an existing one. func NewAccount(name string) (*Account, error) { var ( a = &Account{Name: name} err error ) if name != "" { if err = wf.Cache.LoadJSON(a.CacheName(), a); err != nil { return nil, errors.Wrap(err, "load account") } } return a, nil } // LoadAccounts reads saved accounts from disk. func LoadAccounts() ([]*Account, error) { var ( accounts = []*Account{} infos []os.FileInfo err error ) if infos, err = ioutil.ReadDir(wf.CacheDir()); err != nil { return nil, errors.Wrap(err, "read accountsDir") } for _, fi := range infos { if fi.IsDir() || !strings.HasSuffix(fi.Name(), ".json") || !strings.HasPrefix(fi.Name(), "account-") { continue } acc := &Account{} if err := wf.Cache.LoadJSON(fi.Name(), acc); err != nil { return nil, errors.Wrap(err, "load account") } log.Printf("[account] loaded %+v", acc) accounts = append(accounts, acc) } return accounts, nil } // CacheName returns the name of Account's cache file. func (a *Account) CacheName() string { return "account-" + a.Name + ".json" } // IconPath returns the path to the cached user avatar. func (a *Account) IconPath() string { return filepath.Join(cacheDirIcons, a.Name+filepath.Ext(a.AvatarURL)) } // Icon returns Account user avatar. func (a *Account) Icon() *aw.Icon { p := a.IconPath() if util.PathExists(p) { return &aw.Icon{Value: p} } return iconAccount } // Authenticator creates a new Authenticator for Account. func (a *Account) Authenticator() *Authenticator { if a.auth == nil { a.auth = NewAuthenticator(a, []byte(secret)) } return a.auth } // Save saves authentication token. func (a *Account) Save() error { if err := wf.Cache.StoreJSON(a.CacheName(), a); err != nil { return errors.Wrap(err, "save account") } log.Printf("[account] saved %q", a.Name) return nil } // Service returns a Calendar Service for this Account. func (a *Account) Service() (*calendar.Service, error) { var ( client *http.Client srv *calendar.Service err error ) if client, err = a.Authenticator().GetClient(); err != nil { return nil, errors.Wrap(err, "get authenticator client") } if srv, err = calendar.NewService(context.Background(), option.WithHTTPClient(client)); err != nil { return nil, errors.Wrap(err, "create new calendar client") } return srv, nil } // FetchCalendars retrieves a list of all calendars in Account. func (a *Account) FetchCalendars() error { var ( srv *calendar.Service ls *calendar.CalendarList cals []*Calendar err error ) if srv, err = a.Service(); err != nil { return errors.Wrap(err, "create service") } if ls, err = srv.CalendarList.List().Do(); err != nil { return errors.Wrap(err, "retrieve calendar list") } for _, entry := range ls.Items { if entry.Hidden { log.Printf("[account] ignoring hidden calendar %q in %q", entry.Summary, a.Name) continue } c := &Calendar{ ID: entry.Id, Title: entry.Summary, Description: entry.Description, Colour: entry.BackgroundColor, AccountName: a.Name, } if entry.SummaryOverride != "" { c.Title = entry.SummaryOverride } cals = append(cals, c) } sort.Sort(CalsByTitle(cals)) a.Calendars = cals return a.Save() } // FetchEvents returns events from the specified calendar. func (a *Account) FetchEvents(cal *Calendar, start time.Time) ([]*Event, error) { var ( end = start.Add(opts.ScheduleDuration()) events = []*Event{} startTime = start.Format(time.RFC3339) endTime = end.Format(time.RFC3339) srv *calendar.Service err error ) log.Printf("[account] account=%q, cal=%q, start=%s, end=%s", a.Name, cal.Title, start, end) if srv, err = a.Service(); err != nil { return nil, a.handleAPIError(err) } evs, err := srv.Events.List(cal.ID). SingleEvents(true). MaxResults(2500). TimeMin(startTime). TimeMax(endTime). OrderBy("startTime").Do() if err != nil { return nil, a.handleAPIError(err) } for _, e := range evs.Items { if e.Start.DateTime == "" { // all-day event continue } var ( start time.Time end time.Time err error ) if start, err = time.Parse(time.RFC3339, e.Start.DateTime); err != nil { log.Printf("[events] ERR: parse start time (%s): %v", e.Start.DateTime, err) continue } if end, err = time.Parse(time.RFC3339, e.End.DateTime); err != nil { log.Printf("[events] ERR: parse end time (%s): %v", e.End.DateTime, err) continue } events = append(events, &Event{ ID: e.Id, IcalUID: e.ICalUID, Title: e.Summary, Description: e.Description, URL: e.HtmlLink, Location: e.Location, Start: start, End: end, Colour: cal.Colour, CalendarID: cal.ID, CalendarTitle: cal.Title, }) } return events, nil } // QuickAdd creates a new event in the passed calendar from Account. func (a *Account) QuickAdd(calendarID string, quick string) error { var ( srv *calendar.Service err error ) if srv, err = a.Service(); err != nil { return errors.Wrap(err, "create service") } if _, err = srv.Events.QuickAdd(calendarID, quick).Do(); err != nil { return errors.Wrap(err, "create new event error") } return err } // Check for OAuth2 error and remove tokens if they've expired/been revoked. func (a *Account) handleAPIError(err error) error { if err2, ok := err.(*url.Error); ok { if err3, ok := err2.Err.(*oauth2.RetrieveError); ok { var resp errorResponse if err4 := json.Unmarshal(err3.Body, &resp); err4 == nil { log.Printf("[events] ERR: OAuth: %s (%s)", resp.Name, resp.Description) err := errorAuthentication{ Name: resp.Name, Description: resp.Description, Err: err3, } if err.Name == "invalid_grant" { log.Printf("[account] clearing invalid token for %q", a.Name) a.Token = nil if err := a.Save(); err != nil { log.Printf("[account] ERR: save %q: %v", a.Name, err) } } return err } } } return err } // for unmarshalling API errors. type errorResponse struct { Name string `json:"error"` Description string `json:"error_description"` } type errorAuthentication struct { Name string Description string Err error } // Error implements error. func (err errorAuthentication) Error() string { return fmt.Sprintf("authentication error: %s (%s)", err.Name, err.Description) } ================================================ FILE: auth.go ================================================ // // Copyright (c) 2017 Dean Jackson // // MIT Licence. See http://opensource.org/licenses/MIT // // Created on 2017-11-25 // package main import ( "crypto/rand" "encoding/json" "fmt" "io" "io/ioutil" "log" "net/http" "os/exec" "sync" "time" "github.com/pkg/errors" "golang.org/x/net/context" "golang.org/x/oauth2" "golang.org/x/oauth2/google" "google.golang.org/api/calendar/v3" ) const ( // URL of local server that receives OAuth2 tokens authServerURL = "localhost:61432" // OAuth2 scope for user's email address userEmailScope = "https://www.googleapis.com/auth/userinfo.email" ) // OAuth2 scopes used by the workflow var scopes = []string{calendar.CalendarEventsScope, calendar.CalendarReadonlyScope, userEmailScope} type response struct { code string err error } // Authenticator creates an authenticated Google API client type Authenticator struct { Secret []byte Account *Account state string client *http.Client mu sync.Mutex // set when authentication fails so other goroutines don't // repeatedly try to log in Failed bool } // NewAuthenticator creates a new Authenticator func NewAuthenticator(acc *Account, secret []byte) *Authenticator { return &Authenticator{Account: acc, Secret: secret} } // GetClient returns an authenticated Google API client func (a *Authenticator) GetClient() (*http.Client, error) { a.mu.Lock() defer a.mu.Unlock() // bail out as previous authentication attempt has failed if a.Failed { return nil, errors.New("authentication failed") } if a.client != nil { return a.client, nil } // generate CSRF token b := make([]byte, 32) _, err := rand.Read(b) if err != nil { return nil, fmt.Errorf("couldn't read random bytes: %v", err) } a.state = fmt.Sprintf("%x", b) ctx := context.Background() cfg, err := google.ConfigFromJSON(a.Secret, scopes...) if err != nil { return nil, errors.Wrap(err, "load config") } var save bool if a.Account.Token == nil { if err = a.tokenFromWeb(cfg); err != nil { a.Failed = true return nil, errors.Wrap(err, "token from web") } a.Account.ReadWrite = cfg.Scopes[0] == calendar.CalendarEventsScope save = true } a.client = cfg.Client(ctx, a.Account.Token) // If Account is empty, fetch user info from Google API if a.Account.Name == "" { if err := a.getUserInfo(); err != nil { return nil, err } save = true } if save { if err = a.Account.Save(); err != nil { return nil, errors.Wrap(err, "save account") } } return a.client, nil } /* // tokenFromFile loads the oauth2 token from a file func (a *Authenticator) tokenFromFile() (*oauth2.Token, error) { f, err := os.Open(a.TokenFile) if err != nil { return nil, fmt.Errorf("open token file: %v", err) } tok := &oauth2.Token{} err = json.NewDecoder(f).Decode(tok) defer f.Close() return tok, err } // saveToken saves an oauth2 token to a file func (a *Authenticator) saveToken(tok *oauth2.Token) error { f, err := os.OpenFile(a.TokenFile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) if err != nil { return fmt.Errorf("open token file: %v", err) } defer f.Close() return json.NewEncoder(f).Encode(tok) } */ // tokenFromWeb initiates web-based authentication and retrieves the OAuth2 token func (a *Authenticator) tokenFromWeb(cfg *oauth2.Config) error { var ( code string token *oauth2.Token err error ) if err = a.openAuthURL(cfg); err != nil { return errors.Wrap(err, "open auth URL") } if code, err = a.codeFromLocalServer(); err != nil { return errors.Wrap(err, "get token from local server") } if token, err = cfg.Exchange(context.Background(), code); err != nil { return errors.Wrap(err, "token from web") } a.Account.Token = token return nil } func (a *Authenticator) getUserInfo() error { var ( resp *http.Response data []byte err error ) if resp, err = a.client.Get("https://accounts.google.com/.well-known/openid-configuration"); err != nil { return fmt.Errorf("get user info: %v", err) } defer resp.Body.Close() if data, err = ioutil.ReadAll(resp.Body); err != nil { return fmt.Errorf("read user response: %v", err) } s := struct { Endpoint string `json:"userinfo_endpoint"` }{} if err = json.Unmarshal(data, &s); err != nil { return fmt.Errorf("parse OpenID JSON: %v", err) } log.Printf("[auth] fetching user info from %s ...", s.Endpoint) if resp, err = a.client.Get(s.Endpoint); err != nil { return fmt.Errorf("read userinfo_endpoint: %v", err) } defer resp.Body.Close() if data, err = ioutil.ReadAll(resp.Body); err != nil { return fmt.Errorf("read userinfo_endpoint response: %v", err) } log.Printf("[auth] response=%s", string(data)) st := struct { Email string `json:"email"` Avatar string `json:"picture"` }{} if err := json.Unmarshal(data, &st); err != nil { return errors.Wrap(err, "unmarshal userinfo") } a.Account.Name = st.Email a.Account.Email = st.Email a.Account.AvatarURL = st.Avatar log.Printf("[auth] fetching user avatar ...") if err := download(a.Account.AvatarURL, a.Account.IconPath()); err != nil { return errors.Wrap(err, "fetch avatar") } return nil } // openAuthURL opens the Google API authentication URL in the default browser func (a *Authenticator) openAuthURL(cfg *oauth2.Config) error { authURL := cfg.AuthCodeURL(a.state, oauth2.AccessTypeOffline, oauth2.ApprovalForce) cmd := exec.Command("/usr/bin/open", authURL) if err := cmd.Run(); err != nil { return fmt.Errorf("open auth URL: %v", err) } return nil } // codeFromLocalServer starts a local webserver to receive the oauth2 token // from Google func (a *Authenticator) codeFromLocalServer() (string, error) { var ( c = make(chan response) mux = http.NewServeMux() srv = &http.Server{ Addr: authServerURL, Handler: mux, } ) go func() { log.Printf("[auth] starting local webserver on %s ...", authServerURL) if err := srv.ListenAndServe(); err != nil { c <- response{err: err} } }() // automatically close server after 3 minutes timeout := time.AfterFunc(time.Minute*3, func() { log.Println("[auth] automatically stopping server after timeout") if err := srv.Shutdown(context.Background()); err != nil && err != http.ErrServerClosed { log.Printf("[error] shutdown: %v", err) c <- response{err: err} return } c <- response{err: errors.New("OAuth server timeout exceeded")} }) mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { vars := req.URL.Query() code := vars.Get("code") state := vars.Get("state") errMsg := vars.Get("error") log.Printf("[auth] oauth2 state=%v", state) log.Printf("[auth] oauth2 code=%s", code) log.Printf("[auth] oauth2 error=%s", errMsg) // Verify state to prevent CSRF if state != a.state { c <- response{err: fmt.Errorf("state mismatch: expected=%s, got=%s", a.state, state)} if _, err := io.WriteString(w, "bad state\n"); err != nil { log.Printf("[error] write server response: %v", err) } return } // authentication failed if errMsg != "" { c <- response{err: errors.New(errMsg)} if _, err := io.WriteString(w, errMsg+"\n"); err != nil { log.Printf("[error] write server response: %v", err) } return } // user rejected if code == "" { c <- response{err: errors.New("user rejected access")} if _, err := io.WriteString(w, "access denied by user\n"); err != nil { log.Printf("[error] write server response: %v", err) } return } c <- response{code: code} if _, err := io.WriteString(w, "ok\n"); err != nil { log.Printf("[error] write server response: %v", err) } }) r := <-c timeout.Stop() // log.Printf("srv=%+v, response=%+v", srv, r) if err := srv.Shutdown(context.Background()); err != nil { log.Printf("shutdown error: %v", err) if err != http.ErrServerClosed { return "", fmt.Errorf("auth webserver: %v", err) } } log.Printf("[auth] local webserver stopped") return r.code, r.err } ================================================ FILE: cmd_calendars.go ================================================ // // Copyright (c) 2017 Dean Jackson // // MIT Licence. See http://opensource.org/licenses/MIT // // Created on 2017-11-25 // package main import ( "log" "os" "os/exec" "strings" aw "github.com/deanishe/awgo" "github.com/pkg/errors" ) var ( errNoActive = errors.New("no active calendars") errNoCalendars = errors.New("no calendars") errNoWritable = errors.New("no writeable calendars") ) // doListCalendars shows a list of available calendars in Alfred. func doListCalendars() error { var ( cals []*Calendar err error ) if cals, err = allCalendars(); err != nil { if err == errNoCalendars { if !wf.IsRunning("update-calendars") { cmd := exec.Command(os.Args[0], "update", "calendars") if err := wf.RunInBackground("update-calendars", cmd); err != nil { return errors.Wrap(err, "run calendar update") } } wf.NewItem("Fetching List of Calendars…"). Subtitle("List will reload shortly"). Valid(false). Icon(ReloadIcon()) wf.Rerun(0.1) wf.SendFeedback() return nil } return err } if len(cals) == 0 && wf.IsRunning("update-calendars") { wf.NewItem("Fetching List of Calendars…"). Subtitle("List will reload shortly"). Valid(false). Icon(ReloadIcon()) wf.Rerun(0.1) wf.SendFeedback() return nil } active, err := activeCalendarIDs() if err != nil && err != errNoActive { return err } for _, c := range cals { on := active[c.ID] icon := iconCalOff if on { icon = iconCalOn } sub := c.Description + " / " + c.AccountName if c.Description == "" { sub = c.AccountName } wf.NewItem(c.Title). Subtitle(sub). Icon(icon). Arg(c.ID). Match(c.Title). Valid(true). Var("action", "toggle"). Var("calendar", c.ID) } wf.NewItem("Back"). Subtitle("Back to configuration"). Icon(iconPrevious). Valid(true). Var("action", "config") if opts.Query != "" { wf.Filter(opts.Query) } wf.WarnEmpty("No Calendars", "Did you log in with the right account?") wf.SendFeedback() return nil } // doListWritableCalendars shows a list of active calendars in Alfred. func doListWritableCalendars() error { var ( cals []*Calendar err error ) if cals, err = writableCalendars(); err != nil { if err == errNoWritable { wf.NewItem("No Writeable Account(s)"). Subtitle("↩ to go to config and re-authenticate account with read-write permission"). Valid(true). Icon(aw.IconWarning). Var("action", "config") wf.SendFeedback() return nil } if err == errNoActive { wf.NewItem("No Active Calendars"). Subtitle("↩ or ⇥ to activate calendars"). Autocomplete("workflow:calendars"). Icon(aw.IconWarning) // TODO: reauth accounts wf.SendFeedback() return nil } if err == errNoCalendars { if !wf.IsRunning("update-calendars") { cmd := exec.Command(os.Args[0], "update", "calendars") if err := wf.RunInBackground("update-calendars", cmd); err != nil { return errors.Wrap(err, "run calendar update") } } wf.NewItem("Fetching List of Calendars…"). Subtitle("List will reload shortly"). Valid(false). Icon(ReloadIcon()) wf.Rerun(0.1) wf.SendFeedback() return nil } return err } for _, c := range cals { query := strings.TrimSpace(opts.Query) sub := c.Description + " / " + c.AccountName if c.Description == "" { sub = c.AccountName } if query != "" { sub = "Create “" + query + "” in " + c.Title } wf.NewItem(c.Title). Subtitle(sub). Icon(ColouredIcon(iconCalendar, c.Colour)). Arg(c.ID). UID(c.ID). Valid(true). Var("action", "create"). Var("quick", opts.Query). Var("calendar", c.ID) } wf.WarnEmpty("No Calendars", "Did you log in with the right account?") wf.SendFeedback() return nil } func allCalendars() ([]*Calendar, error) { var ( jobName = "update-calendars" cals []*Calendar expired bool ) for _, acc := range accounts { if wf.Cache.Expired(acc.CacheName(), opts.MaxAgeCalendar()) { expired = true } cals = append(cals, acc.Calendars...) } if expired { if !wf.IsRunning(jobName) { wf.Rerun(0.1) cmd := exec.Command(os.Args[0], "update", "calendars") if err := wf.RunInBackground(jobName, cmd); err != nil { return nil, err } } } log.Printf("[main] %d calendar(s) in %d account(s)", len(cals), len(accounts)) if len(cals) == 0 { return nil, errNoCalendars } return cals, nil } func activeCalendarIDs() (map[string]bool, error) { var ( IDs []string IDMap = map[string]bool{} name = "active.json" ) if !wf.Cache.Exists(name) { return nil, errNoActive } if err := wf.Cache.LoadJSON(name, &IDs); err != nil { return nil, err } for _, id := range IDs { IDMap[id] = true } if len(IDMap) == 0 { return nil, errNoActive } return IDMap, nil } func activeCalendars() ([]*Calendar, error) { var ( cals []*Calendar all []*Calendar IDs map[string]bool err error ) if IDs, err = activeCalendarIDs(); err != nil { return nil, err } if all, err = allCalendars(); err != nil { return nil, err } if len(all) == 0 { return nil, errNoCalendars } for _, c := range all { if IDs[c.ID] { cals = append(cals, c) } } if len(cals) == 0 { return nil, errNoActive } return cals, nil } func writableCalendars() ([]*Calendar, error) { var ( cals []*Calendar all []*Calendar writeable []*Calendar IDs map[string]bool err error ) if IDs, err = activeCalendarIDs(); err != nil { return nil, err } for _, acc := range accounts { all = append(all, acc.Calendars...) if acc.ReadWrite { writeable = append(writeable, acc.Calendars...) } } if len(all) == 0 { return nil, errNoCalendars } if len(writeable) == 0 { return nil, errNoWritable } for _, c := range writeable { if IDs[c.ID] { cals = append(cals, c) } } if len(cals) == 0 { return nil, errNoActive } return cals, nil } ================================================ FILE: cmd_config.go ================================================ // // Copyright (c) 2017 Dean Jackson // // MIT Licence. See http://opensource.org/licenses/MIT // // Created on 2017-11-25 // package main import ( "io/ioutil" "log" "os" "path/filepath" "strings" aw "github.com/deanishe/awgo" "github.com/pkg/errors" ) // doConfig shows configuration options. func doConfig() error { wf.Var("CALENDAR_APP", "") // Open links in default browser, not CALENDAR_APP if opts.Query == "" { wf.Configure(aw.SuppressUIDs(true)) } if len(accounts) > 0 { wf.NewItem("Active Calendars…"). Subtitle("Turn calendars on/off"). UID("calendars"). Icon(iconCalendars). Valid(true). Var("action", "calendars") wf.NewItem("Add Account…"). Subtitle("Action this item to add a Google account"). UID("add-account"). Autocomplete("workflow:login"). Icon(iconAccountAdd) } else { wf.NewItem("No Accounts Configured"). Subtitle("Action this item to add a Google account"). UID("add-account"). Autocomplete("workflow:login"). Icon(aw.IconWarning) } for _, acc := range accounts { it := wf.NewItem(acc.Name). Subtitle("⌥↩ to remove account / ⌘↩ to re-authenticate"). UID(acc.Name). Arg(acc.Name). Valid(false). Icon(acc.Icon()) it.NewModifier("opt"). Subtitle("Remove account"). Valid(true). Var("action", "logout"). Var("account", acc.Name) it.NewModifier("cmd"). Subtitle("Re-authenticate account with read-write permission"). Valid(true). Var("action", "reauth"). Var("account", acc.Name) } var ( name = "Google Maps" other = "Apple Maps" icon = iconGoogleMaps arg = "apple" ) if opts.UseAppleMaps { name, other = other, name icon = iconAppleMaps arg = "google" } wf.NewItem("Open Locations in "+name). Subtitle("Toggle this setting to use "+other). UID("location"). Arg(arg). Valid(true). Icon(icon). Var("action", "set"). Var("key", "maps"). Var("value", arg) if wf.UpdateAvailable() { wf.NewItem("An Update is Available"). Subtitle("A newer version of the workflow is available"). UID("update"). Autocomplete("workflow:update"). Icon(iconUpdateAvailable). Valid(false) } else { wf.NewItem("Workflow is up to Date"). Subtitle("Action to force update check"). UID("update"). Icon(iconUpdateOK). Valid(true). Var("action", "update") } wf.NewItem("Open Documentation"). Subtitle("Open workflow README in your browser"). UID("docs"). Arg(readmeURL). Valid(true). Icon(iconDocs). Var("action", "open") wf.NewItem("Get Help"). Subtitle("Open alfredforum.com thread in your browser"). UID("forum"). Arg(forumURL). Valid(true). Icon(iconHelp). Var("action", "open") wf.NewItem("Report Issue"). Subtitle("Open GitHub issues in your browser"). UID("issues"). Arg(helpURL). Valid(true). Icon(iconIssue). Var("action", "open") wf.NewItem("Clear Cached Calendars & Events"). Subtitle("Remove cached list of calendars and events"). UID("clear"). Icon(iconDelete). Valid(true). Var("action", "clear") if opts.Query != "" { wf.Filter(opts.Query) } wf.WarnEmpty("No Matches", "Try a different query") wf.SendFeedback() return nil } // doToggle turns a calendar on or off. func doToggle() error { IDs, err := activeCalendarIDs() if err != nil && err != errNoActive { return err } if err == errNoActive { IDs = map[string]bool{} } if IDs[opts.CalendarID] { log.Printf("deactivating calendar %s ...", opts.CalendarID) delete(IDs, opts.CalendarID) } else { log.Printf("activating calendar %s ...", opts.CalendarID) IDs[opts.CalendarID] = true } active := []string{} for ID := range IDs { active = append(active, ID) } if err := wf.Cache.StoreJSON("active.json", active); err != nil { return errors.Wrap(err, "save active calendar list") } // calendars have changed, so delete cached schedules return clearEvents() } // Re-authenticate specified account. func doReauth() error { wf.Configure(aw.TextErrors(true)) log.Printf("[reauth] account=%q", opts.Account) for _, acc := range accounts { if acc.Name == opts.Account { acc.Token = nil if err := acc.Save(); err != nil { return errors.Wrap(err, "reauth: save account") } // retrieve calendar list to trigger authentication if err := acc.FetchCalendars(); err != nil { return errors.Wrap(err, "reauth: fetch calendars") } } } return nil } // doLogout removes an account. func doLogout() error { wf.Configure(aw.TextErrors(true)) log.Printf("[logout] account=%q", opts.Account) deleteMe := map[string]bool{} for _, acc := range accounts { if acc.Name == opts.Account { for _, cal := range acc.Calendars { deleteMe[cal.ID] = true } if err := wf.Cache.Store(acc.CacheName(), nil); err != nil { return errors.Wrap(err, "delete account file") } if err := os.Remove(acc.IconPath()); err != nil && !os.IsNotExist(err) { return errors.Wrap(err, "delete account avatar") } log.Printf("[logout] removed account %q", opts.Account) } } var ( active []string IDs map[string]bool err error ) // Remove active calendars belonging to account if IDs, err = activeCalendarIDs(); err != nil && err != errNoActive { return errors.Wrap(err, "get active calendars") } // No active calendars to change if err == errNoActive || len(deleteMe) == 0 { return nil } for id := range IDs { if !deleteMe[id] { active = append(active, id) } } if err := wf.Cache.StoreJSON("active.json", active); err != nil { return errors.Wrap(err, "save active calendar list") } // delete cached schedules now calendars have changed return clearEvents() } // doClear removes cached calendars and events. func doClear() error { log.Print("clearing cached calendars and events…") wf.Configure(aw.TextErrors(true)) if err := clearEvents(); err != nil { return errors.Wrap(err, "clear cached data") } for _, acc := range accounts { acc.Calendars = []*Calendar{} if err := acc.Save(); err != nil { return errors.Wrap(err, "remove account calendars") } } return nil } // delete cached events. func clearEvents() error { var ( infos []os.FileInfo err error ) if infos, err = ioutil.ReadDir(wf.CacheDir()); err != nil { return errors.Wrap(err, "read cache directory") } for _, fi := range infos { name := fi.Name() if strings.HasPrefix(name, "events-") && strings.HasSuffix(name, ".json") { if err = os.Remove(filepath.Join(wf.CacheDir(), name)); err != nil { return errors.Wrap(err, "delete events cache file") } log.Printf("[cache] deleted %q", name) } } return err } ================================================ FILE: cmd_dates.go ================================================ // // Copyright (c) 2017 Dean Jackson // // MIT Licence. See http://opensource.org/licenses/MIT // // Created on 2017-11-25 // package main import ( "fmt" "regexp" "strconv" "strings" "time" aw "github.com/deanishe/awgo" ) var ( oneDay = time.Hour * 24 oneWeek = oneDay * 7 today = midnight(time.Now()) tomorrow = midnight(today.AddDate(0, 0, 1)) yesterday = midnight(today.AddDate(0, 0, -1)) parseRegex = regexp.MustCompile(`^(\+|-)?(\d+)(d|w)?$`) ) // doDates shows a list of dates in Alfred. func doDates() error { if len(accounts) == 0 { wf.NewItem("No Accounts Configured"). Subtitle("Action this item to add a Google account"). Autocomplete("workflow:login"). Icon(aw.IconWarning) wf.SendFeedback() return nil } var parsed bool if t, ok := parseDate(opts.DateFormat); ok { parsed = true short := t.Format(timeFormat) long := t.Format(timeFormatLong) wf.NewItem(long). Subtitle(relativeDays(t, false)). Arg(short). Autocomplete(short). Valid(true). Icon(iconDefault) } else { for i := -3; i < 4; i++ { var ( t = midnight(today.Add(oneDay * time.Duration(i))) long = t.Format(timeFormatLong) short = t.Format(timeFormat) icon = iconDefault ) if t.Equal(today) { icon = iconCalToday } wf.NewItem(relativeDays(t, true)). Subtitle(short). Match(long + " " + t.Format("Monday")). Arg(short). Autocomplete(short). Valid(true). Icon(icon) } } if !parsed && opts.DateFormat != "" { _ = wf.Filter(opts.DateFormat) } wf.WarnEmpty("Invalid date", "Format is YYYY-MM-DD, YYYMMDD or [+|-]NN[d|w]") wf.SendFeedback() return nil } // Return midnight in local timezone for given Time. func midnight(t time.Time) time.Time { s := t.Local().Format(timeFormat) m, err := time.ParseInLocation(timeFormat, s, time.Local) if err != nil { panic(err) } return m } // parse string into Time. Boolean is true if parsing was successful. func parseDate(s string) (time.Time, bool) { s = strings.TrimSpace(s) if s == "" { return time.Time{}, false } if t, err := time.ParseInLocation(timeFormat, s, time.Local); err == nil { return t, true } if t, err := time.ParseInLocation("20060102", s, time.Local); err == nil { return t, true } // Parse custom format [+|-]NN[d|w] var ( add = true delta time.Duration t time.Time unit = "d" ) m := parseRegex.FindStringSubmatch(s) if m == nil { return time.Time{}, false } // Sign if m[1] == "-" { add = false } // Count n, err := strconv.Atoi(m[2]) if err != nil { return time.Time{}, false } if n == 0 { return today, true } // Optional unit if m[3] != "" { unit = m[3] } // Calculate date if unit == "d" { delta = oneDay * time.Duration(n) } else { delta = oneWeek * time.Duration(n) } if add { t = today.Add(delta) } else { t = today.Add(-delta) } return midnight(t), true } // Return Time as "x day(s) ago" or "in x day(s)" func relativeDays(t time.Time, names bool) string { var ( d time.Duration days int ) if t.Before(today) { d = today.Sub(t) } else if t.After(today) { d = t.Sub(today) } else { return "Today" } days = int(d.Hours() / 24) // Return day name if names { if days == 1 { if t.Before(today) { return "Yesterday" } return "Tomorrow" } return t.Format("Monday") } var ( format string unit = "days" ) // Return in N day(s) or N day(s) ago format = "%d %s ago" if t.After(today) { format = "in %d %s" } if days == 1 { unit = "day" } return fmt.Sprintf(format, days, unit) } // relativeDate returns Yesterday, Today, Tomorrow or long date. func relativeDate(t time.Time) string { t = midnight(t) if t.Equal(today) { return "Today" } if t.Equal(yesterday) { return "Yesterday" } if t.Equal(tomorrow) { return "Tomorrow" } return t.Format("Monday, 2 Jan 2006") } ================================================ FILE: cmd_dates_test.go ================================================ // // Copyright (c) 2017 Dean Jackson // // MIT Licence. See http://opensource.org/licenses/MIT // // Created on 2017-11-25 // package main import "testing" var validFormats = []string{ "2017-11-25", // date strings "20171125", "7", // no units "-7", "1d", // days "+1d", "-1d", "2w", // weeks "+2w", "-2w", } var invalidFormats = []string{ "1m", "2q", "l1d", "*2d", } func TestParseDate(t *testing.T) { tm, ok := parseDate("0") if !tm.Equal(today) || !ok { t.Errorf("zero format failed. tm=%v", tm) } for _, s := range validFormats { tm, ok := parseDate(s) if !ok { t.Errorf("error parsing valid format %q", s) } if tm.IsZero() { t.Errorf("zero time for valid format %q", s) } } for _, s := range invalidFormats { tm, ok := parseDate(s) if ok { t.Errorf("no error parsing invalid format %q", s) } if !tm.IsZero() { t.Errorf("non-zero time for invalid format %q: %v", s, tm) } } } ================================================ FILE: cmd_events.go ================================================ // // Copyright (c) 2017 Dean Jackson // // MIT Licence. See http://opensource.org/licenses/MIT // // Created on 2017-11-25 // package main import ( "fmt" "log" "os" "os/exec" "time" aw "github.com/deanishe/awgo" "github.com/pkg/errors" ) // doEvents shows a list of events in Alfred. func doEvents() error { if len(accounts) == 0 { wf.NewItem("No Accounts Configured"). Subtitle("Action this item to add a Google account"). Autocomplete("workflow:login"). Icon(aw.IconWarning) wf.SendFeedback() return nil } var ( cals []*Calendar err error ) if cals, err = activeCalendars(); err != nil { if err == errNoActive { wf.NewItem("No Active Calendars"). Subtitle("Action this item to choose calendars"). Autocomplete("workflow:calendars"). Icon(aw.IconWarning) wf.SendFeedback() return nil } if err == errNoCalendars { if !wf.IsRunning("update-calendars") { cmd := exec.Command(os.Args[0], "update", "calendars") if err := wf.RunInBackground("update-calendars", cmd); err != nil { return errors.Wrap(err, "run calendar update") } } wf.NewItem("Fetching List of Calendars…"). Subtitle("List will reload shortly"). Valid(false). Icon(ReloadIcon()) wf.Rerun(0.1) wf.SendFeedback() return nil } return err } log.Printf("%d active calendar(s)", len(cals)) var ( all []*Event events []*Event parsed time.Time ) if all, err = loadEvents(opts.StartTime, cals...); err != nil { return errors.Wrap(err, "load events") } // Filter out events after cutoff for _, e := range all { if !opts.ScheduleMode && e.Start.After(opts.EndTime) { break } events = append(events, e) log.Printf("%s", e.Title) } if len(all) == 0 && wf.IsRunning("update-events") { wf.NewItem("Fetching Events…"). Subtitle("Results will refresh shortly"). Icon(ReloadIcon()). Valid(false) wf.Rerun(0.1) } log.Printf("%d event(s) for %s", len(events), opts.StartTime.Format(timeFormat)) if t, ok := parseDate(opts.Query); ok { parsed = t } if len(events) == 0 && opts.Query == "" { wf.NewItem(fmt.Sprintf("No Events on %s", opts.StartTime.Format(timeFormatLong))). Icon(ColouredIcon(iconCalendar, yellow)) } var day time.Time for _, e := range events { // Show day indicator if this is the first event of a given day if opts.ScheduleMode && midnight(e.Start).After(day) { day = midnight(e.Start) wf.NewItem(day.Format(timeFormatLong)). Arg(day.Format(timeFormat)). Valid(true). Icon(iconDay) } icon := ColouredIcon(iconCalendar, e.Colour) sub := fmt.Sprintf("%s – %s / %s", e.Start.Local().Format(hourFormat), e.End.Local().Format(hourFormat), e.CalendarTitle) if e.Location != "" { sub = sub + " / " + e.Location } it := wf.NewItem(e.Title). Subtitle(sub). Icon(icon). Arg(e.URL). Quicklook(previewURL(opts.StartTime, e.ID)). Valid(true). Var("action", "open") if e.Location != "" { app := "Google Maps" if opts.UseAppleMaps { app = "Apple Maps" } icon := ColouredIcon(iconMap, e.Colour) it.NewModifier("cmd"). Subtitle("Open in "+app). Arg(mapURL(e.Location)). Valid(true). Icon(icon). Var("CALENDAR_APP", "") // Don't open Maps URLs in CALENDAR_APP } } if !opts.ScheduleMode { // Navigation items prev := opts.StartTime.AddDate(0, 0, -1) wf.NewItem("Previous: "+relativeDate(prev)). Icon(iconPrevious). Arg(prev.Format(timeFormat)). Valid(true). Var("action", "date") next := opts.StartTime.AddDate(0, 0, 1) wf.NewItem("Next: "+relativeDate(next)). Icon(iconNext). Arg(next.Format(timeFormat)). Valid(true). Var("action", "date") } if opts.Query != "" { wf.Filter(opts.Query) } if !parsed.IsZero() { s := parsed.Format(timeFormat) wf.NewItem(parsed.Format(timeFormatLong)). Subtitle(relativeDays(parsed, false)). Arg(s). Autocomplete(s). Valid(true). Icon(iconDefault) } wf.WarnEmpty("No Matching Events", "Try a different query?") wf.SendFeedback() return nil } // loadEvents loads events for given date calendar(s) from cache or server. func loadEvents(t time.Time, cal ...*Calendar) ([]*Event, error) { var ( events = []*Event{} dateStr = t.Format(timeFormat) name = fmt.Sprintf("events-%s.json", dateStr) jobName = "update-events" ) if wf.Cache.Expired(name, opts.MaxAgeEvents()) { wf.Rerun(0.1) if !wf.IsRunning(jobName) { cmd := exec.Command(os.Args[0], "update", "events", dateStr) if err := wf.RunInBackground(jobName, cmd); err != nil { return nil, err } } } if wf.Cache.Exists(name) { if err := wf.Cache.LoadJSON(name, &events); err != nil { return nil, err } } // Set map URL for _, e := range events { e.MapURL = mapURL(e.Location) } return events, nil } ================================================ FILE: cmd_open.go ================================================ // // Copyright (c) 2017 Dean Jackson // // MIT Licence. See http://opensource.org/licenses/MIT // // Created on 2017-11-30 // package main import ( "log" "os/exec" aw "github.com/deanishe/awgo" ) // Open URL in specified app or in default. func doOpen() error { wf.Configure(aw.TextErrors(true)) args := []string{} if opts.App != "" { log.Printf("[open] opening \"%s\" in \"%s\"…", opts.URL, opts.App) args = append(args, "-a", opts.App) } else { log.Printf("[open] opening \"%s\" in default browser…", opts.URL) } args = append(args, opts.URL) cmd := exec.Command("/usr/bin/open", args...) return cmd.Run() } ================================================ FILE: cmd_quickadd.go ================================================ // // Copyright (c) 2017 Dean Jackson // // MIT Licence. See http://opensource.org/licenses/MIT // // Created on 2019-04-03 // package main import ( "log" ) // quickAdd check if there are configured accounts and pass data to create an event. func quickAdd() error { log.Println("Creating event", opts.Quick, opts.CalendarID) if err := createEvent(opts.Quick, opts.CalendarID); err != nil { return err } if err := doUpdateEvents(); err != nil { return err } return nil } // createEvent looks for account by calendar ID and create new event in that account. func createEvent(quick string, calendarID string) error { for _, acc := range accounts { for _, c := range acc.Calendars { if c.ID == calendarID { return acc.QuickAdd(calendarID, quick) } } } return nil } ================================================ FILE: cmd_reload.go ================================================ // // Copyright (c) 2017 Dean Jackson // // MIT Licence. See http://opensource.org/licenses/MIT // // Created on 2017-11-30 // package main // Show reload bar. func doReload() error { wf.Rerun(0.1) wf.NewItem("Progress…"). Icon(ReloadIcon()) wf.SendFeedback() return nil } ================================================ FILE: cmd_server.go ================================================ // // Copyright (c) 2017 Dean Jackson // // MIT Licence. See http://opensource.org/licenses/MIT // // Created on 2017-11-26 // package main import ( "context" "html/template" "io" "log" "net/http" "net/url" "path/filepath" "sync" "time" ) const ( previewServerURL = "localhost:61433" quitAfter = 90 * time.Second ) // previewURL returns a preview server URL. func previewURL(t time.Time, eventID string) string { u, _ := url.Parse("http://" + previewServerURL) v := u.Query() v.Set("date", midnight(t).Format(timeFormat)) v.Set("event", eventID) u.RawQuery = v.Encode() return u.String() } // doStartServer starts the preview server. func doStartServer() error { log.Printf("[preview] starting preview server on %s ...", previewServerURL) var ( lastRequest = time.Now() mu = sync.Mutex{} c = make(chan struct{}) templates = template.Must(template.ParseFiles(filepath.Join(wf.Dir(), "preview.html"))) mux = http.NewServeMux() srv = &http.Server{ Addr: previewServerURL, Handler: mux, } ) go func() { if err := srv.ListenAndServe(); err != nil { if err == http.ErrServerClosed { log.Print("[preview] server stopped") } else { log.Printf("[preview] ERR: server failed: %v", err) } } c <- struct{}{} }() go func() { c := time.Tick(30 * time.Second) for now := range c { mu.Lock() d := now.Sub(lastRequest) mu.Unlock() log.Printf("[preview] %0.0fs since last request", d.Seconds()) if d >= quitAfter { if err := srv.Shutdown(context.Background()); err != nil { log.Printf("[preview] server shutdown error: %v", err) } // log.Printf("[preview] server stopped") } } }() mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { defer func() { mu.Lock() lastRequest = time.Now() mu.Unlock() }() var ( v = req.URL.Query() dateStr = v.Get("date") eventID = v.Get("event") event *Event ) log.Printf("[preview] date=%s, event=%s", dateStr, eventID) // Load events t, err := time.Parse(timeFormat, dateStr) if err != nil { if _, err := io.WriteString(w, "bad date\n"); err != nil { log.Printf("[error] write server response: %v", err) } return } cals, err := activeCalendars() if err != nil { log.Printf("[preview] ERR: load active calendars: %v", err) return } log.Printf("[preview] %d active calendar(s)", len(cals)) events, err := loadEvents(t, cals...) if err != nil { log.Printf("[preview] ERR: load events: %v", err) return } for _, e := range events { if e.ID == eventID { event = e event.MapURL = mapURL(event.Location) break } } if event == nil { if err := templates.ExecuteTemplate(w, "fail", eventID); err != nil { log.Printf(`[preview] ERR: execute template "fail": %v`, err) } return } if err := templates.ExecuteTemplate(w, "event", event); err != nil { log.Printf(`[preview] ERR: execute template "event": %v`, err) } }) <-c return nil } ================================================ FILE: cmd_set.go ================================================ // Copyright (c) 2019 Dean Jackson // MIT Licence applies http://opensource.org/licenses/MIT package main import ( "fmt" "log" aw "github.com/deanishe/awgo" ) // Change a setting. func doSet() error { wf.Configure(aw.TextErrors(true)) log.Printf("[set] key=%q, value=%q", opts.Key, opts.Value) switch opts.Key { case "maps": value := "1" if opts.Value == "google" { value = "0" } return wf.Config.Set("APPLE_MAPS", value, true).Do() default: return fmt.Errorf("unknown config key: %s", opts.Key) } } ================================================ FILE: cmd_update.go ================================================ // // Copyright (c) 2017 Dean Jackson // // MIT Licence. See http://opensource.org/licenses/MIT // // Created on 2017-11-25 // package main import ( "fmt" "io/ioutil" "log" "os" "path/filepath" "sort" "strings" "sync" "time" aw "github.com/deanishe/awgo" "github.com/deanishe/awgo/util" "github.com/pkg/errors" ) // Check if a new version of the workflow is available. func doUpdateWorkflow() error { wf.Configure(aw.TextErrors(true)) log.Print("[update] checking for new version of workflow…") return wf.CheckForUpdate() } // Fetch and cache list of calendars. func doUpdateCalendars() error { var ( acc *Account err error ) wf.Configure(aw.TextErrors(true)) log.Print("[update] reloading calendars…") if len(accounts) == 0 { log.Print("[update] no Google accounts configured") } for _, acc = range accounts { if err = acc.FetchCalendars(); err != nil { return err } if !util.PathExists(acc.IconPath()) { if err := download(acc.AvatarURL, acc.IconPath()); err != nil { return errors.Wrap(err, "fetch account avatar") } } log.Printf("[update] %d calendar(s) in account %q", len(acc.Calendars), acc.Name) } return nil } // Fetch events for a specified date. func doUpdateEvents() error { wf.Configure(aw.TextErrors(true)) var ( name = fmt.Sprintf("events-%s.json", opts.StartTime.Format(timeFormat)) cals []*Calendar events []*Event err error ) log.Printf("[update] fetching events for %s ...", opts.StartTime.Format(timeFormat)) if err := clearOldFiles(); err != nil { log.Printf("[update] ERR: delete old cache files: %v", err) } if cals, err = activeCalendars(); err != nil { return err } if len(accounts) == 0 { log.Print("[update] no Google accounts configured") return nil } if len(cals) == 0 { log.Print("[update] no active calendars") return nil } log.Printf("[update] %d active calendar(s)", len(cals)) // Fetch events in parallel var ( ch = make(chan *Event) wg sync.WaitGroup wanted = make(map[string]bool, len(cals)) // IDs of calendars to update ) for _, c := range cals { wanted[c.ID] = true } wg.Add(len(cals)) for _, acc := range accounts { for _, c := range acc.Calendars { if _, ok := wanted[c.ID]; !ok { continue } go func(c *Calendar, acc *Account) { defer wg.Done() evs, err := acc.FetchEvents(c, opts.StartTime) if err != nil { log.Printf("[update] ERR: fetching events for calendar %q: %v", c.Title, err) return } log.Printf("[update] %d event(s) in calendar %q", len(evs), c.Title) for _, e := range evs { ch <- e } }(c, acc) } } // Close channel when all goroutines are done go func() { wg.Wait() close(ch) }() colours := map[string]bool{} for e := range ch { log.Printf("[update] %s", e) events = append(events, e) colours[e.Colour] = true } sort.Sort(EventsByStart(events)) if err := wf.Cache.StoreJSON(name, events); err != nil { return err } // Ensure icons exist in all colours for clr := range colours { _ = ColouredIcon(iconCalendar, clr) _ = ColouredIcon(iconMap, clr) } return nil } // Remove events-* files and icons older than two weeks. func clearOldFiles() error { var ( cutoff = time.Now().AddDate(0, 0, -14) dirs = []string{} ) err := filepath.Walk(wf.CacheDir(), func(path string, fi os.FileInfo, err error) error { if err != nil { return err } if fi.Name() == "_aw" && fi.IsDir() { return filepath.SkipDir } if fi.IsDir() { dirs = append(dirs, path) return nil } if fi.ModTime().After(cutoff) { return nil } ext := filepath.Ext(path) if (strings.HasPrefix(fi.Name(), "events-") && ext == ".json") || ext == ".png" { if err := os.Remove(path); err != nil { log.Printf("[cache] ERR: delete %q: %v", path, err) return err } } return nil }) if err != nil { return err } // Remove empty directories. Sort in reverse order so sub-directories are // before their parents. sort.Sort(sort.Reverse(sort.StringSlice(dirs))) Outer: for _, dir := range dirs { infos, err := ioutil.ReadDir(dir) if err != nil { log.Printf("[cache] ERR: open dir %s: %v", dir, err) return err } // ignore dotfiles for _, fi := range infos { if strings.HasPrefix(fi.Name(), ".") { continue } // rel, _ := filepath.Rel(wf.CacheDir(), dir) // log.Printf("[cache] %s -- %d item(s)", rel, len(infos)) continue Outer } if err := os.RemoveAll(dir); err != nil { log.Printf("[cache] ERR: delete dir %s: %v", dir, err) return err } log.Printf("[cache] deleted dir: %s", util.PrettyPath(dir)) } return nil } ================================================ FILE: env.sh ================================================ # When sourced, creates an Alfred-like environment needed by modd # and ./bin/build (which sources the file itself) # getvar | Read a value from info.plist getvar() { local v="$1" /usr/libexec/PlistBuddy -c "Print :$v" info.plist } export alfred_workflow_bundleid=$( getvar "bundleid" ) export alfred_workflow_version=$( getvar "version" ) export alfred_workflow_name=$( getvar "name" ) export alfred_workflow_cache="${HOME}/Library/Caches/com.runningwithcrayons.Alfred/Workflow Data/${alfred_workflow_bundleid}" export alfred_workflow_data="${HOME}/Library/Application Support/Alfred/Workflow Data/${alfred_workflow_bundleid}" if [[ ! -f "$HOME/Library/Preferences/com.runningwithcrayons.Alfred.plist" ]]; then export alfred_workflow_cache="${HOME}/Library/Caches/com.runningwithcrayons.Alfred-3/Workflow Data/${alfred_workflow_bundleid}" export alfred_workflow_data="${HOME}/Library/Application Support/Alfred 3/Workflow Data/${alfred_workflow_bundleid}" export alfred_version="3.8.1" fi export SCHEDULE_DAYS=$( getvar "variables:SCHEDULE_DAYS" ) export EVENT_CACHE_MINS=$( getvar "variables:EVENT_CACHE_MINS" ) ================================================ FILE: events.go ================================================ // // Copyright (c) 2017 Dean Jackson // // MIT Licence. See http://opensource.org/licenses/MIT // // Created on 2017-11-25 // package main import ( "fmt" "net/url" "time" ) const ( gMapsURL = "https://www.google.com/maps/search/?api=1" aMapsURL = "http://maps.apple.com/" ) // Calendar is a Google Calendar type Calendar struct { ID string // Calendar ID Title string // Calendar title Description string // Calendar description Colour string // CSS hex colour of calendar AccountName string // Name of account this calendar belongs to } // CalsByTitle sorts a slice of Calendars by title type CalsByTitle []*Calendar func (s CalsByTitle) Len() int { return len(s) } func (s CalsByTitle) Less(i, j int) bool { return s[i].Title < s[j].Title } func (s CalsByTitle) Swap(i, j int) { s[i], s[j] = s[j], s[i] } // Event is a calendar event type Event struct { ID string // Event ID IcalUID string // Cross-platform UID Title string // Event title Description string // Event summary/description URL string // Event URL MapURL string // Google Maps URL Location string // Where the event takes place Start time.Time // Time event started End time.Time // Time event finished Colour string // CSS hex colour of event CalendarID string // Calendar event belongs to CalendarTitle string // Title of calendar event belongs to } // Duration returns the duration of the Event func (e *Event) Duration() time.Duration { return e.End.Sub(e.Start) } func (e *Event) String() string { date := e.Start.Format("2/1 at 15:04") return fmt.Sprintf("\"%s\" on %s for %0.0fm", e.Title, date, e.Duration().Minutes()) } // EventsByStart sorts a slice of Events by start time. type EventsByStart []*Event func (s EventsByStart) Len() int { return len(s) } func (s EventsByStart) Less(i, j int) bool { return s[i].Start.Before(s[j].Start) } func (s EventsByStart) Swap(i, j int) { s[i], s[j] = s[j], s[i] } // URL that points to location on Google Maps or Apple Maps. func mapURL(location string) string { if location == "" { return "" } if opts.UseAppleMaps { return appleMapsURL(location) } return googleMapsURL(location) } func googleMapsURL(location string) string { u, _ := url.Parse(gMapsURL) v := u.Query() v.Set("query", location) u.RawQuery = v.Encode() return u.String() } func appleMapsURL(location string) string { u, _ := url.Parse(aMapsURL) v := u.Query() v.Set("address", location) u.RawQuery = v.Encode() return u.String() } ================================================ FILE: go.mod ================================================ module github.com/deanishe/alfred-gcal require ( cloud.google.com/go v0.61.0 // indirect github.com/deanishe/awgo v0.25.0 github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 github.com/magefile/mage v1.10.0 github.com/pkg/errors v0.9.1 golang.org/x/net v0.0.0-20200707034311-ab3426394381 golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae // indirect google.golang.org/api v0.29.0 google.golang.org/genproto v0.0.0-20200720141249-1244ee217b7e // indirect ) replace github.com/golang/lint => golang.org/x/lint v0.0.0-20190409202823-959b441ac422 go 1.13 ================================================ FILE: go.sum ================================================ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0 h1:eOI3/cP2VTU6uZLDYAoic+eyzzB9YyGmJ7eIjl8rOPg= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= cloud.google.com/go v0.61.0 h1:NLQf5e1OMspfNT1RAHOB3ublr1TW3YTXO8OiWwVjK2U= cloud.google.com/go v0.61.0/go.mod h1:XukKJg4Y7QsUu0Hxg3qQKUWR4VuWivmyMK2+rUyxAqw= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/bmatcuk/doublestar v1.3.1 h1:rT8rxDPsavp9G+4ZULzqhhUSaI/OPsTZNG88Z3i0xvY= github.com/bmatcuk/doublestar v1.3.1/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/deanishe/awgo v0.25.0 h1:r6/eJFETRB6Zex10XbjixSASGlc2a484b+FgRRAYY9g= github.com/deanishe/awgo v0.25.0/go.mod h1:reRZEwXsMuJISYxmdqkn24KrHN+WV/ZQzRBXTtKDPHY= github.com/deanishe/go-env v0.4.0 h1:tpu14o16gJGTN/w2gxntwxu2l5Eby30jSrnlgOfjzwk= github.com/deanishe/go-env v0.4.0/go.mod h1:RgEcGAqdRnt8ybQteAbv1Ys2lWIRE7TlgON/sbdjuaY= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 h1:bWDMxwH3px2JBh6AyO7hdCn/PkvCZXii8TGj7sbtEbQ= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 h1:ZgQEtGgCBiWRM39fZuwSd1LwSqqSW0hOdXCYYDX0R3I= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7 h1:5ZkaAPbicIKTF2I64qf5Fh8Aa83Q/dnOafMYV0OMwjA= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/magefile/mage v1.10.0 h1:3HiXzCUY12kh9bIuyXShaVe529fJfyqoVM42o/uom2g= github.com/magefile/mage v1.10.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0 h1:C9hSCOW830chIVkdja34wa6Ky+IzWllkUinR+BtRZd4= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2 h1:75k/FF0Q2YM8QYo07VPddOLBslDt1MZOdEslOHvmzAs= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4 h1:LYy1Hy3MJdrCdMwwzxA/dRok4ejH+RwNGbuoD9fCjto= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09 h1:KaQtG+aDELoNmXYas3TVkGNYRuq8JQ1aa7LJt8EXVyo= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f h1:Bl/8QSvNqXvPGPGXa2z5xUTmV7VDcZyvRZ+QQXkXTZQ= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae h1:Ih9Yo4hSPImZOpfGuA4bR/ORKTAbhZo2AbWNRCnevdo= golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200713011307-fd294ab11aed/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0 h1:9sdfJOzWlkqPltHAuzT2Cp+yrBeY1KRVYgms8soxMwM= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.9.0 h1:jbyannxz0XFD3zdjgrSUsaJbgpH4eTrkdhRChkHPfO8= google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.15.0 h1:yzlyyDW/J0w8yNFJIhiAJy4kq74S+1DOLdawELNxFMA= google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.29.0 h1:BaiDisFir8O4IJxvAabCGGkQ6yCJegNQqSVoYUNAnbk= google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= google.golang.org/appengine v1.1.0 h1:igQkv0AAhEIvTEpD5LIpAfav2eeVO9HBTjvKHVJPRSs= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1 h1:QzqyMA1tlu6CgqCDUtU9V+ZKhLFT2dkJuANu5QaxI3I= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= google.golang.org/genproto v0.0.0-20200711021454-869866162049 h1:YFTFpQhgvrLrmxtiIncJxFXeCyq84ixuKWVCaCAi9Oc= google.golang.org/genproto v0.0.0-20200711021454-869866162049/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200720141249-1244ee217b7e h1:KoSCpgvphmtnpVycBCtfV9hNdHbInsqdx4gqnDPQCkg= google.golang.org/genproto v0.0.0-20200720141249-1244ee217b7e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0 h1:AzbTB6ux+okLTzP8Ru1Xs41C303zdcfEht7MQnYJt5A= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.26.0 h1:2dTRdpdFEEhJYQD8EMLB61nnrzSCTbG38PhqdhvOltg= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.30.0 h1:M5a8xTlYTxwMn5ZFkwhRabsygDY5G8TYLyQDBxJNAxE= google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= howett.net/plist v0.0.0-20200419221736-3b63eb3a43b5 h1:AQkaJpH+/FmqRjmXZPELom5zIERYZfwTjnHpfoVMQEc= howett.net/plist v0.0.0-20200419221736-3b63eb3a43b5/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= ================================================ FILE: icons.go ================================================ // // Copyright (c) 2017 Dean Jackson // // MIT Licence. See http://opensource.org/licenses/MIT // // Created on 2017-11-26 // package main import ( "encoding/hex" "fmt" "image" "image/color" "image/draw" "image/png" "io" "log" "net" "net/http" "os" "path/filepath" "time" aw "github.com/deanishe/awgo" "github.com/deanishe/awgo/util" "github.com/pkg/errors" ) var ( // Static icons iconAccount = &aw.Icon{Value: "icons/account.png"} iconAccountAdd = &aw.Icon{Value: "icons/account-add.png"} iconDefault = &aw.Icon{Value: "icon.png"} // Workflow icon iconCalendar = &aw.Icon{Value: "icons/calendar.png"} iconCalendars = &aw.Icon{Value: "icons/calendars.png"} iconCalOff = &aw.Icon{Value: "icons/calendar-off.png"} iconCalOn = &aw.Icon{Value: "icons/calendar-on.png"} iconCalToday = &aw.Icon{Value: "icons/calendar-today.png"} iconDay = &aw.Icon{Value: "icons/day.png"} iconDelete = &aw.Icon{Value: "icons/trash.png"} iconDocs = &aw.Icon{Value: "icons/docs.png"} iconIssue = &aw.Icon{Value: "icons/issue.png"} iconHelp = &aw.Icon{Value: "icons/help.png"} iconMap = &aw.Icon{Value: "icons/map.png"} iconNext = &aw.Icon{Value: "icons/next.png"} iconPrevious = &aw.Icon{Value: "icons/previous.png"} iconLoading = &aw.Icon{Value: "icons/loading.png"} iconUpdateOK = &aw.Icon{Value: "icons/update-ok.png"} iconUpdateAvailable = &aw.Icon{Value: "icons/update-available.png"} iconWarning = &aw.Icon{Value: "icons/warning.png"} iconAppleMaps = &aw.Icon{Value: "/Applications/Maps.app", Type: aw.IconTypeFileIcon} iconGoogleMaps = &aw.Icon{Value: "icons/google-maps.png"} ) func init() { aw.IconWarning = iconWarning // Maps.app has moved on Catalina if util.PathExists("/System/Applications/Maps.app") { iconAppleMaps.Value = "/System/Applications/Maps.app" } } // ColouredIcon returns a version of icon in the given colour. If no colour // is specified or something goes wrong, icon is simply returned. func ColouredIcon(icon *aw.Icon, colour string) *aw.Icon { var ( c color.RGBA path string err error ) if c, err = ParseHexColour(colour); err != nil { log.Printf("[icons] ERR: %s", err) return icon } path = iconCachePath(icon, c) if util.PathExists(path) { return &aw.Icon{Value: path} } if err = generateIcon(icon.Value, path, c); err != nil { log.Printf("[icons] ERR: generate icon: %v", err) return icon } return &aw.Icon{Value: path} } var client = &http.Client{ Transport: &http.Transport{ Dial: (&net.Dialer{ Timeout: 60 * time.Second, KeepAlive: 60 * time.Second, }).Dial, TLSHandshakeTimeout: 30 * time.Second, ResponseHeaderTimeout: 30 * time.Second, ExpectContinueTimeout: 10 * time.Second, }, } // Save contents of URL to path. func download(url, path string) error { r, err := client.Get(url) if err != nil { return err } defer r.Body.Close() log.Printf("[%d] %s", r.StatusCode, url) if r.StatusCode > 299 { return fmt.Errorf("bad HTTP response: [%d] %s", r.StatusCode, url) } f, err := os.Create(path) if err != nil { return err } defer f.Close() if _, err := io.Copy(f, r.Body); err != nil { return err } log.Printf("[icons] saved %q to %q\n", url, path) return nil } func generateIcon(src, dest string, c color.RGBA) error { // defer util.Timed(time.Now(), "generate icon") var ( f *os.File mask image.Image err error ) if f, err = os.Open(src); err != nil { return errors.Wrap(err, "open file") } defer f.Close() if mask, _, err = image.Decode(f); err != nil { return errors.Wrap(err, "decode image") } img := image.NewRGBA(mask.Bounds()) draw.DrawMask(img, img.Bounds(), &image.Uniform{c}, image.Point{}, mask, image.Point{}, draw.Src) if f, err = os.Create(dest); err != nil { return errors.Wrap(err, "create file") } defer f.Close() if err = png.Encode(f, img); err != nil { return errors.Wrap(err, "write PNG data") } rel, _ := filepath.Rel(wf.CacheDir(), dest) log.Printf("[icons] new icon: %s", rel) return nil } func iconCachePath(i *aw.Icon, c color.RGBA) string { name := filepath.Base(i.Value) dir := fmt.Sprintf("%02X/%02X/%02X/%02X", c.R, c.G, c.B, c.A) dir = filepath.Join(cacheDirIcons, dir) util.MustExist(dir) return filepath.Join(dir, name) } // ParseHexColour parses a CSS hex (e.g. #ffffff) to RGBA. // // Input must be with preceding # and have 6 (RGB) or 8 (RGBA) characters. func ParseHexColour(s string) (color.RGBA, error) { var ( // default to 100% opaque c = color.RGBA{A: 0xff} err error ) if s[0] == '#' { s = s[1:] } b, err := hex.DecodeString(s) if err != nil { return c, fmt.Errorf("invalid colour: %q", s) } switch len(b) { case 3: c.R = b[0] c.G = b[1] c.B = b[2] case 4: c.R = b[0] c.G = b[1] c.B = b[2] c.A = b[3] default: err = fmt.Errorf("invalid colour: %q", s) } return c, err } // ReloadIcon returns a spinner icon. It rotates by 15 deg on every // subsequent call. Use with wf.Reload(0.1) to implement an animated // spinner. func ReloadIcon() *aw.Icon { var ( step = 15 max = (45 / step) - 1 current = wf.Config.GetInt("RELOAD_PROGRESS", 0) next = current + 1 ) if next > max { next = 0 } log.Printf("progress: current=%d, next=%d", current, next) wf.Var("RELOAD_PROGRESS", fmt.Sprintf("%d", next)) if current == 0 { return iconLoading } return &aw.Icon{Value: fmt.Sprintf("icons/loading-%d.png", current*step)} } ================================================ FILE: info.plist ================================================ bundleid net.deanishe.alfred.gcal connections 0553156D-6606-42C4-8BE8-18AE49A7A6D6 destinationuid 303E565E-8048-4ED5-BD89-7E58DF94BF3A modifiers 0 modifiersubtext vitoclose 09A5FA54-F59A-45B7-8B66-A08BC6AB8EAC destinationuid CD8F2AB1-13E0-47E1-BFCA-701444818091 modifiers 0 modifiersubtext vitoclose 0D457EF4-A96F-4760-A4DC-07F46AE409C6 destinationuid 8C1ADFA7-4A9A-4218-9125-F0D4C2763FEC modifiers 0 modifiersubtext vitoclose 11608658-A256-4625-AAD6-517E03644231 destinationuid 511603A0-C36D-4AB7-9520-0BA422CE5F05 modifiers 0 modifiersubtext vitoclose 14C00640-8D74-4090-AB64-5BDEA4489D4D destinationuid CD8F2AB1-13E0-47E1-BFCA-701444818091 modifiers 0 modifiersubtext vitoclose 2F7191FB-FE4C-4B0F-832C-A9BA3DE8F841 303E565E-8048-4ED5-BD89-7E58DF94BF3A destinationuid 9FA29DC3-2E67-45B6-B93B-F33F57EDF7E7 modifiers 0 modifiersubtext vitoclose destinationuid F5C23C7D-94BA-400C-8004-EC304CE8818D modifiers 0 modifiersubtext vitoclose 38618982-EFBD-4CEE-AB65-D33F0BBD3C54 destinationuid FF2770C7-3B9E-49CF-9F1D-9F2A603CB11C modifiers 0 modifiersubtext vitoclose 3CCB9C03-CCBD-43F3-A51F-C95DBBD1A21C destinationuid 862EBB67-6E29-4467-AFE1-7E6717945CB6 modifiers 0 modifiersubtext vitoclose 3F938397-2CD9-45EB-B0CD-FC962DC9031F destinationuid EAF06D56-D2F1-4FB9-B0D7-89D494AA865B modifiers 0 modifiersubtext vitoclose 4C28D7FF-5BFC-4A3B-BC5C-738D3069B9E1 destinationuid 60FDD3AD-D600-4F4E-A43A-B413E83FC298 modifiers 0 modifiersubtext vitoclose 55D10CF4-8457-4AE5-9DC0-64A7E262D61D destinationuid 2512097E-AB92-489E-93AF-0146592CB0D4 modifiers 0 modifiersubtext vitoclose 57F4280F-D680-47D8-B5CA-ED9FFDA29082 destinationuid 36D47E80-4BA8-4158-8B18-C1EDB477E6D7 modifiers 0 modifiersubtext vitoclose 5C03EE71-D6D9-42F5-B608-403F47234C56 destinationuid 3F938397-2CD9-45EB-B0CD-FC962DC9031F modifiers 0 modifiersubtext vitoclose 62F44B78-0D8E-4215-BB14-E3950B214795 destinationuid 303E565E-8048-4ED5-BD89-7E58DF94BF3A modifiers 0 modifiersubtext vitoclose 65A55A9B-1414-464D-A0FB-9A2CF705164C destinationuid E77CCBEC-A3BA-4614-B1B0-12B880D25580 modifiers 0 modifiersubtext vitoclose 6CFB2AE8-7BAF-4DE6-B949-D0FF2DB50FD7 destinationuid CD8F2AB1-13E0-47E1-BFCA-701444818091 modifiers 0 modifiersubtext vitoclose 78516575-8825-4598-A589-2F3475E25DF8 destinationuid 6CFB2AE8-7BAF-4DE6-B949-D0FF2DB50FD7 modifiers 0 modifiersubtext vitoclose 7A027517-A35E-4028-89FF-50172EA74768 8604FB3C-23FB-467B-803B-17F6A73073AB destinationuid 55D10CF4-8457-4AE5-9DC0-64A7E262D61D modifiers 0 modifiersubtext vitoclose 862EBB67-6E29-4467-AFE1-7E6717945CB6 destinationuid 12391721-6113-4936-B640-C307F04B697C modifiers 0 modifiersubtext vitoclose 8C1ADFA7-4A9A-4218-9125-F0D4C2763FEC destinationuid 969420B1-18E8-4F0B-AC39-06D038718311 modifiers 0 modifiersubtext vitoclose 92FC681E-1D4D-4E70-8010-84836DC8B371 destinationuid 532E1785-14EE-4BE7-B64A-BE7E809EDDF8 modifiers 0 modifiersubtext vitoclose 998D04E2-9538-4970-9233-1E57F0929ED0 destinationuid BF14B152-A9EC-4FCE-97DA-F62C1C7846D3 modifiers 0 modifiersubtext vitoclose BC6819C2-77D8-4E53-BA51-2787F2087BFD destinationuid 14C00640-8D74-4090-AB64-5BDEA4489D4D modifiers 0 modifiersubtext vitoclose BF14B152-A9EC-4FCE-97DA-F62C1C7846D3 destinationuid 7A027517-A35E-4028-89FF-50172EA74768 modifiers 0 modifiersubtext vitoclose CC4D4EE8-FD80-4612-948E-378FB259148C destinationuid 2F7191FB-FE4C-4B0F-832C-A9BA3DE8F841 modifiers 0 modifiersubtext vitoclose CD8F2AB1-13E0-47E1-BFCA-701444818091 destinationuid BF14B152-A9EC-4FCE-97DA-F62C1C7846D3 modifiers 0 modifiersubtext vitoclose CE59FD32-48E0-467C-AD19-1D9288B0DD2B destinationuid 92FC681E-1D4D-4E70-8010-84836DC8B371 modifiers 0 modifiersubtext vitoclose D55FAAFD-ABA8-4B37-940B-CB883E3BB590 destinationuid 38618982-EFBD-4CEE-AB65-D33F0BBD3C54 modifiers 0 modifiersubtext vitoclose E77CCBEC-A3BA-4614-B1B0-12B880D25580 destinationuid CD8F2AB1-13E0-47E1-BFCA-701444818091 modifiers 0 modifiersubtext vitoclose EE1CC8A9-8FDF-4BC9-A5C6-A4CF2F886D97 destinationuid 5EE513CB-126C-4EC9-8D51-7205A217CA03 modifiers 0 modifiersubtext vitoclose FE9B2118-827D-416D-9829-1A9DC2CAACA4 destinationuid F5C23C7D-94BA-400C-8004-EC304CE8818D modifiers 0 modifiersubtext vitoclose destinationuid 6CEAE7ED-F6DF-403F-988D-4E847AF569B3 modifiers 0 modifiersubtext vitoclose FF2770C7-3B9E-49CF-9F1D-9F2A603CB11C destinationuid 62F44B78-0D8E-4215-BB14-E3950B214795 modifiers 0 modifiersubtext sourceoutputuid D2BA16A8-E9C4-40E1-A365-9391D712D9DD vitoclose destinationuid 0553156D-6606-42C4-8BE8-18AE49A7A6D6 modifiers 0 modifiersubtext sourceoutputuid EFC34E5C-A25D-47BA-82F6-67BD4E221FE7 vitoclose destinationuid FE9B2118-827D-416D-9829-1A9DC2CAACA4 modifiers 0 modifiersubtext sourceoutputuid C4EC175E-156C-4866-8B6C-CAA71D1717B0 vitoclose destinationuid 05634575-6FA7-40A9-8772-47F71A9C0DFC modifiers 0 modifiersubtext sourceoutputuid 0AFA406F-38EB-4B7A-9757-C525540B1120 vitoclose destinationuid 0D457EF4-A96F-4760-A4DC-07F46AE409C6 modifiers 0 modifiersubtext sourceoutputuid 770B9A97-3870-4524-BEF4-795B08C38059 vitoclose destinationuid CC4D4EE8-FD80-4612-948E-378FB259148C modifiers 0 modifiersubtext sourceoutputuid 82716642-AD94-47F6-AC7B-B1578DABAB9A vitoclose destinationuid 11608658-A256-4625-AAD6-517E03644231 modifiers 0 modifiersubtext sourceoutputuid 50D356F8-9115-4738-8AD1-A5C868482186 vitoclose destinationuid EE1CC8A9-8FDF-4BC9-A5C6-A4CF2F886D97 modifiers 0 modifiersubtext sourceoutputuid 1213CDA2-9D5F-40C3-91F9-42DE831983E7 vitoclose destinationuid 50009EAD-DA7A-48FC-8F4F-C5EC1CAE4D97 modifiers 0 modifiersubtext sourceoutputuid 379E6121-5B4C-4936-95C8-B4A0BAEF07C3 vitoclose destinationuid CE59FD32-48E0-467C-AD19-1D9288B0DD2B modifiers 0 modifiersubtext sourceoutputuid 16447B01-A94F-420F-9E78-CB34F131700C vitoclose destinationuid 4C28D7FF-5BFC-4A3B-BC5C-738D3069B9E1 modifiers 0 modifiersubtext sourceoutputuid 01B754FE-3D13-4463-9FC6-D08841BBB800 vitoclose createdby Dean Jackson <deanishe@deanishe.net> description View upcoming events in Google Calendar disabled name Google Calendar View objects config externaltriggerid config passinputasargument passvariables workflowbundleid self type alfred.workflow.output.callexternaltrigger uid 9FA29DC3-2E67-45B6-B93B-F33F57EDF7E7 version 1 config argumenttype 2 keyword gcal subtext Show upcoming events text Upcoming Events withspace type alfred.workflow.input.keyword uid 998D04E2-9538-4970-9233-1E57F0929ED0 version 1 config concurrently escaping 102 script ./gcal clear scriptargtype 1 scriptfile type 0 type alfred.workflow.action.script uid 62F44B78-0D8E-4215-BB14-E3950B214795 version 2 type alfred.workflow.utility.junction uid 303E565E-8048-4ED5-BD89-7E58DF94BF3A version 1 config concurrently escaping 102 script ./gcal update workflow scriptargtype 1 scriptfile type 0 type alfred.workflow.action.script uid 0553156D-6606-42C4-8BE8-18AE49A7A6D6 version 2 config argumenttype 2 keyword today subtext Today's events from your Google Calendar(s) text Today's Events withspace type alfred.workflow.input.keyword uid BC6819C2-77D8-4E53-BA51-2787F2087BFD version 1 config lastpathcomponent onlyshowifquerypopulated removeextension text {query} title ERROR type alfred.workflow.output.notification uid F5C23C7D-94BA-400C-8004-EC304CE8818D version 1 config concurrently escaping 102 script date '+%Y-%m-%d' scriptargtype 1 scriptfile type 0 type alfred.workflow.action.script uid 14C00640-8D74-4090-AB64-5BDEA4489D4D version 2 config argumenttype 2 keyword tomorrow subtext Tomorrow's events from your Google Calendar(s) text Tomorrow's Events withspace type alfred.workflow.input.keyword uid 65A55A9B-1414-464D-A0FB-9A2CF705164C version 1 config concurrently escaping 102 script date -v '+1d' '+%Y-%m-%d' scriptargtype 1 scriptfile type 0 type alfred.workflow.action.script uid E77CCBEC-A3BA-4614-B1B0-12B880D25580 version 2 config externaltriggerid action passinputasargument passvariables workflowbundleid self type alfred.workflow.output.callexternaltrigger uid 7A027517-A35E-4028-89FF-50172EA74768 version 1 config concurrently escaping 102 script test -n "$CALENDAR_APP" && { ./gcal open --app="$CALENDAR_APP" "$1" } || { ./gcal open "$1" } scriptargtype 1 scriptfile type 5 type alfred.workflow.action.script uid FE9B2118-827D-416D-9829-1A9DC2CAACA4 version 2 config argument {query} passthroughargument variables action date type alfred.workflow.utility.argument uid BF14B152-A9EC-4FCE-97DA-F62C1C7846D3 version 1 config type 0 type alfred.workflow.utility.transform uid CD8F2AB1-13E0-47E1-BFCA-701444818091 version 1 type alfred.workflow.utility.hidealfred uid 6CEAE7ED-F6DF-403F-988D-4E847AF569B3 version 1 config argumenttype 2 keyword yesterday subtext Yesterday's events from your Google Calendar(s) text Yesterday's Events withspace type alfred.workflow.input.keyword uid 78516575-8825-4598-A589-2F3475E25DF8 version 1 config concurrently escaping 102 script date -v '-1d' '+%Y-%m-%d' scriptargtype 1 scriptfile type 0 type alfred.workflow.action.script uid 6CFB2AE8-7BAF-4DE6-B949-D0FF2DB50FD7 version 2 config externaltriggerid calendars passinputasargument passvariables workflowbundleid self type alfred.workflow.output.callexternaltrigger uid 05634575-6FA7-40A9-8772-47F71A9C0DFC version 1 config alfredfiltersresults alfredfiltersresultsmatchmode 0 argumenttreatemptyqueryasnil argumenttrimmode 0 argumenttype 1 escaping 102 keyword gdate queuedelaycustom 3 queuedelayimmediatelyinitially queuedelaymode 0 queuemode 1 runningsubtext Loading… script ./gcal dates -- "$1" scriptargtype 1 scriptfile subtext Enter a date title Events for a Specific Date type 0 withspace type alfred.workflow.input.scriptfilter uid 09A5FA54-F59A-45B7-8B66-A08BC6AB8EAC version 3 config externaltriggerid action passinputasargument passvariables workflowbundleid self type alfred.workflow.output.callexternaltrigger uid 969420B1-18E8-4F0B-AC39-06D038718311 version 1 config alfredfiltersresults alfredfiltersresultsmatchmode 0 argumenttreatemptyqueryasnil argumenttrimmode 0 argumenttype 1 escaping 102 queuedelaycustom 3 queuedelayimmediatelyinitially queuedelaymode 0 queuemode 1 runningsubtext script test -n "$date" && { ./gcal events --date="$date" -- "$1" } || { ./gcal events -- "$1" } scriptargtype 1 scriptfile subtext title type 5 withspace type alfred.workflow.input.scriptfilter uid 8C1ADFA7-4A9A-4218-9125-F0D4C2763FEC version 3 config argument passthroughargument variables date {query} type alfred.workflow.utility.argument uid 0D457EF4-A96F-4760-A4DC-07F46AE409C6 version 1 config conditions inputstring {var:action} matchcasesensitive matchmode 0 matchstring clear outputlabel Clear uid D2BA16A8-E9C4-40E1-A365-9391D712D9DD inputstring {var:action} matchcasesensitive matchmode 0 matchstring update outputlabel Update uid EFC34E5C-A25D-47BA-82F6-67BD4E221FE7 inputstring {var:action} matchcasesensitive matchmode 0 matchstring open outputlabel Open uid C4EC175E-156C-4866-8B6C-CAA71D1717B0 inputstring {var:action} matchcasesensitive matchmode 0 matchstring calendars outputlabel Show Calendars uid 0AFA406F-38EB-4B7A-9757-C525540B1120 inputstring {var:action} matchcasesensitive matchmode 0 matchstring date outputlabel Events for Date uid 770B9A97-3870-4524-BEF4-795B08C38059 inputstring {var:action} matchcasesensitive matchmode 0 matchstring set outputlabel Save Workflow Variable uid 82716642-AD94-47F6-AC7B-B1578DABAB9A inputstring {var:action} matchcasesensitive matchmode 0 matchstring logout outputlabel Log out from Account uid 50D356F8-9115-4738-8AD1-A5C868482186 inputstring {var:action} matchcasesensitive matchmode 0 matchstring toggle outputlabel Toggle Calendar On/Off uid 1213CDA2-9D5F-40C3-91F9-42DE831983E7 inputstring {var:action} matchcasesensitive matchmode 0 matchstring config outputlabel Show Configuration uid 379E6121-5B4C-4936-95C8-B4A0BAEF07C3 inputstring {var:action} matchcasesensitive matchmode 0 matchstring create outputlabel Create Event uid 16447B01-A94F-420F-9E78-CB34F131700C inputstring {var:action} matchcasesensitive matchmode 0 matchstring reauth outputlabel Re-Authenticate Account uid 01B754FE-3D13-4463-9FC6-D08841BBB800 elselabel else type alfred.workflow.utility.conditional uid FF2770C7-3B9E-49CF-9F1D-9F2A603CB11C version 1 config externaltriggerid config passinputasargument passvariables workflowbundleid self type alfred.workflow.output.callexternaltrigger uid 2F7191FB-FE4C-4B0F-832C-A9BA3DE8F841 version 1 config triggerid config type alfred.workflow.trigger.external uid 5C03EE71-D6D9-42F5-B608-403F47234C56 version 1 config externaltriggerid action passinputasargument passvariables workflowbundleid self type alfred.workflow.output.callexternaltrigger uid EAF06D56-D2F1-4FB9-B0D7-89D494AA865B version 1 config triggerid action type alfred.workflow.trigger.external uid D55FAAFD-ABA8-4B37-940B-CB883E3BB590 version 1 config concurrently escaping 102 script ./gcal set "$key" "$value" scriptargtype 1 scriptfile type 0 type alfred.workflow.action.script uid CC4D4EE8-FD80-4612-948E-378FB259148C version 2 config alfredfiltersresults alfredfiltersresultsmatchmode 0 argumenttreatemptyqueryasnil argumenttrimmode 0 argumenttype 1 escaping 102 keyword gcalconf queuedelaycustom 3 queuedelayimmediatelyinitially queuedelaymode 0 queuemode 1 runningsubtext Loading… script ./gcal config "$1" scriptargtype 1 scriptfile subtext View and edit workflow settings title Google Calendar Config type 0 withspace inboundconfig inputmode 1 type alfred.workflow.input.scriptfilter uid 3F938397-2CD9-45EB-B0CD-FC962DC9031F version 3 config argument . /---- ACTION IN ----\ query={query} variables={allvars} \-------------------/ cleardebuggertext processoutputs type alfred.workflow.utility.debug uid 38618982-EFBD-4CEE-AB65-D33F0BBD3C54 version 1 config concurrently escaping 102 script ./gcal logout "$account" scriptargtype 1 scriptfile type 0 type alfred.workflow.action.script uid 11608658-A256-4625-AAD6-517E03644231 version 2 config externaltriggerid config passinputasargument passvariables workflowbundleid self type alfred.workflow.output.callexternaltrigger uid 511603A0-C36D-4AB7-9520-0BA422CE5F05 version 1 config alfredfiltersresults alfredfiltersresultsmatchmode 0 argumenttreatemptyqueryasnil argumenttrimmode 0 argumenttype 1 escaping 102 keyword gnew queuedelaycustom 3 queuedelayimmediatelyinitially queuedelaymode 0 queuemode 1 runningsubtext Loading... script ./gcal active "$1" scriptargtype 1 scriptfile subtext Enter event details & select calendar title Add New Event type 0 withspace type alfred.workflow.input.scriptfilter uid 862EBB67-6E29-4467-AFE1-7E6717945CB6 version 3 config externaltriggerid action passinputasargument passvariables workflowbundleid self type alfred.workflow.output.callexternaltrigger uid 12391721-6113-4936-B640-C307F04B697C version 1 config triggerid create type alfred.workflow.trigger.external uid 3CCB9C03-CCBD-43F3-A51F-C95DBBD1A21C version 1 config concurrently escaping 102 script ./gcal toggle "$calendar" scriptargtype 1 scriptfile type 0 type alfred.workflow.action.script uid EE1CC8A9-8FDF-4BC9-A5C6-A4CF2F886D97 version 2 config externaltriggerid calendars passinputasargument passvariables workflowbundleid self type alfred.workflow.output.callexternaltrigger uid 5EE513CB-126C-4EC9-8D51-7205A217CA03 version 1 config externaltriggerid action passinputasargument passvariables workflowbundleid self type alfred.workflow.output.callexternaltrigger uid 2512097E-AB92-489E-93AF-0146592CB0D4 version 1 config alfredfiltersresults alfredfiltersresultsmatchmode 0 argumenttreatemptyqueryasnil argumenttrimmode 0 argumenttype 1 escaping 102 queuedelaycustom 3 queuedelayimmediatelyinitially queuedelaymode 0 queuemode 1 runningsubtext script ./gcal calendars "$1" scriptargtype 1 scriptfile subtext title type 0 withspace type alfred.workflow.input.scriptfilter uid 55D10CF4-8457-4AE5-9DC0-64A7E262D61D version 3 config triggerid calendars type alfred.workflow.trigger.external uid 8604FB3C-23FB-467B-803B-17F6A73073AB version 1 config triggerid close type alfred.workflow.trigger.external uid 57F4280F-D680-47D8-B5CA-ED9FFDA29082 version 1 config externaltriggerid config passinputasargument passvariables workflowbundleid self type alfred.workflow.output.callexternaltrigger uid 50009EAD-DA7A-48FC-8F4F-C5EC1CAE4D97 version 1 type alfred.workflow.utility.hidealfred uid 36D47E80-4BA8-4158-8B18-C1EDB477E6D7 version 1 config externaltriggerid action passinputasargument passvariables workflowbundleid self type alfred.workflow.output.callexternaltrigger uid 532E1785-14EE-4BE7-B64A-BE7E809EDDF8 version 1 config concurrently escaping 102 script ./gcal create "$quick" "$calendar" scriptargtype 1 scriptfile type 0 type alfred.workflow.action.script uid CE59FD32-48E0-467C-AD19-1D9288B0DD2B version 2 config argument passthroughargument variables action date type alfred.workflow.utility.argument uid 92FC681E-1D4D-4E70-8010-84836DC8B371 version 1 config externaltriggerid config passinputasargument passvariables workflowbundleid self type alfred.workflow.output.callexternaltrigger uid 60FDD3AD-D600-4F4E-A43A-B413E83FC298 version 1 config concurrently escaping 102 script ./gcal reauth "$account" scriptargtype 1 scriptfile type 0 type alfred.workflow.action.script uid 4C28D7FF-5BFC-4A3B-BC5C-738D3069B9E1 version 2 readme Google Calendar =============== View events from your Google calendars. Configuration ------------- `APPLE_MAPS`: Set to "1" to open location URLs in Apple Maps, not Google Maps. `CALENDAR_APP`: Set to an application name to open calendar URLs (not map URLs) in an application other than your default browser (e.g. a session-specific browser). `EVENT_CACHE_MINUTES`: How many minutes to cache events for. `SCHEDULE_DAYS`: How many days' events to show in the "Upcoming Events" list (keyword: "gcal"). uidata 0553156D-6606-42C4-8BE8-18AE49A7A6D6 note Check for new version of the workflow xpos 1180 ypos 200 05634575-6FA7-40A9-8772-47F71A9C0DFC xpos 1180 ypos 530 09A5FA54-F59A-45B7-8B66-A08BC6AB8EAC note Show events for a given date xpos 210 ypos 660 0D457EF4-A96F-4760-A4DC-07F46AE409C6 note Set $date from {query} xpos 1220 ypos 700 11608658-A256-4625-AAD6-517E03644231 note Remove account xpos 1180 ypos 1000 12391721-6113-4936-B640-C307F04B697C xpos 400 ypos 1010 14C00640-8D74-4090-AB64-5BDEA4489D4D xpos 210 ypos 200 2512097E-AB92-489E-93AF-0146592CB0D4 xpos 400 ypos 1170 2F7191FB-FE4C-4B0F-832C-A9BA3DE8F841 xpos 1360 ypos 830 303E565E-8048-4ED5-BD89-7E58DF94BF3A xpos 1400 ypos 150 36D47E80-4BA8-4158-8B18-C1EDB477E6D7 xpos 250 ypos 1370 38618982-EFBD-4CEE-AB65-D33F0BBD3C54 xpos 780 ypos 860 3CCB9C03-CCBD-43F3-A51F-C95DBBD1A21C xpos 40 ypos 1010 3F938397-2CD9-45EB-B0CD-FC962DC9031F note Show workflow configuration xpos 210 ypos 830 4C28D7FF-5BFC-4A3B-BC5C-738D3069B9E1 note Re-authenticate account xpos 1180 ypos 1660 50009EAD-DA7A-48FC-8F4F-C5EC1CAE4D97 xpos 1180 ypos 1355 511603A0-C36D-4AB7-9520-0BA422CE5F05 xpos 1370 ypos 1000 532E1785-14EE-4BE7-B64A-BE7E809EDDF8 xpos 1370 ypos 1510 55D10CF4-8457-4AE5-9DC0-64A7E262D61D note Toggle calendars on/off xpos 210 ypos 1170 57F4280F-D680-47D8-B5CA-ED9FFDA29082 xpos 40 ypos 1340 5C03EE71-D6D9-42F5-B608-403F47234C56 xpos 40 ypos 830 5EE513CB-126C-4EC9-8D51-7205A217CA03 xpos 1370 ypos 1170 60FDD3AD-D600-4F4E-A43A-B413E83FC298 xpos 1370 ypos 1660 62F44B78-0D8E-4215-BB14-E3950B214795 note Clear old cache files xpos 1180 ypos 40 65A55A9B-1414-464D-A0FB-9A2CF705164C note Show tomorrow's events xpos 40 ypos 360 6CEAE7ED-F6DF-403F-988D-4E847AF569B3 xpos 1400 ypos 400 6CFB2AE8-7BAF-4DE6-B949-D0FF2DB50FD7 xpos 210 ypos 520 78516575-8825-4598-A589-2F3475E25DF8 note Show yesterday's events xpos 40 ypos 520 7A027517-A35E-4028-89FF-50172EA74768 xpos 760 ypos 360 8604FB3C-23FB-467B-803B-17F6A73073AB xpos 40 ypos 1170 862EBB67-6E29-4467-AFE1-7E6717945CB6 note Quick-add a new event xpos 210 ypos 1010 8C1ADFA7-4A9A-4218-9125-F0D4C2763FEC note Show events xpos 1360 ypos 670 92FC681E-1D4D-4E70-8010-84836DC8B371 note $action to "date" xpos 1310 ypos 1540 969420B1-18E8-4F0B-AC39-06D038718311 xpos 1540 ypos 670 998D04E2-9538-4970-9233-1E57F0929ED0 note Show upcoming events xpos 210 ypos 40 9FA29DC3-2E67-45B6-B93B-F33F57EDF7E7 xpos 1540 ypos 40 BC6819C2-77D8-4E53-BA51-2787F2087BFD note Show today's events xpos 40 ypos 200 BF14B152-A9EC-4FCE-97DA-F62C1C7846D3 note Set $action to "date" xpos 620 ypos 390 CC4D4EE8-FD80-4612-948E-378FB259148C note Change configuration xpos 1180 ypos 830 CD8F2AB1-13E0-47E1-BFCA-701444818091 note Trim whitespace xpos 450 ypos 390 CE59FD32-48E0-467C-AD19-1D9288B0DD2B xpos 1180 ypos 1510 D55FAAFD-ABA8-4B37-940B-CB883E3BB590 xpos 590 ypos 830 E77CCBEC-A3BA-4614-B1B0-12B880D25580 xpos 210 ypos 360 EAF06D56-D2F1-4FB9-B0D7-89D494AA865B xpos 400 ypos 830 EE1CC8A9-8FDF-4BC9-A5C6-A4CF2F886D97 note Toggle calendar xpos 1180 ypos 1170 F5C23C7D-94BA-400C-8004-EC304CE8818D xpos 1540 ypos 200 FE9B2118-827D-416D-9829-1A9DC2CAACA4 note Open calendar URL xpos 1180 ypos 370 FF2770C7-3B9E-49CF-9F1D-9F2A603CB11C xpos 880 ypos 700 variables APPLE_MAPS 0 CALENDAR_APP EVENT_CACHE_MINS 15 SCHEDULE_DAYS 7 TIME_12H 0 version 0.5.1 webaddress ================================================ FILE: magefile.go ================================================ // Copyright (c) 2019 Dean Jackson // MIT Licence applies http://opensource.org/licenses/MIT // +build mage package main import ( "fmt" "io/ioutil" "path/filepath" "github.com/deanishe/awgo/util" "github.com/deanishe/awgo/util/build" "github.com/magefile/mage/mg" "github.com/magefile/mage/sh" ) // Default target to run when none is specified // If not set, running mage will list available targets // var Default = Build var ( info *build.Info buildDir = "./build" distDir = "./dist" env map[string]string ) func init() { var err error if info, err = build.NewInfo(); err != nil { panic(err) } env = info.Env() env["TAGS"] = "private" } func mod(args ...string) error { argv := append([]string{"mod"}, args...) return sh.RunWith(info.Env(), "go", argv...) } // Aliases are mage command aliases. var Aliases = map[string]interface{}{ "b": Build, "c": Clean, "d": Dist, "l": Link, } // Build build workflow in ./build func Build() error { mg.Deps(cleanBuild) // mg.Deps(Deps) fmt.Println("building ...") if err := sh.RunWith(env, "go", "build", "-tags", "$TAGS", "-o", buildDir+"/gcal", "."); err != nil { return err } globs := build.Globs( "*.png", "info.plist", "*.html", "README.md", "LICENCE.txt", "icons/*.png", ) return build.SymlinkGlobs(buildDir, globs...) } // run workflow func Run() error { mg.Deps(Build) fmt.Println("running ...") return sh.RunWith(info.Env(), buildDir+"/gcal", "-h") } // build .alfredworkflow file in ./dist func Dist() error { mg.SerialDeps(Clean, Build) p, err := build.Export(buildDir, distDir) if err != nil { return err } fmt.Printf("exported %q\n", p) return nil } // symlink ./build directory to Alfred's workflow directory func Link() error { mg.Deps(Build) dir := filepath.Join(info.AlfredWorkflowDir, info.BundleID) fmt.Printf("linking %q to %q ...\n", buildDir, dir) if err := sh.Rm(dir); err != nil { return err } return build.Symlink(dir, buildDir, true) } // clean & download dependencies func Deps() error { mg.Deps(cleanDeps) fmt.Println("downloading deps ...") return mod("download") } // remove build files func Clean() { fmt.Println("cleaning ...") mg.Deps(cleanBuild, cleanMage) } func cleanDeps() error { return mod("tidy", "-v") } func cleanDir(name string) error { if !util.PathExists(name) { return nil } infos, err := ioutil.ReadDir(name) if err != nil { return err } for _, fi := range infos { if err := sh.Rm(filepath.Join(name, fi.Name())); err != nil { return err } } return nil } // remove generated icons func CleanIcons() error { return cleanDir("./icons") } func cleanBuild() error { return cleanDir("./build") } func cleanMage() error { return sh.Run("mage", "-clean") } ================================================ FILE: magic.go ================================================ // // Copyright (c) 2017 Dean Jackson // // MIT Licence. See http://opensource.org/licenses/MIT // // Created on 2017-11-25 // package main import ( aw "github.com/deanishe/awgo" "github.com/pkg/errors" ) // "magic" action to open list of calendars type calendarMagic struct{} func (cm *calendarMagic) Keyword() string { return "calendars" } func (cm *calendarMagic) Description() string { return "Activate/deactivate calendars" } func (cm *calendarMagic) RunText() string { return "Opening calendar list…" } func (cm *calendarMagic) Run() error { return aw.NewAlfred().RunTrigger("calendars", "") } // "magic" action to log in to a new account type loginMagic struct{} func (lm *loginMagic) Keyword() string { return "login" } func (lm *loginMagic) Description() string { return "Add a Google account" } func (lm *loginMagic) RunText() string { return "Opening Google signin page…" } func (lm *loginMagic) Run() error { wf := aw.New() if err := wf.Alfred.RunTrigger("close", ""); err != nil { return errors.Wrap(err, "close Alfred") } acc, err := NewAccount("") if err != nil { return errors.Wrap(err, "magic: new account") } if err := acc.FetchCalendars(); err != nil { return errors.Wrap(err, "magic: fetch calendars") } // clear cached schedules now calendars have changed if err := clearEvents(); err != nil { return errors.Wrap(err, "clear cached events") } // re-open workflow configuration return wf.Alfred.RunTrigger("config", "") } ================================================ FILE: main.go ================================================ // // Copyright (c) 2017 Dean Jackson // // MIT Licence. See http://opensource.org/licenses/MIT // // Created on 2017-11-25 // // Command gcal is an Alfred 3 workflow for viewing Google Calendar events. package main import ( "log" "os" "os/exec" "path/filepath" "time" aw "github.com/deanishe/awgo" "github.com/deanishe/awgo/update" "github.com/deanishe/awgo/util" docopt "github.com/docopt/docopt-go" "github.com/pkg/errors" ) const ( timeFormat = "2006-01-02" timeFormatLong = "Monday, 2 January 2006" // Workflow icon colours yellow = "f8ac30" // green = "03ae03" // blue = "5484f3" // red = "b00000" // Workflow settings & URLs repo = "deanishe/alfred-gcal" helpURL = "https://github.com/deanishe/alfred-gcal/issues" readmeURL = "https://github.com/deanishe/alfred-gcal/blob/master/README.md" forumURL = "https://www.alfredforum.com/topic/11016-google-calendar-view/" ) const usage = ` gcal [] [options] [] Usage: gcal dates [--] [] gcal events [--date=] [--] [] gcal calendars [] gcal active [] gcal toggle gcal set gcal update (workflow|calendars|events) [] gcal config [] gcal logout gcal reauth gcal clear gcal open [--app=] gcal server gcal reload gcal create gcal -h Options: -a --app Application to open URLs in. -d --date Date to show events for (format YYYY-MM-DD). -h --help Show this message and exit. --version Show workflow version and exit. ` var ( wf *aw.Workflow accounts []*Account cacheDirIcons string // directory generated icons are stored in // CLI args opts *options // display times using 24h clock hourFormat = "15:04" ) // CLI flags type options struct { // commands Calendars bool Active bool Clear bool Config bool Dates bool Events bool Logout bool Reauth bool Open bool Reload bool Server bool Set bool Toggle bool Update bool Create bool // sub-commands Workflow bool // flags Account string App string CalendarID string `docopt:""` Date string `docopt:",--date"` DateFormat string `docopt:""` Query string URL string `docopt:""` Key string Value string Quick string `docopt:""` // options UseAppleMaps bool `env:"APPLE_MAPS"` EventCacheMins int `env:"EVENT_CACHE_MINS"` ScheduleDays int `env:"SCHEDULE_DAYS"` Use12HourTime bool `env:"TIME_12H"` ScheduleMode bool StartTime time.Time EndTime time.Time // needed to make '--' work EndOfOptions bool `docopt:"--"` } func (opts *options) MaxAgeCalendar() time.Duration { return time.Hour * 3 } func (opts *options) MaxAgeEvents() time.Duration { d := time.Duration(opts.EventCacheMins) * time.Minute if d < time.Minute*5 { d = time.Minute * 5 } return d } func (opts *options) ScheduleDuration() time.Duration { return time.Duration(opts.ScheduleDays) * time.Hour * 24 } func init() { opts = &options{} wf = aw.New(update.GitHub(repo), aw.HelpURL(helpURL)) wf.Configure(aw.AddMagic(&calendarMagic{}, &loginMagic{})) cacheDirIcons = filepath.Join(wf.CacheDir(), "icons") } // Parse command-line flags. func parseFlags() error { args, err := docopt.ParseArgs(usage, wf.Args(), wf.Version()) if err != nil { return errors.Wrap(err, "docopt parse") } if err := args.Bind(opts); err != nil { return errors.Wrap(err, "bind docopt") } if err := wf.Config.To(opts); err != nil { return errors.Wrap(err, "bind config") } // We don't need to be fussy about the default start and end times: // The default startTime is only used in schedule mode, and it (and endTime) // will be set to midnight if user specifies a date. opts.StartTime = time.Now().Local() opts.ScheduleMode = true if opts.Date != "" { opts.StartTime, err = time.ParseInLocation(timeFormat, opts.Date, time.Local) if err != nil { return err } opts.ScheduleMode = false } if opts.Use12HourTime { hourFormat = "3:04" } opts.EndTime = opts.StartTime.Add(time.Hour * 24) log.Printf("[main] query=%q, startTime=%v, endTime=%v", opts.Query, opts.StartTime, opts.EndTime) return nil } // Main program entry point. func run() { var err error if err = parseFlags(); err != nil { wf.FatalError(err) } // Ensure required directories exist util.MustExist(cacheDirIcons) if accounts, err = LoadAccounts(); err != nil { panic(err) } if !wf.IsRunning("server") { cmd := exec.Command(os.Args[0], "server") if err := wf.RunInBackground("server", cmd); err != nil { wf.FatalError(err) } } switch { // check for Update first as Calendars and Events are also // set by the corresponding top-level commands. case opts.Update: switch { case opts.Calendars: err = doUpdateCalendars() case opts.Events: err = doUpdateEvents() case opts.Workflow: err = doUpdateWorkflow() } case opts.Calendars: err = doListCalendars() case opts.Clear: err = doClear() case opts.Config: err = doConfig() case opts.Dates: err = doDates() case opts.Events: err = doEvents() case opts.Logout: err = doLogout() case opts.Open: err = doOpen() case opts.Set: err = doSet() case opts.Server: err = doStartServer() case opts.Toggle: err = doToggle() case opts.Reauth: err = doReauth() case opts.Reload: err = doReload() case opts.Create: err = quickAdd() case opts.Active: err = doListWritableCalendars() } if err != nil { if err == errNoActive { wf.NewItem("No active calendars"). Subtitle("↩ or ⇥ to choose calendars"). Autocomplete("workflow:calendars"). Valid(false). Icon(aw.IconWarning) wf.SendFeedback() return } wf.FatalError(err) } } // Call via Workflow.Run() to rescue panics and show an error message in Alfred. func main() { wf.Run(run) } ================================================ FILE: modd.conf ================================================ magefile.go magefile_*.go { prep: mage -l } modd.conf *.go *.html ./bin/build !mage*.go !vendor/** !secret* { prep: " # run unit tests go test -v @dirmods \ && mage -v run " } ================================================ FILE: preview.html ================================================ {{ define "event" }} {{ .Title }}

{{ .Title }}

in {{ .CalendarTitle }}

{{ if .Location }} {{ end }} {{ if .Description }} {{ end }}
Date {{ .Start.Format "Monday, 2 Jan 2006" }}
Time {{ .Start.Format "15:04" }} – {{ .End.Format "15:04" }}
Location {{ .Location }}
Description {{ .Description }}
{{ end }} {{ define "fail" }} Event Not Found

Event Not Found

Couldn't find an event for ID {{ . }}

{{ end }} ================================================ FILE: secret.go ================================================ // // Copyright (c) 2017 Dean Jackson // // MIT Licence. See http://opensource.org/licenses/MIT // // Created on 2017-11-25 // // +build !private package main // Google doesn't allow app keys to be stored in open-source source code. // The built workflow includes a key. // // If you want to hack on the source code, register your own project here: // https://console.developers.google.com/apis/dashboard // // Add the Google Calendar API and create credentials for a web app, with // http://localhost:61432 as the redirect URI. // // The workflow only requires read access. const secret = ` { "web": { "redirect_uris": [ "http://localhost:61432" ], "auth_uri": "https://accounts.google.com/o/oauth2/auth", "client_id": "", "project_id": "", "client_secret": "", "token_uri": "https://accounts.google.com/o/oauth2/token", "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs" } } `