From 80b4f981a9e1173b09d64045d8a8669a53799f5d Mon Sep 17 00:00:00 2001 From: verveguy Date: Fri, 31 Mar 2023 15:52:34 -0400 Subject: [PATCH 1/3] Added docs for getcalendar and helper script for authorization --- calendar_auth.scpt | 1 + getcalendar.readme.md | 35 +++++++++++++++++++++++++++++++++++ getcalendar.swift | 9 ++++++++- 3 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 calendar_auth.scpt create mode 100644 getcalendar.readme.md mode change 100644 => 100755 getcalendar.swift 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..e61b804 --- a/getcalendar.swift +++ b/getcalendar.swift @@ -37,6 +37,13 @@ Example: ./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. */ import Foundation @@ -123,7 +130,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 From 15ebf72015d1616103cd8278fa88dd7e803a84da Mon Sep 17 00:00:00 2001 From: verveguy Date: Sun, 2 Apr 2023 21:35:35 -0400 Subject: [PATCH 2/3] Added -help option as well as -offset and -range Allows for retrieving other days and more days at once --- getcalendar.swift | 64 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 44 insertions(+), 20 deletions(-) diff --git a/getcalendar.swift b/getcalendar.swift index e61b804..2228a32 100755 --- a/getcalendar.swift +++ b/getcalendar.swift @@ -1,42 +1,53 @@ #!/usr/bin/swift /* + +*/ + +import Foundation +import EventKit + +func usage() { + 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. Example: - ./getcalendar.swift -me "Brett Adam" -person "#people" + ./getcalendar.swift -me "Brett Adam" -person "#people" Calendar access authorization: @@ -44,10 +55,8 @@ your calendar via Calendar.app See the associated getcalendar.readme.md file for instructions. -*/ - -import Foundation -import EventKit + """, stdout) +} // TODO: make these parameters somehow! var calendar_name = "Calendar" @@ -58,12 +67,19 @@ var ignore_solo_meetings = true 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() +if args.count == 0 { + usage() + exit(1) +} + for argument in args { if next != nil { switch next { @@ -81,6 +97,10 @@ for argument in args { 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) @@ -89,6 +109,10 @@ for argument in args { } else { next = argument + if next == "-help" { + usage(); + exit(0) + } } } @@ -147,8 +171,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 ) From bd111bc56969cd30766c829530b2739639636dc2 Mon Sep 17 00:00:00 2001 From: verveguy Date: Tue, 4 Apr 2023 10:22:00 -0400 Subject: [PATCH 3/3] Restored JSON feature as option, improved large meeting node names Also improved cmd line option handling --- getcalendar.swift | 92 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 67 insertions(+), 25 deletions(-) diff --git a/getcalendar.swift b/getcalendar.swift index 2228a32..f06e114 100755 --- a/getcalendar.swift +++ b/getcalendar.swift @@ -7,8 +7,12 @@ import Foundation import EventKit -func usage() { - fputs(""" +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 @@ -45,6 +49,9 @@ func usage() { -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" @@ -58,11 +65,19 @@ func usage() { """, stdout) } +func usage() { + print("Usage: \(script_name)\n") + help() + exit(1) +} + + // 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]]" @@ -72,15 +87,31 @@ var day_range = 1 var next:String? = nil -var args = CommandLine.arguments -args.removeFirst() - -if args.count == 0 { - usage() - exit(1) -} - +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": @@ -89,8 +120,6 @@ 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": @@ -102,18 +131,16 @@ for argument in args { 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 == "-help" { - usage(); - exit(0) - } - } +} + +if next != nil { + fputs("Missing argument for " + next! + "\n\n", stderr) + usage() } // tana-paste format to follow... @@ -180,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 @@ -187,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" @@ -238,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 { @@ -268,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