Full Code of deanishe/alfred-gcal for AI

master cd3271d165e0 cached
31 files
174.3 KB
69.2k tokens
123 symbols
1 requests
Download .txt
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 <deanishe@deanishe.net>

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
================================================

<div align="center">
    <img height="128" width="128" src="https://raw.githubusercontent.com/deanishe/alfred-gcal/master/icons/icon.png">
</div>

Google Calendar for Alfred
==========================

View Google Calendar events in [Alfred][alfred]. Supports multiple accounts.

<!-- MarkdownTOC autolink="true" bracket="round" depth="3" autoanchor="true" -->

- [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)

<!-- /MarkdownTOC -->


<a name="download--installation"></a>
Download & installation
-----------------------

Grab the workflow from [GitHub releases][download]. Download the `Google-Calendar-View-X.X.alfredworkflow` file and double-click it to install.


<a name="usage"></a>
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.
    - `<query>` — 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.
    - `<query>` / `↩` / `⌘↩` / `⇧` / `⌘Y` — As above.
- `gdate [<date>]` — Show one or more dates. See below for query format.
    - `↩` — Show events for the given day.
- `gnew [<query>]` — 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 [<query>]` — 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.


<a name="date-format"></a>
### 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


<a name="add-event-format"></a>
### 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.


<a name="configuration"></a>
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`. |


<a name="licensing--thanks"></a>
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.


