Skip to content

Commit 5757be3

Browse files
committed
✨ Watch calendar for changes
1 parent 3c768a1 commit 5757be3

File tree

4 files changed

+127
-0
lines changed

4 files changed

+127
-0
lines changed

Sources/Cli/Watch.swift

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import ArgumentParser
2+
import Foundation
3+
4+
/// `plan watch`
5+
///
6+
/// Watch for calendar changes
7+
struct Watch: ParsableCommand {
8+
static var configuration = CommandConfiguration(
9+
abstract: "Watch for changes",
10+
shouldDisplay: false
11+
)
12+
13+
mutating func run() {
14+
let home = FileManager.default.homeDirectoryForCurrentUser
15+
let dir = home.path + "/Library/Calendars"
16+
let files = [
17+
dir + "/Calendar.sqlitedb",
18+
dir + "/Calendar.sqlitedb-wal",
19+
dir + "/Extras.db",
20+
dir + "/Extras.db-shm",
21+
dir + "/Extras.db-wal",
22+
]
23+
let sketchybar = Sketchybar(event: "calendar_changed")
24+
let watcher = FileWatcher(paths: files, callback: sketchybar.trigger)
25+
watcher.start()
26+
27+
dispatchMain()
28+
}
29+
}

Sources/IO/FileWatcher.swift

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import Foundation
2+
3+
class FileWatcher {
4+
private let minSeconds = 2
5+
private var lastChange = Int(Date().timeIntervalSince1970)
6+
7+
private var sources: [Int32: DispatchSourceFileSystemObject] = [:]
8+
private let queue = DispatchQueue.global()
9+
10+
private let paths: [String]
11+
private let callback: () -> Void
12+
13+
init(paths: [String], callback: @escaping () -> Void) {
14+
self.paths = paths
15+
self.callback = callback
16+
}
17+
18+
func start() {
19+
for path in paths {
20+
let isMonitoring = monitorFile(path: path)
21+
if !isMonitoring {
22+
StdErr.print("Not monitoring \(path)")
23+
}
24+
}
25+
}
26+
27+
private func monitorFile(path: String) -> Bool {
28+
let fileDescriptor = open(path, O_EVTONLY)
29+
guard fileDescriptor != -1 else { return false }
30+
31+
let source = DispatchSource.makeFileSystemObjectSource(fileDescriptor: fileDescriptor, eventMask: [.write, .delete, .rename, .extend, .attrib], queue: queue)
32+
33+
source.setEventHandler { [weak self] in
34+
guard let self = self else { return }
35+
let event = source.data
36+
37+
if event.contains(.delete) || event.contains(.rename) {
38+
self.stopMonitoring(fileDescriptor)
39+
} else if event.contains(.write) || event.contains(.extend) || event.contains(.attrib) {
40+
self.handleWriteEvent(at: path)
41+
}
42+
}
43+
44+
source.setCancelHandler {
45+
close(fileDescriptor)
46+
}
47+
48+
source.resume()
49+
sources[fileDescriptor] = source
50+
51+
return true
52+
}
53+
54+
private func stopMonitoring(_ fd: Int32) {
55+
sources[fd]?.cancel()
56+
sources.removeValue(forKey: fd)
57+
}
58+
59+
private func handleWriteEvent(at _: String) {
60+
let now = Int(Date().timeIntervalSince1970)
61+
if now - lastChange < minSeconds {
62+
} else {
63+
lastChange = now
64+
callback()
65+
}
66+
}
67+
68+
deinit {
69+
for (fd, source) in sources {
70+
source.cancel()
71+
close(fd)
72+
}
73+
}
74+
}

Sources/IO/Sketchybar.swift

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import Foundation
2+
3+
class Sketchybar {
4+
private let event: String
5+
6+
init(event: String) {
7+
self.event = event
8+
}
9+
10+
func trigger() {
11+
let task = Process()
12+
task.executableURL = URL(fileURLWithPath: "/opt/homebrew/bin/sketchybar")
13+
task.arguments = ["--trigger", event]
14+
15+
do {
16+
try task.run()
17+
task.waitUntilExit()
18+
print("Fired sketchybar event \(event)")
19+
} catch {
20+
print("Failed to execute command: \(error)")
21+
}
22+
}
23+
}

Sources/Plan.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ struct Plan: ParsableCommand {
1212
Next.self,
1313
Today.self,
1414
Usage.self,
15+
Watch.self,
1516
],
1617
defaultSubcommand: Usage.self
1718
)

0 commit comments

Comments
 (0)