diff --git a/calendar_auth.scpt b/calendar_auth.scpt new file mode 100644 index 0000000..db1cf7e --- /dev/null +++ b/calendar_auth.scpt @@ -0,0 +1 @@ +tell app "Calendar" to calendars diff --git a/getcalendar.readme.md b/getcalendar.readme.md new file mode 100644 index 0000000..63c393e --- /dev/null +++ b/getcalendar.readme.md @@ -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. + diff --git a/getcalendar.swift b/getcalendar.swift old mode 100644 new mode 100755 index d742509..f06e114 --- a/getcalendar.swift +++ b/getcalendar.swift @@ -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" + -calendar "name of calendar" - -me "name of yourself in meeting attendees" - 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" + 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) - + - -solo (if present, include meetings with a single attendee) + -solo (if present, include meetings with a single attendee) - -one2one "#[[tag name for one2one meetings]]" + -one2one "#[[tag name for one2one meetings]]" - -meeting "#[[tag name for regular meetings]]" + -meeting "#[[tag name for regular meetings]]" - -person "#[[tag name for attendees]]" + -person "#[[tag name for attendees]]" + -offset + Which day to query for. +1 means tomorrow, -1 mean yesterday + + -range + 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": @@ -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... @@ -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 @@ -140,8 +198,8 @@ 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 ) @@ -149,6 +207,8 @@ let predicate = eventStore.predicateForEvents(withStart: startDate, end: endDate 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 @@ -156,7 +216,8 @@ let filteredEvents = events.filter { event in && !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" @@ -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 { @@ -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