<a name="privacy"></a>
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 <deanishe@deanishe.net>
// 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 <deanishe@deanishe.net>
//
// 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 <deanishe@deanishe.net>
//
// 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 <deanishe@deanishe.net>
//
// 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 <deanishe@deanishe.net>
//
// 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 <deanishe@deanishe.net>
//
// 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 <deanishe@deanishe.net>
//
// 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 <deanishe@deanishe.net>
//
// 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 <deanishe@deanishe.net>
//
// 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 <deanishe@deanishe.net>
//
// 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 <deanishe@deanishe.net>
//
// 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 <deanishe@deanishe.net>
// 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 <deanishe@deanishe.net>
//
// 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 <name> | 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 <deanishe@deanishe.net>
//
// 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 <deanishe@deanishe.net>
//
// 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
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>bundleid</key>
	<string>net.deanishe.alfred.gcal</string>
	<key>connections</key>
	<dict>
		<key>0553156D-6606-42C4-8BE8-18AE49A7A6D6</key>
		<array>
			<dict>
				<key>destinationuid</key>
				<string>303E565E-8048-4ED5-BD89-7E58DF94BF3A</string>
				<key>modifiers</key>
				<integer>0</integer>
				<key>modifiersubtext</key>
				<string></string>
				<key>vitoclose</key>
				<false/>
			</dict>
		</array>
		<key>09A5FA54-F59A-45B7-8B66-A08BC6AB8EAC</key>
		<array>
			<dict>
				<key>destinationuid</key>
				<string>CD8F2AB1-13E0-47E1-BFCA-701444818091</string>
				<key>modifiers</key>
				<integer>0</integer>
				<key>modifiersubtext</key>
				<string></string>
				<key>vitoclose</key>
				<true/>
			</dict>
		</array>
		<key>0D457EF4-A96F-4760-A4DC-07F46AE409C6</key>
		<array>
			<dict>
				<key>destinationuid</key>
				<string>8C1ADFA7-4A9A-4218-9125-F0D4C2763FEC</string>
				<key>modifiers</key>
				<integer>0</integer>
				<key>modifiersubtext</key>
				<string></string>
				<key>vitoclose</key>
				<false/>
			</dict>
		</array>
		<key>11608658-A256-4625-AAD6-517E03644231</key>
		<array>
			<dict>
				<key>destinationuid</key>
				<string>511603A0-C36D-4AB7-9520-0BA422CE5F05</string>
				<key>modifiers</key>
				<integer>0</integer>
				<key>modifiersubtext</key>
				<string></string>
				<key>vitoclose</key>
				<false/>
			</dict>
		</array>
		<key>14C00640-8D74-4090-AB64-5BDEA4489D4D</key>
		<array>
			<dict>
				<key>destinationuid</key>
				<string>CD8F2AB1-13E0-47E1-BFCA-701444818091</string>
				<key>modifiers</key>
				<integer>0</integer>
				<key>modifiersubtext</key>
				<string></string>
				<key>vitoclose</key>
				<false/>
			</dict>
		</array>
		<key>2F7191FB-FE4C-4B0F-832C-A9BA3DE8F841</key>
		<array/>
		<key>303E565E-8048-4ED5-BD89-7E58DF94BF3A</key>
		<array>
			<dict>
				<key>destinationuid</key>
				<string>9FA29DC3-2E67-45B6-B93B-F33F57EDF7E7</string>
				<key>modifiers</key>
				<integer>0</integer>
				<key>modifiersubtext</key>
				<string></string>
				<key>vitoclose</key>
				<false/>
			</dict>
			<dict>
				<key>destinationuid</key>
				<string>F5C23C7D-94BA-400C-8004-EC304CE8818D</string>
				<key>modifiers</key>
				<integer>0</integer>
				<key>modifiersubtext</key>
				<string></string>
				<key>vitoclose</key>
				<false/>
			</dict>
		</array>
		<key>38618982-EFBD-4CEE-AB65-D33F0BBD3C54</key>
		<array>
			<dict>
				<key>destinationuid</key>
				<string>FF2770C7-3B9E-49CF-9F1D-9F2A603CB11C</string>
				<key>modifiers</key>
				<integer>0</integer>
				<key>modifiersubtext</key>
				<string></string>
				<key>vitoclose</key>
				<false/>
			</dict>
		</array>
		<key>3CCB9C03-CCBD-43F3-A51F-C95DBBD1A21C</key>
		<array>
			<dict>
				<key>destinationuid</key>
				<string>862EBB67-6E29-4467-AFE1-7E6717945CB6</string>
				<key>modifiers</key>
				<integer>0</integer>
				<key>modifiersubtext</key>
				<string></string>
				<key>vitoclose</key>
				<false/>
			</dict>
		</array>
		<key>3F938397-2CD9-45EB-B0CD-FC962DC9031F</key>
		<array>
			<dict>
				<key>destinationuid</key>
				<string>EAF06D56-D2F1-4FB9-B0D7-89D494AA865B</string>
				<key>modifiers</key>
				<integer>0</integer>
				<key>modifiersubtext</key>
				<string></string>
				<key>vitoclose</key>
				<true/>
			</dict>
		</array>
		<key>4C28D7FF-5BFC-4A3B-BC5C-738D3069B9E1</key>
		<array>
			<dict>
				<key>destinationuid</key>
				<string>60FDD3AD-D600-4F4E-A43A-B413E83FC298</string>
				<key>modifiers</key>
				<integer>0</integer>
				<key>modifiersubtext</key>
				<string></string>
				<key>vitoclose</key>
				<false/>
			</dict>
		</array>
		<key>55D10CF4-8457-4AE5-9DC0-64A7E262D61D</key>
		<array>
			<dict>
				<key>destinationuid</key>
				<string>2512097E-AB92-489E-93AF-0146592CB0D4</string>
				<key>modifiers</key>
				<integer>0</integer>
				<key>modifiersubtext</key>
				<string></string>
				<key>vitoclose</key>
				<true/>
			</dict>
		</array>
		<key>57F4280F-D680-47D8-B5CA-ED9FFDA29082</key>
		<array>
			<dict>
				<key>destinationuid</key>
				<string>36D47E80-4BA8-4158-8B18-C1EDB477E6D7</string>
				<key>modifiers</key>
				<integer>0</integer>
				<key>modifiersubtext</key>
				<string></string>
				<key>vitoclose</key>
				<false/>
			</dict>
		</array>
		<key>5C03EE71-D6D9-42F5-B608-403F47234C56</key>
		<array>
			<dict>
				<key>destinationuid</key>
				<string>3F938397-2CD9-45EB-B0CD-FC962DC9031F</string>
				<key>modifiers</key>
				<integer>0</integer>
				<key>modifiersubtext</key>
				<string></string>
				<key>vitoclose</key>
				<false/>
			</dict>
		</array>
		<key>62F44B78-0D8E-4215-BB14-E3950B214795</key>
		<array>
			<dict>
				<key>destinationuid</key>
				<string>303E565E-8048-4ED5-BD89-7E58DF94BF3A</string>
				<key>modifiers</key>
				<integer>0</integer>
				<key>modifiersubtext</key>
				<string></string>
				<key>vitoclose</key>
				<false/>
			</dict>
		</array>
		<key>65A55A9B-1414-464D-A0FB-9A2CF705164C</key>
		<array>
			<dict>
				<key>destinationuid</key>
				<string>E77CCBEC-A3BA-4614-B1B0-12B880D25580</string>
				<key>modifiers</key>
				<integer>0</integer>
				<key>modifiersubtext</key>
				<string></string>
				<key>vitoclose</key>
				<true/>
			</dict>
		</array>
		<key>6CFB2AE8-7BAF-4DE6-B949-D0FF2DB50FD7</key>
		<array>
			<dict>
				<key>destinationuid</key>
				<string>CD8F2AB1-13E0-47E1-BFCA-701444818091</string>
				<key>modifiers</key>
				<integer>0</integer>
				<key>modifiersubtext</key>
				<string></string>
				<key>vitoclose</key>
				<false/>
			</dict>
		</array>
		<key>78516575-8825-4598-A589-2F3475E25DF8</key>
		<array>
			<dict>
				<key>destinationuid</key>
				<string>6CFB2AE8-7BAF-4DE6-B949-D0FF2DB50FD7</string>
				<key>modifiers</key>
				<integer>0</integer>
				<key>modifiersubtext</key>
				<string></string>
				<key>vitoclose</key>
				<true/>
			</dict>
		</array>
		<key>7A027517-A35E-4028-89FF-50172EA74768</key>
		<array/>
		<key>8604FB3C-23FB-467B-803B-17F6A73073AB</key>
		<array>
			<dict>
				<key>destinationuid</key>
				<string>55D10CF4-8457-4AE5-9DC0-64A7E262D61D</string>
				<key>modifiers</key>
				<integer>0</integer>
				<key>modifiersubtext</key>
				<string></string>
				<key>vitoclose</key>
				<false/>
			</dict>
		</array>
		<key>862EBB67-6E29-4467-AFE1-7E6717945CB6</key>
		<array>
			<dict>
				<key>destinationuid</key>
				<string>12391721-6113-4936-B640-C307F04B697C</string>
				<key>modifiers</key>
				<integer>0</integer>
				<key>modifiersubtext</key>
				<string></string>
				<key>vitoclose</key>
				<false/>
			</dict>
		</array>
		<key>8C1ADFA7-4A9A-4218-9125-F0D4C2763FEC</key>
		<array>
			<dict>
				<key>destinationuid</key>
				<string>969420B1-18E8-4F0B-AC39-06D038718311</string>
				<key>modifiers</key>
				<integer>0</integer>
				<key>modifiersubtext</key>
				<string></string>
				<key>vitoclose</key>
				<true/>
			</dict>
		</array>
		<key>92FC681E-1D4D-4E70-8010-84836DC8B371</key>
		<array>
			<dict>
				<key>destinationuid</key>
				<string>532E1785-14EE-4BE7-B64A-BE7E809EDDF8</string>
				<key>modifiers</key>
				<integer>0</integer>
				<key>modifiersubtext</key>
				<string></string>
				<key>vitoclose</key>
				<false/>
			</dict>
		</array>
		<key>998D04E2-9538-4970-9233-1E57F0929ED0</key>
		<array>
			<dict>
				<key>destinationuid</key>
				<string>BF14B152-A9EC-4FCE-97DA-F62C1C7846D3</string>
				<key>modifiers</key>
				<integer>0</integer>
				<key>modifiersubtext</key>
				<string></string>
				<key>vitoclose</key>
				<true/>
			</dict>
		</array>
		<key>BC6819C2-77D8-4E53-BA51-2787F2087BFD</key>
		<array>
			<dict>
				<key>destinationuid</key>
				<string>14C00640-8D74-4090-AB64-5BDEA4489D4D</string>
				<key>modifiers</key>
				<integer>0</integer>
				<key>modifiersubtext</key>
				<string></string>
				<key>vitoclose</key>
				<true/>
			</dict>
		</array>
		<key>BF14B152-A9EC-4FCE-97DA-F62C1C7846D3</key>
		<array>
			<dict>
				<key>destinationuid</key>
				<string>7A027517-A35E-4028-89FF-50172EA74768</string>
				<key>modifiers</key>
				<integer>0</integer>
				<key>modifiersubtext</key>
				<string></string>
				<key>vitoclose</key>
				<false/>
			</dict>
		</array>
		<key>CC4D4EE8-FD80-4612-948E-378FB259148C</key>
		<array>
			<dict>
				<key>destinationuid</key>
				<string>2F7191FB-FE4C-4B0F-832C-A9BA3DE8F841</string>
				<key>modifiers</key>
				<integer>0</integer>
				<key>modifiersubtext</key>
				<string></string>
				<key>vitoclose</key>
				<false/>
			</dict>
		</array>
		<key>CD8F2AB1-13E0-47E1-BFCA-701444818091</key>
		<array>
			<dict>
				<key>destinationuid</key>
				<string>BF14B152-A9EC-4FCE-97DA-F62C1C7846D3</string>
				<key>modifiers</key>
				<integer>0</integer>
				<key>modifiersubtext</key>
				<string></string>
				<key>vitoclose</key>
				<false/>
			</dict>
		</array>
		<key>CE59FD32-48E0-467C-AD19-1D9288B0DD2B</key>
		<array>
			<dict>
				<key>destinationuid</key>
				<string>92FC681E-1D4D-4E70-8010-84836DC8B371</string>
				<key>modifiers</key>
				<integer>0</integer>
				<key>modifiersubtext</key>
				<string></string>
				<key>vitoclose</key>
				<false/>
			</dict>
		</array>
		<key>D55FAAFD-ABA8-4B37-940B-CB883E3BB590</key>
		<array>
			<dict>
				<key>destinationuid</key>
				<string>38618982-EFBD-4CEE-AB65-D33F0BBD3C54</string>
				<key>modifiers</key>
				<integer>0</integer>
				<key>modifiersubtext</key>
				<string></string>
				<key>vitoclose</key>
				<false/>
			</dict>
		</array>
		<key>E77CCBEC-A3BA-4614-B1B0-12B880D25580</key>
		<array>
			<dict>
				<key>destinationuid</key>
				<string>CD8F2AB1-13E0-47E1-BFCA-701444818091</string>
				<key>modifiers</key>
				<integer>0</integer>
				<key>modifiersubtext</key>
				<string></string>
				<key>vitoclose</key>
				<false/>
			</dict>
		</array>
		<key>EE1CC8A9-8FDF-4BC9-A5C6-A4CF2F886D97</key>
		<array>
			<dict>
				<key>destinationuid</key>
				<string>5EE513CB-126C-4EC9-8D51-7205A217CA03</string>
				<key>modifiers</key>
				<integer>0</integer>
				<key>modifiersubtext</key>
				<string></string>
				<key>vitoclose</key>
				<false/>
			</dict>
		</array>
		<key>FE9B2118-827D-416D-9829-1A9DC2CAACA4</key>
		<array>
			<dict>
				<key>destinationuid</key>
				<string>F5C23C7D-94BA-400C-8004-EC304CE8818D</string>
				<key>modifiers</key>
				<integer>0</integer>
				<key>modifiersubtext</key>
				<string></string>
				<key>vitoclose</key>
				<false/>
			</dict>
			<dict>
				<key>destinationuid</key>
				<string>6CEAE7ED-F6DF-403F-988D-4E847AF569B3</string>
				<key>modifiers</key>
				<integer>0</integer>
				<key>modifiersubtext</key>
				<string></string>
				<key>vitoclose</key>
				<false/>
			</dict>
		</array>
		<key>FF2770C7-3B9E-49CF-9F1D-9F2A603CB11C</key>
		<array>
			<dict>
				<key>destinationuid</key>
				<string>62F44B78-0D8E-4215-BB14-E3950B214795</string>
				<key>modifiers</key>
				<integer>0</integer>
				<key>modifiersubtext</key>
				<string></string>
				<key>sourceoutputuid</key>
				<string>D2BA16A8-E9C4-40E1-A365-9391D712D9DD</string>
				<key>vitoclose</key>
				<false/>
			</dict>
			<dict>
				<key>destinationuid</key>
				<string>0553156D-6606-42C4-8BE8-18AE49A7A6D6</string>
				<key>modifiers</key>
				<integer>0</integer>
				<key>modifiersubtext</key>
				<string></string>
				<key>sourceoutputuid</key>
				<string>EFC34E5C-A25D-47BA-82F6-67BD4E221FE7</string>
				<key>vitoclose</key>
				<false/>
			</dict>
			<dict>
				<key>destinationuid</key>
				<string>FE9B2118-827D-416D-9829-1A9DC2CAACA4</string>
				<key>modifiers</key>
				<integer>0</integer>
				<key>modifiersubtext</key>
				<string></string>
				<key>sourceoutputuid</key>
				<string>C4EC175E-156C-4866-8B6C-CAA71D1717B0</string>
				<key>vitoclose</key>
				<false/>
			</dict>
			<dict>
				<key>destinationuid</key>
				<string>05634575-6FA7-40A9-8772-47F71A9C0DFC</string>
				<key>modifiers</key>
				<integer>0</integer>
				<key>modifiersubtext</key>
				<string></string>
				<key>sourceoutputuid</key>
				<string>0AFA406F-38EB-4B7A-9757-C525540B1120</string>
				<key>vitoclose</key>
				<false/>
			</dict>
			<dict>
				<key>destinationuid</key>
				<string>0D457EF4-A96F-4760-A4DC-07F46AE409C6</string>
				<key>modifiers</key>
				<integer>0</integer>
				<key>modifiersubtext</key>
				<string></string>
				<key>sourceoutputuid</key>
				<string>770B9A97-3870-4524-BEF4-795B08C38059</string>
				<key>vitoclose</key>
				<false/>
			</dict>
			<dict>
				<key>destinationuid</key>
				<string>CC4D4EE8-FD80-4612-948E-378FB259148C</string>
				<key>modifiers</key>
				<integer>0</integer>
				<key>modifiersubtext</key>
				<string></string>
				<key>sourceoutputuid</key>
				<string>82716642-AD94-47F6-AC7B-B1578DABAB9A</string>
				<key>vitoclose</key>
				<false/>
			</dict>
			<dict>
				<key>destinationuid</key>
				<string>11608658-A256-4625-AAD6-517E03644231</string>
				<key>modifiers</key>
				<integer>0</integer>
				<key>modifiersubtext</key>
				<string></string>
				<key>sourceoutputuid</key>
				<string>50D356F8-9115-4738-8AD1-A5C868482186</string>
				<key>vitoclose</key>
				<false/>
			</dict>
			<dict>
				<key>destinationuid</key>
				<string>EE1CC8A9-8FDF-4BC9-A5C6-A4CF2F886D97</string>
				<key>modifiers</key>
				<integer>0</integer>
				<key>modifiersubtext</key>
				<string></string>
				<key>sourceoutputuid</key>
				<string>1213CDA2-9D5F-40C3-91F9-42DE831983E7</string>
				<key>vitoclose</key>
				<false/>
			</dict>
			<dict>
				<key>destinationuid</key>
				<string>50009EAD-DA7A-48FC-8F4F-C5EC1CAE4D97</string>
				<key>modifiers</key>
				<integer>0</integer>
				<key>modifiersubtext</key>
				<string></string>
				<key>sourceoutputuid</key>
				<string>379E6121-5B4C-4936-95C8-B4A0BAEF07C3</string>
				<key>vitoclose</key>
				<false/>
			</dict>
			<dict>
				<key>destinationuid</key>
				<string>CE59FD32-48E0-467C-AD19-1D9288B0DD2B</string>
				<key>modifiers</key>
				<integer>0</integer>
				<key>modifiersubtext</key>
				<string></string>
				<key>sourceoutputuid</key>
				<string>16447B01-A94F-420F-9E78-CB34F131700C</string>
				<key>vitoclose</key>
				<false/>
			</dict>
			<dict>
				<key>destinationuid</key>
				<string>4C28D7FF-5BFC-4A3B-BC5C-738D3069B9E1</string>
				<key>modifiers</key>
				<integer>0</integer>
				<key>modifiersubtext</key>
				<string></string>
				<key>sourceoutputuid</key>
				<string>01B754FE-3D13-4463-9FC6-D08841BBB800</string>
				<key>vitoclose</key>
				<false/>
			</dict>
		</array>
	</dict>
	<key>createdby</key>
	<string>Dean Jackson &lt;deanishe@deanishe.net&gt;</string>
	<key>description</key>
	<string>View upcoming events in Google Calendar</string>
	<key>disabled</key>
	<false/>
	<key>name</key>
	<string>Google Calendar View</string>
	<key>objects</key>
	<array>
		<dict>
			<key>config</key>
			<dict>
				<key>externaltriggerid</key>
				<string>config</string>
				<key>passinputasargument</key>
				<false/>
				<key>passvariables</key>
				<false/>
				<key>workflowbundleid</key>
				<string>self</string>
			</dict>
			<key>type</key>
			<string>alfred.workflow.output.callexternaltrigger</string>
			<key>uid</key>
			<string>9FA29DC3-2E67-45B6-B93B-F33F57EDF7E7</string>
			<key>version</key>
			<integer>1</integer>
		</dict>
		<dict>
			<key>config</key>
			<dict>
				<key>argumenttype</key>
				<integer>2</integer>
				<key>keyword</key>
				<string>gcal</string>
				<key>subtext</key>
				<string>Show upcoming events</string>
				<key>text</key>
				<string>Upcoming Events</string>
				<key>withspace</key>
				<false/>
			</dict>
			<key>type</key>
			<string>alfred.workflow.input.keyword</string>
			<key>uid</key>
			<string>998D04E2-9538-4970-9233-1E57F0929ED0</string>
			<key>version</key>
			<integer>1</integer>
		</dict>
		<dict>
			<key>config</key>
			<dict>
				<key>concurrently</key>
				<false/>
				<key>escaping</key>
				<integer>102</integer>
				<key>script</key>
				<string>./gcal clear</string>
				<key>scriptargtype</key>
				<integer>1</integer>
				<key>scriptfile</key>
				<string></string>
				<key>type</key>
				<integer>0</integer>
			</dict>
			<key>type</key>
			<string>alfred.workflow.action.script</string>
			<key>uid</key>
			<string>62F44B78-0D8E-4215-BB14-E3950B214795</string>
			<key>version</key>
			<integer>2</integer>
		</dict>
		<dict>
			<key>type</key>
			<string>alfred.workflow.utility.junction</string>
			<key>uid</key>
			<string>303E565E-8048-4ED5-BD89-7E58DF94BF3A</string>
			<key>version</key>
			<integer>1</integer>
		</dict>
		<dict>
			<key>config</key>
			<dict>
				<key>concurrently</key>
				<false/>
				<key>escaping</key>
				<integer>102</integer>
				<key>script</key>
				<string>./gcal update workflow</string>
				<key>scriptargtype</key>
				<integer>1</integer>
				<key>scriptfile</key>
				<string></string>
				<key>type</key>
				<integer>0</integer>
			</dict>
			<key>type</key>
			<string>alfred.workflow.action.script</string>
			<key>uid</key>
			<string>0553156D-6606-42C4-8BE8-18AE49A7A6D6</string>
			<key>version</key>
			<integer>2</integer>
		</dict>
		<dict>
			<key>config</key>
			<dict>
				<key>argumenttype</key>
				<integer>2</integer>
				<key>keyword</key>
				<string>today</string>
				<key>subtext</key>
				<string>Today's events from your Google Calendar(s)</string>
				<key>text</key>
				<string>Today's Events</string>
				<key>withspace</key>
				<false/>
			</dict>
			<key>type</key>
			<string>alfred.workflow.input.keyword</string>
			<key>uid</key>
			<string>BC6819C2-77D8-4E53-BA51-2787F2087BFD</string>
			<key>version</key>
			<integer>1</integer>
		</dict>
		<dict>
			<key>config</key>
			<dict>
				<key>lastpathcomponent</key>
				<false/>
				<key>onlyshowifquerypopulated</key>
				<true/>
				<key>removeextension</key>
				<false/>
				<key>text</key>
				<string>{query}</string>
				<key>title</key>
				<string>ERROR</string>
			</dict>
			<key>type</key>
			<string>alfred.workflow.output.notification</string>
			<key>uid</key>
			<string>F5C23C7D-94BA-400C-8004-EC304CE8818D</string>
			<key>version</key>
			<integer>1</integer>
		</dict>
		<dict>
			<key>config</key>
			<dict>
				<key>concurrently</key>
				<false/>
				<key>escaping</key>
				<integer>102</integer>
				<key>script</key>
				<string>date '+%Y-%m-%d'</string>
				<key>scriptargtype</key>
				<integer>1</integer>
				<key>scriptfile</key>
				<string></string>
				<key>type</key>
				<integer>0</integer>
			</dict>
			<key>type</key>
			<string>alfred.workflow.action.script</string>
			<key>uid</key>
			<string>14C00640-8D74-4090-AB64-5BDEA4489D4D</string>
			<key>version</key>
			<integer>2</integer>
		</dict>
		<dict>
			<key>config</key>
			<dict>
				<key>argumenttype</key>
				<integer>2</integer>
				<key>keyword</key>
				<string>tomorrow</string>
				<key>subtext</key>
				<string>Tomorrow's events from your Google Calendar(s)</string>
				<key>text</key>
				<string>Tomorrow's Events</string>
				<key>withspace</key>
				<false/>
			</dict>
			<key>type</key>
			<string>alfred.workflow.input.keyword</string>
			<key>uid</key>
			<string>65A55A9B-1414-464D-A0FB-9A2CF705164C</string>
			<key>version</key>
			<integer>1</integer>
		</dict>
		<dict>
			<key>config</key>
			<dict>
				<key>concurrently</key>
				<false/>
				<key>escaping</key>
				<integer>102</integer>
				<key>script</key>
				<string>date -v '+1d' '+%Y-%m-%d'</string>
				<key>scriptargtype</key>
				<integer>1</integer>
				<key>scriptfile</key>
				<string></string>
				<key>type</key>
				<integer>0</integer>
			</dict>
			<key>type</key>
			<string>alfred.workflow.action.script</string>
			<key>uid</key>
			<string>E77CCBEC-A3BA-4614-B1B0-12B880D25580</string>
			<key>version</key>
			<integer>2</integer>
		</dict>
		<dict>
			<key>config</key>
			<dict>
				<key>externaltriggerid</key>
				<string>action</string>
				<key>passinputasargument</key>
				<true/>
				<key>passvariables</key>
				<true/>
				<key>workflowbundleid</key>
				<string>self</string>
			</dict>
			<key>type</key>
			<string>alfred.workflow.output.callexternaltrigger</string>
			<key>uid</key>
			<string>7A027517-A35E-4028-89FF-50172EA74768</string>
			<key>version</key>
			<integer>1</integer>
		</dict>
		<dict>
			<key>config</key>
			<dict>
				<key>concurrently</key>
				<false/>
				<key>escaping</key>
				<integer>102</integer>
				<key>script</key>
				<string>test -n "$CALENDAR_APP" &amp;&amp; {
  ./gcal open --app="$CALENDAR_APP" "$1"
} || {
  ./gcal open "$1"
}</string>
				<key>scriptargtype</key>
				<integer>1</integer>
				<key>scriptfile</key>
				<string></string>
				<key>type</key>
				<integer>5</integer>
			</dict>
			<key>type</key>
			<string>alfred.workflow.action.script</string>
			<key>uid</key>
			<string>FE9B2118-827D-416D-9829-1A9DC2CAACA4</string>
			<key>version</key>
			<integer>2</integer>
		</dict>
		<dict>
			<key>config</key>
			<dict>
				<key>argument</key>
				<string>{query}</string>
				<key>passthroughargument</key>
				<false/>
				<key>variables</key>
				<dict>
					<key>action</key>
					<string>date</string>
				</dict>
			</dict>
			<key>type</key>
			<string>alfred.workflow.utility.argument</string>
			<key>uid</key>
			<string>BF14B152-A9EC-4FCE-97DA-F62C1C7846D3</string>
			<key>version</key>
			<integer>1</integer>
		</dict>
		<dict>
			<key>config</key>
			<dict>
				<key>type</key>
				<integer>0</integer>
			</dict>
			<key>type</key>
			<string>alfred.workflow.utility.transform</string>
			<key>uid</key>
			<string>CD8F2AB1-13E0-47E1-BFCA-701444818091</string>
			<key>version</key>
			<integer>1</integer>
		</dict>
		<dict>
			<key>type</key>
			<string>alfred.workflow.utility.hidealfred</string>
			<key>uid</key>
			<string>6CEAE7ED-F6DF-403F-988D-4E847AF569B3</string>
			<key>version</key>
			<integer>1</integer>
		</dict>
		<dict>
			<key>config</key>
			<dict>
				<key>argumenttype</key>
				<integer>2</integer>
				<key>keyword</key>
				<string>yesterday</string>
				<key>subtext</key>
				<string>Yesterday's events from your Google Calendar(s)</string>
				<key>text</key>
				<string>Yesterday's Events</string>
				<key>withspace</key>
				<false/>
			</dict>
			<key>type</key>
			<string>alfred.workflow.input.keyword</string>
			<key>uid</key>
			<string>78516575-8825-4598-A589-2F3475E25DF8</string>
			<key>version</key>
			<integer>1</integer>
		</dict>
		<dict>
			<key>config</key>
			<dict>
				<key>concurrently</key>
				<false/>
				<key>escaping</key>
				<integer>102</integer>
				<key>script</key>
				<string>date -v '-1d' '+%Y-%m-%d'</string>
				<key>scriptargtype</key>
				<integer>1</integer>
				<key>scriptfile</key>
				<string></string>
				<key>type</key>
				<integer>0</integer>
			</dict>
			<key>type</key>
			<string>alfred.workflow.action.script</string>
			<key>uid</key>
			<string>6CFB2AE8-7BAF-4DE6-B949-D0FF2DB50FD7</string>
			<key>version</key>
			<integer>2</integer>
		</dict>
		<dict>
			<key>config</key>
			<dict>
				<key>externaltriggerid</key>
				<string>calendars</string>
				<key>passinputasargument</key>
				<false/>
				<key>passvariables</key>
				<false/>
				<key>workflowbundleid</key>
				<string>self</string>
			</dict>
			<key>type</key>
			<string>alfred.workflow.output.callexternaltrigger</string>
			<key>uid</key>
			<string>05634575-6FA7-40A9-8772-47F71A9C0DFC</string>
			<key>version</key>
			<integer>1</integer>
		</dict>
		<dict>
			<key>config</key>
			<dict>
				<key>alfredfiltersresults</key>
				<false/>
				<key>alfredfiltersresultsmatchmode</key>
				<integer>0</integer>
				<key>argumenttreatemptyqueryasnil</key>
				<false/>
				<key>argumenttrimmode</key>
				<integer>0</integer>
				<key>argumenttype</key>
				<integer>1</integer>
				<key>escaping</key>
				<integer>102</integer>
				<key>keyword</key>
				<string>gdate</string>
				<key>queuedelaycustom</key>
				<integer>3</integer>
				<key>queuedelayimmediatelyinitially</key>
				<true/>
				<key>queuedelaymode</key>
				<integer>0</integer>
				<key>queuemode</key>
				<integer>1</integer>
				<key>runningsubtext</key>
				<string>Loading…</string>
				<key>script</key>
				<string>./gcal dates -- "$1"</string>
				<key>scriptargtype</key>
				<integer>1</integer>
				<key>scriptfile</key>
				<string></string>
				<key>subtext</key>
				<string>Enter a date</string>
				<key>title</key>
				<string>Events for a Specific Date</string>
				<key>type</key>
				<integer>0</integer>
				<key>withspace</key>
				<true/>
			</dict>
			<key>type</key>
			<string>alfred.workflow.input.scriptfilter</string>
			<key>uid</key>
			<string>09A5FA54-F59A-45B7-8B66-A08BC6AB8EAC</string>
			<key>version</key>
			<integer>3</integer>
		</dict>
		<dict>
			<key>config</key>
			<dict>
				<key>externaltriggerid</key>
				<string>action</string>
				<key>passinputasargument</key>
				<true/>
				<key>passvariables</key>
				<true/>
				<key>workflowbundleid</key>
				<string>self</string>
			</dict>
			<key>type</key>
			<string>alfred.workflow.output.callexternaltrigger</string>
			<key>uid</key>
			<string>969420B1-18E8-4F0B-AC39-06D038718311</string>
			<key>version</key>
			<integer>1</integer>
		</dict>
		<dict>
			<key>config</key>
			<dict>
				<key>alfredfiltersresults</key>
				<false/>
				<key>alfredfiltersresultsmatchmode</key>
				<integer>0</integer>
				<key>argumenttreatemptyqueryasnil</key>
				<false/>
				<key>argumenttrimmode</key>
				<integer>0</integer>
				<key>argumenttype</key>
				<integer>1</integer>
				<key>escaping</key>
				<integer>102</integer>
				<key>queuedelaycustom</key>
				<integer>3</integer>
				<key>queuedelayimmediatelyinitially</key>
				<true/>
				<key>queuedelaymode</key>
				<integer>0</integer>
				<key>queuemode</key>
				<integer>1</integer>
				<key>runningsubtext</key>
				<string></string>
				<key>script</key>
				<string>test -n "$date" &amp;&amp; {
  ./gcal events --date="$date" -- "$1"
} || {
  ./gcal events -- "$1"
}</string>
				<key>scriptargtype</key>
				<integer>1</integer>
				<key>scriptfile</key>
				<string></string>
				<key>subtext</key>
				<string></string>
				<key>title</key>
				<string></string>
				<key>type</key>
				<integer>5</integer>
				<key>withspace</key>
				<true/>
			</dict>
			<key>type</key>
			<string>alfred.workflow.input.scriptfilter</string>
			<key>uid</key>
			<string>8C1ADFA7-4A9A-4218-9125-F0D4C2763FEC</string>
			<key>version</key>
			<integer>3</integer>
		</dict>
		<dict>
			<key>config</key>
			<dict>
				<key>argument</key>
				<string></string>
				<key>passthroughargument</key>
				<false/>
				<key>variables</key>
				<dict>
					<key>date</key>
					<string>{query}</string>
				</dict>
			</dict>
			<key>type</key>
			<string>alfred.workflow.utility.argument</string>
			<key>uid</key>
			<string>0D457EF4-A96F-4760-A4DC-07F46AE409C6</string>
			<key>version</key>
			<integer>1</integer>
		</dict>
		<dict>
			<key>config</key>
			<dict>
				<key>conditions</key>
				<array>
					<dict>
						<key>inputstring</key>
						<string>{var:action}</string>
						<key>matchcasesensitive</key>
						<false/>
						<key>matchmode</key>
						<integer>0</integer>
						<key>matchstring</key>
						<string>clear</string>
						<key>outputlabel</key>
						<string>Clear</string>
						<key>uid</key>
						<string>D2BA16A8-E9C4-40E1-A365-9391D712D9DD</string>
					</dict>
					<dict>
						<key>inputstring</key>
						<string>{var:action}</string>
						<key>matchcasesensitive</key>
						<false/>
						<key>matchmode</key>
						<integer>0</integer>
						<key>matchstring</key>
						<string>update</string>
						<key>outputlabel</key>
						<string>Update</string>
						<key>uid</key>
						<string>EFC34E5C-A25D-47BA-82F6-67BD4E221FE7</string>
					</dict>
					<dict>
						<key>inputstring</key>
						<string>{var:action}</string>
						<key>matchcasesensitive</key>
						<false/>
						<key>matchmode</key>
						<integer>0</integer>
						<key>matchstring</key>
						<string>open</string>
						<key>outputlabel</key>
						<string>Open</string>
						<key>uid</key>
						<string>C4EC175E-156C-4866-8B6C-CAA71D1717B0</string>
					</dict>
					<dict>
						<key>inputstring</key>
						<string>{var:action}</string>
						<key>matchcasesensitive</key>
						<false/>
						<key>matchmode</key>
						<integer>0</integer>
						<key>matchstring</key>
						<string>calendars</string>
						<key>outputlabel</key>
						<string>Show Calendars</string>
						<key>uid</key>
						<string>0AFA406F-38EB-4B7A-9757-C525540B1120</string>
					</dict>
					<dict>
						<key>inputstring</key>
						<string>{var:action}</string>
						<key>matchcasesensitive</key>
						<false/>
						<key>matchmode</key>
						<integer>0</integer>
						<key>matchstring</key>
						<string>date</string>
						<key>outputlabel</key>
						<string>Events for Date</string>
						<key>uid</key>
						<string>770B9A97-3870-4524-BEF4-795B08C38059</string>
					</dict>
					<dict>
						<key>inputstring</key>
						<string>{var:action}</string>
						<key>matchcasesensitive</key>
						<false/>
						<key>matchmode</key>
						<integer>0</integer>
						<key>matchstring</key>
						<string>set</string>
						<key>outputlabel</key>
						<string>Save Workflow Variable</string>
						<key>uid</key>
						<string>82716642-AD94-47F6-AC7B-B1578DABAB9A</string>
					</dict>
					<dict>
						<key>inputstring</key>
						<string>{var:action}</string>
						<key>matchcasesensitive</key>
						<false/>
						<key>matchmode</key>
						<integer>0</integer>
						<key>matchstring</key>
						<string>logout</string>
						<key>outputlabel</key>
						<string>Log out from Account</string>
						<key>uid</key>
						<string>50D356F8-9115-4738-8AD1-A5C868482186</string>
					</dict>
					<dict>
						<key>inputstring</key>
						<string>{var:action}</string>
						<key>matchcasesensitive</key>
						<false/>
						<key>matchmode</key>
						<integer>0</integer>
						<key>matchstring</key>
						<string>toggle</string>
						<key>outputlabel</key>
						<string>Toggle Calendar On/Off</string>
						<key>uid</key>
						<string>1213CDA2-9D5F-40C3-91F9-42DE831983E7</string>
					</dict>
					<dict>
						<key>inputstring</key>
						<string>{var:action}</string>
						<key>matchcasesensitive</key>
						<false/>
						<key>matchmode</key>
						<integer>0</integer>
						<key>matchstring</key>
						<string>config</string>
						<key>outputlabel</key>
						<string>Show Configuration</string>
						<key>uid</key>
						<string>379E6121-5B4C-4936-95C8-B4A0BAEF07C3</string>
					</dict>
					<dict>
						<key>inputstring</key>
						<string>{var:action}</string>
						<key>matchcasesensitive</key>
						<false/>
						<key>matchmode</key>
						<integer>0</integer>
						<key>matchstring</key>
						<string>create</string>
						<key>outputlabel</key>
						<string>Create Event</string>
						<key>uid</key>
						<string>16447B01-A94F-420F-9E78-CB34F131700C</string>
					</dict>
					<dict>
						<key>inputstring</key>
						<string>{var:action}</string>
						<key>matchcasesensitive</key>
						<false/>
						<key>matchmode</key>
						<integer>0</integer>
						<key>matchstring</key>
						<string>reauth</string>
						<key>outputlabel</key>
						<string>Re-Authenticate Account</string>
						<key>uid</key>
						<string>01B754FE-3D13-4463-9FC6-D08841BBB800</string>
					</dict>
				</array>
				<key>elselabel</key>
				<string>else</string>
			</dict>
			<key>type</key>
			<string>alfred.workflow.utility.conditional</string>
			<key>uid</key>
			<string>FF2770C7-3B9E-49CF-9F1D-9F2A603CB11C</string>
			<key>version</key>
			<integer>1</integer>
		</dict>
		<dict>
			<key>config</key>
			<dict>
				<key>externaltriggerid</key>
				<string>config</string>
				<key>passinputasargument</key>
				<false/>
				<key>passvariables</key>
				<false/>
				<key>workflowbundleid</key>
				<string>self</string>
			</dict>
			<key>type</key>
			<string>alfred.workflow.output.callexternaltrigger</string>
			<key>uid</key>
			<string>2F7191FB-FE4C-4B0F-832C-A9BA3DE8F841</string>
			<key>version</key>
			<integer>1</integer>
		</dict>
		<dict>
			<key>config</key>
			<dict>
				<key>triggerid</key>
				<string>config</string>
			</dict>
			<key>type</key>
			<string>alfred.workflow.trigger.external</string>
			<key>uid</key>
			<string>5C03EE71-D6D9-42F5-B608-403F47234C56</string>
			<key>version</key>
			<integer>1</integer>
		</dict>
		<dict>
			<key>config</key>
			<dict>
				<key>externaltriggerid</key>
				<string>action</string>
				<key>passinputasargument</key>
				<true/>
				<key>passvariables</key>
				<true/>
				<key>workflowbundleid</key>
				<string>self</string>
			</dict>
			<key>type</key>
			<string>alfred.workflow.output.callexternaltrigger</string>
			<key>uid</key>
			<string>EAF06D56-D2F1-4FB9-B0D7-89D494AA865B</string>
			<key>version</key>
			<integer>1</integer>
		</dict>
		<dict>
			<key>config</key>
			<dict>
				<key>triggerid</key>
				<string>action</string>
			</dict>
			<key>type</key>
			<string>alfred.workflow.trigger.external</string>
			<key>uid</key>
			<string>D55FAAFD-ABA8-4B37-940B-CB883E3BB590</string>
			<key>version</key>
			<integer>1</integer>
		</dict>
		<dict>
			<key>config</key>
			<dict>
				<key>concurrently</key>
				<false/>
				<key>escaping</key>
				<integer>102</integer>
				<key>script</key>
				<string>./gcal set "$key" "$value"</string>
				<key>scriptargtype</key>
				<integer>1</integer>
				<key>scriptfile</key>
				<string></string>
				<key>type</key>
				<integer>0</integer>
			</dict>
			<key>type</key>
			<string>alfred.workflow.action.script</string>
			<key>uid</key>
			<string>CC4D4EE8-FD80-4612-948E-378FB259148C</string>
			<key>version</key>
			<integer>2</integer>
		</dict>
		<dict>
			<key>config</key>
			<dict>
				<key>alfredfiltersresults</key>
				<false/>
				<key>alfredfiltersresultsmatchmode</key>
				<integer>0</integer>
				<key>argumenttreatemptyqueryasnil</key>
				<false/>
				<key>argumenttrimmode</key>
				<integer>0</integer>
				<key>argumenttype</key>
				<integer>1</integer>
				<key>escaping</key>
				<integer>102</integer>
				<key>keyword</key>
				<string>gcalconf</string>
				<key>queuedelaycustom</key>
				<integer>3</integer>
				<key>queuedelayimmediatelyinitially</key>
				<true/>
				<key>queuedelaymode</key>
				<integer>0</integer>
				<key>queuemode</key>
				<integer>1</integer>
				<key>runningsubtext</key>
				<string>Loading…</string>
				<key>script</key>
				<string>./gcal config "$1"</string>
				<key>scriptargtype</key>
				<integer>1</integer>
				<key>scriptfile</key>
				<string></string>
				<key>subtext</key>
				<string>View and edit workflow settings</string>
				<key>title</key>
				<string>Google Calendar Config</string>
				<key>type</key>
				<integer>0</integer>
				<key>withspace</key>
				<true/>
			</dict>
			<key>inboundconfig</key>
			<dict>
				<key>inputmode</key>
				<integer>1</integer>
			</dict>
			<key>type</key>
			<string>alfred.workflow.input.scriptfilter</string>
			<key>uid</key>
			<string>3F938397-2CD9-45EB-B0CD-FC962DC9031F</string>
			<key>version</key>
			<integer>3</integer>
		</dict>
		<dict>
			<key>config</key>
			<dict>
				<key>argument</key>
				<string>.
