Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added docs for getcalendar and helper script for authorization #9

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions calendar_auth.scpt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
tell app "Calendar" to calendars
35 changes: 35 additions & 0 deletions getcalendar.readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Setup instructions for getcalendar script

There's one particular piece of the MacOS Calendar integration that is tricky enough it deserves explanation: authorization.

Before the `getcalendar.swift` script can actually read calendar data, _the process that invokes it needs to be granted permission to read the Calendar_

However, you can't just add this permission via System Preferences. There's no "add" button for Calendar. Why not? No one seems to know.

Instead, you need to have the invoking process run a small AppleScript that triggers the authorization panel for user interaction. This script is included here as `calendar_auth.scpt`

What do I mean by "invoking process"?

Well, if you're using Espanso as a keyboard macro / substitution tool, Espanso has to be granted permission. Same with tools like Keyboard Maestro. For these tools, the solution is to have them invoke the provided Apple Script _just once_ after which they're good to go.

For Espanso, do something like this in your base.yml configuration:
```
# run getcalendar tana paste integration
- trigger: ";;cal"
replace: "{{output}}"
vars:
- name: output
type: shell
params:
cmd: "~/dev/tana/tana-paste-examples/getcalendar.swift -me 'Brett Adam'"

- trigger: ";;setup"
replace: "{{output}}"
vars:
- name: output
type: shell
params:
cmd: "osascript ~/dev/tana/tana-paste-examples/calendar_auth.scpt"
```
And then invoke the `;;setup` macro one time.

141 changes: 107 additions & 34 deletions getcalendar.swift
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,63 +1,117 @@
#!/usr/bin/swift

/*

*/

import Foundation
import EventKit

var args = CommandLine.arguments
let script_name = args[0]

func help() {
fputs(
"""
Simple script to grab calendar events via the Apple Calendar
app, filter them and then format them as a tana-paste format
blob of text. Use a keyboard macro accelerator or other
mechanism to get this into Tana

If you run this from terminal you can do:

./getcalendar | pbcopy
./getcalendar | pbcopy

And then simply paste the result into Tana.


Accepts arguments, many of which should be quoted on the cmd line:

-calendar "name of calendar" <default: Calendar >
-calendar "name of calendar" <default: Calendar >

-me "name of yourself in meeting attendees" <default: Me >
Script removes yourself from meeting attendees. If you leave it as default,
you will be included since Calendar doesn't use "me" as a name anywhere
-me "name of yourself in meeting attendees" <default: Me >
Script removes yourself from meeting attendees. If you leave it as default,
you will be included since Calendar doesn't use "me" as a name anywhere

-ignore "event title to ignore" (can be repeated)
-ignore "event title to ignore" (can be repeated)

<starts out as defaults "Block", "Lunch", "DNS/Focus time", "DNS/Lunch", "Focus time" >
<starts out as defaults "Block", "Lunch", "DNS/Focus time", "DNS/Lunch", "Focus time" >

-solo (if present, include meetings with a single attendee)
-solo (if present, include meetings with a single attendee)

-one2one "#[[tag name for one2one meetings]]" <default #[[1:1]] >
-one2one "#[[tag name for one2one meetings]]" <default #[[1:1]] >

-meeting "#[[tag name for regular meetings]]" <default: #[[meeting]] >
-meeting "#[[tag name for regular meetings]]" <default: #[[meeting]] >

-person "#[[tag name for attendees]]" <default: #person >
-person "#[[tag name for attendees]]" <default: #person >

-offset <default 0>
Which day to query for. +1 means tomorrow, -1 mean yesterday

-range <default 1>
How many days to query for from offset.

-json
Emit a JSON blob per event in addition to the other output as a Tana field

Example:

./getcalendar.swift -me "Brett Adam" -person "#people"
*/
./getcalendar.swift -me "Brett Adam" -person "#people"

Calendar access authorization:

This script will produce empty results until your script runner is authorized to access
your calendar via Calendar.app

See the associated getcalendar.readme.md file for instructions.
""", stdout)
}

func usage() {
print("Usage: \(script_name)\n")
help()
exit(1)
}

import Foundation
import EventKit

// TODO: make these parameters somehow!
var calendar_name = "Calendar"
var self_name = "Me"
var titles_to_ignore = ["Block", "Lunch", "DNS/Focus time", "DNS/Lunch", "Focus time" ]
var ignore_solo_meetings = true
var emit_json = false

