diff options
| author | Grégoire Duchêne <gduchene@awhk.org> | 2025-11-02 19:27:32 +0000 |
|---|---|---|
| committer | Grégoire Duchêne <gduchene@awhk.org> | 2025-11-02 19:29:30 +0000 |
| commit | c7d428455c42e8039a6aedeb032b0685d7a47ebb (patch) | |
| tree | f2b2b176fea7d019d578a873fc23af70f54f829a /Sources | |
| parent | aee698176a33879e7cb5b23e89157104c4b05c19 (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.swift | 30 | ||||
| -rw-r--r-- | Sources/SleepInhibitor.swift | 48 | ||||
| -rw-r--r-- | Sources/Watcher.swift | 58 |
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 + } + } + } +} |