/---- ACTION IN ----\
query={query}
variables={allvars}
\-------------------/</string>
				<key>cleardebuggertext</key>
				<false/>
				<key>processoutputs</key>
				<true/>
			</dict>
			<key>type</key>
			<string>alfred.workflow.utility.debug</string>
			<key>uid</key>
			<string>38618982-EFBD-4CEE-AB65-D33F0BBD3C54</string>
			<key>version</key>
			<integer>1</integer>
		</dict>
		<dict>
			<key>config</key>
			<dict>
				<key>concurrently</key>
				<false/>
				<key>escaping</key>
				<integer>102</integer>
				<key>script</key>
				<string>./gcal logout "$account"</string>
				<key>scriptargtype</key>
				<integer>1</integer>
				<key>scriptfile</key>
				<string></string>
				<key>type</key>
				<integer>0</integer>
			</dict>
			<key>type</key>
			<string>alfred.workflow.action.script</string>
			<key>uid</key>
			<string>11608658-A256-4625-AAD6-517E03644231</string>
			<key>version</key>
			<integer>2</integer>
		</dict>
		<dict>
			<key>config</key>
			<dict>
				<key>externaltriggerid</key>
				<string>config</string>
				<key>passinputasargument</key>
				<false/>
				<key>passvariables</key>
				<false/>
				<key>workflowbundleid</key>
				<string>self</string>
			</dict>
			<key>type</key>
			<string>alfred.workflow.output.callexternaltrigger</string>
			<key>uid</key>
			<string>511603A0-C36D-4AB7-9520-0BA422CE5F05</string>
			<key>version</key>
			<integer>1</integer>
		</dict>
		<dict>
			<key>config</key>
			<dict>
				<key>alfredfiltersresults</key>
				<false/>
				<key>alfredfiltersresultsmatchmode</key>
				<integer>0</integer>
				<key>argumenttreatemptyqueryasnil</key>
				<false/>
				<key>argumenttrimmode</key>
				<integer>0</integer>
				<key>argumenttype</key>
				<integer>1</integer>
				<key>escaping</key>
				<integer>102</integer>
				<key>keyword</key>
				<string>gnew</string>
				<key>queuedelaycustom</key>
				<integer>3</integer>
				<key>queuedelayimmediatelyinitially</key>
				<true/>
				<key>queuedelaymode</key>
				<integer>0</integer>
				<key>queuemode</key>
				<integer>1</integer>
				<key>runningsubtext</key>
				<string>Loading...</string>
				<key>script</key>
				<string>./gcal active "$1"</string>
				<key>scriptargtype</key>
				<integer>1</integer>
				<key>scriptfile</key>
				<string></string>
				<key>subtext</key>
				<string>Enter event details &amp; select calendar</string>
				<key>title</key>
				<string>Add New Event</string>
				<key>type</key>
				<integer>0</integer>
				<key>withspace</key>
				<true/>
			</dict>
			<key>type</key>
			<string>alfred.workflow.input.scriptfilter</string>
			<key>uid</key>
			<string>862EBB67-6E29-4467-AFE1-7E6717945CB6</string>
			<key>version</key>
			<integer>3</integer>
		</dict>
		<dict>
			<key>config</key>
			<dict>
				<key>externaltriggerid</key>
				<string>action</string>
				<key>passinputasargument</key>
				<false/>
				<key>passvariables</key>
				<true/>
				<key>workflowbundleid</key>
				<string>self</string>
			</dict>
			<key>type</key>
			<string>alfred.workflow.output.callexternaltrigger</string>
			<key>uid</key>
			<string>12391721-6113-4936-B640-C307F04B697C</string>
			<key>version</key>
			<integer>1</integer>
		</dict>
		<dict>
			<key>config</key>
			<dict>
				<key>triggerid</key>
				<string>create</string>
			</dict>
			<key>type</key>
			<string>alfred.workflow.trigger.external</string>
			<key>uid</key>
			<string>3CCB9C03-CCBD-43F3-A51F-C95DBBD1A21C</string>
			<key>version</key>
			<integer>1</integer>
		</dict>
		<dict>
			<key>config</key>
			<dict>
				<key>concurrently</key>
				<false/>
				<key>escaping</key>
				<integer>102</integer>
				<key>script</key>
				<string>./gcal toggle "$calendar"</string>
				<key>scriptargtype</key>
				<integer>1</integer>
				<key>scriptfile</key>
				<string></string>
				<key>type</key>
				<integer>0</integer>
			</dict>
			<key>type</key>
			<string>alfred.workflow.action.script</string>
			<key>uid</key>
			<string>EE1CC8A9-8FDF-4BC9-A5C6-A4CF2F886D97</string>
			<key>version</key>
			<integer>2</integer>
		</dict>
		<dict>
			<key>config</key>
			<dict>
				<key>externaltriggerid</key>
				<string>calendars</string>
				<key>passinputasargument</key>
				<false/>
				<key>passvariables</key>
				<false/>
				<key>workflowbundleid</key>
				<string>self</string>
			</dict>
			<key>type</key>
			<string>alfred.workflow.output.callexternaltrigger</string>
			<key>uid</key>
			<string>5EE513CB-126C-4EC9-8D51-7205A217CA03</string>
			<key>version</key>
			<integer>1</integer>
		</dict>
		<dict>
			<key>config</key>
			<dict>
				<key>externaltriggerid</key>
				<string>action</string>
				<key>passinputasargument</key>
				<true/>
				<key>passvariables</key>
				<true/>
				<key>workflowbundleid</key>
				<string>self</string>
			</dict>
			<key>type</key>
			<string>alfred.workflow.output.callexternaltrigger</string>
			<key>uid</key>
			<string>2512097E-AB92-489E-93AF-0146592CB0D4</string>
			<key>version</key>
			<integer>1</integer>
		</dict>
		<dict>
			<key>config</key>
			<dict>
				<key>alfredfiltersresults</key>
				<false/>
				<key>alfredfiltersresultsmatchmode</key>
				<integer>0</integer>
				<key>argumenttreatemptyqueryasnil</key>
				<false/>
				<key>argumenttrimmode</key>
				<integer>0</integer>
				<key>argumenttype</key>
				<integer>1</integer>
				<key>escaping</key>
				<integer>102</integer>
				<key>queuedelaycustom</key>
				<integer>3</integer>
				<key>queuedelayimmediatelyinitially</key>
				<true/>
				<key>queuedelaymode</key>
				<integer>0</integer>
				<key>queuemode</key>
				<integer>1</integer>
				<key>runningsubtext</key>
				<string></string>
				<key>script</key>
				<string>./gcal calendars "$1"</string>
				<key>scriptargtype</key>
				<integer>1</integer>
				<key>scriptfile</key>
				<string></string>
				<key>subtext</key>
				<string></string>
				<key>title</key>
				<string></string>
				<key>type</key>
				<integer>0</integer>
				<key>withspace</key>
				<false/>
			</dict>
			<key>type</key>
			<string>alfred.workflow.input.scriptfilter</string>
			<key>uid</key>
			<string>55D10CF4-8457-4AE5-9DC0-64A7E262D61D</string>
			<key>version</key>
			<integer>3</integer>
		</dict>
		<dict>
			<key>config</key>
			<dict>
				<key>triggerid</key>
				<string>calendars</string>
			</dict>
			<key>type</key>
			<string>alfred.workflow.trigger.external</string>
			<key>uid</key>
			<string>8604FB3C-23FB-467B-803B-17F6A73073AB</string>
			<key>version</key>
			<integer>1</integer>
		</dict>
		<dict>
			<key>config</key>
			<dict>
				<key>triggerid</key>
				<string>close</string>
			</dict>
			<key>type</key>
			<string>alfred.workflow.trigger.external</string>
			<key>uid</key>
			<string>57F4280F-D680-47D8-B5CA-ED9FFDA29082</string>
			<key>version</key>
			<integer>1</integer>
		</dict>
		<dict>
			<key>config</key>
			<dict>
				<key>externaltriggerid</key>
				<string>config</string>
				<key>passinputasargument</key>
				<false/>
				<key>passvariables</key>
				<false/>
				<key>workflowbundleid</key>
				<string>self</string>
			</dict>
			<key>type</key>
			<string>alfred.workflow.output.callexternaltrigger</string>
			<key>uid</key>
			<string>50009EAD-DA7A-48FC-8F4F-C5EC1CAE4D97</string>
			<key>version</key>
			<integer>1</integer>
		</dict>
		<dict>
			<key>type</key>
			<string>alfred.workflow.utility.hidealfred</string>
			<key>uid</key>
			<string>36D47E80-4BA8-4158-8B18-C1EDB477E6D7</string>
			<key>version</key>
			<integer>1</integer>
		</dict>
		<dict>
			<key>config</key>
			<dict>
				<key>externaltriggerid</key>
				<string>action</string>
				<key>passinputasargument</key>
				<true/>
				<key>passvariables</key>
				<true/>
				<key>workflowbundleid</key>
				<string>self</string>
			</dict>
			<key>type</key>
			<string>alfred.workflow.output.callexternaltrigger</string>
			<key>uid</key>
			<string>532E1785-14EE-4BE7-B64A-BE7E809EDDF8</string>
			<key>version</key>
			<integer>1</integer>
		</dict>
		<dict>
			<key>config</key>
			<dict>
				<key>concurrently</key>
				<false/>
				<key>escaping</key>
				<integer>102</integer>
				<key>script</key>
				<string>./gcal create "$quick" "$calendar"</string>
				<key>scriptargtype</key>
				<integer>1</integer>
				<key>scriptfile</key>
				<string></string>
				<key>type</key>
				<integer>0</integer>
			</dict>
			<key>type</key>
			<string>alfred.workflow.action.script</string>
			<key>uid</key>
			<string>CE59FD32-48E0-467C-AD19-1D9288B0DD2B</string>
			<key>version</key>
			<integer>2</integer>
		</dict>
		<dict>
			<key>config</key>
			<dict>
				<key>argument</key>
				<string></string>
				<key>passthroughargument</key>
				<false/>
				<key>variables</key>
				<dict>
					<key>action</key>
					<string>date</string>
				</dict>
			</dict>
			<key>type</key>
			<string>alfred.workflow.utility.argument</string>
			<key>uid</key>
			<string>92FC681E-1D4D-4E70-8010-84836DC8B371</string>
			<key>version</key>
			<integer>1</integer>
		</dict>
		<dict>
			<key>config</key>
			<dict>
				<key>externaltriggerid</key>
				<string>config</string>
				<key>passinputasargument</key>
				<false/>
				<key>passvariables</key>
				<false/>
				<key>workflowbundleid</key>
				<string>self</string>
			</dict>
			<key>type</key>
			<string>alfred.workflow.output.callexternaltrigger</string>
			<key>uid</key>
			<string>60FDD3AD-D600-4F4E-A43A-B413E83FC298</string>
			<key>version</key>
			<integer>1</integer>
		</dict>
		<dict>
			<key>config</key>
			<dict>
				<key>concurrently</key>
				<false/>
				<key>escaping</key>
				<integer>102</integer>
				<key>script</key>
				<string>./gcal reauth "$account"</string>
				<key>scriptargtype</key>
				<integer>1</integer>
				<key>scriptfile</key>
				<string></string>
				<key>type</key>
				<integer>0</integer>
			</dict>
			<key>type</key>
			<string>alfred.workflow.action.script</string>
			<key>uid</key>
			<string>4C28D7FF-5BFC-4A3B-BC5C-738D3069B9E1</string>
			<key>version</key>
			<integer>2</integer>
		</dict>
	</array>
	<key>readme</key>
	<string>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").</string>
	<key>uidata</key>
	<dict>
		<key>0553156D-6606-42C4-8BE8-18AE49A7A6D6</key>
		<dict>
			<key>note</key>
			<string>Check for new version of the workflow</string>
			<key>xpos</key>
			<integer>1180</integer>
			<key>ypos</key>
			<integer>200</integer>
		</dict>
		<key>05634575-6FA7-40A9-8772-47F71A9C0DFC</key>
		<dict>
			<key>xpos</key>
			<integer>1180</integer>
			<key>ypos</key>
			<integer>530</integer>
		</dict>
		<key>09A5FA54-F59A-45B7-8B66-A08BC6AB8EAC</key>
		<dict>
			<key>note</key>
			<string>Show events for a given date</string>
			<key>xpos</key>
			<integer>210</integer>
			<key>ypos</key>
			<integer>660</integer>
		</dict>
		<key>0D457EF4-A96F-4760-A4DC-07F46AE409C6</key>
		<dict>
			<key>note</key>
			<string>Set $date from {query}</string>
			<key>xpos</key>
			<integer>1220</integer>
			<key>ypos</key>
			<integer>700</integer>
		</dict>
		<key>11608658-A256-4625-AAD6-517E03644231</key>
		<dict>
			<key>note</key>
			<string>Remove account</string>
			<key>xpos</key>
			<integer>1180</integer>
			<key>ypos</key>
			<integer>1000</integer>
		</dict>
		<key>12391721-6113-4936-B640-C307F04B697C</key>
		<dict>
			<key>xpos</key>
			<integer>400</integer>
			<key>ypos</key>
			<integer>1010</integer>
		</dict>
		<key>14C00640-8D74-4090-AB64-5BDEA4489D4D</key>
		<dict>
			<key>xpos</key>
			<integer>210</integer>
			<key>ypos</key>
			<integer>200</integer>
		</dict>
		<key>2512097E-AB92-489E-93AF-0146592CB0D4</key>
		<dict>
			<key>xpos</key>
			<integer>400</integer>
			<key>ypos</key>
			<integer>1170</integer>
		</dict>
		<key>2F7191FB-FE4C-4B0F-832C-A9BA3DE8F841</key>
		<dict>
			<key>xpos</key>
			<integer>1360</integer>
			<key>ypos</key>
			<integer>830</integer>
		</dict>
		<key>303E565E-8048-4ED5-BD89-7E58DF94BF3A</key>
		<dict>
			<key>xpos</key>
			<integer>1400</integer>
			<key>ypos</key>
			<integer>150</integer>
		</dict>
		<key>36D47E80-4BA8-4158-8B18-C1EDB477E6D7</key>
		<dict>
			<key>xpos</key>
			<integer>250</integer>
			<key>ypos</key>
			<integer>1370</integer>
		</dict>
		<key>38618982-EFBD-4CEE-AB65-D33F0BBD3C54</key>
		<dict>
			<key>xpos</key>
			<integer>780</integer>
			<key>ypos</key>
			<integer>860</integer>
		</dict>
		<key>3CCB9C03-CCBD-43F3-A51F-C95DBBD1A21C</key>
		<dict>
			<key>xpos</key>
			<integer>40</integer>
			<key>ypos</key>
			<integer>1010</integer>
		</dict>
		<key>3F938397-2CD9-45EB-B0CD-FC962DC9031F</key>
		<dict>
			<key>note</key>
			<string>Show workflow configuration</string>
			<key>xpos</key>
			<integer>210</integer>
			<key>ypos</key>
			<integer>830</integer>
		</dict>
		<key>4C28D7FF-5BFC-4A3B-BC5C-738D3069B9E1</key>
		<dict>
			<key>note</key>
			<string>Re-authenticate account</string>
			<key>xpos</key>
			<integer>1180</integer>
			<key>ypos</key>
			<integer>1660</integer>
		</dict>
		<key>50009EAD-DA7A-48FC-8F4F-C5EC1CAE4D97</key>
		<dict>
			<key>xpos</key>
			<integer>1180</integer>
			<key>ypos</key>
			<integer>1355</integer>
		</dict>
		<key>511603A0-C36D-4AB7-9520-0BA422CE5F05</key>
		<dict>
			<key>xpos</key>
			<integer>1370</integer>
			<key>ypos</key>
			<integer>1000</integer>
		</dict>
		<key>532E1785-14EE-4BE7-B64A-BE7E809EDDF8</key>
		<dict>
			<key>xpos</key>
			<integer>1370</integer>
			<key>ypos</key>
			<integer>1510</integer>
		</dict>
		<key>55D10CF4-8457-4AE5-9DC0-64A7E262D61D</key>
		<dict>
			<key>note</key>
			<string>Toggle calendars on/off</string>
			<key>xpos</key>
			<integer>210</integer>
			<key>ypos</key>
			<integer>1170</integer>
		</dict>
		<key>57F4280F-D680-47D8-B5CA-ED9FFDA29082</key>
		<dict>
			<key>xpos</key>
			<integer>40</integer>
			<key>ypos</key>
			<integer>1340</integer>
		</dict>
		<key>5C03EE71-D6D9-42F5-B608-403F47234C56</key>
		<dict>
			<key>xpos</key>
			<integer>40</integer>
			<key>ypos</key>
			<integer>830</integer>
		</dict>
		<key>5EE513CB-126C-4EC9-8D51-7205A217CA03</key>
		<dict>
			<key>xpos</key>
			<integer>1370</integer>
			<key>ypos</key>
			<integer>1170</integer>
		</dict>
		<key>60FDD3AD-D600-4F4E-A43A-B413E83FC298</key>
		<dict>
			<key>xpos</key>
			<integer>1370</integer>
			<key>ypos</key>
			<integer>1660</integer>
		</dict>
		<key>62F44B78-0D8E-4215-BB14-E3950B214795</key>
		<dict>
			<key>note</key>
			<string>Clear old cache files</string>
			<key>xpos</key>
			<integer>1180</integer>
			<key>ypos</key>
			<integer>40</integer>
		</dict>
		<key>65A55A9B-1414-464D-A0FB-9A2CF705164C</key>
		<dict>
			<key>note</key>
			<string>Show tomorrow's events</string>
			<key>xpos</key>
			<integer>40</integer>
			<key>ypos</key>
			<integer>360</integer>
		</dict>
		<key>6CEAE7ED-F6DF-403F-988D-4E847AF569B3</key>
		<dict>
			<key>xpos</key>
			<integer>1400</integer>
			<key>ypos</key>
			<integer>400</integer>
		</dict>
		<key>6CFB2AE8-7BAF-4DE6-B949-D0FF2DB50FD7</key>
		<dict>
			<key>xpos</key>
			<integer>210</integer>
			<key>ypos</key>
			<integer>520</integer>
		</dict>
		<key>78516575-8825-4598-A589-2F3475E25DF8</key>
		<dict>
			<key>note</key>
			<string>Show yesterday's events</string>
			<key>xpos</key>
			<integer>40</integer>
			<key>ypos</key>
			<integer>520</integer>
		</dict>
		<key>7A027517-A35E-4028-89FF-50172EA74768</key>
		<dict>
			<key>xpos</key>
			<integer>760</integer>
			<key>ypos</key>
			<integer>360</integer>
		</dict>
		<key>8604FB3C-23FB-467B-803B-17F6A73073AB</key>
		<dict>
			<key>xpos</key>
			<integer>40</integer>
			<key>ypos</key>
			<integer>1170</integer>
		</dict>
		<key>862EBB67-6E29-4467-AFE1-7E6717945CB6</key>
		<dict>
			<key>note</key>
			<string>Quick-add a new event</string>
			<key>xpos</key>
			<integer>210</integer>
			<key>ypos</key>
			<integer>1010</integer>
		</dict>
		<key>8C1ADFA7-4A9A-4218-9125-F0D4C2763FEC</key>
		<dict>
			<key>note</key>
			<string>Show events</string>
			<key>xpos</key>
			<integer>1360</integer>
			<key>ypos</key>
			<integer>670</integer>
		</dict>
		<key>92FC681E-1D4D-4E70-8010-84836DC8B371</key>
		<dict>
			<key>note</key>
			<string>$action to "date"</string>
			<key>xpos</key>
			<integer>1310</integer>
			<key>ypos</key>
			<integer>1540</integer>
		</dict>
		<key>969420B1-18E8-4F0B-AC39-06D038718311</key>
		<dict>
			<key>xpos</key>
			<integer>1540</integer>
			<key>ypos</key>
			<integer>670</integer>
		</dict>
		<key>998D04E2-9538-4970-9233-1E57F0929ED0</key>
		<dict>
			<key>note</key>
			<string>Show upcoming events</string>
			<key>xpos</key>
			<integer>210</integer>
			<key>ypos</key>
			<integer>40</integer>
		</dict>
		<key>9FA29DC3-2E67-45B6-B93B-F33F57EDF7E7</key>
		<dict>
			<key>xpos</key>
			<integer>1540</integer>
			<key>ypos</key>
			<integer>40</integer>
		</dict>
		<key>BC6819C2-77D8-4E53-BA51-2787F2087BFD</key>
		<dict>
			<key>note</key>
			<string>Show today's events</string>
			<key>xpos</key>
			<integer>40</integer>
			<key>ypos</key>
			<integer>200</integer>
		</dict>
		<key>BF14B152-A9EC-4FCE-97DA-F62C1C7846D3</key>
		<dict>
			<key>note</key>
			<string>Set $action to "date"</string>
			<key>xpos</key>
			<integer>620</integer>
			<key>ypos</key>
			<integer>390</integer>
		</dict>
		<key>CC4D4EE8-FD80-4612-948E-378FB259148C</key>
		<dict>
			<key>note</key>
			<string>Change configuration</string>
			<key>xpos</key>
			<integer>1180</integer>
			<key>ypos</key>
			<integer>830</integer>
		</dict>
		<key>CD8F2AB1-13E0-47E1-BFCA-701444818091</key>
		<dict>
			<key>note</key>
			<string>Trim whitespace</string>
			<key>xpos</key>
			<integer>450</integer>
			<key>ypos</key>
			<integer>390</integer>
		</dict>
		<key>CE59FD32-48E0-467C-AD19-1D9288B0DD2B</key>
		<dict>
			<key>xpos</key>
			<integer>1180</integer>
			<key>ypos</key>
			<integer>1510</integer>
		</dict>
		<key>D55FAAFD-ABA8-4B37-940B-CB883E3BB590</key>
		<dict>
			<key>xpos</key>
			<integer>590</integer>
			<key>ypos</key>
			<integer>830</integer>
		</dict>
		<key>E77CCBEC-A3BA-4614-B1B0-12B880D25580</key>
		<dict>
			<key>xpos</key>
			<integer>210</integer>
			<key>ypos</key>
			<integer>360</integer>
		</dict>
		<key>EAF06D56-D2F1-4FB9-B0D7-89D494AA865B</key>
		<dict>
			<key>xpos</key>
			<integer>400</integer>
			<key>ypos</key>
			<integer>830</integer>
		</dict>
		<key>EE1CC8A9-8FDF-4BC9-A5C6-A4CF2F886D97</key>
		<dict>
			<key>note</key>
			<string>Toggle calendar</string>
			<key>xpos</key>
			<integer>1180</integer>
			<key>ypos</key>
			<integer>1170</integer>
		</dict>
		<key>F5C23C7D-94BA-400C-8004-EC304CE8818D</key>
		<dict>
			<key>xpos</key>
			<integer>1540</integer>
			<key>ypos</key>
			<integer>200</integer>
		</dict>
		<key>FE9B2118-827D-416D-9829-1A9DC2CAACA4</key>
		<dict>
			<key>note</key>
			<string>Open calendar URL</string>
			<key>xpos</key>
			<integer>1180</integer>
			<key>ypos</key>
			<integer>370</integer>
		</dict>
		<key>FF2770C7-3B9E-49CF-9F1D-9F2A603CB11C</key>
		<dict>
			<key>xpos</key>
			<integer>880</integer>
			<key>ypos</key>
			<integer>700</integer>
		</dict>
	</dict>
	<key>variables</key>
	<dict>
		<key>APPLE_MAPS</key>
		<string>0</string>
		<key>CALENDAR_APP</key>
		<string></string>
		<key>EVENT_CACHE_MINS</key>
		<string>15</string>
		<key>SCHEDULE_DAYS</key>
		<string>7</string>
		<key>TIME_12H</key>
		<string>0</string>
	</dict>
	<key>version</key>
	<string>0.5.1</string>
	<key>webaddress</key>
	<string></string>
