From afc3b68480ea6d00f1e4b3494bf1f55874333145 Mon Sep 17 00:00:00 2001 From: Dean Jackson Date: Thu, 30 Nov 2017 14:36:03 +0100 Subject: [PATCH] Initial commit --- .gitignore | 69 ++ LICENCE.txt | 21 + README.md | 112 +++ alfred_env.sh | 16 + auth.go | 175 ++++ bin/build | 139 ++++ bin/icons | 137 ++++ cmd_calendars.go | 132 ++++ cmd_config.go | 130 +++ cmd_dates.go | 188 +++++ cmd_dates_test.go | 58 ++ cmd_events.go | 191 +++++ cmd_open.go | 30 + cmd_server.go | 132 ++++ cmd_update.go | 133 ++++ events.go | 202 +++++ gcal.go | 303 +++++++ icon.png | 1 + icons.go | 225 ++++++ icons/calendar-off.png | Bin 0 -> 2611 bytes icons/calendar-on.png | Bin 0 -> 2265 bytes icons/day.png | Bin 0 -> 7267 bytes icons/docs.png | Bin 0 -> 5591 bytes icons/help.png | Bin 0 -> 5528 bytes icons/icon.png | Bin 0 -> 1718 bytes icons/icons.txt | 31 + icons/issue.png | Bin 0 -> 4841 bytes icons/map.png | Bin 0 -> 5506 bytes icons/next.png | Bin 0 -> 5701 bytes icons/off.png | Bin 0 -> 6141 bytes icons/on.png | Bin 0 -> 7137 bytes icons/previous.png | Bin 0 -> 5775 bytes icons/reload.png | Bin 0 -> 5657 bytes icons/trash.png | Bin 0 -> 2668 bytes icons/update-available.png | Bin 0 -> 3856 bytes icons/update-ok.png | Bin 0 -> 4563 bytes icons/url.png | Bin 0 -> 8395 bytes info.plist | 1539 ++++++++++++++++++++++++++++++++++++ magic.go | 25 + modd.conf | 3 + preview.html | 124 +++ secret.go | 25 + 42 files changed, 4141 insertions(+) create mode 100644 .gitignore create mode 100644 LICENCE.txt create mode 100644 README.md create mode 100644 alfred_env.sh create mode 100644 auth.go create mode 100755 bin/build create mode 100755 bin/icons create mode 100644 cmd_calendars.go create mode 100644 cmd_config.go create mode 100644 cmd_dates.go create mode 100644 cmd_dates_test.go create mode 100644 cmd_events.go create mode 100644 cmd_open.go create mode 100644 cmd_server.go create mode 100644 cmd_update.go create mode 100644 events.go create mode 100644 gcal.go create mode 120000 icon.png create mode 100644 icons.go create mode 100644 icons/calendar-off.png create mode 100644 icons/calendar-on.png create mode 100644 icons/day.png create mode 100644 icons/docs.png create mode 100644 icons/help.png create mode 100644 icons/icon.png create mode 100644 icons/icons.txt create mode 100644 icons/issue.png create mode 100644 icons/map.png create mode 100644 icons/next.png create mode 100644 icons/off.png create mode 100644 icons/on.png create mode 100644 icons/previous.png create mode 100644 icons/reload.png create mode 100644 icons/trash.png create mode 100644 icons/update-available.png create mode 100644 icons/update-ok.png create mode 100644 icons/url.png create mode 100644 info.plist create mode 100644 magic.go create mode 100644 modd.conf create mode 100644 preview.html create mode 100644 secret.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9d2311c --- /dev/null +++ b/.gitignore @@ -0,0 +1,69 @@ +/gcal +/build +.autoenv* + +# Created by https://www.gitignore.io/api/go,sublimetext,vim + +### Go ### +# Binaries for programs and plugins +*.exe +*.dll +*.so +*.dylib + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 +.glide/ + +### SublimeText ### +# cache files for sublime text +*.tmlanguage.cache +*.tmPreferences.cache +*.stTheme.cache + +# workspace files are user-specific +*.sublime-workspace + +# project files should be checked into the repository, unless a significant +# proportion of contributors will probably not be using SublimeText +# *.sublime-project + +# sftp configuration file +sftp-config.json + +# Package control specific files +Package Control.last-run +Package Control.ca-list +Package Control.ca-bundle +Package Control.system-ca-bundle +Package Control.cache/ +Package Control.ca-certs/ +Package Control.merged-ca-bundle +Package Control.user-ca-bundle +oscrypto-ca-bundle.crt +bh_unicode_properties.cache + +# Sublime-github package stores a github token in this file +# https://packagecontrol.io/packages/sublime-github +GitHub.sublime-settings + +### Vim ### +# swap +[._]*.s[a-v][a-z] +[._]*.sw[a-p] +[._]s[a-v][a-z] +[._]sw[a-p] +# session +Session.vim +# temporary +.netrwhist +*~ +# auto-generated tag files +tags + +# End of https://www.gitignore.io/api/go,sublimetext,vim diff --git a/LICENCE.txt b/LICENCE.txt new file mode 100644 index 0000000..51eec0d --- /dev/null +++ b/LICENCE.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2017 Dean Jackson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..7668b6c --- /dev/null +++ b/README.md @@ -0,0 +1,112 @@ + +
+ +
+ +Google Calendar for Alfred +========================== + +View Google Calendar events in [Alfred][alfred]. + + + +- [Download & installation](#download--installation) +- [Usage](#usage) + - [Date format](#date-format) +- [Configuration](#configuration) +- [Licensing & thanks](#licensing--thanks) + + + + + +Download & installation +----------------------- + +Grab the workflow from [GitHub releases][download]. Download the `Google-Calendar-Events-X.X.alfredworkflow` file and double-click it to install. + + + +Usage +----- + +When run, the workflow will open Google Calendar in your browser and ask for permission to read your calendars. If you do not grant permission, it won't work. + +You will also be prompted to activate some calendars (the workflow will show events from these calendars). + +- `gcal` — Show upcoming events. + - `` — Filter list of events. + - `↩` — Open event in browser or day in workflow. + - `⌘↩` — Open event in Google or Apple Maps (if event has a location). + - `⇧` / `⌘Y` — Quicklook event details. +- `today` / `tomorrow` / `yesterday` — Show events for the given day. + - `` / `↩` / `⌘↩` / `⇧` / `⌘Y` — As above. +- `gdate []` — Show one or more dates. See below for query format. + - `↩` — Show events for the given day. +- `gcalconf []` — Show workflow configuration. + - `Active Calendars` — Turn calendars on/off. + - `↩` — Toggle calendar on/off. + - `Workflow is up to Date` / `An Update is Available` — Whether a newer version of the workflow is available. + - `↩` — Check for or install update. + - `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 data. + + + +### Date format ### + +The keyword `gdate` supports an optional date. This can be specified in a number of 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 + + +Configuration +------------- + +There are a couple of options in the workflow's configuration sheet (the `[x]` button in Alfred Preferences): + +| Setting | Description | +|--------------------|-------------------------------------------------------------------------------------------------------------| +| `APPLE_MAPS` | Set to `1` to open map links in Apple Maps instead of Google Maps. | +| `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. | + + + +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 [Elusive Icons][elusive], [Font Awesome][awesome], [Material Icons][material], [Weather Icons][weather] (all [SIL][sil]) and [Octicons][octicons] ([MIT][mit]), via the [workflow icon generator][icongen]. + + +[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/ +[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 +[elusive]: https://github.com/aristath/elusive-iconfont +[awesome]: http://fortawesome.github.io/Font-Awesome/ +[material]: http://zavoloklom.github.io/material-design-iconic-font/ +[octicons]: https://octicons.github.com/ +[weather]: https://erikflowers.github.io/weather-icons/ +[icongen]: http://icons.deanishe.net diff --git a/alfred_env.sh b/alfred_env.sh new file mode 100644 index 0000000..e927762 --- /dev/null +++ b/alfred_env.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +# When sourced, creates an Alfred-like environment needed by modd +# and ./bin/build (which sources the file itself) + +# getvar | Read a value from info.plist +getvar() { + local v="$1" + /usr/libexec/PlistBuddy -c "Print :$v" info.plist +} + +export alfred_workflow_bundleid=$( getvar "bundleid" ) +export alfred_workflow_version=$( getvar "version" ) +export alfred_workflow_name=$( getvar "name" ) +export SCHEDULE_DAYS=$( getvar "variables:SCHEDULE_DAYS" ) +export EVENT_CACHE_MINS=$( getvar "variables:EVENT_CACHE_MINS" ) diff --git a/auth.go b/auth.go new file mode 100644 index 0000000..7d6dcd0 --- /dev/null +++ b/auth.go @@ -0,0 +1,175 @@ +// +// Copyright (c) 2017 Dean Jackson +// +// MIT Licence. See http://opensource.org/licenses/MIT +// +// Created on 2017-11-25 +// + +package main + +import ( + "crypto/rand" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "os" + "os/exec" + + "golang.org/x/net/context" + "golang.org/x/oauth2" + "golang.org/x/oauth2/google" + "google.golang.org/api/calendar/v3" +) + +const ( + authServerURL = "localhost:61432" +) + +type response struct { + code string + err error +} + +// Authenticator creates an authenticated Google API client +type Authenticator struct { + Secret []byte + TokenFile string + state string + client *http.Client +} + +// NewAuthenticator creates a new Authenticator +func NewAuthenticator(tokenFile string, secret []byte) *Authenticator { + return &Authenticator{Secret: secret, TokenFile: tokenFile} +} + +// GetClient returns an authenticated Google API client +func (a *Authenticator) GetClient() (*http.Client, error) { + 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, calendar.CalendarReadonlyScope) + if err != nil { + return nil, fmt.Errorf("couldn't load config: %v", err) + } + + tok, err := a.tokenFromFile() + if err != nil { + tok, err = a.tokenFromWeb(cfg) + if err != nil { + return nil, fmt.Errorf("couldn't get token from web: %v", err) + } + a.saveToken(tok) + } + + a.client = cfg.Client(ctx, tok) + 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("couldn't 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("couldn't 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) (*oauth2.Token, error) { + if err := a.openAuthURL(cfg); err != nil { + return nil, fmt.Errorf("couldn't open auth URL: %v", err) + } + + code, err := a.codeFromLocalServer() + if err != nil { + return nil, fmt.Errorf("couldn't get token from local server: %v", err) + } + + tok, err := cfg.Exchange(oauth2.NoContext, code) + if err != nil { + return nil, fmt.Errorf("couldn't retrieve token from web: %v", err) + } + return tok, 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("couldn't 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) { + c := make(chan response) + srv := &http.Server{Addr: authServerURL} + + go func() { + log.Printf("local webserver started") + if err := srv.ListenAndServe(); err != nil { + c <- response{err: err} + } + }() + + http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { + vars := req.URL.Query() + code := vars.Get("code") + state := vars.Get("state") + log.Printf("oauth2 state=%v", state) + log.Printf("oauth2 code=%s", code) + + // Verify state to prevent CSRF + if state != a.state { + c <- response{err: fmt.Errorf("state mismatch: expected=%s, got=%s", a.state, state)} + io.WriteString(w, "bad state\n") + return + } + + c <- response{code: code} + io.WriteString(w, "ok\n") + }) + + r := <-c + + // 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("local webserver error: %v", err) + } + } + log.Printf("local webserver stopped") + + return r.code, r.err +} diff --git a/bin/build b/bin/build new file mode 100755 index 0000000..42d20f9 --- /dev/null +++ b/bin/build @@ -0,0 +1,139 @@ +#!/usr/bin/env zsh + +# Path to this script's directory (i.e. workflow root) +here="$( cd "$( dirname "$0" )"; pwd )" +root="$( cd "$here/../"; pwd )" +builddir="${root}/build" + +source "${root}/alfred_env.sh" + +verbose= +devmode=true +runtests=false +force=false + +# log ... | Echo args to STDERR +log() { + echo "$@" >&2 +} + +# cleanup | Delete temporary build files +cleanup() { + log "Cleaning up ..." + test -d "$builddir" && rm -rf $verbose "${builddir}/"* +} + +# usage | Show usage message +usage() { + cat < /dev/null +# ------------------------------------------------------- +# Run unit tests +$runtests && { + log "Running unit tests ..." + go test $verbose . || exit 1 +} + +# ------------------------------------------------------- +# Build +test -d "${builddir}" && { log "Cleaning build directory ..."; cleanup } + +log "Building executable(s) ..." +go build $verbose -o ./gcal . +zipname="Google-Calendar-Events-${alfred_workflow_version}.alfredworkflow" +outpath="${root}/${zipname}" +# $devmode && { sym="-s" } + +log "Linking assets to build directory ..." +# mkdir -vp "$builddir" +# mkdir -p $verbose "${builddir}/scripts/"{tab,url} +mkdir -p $verbose "${builddir}/icons" + +pushd "$builddir" &> /dev/null + +ln $verbose ../*.png . +ln $verbose ../*.html . +ln $verbose ../info.plist . +ln $verbose ../gcal . +ln $verbose ../README.md . +ln $verbose ../LICENCE.txt . +ln $verbose ../icons/*.png ./icons/ +# ln $verbose scripts/tab/* "${builddir}/scripts/tab/" +# ln $verbose scripts/url/* "${builddir}/scripts/url/" +popd &> /dev/null + +# ------------------------------------------------------- +# Build .alfredworkflow file +$devmode || { + test -f "${outpath}" && { + $force && { + rm $verbose "${outpath}" + } || { + log "Destination file already exists. Use -f to overwrite." + exit 1 + } + } + log "Building .alfredworkflow file ..." + pushd "$builddir" &> /dev/null + zip -9 -r "${outpath}" ./* + ST_ZIP=$? + test "$ST_ZIP" -ne 0 && { + log "Error creating .alfredworkflow file." + popd &> /dev/null + popd &> /dev/null + exit $ST_ZIP + } + popd &> /dev/null + log "Wrote '${zipname}' file in '$( pwd )'" +} + +popd &> /dev/null diff --git a/bin/icons b/bin/icons new file mode 100755 index 0000000..b6bf36f --- /dev/null +++ b/bin/icons @@ -0,0 +1,137 @@ +#!/usr/bin/env zsh + +set -e + +# URL of icon generator +api="http://icons.deanishe.net/icon" +here="$( cd "$( dirname "$0" )"; pwd )" +root="$( cd "$here/../"; pwd )" +# where workflow icons belong +icondir="${root}/icons" +# icon config file +iconfile="${root}/icons/icons.txt" + +prog="$( basename "$0" )" + +force=false +verbose=false +vopt= +icons=() + +# log ... | Echo arguments to STDERR +log() { + echo "$@" >&2 +} + +# info .. | Write args to STDERR if VERBOSE is true +info() { + $verbose && log $(print -P "%F{blue}..%f") "$@" + return 0 +} + +# success .. | Write green "ok" and args to STDERR if VERBOSE is true +success() { + $verbose && log $(print -P "%F{green}ok%f") "$@" + return 0 +} + +# error .. | Write red "error" and args to STDERR +error() { + log $(print -P '%F{red}error%f') "$@" +} + +# fail .. | Write red "error" and args to STDERR, then exit with status 1 +fail() { + error "$@" + exit 1 +} + +# load | Read configuration file from STDIN +load() { + typeset -g icons + while read line; do + + # ignore lines that start with # or are empty + [[ $line =~ "#" ]] || test -z "$line" && continue + + read fname font colour name <<< "$line" + icons+=($fname $font $colour $name) + + done +} + +usage() { +cat < +// +// MIT Licence. See http://opensource.org/licenses/MIT +// +// Created on 2017-11-25 +// + +package main + +import ( + "errors" + "os/exec" + + aw "github.com/deanishe/awgo" +) + +var ( + errNoActive = errors.New("no active calendars") +) + +// Active calendars +type Active []string + +func allCalendars() ([]*Calendar, error) { + var ( + cals []*Calendar + name = "calendars.json" + jobName = "update-calendars" + ) + + if wf.Cache.Expired(name, maxAgeCals) { + if !aw.IsRunning(jobName) { + wf.Rerun(0.3) + cmd := exec.Command("./gcal", "update", "calendars") + if err := aw.RunInBackground(jobName, cmd); err != nil { + return nil, err + } + } + } + + if wf.Cache.Exists(name) { + if err := wf.Cache.LoadJSON("calendars.json", &cals); err != nil { + return nil, err + } + } + + 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 IDMap, errNoActive + } + + if err := wf.Cache.LoadJSON(name, &IDs); err != nil { + return nil, err + } + for _, id := range IDs { + IDMap[id] = true + } + return IDMap, nil +} + +func activeCalendars() ([]*Calendar, error) { + var cals []*Calendar + IDs, err := activeCalendarIDs() + if err != nil { + return nil, err + } + + all, err := allCalendars() + if err != nil { + return nil, err + } + + for _, c := range all { + if IDs[c.ID] { + cals = append(cals, c) + } + } + return cals, nil +} + +// doListCalendars shows a list of available calendars in Alfred. +func doListCalendars() error { + cals, err := allCalendars() + if err != nil { + return err + } + + if len(cals) == 0 && aw.IsRunning("update-calendars") { + wf.NewItem("Fetching List of Calendars…"). + Subtitle("List will reload shortly"). + Valid(false). + Icon(iconReload) + wf.Rerun(0.3) + 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 + } + wf.NewItem(c.Title). + Subtitle(c.Description). + Icon(icon). + Arg(c.ID). + Match(c.Title). + Valid(true) + } + if query != "" { + wf.Filter(query) + } + wf.WarnEmpty("No Calendars", "Did you log in with the right account?") + wf.SendFeedback() + return nil +} diff --git a/cmd_config.go b/cmd_config.go new file mode 100644 index 0000000..bc35d32 --- /dev/null +++ b/cmd_config.go @@ -0,0 +1,130 @@ +// +// Copyright (c) 2017 Dean Jackson +// +// MIT Licence. See http://opensource.org/licenses/MIT +// +// Created on 2017-11-25 +// + +package main + +import ( + "io/ioutil" + "log" + "os" + "path/filepath" + "strings" + + "github.com/deanishe/awgo/util" +) + +// doConfig shows configuration options. +func doConfig() error { + wf.NewItem("Active Calendars"). + Subtitle("Turn calendars on/off"). + Icon(iconCalOn). + Valid(true). + Var("action", "calendars") + + if wf.UpdateAvailable() { + wf.NewItem("An Update is Available"). + Subtitle("A newer version of the workflow is available"). + Autocomplete("workflow:update"). + Icon(iconUpdateAvailable). + Valid(false) + } else { + wf.NewItem("Workflow is up to Date"). + Subtitle("Action to force update check"). + Icon(iconUpdateOK). + Valid(true). + Var("action", "update") + } + + wf.NewItem("Open Documentation"). + Subtitle("Open workflow README in your browser"). + Arg(readmeURL). + Valid(true). + Icon(iconDocs). + Var("action", "open") + + wf.NewItem("Get Help"). + Subtitle("Open alfredforum.com thread in your browser"). + Arg(forumURL). + Valid(true). + Icon(iconHelp). + Var("action", "open") + + wf.NewItem("Report Issue"). + Subtitle("Open GitHub issues in your browser"). + Arg(helpURL). + Valid(true). + Icon(iconIssue). + Var("action", "open") + + wf.NewItem("Clear Cached Calendars & Events"). + Subtitle("Remove cached list of calendars and events"). + Icon(iconDelete). + Valid(true). + Var("action", "clear") + + if query != "" { + wf.Filter(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 IDs[calendarID] { + log.Printf("deactivating calendar %s ...", calendarID) + delete(IDs, calendarID) + } else { + log.Printf("activating calendar %s ...", calendarID) + IDs[calendarID] = true + } + + active := []string{} + for ID, _ := range IDs { + active = append(active, ID) + } + return wf.Cache.StoreJSON("active.json", active) +} + +// doClear removes cached calendars and events. +func doClear() error { + log.Print("clearing cached calendars and events…") + wf.TextErrors = true + + paths := []string{filepath.Join(wf.CacheDir(), "calendars.json")} + + files, err := ioutil.ReadDir(wf.CacheDir()) + if err != nil { + return err + } + + for _, fi := range files { + fn := fi.Name() + if strings.HasPrefix(fn, "events-") && strings.HasSuffix(fn, ".json") { + paths = append(paths, filepath.Join(wf.CacheDir(), fn)) + } + } + + for _, p := range paths { + if err := os.Remove(p); err != nil { + if os.IsNotExist(err) { + continue + } + log.Printf("[ERROR] couldn't delete \"%s\": %v", util.PrettyPath(p), err) + return err + } + log.Printf("deleted \"%s\"", util.PrettyPath(p)) + } + return nil +} diff --git a/cmd_dates.go b/cmd_dates.go new file mode 100644 index 0000000..3d555f4 --- /dev/null +++ b/cmd_dates.go @@ -0,0 +1,188 @@ +// +// Copyright (c) 2017 Dean Jackson +// +// MIT Licence. See http://opensource.org/licenses/MIT +// +// Created on 2017-11-25 +// + +package main + +import ( + "fmt" + "regexp" + "strconv" + "time" +) + +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 { + var ( + dates = []time.Time{} + parsed bool // whether date was parsed from user input + ) + + if dateFormat == "" { // show default list + for i := -3; i < 4; i++ { + dates = append(dates, midnight(today.Add(oneDay*time.Duration(i)))) + } + } else { + t, err := parseDate(dateFormat) + if err != nil { + wf.Warn("Invalid date", "Format is YYYY-MM-DD, YYYMMDD or [+|-]NN[d|w]") + return nil + } + parsed = true + dates = append(dates, t) + } + + for _, t := range dates { + var sub, title string + dateStr := t.Format(timeFormat) + longDate := t.Format(timeFormatLong) + title = relativeDays(t, !parsed) + sub = dateStr + if parsed { + title, sub = longDate, title + } + wf.NewItem(title). + Subtitle(sub). + Arg(dateStr). + Autocomplete(dateStr). + Valid(true) + } + + wf.SendFeedback() + return nil +} + +// Return midnight in local timezone for given Time. +func midnight(t time.Time) time.Time { + s := t.In(time.Local).Format(timeFormat) + m, err := time.ParseInLocation(timeFormat, s, time.Local) + if err != nil { + panic(err) + } + return m +} + +func parseDate(s string) (time.Time, error) { + if t, err := time.ParseInLocation(timeFormat, s, time.Local); err == nil { + return t, nil + } + if t, err := time.ParseInLocation("20060102", s, time.Local); err == nil { + return t, nil + } + + // 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{}, fmt.Errorf("invalid format: %s", s) + } + + // Sign + if m[1] == "-" { + add = false + } + // Count + n, err := strconv.Atoi(m[2]) + if err != nil { + return time.Time{}, fmt.Errorf("invalid number: %s", m[2]) + } + + if n == 0 { + return today, nil + } + + // 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), nil +} + +// 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") +} diff --git a/cmd_dates_test.go b/cmd_dates_test.go new file mode 100644 index 0000000..4705444 --- /dev/null +++ b/cmd_dates_test.go @@ -0,0 +1,58 @@ +// +// Copyright (c) 2017 Dean Jackson +// +// MIT Licence. See http://opensource.org/licenses/MIT +// +// Created on 2017-11-25 +// + +package main + +import "testing" + +var validFormats = []string{ + "2017-11-25", // date strings + "20171125", + "7", // no units + "-7", + "1d", // days + "+1d", + "-1d", + "2w", // weeks + "+2w", + "-2w", +} + +var invalidFormats = []string{ + "1m", + "2q", + "l1d", + "*2d", +} + +func TestParseDate(t *testing.T) { + tm, err := parseDate("0") + if !tm.Equal(today) || err != nil { + t.Error("zero format failed. tm=%v, err=%v", tm, err) + } + + for _, s := range validFormats { + tm, err := parseDate(s) + if err != nil { + t.Errorf("error parsing valid format \"%s\": %s", s, err) + } + if tm.IsZero() { + t.Errorf("zero time for valid format \"%s\"", s) + } + } + + for _, s := range invalidFormats { + tm, err := parseDate(s) + if err == nil { + t.Errorf("no error parsing invalid format \"%s\"", s) + } + if !tm.IsZero() { + t.Errorf("non-zero time for invalid format \"%s\": %v", s, tm) + } + } +} diff --git a/cmd_events.go b/cmd_events.go new file mode 100644 index 0000000..7743668 --- /dev/null +++ b/cmd_events.go @@ -0,0 +1,191 @@ +// +// Copyright (c) 2017 Dean Jackson +// +// MIT Licence. See http://opensource.org/licenses/MIT +// +// Created on 2017-11-25 +// + +package main + +import ( + "fmt" + "log" + "os/exec" + "time" + + aw "github.com/deanishe/awgo" +) + +// doEvents shows a list of events in Alfred. +func doEvents() error { + var ( + last = today + cur = today + ) + gen, err := NewIconGenerator(cacheDirIcons, aw.IconWorkflow) + if err != nil { + return err + } + + cals, err := activeCalendars() + if 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 + } + return err + } + log.Printf("%d active calendar(s)", len(cals)) + + if len(cals) == 0 && aw.IsRunning("update-calendars") { + wf.NewItem("Fetching List of Calendars…"). + Subtitle("List will reload shortly"). + Valid(false). + Icon(iconReload) + wf.Rerun(0.3) + wf.SendFeedback() + return nil + } + + all, err := loadEvents(startTime, cals...) + if err != nil { + return err + } + + events := []*Event{} + + // Filter out events after cutoff + for _, e := range all { + if !schedule && e.Start.After(endTime) { + break + } + events = append(events, e) + log.Printf("%s", e.Title) + } + + if len(all) == 0 && aw.IsRunning("update-events") { + wf.NewItem("Fetching Events…"). + Subtitle("Results will refresh shortly"). + Icon(iconReload). + Valid(false) + } + + log.Printf("%d event(s) for %s", len(events), startTime.Format(timeFormat)) + + if len(events) == 0 && query == "" { + wf.NewItem(fmt.Sprintf("No Events on %s", startTime.Format(timeFormatLong))). + Icon(aw.IconWorkflow) + } + + for _, e := range events { + if schedule { // Show next day indicator + cur = midnight(e.Start) + if cur.After(last) { + last = cur + wf.NewItem(cur.Format(timeFormatLong)). + Arg(cur.Format(timeFormat)). + Valid(true). + Icon(iconDay) + } + } + + icon := gen.Icon(eventIconFont, eventIconName, e.Colour) + sub := fmt.Sprintf("%s – %s / %s", e.Start.Format("15:04"), e.End.Format("15:04"), e.CalendarTitle) + it := wf.NewItem(e.Title). + Subtitle(sub). + Icon(icon). + Arg(e.URL). + Quicklook(previewURL(startTime, e.ID)). + Valid(true). + Var("action", "open") + + if e.Location != "" { + app := "Google Maps" + if useAppleMaps { + app = "Apple Maps" + } + icon := gen.Icon(mapIconFont, mapIconName, 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 !schedule { + // Navigation items + prev := startTime.AddDate(0, 0, -1) + wf.NewItem("Previous: "+relativeDate(prev)). + Icon(iconPrevious). + Arg(prev.Format(timeFormat)). + Valid(true). + Var("action", "date") + + next := startTime.AddDate(0, 0, 1) + wf.NewItem("Next: "+relativeDate(next)). + Icon(iconNext). + Arg(next.Format(timeFormat)). + Valid(true). + Var("action", "date") + } + + if query != "" { + wf.Filter(query) + } + + if gen.HasQueue() { + wf.Rerun(0.3) + if err := gen.Save(); err != nil { + return err + } + if !aw.IsRunning("icons") { + cmd := exec.Command("./gcal", "update", "icons") + if err := aw.RunInBackground("icons", cmd); err != nil { + return err + } + } + } + + 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, maxAgeEvents) { + wf.Rerun(0.3) + if !aw.IsRunning(jobName) { + cmd := exec.Command("./gcal", "update", "events", dateStr) + if err := aw.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 +} diff --git a/cmd_open.go b/cmd_open.go new file mode 100644 index 0000000..a0d008c --- /dev/null +++ b/cmd_open.go @@ -0,0 +1,30 @@ +// +// Copyright (c) 2017 Dean Jackson +// +// MIT Licence. See http://opensource.org/licenses/MIT +// +// Created on 2017-11-30 +// + +package main + +import ( + "log" + "os/exec" +) + +// Open URL in specified app or in default. +func doOpen() error { + wf.TextErrors = true + args := []string{} + if openApp != "" { + log.Printf("[open] opening \"%s\" in \"%s\"…", calURL, openApp) + args = append(args, "-a", openApp) + } else { + log.Printf("[open] opening \"%s\" in default browser…", calURL) + } + args = append(args, calURL) + + cmd := exec.Command("/usr/bin/open", args...) + return cmd.Run() +} diff --git a/cmd_server.go b/cmd_server.go new file mode 100644 index 0000000..71d1e30 --- /dev/null +++ b/cmd_server.go @@ -0,0 +1,132 @@ +// +// Copyright (c) 2017 Dean Jackson +// +// MIT Licence. See http://opensource.org/licenses/MIT +// +// Created on 2017-11-26 +// + +package main + +import ( + "context" + "html/template" + "io" + "log" + "net/http" + "net/url" + "path/filepath" + "sync" + "time" +) + +const ( + previewServerURL = "localhost:61433" + quitAfter = 60 * time.Second + // quitAfter = 20 * 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…") + var ( + lastRequest = time.Now() + mu = sync.Mutex{} + c = make(chan struct{}) + templates = template.Must(template.ParseFiles(filepath.Join(wf.Dir(), "preview.html"))) + ) + srv := &http.Server{Addr: previewServerURL} + + go func() { + if err := srv.ListenAndServe(); err != nil { + if err == http.ErrServerClosed { + log.Print("[preview] server stopped") + } else { + log.Printf("[preview/error] preview server failed: %v", err) + } + } + c <- struct{}{} + }() + + go func() { + c := time.Tick(10 * 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 { + log.Print("[preview] stopping server…") + if err := srv.Shutdown(context.Background()); err != nil { + log.Printf("[preview] server shutdown error: %v", err) + } + // log.Printf("[preview] server stopped") + } + } + }() + + http.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 { + io.WriteString(w, "bad date\n") + return + } + cals, err := activeCalendars() + if err != nil { + log.Printf("[preview/error] couldn't 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/error] couldn't 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/error] couldn't execute template \"fail\": %v", err) + } + return + } + + if err := templates.ExecuteTemplate(w, "event", event); err != nil { + log.Printf("[preview/error] couldn't execute template \"event\": %v", err) + } + }) + + <-c + return nil +} diff --git a/cmd_update.go b/cmd_update.go new file mode 100644 index 0000000..933213c --- /dev/null +++ b/cmd_update.go @@ -0,0 +1,133 @@ +// +// Copyright (c) 2017 Dean Jackson +// +// MIT Licence. See http://opensource.org/licenses/MIT +// +// Created on 2017-11-25 +// + +package main + +import ( + "fmt" + "io/ioutil" + "log" + "os" + "path/filepath" + "sort" + "strings" + "sync" + "time" + + aw "github.com/deanishe/awgo" +) + +// Check if a new version of the workflow is available. +func doUpdateWorkflow() error { + log.Print("[update] checking for new version of workflow…") + wf.TextErrors = true + return wf.CheckForUpdate() +} + +// Fetch and cache list of calendars. +func doUpdateCalendars() error { + log.Print("[update] reloading calendars…") + wf.TextErrors = true + cals, err := FetchCalendars(auth) + if err != nil { + return fmt.Errorf("couldn't load calendars: %v", err) + } + return wf.Cache.StoreJSON("calendars.json", cals) +} + +// Fetch events for a specified date. +func doUpdateEvents() error { + log.Printf("[update] fetching events for %s…", startTime.Format(timeFormat)) + wf.TextErrors = true + var ( + events = []*Event{} + name = fmt.Sprintf("events-%s.json", startTime.Format(timeFormat)) + ) + if err := clearOldEvents(); err != nil { + log.Printf("[update/error] problem deleting old cache files: %v", err) + } + cals, err := activeCalendars() + if err != nil { + log.Printf("[update/error] couldn't load active calendars: %v", err) + return err + } + log.Printf("[update] %d active calendar(s)", len(cals)) + + // Fetch events in parallel + var ( + ch = make(chan *Event) + wg sync.WaitGroup + ) + + wg.Add(len(cals)) + + for _, c := range cals { + go func(c *Calendar) { + defer wg.Done() + evs, err := FetchEvents(auth, c, startTime) + if err != nil { + log.Printf("[update/error] fetching events for calendar \"%s\": %v", c.Title, err) + return + } + + log.Printf("[update] %d event(s) in calendar \"%s\"", len(evs), c.Title) + for _, e := range evs { + ch <- e + // events = append(events, e) + } + }(c) + } + + // Close channel when all goroutines are done + go func() { + wg.Wait() + close(ch) + }() + + for e := range ch { + log.Printf("[update] %s", e) + events = append(events, e) + } + + sort.Sort(EventsByStart(events)) + + if err := wf.Cache.StoreJSON(name, events); err != nil { + return err + } + return nil +} + +// doUpdateIcons fetches queued icons. +func doUpdateIcons() error { + gen, err := NewIconGenerator(cacheDirIcons, aw.IconWorkflow) + if err != nil { + return err + } + return gen.Download() +} + +// Remove events-* files that haven't been updated in a week. +func clearOldEvents() error { + files, err := ioutil.ReadDir(wf.CacheDir()) + if err != nil { + return err + } + for _, fi := range files { + name := fi.Name() + cutoff := time.Now().AddDate(0, 0, -7) + if strings.HasPrefix(name, "events-") && strings.HasSuffix(name, ".json") { + if fi.ModTime().Before(cutoff) { + p := filepath.Join(wf.CacheDir(), name) + if err := os.Remove(p); err != nil { + log.Printf("[ERROR] couldn't delete file \"%s\": %v", p, err) + } + } + } + } + return nil +} diff --git a/events.go b/events.go new file mode 100644 index 0000000..d9d6e0f --- /dev/null +++ b/events.go @@ -0,0 +1,202 @@ +// +// Copyright (c) 2017 Dean Jackson +// +// MIT Licence. See http://opensource.org/licenses/MIT +// +// Created on 2017-11-25 +// + +package main + +import ( + "fmt" + "log" + "net/url" + "sort" + "time" + + calendar "google.golang.org/api/calendar/v3" +) + +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 +} + +// 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] } + +// FetchCalendars retrieves a list of the user's calendars. +func FetchCalendars(auth *Authenticator) ([]*Calendar, error) { + + srv, err := calendarService(auth) + if err != nil { + return nil, err + } + + ls, err := srv.CalendarList.List().Do() + if err != nil { + return nil, err + } + + var cals []*Calendar + for _, entry := range ls.Items { + if entry.Hidden { + log.Printf("[events] ignoring hidden calendar \"%s\"", entry.Summary) + continue + } + + c := &Calendar{ + ID: entry.Id, + Title: entry.Summary, + Description: entry.Description, + Colour: entry.BackgroundColor, + } + if entry.SummaryOverride != "" { + c.Title = entry.SummaryOverride + } + cals = append(cals, c) + } + sort.Sort(CalsByTitle(cals)) + return cals, nil +} + +// Event is a calendar event +type Event struct { + ID string // Calendar ID + 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] } + +// FetchEvents retrieves events for given date +func FetchEvents(auth *Authenticator, cal *Calendar, start time.Time) ([]*Event, error) { + var ( + end = start.Add(scheduleDuration) + events = []*Event{} + startTime = start.Format(time.RFC3339) + endTime = end.Format(time.RFC3339) + ) + + log.Printf("[events] cal=\"%s\", start=%s, end=%s", cal.Title, start, end) + + srv, err := calendarService(auth) + if err != nil { + return nil, err + } + + evs, err := srv.Events.List(cal.ID). + SingleEvents(true). + MaxResults(2500). + TimeMin(startTime). + TimeMax(endTime). + OrderBy("startTime").Do() + + if err != nil { + return nil, fmt.Errorf("couldn't retrieve events for calendar %s: %v", cal.ID, err) + } + + for _, e := range evs.Items { + if e.Start.DateTime == "" { // all-day event + continue + } + start, err := time.Parse(time.RFC3339, e.Start.DateTime) + if err != nil { + log.Printf("[events/error] couldn't parse start time (%s): %v", e.Start.DateTime, err) + continue + } + end, err := time.Parse(time.RFC3339, e.End.DateTime) + if err != nil { + log.Printf("[events/error] couldn't parse end time (%s): %v", e.End.DateTime, err) + continue + } + + events = append(events, &Event{ + ID: e.Id, + 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 +} + +func calendarService(auth *Authenticator) (*calendar.Service, error) { + client, err := auth.GetClient() + if err != nil { + return nil, fmt.Errorf("couldn't get API client: %v", err) + } + + srv, err := calendar.New(client) + if err != nil { + return nil, fmt.Errorf("couldn't create Calendar Service: %v", err) + } + return srv, nil +} + +// URL that points to location on Google Maps or Apple Maps. +func mapURL(location string) string { + if location == "" { + return "" + } + if 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() +} diff --git a/gcal.go b/gcal.go new file mode 100644 index 0000000..9bfd3d1 --- /dev/null +++ b/gcal.go @@ -0,0 +1,303 @@ +// +// Copyright (c) 2017 Dean Jackson +// +// MIT Licence. See http://opensource.org/licenses/MIT +// +// Created on 2017-11-25 +// + +// Command gcal is an Alfred 3 workflow for viewing Google Calendar events. +package main + +import ( + "log" + "os" + "os/exec" + "path/filepath" + "strconv" + "time" + + "github.com/deanishe/awgo" + "github.com/deanishe/awgo/update" + "github.com/deanishe/awgo/util" + "github.com/docopt/docopt-go" +) + +const ( + timeFormat = "2006-01-02" + timeFormatLong = "Monday, 2 January 2006" +) + +var ( + repo = "deanishe/alfred-gcal" + helpURL = "https://github.com/deanishe/alfred-gcal/issues" + readmeURL = "https://github.com/deanishe/alfred-gcal/" + forumURL = "https://alfredforum.com" + + usage = ` +gcal (events|calendars|toggle) [options] [] + +Usage: + gcal dates [--] [] + gcal events [--date=] [] + gcal calendars [] + gcal toggle + gcal update (workflow|calendars|events|icons) [] + gcal config [] + gcal clear + gcal open [--app=] + gcal server + gcal -h + +Options: + -a --app Application to open URLs in. + -d --date Date to show events for (format YYYY-MM-DD). + -h --help Show this message and exit. +` + auth *Authenticator + wf *aw.Workflow + tokenFile string + cacheDirIcons string + useAppleMaps bool + schedule bool + + // Cache ages + maxAgeCals = time.Hour * 3 + maxAgeEvents time.Duration + + // CLI args + query string + calendarID string + command Cmd + dateFormat string + updateWhat string + openApp string + calURL string + startTime time.Time + scheduleDuration time.Duration + endTime time.Time +) + +// Cmd is a program sub-command +type Cmd int + +// String returns the name of the command +func (c Cmd) String() string { + commands := map[Cmd]string{ + cmdCalendars: "calendars", + cmdClear: "clear", + cmdConfig: "config", + cmdDates: "dates", + cmdEvents: "events", + cmdOpen: "open", + cmdServer: "server", + cmdToggle: "toggle", + cmdUpdateCalendars: "updateCalendars", + cmdUpdateEvents: "updateEvents", + cmdUpdateIcons: "updateIcons", + cmdUpdateWorkflow: "updateWorkflow", + } + return commands[c] +} + +const ( + cmdCalendars Cmd = iota + cmdClear + cmdConfig + cmdDates + cmdEvents + cmdOpen + cmdServer + cmdToggle + cmdUpdateCalendars + cmdUpdateEvents + cmdUpdateIcons + cmdUpdateWorkflow +) + +func init() { + wf = aw.New(update.GitHub(repo), aw.HelpURL(helpURL)) + wf.MagicActions.Register(&calendarMagic{}) + + tokenFile = filepath.Join(wf.CacheDir(), "gapi-token.json") + cacheDirIcons = filepath.Join(wf.CacheDir(), "icons") + util.MustExist(cacheDirIcons) + + auth = NewAuthenticator(tokenFile, []byte(secret)) + + v := os.Getenv("APPLE_MAPS") + if v == "1" || v == "yes" || v == "true" { + useAppleMaps = true + } + + n := envInt("SCHEDULE_DAYS", 3) + scheduleDuration = time.Hour * time.Duration(n*24) + n = envInt("EVENT_CACHE_MINS", 30) + maxAgeEvents = time.Minute * time.Duration(n) +} + +// Parse command-line flags +func parseFlags() error { + args, err := docopt.Parse(usage, wf.Args(), true, wf.Version(), false, true) + if err != nil { + return err + } + // log.Printf("args=%#v", args) + + // Default start and end times + s := time.Now().In(time.Local).Format(timeFormat) + startTime, err = time.ParseInLocation(timeFormat, s, time.Local) + if err != nil { + return err + } + schedule = true + + if args["calendars"] == true { + command = cmdCalendars + } + if args["clear"] == true { + command = cmdClear + } + if args["config"] == true { + command = cmdConfig + } + if args["dates"] == true { + command = cmdDates + } + if args["events"] == true { + command = cmdEvents + } + if args["open"] == true { + command = cmdOpen + } + if args["server"] == true { + command = cmdServer + } + if args["toggle"] == true { + command = cmdToggle + } + if args["update"] == true { + if args["calendars"] == true { + command = cmdUpdateCalendars + } + if args["events"] == true { + command = cmdUpdateEvents + } + if args["icons"] == true { + command = cmdUpdateIcons + } + if args["workflow"] == true { + command = cmdUpdateWorkflow + } + } + + if s, ok := args[""].(string); ok { + startTime, err = time.ParseInLocation(timeFormat, s, time.Local) + if err != nil { + return err + } + } + if s, ok := args[""].(string); ok { + query = s + } + if s, ok := args["--app"].(string); ok { + openApp = s + } + if s, ok := args[""].(string); ok { + calURL = s + } + if s, ok := args[""].(string); ok { + calendarID = s + } + if s, ok := args[""].(string); ok { + dateFormat = s + } + if s, ok := args["--date"].(string); ok { + startTime, err = time.ParseInLocation(timeFormat, s, time.Local) + if err != nil { + return err + } + schedule = false + } + endTime = startTime.Add(time.Hour * 24) + return nil +} + +func run() { + var err error + + if err := parseFlags(); err != nil { + wf.FatalError(err) + } + + log.Printf("command=%v, calendarID=%v, query=%v, startTime=%v, endTime=%v, dateFormat=%v", + command, calendarID, query, startTime, endTime, dateFormat) + + if !aw.IsRunning("server") { + cmd := exec.Command("./gcal", "server") + if err := aw.RunInBackground("server", cmd); err != nil { + wf.FatalError(err) + } + } + + switch command { + case cmdCalendars: + err = doListCalendars() + case cmdClear: + err = doClear() + case cmdConfig: + err = doConfig() + case cmdDates: + err = doDates() + case cmdEvents: + err = doEvents() + case cmdOpen: + err = doOpen() + case cmdServer: + err = doStartServer() + case cmdToggle: + err = doToggle() + case cmdUpdateCalendars: + err = doUpdateCalendars() + case cmdUpdateEvents: + err = doUpdateEvents() + case cmdUpdateIcons: + err = doUpdateIcons() + case cmdUpdateWorkflow: + err = doUpdateWorkflow() + } + + 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) + } +} + +func main() { + wf.Run(run) +} + +// Get an environment variable as an int. +func envInt(name string, fallback int) int { + s := os.Getenv(name) + if s == "" { + log.Printf("[ERROR] environment variable \"%s\" isn't set", name) + return fallback + } + n, err := strconv.Atoi(s) + if err != nil { + log.Printf("[ERROR] environment variable \"%s\" is not a number: %s", name, s) + return fallback + } + log.Printf("[env] %s=%d", name, n) + return n +} diff --git a/icon.png b/icon.png new file mode 120000 index 0000000..b912354 --- /dev/null +++ b/icon.png @@ -0,0 +1 @@ +./icons/icon.png \ No newline at end of file diff --git a/icons.go b/icons.go new file mode 100644 index 0000000..db376a4 --- /dev/null +++ b/icons.go @@ -0,0 +1,225 @@ +// +// Copyright (c) 2017 Dean Jackson +// +// MIT Licence. See http://opensource.org/licenses/MIT +// +// Created on 2017-11-26 +// + +package main + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "log" + "net" + "net/http" + "os" + "path/filepath" + "strings" + "time" + + "github.com/deanishe/awgo" + "github.com/deanishe/awgo/util" +) + +var ( + apiURL = "http://icons.deanishe.net/icon" + // Static icons + iconCalOff = &aw.Icon{Value: "icons/calendar-off.png"} + iconCalOn = &aw.Icon{Value: "icons/calendar-on.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"} + iconReload = &aw.Icon{Value: "icons/reload.png"} + iconUpdateOK = &aw.Icon{Value: "icons/update-ok.png"} + iconUpdateAvailable = &aw.Icon{Value: "icons/update-available.png"} + + // Font & name of dynamic icons + eventIconFont = "material" + eventIconName = "calendar" + mapIconFont = "elusive" + mapIconName = "map-marker" + + // HTTP client + webClient = &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, + }, + } +) + +// IconConfig is an icon from the server +type IconConfig struct { + Font string + Name string + Colour string +} + +// Filename returns the filename for the retrieved icon. +func (ic *IconConfig) Filename() string { + return fmt.Sprintf("%s-%s-%s.png", ic.Font, ic.Name, ic.Colour) +} + +// String is a synonym for Filename(). +func (ic *IconConfig) String() string { return ic.Filename() } + +// IconGenerator fetches and caches icons from the icon server +type IconGenerator struct { + Dir string // Directory to store icons in + Default *aw.Icon // Icon to return if requested icon isn't cached yet + Queue []*IconConfig // Icons that need to be downloaded + icMap map[string]bool // Map of IconConfig filenames to prevent duplicates in Queue +} + +// NewIconGenerator creates an initialised IconGenerator +func NewIconGenerator(dir string, def *aw.Icon) (*IconGenerator, error) { + util.MustExist(dir) + g := &IconGenerator{ + Dir: dir, + Default: def, + Queue: []*IconConfig{}, + icMap: map[string]bool{}, + } + if err := g.loadQueue(); err != nil { + return nil, err + } + return g, nil +} + +// loadQueue loads the Generator's queue from the queuefile. +func (g *IconGenerator) loadQueue() error { + p := g.Queuefile() + data, err := ioutil.ReadFile(p) + if err != nil { + if os.IsNotExist(err) { + return nil + } + return err + } + return json.Unmarshal(data, &g.Queue) +} + +// Cachefile returns the path to Generators queue file. +func (g *IconGenerator) Queuefile() string { return filepath.Join(g.Dir, "queue.json") } + +// Icon returns an *aw.Icon from the cache. +func (g *IconGenerator) Icon(font, name, colour string) *aw.Icon { + if strings.HasPrefix(colour, "#") { + colour = colour[1:] + } + + ic := &IconConfig{font, name, colour} + p := g.path(ic) + + if util.PathExists(p) { + return &aw.Icon{Value: p} + } + + if !g.icMap[ic.Filename()] { + g.Queue = append(g.Queue, ic) + log.Printf("queued icon for retrieval: %s", ic) + g.icMap[ic.Filename()] = true + } + return g.Default +} + +// path returns the local path to the cached icon. +func (g *IconGenerator) path(ic *IconConfig) string { + return filepath.Join(g.Dir, ic.Filename()) +} + +// url returns the API URL of the IconConfig. +func (g *IconGenerator) url(ic *IconConfig) string { + return fmt.Sprintf("%s/%s/%s/%s", apiURL, ic.Font, ic.Colour, ic.Name) +} + +// HasQueue returns true if there are icons queued for retrieval. +func (g *IconGenerator) HasQueue() bool { return len(g.Queue) > 0 } + +// Save caches the Generator queue to queuefile. +func (g *IconGenerator) Save() error { + if !g.HasQueue() { + return nil + } + data, err := json.MarshalIndent(g.Queue, "", " ") + if err != nil { + return err + } + return ioutil.WriteFile(g.Queuefile(), data, 0600) +} + +// Download retrieves queued icons. +func (g *IconGenerator) Download() error { + var errs []error + if !g.HasQueue() { + log.Print("no icons to download") + return nil + } + for _, ic := range g.Queue { + URL := g.url(ic) + path := g.path(ic) + if util.PathExists(path) { + continue + } + if err := download(URL, path); err != nil { + log.Printf("couldn't download \"%s\": %v", URL, err) + errs = append(errs, err) + } + } + + if len(errs) > 0 { + return fmt.Errorf("%d error(s) downloading icons", len(errs)) + } + return os.Remove(g.Queuefile()) +} + +// download saves a URL to a filepath. +func download(URL string, path string) error { + res, err := openURL(URL) + if err != nil { + return err + } + defer res.Body.Close() + out, err := os.Create(path) + if err != nil { + return err + } + defer out.Close() + n, err := io.Copy(out, res.Body) + if err != nil { + return err + } + log.Printf("wrote \"%s\" (%d bytes)", path, n) + return nil +} + +// openURL returns an http.Response. It will return an error if the +// HTTP status code > 299. +func openURL(URL string) (*http.Response, error) { + log.Printf("fetching %s ...", URL) + res, err := webClient.Get(URL) + if err != nil { + return nil, err + } + log.Printf("[%d] %s", res.StatusCode, URL) + if res.StatusCode > 299 { + res.Body.Close() + return nil, errors.New(res.Status) + } + return res, nil +} diff --git a/icons/calendar-off.png b/icons/calendar-off.png new file mode 100644 index 0000000000000000000000000000000000000000..482ef5cb489e108cd070c6989f3df898d2647c85 GIT binary patch literal 2611 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 literal 0 HcmV?d00001 diff --git a/icons/help.png b/icons/help.png new file mode 100644 index 0000000000000000000000000000000000000000..59ac8c62b85bd40c82ae8d431776a8890aabc90a GIT binary patch literal 5528 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 NAo2+{syQ;b=pXrDtl0nn literal 0 HcmV?d00001 diff --git a/icons/icons.txt b/icons/icons.txt new file mode 100644 index 0000000..866e62d --- /dev/null +++ b/icons/icons.txt @@ -0,0 +1,31 @@ +# Icons from webfonts via http://icons.deanishe.net +# +# Script ../bin/icons reads this file and downloads the +# specified icons to this directory. +# +# Colours +# blue: 5485F3 +# yellow: F8AC30 +# red: B00000 +# green: 03AE03 +# + +# filename font name colour icon name + +icon material 5485F3 calendar +calendar-on material 03AE03 calendar-check +calendar-off material B00000 calendar-close +day weathericons F8AC30 sunrise +previous fontawesome 5484F3 arrow-circle-left +next fontawesome 5484F3 arrow-circle-right +map elusive 03AE03 map-marker +on fontawesome 03AE03 dot-circle-o +off fontawesome B00000 circle-o +reload fontawesome F8AC30 refresh +trash fontawesome B00000 trash-o +update-available material F8AC30 cloud-download +update-ok material 03AE03 cloud-done +url fontawesome 5485F3 globe +help material 03AE03 help +docs material 5485F3 help +issue fontawesome F8AC30 bug diff --git a/icons/issue.png b/icons/issue.png new file mode 100644 index 0000000000000000000000000000000000000000..2a0e3fc6cfb003d0654f798c9520720dd497dc2e GIT binary patch literal 4841 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 ZH6@hVO#aV8SNI7FI6Ju7Kd>cU{6E}mlWzb3 literal 0 HcmV?d00001 diff --git a/icons/map.png b/icons/map.png new file mode 100644 index 0000000000000000000000000000000000000000..0ee14010abb66075737e56d9b3d418ca2c188d13 GIT binary patch literal 5506 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 literal 0 HcmV?d00001 diff --git a/icons/next.png b/icons/next.png new file mode 100644 index 0000000000000000000000000000000000000000..dd65e7a995dd9d608789d0cfc34ee83a929f9420 GIT binary patch literal 5701 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 Wf7EPsSD4Skz}oz*S&a$)_WuAlfL(3? literal 0 HcmV?d00001 diff --git a/icons/off.png b/icons/off.png new file mode 100644 index 0000000000000000000000000000000000000000..9502bee2cab0b6575a4bead9ccada37e0cb8f3e2 GIT binary patch literal 6141 zcma)gby$;8`|z`1FxmkEQZqnO>6)~3DIEd>6hu;xu8mSUMG+8143yXa0m;!AbT~oB z0Htx8NQ&^A@B90%>-+Ef&Ogt&o^$TL?{i1$6$?W)=2Of70I(SwUA6)M5Pb^*m|*nJ zwcuA?0KnU8d|AgPqGa=1(kL=c?wpb>C&FG@^ymHF(DYNUBUr1|D!i2L zTg}@AIxAh9DlGSU?q$zuE%4eFu2fsBoC=kLrmjA?b{WS1Q)F&#vxrm=Nk(7^O`Y)*!3_2dqA{Wvq6SMZs(d>#euY zhafYC7Y%!H{Cf>tG^3OPV0@CHwb>9jTil@%oVoI`1yrmUyK{@S`w{aQlPaTWU3}z< zcxH%EyaP%Bp*(mGgV3R$?{(jTKJL!zS|o&spPsBn;G{(XEh!Mnv-9y>|KraeJ(Q_P zS@sClhZF&e4jXXcb7)RA4DGg2KF4ngzOl<+2<-=XV%0k4D{BA2V)j}KSJvFss;hyC@KG zSnXu0Cv;jRNI0y6K@`AsiCTI>Oeba2su`7oe9z$UA=1X;(cnE{>KWYIaAaPkJMkOg z#PIEADPQmk%U6bL*|4nwTM@q*novP==gSN@8Y&Cl%c9>*f?$sOX7pDJWI%$DLk5Qt z6sPfr1EN#1VI*VT+q!d@pM)R842{s=#=cSD&)^4F%ctr-<^ZQ~SKa4!*)IprpCe}% zT<ZMbf?xRiu3*ROx#X=VmMNcM+vmSa0y(s zAaw8h%xmHzp{X5``urZ8%t;n*P)$l26L8L^qAA)Q;!a>y;XR1xTHk%MI|DOQG!iz+ zt^z6xYety=DNt?k*!0Z^N1o>S@!*%ApR(JR*raMts$}Casp{mM2$;ogPr6t5epD^h z5yFR&NgF?GmuvLA-TCQA$w$3&^$+;7I9Ng#tcneGJ)FQh`j2?<{0caD zliV&WkYWCsUuVBiPfKLBhbl?YR+pkUCkiEK6yKk3jF;c>@&t#?$FB$s2T%7lDvH22 zEYwbt9GC^uWKe~b^ho_x0_^mL%}aEkzka8+$$m}5d5TeP>cznClGMJ0({QGEcR3)x}zSEPn6J9sz%8?Z~cw#oM~ zH-5;0{IW7fCC8Cv99U-@OMG64v>aRCNq4mZk5I9=3>i@s(ew}WAp0=uOMsyT``+!j z^@vGSa~xtk+z!#l9oG1GN-8l+Qy(X}^RTcLemT+v_=atj=qDa6V<_Na4@#l$YVyT| z5sVbBiIEW)`IExc+fS6Vez!&$v<5d99FD9NXM|?>r;FB$cFZ?BC%7o~VA}DE`Q9i{ z=MH=M$w*^h08RO`wdPMYLYr3EyLI>phQ(xcXW6@!8fDp?{GbQsORZkYH{8M=LzO`R z+MRnsjtMf8>P)DB$XLaeypk%phf)8$*Ff_D?G(o$i?YdWY>0HEf(R*Y#}=hnreKKCBb0TyhE99u^h@4 zN6Hj!<|RA7pIr_V?OS&MWkAxgQVgTxP#f?136cfddCB)|*J_e!<8CNZ{F0!l%!RY_ z{)1L#z|e?gForfZBOK!WMtEM9J~k=?KZC5t&fG6#k~ zI+L(>(5lm^MHw~-?Rc^-1*tB}BXEB&EgU)idX>0ev?OH2cmX<1H;bbroNR?wq9A8z zU@C=xB4LeMy*#8i+gF5)PzT`7+gbB066Y{3xJrEGBg~MlnGc($&nzRcF^H=j;xmAC zXboR31zNzosDaVPyci7}kJ~vd&3<*5bq7~7-|_m02Brshquff7$9dhHH(oO~qqNiJ z4>ZLiMDSXq7{-+_BDfwnB0x|X^N%ORVoC>W=GYHemo=m{xCq9?jLJzxk4yuhxIS4f zR9Q&r;>{r#@lj4d8$#P{{Hv$GoHB%FOVK{}YL)n@joXm*apIzYY%MhCq)*M00|@yx z1$i!wM&M{qyr~Zf(`cp|0c^w;pUQWIhZi)&ruNxn^OBdE==&+$Bl z6yLwKe?SzlhThZ!JRehyc}{nXx)%4%>-WNuM2;!IkN@C@4cHFxhaD|4-n`f4b!~Ow z!H)FlQlGVUOsuCp0tMO-dUtqBgYD9SP}28qRli^d=5h|^v?9O`7MA7^Cu8LrVKbg@ z68(kWLBfkcj!XMD`HiAb^Bjul{rzYRKS>9zRgM~{LVAG*@0f+4F6fLLSrv;CeY?0X zw`X~@Gpr<-H_}MpRfymG7{l;wqob-M8S$ABwX82h)kimm;;+ITBC1dre_H1rD8PWX zbJANgC_S8$*iqEu!6PgM+Tg4F>hz5)g!%3yxp{tOn_CK0A-hF!M5l#w=K1Cuc}C+; z;F+^>ffis^U%biAY1L5I=&2Cfd33yssT_j9SyHGP;C>?LZV0OtYm>V`f5a^SHXyEQ z05+Q6IfggPWG9U=U0M}_<_beR&S7qjNGU}SP=G|iZ4&4S^h!Rr#Y1jG=1mD+IGP8I zCd>P7b2KbuCn-T5ZV2`m5ZQHgk}ZI%>XNrA9ODs*;jL?6zmqMaXT&gY63FQE^U<1w zi#7;#P(*D-9c#n~HJHQ;?ipLE2(kPd$PA(xoTZaSSIw<=FTYcOHZE=ECLgP7T&3%l z2kcLHaqfg)YqXM(zgB*vO7bl@id4J;@CRFbvpFHR{hl5>v zn`I8SGqaYgPHlY-9IZ{LTcd&2fP!YyKxZaH`*7MF#vI+S!iB8>?-t0-o;SnWrfLyQ z_1C#DyFvuN<%8#fZL^Mb*#IdVs4rQ65KIm5eovw^4*p(R^q(`m!ERkKfZ~E_Q>ZUU z3`eaC)!}mB$eZZUy-_+pjFhk!&{3E_aK@1iY6AFEHRY_>Nt-J zU4MFw{!3On8*^jIGnXzNJ4@+0h5sKi@)rHy*%j;wAHC82y-=p8(quCrxkU-#L#M|Q zMMrRNvj6|aqacb=jws{%U*aiB|A%og%s%Mj>$|&muKzna`CFZ(2v~wjzY!*}K=u{rwvG#F^B6=iB9rNRlOuLyG89)F4pQpSp-+KTY3DUzkbBA3!SmyrDRzy{Trzpl7 zyL;xoOZwHl!0*WY21L*+diWn6FaIa}r6Ix@HeoW;-FXf+&Dt zJX7eBK9Tx|Qn0WJD_Ih{oPS%6B|FmnQrdBV4%%9EcZXd!_=X$$ECq>MU&cS==`{n& zaxJ{c{B*f%D#l%f&;H83x#7I{s_CKFAS*+g!FAEDLAH-?0!8s=f3Fd}e;n@{hlA7C`{pD3Y|nUfY{R3zhR z<2AO!Nz}cQ0fXgv(g;GCk>>;_E?L)t#;%)pM;%ua&S_5SB9-eu&<8t!zn17|OdY_T zd(evW--F_Y$O&1ZpxqT z{7DfGD#b4;rdWr4Ak{dLEl-d2f7yqfF;{;2VHkC;cy6p9n>;j$6) zBCH9%WAN#%^;ZRuG!VDp5Ff4Fd&*pkqFrG<4fN;#{9YS!du2cHeROh(7H}Mt^;AX= z=iP5h=c1)5t+ca?gl%v~{7YLTx*n-|%X~BSY*bDYaAdt?7^!iGy+X)4hxV#-I^sNl zb8cv*n`&T(IBgasICxhHx1}_W$_e|L`ZhXT;v9h6tq;(6YDV+lfjjDYDZ#3i8&=+7?nMn8gT{rPVZ*fxu%lI>-n6zQaKp>8F&1x<#4C(%g#&2kR3Wn{>y%k})(H5lt-*porp!|A zgYm-8DNE`Sh9 z&>-ndt^jCer{GHs6tH!1$JX}%`I&_`t`hV&?HRVIJ@M5T;CFpduL1=Zz1RV}l=1i1 z?k?MNF9m#)ld-7==u_7LBrhgzjnIT^#VU-YRJ>hF7HWD=_&JQ9Tnr&2{U+VU>1_!b z^}J+5=yItY>_Yc}BKuj9fmv&NGw5#EJVZv6Fb700wr90r z;Qi;>H`kO1eS{;zItdHK2?br8u8(DHbT=vb-JjCe!J5&RWk?z#kdEjrMywjW;R8fK z(V(&jM#Lb}PnA|oyfSvWWBziJ8r766vw;ubnhQ$Uhx$IqBw;5d`<~KgDkoZRNPAEY z6bPBbC*=j7jb7Qg`Em4*5gI$5Ud98_i}_!ye*NoCl|aA-+absPLnf{~t}FG{PZ5-Ke<0tYsxX3U|7;phAi%A29!X`h|kK-G_{cg@52 zQ@57_=LIap3f(s;(^XP6^!pSz;O825OCYPik#$|W_}-8~NkXW&S1$K~dn`{PKK063 ztht0CB48C-;%aUWF0@U80zt}DmO0KmDyWdvwWqN3(bU2py^YAXKfF&U|83c2E>pst z9{TGX6PV%h+t+KS*7)-m0)y-Qf4~LH%S-j zWvD`>=z|K`LieV>5aT!7pf5W0FTz&&83RF=yKbDA4$A{79AZPAr+O zr>O^#f$=BP9s z6r;svnGb2^BHHyOO~E(Al$7&j16eXnt|{QdZdZpR9i^PXK(U!Z!J&-Wh1*{<-`<6j zuo2>(V8GcwBHr%{t!+d5ocjOU*!EpN4-xuw({GaH^;m9F7KtBmAVNX zCMY|-<43t!F^FPjqzPeEex^f}QMPGQSDQS*X#VGnwhg=TH;K**O&1$;cJGWNC*r<+rbpSS<^J2&Uz6{3e6PnB=x0u2c`*)L|w0ASJ&+pXl{U1zk%k+)K%SK_4Nd)hS&0P>6Oe(#qI;({!=1o|Z*0$-4dA zLzC+5C%TMZd8cXJ0-@H+Q_azqXb!*Q_PU2`my-U@Xr+mQDy>A(jwXQ53fPZjBgx$9 z3%kQNrE=U7x}GM6minog-oNzu--K5KT;a}7HJf0RKJAw!nz%HgT{gNV51DO=ITJQ! zslywSN3Zuq-k@u{QWRrVjwAJANK4XP$}UA|E@W%xn1-{~m+3LT%8u01>h8&{IdMY@b;DM|z_uwvSSDbEW#;|Iae=KGb3xT3D6s Ts4u0D^#Nmji_0~-=!E|P>S1f4 literal 0 HcmV?d00001 diff --git a/icons/on.png b/icons/on.png new file mode 100644 index 0000000000000000000000000000000000000000..2e96bece05eb28989718e824ea35b64840facfd1 GIT binary patch literal 7137 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 literal 0 HcmV?d00001 diff --git a/icons/previous.png b/icons/previous.png new file mode 100644 index 0000000000000000000000000000000000000000..6b1361f1f46ea8333ca0f831afa0c8d9cd6e8a0a GIT binary patch literal 5775 zcma)Ac|28J)ZTaIny=w1QD!P*h9Z%SnJK&>H&n`1l38+YnPpDBl`&&7TywcD2lXap zDsnU4>Mhe1rAVgl^!~p8zwi8W_HUnc*51!rYp?a}m1u8k&PUuy1OV_^T9`NjfI@~S zz=cO%?t$g!01$JwG&y=AEN5XNyy5i8@E6}Mzh?T}crHjHrIRq3+{Vd!G==u$ERjqp2AqtrcUvs<8R#>G`uu z3gJ_ct4I1Lr#GYCMYudn<}3?--D0li)Dr}S{_iK+!&qDELuYK^{mkD%sNc;|zZ;`| z*R1$?7AOudbM15~IJ%Eyej3ya>5g6Z*j!zP1djh&%?cS`>9x7h@K&T)RQZ@N-9Q~H zKDsh5NKqenEW8kcyH88qFzF?n!>CpMoNu)ql9hJ^#gBK>S1;zb<8;37FPDrOJ+fZ) zb32;rF#qd|>xWZQ6y!9BP!KOLtFP!12&yW*s)I9Yi~^@<+s`{G3hI)sY{z?*M^WrD zo~6Io>XgIu`pbtLf^i}K>(@kmQBrsXj1zQs^SB?C2nA|myy>>lN;AGhytT$X|c$AVNz=Fyk>%ksh%VtH(E6(4Qbn*3{<`SkYr&#U7l|L2qcC zrbzoV*z9cK2BuM0Y57hIgwhg65M+B`X%#*IpI{qJ1@67P(7u?__W55s)PRN!OdizD zEYpB_IXhm7PoJtQ0p(MJ@G{&ulc$AacooqI^l(u}0?ue*g&>8wgb4|%Hr*QKqVjjh z{g^5{M}+t!;+Uxsf~>A)kw+a^=ahk_z-sw$A{0E2a>pJ{VWwtY6Y2LohdD18@IG=_ z35@FQ4C6nzJM#Pf@X=kr?5IMSQbSG0Q1sI4a4)R&{d9_=->7gH@xd9^+oyMmm4Ne_ zIkh`1CsdkbD!dfTS%b^@HtP9~p(ypQzk=&T>KC`9+Z3SF$=EeUsw&iu0J{=;_v*i4 z2r8c>vv+YCKiE&7a@W2Hw7geu9)SFF{Dce{6hGazj~)qk&3GY6$h>y_C{rp^3Lb_H zwAg_QW;~DsEX>OE$QYJHZ%B=`mYmUY1UZ-ue5CIFH+d*nmKBi}H^7A&;2ZOr3#4Xo zXZ)Ovty28O&liyF`V-8=_kJ*Z-pu>bn#^`%&3{PA^-HYiL8)UIW$Orx%qm&>g=g6l z%%>G7y2O&;sT#Fh-Cg>x|8<&-eP;h090X )37IYoRsfEv^2Vkv>o%20qnJoax+^ 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 + + + + bundleid + net.deanishe.alfred.gcal + connections + + 0553156D-6606-42C4-8BE8-18AE49A7A6D6 + + + destinationuid + 303E565E-8048-4ED5-BD89-7E58DF94BF3A + modifiers + 0 + modifiersubtext + + vitoclose + + + + 09A5FA54-F59A-45B7-8B66-A08BC6AB8EAC + + + destinationuid + CD8F2AB1-13E0-47E1-BFCA-701444818091 + modifiers + 0 + modifiersubtext + + vitoclose + + + + 0D457EF4-A96F-4760-A4DC-07F46AE409C6 + + + destinationuid + 8C1ADFA7-4A9A-4218-9125-F0D4C2763FEC + modifiers + 0 + modifiersubtext + + vitoclose + + + + 14C00640-8D74-4090-AB64-5BDEA4489D4D + + + destinationuid + CD8F2AB1-13E0-47E1-BFCA-701444818091 + modifiers + 0 + modifiersubtext + + vitoclose + + + + 29048BAE-27AC-42F1-B5B2-096753EDC2BF + + + destinationuid + 0D457EF4-A96F-4760-A4DC-07F46AE409C6 + modifiers + 0 + modifiersubtext + + vitoclose + + + + 2BCC750A-E35C-4EC6-8176-A0B302ECD417 + + + destinationuid + 05634575-6FA7-40A9-8772-47F71A9C0DFC + modifiers + 0 + modifiersubtext + + vitoclose + + + + 303E565E-8048-4ED5-BD89-7E58DF94BF3A + + + destinationuid + 9FA29DC3-2E67-45B6-B93B-F33F57EDF7E7 + modifiers + 0 + modifiersubtext + + vitoclose + + + + destinationuid + F5C23C7D-94BA-400C-8004-EC304CE8818D + modifiers + 0 + modifiersubtext + + vitoclose + + + + 30D50965-B9A7-4A5A-B69B-382D1FC7CA33 + + + destinationuid + FE9B2118-827D-416D-9829-1A9DC2CAACA4 + modifiers + 0 + modifiersubtext + + vitoclose + + + + 35996F87-80D8-414C-BFC0-8713D145E02F + + + destinationuid + 2512097E-AB92-489E-93AF-0146592CB0D4 + modifiers + 0 + modifiersubtext + + vitoclose + + + + 38618982-EFBD-4CEE-AB65-D33F0BBD3C54 + + + destinationuid + 63875CCD-EE31-4820-9FA8-D2058415B876 + modifiers + 0 + modifiersubtext + + vitoclose + + + + destinationuid + 5C211D72-B49C-4E67-BB21-5AA6934F0DCB + modifiers + 0 + modifiersubtext + + vitoclose + + + + destinationuid + 30D50965-B9A7-4A5A-B69B-382D1FC7CA33 + modifiers + 0 + modifiersubtext + + vitoclose + + + + destinationuid + 2BCC750A-E35C-4EC6-8176-A0B302ECD417 + modifiers + 0 + modifiersubtext + + vitoclose + + + + destinationuid + 29048BAE-27AC-42F1-B5B2-096753EDC2BF + modifiers + 0 + modifiersubtext + + vitoclose + + + + 3F938397-2CD9-45EB-B0CD-FC962DC9031F + + + destinationuid + EAF06D56-D2F1-4FB9-B0D7-89D494AA865B + modifiers + 0 + modifiersubtext + + vitoclose + + + + 55D10CF4-8457-4AE5-9DC0-64A7E262D61D + + + destinationuid + 35996F87-80D8-414C-BFC0-8713D145E02F + modifiers + 0 + modifiersubtext + + vitoclose + + + + 5C211D72-B49C-4E67-BB21-5AA6934F0DCB + + + destinationuid + 0553156D-6606-42C4-8BE8-18AE49A7A6D6 + modifiers + 0 + modifiersubtext + + vitoclose + + + + 62F44B78-0D8E-4215-BB14-E3950B214795 + + + destinationuid + 303E565E-8048-4ED5-BD89-7E58DF94BF3A + modifiers + 0 + modifiersubtext + + vitoclose + + + + 63875CCD-EE31-4820-9FA8-D2058415B876 + + + destinationuid + 62F44B78-0D8E-4215-BB14-E3950B214795 + modifiers + 0 + modifiersubtext + + vitoclose + + + + 65A55A9B-1414-464D-A0FB-9A2CF705164C + + + destinationuid + E77CCBEC-A3BA-4614-B1B0-12B880D25580 + modifiers + 0 + modifiersubtext + + vitoclose + + + + 6CFB2AE8-7BAF-4DE6-B949-D0FF2DB50FD7 + + + destinationuid + CD8F2AB1-13E0-47E1-BFCA-701444818091 + modifiers + 0 + modifiersubtext + + vitoclose + + + + 78516575-8825-4598-A589-2F3475E25DF8 + + + destinationuid + 6CFB2AE8-7BAF-4DE6-B949-D0FF2DB50FD7 + modifiers + 0 + modifiersubtext + + vitoclose + + + + 7A027517-A35E-4028-89FF-50172EA74768 + + 8604FB3C-23FB-467B-803B-17F6A73073AB + + + destinationuid + 55D10CF4-8457-4AE5-9DC0-64A7E262D61D + modifiers + 0 + modifiersubtext + + vitoclose + + + + 8C1ADFA7-4A9A-4218-9125-F0D4C2763FEC + + + destinationuid + 969420B1-18E8-4F0B-AC39-06D038718311 + modifiers + 0 + modifiersubtext + + vitoclose + + + + 8EBC4884-E01F-4999-8A4A-F5D50F75396A + + + destinationuid + 3F938397-2CD9-45EB-B0CD-FC962DC9031F + modifiers + 0 + modifiersubtext + + vitoclose + + + + 998D04E2-9538-4970-9233-1E57F0929ED0 + + + destinationuid + BF14B152-A9EC-4FCE-97DA-F62C1C7846D3 + modifiers + 0 + modifiersubtext + + vitoclose + + + + BC6819C2-77D8-4E53-BA51-2787F2087BFD + + + destinationuid + 14C00640-8D74-4090-AB64-5BDEA4489D4D + modifiers + 0 + modifiersubtext + + vitoclose + + + + BF14B152-A9EC-4FCE-97DA-F62C1C7846D3 + + + destinationuid + 7A027517-A35E-4028-89FF-50172EA74768 + modifiers + 0 + modifiersubtext + + vitoclose + + + + CD8F2AB1-13E0-47E1-BFCA-701444818091 + + + destinationuid + BF14B152-A9EC-4FCE-97DA-F62C1C7846D3 + modifiers + 0 + modifiersubtext + + vitoclose + + + + D55FAAFD-ABA8-4B37-940B-CB883E3BB590 + + + destinationuid + 38618982-EFBD-4CEE-AB65-D33F0BBD3C54 + modifiers + 0 + modifiersubtext + + vitoclose + + + + E77CCBEC-A3BA-4614-B1B0-12B880D25580 + + + destinationuid + CD8F2AB1-13E0-47E1-BFCA-701444818091 + modifiers + 0 + modifiersubtext + + vitoclose + + + + FE9B2118-827D-416D-9829-1A9DC2CAACA4 + + + destinationuid + F5C23C7D-94BA-400C-8004-EC304CE8818D + modifiers + 0 + modifiersubtext + + vitoclose + + + + + createdby + Dean Jackson <deanishe@deanishe.net> + description + View upcoming events in Google Calendar + disabled + + name + Google Calendar Events + objects + + + config + + argumenttype + 2 + keyword + gcal + subtext + Show upcoming events + text + Upcoming Events + withspace + + + type + alfred.workflow.input.keyword + uid + 998D04E2-9538-4970-9233-1E57F0929ED0 + version + 1 + + + config + + argumenttype + 2 + keyword + today + subtext + Today's events from your Google Calendar(s) + text + Today's Events + withspace + + + type + alfred.workflow.input.keyword + uid + BC6819C2-77D8-4E53-BA51-2787F2087BFD + version + 1 + + + config + + concurrently + + escaping + 102 + script + date '+%Y-%m-%d' + scriptargtype + 1 + scriptfile + + type + 0 + + type + alfred.workflow.action.script + uid + 14C00640-8D74-4090-AB64-5BDEA4489D4D + version + 2 + + + config + + argumenttype + 2 + keyword + tomorrow + subtext + Tomorrow's events from your Google Calendar(s) + text + Tomorrow's Events + withspace + + + type + alfred.workflow.input.keyword + uid + 65A55A9B-1414-464D-A0FB-9A2CF705164C + version + 1 + + + config + + concurrently + + escaping + 102 + script + date -v '+1d' '+%Y-%m-%d' + scriptargtype + 1 + scriptfile + + type + 0 + + type + alfred.workflow.action.script + uid + E77CCBEC-A3BA-4614-B1B0-12B880D25580 + version + 2 + + + config + + externaltriggerid + action + passinputasargument + + passvariables + + workflowbundleid + self + + type + alfred.workflow.output.callexternaltrigger + uid + 7A027517-A35E-4028-89FF-50172EA74768 + version + 1 + + + config + + argument + {query} + variables + + action + date + + + type + alfred.workflow.utility.argument + uid + BF14B152-A9EC-4FCE-97DA-F62C1C7846D3 + version + 1 + + + config + + type + 0 + + type + alfred.workflow.utility.transform + uid + CD8F2AB1-13E0-47E1-BFCA-701444818091 + version + 1 + + + config + + argumenttype + 2 + keyword + yesterday + subtext + Yesterday's events from your Google Calendar(s) + text + Yesterday's Events + withspace + + + type + alfred.workflow.input.keyword + uid + 78516575-8825-4598-A589-2F3475E25DF8 + version + 1 + + + config + + concurrently + + escaping + 102 + script + date -v '-1d' '+%Y-%m-%d' + scriptargtype + 1 + scriptfile + + type + 0 + + type + alfred.workflow.action.script + uid + 6CFB2AE8-7BAF-4DE6-B949-D0FF2DB50FD7 + version + 2 + + + config + + alfredfiltersresults + + alfredfiltersresultsmatchmode + 0 + argumenttrimmode + 0 + argumenttype + 1 + escaping + 102 + keyword + gdate + queuedelaycustom + 3 + queuedelayimmediatelyinitially + + queuedelaymode + 0 + queuemode + 1 + runningsubtext + Loading… + script + ./gcal dates -- "$1" + scriptargtype + 1 + scriptfile + + subtext + Enter a date + title + Events for a Specific Date + type + 0 + withspace + + + type + alfred.workflow.input.scriptfilter + uid + 09A5FA54-F59A-45B7-8B66-A08BC6AB8EAC + version + 2 + + + config + + externaltriggerid + action + passinputasargument + + passvariables + + workflowbundleid + self + + type + alfred.workflow.output.callexternaltrigger + uid + EAF06D56-D2F1-4FB9-B0D7-89D494AA865B + version + 1 + + + config + + triggerid + config + + type + alfred.workflow.trigger.external + uid + 8EBC4884-E01F-4999-8A4A-F5D50F75396A + version + 1 + + + config + + alfredfiltersresults + + alfredfiltersresultsmatchmode + 0 + argumenttrimmode + 0 + argumenttype + 1 + escaping + 102 + keyword + gcalconf + queuedelaycustom + 3 + queuedelayimmediatelyinitially + + queuedelaymode + 0 + queuemode + 1 + runningsubtext + Loading… + script + ./gcal config "$1" + scriptargtype + 1 + scriptfile + + subtext + View and edit workflow settings + title + Google Calendar Config + type + 0 + withspace + + + type + alfred.workflow.input.scriptfilter + uid + 3F938397-2CD9-45EB-B0CD-FC962DC9031F + version + 2 + + + config + + externaltriggerid + calendars + passinputasargument + + passvariables + + workflowbundleid + self + + type + alfred.workflow.output.callexternaltrigger + uid + 2512097E-AB92-489E-93AF-0146592CB0D4 + version + 1 + + + config + + concurrently + + escaping + 102 + script + ./gcal toggle "$1" + scriptargtype + 1 + scriptfile + + type + 0 + + type + alfred.workflow.action.script + uid + 35996F87-80D8-414C-BFC0-8713D145E02F + version + 2 + + + config + + alfredfiltersresults + + alfredfiltersresultsmatchmode + 0 + argumenttrimmode + 0 + argumenttype + 1 + escaping + 102 + queuedelaycustom + 3 + queuedelayimmediatelyinitially + + queuedelaymode + 0 + queuemode + 1 + runningsubtext + + script + ./gcal calendars "$1" + scriptargtype + 1 + scriptfile + + subtext + + title + + type + 0 + withspace + + + type + alfred.workflow.input.scriptfilter + uid + 55D10CF4-8457-4AE5-9DC0-64A7E262D61D + version + 2 + + + config + + triggerid + calendars + + type + alfred.workflow.trigger.external + uid + 8604FB3C-23FB-467B-803B-17F6A73073AB + version + 1 + + + config + + externaltriggerid + config + passinputasargument + + passvariables + + workflowbundleid + self + + type + alfred.workflow.output.callexternaltrigger + uid + 9FA29DC3-2E67-45B6-B93B-F33F57EDF7E7 + version + 1 + + + config + + concurrently + + escaping + 102 + script + ./gcal clear + scriptargtype + 1 + scriptfile + + type + 0 + + type + alfred.workflow.action.script + uid + 62F44B78-0D8E-4215-BB14-E3950B214795 + version + 2 + + + config + + inputstring + {var:action} + matchcasesensitive + + matchmode + 0 + matchstring + clear + + type + alfred.workflow.utility.filter + uid + 63875CCD-EE31-4820-9FA8-D2058415B876 + version + 1 + + + type + alfred.workflow.utility.junction + uid + 303E565E-8048-4ED5-BD89-7E58DF94BF3A + version + 1 + + + config + + concurrently + + escaping + 102 + script + ./gcal update workflow + scriptargtype + 1 + scriptfile + + type + 0 + + type + alfred.workflow.action.script + uid + 0553156D-6606-42C4-8BE8-18AE49A7A6D6 + version + 2 + + + config + + lastpathcomponent + + onlyshowifquerypopulated + + removeextension + + text + {query} + title + ERROR + + type + alfred.workflow.output.notification + uid + F5C23C7D-94BA-400C-8004-EC304CE8818D + version + 1 + + + config + + inputstring + {var:action} + matchcasesensitive + + matchmode + 0 + matchstring + update + + type + alfred.workflow.utility.filter + uid + 5C211D72-B49C-4E67-BB21-5AA6934F0DCB + version + 1 + + + config + + concurrently + + escaping + 102 + script + test -n "$CALENDAR_APP" && { + ./gcal open --app="$CALENDAR_APP" "$1" +} || { + ./gcal open "$1" +} + scriptargtype + 1 + scriptfile + + type + 5 + + type + alfred.workflow.action.script + uid + FE9B2118-827D-416D-9829-1A9DC2CAACA4 + version + 2 + + + config + + triggerid + action + + type + alfred.workflow.trigger.external + uid + D55FAAFD-ABA8-4B37-940B-CB883E3BB590 + version + 1 + + + config + + argument + . +/---- ACTION IN ----\ +query={query} +variables={allvars} +\-------------------/ + cleardebuggertext + + processoutputs + + + type + alfred.workflow.utility.debug + uid + 38618982-EFBD-4CEE-AB65-D33F0BBD3C54 + version + 1 + + + config + + inputstring + {var:action} + matchcasesensitive + + matchmode + 0 + matchstring + open + + type + alfred.workflow.utility.filter + uid + 30D50965-B9A7-4A5A-B69B-382D1FC7CA33 + version + 1 + + + config + + externaltriggerid + calendars + passinputasargument + + passvariables + + workflowbundleid + self + + type + alfred.workflow.output.callexternaltrigger + uid + 05634575-6FA7-40A9-8772-47F71A9C0DFC + version + 1 + + + config + + inputstring + {var:action} + matchcasesensitive + + matchmode + 0 + matchstring + calendars + + type + alfred.workflow.utility.filter + uid + 2BCC750A-E35C-4EC6-8176-A0B302ECD417 + version + 1 + + + config + + externaltriggerid + action + passinputasargument + + passvariables + + workflowbundleid + self + + type + alfred.workflow.output.callexternaltrigger + uid + 969420B1-18E8-4F0B-AC39-06D038718311 + version + 1 + + + config + + alfredfiltersresults + + alfredfiltersresultsmatchmode + 0 + argumenttrimmode + 0 + argumenttype + 1 + escaping + 102 + queuedelaycustom + 3 + queuedelayimmediatelyinitially + + queuedelaymode + 0 + queuemode + 1 + runningsubtext + + script + test -n "$date" && { + ./gcal events --date="$date" "$1" +} || { + ./gcal events "$1" +} + scriptargtype + 1 + scriptfile + + subtext + + title + + type + 5 + withspace + + + type + alfred.workflow.input.scriptfilter + uid + 8C1ADFA7-4A9A-4218-9125-F0D4C2763FEC + version + 2 + + + config + + argument + + variables + + date + {query} + + + type + alfred.workflow.utility.argument + uid + 0D457EF4-A96F-4760-A4DC-07F46AE409C6 + version + 1 + + + config + + inputstring + {var:action} + matchcasesensitive + + matchmode + 0 + matchstring + date + + type + alfred.workflow.utility.filter + uid + 29048BAE-27AC-42F1-B5B2-096753EDC2BF + version + 1 + + + readme + Google Calendar +=============== + +View events from your Google calendars. + +Configuration +------------- + +`APPLE_MAPS`: Set to "1" to open location URLs in Apple Maps, not Google Maps. + +`CALENDAR_APP`: Set to an application name to open calendar URLs (not map URLs) in an application other than your default browser (e.g. a session-specific browser). + +`EVENT_CACHE_MINUTES`: How many minutes to cache events for. + +`SCHEDULE_DAYS`: How many days' events to show in the "Upcoming Events" list (keyword: "gcal"). + uidata + + 0553156D-6606-42C4-8BE8-18AE49A7A6D6 + + note + Check for new version of the workflow + xpos + 560 + ypos + 1310 + + 05634575-6FA7-40A9-8772-47F71A9C0DFC + + xpos + 560 + ypos + 1640 + + 09A5FA54-F59A-45B7-8B66-A08BC6AB8EAC + + note + Show events for a given date + xpos + 210 + ypos + 660 + + 0D457EF4-A96F-4760-A4DC-07F46AE409C6 + + note + Set $date from {query} + xpos + 600 + ypos + 1810 + + 14C00640-8D74-4090-AB64-5BDEA4489D4D + + xpos + 210 + ypos + 200 + + 2512097E-AB92-489E-93AF-0146592CB0D4 + + xpos + 560 + ypos + 1010 + + 29048BAE-27AC-42F1-B5B2-096753EDC2BF + + note + $action = date + xpos + 430 + ypos + 1810 + + 2BCC750A-E35C-4EC6-8176-A0B302ECD417 + + note + $action = calendars + xpos + 430 + ypos + 1670 + + 303E565E-8048-4ED5-BD89-7E58DF94BF3A + + xpos + 780 + ypos + 1260 + + 30D50965-B9A7-4A5A-B69B-382D1FC7CA33 + + note + $action = open + xpos + 430 + ypos + 1510 + + 35996F87-80D8-414C-BFC0-8713D145E02F + + xpos + 390 + ypos + 1010 + + 38618982-EFBD-4CEE-AB65-D33F0BBD3C54 + + xpos + 250 + ypos + 1510 + + 3F938397-2CD9-45EB-B0CD-FC962DC9031F + + note + Show workflow configuration + xpos + 210 + ypos + 830 + + 55D10CF4-8457-4AE5-9DC0-64A7E262D61D + + note + Toggle calendars on/off + xpos + 210 + ypos + 1010 + + 5C211D72-B49C-4E67-BB21-5AA6934F0DCB + + note + $action = update + xpos + 430 + ypos + 1340 + + 62F44B78-0D8E-4215-BB14-E3950B214795 + + note + Clear old cache files + xpos + 560 + ypos + 1150 + + 63875CCD-EE31-4820-9FA8-D2058415B876 + + note + $action = clear + xpos + 430 + ypos + 1180 + + 65A55A9B-1414-464D-A0FB-9A2CF705164C + + note + Show tomorrow's events + xpos + 40 + ypos + 360 + + 6CFB2AE8-7BAF-4DE6-B949-D0FF2DB50FD7 + + xpos + 210 + ypos + 520 + + 78516575-8825-4598-A589-2F3475E25DF8 + + note + Show yesterday's events + xpos + 40 + ypos + 520 + + 7A027517-A35E-4028-89FF-50172EA74768 + + xpos + 760 + ypos + 360 + + 8604FB3C-23FB-467B-803B-17F6A73073AB + + xpos + 40 + ypos + 1010 + + 8C1ADFA7-4A9A-4218-9125-F0D4C2763FEC + + note + Show events + xpos + 740 + ypos + 1780 + + 8EBC4884-E01F-4999-8A4A-F5D50F75396A + + xpos + 40 + ypos + 830 + + 969420B1-18E8-4F0B-AC39-06D038718311 + + xpos + 920 + ypos + 1780 + + 998D04E2-9538-4970-9233-1E57F0929ED0 + + note + Show upcoming events + xpos + 210 + ypos + 40 + + 9FA29DC3-2E67-45B6-B93B-F33F57EDF7E7 + + xpos + 920 + ypos + 1150 + + BC6819C2-77D8-4E53-BA51-2787F2087BFD + + note + Show today's events + xpos + 40 + ypos + 200 + + BF14B152-A9EC-4FCE-97DA-F62C1C7846D3 + + note + Set $action to "date" + xpos + 620 + ypos + 390 + + CD8F2AB1-13E0-47E1-BFCA-701444818091 + + note + Trim whitespace + xpos + 450 + ypos + 390 + + D55FAAFD-ABA8-4B37-940B-CB883E3BB590 + + xpos + 40 + ypos + 1480 + + E77CCBEC-A3BA-4614-B1B0-12B880D25580 + + xpos + 210 + ypos + 360 + + EAF06D56-D2F1-4FB9-B0D7-89D494AA865B + + xpos + 390 + ypos + 830 + + F5C23C7D-94BA-400C-8004-EC304CE8818D + + xpos + 920 + ypos + 1310 + + FE9B2118-827D-416D-9829-1A9DC2CAACA4 + + note + Open calendar URL + xpos + 560 + ypos + 1480 + + + variables + + APPLE_MAPS + 1 + CALENDAR_APP + + EVENT_CACHE_MINS + 15 + SCHEDULE_DAYS + 5 + + version + 0.1 + webaddress + + + diff --git a/magic.go b/magic.go new file mode 100644 index 0000000..2264bdc --- /dev/null +++ b/magic.go @@ -0,0 +1,25 @@ +// +// Copyright (c) 2017 Dean Jackson +// +// MIT Licence. See http://opensource.org/licenses/MIT +// +// Created on 2017-11-25 +// + +package main + +import ( + "fmt" + "os/exec" +) + +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 { + script := fmt.Sprintf(`tell application "Alfred 3" to run trigger "calendars" in workflow "%s"`, wf.BundleID()) + cmd := exec.Command("/usr/bin/osascript", "-e", script) + return cmd.Run() +} diff --git a/modd.conf b/modd.conf new file mode 100644 index 0000000..9cef29e --- /dev/null +++ b/modd.conf @@ -0,0 +1,3 @@ +**/*.go *.html ./bin/build { + prep: go test -v @dirmods && ./bin/build +} diff --git a/preview.html b/preview.html new file mode 100644 index 0000000..bc39770 --- /dev/null +++ b/preview.html @@ -0,0 +1,124 @@ +{{ define "event" }} + + + + + {{ .Title }} + + + +
+

{{ .Title }}

+

in {{ .CalendarTitle }}

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

Event Not Found

+

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

+ + +{{ end }} \ No newline at end of file diff --git a/secret.go b/secret.go new file mode 100644 index 0000000..692195e --- /dev/null +++ b/secret.go @@ -0,0 +1,25 @@ +// +// Copyright (c) 2017 Dean Jackson +// +// MIT Licence. See http://opensource.org/licenses/MIT +// +// Created on 2017-11-25 +// + +package main + +const secret = ` +{ + "web": { + "redirect_uris": [ + "http://localhost:61432" + ], + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "client_id": "65151598976-4c31heuthvab00tddcfplaev96bduv25.apps.googleusercontent.com", + "project_id": "alfred-calendar-summary", + "client_secret": "YFfDSH18F_AJDpJFq-s9TrGr", + "token_uri": "https://accounts.google.com/o/oauth2/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs" + } +} +`