diff --git a/Package.resolved b/Package.resolved index e22e80e..9ed5f0f 100644 --- a/Package.resolved +++ b/Package.resolved @@ -9,6 +9,15 @@ "version" : "2.3.0" } }, + { + "identity" : "simplycoreaudio", + "kind" : "remoteSourceControl", + "location" : "https://github.com/rnine/SimplyCoreAudio.git", + "state" : { + "revision" : "35cc0e6eac5c2ee5049431f4238b0e333cf79869", + "version" : "4.1.1" + } + }, { "identity" : "swift-argument-parser", "kind" : "remoteSourceControl", @@ -18,6 +27,15 @@ "version" : "1.2.3" } }, + { + "identity" : "swift-atomics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-atomics.git", + "state" : { + "revision" : "cd142fd2f64be2100422d658e7411e39489da985", + "version" : "1.2.0" + } + }, { "identity" : "yams", "kind" : "remoteSourceControl", diff --git a/Package.swift b/Package.swift index 6db686f..eafe4fc 100644 --- a/Package.swift +++ b/Package.swift @@ -11,7 +11,8 @@ let package = Package( dependencies: [ .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.2.0"), .package(url: "https://github.com/JohnSundell/ShellOut.git", from: "2.0.0"), - .package(url: "https://github.com/jpsim/Yams.git", from: "5.0.6") + .package(url: "https://github.com/jpsim/Yams.git", from: "5.0.6"), + .package(url: "https://github.com/rnine/SimplyCoreAudio.git", from: "4.1.1"), ], targets: [ .executableTarget( @@ -19,7 +20,8 @@ let package = Package( dependencies: [ .product(name: "ArgumentParser", package: "swift-argument-parser"), .product(name: "ShellOut", package: "ShellOut"), - .product(name: "Yams", package: "Yams") + .product(name: "Yams", package: "Yams"), + .product(name: "SimplyCoreAudio", package: "SimplyCoreAudio") ] ) ] diff --git a/README.md b/README.md index 1c3c534..17ef870 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,5 @@ If you want to run a command on an event, regardless of the input (`with`), then The following event sources can be subscribed to: -* `screen` — change connected displays. declares `connected` and `disconnected` events. In `with` takes the display name. - - +* `screen` — connected displays list change. declares `connected` and `disconnected` events. In `with` takes the display name. +* `audio` — connected audio device list change. declares `connected` and `disconnected` events. In `with` takes the audio device name. Handles both input and output devices. diff --git a/Sources/Events/AudioSource.swift b/Sources/Events/AudioSource.swift new file mode 100644 index 0000000..db0e2b2 --- /dev/null +++ b/Sources/Events/AudioSource.swift @@ -0,0 +1,40 @@ +import Cocoa +import SimplyCoreAudio + +class AudioSource: EventSource { + let coreAudio = SimplyCoreAudio() + var name = "audio" + var listener: EventListener? + var lastScreens: [String]? + var updating = false + + func emitAll(kind: String, devices: [AudioDevice]) { + var names: [String] = [] + for device in devices where !names.contains(device.name) { + emit(kind: kind, target: device.name) + names.append(device.name) + } + } + + @objc + func handleDeviceListChanged(notification: Notification) { + let addedDevices = notification.userInfo?["addedDevices"] as? [AudioDevice] + if addedDevices != nil && !addedDevices!.isEmpty { + emitAll(kind: "connected", devices: addedDevices!) + } + + let removedDevices = notification.userInfo?["removedDevices"] as? [AudioDevice] + if removedDevices != nil && !removedDevices!.isEmpty { + emitAll(kind: "disconnected", devices: removedDevices!) + } + } + + func subscribe() { + NotificationCenter.default.addObserver( + self, + selector: #selector(self.handleDeviceListChanged), + name: Notification.Name.deviceListChanged, + object: nil + ) + } +} diff --git a/Sources/Events/Event.swift b/Sources/Events/Event.swift index 9f1b972..3554a0d 100644 --- a/Sources/Events/Event.swift +++ b/Sources/Events/Event.swift @@ -15,14 +15,10 @@ protocol EventProvider { protocol EventSource: EventProvider { var name: String { get } - func handle() + func subscribe() } extension EventSource { - func handle() { - // Do nothing by default - } - func emit(kind: String, target: String) { if listener == nil { return diff --git a/Sources/Events/EventObserver.swift b/Sources/Events/EventObserver.swift index 6fe4964..f50b4de 100644 --- a/Sources/Events/EventObserver.swift +++ b/Sources/Events/EventObserver.swift @@ -6,8 +6,8 @@ class EventObserver: EventListener, EventProvider { var sources: [EventSource] = [] init(sources: [EventSource]) { - for var provider in sources { - provider.listener = self + for var source in sources { + source.listener = self } self.sources = sources } @@ -19,20 +19,19 @@ class EventObserver: EventListener, EventProvider { } } + func subscribeSources() { + for source in sources { + source.subscribe() + } + } + func runLoop() { + subscribeSources() _ = NSApplication.shared let ticket = Timer.publish(every: 5, on: .main, in: .common) .autoconnect() - .sink { _ in - self.run() - } + .sink { _ in } RunLoop.main.run() ticket.cancel() } - - func run() { - for provider in sources { - provider.handle() - } - } } diff --git a/Sources/Events/ScreenSource.swift b/Sources/Events/ScreenSource.swift index 2a39fc1..f18af4a 100644 --- a/Sources/Events/ScreenSource.swift +++ b/Sources/Events/ScreenSource.swift @@ -7,23 +7,19 @@ class ScreenSource: EventSource { var lastScreens: [String]? var updating = false - init() { - subscribeChange() - refreshScreens() - } - @objc func handleDisplayConnection(notification _: Notification) { refreshScreens() } - func subscribeChange() { + func subscribe() { NotificationCenter.default.addObserver( self, selector: #selector(handleDisplayConnection), name: NSApplication.didChangeScreenParametersNotification, object: nil ) + refreshScreens() } func getScreenNames() -> [String] { diff --git a/Sources/RunIf.swift b/Sources/RunIf.swift index 5156102..0cd4b69 100644 --- a/Sources/RunIf.swift +++ b/Sources/RunIf.swift @@ -15,17 +15,21 @@ struct RunIf: ParsableCommand { var configPath: String? mutating func run() throws { + let sources: [EventSource] = [ + ScreenSource(), + AudioSource() + ] let config = ConfigLoader.read(handlersOf: configPath) if config == nil { throw ValidationError("Can't open config file.") } + let activeSources = sources.filter { source in + config!.keys.contains(source.name) + } let runner = CommandRunner(with: config!) - let sources: [EventSource] = [ - ScreenSource() - ] - let observer = EventObserver(sources: sources) + let observer = EventObserver(sources: activeSources) observer.listener = runner - print("starting with \(sources.count) event sources") + print("starting with \(activeSources.count) event sources") observer.runLoop() } }