</dict>
</plist>


================================================
FILE: magefile.go
================================================
// Copyright (c) 2019 Dean Jackson <deanishe@deanishe.net>
// 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 <deanishe@deanishe.net>
//
// 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 <deanishe@deanishe.net>
//
// 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 [<command>] [options] [<query>]

Usage:
    gcal dates [--] [<format>]
    gcal events [--date=<date>] [--] [<query>]
    gcal calendars [<query>]
    gcal active [<query>]
    gcal toggle <calID>
    gcal set <key> <value>
    gcal update (workflow|calendars|events) [<date>]
    gcal config [<query>]
    gcal logout <account>
    gcal reauth <account>
    gcal clear
    gcal open [--app=<app>] <url>
    gcal server
    gcal reload
    gcal create <quick> <calID>
    gcal -h

Options:
    -a --app <app>     Application to open URLs in.
    -d --date <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:"<calID>"`
	Date       string `docopt:"<date>,--date"`
	DateFormat string `docopt:"<format>"`
	Query      string
	URL        string `docopt:"<url>"`
	Key        string
	Value      string
	Quick      string `docopt:"<quick>"`

	// 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" }}
<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8" />
		<title>{{ .Title }}</title>
		<style>
			html {
				background-color: {{ .Colour }};
				color: #333;
				box-sizing: border-box;
				font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
				font-size: 16px;
			}

			html, body {
				height: 100%;
				margin: 0;
				padding: 0;
			}

			*, *:before, *:after {
			  box-sizing: inherit;
			}

			body, h1, h2, h3, h4, h5, h6, p, ol, ul {
			  margin: 0;
			  padding: 0;
			  font-weight: normal;
			}

			h1, h2, h3, h4, h5, h6 {
				font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
			}

			a, a:active, a:visited {
				color: #0000fc;
				/*font-weight: bold;*/
				text-decoration: none;
			}

			h1 > a, h1 > a:visited {
				/*text-decoration: none;*/
				color: #333;
			}

			body {
				background-color: #fdfdfd;
				margin-left: auto;
				margin-right: auto;
				max-width: 600px;
				padding: 32px 16px;
			}

			header {
				margin: 0 32px;
			}

			table {
				margin-top: 16px;
			}

			th, td {
				padding: 5px;
			}

			th {
				text-align: right;
			}

			ol, ul {
			  list-style: none;
			}

			img {
			  max-width: 100%;
			  height: auto;
			}
		</style>
	</head>
	<body>
		<header>
			<h1><a href="{{ .URL }}">{{ .Title }}</a></h1>
			<p>in {{ .CalendarTitle }}</p>
		</header>
		<table>
			<tr>
				<th>Date</th>
				<td>{{ .Start.Format "Monday, 2 Jan 2006" }}</td>
			</tr>
			<tr>
				<th>Time</th>
				<td>{{ .Start.Format "15:04" }} &ndash; {{ .End.Format "15:04" }}</td>
			</tr>
			{{ if .Location }}
			<tr>
				<th>Location</th>
				<td><a href="{{ .MapURL }}">{{ .Location }}</a></td>
			</tr>
			{{ end }}
			{{ if .Description }}
			<tr>
				<th>Description</th>
				<td>{{ .Description }}</td>
			</tr>
			{{ end }}
		</table>
	</body>
</html>
{{ end }}

{{ define "fail" }}
<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8" />
		<title>Event Not Found</title>
	</head>
	<body>
		<h1>Event Not Found</h1>
		<p>Couldn't find an event for ID <strong>{{ . }}</strong></p>
	</body>
</html>
{{ end }}

================================================
FILE: secret.go
================================================
//
// Copyright (c) 2017 Dean Jackson <deanishe@deanishe.net>
//
// 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"
  }
}
`
Download .txt
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
Download .txt
SYMBOL INDEX (123 symbols across 19 files)

