Jump to content

Calendar Availability workflow - w/ support for multiple calendars and recurring events


Recommended Posts

Hi all, I've been looking for ages for a workflow that helps me to paste my next available calendar slots for the next X days. I've come across a ton of workflows / options after thorough research (see below) but neither one of them are working 100%. Could anyone point me in the right direction on the options below?

1) Existing Alfred workflow: Calendar Availability

This fit exactly my needs but seems not maintained / updated anymore - except one missing feature: it doesn't support recurring events and also pulls the calendar events still from the sqlite cache.


2) Swift script using EventKit (needs adaptions to support a bigger lookahead window
I found a Swift shell script using EventKit and AppKit in this blog post. I'm trying to directly call the swift script (Github) from Raycast but it fails. I checked all the path variables and i'm using zsh as shell. Unfortunately I got stuck here. This script gives only availability for today and doesn't support the next X days, but it seems easy to refactor. Any thoughts why this wouldn't work right out of the box?

 

Code:

#!/usr/bin/swift

// Required parameters:
// @raycast.schemaVersion 1
// @raycast.title Copy Availability
// @raycast.mode silent
// @raycast.packageName System
//
// Optional parameters:
// @raycast.icon 📅
//
// Documentation:
// @raycast.description Copies the calendar availability of today.

import AppKit
import EventKit

// MARK: - Main

let now = Date()
let startOfToday = Calendar.current.startOfDay(for: now)
let endOfToday = Calendar.current.date(byAdding: .day, value: 1, to: startOfToday)!

let eventStore = EKEventStore()
let predicate = eventStore.predicateForEvents(withStart: startOfToday, end: endOfToday, calendars: nil)
let eventsOfToday = eventStore.events(matching: predicate).filter { !$0.isAllDay }

let availability: String
if eventsOfToday.isEmpty {
  availability = "I'm available the full day."
} else if eventsOfToday.allSatisfy({ $0.endDate.isAfternoon }) {
  availability = "I'm available in the morning."
} else if eventsOfToday.allSatisfy({ $0.endDate.isMorning }) {
  availability = "I'm available in the afternoon."
} else {
  let busyTimes = eventsOfToday.map { $0.startDate...$0.endDate }
  
  let availableTimes = getAvailableTimesForToday(excluding: busyTimes)
  let prettyPrintedAvailableTimes = availableTimes
    .map { (from: DateFormatter.shortTime.string(from: $0.lowerBound), to: DateFormatter.shortTime.string(from: $0.upperBound)) }
    .map { "- \($0.from) - \($0.to)" }
    .joined(separator: "\n")

  availability = "Here's my availability for today:\n\(prettyPrintedAvailableTimes)"
}

copy(availability)
print("Copied availability")

// MARK: - Convenience

extension DateFormatter {
  static var shortTime: DateFormatter {
    let dateFormatter = DateFormatter()
    dateFormatter.dateStyle = .none
    dateFormatter.timeStyle = .short
    return dateFormatter
  }
}

extension Date {
  var isMorning: Bool { Calendar.current.component(.hour, from: self) <= 11 }
  var isAfternoon: Bool { Calendar.current.component(.hour, from: self) >= 12 }
}

func getAvailableTimesForToday(excluding excludedTimes: [ClosedRange<Date>]) -> [ClosedRange<Date>] {
  let startOfWorkDay = Calendar.current.date(bySettingHour: 9, minute: 0, second: 0, of: startOfToday)!
  let endOfWorkDay = Calendar.current.date(bySettingHour: 17, minute: 0, second: 0, of: startOfToday)!
  let workDay = startOfWorkDay...endOfWorkDay

  let busyTimes = [startOfToday...startOfWorkDay] + excludedTimes + [endOfWorkDay...endOfToday]
  var previousBusyTime = busyTimes.first
  var availableTimes = [ClosedRange<Date>]()
  for time in busyTimes {
    if let previousEnd = previousBusyTime?.upperBound, previousEnd < time.lowerBound {
      var newAvailability = previousEnd...time.lowerBound
      if let lastAvailability = availableTimes.last, newAvailability.overlaps(lastAvailability) {
        newAvailability = newAvailability.clamped(to: lastAvailability).clamped(to: workDay)
        availableTimes.insert(newAvailability, at: availableTimes.count - 1)
      } else {
        newAvailability = newAvailability.clamped(to: workDay)
        availableTimes.append(newAvailability)
      }
    }
    previousBusyTime = time
  }

  return availableTimes
}

func copy(_ string: String) {
  NSPasteboard.general.declareTypes([NSPasteboard.PasteboardType.string], owner: nil)
  NSPasteboard.general.setString(string, forType: NSPasteboard.PasteboardType.string)
}



Alfred log:

[13:52:22.059] Swift Test[Keyword] Processing complete
[13:52:22.060] Swift Test[Keyword] Passing output '' to Run Script
[13:52:22.070] ERROR: Swift Test[Run Script] zsh:27: parse error near `}'

 

3) Updating Calendar Avail workflow
Last option I tried to get to work is to combine the existing but dated Calendar Availability workflow with the amazing work of @deanishe 's Video Conference workflow and replacing the SQlite part (as it doesn't support recurring events) of retrieving the events by the more recent EventKit in swift (using CalendarEvents.scpt from the video conference workflow). However, I got stuck here too unfortunately as I don't know how to build the package/workflow.

4) iOS / MacOS Shortcut apps
Then, the last option was to look into different Shortcut apps (see one here) which I could then trigger from Alfred. However, this is not ideal as I prefer a script and most workflows support also only one calendar. 


5) Then there's a Non-alfred option / chrome extension:

Available: Calendar Availability Chrome Extension;  this one looks OK but rather not give permissions to non-established extensions and prefer Alfred.

So these are all the existing options I found but none of them are either recent or fulfill my needs. Is there anyone that could point me in the right direction for a working workflow? Hugely appreciated!

Requirements are quite simple:

- supporting recurring events seen as unavailable

- supporting multiple calendars (I have my work calendar, a shared family calendar and my personal calendar

- support 24h format, working hours and flexbility with all day events

- prefer an alfred workflow rather than another tool (calendly, keyboard meastro, etc) - only desktop support is ok for now

- paste my next available calendar slots to clipboard

Link to comment
On 3/8/2022 at 9:59 PM, jxxst said:
[13:52:22.070] ERROR: Swift Test[Run Script] zsh:27: parse error near `}'

 

