aboutsummaryrefslogtreecommitdiff
path: root/Sources
diff options
context:
space:
mode:
authorGrégoire Duchêne <gduchene@awhk.org>2025-11-02 19:27:32 +0000
committerGrégoire Duchêne <gduchene@awhk.org>2025-11-02 19:29:30 +0000
commitc7d428455c42e8039a6aedeb032b0685d7a47ebb (patch)
treef2b2b176fea7d019d578a873fc23af70f54f829a /Sources
parentaee698176a33879e7cb5b23e89157104c4b05c19 (diff)
First version of the CLI
It’s pretty basic, and it only works on macOS for now.
Diffstat (limited to 'Sources')
-rw-r--r--Sources/Main.swift30
-rw-r--r--Sources/SleepInhibitor.swift48
-rw-r--r--Sources/Watcher.swift58
3 files changed, 136 insertions, 0 deletions
diff --git a/Sources/Main.swift b/Sources/Main.swift
new file mode 100644
index 0000000..9ffff7e
--- /dev/null
+++ b/Sources/Main.swift
@@ -0,0 +1,30 @@
+import ArgumentParser
+import Foundation
+import Logging
+import ServiceLifecycle
+import SystemPackage
+
+@main
+struct EntryPoint: AsyncParsableCommand {
+ @Option(help: "Directory to search for ongoing downloads.", transform: { FilePath($0) })
+ var directory = FilePath("\(NSHomeDirectory())/Downloads")
+
+ @Option(help: "Suffix of ongoing downloads.")
+ var suffix = "part"
+
+ @Flag(help: "Enable verbose output.")
+ var verbose = false
+
+ func run() async throws {
+ var logger = Logger(label: "caffeinate-downloads")
+ if self.verbose {
+ logger.logLevel = .debug
+ }
+
+ try await ServiceGroup(
+ services: [Watcher(directory: self.directory, logger: logger, suffix: self.suffix)],
+ cancellationSignals: [.sigint, .sigquit],
+ logger: logger
+ ).run()
+ }
+}
diff --git a/Sources/SleepInhibitor.swift b/Sources/SleepInhibitor.swift
new file mode 100644
index 0000000..0cfe895
--- /dev/null
+++ b/Sources/SleepInhibitor.swift
@@ -0,0 +1,48 @@
+import IOKit.pwr_mgt
+
+actor SleepInhibitor {
+ var assertionID = IOPMAssertionID?.none
+ var isInhibitingSleep: Bool { self.assertionID != nil }
+
+ func create(name: String, details: String) throws {
+ guard self.assertionID == nil else {
+ return
+ }
+
+ var assertionID = IOPMAssertionID(0)
+ let ioReturn = IOPMAssertionCreateWithDescription(
+ kIOPMAssertionTypePreventUserIdleSystemSleep as CFString,
+ name as CFString,
+ details as CFString,
+ nil, nil, 0, nil,
+ &assertionID
+ )
+ guard ioReturn == kIOReturnSuccess else {
+ throw SleepInhibitorError(ioReturn: ioReturn)
+ }
+ self.assertionID = assertionID
+ }
+
+ func release() throws {
+ guard let assertionID = self.assertionID else {
+ return
+ }
+
+ let ioReturn = IOPMAssertionRelease(assertionID)
+ guard ioReturn == kIOReturnSuccess else {
+ throw SleepInhibitorError(ioReturn: ioReturn)
+ }
+ self.assertionID = nil
+ }
+}
+
+struct SleepInhibitorError: CustomStringConvertible, Error {
+ let ioReturn: IOReturn
+
+ var description: String {
+ guard let cString = mach_error_string(self.ioReturn) else {
+ return self.ioReturn.description
+ }
+ return String(cString: cString)
+ }
+}
diff --git a/Sources/Watcher.swift b/Sources/Watcher.swift
new file mode 100644
index 0000000..c7ca85a
--- /dev/null
+++ b/Sources/Watcher.swift
@@ -0,0 +1,58 @@
+import Logging
+import ServiceLifecycle
+import SystemPackage
+import _NIOFileSystem
+
+struct Watcher: Service {
+ let directory: FilePath
+ let logger: Logger
+ let suffix: String
+
+ func run() async {
+ let sleepInhibitor = SleepInhibitor()
+
+ while !Task.isCancelled {
+ let isDownloading: Bool
+ do {
+ isDownloading = try await FileSystem.shared.withDirectoryHandle(atPath: self.directory) {
+ try await $0.listContents().contains {
+ $0.name.extension?.hasSuffix(self.suffix) ?? false
+ }
+ }
+ } catch is CancellationError {
+ return
+ } catch {
+ self.logger.error("Failed to check directory: \(error)")
+ return
+ }
+
+ switch (isDownloading, await sleepInhibitor.isInhibitingSleep) {
+ case (true, false):
+ self.logger.debug("Ongoing downloads found, inhibiting sleep")
+ do {
+ try await sleepInhibitor.create(
+ name: "caffeinate-downloads",
+ details: "There are files being downloaded"
+ )
+ } catch {
+ self.logger.error("Failed to create assertion: \(error)")
+ }
+
+ case (false, true):
+ self.logger.debug("No ongoing downloads found, allowing sleep")
+ do {
+ try await sleepInhibitor.release()
+ } catch {
+ self.logger.error("Failed to release assertion: \(error)")
+ }
+
+ default:
+ break
+ }
+
+ guard (try? await Task.sleep(for: .seconds(30))) != nil else {
+ return
+ }
+ }
+ }
+}