FILE: account.go
  type Account (line 30) | type Account struct
    method CacheName (line 99) | func (a *Account) CacheName() string { return "account-" + a.Name + "....
    method IconPath (line 102) | func (a *Account) IconPath() string {
    method Icon (line 107) | func (a *Account) Icon() *aw.Icon {
    method Authenticator (line 117) | func (a *Account) Authenticator() *Authenticator {
    method Save (line 126) | func (a *Account) Save() error {
    method Service (line 135) | func (a *Account) Service() (*calendar.Service, error) {
    method FetchCalendars (line 154) | func (a *Account) FetchCalendars() error {
    method FetchEvents (line 195) | func (a *Account) FetchEvents(cal *Calendar, start time.Time) ([]*Even...
    method QuickAdd (line 261) | func (a *Account) QuickAdd(calendarID string, quick string) error {
    method handleAPIError (line 279) | func (a *Account) handleAPIError(err error) error {
  function NewAccount (line 52) | func NewAccount(name string) (*Account, error) {
  function LoadAccounts (line 68) | func LoadAccounts() ([]*Account, error) {
  type errorResponse (line 310) | type errorResponse struct
  type errorAuthentication (line 315) | type errorAuthentication struct
    method Error (line 322) | func (err errorAuthentication) Error() string {

FILE: auth.go
  constant authServerURL (line 32) | authServerURL = "localhost:61432"
  constant userEmailScope (line 34) | userEmailScope = "https://www.googleapis.com/auth/userinfo.email"
  type response (line 40) | type response struct
  type Authenticator (line 46) | type Authenticator struct
    method GetClient (line 65) | func (a *Authenticator) GetClient() (*http.Client, error) {
    method tokenFromWeb (line 146) | func (a *Authenticator) tokenFromWeb(cfg *oauth2.Config) error {
    method getUserInfo (line 170) | func (a *Authenticator) getUserInfo() error {
    method openAuthURL (line 229) | func (a *Authenticator) openAuthURL(cfg *oauth2.Config) error {
    method codeFromLocalServer (line 240) | func (a *Authenticator) codeFromLocalServer() (string, error) {
  function NewAuthenticator (line 60) | func NewAuthenticator(acc *Account, secret []byte) *Authenticator {

FILE: cmd_calendars.go
  function doListCalendars (line 28) | func doListCalendars() error {
  function doListWritableCalendars (line 110) | func doListWritableCalendars() error {
  function allCalendars (line 191) | func allCalendars() ([]*Calendar, error) {
  function activeCalendarIDs (line 225) | func activeCalendarIDs() (map[string]bool, error) {
  function activeCalendars (line 250) | func activeCalendars() ([]*Calendar, error) {
  function writableCalendars (line 283) | func writableCalendars() ([]*Calendar, error) {

FILE: cmd_config.go
  function doConfig (line 23) | func doConfig() error {
  function doToggle (line 152) | func doToggle() error {
  function doReauth (line 183) | func doReauth() error {
  function doLogout (line 205) | func doLogout() error {
  function doClear (line 260) | func doClear() error {
  function clearEvents (line 279) | func clearEvents() error {

FILE: cmd_dates.go
  function doDates (line 31) | func doDates() error {
  function midnight (line 90) | func midnight(t time.Time) time.Time {
  function parseDate (line 100) | func parseDate(s string) (time.Time, bool) {
  function relativeDays (line 161) | func relativeDays(t time.Time, names bool) string {
  function relativeDate (line 203) | func relativeDate(t time.Time) string {

FILE: cmd_dates_test.go
  function TestParseDate (line 33) | func TestParseDate(t *testing.T) {

FILE: cmd_events.go
  function doEvents (line 23) | func doEvents() error {
  function loadEvents (line 200) | func loadEvents(t time.Time, cal ...*Calendar) ([]*Event, error) {

FILE: cmd_open.go
  function doOpen (line 19) | func doOpen() error {

FILE: cmd_quickadd.go
  function quickAdd (line 16) | func quickAdd() error {
  function createEvent (line 31) | func createEvent(quick string, calendarID string) error {

FILE: cmd_reload.go
  function doReload (line 12) | func doReload() error {

FILE: cmd_server.go
  constant previewServerURL (line 24) | previewServerURL = "localhost:61433"
  constant quitAfter (line 25) | quitAfter        = 90 * time.Second
  function previewURL (line 29) | func previewURL(t time.Time, eventID string) string {
  function doStartServer (line 39) | func doStartServer() error {

FILE: cmd_set.go
  function doSet (line 14) | func doSet() error {

FILE: cmd_update.go
  function doUpdateWorkflow (line 28) | func doUpdateWorkflow() error {
  function doUpdateCalendars (line 37) | func doUpdateCalendars() error {
  function doUpdateEvents (line 69) | func doUpdateEvents() error {
  function clearOldFiles (line 167) | func clearOldFiles() error {

FILE: events.go
  constant gMapsURL (line 18) | gMapsURL = "https://www.google.com/maps/search/?api=1"
  constant aMapsURL (line 19) | aMapsURL = "http://maps.apple.com/"
  type Calendar (line 23) | type Calendar struct
  type CalsByTitle (line 33) | type CalsByTitle
    method Len (line 35) | func (s CalsByTitle) Len() int           { return len(s) }
    method Less (line 36) | func (s CalsByTitle) Less(i, j int) bool { return s[i].Title < s[j].Ti...
    method Swap (line 37) | func (s CalsByTitle) Swap(i, j int)      { s[i], s[j] = s[j], s[i] }
  type Event (line 40) | type Event struct
    method Duration (line 56) | func (e *Event) Duration() time.Duration { return e.End.Sub(e.Start) }
    method String (line 58) | func (e *Event) String() string {
  type EventsByStart (line 64) | type EventsByStart
    method Len (line 66) | func (s EventsByStart) Len() int           { return len(s) }
    method Less (line 67) | func (s EventsByStart) Less(i, j int) bool { return s[i].Start.Before(...
    method Swap (line 68) | func (s EventsByStart) Swap(i, j int)      { s[i], s[j] = s[j], s[i] }
  function mapURL (line 71) | func mapURL(location string) string {
  function googleMapsURL (line 81) | func googleMapsURL(location string) string {
  function appleMapsURL (line 89) | func appleMapsURL(location string) string {

FILE: icons.go
  function init (line 57) | func init() {
  function ColouredIcon (line 67) | func ColouredIcon(icon *aw.Icon, colour string) *aw.Icon {
  function download (line 106) | func download(url, path string) error {
  function generateIcon (line 133) | func generateIcon(src, dest string, c color.RGBA) error {
  function iconCachePath (line 169) | func iconCachePath(i *aw.Icon, c color.RGBA) string {
  function ParseHexColour (line 182) | func ParseHexColour(s string) (color.RGBA, error) {
  function ReloadIcon (line 218) | func ReloadIcon() *aw.Icon {

FILE: magefile.go
  function init (line 30) | func init() {
  function mod (line 39) | func mod(args ...string) error {
  function Build (line 53) | func Build() error {
  function Run (line 75) | func Run() error {
  function Dist (line 82) | func Dist() error {
  function Link (line 93) | func Link() error {
  function Deps (line 105) | func Deps() error {
  function Clean (line 112) | func Clean() {
  function cleanDeps (line 117) | func cleanDeps() error { return mod("tidy", "-v") }
  function cleanDir (line 119) | func cleanDir(name string) error {
  function CleanIcons (line 138) | func CleanIcons() error { return cleanDir("./icons") }
  function cleanBuild (line 140) | func cleanBuild() error { return cleanDir("./build") }
  function cleanMage (line 141) | func cleanMage() error  { return sh.Run("mage", "-clean") }

FILE: magic.go
  type calendarMagic (line 17) | type calendarMagic struct
    method Keyword (line 19) | func (cm *calendarMagic) Keyword() string     { return "calendars" }
    method Description (line 20) | func (cm *calendarMagic) Description() string { return "Activate/deact...
    method RunText (line 21) | func (cm *calendarMagic) RunText() string     { return "Opening calend...
    method Run (line 22) | func (cm *calendarMagic) Run() error          { return aw.NewAlfred()....
  type loginMagic (line 25) | type loginMagic struct
    method Keyword (line 27) | func (lm *loginMagic) Keyword() string     { return "login" }
    method Description (line 28) | func (lm *loginMagic) Description() string { return "Add a Google acco...
    method RunText (line 29) | func (lm *loginMagic) RunText() string     { return "Opening Google si...
    method Run (line 30) | func (lm *loginMagic) Run() error {

FILE: main.go
  constant timeFormat (line 27) | timeFormat     = "2006-01-02"
  constant timeFormatLong (line 28) | timeFormatLong = "Monday, 2 January 2006"
  constant yellow (line 31) | yellow = "f8ac30"
  constant repo (line 37) | repo      = "deanishe/alfred-gcal"
  constant helpURL (line 38) | helpURL   = "https://github.com/deanishe/alfred-gcal/issues"
  constant readmeURL (line 39) | readmeURL = "https://github.com/deanishe/alfred-gcal/blob/master/README.md"
  constant forumURL (line 40) | forumURL  = "https://www.alfredforum.com/topic/11016-google-calendar-view/"
  constant usage (line 43) | usage = `
  type options (line 85) | type options struct
    method MaxAgeCalendar (line 131) | func (opts *options) MaxAgeCalendar() time.Duration { return time.Hour...
    method MaxAgeEvents (line 133) | func (opts *options) MaxAgeEvents() time.Duration {
    method ScheduleDuration (line 142) | func (opts *options) ScheduleDuration() time.Duration {
  function init (line 146) | func init() {
  function parseFlags (line 156) | func parseFlags() error {
  function run (line 197) | func run() {
  function main (line 276) | func main() {

FILE: secret.go
  constant secret (line 23) | secret = `
Condensed preview — 31 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (199K chars).
[
  {
    "path": ".gitignore",
    "chars": 163,
    "preview": "/gcal\n/build\n/dist\n/vendor\n/.autoenv.zsh\n/.autoenv_leave.zsh\n/*_private.go\n/tags\n\n# vim turds\n[._]*.s[a-v][a-z]\n[._]*.sw"
  },
  {
    "path": ".golangci.toml",
    "chars": 644,
    "preview": "[run]\ndeadline = \"5m\"\n\n[linters]\ndisable-all = true\nenable = [\n  \"deadcode\",\n  \"goconst\",\n  \"gocritic\",\n  \"gofmt\",\n  \"go"
  },
  {
    "path": "LICENCE.txt",
    "chars": 1103,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2017 Dean Jackson <deanishe@deanishe.net>\n\nPermission is hereby granted, free of ch"
  },
  {
    "path": "README.md",
    "chars": 6626,
    "preview": "\n<div align=\"center\">\n    <img height=\"128\" width=\"128\" src=\"https://raw.githubusercontent.com/deanishe/alfred-gcal/mast"
  },
  {
    "path": "TODO.taskpaper",
    "chars": 293,
    "preview": "Bugs:\n\t- Clear cache after selected calendars change\n\tCurrently, workflow shows events from inactive calendars and no ev"
  },
  {
    "path": "account.go",
    "chars": 7727,
    "preview": "// Copyright (c) 2019 Dean Jackson <deanishe@deanishe.net>\n// MIT Licence applies http://opensource.org/licenses/MIT\n\npa"
  },
  {
    "path": "auth.go",
    "chars": 7982,
    "preview": "//\n// Copyright (c) 2017 Dean Jackson <deanishe@deanishe.net>\n//\n// MIT Licence. See http://opensource.org/licenses/MIT\n"
  },
  {
    "path": "cmd_calendars.go",
    "chars": 5993,
    "preview": "//\n// Copyright (c) 2017 Dean Jackson <deanishe@deanishe.net>\n//\n// MIT Licence. See http://opensource.org/licenses/MIT\n"
  },
  {
    "path": "cmd_config.go",
    "chars": 6670,
    "preview": "//\n// Copyright (c) 2017 Dean Jackson <deanishe@deanishe.net>\n//\n// MIT Licence. See http://opensource.org/licenses/MIT\n"
  },
  {
    "path": "cmd_dates.go",
    "chars": 3948,
    "preview": "//\n// Copyright (c) 2017 Dean Jackson <deanishe@deanishe.net>\n//\n// MIT Licence. See http://opensource.org/licenses/MIT\n"
  },
  {
    "path": "cmd_dates_test.go",
    "chars": 967,
    "preview": "//\n// Copyright (c) 2017 Dean Jackson <deanishe@deanishe.net>\n//\n// MIT Licence. See http://opensource.org/licenses/MIT\n"
  },
  {
    "path": "cmd_events.go",
    "chars": 4879,
    "preview": "//\n// Copyright (c) 2017 Dean Jackson <deanishe@deanishe.net>\n//\n// MIT Licence. See http://opensource.org/licenses/MIT\n"
  },
  {
    "path": "cmd_open.go",
    "chars": 657,
    "preview": "//\n// Copyright (c) 2017 Dean Jackson <deanishe@deanishe.net>\n//\n// MIT Licence. See http://opensource.org/licenses/MIT\n"
  },
  {
    "path": "cmd_quickadd.go",
    "chars": 821,
    "preview": "//\n// Copyright (c) 2017 Dean Jackson <deanishe@deanishe.net>\n//\n// MIT Licence. See http://opensource.org/licenses/MIT\n"
  },
  {
    "path": "cmd_reload.go",
    "chars": 308,
    "preview": "//\n// Copyright (c) 2017 Dean Jackson <deanishe@deanishe.net>\n//\n// MIT Licence. See http://opensource.org/licenses/MIT\n"
  },
  {
    "path": "cmd_server.go",
    "chars": 3079,
    "preview": "//\n// Copyright (c) 2017 Dean Jackson <deanishe@deanishe.net>\n//\n// MIT Licence. See http://opensource.org/licenses/MIT\n"
  },
  {
    "path": "cmd_set.go",
    "chars": 552,
    "preview": "// Copyright (c) 2019 Dean Jackson <deanishe@deanishe.net>\n// MIT Licence applies http://opensource.org/licenses/MIT\n\npa"
  },
  {
    "path": "cmd_update.go",
    "chars": 4712,
    "preview": "//\n// Copyright (c) 2017 Dean Jackson <deanishe@deanishe.net>\n//\n// MIT Licence. See http://opensource.org/licenses/MIT\n"
  },
  {
    "path": "env.sh",
    "chars": 1147,
    "preview": "\n# When sourced, creates an Alfred-like environment needed by modd\n# and ./bin/build (which sources the file itself)\n\n# "
  },
  {
    "path": "events.go",
    "chars": 2655,
    "preview": "//\n// Copyright (c) 2017 Dean Jackson <deanishe@deanishe.net>\n//\n// MIT Licence. See http://opensource.org/licenses/MIT\n"
  },
  {
    "path": "go.mod",
    "chars": 632,
    "preview": "module github.com/deanishe/alfred-gcal\n\nrequire (\n\tcloud.google.com/go v0.61.0 // indirect\n\tgithub.com/deanishe/awgo v0."
  },
  {
    "path": "go.sum",
    "chars": 42293,
    "preview": "cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.34.0 h1:eOI3/c"
  },
  {
    "path": "icons.go",
    "chars": 5641,
    "preview": "//\n// Copyright (c) 2017 Dean Jackson <deanishe@deanishe.net>\n//\n// MIT Licence. See http://opensource.org/licenses/MIT\n"
  },
  {
    "path": "info.plist",
    "chars": 55336,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "magefile.go",
    "chars": 2786,
    "preview": "// Copyright (c) 2019 Dean Jackson <deanishe@deanishe.net>\n// MIT Licence applies http://opensource.org/licenses/MIT\n\n//"
  },
  {
    "path": "magic.go",
    "chars": 1524,
    "preview": "//\n// Copyright (c) 2017 Dean Jackson <deanishe@deanishe.net>\n//\n// MIT Licence. See http://opensource.org/licenses/MIT\n"
  },
  {
    "path": "main.go",
    "chars": 6069,
    "preview": "//\n// Copyright (c) 2017 Dean Jackson <deanishe@deanishe.net>\n//\n// MIT Licence. See http://opensource.org/licenses/MIT\n"
  },
  {
    "path": "modd.conf",
    "chars": 211,
    "preview": "\nmagefile.go\nmagefile_*.go {\n    prep: mage -l\n}\n\nmodd.conf\n*.go\n*.html\n./bin/build\n!mage*.go\n!vendor/**\n!secret* {\n\tpre"
  },
  {
    "path": "preview.html",
    "chars": 2088,
    "preview": "{{ define \"event\" }}\n<!DOCTYPE html>\n<html>\n\t<head>\n\t\t<meta charset=\"UTF-8\" />\n\t\t<title>{{ .Title }}</title>\n\t\t<style>\n\t"
  },
  {
    "path": "secret.go",
    "chars": 973,
    "preview": "//\n// Copyright (c) 2017 Dean Jackson <deanishe@deanishe.net>\n//\n// MIT Licence. See http://opensource.org/licenses/MIT\n"
  }
]

// ... and 1 more files (download for full content)

About this extraction

This page contains the full source code of the deanishe/alfred-gcal GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 31 files (174.3 KB), approximately 69.2k tokens, and a symbol index with 123 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!