Are you pasting the code into the Script box? You’re trying to use Zsh to run Swift code. Set the language to External Script instead and put the code in the file.

 

On 3/8/2022 at 9:59 PM, jxxst said:

This script gives only availability for today and doesn't support the next X days, but it seems easy to refactor.

 

Increasing the 1 in the line below to however many days you want to check in advance might work. But it might not: I have neither dealt with the Calendar API nor have I thoroughly checked the rest of the script to see if there’s something else which would make it not work well for multiple days.

 

On 3/8/2022 at 9:59 PM, jxxst said:
let endOfToday = Calendar.current.date(byAdding: .day, value: 1, to: startOfToday)!
Link to comment

Thanks for the pointers! I didn't think about that and thought the swift shebang would be enough. I now tried running it as external script, added it to the workflow folder and called it as external script (./calendar-availability.swift). It reads the file into the text area so the location is correct. Running the workflow now gives a "Unable to run task! Reason: Couldn't posix_spawn: error 1" error, unfortunately.

 

Swift (version 5..) is at the correct path and matches the shebang in the file:

 ~ which swift
/usr/bin/swift

 

Also I applied chmod +x on the entire workflow directory, which would still give me the error. At least we're making a bit of progress and I learned something! 🙂

 

EDIT - the working version:

In the end I'm able to run the script successfully by calling the script within the workflow directory from using Run Script > /bin/zsh/ directly from shell instead of the external script option. Everything working so far.

Now I just need to find a way to refactor the script to 1) support multiple days lookahead and 2) exclude weekends as just changing the numbers didn't work. 👍

Script:

#!/bin/zsh
swift ./calendar-availability.swift


Output:

[16:22:30.472] STDERR: Swift Test[Run Script] 2022-03-10 16:22:30.202 swift-frontend[22343:244193] XXX: countOfStores: 1, countOfAccounts: 1
[16:22:30.488] Swift Test[Run Script] Processing complete
[16:22:30.490] Swift Test[Run Script] Passing output 'Copied availability
' to Post Notification

 

 

Edited by jxxst
Link to comment
  • 10 months later...
On 3/9/2022 at 3:29 AM, jxxst said:

Hi all, I've been looking for ages for a workflow that helps me to paste my next available calendar slots for the next X days. I've come across a ton of workflows / options after thorough research (see below) but neither one of them are working 100%. Could anyone point me in the right direction on the options below?

1) Existing Alfred workflow: Calendar Availability

This fit exactly my needs but seems not maintained / updated anymore - except one missing feature: it doesn't support recurring events and also pulls the calendar events still from the sqlite cache.


