[sketchybar] meetingbar-esque plugin
diff --git a/.config/sketchybar/meetingbar.swift b/.config/sketchybar/meetingbar.swift
new file mode 100755
index 00000000..717c184a
--- /dev/null
+++ b/.config/sketchybar/meetingbar.swift
@@ -0,0 +1,180 @@
+#!/usr/bin/swift
+
+/* Usage:
+
+In ~/.config/sketchybar/sketchybarrc:
+
+ --add item meetingbar right \
+ --set meetingbar \
+ icon= \
+ icon.padding_right=8 \
+ label.y_offset=2 \
+ update_freq=120 \
+ script="$PLUGIN_DIR/meetingbar.sh"
+
+In ~/.config/sketchybar/plugins/meetingbar.sh:
+
+ CALENDAR="alice@example.com"
+ MEETING=$("$CONFIG_DIR/meetingbar.swift" "$CALENDAR")
+ sketchybar --set "$NAME" label="$MEETING"
+
+*/
+
+import Foundation
+import EventKit
+
+struct MeetingBarError: Error {
+ let message: String
+}
+
+class MeetingBar {
+ let calendarTitle: String
+ let eventStore: EKEventStore
+
+ init(calendarTitle: String, eventStore: EKEventStore) {
+ self.calendarTitle = calendarTitle
+ self.eventStore = eventStore
+ }
+
+ convenience init(calendarTitle: String) throws {
+ let eventStore = EKEventStore()
+ let status = EKEventStore.authorizationStatus(for: .event)
+
+ switch status {
+ case .authorized, .fullAccess, .writeOnly:
+ self.init(calendarTitle: calendarTitle, eventStore: eventStore)
+ case .denied:
+ throw MeetingBarError(message: "Calendar access denied")
+ case .restricted:
+ throw MeetingBarError(message: "Calendar access restricted")
+ case .notDetermined:
+ let semaphore = DispatchSemaphore(value: 0)
+ var accessGranted = false
+
+ eventStore.requestFullAccessToEvents { granted, error in
+ accessGranted = granted
+ semaphore.signal()
+ }
+
+ semaphore.wait()
+
+ if accessGranted {
+ self.init(calendarTitle: calendarTitle, eventStore: eventStore)
+ } else {
+ throw MeetingBarError(message: "Calendar access denied")
+ }
+ @unknown default:
+ throw MeetingBarError(message: "Unknown authorization status")
+ }
+ }
+
+ // Computed property to get the next meeting
+ var nextMeeting: EKEvent? {
+ get throws {
+ let currentDate = Date()
+
+ // Get all calendars and filter for the specified calendar title
+ let calendars = eventStore.calendars(for: .event).filter { $0.title.contains(self.calendarTitle) }
+
+ guard !calendars.isEmpty else {
+ throw MeetingBarError(message: "No calendar found for '\(self.calendarTitle)'")
+ }
+
+ // Create a predicate for events starting from now until the end of the day
+ let startDate = currentDate
+
+ // Create a date for the end of tomorrow
+ let calendar = Calendar.current
+ guard let tomorrow = calendar.date(byAdding: .day, value: 1, to: currentDate),
+ let endOfTomorrow = calendar.date(bySettingHour: 0, minute: 0, second: 0, of: tomorrow) else {
+ throw MeetingBarError(message: "Failed to calculate date")
+ }
+
+ let endDate = endOfTomorrow
+
+ let predicate = eventStore.predicateForEvents(withStart: startDate, end: endDate, calendars: calendars)
+ let events = eventStore.events(matching: predicate).filter { !$0.isAllDay }
+
+ let currentMeeting = events.first(where: { currentDate >= $0.startDate && currentDate <= $0.endDate })
+ let nextMeeting = events.first(where: { $0.startDate > currentDate })
+
+ // No current meeting, show next meeting if available
+ guard let current = currentMeeting else {
+ return nextMeeting
+ }
+
+ // No next meeting, show current meeting
+ guard let next = nextMeeting else {
+ return current
+ }
+
+ // Decide which meeting to show based on both meetings
+ let duration = current.endDate.timeIntervalSince(current.startDate)
+ let timeUntilNext = next.startDate.timeIntervalSince(currentDate)
+
+ // The longer the current meeting, the more we prioritize showing the next one
+ // Threshold increases with meeting duration: 5 min base + 10 min per hour of meeting
+ let threshold = 5 * 60 + (duration / 3600) * 10 * 60
+
+ if timeUntilNext < threshold {
+ return next
+ }
+
+ return current
+ }
+ }
+}
+
+// Get calendar title from command line arguments
+guard CommandLine.arguments.count > 1 else {
+ print("Error: Calendar title argument is required")
+ print("Usage: \(CommandLine.arguments[0]) <calendar-title>")
+ exit(1)
+}
+
+let calendarTitle = CommandLine.arguments[1]
+
+func formatTimeUntil(_ date: Date, from currentDate: Date) -> String {
+ let minutes = Int(date.timeIntervalSince(currentDate) / 60)
+ if minutes <= 0 {
+ return "now"
+ } else if minutes < 60 {
+ if minutes == 1 {
+ return "in 1 minute"
+ } else {
+ return "in \(minutes) minutes"
+ }
+ } else {
+ let hours = minutes / 60
+ let remainingMinutes = minutes % 60
+ if remainingMinutes == 0 {
+ return "in \(hours)h"
+ } else {
+ return "in \(hours)h\(remainingMinutes)m"
+ }
+ }
+}
+
+do {
+ let manager = try MeetingBar(calendarTitle: calendarTitle)
+ if let event = try manager.nextMeeting {
+ let currentDate = Date()
+ let isCurrentMeeting = currentDate >= event.startDate && currentDate <= event.endDate
+
+ if isCurrentMeeting {
+ let timeStr = formatTimeUntil(event.endDate, from: currentDate)
+ print("\(event.title ?? "Untitled Event") ends \(timeStr)")
+ } else {
+ let timeStr = formatTimeUntil(event.startDate, from: currentDate)
+ print("\(event.title ?? "Untitled Event") \(timeStr)")
+ }
+ } else {
+ print("No meetings")
+ }
+} catch let error as MeetingBarError {
+ print("Error: \(error.message)")
+ exit(1)
+} catch {
+ print("Error: \(error)")
+ exit(1)
+}
diff --git a/.config/sketchybar/plugins/meetingbar.sh b/.config/sketchybar/plugins/meetingbar.sh
new file mode 100755
index 00000000..3314de2c
--- /dev/null
+++ b/.config/sketchybar/plugins/meetingbar.sh
@@ -0,0 +1,6 @@
+#!/bin/sh
+
+CALENDAR=
+MEETING=$("$CONFIG_DIR/meetingbar.swift" "$CALENDAR")
+
+sketchybar --set "$NAME" label="$MEETING"
diff --git a/.config/sketchybar/sketchybarrc b/.config/sketchybar/sketchybarrc
index 9b338982..5f95922b 100755
--- a/.config/sketchybar/sketchybarrc
+++ b/.config/sketchybar/sketchybarrc
@@ -84,7 +84,7 @@ sketchybar --add item front_app left \
sketchybar --add item datetime right \
--subscribe datetime system_woke \
- --set datetime update_freq=30 icon= script="$PLUGIN_DIR/datetime.sh" \
+ --set datetime update_freq=30 script="$PLUGIN_DIR/datetime.sh" \
--add item wifi right \
--subscribe wifi wifi_change \
--set wifi \
@@ -110,6 +110,13 @@ sketchybar --add item datetime right \
click_script="sketchybar -m --set \$NAME label.drawing=toggle" \
script="$PLUGIN_DIR/battery.sh" \
--subscribe battery system_woke power_source_change
+ # --add item meetingbar right \
+ # --set meetingbar \
+ # icon= \
+ # icon.padding_right=8 \
+ # label.y_offset=2 \
+ # update_freq=120 \
+ # script="$PLUGIN_DIR/meetingbar.sh"
##### Force all scripts to run the first time (never do this in a script) #####
sketchybar --update