var meeting_tag = "#meeting"
var one2one_tag = "#[[1:1]]"
var person_tag = "#person"
var day_offset:Int = 0
var day_range = 1

var next:String? = nil

var args = CommandLine.arguments
args.removeFirst()

args.removeFirst() // strip command itself
for argument in args {
if next == nil {
next = argument
// process zero-param toggles
if next != nil {
switch next {
case "-help":
help()
exit(0)
case "-solo":
ignore_solo_meetings = false
next = nil
continue // get next arg
case "-json":
emit_json = true
next = nil
continue // get next arg
default:
continue // move on to process arg
}
}
}

// process the arg after the switch
if next != nil {
switch next {
case "-calendar":
Expand All @@ -66,23 +120,27 @@ for argument in args {
self_name = argument
case "-ignore":
titles_to_ignore.append(argument)
case "-solo":
ignore_solo_meetings = false
case "-one2one":
one2one_tag = argument
case "-meeting":
meeting_tag = argument
case "-person":
person_tag = argument
case "-offset":
day_offset = Int(argument) ?? 0
case "-range":
day_range = Int(argument) ?? 1
default:
fputs("Unknown argument " + next! + "\n", stderr)
exit(1)
fputs("Unknown argument " + next! + "\n\n", stderr)
usage()
}
next = nil
}
else {
next = argument
}
}

if next != nil {
fputs("Missing argument for " + next! + "\n\n", stderr)
usage()
}

// tana-paste format to follow...
Expand Down Expand Up @@ -123,7 +181,7 @@ struct Attendee: Codable {

let eventStore = EKEventStore()

// Ask for Calendar access, reset in casse we messed up earlier
// Ask for Calendar access, reset in case we messed up earlier
// See stackoverflow.
// IMPORTANT: you must grant Calendar access to whatever script runner
// you are using. This can be tricky to pull off since you cannot do
Expand All @@ -140,23 +198,26 @@ eventStore.requestAccess(to: .event) { (granted, error) in

let today = Calendar.current.startOfDay(for: Date())

let startDate = Calendar.current.date(byAdding: .day, value: 0, to: today)!
let endDate = Calendar.current.date(byAdding: .day, value: +1, to: today)!
let startDate = Calendar.current.date(byAdding: .day, value: 0 + day_offset, to: today)!
let endDate = Calendar.current.date(byAdding: .day, value: day_range + day_offset, to: today)!

let calendars = eventStore.calendars(for: .event )

let predicate = eventStore.predicateForEvents(withStart: startDate, end: endDate, calendars: nil)

let events = eventStore.events(matching: predicate)

// process all of the evewnts

// filter all the events we don't care about
// and narrow to our single relevant calendar
let filteredEvents = events.filter { event in
event.calendar.title == calendar_name
&& !titles_to_ignore.contains(event.title)
}

// Now map the event array to JSON strings
// Now map the event array to our own internal structure
// stripping off various aspects as we go along
let eventArray = filteredEvents.map { event in
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd H:mm"
Expand Down Expand Up @@ -207,16 +268,26 @@ let eventArray = filteredEvents.map { event in
)
}


// generate ouutput in tana-paste format
for event in eventArray {
var node_tag = meeting_tag
var name = "- " + event.title + " with "
var attendee_field = " - Attendees:: \n"
var count = 0
let num_attendees = event.attendees?.count ?? 0

if num_attendees >= 5 {
name = name + " (many people)"
}

for attendee in event.attendees ?? [] {
count += 1
name = name + " [[" + attendee.name + "]]"
if attendee.name != self_name {
// don't put more than 5 people in the name of the meeting node
if num_attendees < 5 {
name = name + " [[" + attendee.name + "]]"
}

attendee_field = attendee_field + " - [[" + attendee.name + person_tag + "]]\n"
}
else {
Expand All @@ -237,7 +308,9 @@ for event in eventArray {
print(" - Start time:: [[date:" + String(event.startDate) + "/" + String(event.endDate) + "]]")

// spit out JSON for further examination or to feed RAW to some other API
// emitJSON(event:event);
if emit_json {
emitJSON(event:event)
}
}

// OLD JSON code if you want to see raw data
Expand Down