2) Swift script using EventKit (needs adaptions to support a bigger lookahead window
I found a Swift shell script using EventKit and AppKit in this blog post. I'm trying to directly call the swift script (Github) from Raycast but it fails. I checked all the path variables and i'm using zsh as shell. Unfortunately I got stuck here. This script gives only availability for today and doesn't support the next X days, but it seems easy to refactor. Any thoughts why this wouldn't work right out of the box?

 

Code:

#!/usr/bin/swift

// Required parameters:
// @raycast.schemaVersion 1
// @raycast.title Copy Availability
// @raycast.mode silent
// @raycast.packageName System
//
// Optional parameters:
// @raycast.icon 📅
//
// Documentation:
// @raycast.description Copies the calendar availability of today.

import AppKit
import EventKit

// MARK: - Main

let now = Date()
let startOfToday = Calendar.current.startOfDay(for: now)
let endOfToday = Calendar.current.date(byAdding: .day, value: 1, to: startOfToday)!

let eventStore = EKEventStore()
let predicate = eventStore.predicateForEvents(withStart: startOfToday, end: endOfToday, calendars: nil)
let eventsOfToday = eventStore.events(matching: predicate).filter { !$0.isAllDay }

let availability: String
if eventsOfToday.isEmpty {
  availability = "I'm available the full day."
} else if eventsOfToday.allSatisfy({ $0.endDate.isAfternoon }) {
  availability = "I'm available in the morning."
} else if eventsOfToday.allSatisfy({ $0.endDate.isMorning }) {
  availability = "I'm available in the afternoon."
} else {
  let busyTimes = eventsOfToday.map { $0.startDate...$0.endDate }
  
  let availableTimes = getAvailableTimesForToday(excluding: busyTimes)
  let prettyPrintedAvailableTimes = availableTimes
    .map { (from: DateFormatter.shortTime.string(from: $0.lowerBound), to: DateFormatter.shortTime.string(from: $0.upperBound)) }
    .map { "- \($0.from) - \($0.to)" }
    .joined(separator: "\n")

  availability = "Here's my availability for today:\n\(prettyPrintedAvailableTimes)"
}

copy(availability)
print("Copied availability")

// MARK: - Convenience

extension DateFormatter {
  static var shortTime: DateFormatter {
    let dateFormatter = DateFormatter()
    dateFormatter.dateStyle = .none
    dateFormatter.timeStyle = .short
    return dateFormatter
  }
}

extension Date {
  var isMorning: Bool { Calendar.current.component(.hour, from: self) <= 11 }
  var isAfternoon: Bool { Calendar.current.component(.hour, from: self) >= 12 }
}

func getAvailableTimesForToday(excluding excludedTimes: [ClosedRange<Date>]) -> [ClosedRange<Date>] {
  let startOfWorkDay = Calendar.current.date(bySettingHour: 9, minute: 0, second: 0, of: startOfToday)!
  let endOfWorkDay = Calendar.current.date(bySettingHour: 17, minute: 0, second: 0, of: startOfToday)!
  let workDay = startOfWorkDay...endOfWorkDay

  let busyTimes = [startOfToday...startOfWorkDay] + excludedTimes + [endOfWorkDay...endOfToday]
  var previousBusyTime = busyTimes.first
  var availableTimes = [ClosedRange<Date>]()
  for time in busyTimes {
    if let previousEnd = previousBusyTime?.upperBound, previousEnd < time.lowerBound {
      var newAvailability = previousEnd...time.lowerBound
      if let lastAvailability = availableTimes.last, newAvailability.overlaps(lastAvailability) {
        newAvailability = newAvailability.clamped(to: lastAvailability).clamped(to: workDay)
        availableTimes.insert(newAvailability, at: availableTimes.count - 1)
      } else {
        newAvailability = newAvailability.clamped(to: workDay)
        availableTimes.append(newAvailability)
      }
    }
    previousBusyTime = time
  }

  return availableTimes
}

func copy(_ string: String) {
  NSPasteboard.general.declareTypes([NSPasteboard.PasteboardType.string], owner: nil)
  NSPasteboard.general.setString(string, forType: NSPasteboard.PasteboardType.string)
}



Alfred log:

[13:52:22.059] Swift Test[Keyword] Processing complete
[13:52:22.060] Swift Test[Keyword] Passing output '' to Run Script
[13:52:22.070] ERROR: Swift Test[Run Script] zsh:27: parse error near `}'

 

3) Updating Calendar Avail workflow
Last option I tried to get to work is to combine the existing but dated Calendar Availability workflow with the amazing work of @deanishe 's Video Conference workflow and replacing the SQlite part (as it doesn't support recurring events) of retrieving the events by the more recent EventKit in swift (using CalendarEvents.scpt from the video conference workflow). However, I got stuck here too unfortunately as I don't know how to build the package/workflow.

4) iOS / MacOS Shortcut apps
Then, the last option was to look into different Shortcut apps (see one here) which I could then trigger from Alfred. However, this is not ideal as I prefer a script and most workflows support also only one calendar. 


5) Then there's a Non-alfred option / chrome extension:

Available: Calendar Availability Chrome Extension;  this one looks OK but rather not give permissions to non-established extensions and prefer Alfred.

So these are all the existing options I found but none of them are either recent or fulfill my needs. Is there anyone that could point me in the right direction for a working workflow? Hugely appreciated!

Requirements are quite simple:

- supporting recurring events seen as unavailable

- supporting multiple calendars (I have my work calendar, a shared family calendar and my personal calendar

- support 24h format, working hours and flexbility with all day events

- prefer an alfred workflow rather than another tool (calendly, keyboard meastro, etc) - only desktop support is ok for now

- paste my next available calendar slots to clipboard

Is there a solution or workflow available now to achieve this?

 

Does the workflow below need to be changed to work with macos Ventura and Python homebrew?

https://github.com/kmarchand/calendar-avail

 

Link to comment

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
×
×
  • Create New...