macOS: add --priority flag for time-sensitive notifications
Add NotificationPriority enum with passive/active/timeSensitive levels that map to UNNotificationInterruptionLevel. timeSensitive breaks through Focus modes for urgent notifications. Usage: clawdis-mac notify --title X --body Y --priority timeSensitivemain
parent
8ca240fb2c
commit
c86cb4e9a5
|
|
@ -14,9 +14,9 @@ enum ControlRequestHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
switch request {
|
switch request {
|
||||||
case let .notify(title, body, sound):
|
case let .notify(title, body, sound, priority):
|
||||||
let chosenSound = sound?.trimmingCharacters(in: .whitespacesAndNewlines)
|
let chosenSound = sound?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
let ok = await notifier.send(title: title, body: body, sound: chosenSound)
|
let ok = await notifier.send(title: title, body: body, sound: chosenSound, priority: priority)
|
||||||
return ok ? Response(ok: true) : Response(ok: false, message: "notification not authorized")
|
return ok ? Response(ok: true) : Response(ok: false, message: "notification not authorized")
|
||||||
|
|
||||||
case let .ensurePermissions(caps, interactive):
|
case let .ensurePermissions(caps, interactive):
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
|
import ClawdisIPC
|
||||||
import Foundation
|
import Foundation
|
||||||
import UserNotifications
|
import UserNotifications
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
struct NotificationManager {
|
struct NotificationManager {
|
||||||
func send(title: String, body: String, sound: String?) async -> Bool {
|
func send(title: String, body: String, sound: String?, priority: NotificationPriority? = nil) async -> Bool {
|
||||||
let center = UNUserNotificationCenter.current()
|
let center = UNUserNotificationCenter.current()
|
||||||
let status = await center.notificationSettings()
|
let status = await center.notificationSettings()
|
||||||
if status.authorizationStatus == .notDetermined {
|
if status.authorizationStatus == .notDetermined {
|
||||||
|
|
@ -20,6 +21,18 @@ struct NotificationManager {
|
||||||
content.sound = UNNotificationSound(named: UNNotificationSoundName(soundName))
|
content.sound = UNNotificationSound(named: UNNotificationSoundName(soundName))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set interruption level based on priority
|
||||||
|
if let priority {
|
||||||
|
switch priority {
|
||||||
|
case .passive:
|
||||||
|
content.interruptionLevel = .passive
|
||||||
|
case .active:
|
||||||
|
content.interruptionLevel = .active
|
||||||
|
case .timeSensitive:
|
||||||
|
content.interruptionLevel = .timeSensitive
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let req = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
|
let req = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
|
||||||
do {
|
do {
|
||||||
try await center.add(req)
|
try await center.add(req)
|
||||||
|
|
|
||||||
|
|
@ -54,17 +54,20 @@ struct ClawdisCLI {
|
||||||
var title: String?
|
var title: String?
|
||||||
var body: String?
|
var body: String?
|
||||||
var sound: String?
|
var sound: String?
|
||||||
|
var priority: NotificationPriority?
|
||||||
while !args.isEmpty {
|
while !args.isEmpty {
|
||||||
let arg = args.removeFirst()
|
let arg = args.removeFirst()
|
||||||
switch arg {
|
switch arg {
|
||||||
case "--title": title = args.popFirst()
|
case "--title": title = args.popFirst()
|
||||||
case "--body": body = args.popFirst()
|
case "--body": body = args.popFirst()
|
||||||
case "--sound": sound = args.popFirst()
|
case "--sound": sound = args.popFirst()
|
||||||
|
case "--priority":
|
||||||
|
if let val = args.popFirst(), let p = NotificationPriority(rawValue: val) { priority = p }
|
||||||
default: break
|
default: break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
guard let t = title, let b = body else { throw CLIError.help }
|
guard let t = title, let b = body else { throw CLIError.help }
|
||||||
return .notify(title: t, body: b, sound: sound)
|
return .notify(title: t, body: b, sound: sound, priority: priority)
|
||||||
|
|
||||||
case "ensure-permissions":
|
case "ensure-permissions":
|
||||||
var caps: [Capability] = []
|
var caps: [Capability] = []
|
||||||
|
|
@ -169,7 +172,7 @@ struct ClawdisCLI {
|
||||||
clawdis-mac — talk to the running Clawdis.app XPC service
|
clawdis-mac — talk to the running Clawdis.app XPC service
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
clawdis-mac notify --title <t> --body <b> [--sound <name>]
|
clawdis-mac notify --title <t> --body <b> [--sound <name>] [--priority <passive|active|timeSensitive>]
|
||||||
clawdis-mac ensure-permissions
|
clawdis-mac ensure-permissions
|
||||||
[--cap <notifications|accessibility|screenRecording|microphone|speechRecognition>]
|
[--cap <notifications|accessibility|screenRecording|microphone|speechRecognition>]
|
||||||
[--interactive]
|
[--interactive]
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,15 @@ public enum Capability: String, Codable, CaseIterable, Sendable {
|
||||||
|
|
||||||
// MARK: - Requests
|
// MARK: - Requests
|
||||||
|
|
||||||
|
/// Notification interruption level (maps to UNNotificationInterruptionLevel)
|
||||||
|
public enum NotificationPriority: String, Codable, Sendable {
|
||||||
|
case passive // silent, no wake
|
||||||
|
case active // default
|
||||||
|
case timeSensitive // breaks through Focus modes
|
||||||
|
}
|
||||||
|
|
||||||
public enum Request: Sendable {
|
public enum Request: Sendable {
|
||||||
case notify(title: String, body: String, sound: String?)
|
case notify(title: String, body: String, sound: String?, priority: NotificationPriority?)
|
||||||
case ensurePermissions([Capability], interactive: Bool)
|
case ensurePermissions([Capability], interactive: Bool)
|
||||||
case screenshot(displayID: UInt32?, windowID: UInt32?, format: String)
|
case screenshot(displayID: UInt32?, windowID: UInt32?, format: String)
|
||||||
case runShell(
|
case runShell(
|
||||||
|
|
@ -49,7 +56,7 @@ public struct Response: Codable, Sendable {
|
||||||
extension Request: Codable {
|
extension Request: Codable {
|
||||||
private enum CodingKeys: String, CodingKey {
|
private enum CodingKeys: String, CodingKey {
|
||||||
case type
|
case type
|
||||||
case title, body, sound
|
case title, body, sound, priority
|
||||||
case caps, interactive
|
case caps, interactive
|
||||||
case displayID, windowID, format
|
case displayID, windowID, format
|
||||||
case command, cwd, env, timeoutSec, needsScreenRecording
|
case command, cwd, env, timeoutSec, needsScreenRecording
|
||||||
|
|
@ -70,11 +77,12 @@ extension Request: Codable {
|
||||||
public func encode(to encoder: Encoder) throws {
|
public func encode(to encoder: Encoder) throws {
|
||||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
switch self {
|
switch self {
|
||||||
case let .notify(title, body, sound):
|
case let .notify(title, body, sound, priority):
|
||||||
try container.encode(Kind.notify, forKey: .type)
|
try container.encode(Kind.notify, forKey: .type)
|
||||||
try container.encode(title, forKey: .title)
|
try container.encode(title, forKey: .title)
|
||||||
try container.encode(body, forKey: .body)
|
try container.encode(body, forKey: .body)
|
||||||
try container.encodeIfPresent(sound, forKey: .sound)
|
try container.encodeIfPresent(sound, forKey: .sound)
|
||||||
|
try container.encodeIfPresent(priority, forKey: .priority)
|
||||||
|
|
||||||
case let .ensurePermissions(caps, interactive):
|
case let .ensurePermissions(caps, interactive):
|
||||||
try container.encode(Kind.ensurePermissions, forKey: .type)
|
try container.encode(Kind.ensurePermissions, forKey: .type)
|
||||||
|
|
@ -119,7 +127,8 @@ extension Request: Codable {
|
||||||
let title = try container.decode(String.self, forKey: .title)
|
let title = try container.decode(String.self, forKey: .title)
|
||||||
let body = try container.decode(String.self, forKey: .body)
|
let body = try container.decode(String.self, forKey: .body)
|
||||||
let sound = try container.decodeIfPresent(String.self, forKey: .sound)
|
let sound = try container.decodeIfPresent(String.self, forKey: .sound)
|
||||||
self = .notify(title: title, body: body, sound: sound)
|
let priority = try container.decodeIfPresent(NotificationPriority.self, forKey: .priority)
|
||||||
|
self = .notify(title: title, body: body, sound: sound, priority: priority)
|
||||||
|
|
||||||
case .ensurePermissions:
|
case .ensurePermissions:
|
||||||
let caps = try container.decode([Capability].self, forKey: .caps)
|
let caps = try container.decode([Capability].self, forKey: .caps)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue