Skip to content

Commit 20c7b49

Browse files
authored
Merge pull request #21 from akuligowski9/notifications-and-export
Add system notifications and session log export
2 parents b43578d + 8a586d0 commit 20c7b49

1 file changed

Lines changed: 46 additions & 0 deletions

File tree

macos-app/Sources/KestrelBar/AppDelegate.swift

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import AppKit
22
import KestrelBarLib
33
import ServiceManagement
4+
import UserNotifications
45

56
class AppDelegate: NSObject, NSApplicationDelegate {
67
private var statusItem: NSStatusItem!
@@ -58,6 +59,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
5859
updateIcon()
5960
buildMenu()
6061
startCore()
62+
UNUserNotificationCenter.current().requestAuthorization(options: [.alert]) { _, _ in }
6163
}
6264

6365
func applicationWillTerminate(_ notification: Notification) {
@@ -318,6 +320,11 @@ class AppDelegate: NSObject, NSApplicationDelegate {
318320
settingsItem.submenu = settingsSub
319321
menu.addItem(settingsItem)
320322

323+
// --- Export ---
324+
let exportItem = NSMenuItem(title: "Export Session Log...", action: #selector(exportLog), keyEquivalent: "e")
325+
exportItem.target = self
326+
menu.addItem(exportItem)
327+
321328
menu.addItem(NSMenuItem.separator())
322329
menu.addItem(NSMenuItem(title: "Quit Kestrel", action: #selector(quit), keyEquivalent: "q"))
323330

@@ -608,6 +615,42 @@ class AppDelegate: NSObject, NSApplicationDelegate {
608615
}
609616
}
610617

618+
private func sendNotification(sensor: String, from: String, to: String) {
619+
let label = sensorInfo[sensor]?.label ?? sensor
620+
let content = UNMutableNotificationContent()
621+
content.title = "Kestrel: \(label) \(to)"
622+
content.body = "\(label) changed from \(from) to \(to)."
623+
624+
let request = UNNotificationRequest(
625+
identifier: "kestrel-\(sensor)-\(Date().timeIntervalSince1970)",
626+
content: content,
627+
trigger: nil
628+
)
629+
UNUserNotificationCenter.current().add(request)
630+
}
631+
632+
@objc private func exportLog() {
633+
let panel = NSSavePanel()
634+
panel.nameFieldStringValue = "kestrel-session.jsonl"
635+
panel.allowedContentTypes = [.json]
636+
637+
guard panel.runModal() == .OK, let url = panel.url else { return }
638+
639+
let lines = sensorStates.compactMap { sensor, state -> String? in
640+
guard let data = try? JSONSerialization.data(withJSONObject: [
641+
"sensor": sensor,
642+
"value": state.value,
643+
"state": state.state,
644+
"valid": state.valid,
645+
"exported": ISO8601DateFormatter().string(from: Date()),
646+
]) else { return nil }
647+
return String(data: data, encoding: .utf8)
648+
}
649+
650+
let output = lines.joined(separator: "\n") + "\n"
651+
try? output.write(to: url, atomically: true, encoding: .utf8)
652+
}
653+
611654
// MARK: - Icon
612655

613656
private func updateIcon() {
@@ -831,6 +874,9 @@ class AppDelegate: NSObject, NSApplicationDelegate {
831874
display.state = to
832875
sensorStates[sensor] = display
833876

877+
// Notify on state changes
878+
sendNotification(sensor: sensor, from: from, to: to)
879+
834880
// Track resolution timing
835881
if to != "OK" && degradedSince[sensor] == nil {
836882
degradedSince[sensor] = Date()

0 commit comments

Comments
 (0)