Initial commit: KPopBird - macOS sleep prevention menu bar app
Swift macOS menu bar app with Samjokoo (삼족오) icon. Features: display/system sleep prevention, timer mode, battery awareness, auto-start on login. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
.build/
|
||||||
|
.swiftpm/
|
||||||
|
*.xcodeproj/
|
||||||
|
*.xcworkspace/
|
||||||
|
DerivedData/
|
||||||
|
.DS_Store
|
||||||
13
Package.swift
Normal file
13
Package.swift
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
// swift-tools-version:5.9
|
||||||
|
import PackageDescription
|
||||||
|
|
||||||
|
let package = Package(
|
||||||
|
name: "KPopBird",
|
||||||
|
platforms: [.macOS(.v13)],
|
||||||
|
targets: [
|
||||||
|
.executableTarget(
|
||||||
|
name: "KPopBird",
|
||||||
|
path: "Sources/KPopBird"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
BIN
Resources/samjokoo.pdf
Normal file
BIN
Resources/samjokoo.pdf
Normal file
Binary file not shown.
54
Resources/samjokoo.svg
Normal file
54
Resources/samjokoo.svg
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 12.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 51448) -->
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
|
||||||
|
<!ENTITY ns_svg "http://www.w3.org/2000/svg">
|
||||||
|
<!ENTITY ns_xlink "http://www.w3.org/1999/xlink">
|
||||||
|
]>
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="&ns_svg;" xmlns:xlink="&ns_xlink;" width="500" height="460" viewBox="0 0 500 460"
|
||||||
|
style="overflow:visible;enable-background:new 0 0 500 460;" xml:space="preserve">
|
||||||
|
<style type="text/css">
|
||||||
|
<![CDATA[
|
||||||
|
.st0{fill-rule:evenodd;clip-rule:evenodd;}
|
||||||
|
.st1{fill:none;}
|
||||||
|
.st2{fill-rule:evenodd;clip-rule:evenodd;fill:#000000;}
|
||||||
|
]]>
|
||||||
|
</style>
|
||||||
|
<path class="st2" d="M259.671,318c4.255,0.712-5.295,8.543-6,11c-0.86,3,1.269,9.854,0,16c-1.105,5.353-6.372,9.959-7,13
|
||||||
|
c-1.013,4.899,2.025,8.452,0,10c-3.141,2.398-15.412-1.614-21-1c-7.167,0.786-3.856,0.022-3-7c0.66-5.416-0.699-12.623,0-15
|
||||||
|
c0.712-2.421,3.79-3.116,6-6c1.736-2.266,1.688-6.518,4-8c1.484-0.952,8.828-1.187,11-2
|
||||||
|
C250.568,326.415,256.254,317.428,259.671,318z"/>
|
||||||
|
<path class="st2" d="M278.671,319c5.49,8.939-6.158,27.4-6,35c0.087,4.2,7.173,10.737,5,14c-1.836,2.755-11.129,0.585-14,0
|
||||||
|
c-4.852-0.989-9.148,1.092-10,0c-0.469-0.601-0.556-9.62,0-12c0.376-1.611,5.021-6.449,7-12c1.798-5.045,1.667-13.379,3-15
|
||||||
|
c1.109-1.351,5.688-1.419,7-2C274.416,325.34,276.528,323.492,278.671,319z"/>
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<path class="st0" d="M388.671,254c-13.233,23.05-35.762,36.475-66,45c-3.237,0.912-21.35,4.236-22,5
|
||||||
|
c-1.313,1.541,8.384,12.646,10,15c7.567,11.021,11.606,18.713,25,23c-16.342,4.211-32.363-5.416-41-16
|
||||||
|
c-5.62-6.889-3.176,9.273-5,15c-1.956,6.14-7.55,12.092-8,15c-0.386,2.497,0.337,10.319,1,11c1.747,1.792,8.929-2.188,14-2
|
||||||
|
c-0.786-0.029,11.557,3.098,13,4c0.119,0.074,2.768,2.428,3,2c-1.574,2.902-21.896,1.586-31,2c-3.587,0.162-13.943,2.136-17,2
|
||||||
|
c-7.656-0.342-16.387-3.266-27-4c0.233,0.016-0.422,1.577-1,1c-0.117-0.117,0.098-1.997,0-2c-2.463-0.085-5.22,0.941-8,1
|
||||||
|
c-8.309,0.173-16.016-1.163-25-1c0.881-4.443,6.881,0.122,10-2c4.725-3.215,1.534-16.363,3-24c0.733-3.822,7.005-8.967,5-12
|
||||||
|
c-0.735-1.112-6.617,1.871-7,2c-13.336,4.465-30.386,5.691-50,5c26.413-7.141,41.822-13.103,66-23c8.53-3.492-2.806-1.999-10-3
|
||||||
|
c-31.338-4.362-55.012-19.218-72-35c-18.942-17.6-31.463-36.518-41-67c-0.707-2.26-4.535-10.465-3-12c0.675-0.675,7.951,7.794,4,3
|
||||||
|
c29.772,36.127,57.773,68.089,112,81c12.193,2.902-9.467-7.307-12-9c-11.936-7.981-15.776-14.186-26-25
|
||||||
|
c-4.771-5.047-19.127-18.604-10-14c5.037,2.54,17.975,14.539,23,18c4.363,3.003,8.252,6.297,14,9c1.941,0.912,16.153,6.847,18,5
|
||||||
|
c1.111-1.112-5.383-6.456-7-8c-15.717-15.011-34.139-34.362-39-59c-2.125-10.773-1.536-19.645-4-29c-1.638-6.217-5.078-13.53,1-6
|
||||||
|
c0.567,0.703,3.876,6.124,4,6c2.512-2.512,0.116-11.912,0-13c-0.685-6.424-6.729-16.324,1-10c12.732,10.417-4.997-27.458,5-20
|
||||||
|
c18.703,13.953,15.191,48.101,29,66c4.547,5.894,10.551,10.367,16,15c8.498,7.226,17.526,12.205,22,23c0.718,1.73,1.123,6.551,2,7
|
||||||
|
c5.688,2.908,16.494-21.346,17-25c2.617-18.903-9.566-39.435-6-56c1.383-6.425,14.401-19.07,9-27c-4.435-6.511-47.828-1.407-56-3
|
||||||
|
c-7.658-1.492-17.97-10.516-11-21c2.648-3.982,16.5-10.718,18-4c1.404,6.292-13.102,6.874-14,10c-1.702,5.921,3.389,8.073,10,9
|
||||||
|
c17.853,2.503,69.597-16.014,66,13c-0.061,0.486-1.071,6.929-2,6c2.206,2.206,10.995-0.438,15,0c5.598,0.613,14.604,4.784,20,7
|
||||||
|
c10.687,4.389,25.524,6.003,11,10c-10.661,2.934-26.078,5.874-33,11c-9.344,6.919-7.355,9.393-5,21c1.554,7.653,2.588,15.333,3,24
|
||||||
|
c0.705,14.833-6.435,35.15,9,37c5.476,0.656,13.352-2.878,17-6c2.812-2.408,3.729-8.663,6-10c3.612-2.128,13.58-1.155,19-3
|
||||||
|
c3.53-1.202,10.804-4.669,13-7c1.78-1.889,3.782-6.748,4-1c0.02,0.511-1.95,2.752-1,4c1.658,2.177,12.366-13.502,15-20
|
||||||
|
c1.521-3.75,3.845-14.247,6-22c0.625-2.249,2.561-11.265,4-3c1.273,7.308,0.767,11.693,1,17c0.571,12.967-1.814,39.675-7,51
|
||||||
|
C382.968,256.904,383.73,256.902,388.671,254z M243.671,329c-2.172,0.813-9.516,1.048-11,2c-2.312,1.482-2.264,5.734-4,8
|
||||||
|
c-2.21,2.884-5.288,3.579-6,6c-0.699,2.377,0.66,9.584,0,15c-0.856,7.022-4.167,7.786,3,7c5.588-0.614,17.859,3.398,21,1
|
||||||
|
c2.025-1.548-1.013-5.101,0-10c0.628-3.041,5.895-7.647,7-13c1.269-6.146-0.86-13,0-16c0.705-2.457,10.255-10.288,6-11
|
||||||
|
C256.254,317.428,250.568,326.415,243.671,329z M270.671,327c-1.312,0.581-5.891,0.649-7,2c-1.333,1.621-1.202,9.955-3,15
|
||||||
|
c-1.979,5.551-6.624,10.389-7,12c-0.556,2.38-0.469,11.399,0,12c0.852,1.092,5.148-0.989,10,0c2.871,0.585,12.164,2.755,14,0
|
||||||
|
c2.173-3.263-4.913-9.8-5-14c-0.158-7.6,11.49-26.061,6-35C276.528,323.492,274.416,325.34,270.671,327z"/>
|
||||||
|
</g>
|
||||||
|
<rect class="st1" width="500" height="460"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.6 KiB |
275
Sources/KPopBird/AppDelegate.swift
Normal file
275
Sources/KPopBird/AppDelegate.swift
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
import AppKit
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
|
private var statusItem: NSStatusItem!
|
||||||
|
private let sleepManager = SleepManager()
|
||||||
|
private var timer: Timer?
|
||||||
|
private var expireTimer: Timer?
|
||||||
|
private var expireDate: Date?
|
||||||
|
private var batteryAwareDisable = false
|
||||||
|
|
||||||
|
private lazy var defaults = UserDefaults.standard
|
||||||
|
|
||||||
|
func applicationDidFinishLaunching(_ notification: Notification) {
|
||||||
|
NSApp.setActivationPolicy(.accessory)
|
||||||
|
defaults.register(defaults: [
|
||||||
|
"activateOnLaunch": true,
|
||||||
|
"preventDisplaySleep": true,
|
||||||
|
"preventSystemSleep": true
|
||||||
|
])
|
||||||
|
loadPreferences()
|
||||||
|
setupStatusItem()
|
||||||
|
startTickingTimer()
|
||||||
|
|
||||||
|
if defaults.bool(forKey: "activateOnLaunch") {
|
||||||
|
activateIndefinitely()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func applicationWillTerminate(_ notification: Notification) {
|
||||||
|
sleepManager.deactivate()
|
||||||
|
savePreferences()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Preferences
|
||||||
|
|
||||||
|
private func loadPreferences() {
|
||||||
|
if defaults.object(forKey: "preventDisplaySleep") != nil {
|
||||||
|
sleepManager.preventDisplaySleep = defaults.bool(forKey: "preventDisplaySleep")
|
||||||
|
}
|
||||||
|
if defaults.object(forKey: "preventSystemSleep") != nil {
|
||||||
|
sleepManager.preventSystemSleep = defaults.bool(forKey: "preventSystemSleep")
|
||||||
|
}
|
||||||
|
batteryAwareDisable = defaults.bool(forKey: "batteryAwareDisable")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func savePreferences() {
|
||||||
|
defaults.set(sleepManager.preventDisplaySleep, forKey: "preventDisplaySleep")
|
||||||
|
defaults.set(sleepManager.preventSystemSleep, forKey: "preventSystemSleep")
|
||||||
|
defaults.set(batteryAwareDisable, forKey: "batteryAwareDisable")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Status Item
|
||||||
|
|
||||||
|
private func setupStatusItem() {
|
||||||
|
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
|
||||||
|
updateStatusIcon()
|
||||||
|
rebuildMenu()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateStatusIcon() {
|
||||||
|
guard let button = statusItem.button else { return }
|
||||||
|
button.image = Self.makeIcon(active: sleepManager.isActive)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static let activeIcon: NSImage = loadIcon(active: true)
|
||||||
|
private static let inactiveIcon: NSImage = loadIcon(active: false)
|
||||||
|
|
||||||
|
private static func makeIcon(active: Bool) -> NSImage {
|
||||||
|
active ? activeIcon : inactiveIcon
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func loadIcon(active: Bool) -> NSImage {
|
||||||
|
let url = Bundle.main.url(forResource: "samjokoo", withExtension: "pdf")
|
||||||
|
?? URL(fileURLWithPath: "/Users/charleskwon/Applications/KPopBird.app/Contents/Resources/samjokoo.pdf")
|
||||||
|
guard let base = NSImage(contentsOf: url) else {
|
||||||
|
return NSImage(systemSymbolName: "bird.fill", accessibilityDescription: "KPopBird") ?? NSImage()
|
||||||
|
}
|
||||||
|
let target = NSSize(width: 20, height: 18)
|
||||||
|
let rendered = NSImage(size: target, flipped: false) { _ in
|
||||||
|
let rect = NSRect(origin: .zero, size: target)
|
||||||
|
if active {
|
||||||
|
base.draw(in: rect)
|
||||||
|
} else {
|
||||||
|
base.draw(in: rect, from: .zero, operation: .sourceOver, fraction: 0.45)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
rendered.isTemplate = true
|
||||||
|
return rendered
|
||||||
|
}
|
||||||
|
|
||||||
|
private func rebuildMenu() {
|
||||||
|
let menu = NSMenu()
|
||||||
|
|
||||||
|
let statusTitle: String
|
||||||
|
if sleepManager.isActive {
|
||||||
|
if let expire = expireDate {
|
||||||
|
let remaining = Int(expire.timeIntervalSinceNow)
|
||||||
|
statusTitle = "활성: \(formatDuration(remaining)) 남음"
|
||||||
|
} else if let started = sleepManager.activatedAt {
|
||||||
|
let elapsed = Int(Date().timeIntervalSince(started))
|
||||||
|
statusTitle = "활성: \(formatDuration(elapsed)) 경과"
|
||||||
|
} else {
|
||||||
|
statusTitle = "활성"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
statusTitle = "비활성"
|
||||||
|
}
|
||||||
|
|
||||||
|
let statusItemMenu = NSMenuItem(title: statusTitle, action: nil, keyEquivalent: "")
|
||||||
|
statusItemMenu.isEnabled = false
|
||||||
|
menu.addItem(statusItemMenu)
|
||||||
|
|
||||||
|
menu.addItem(.separator())
|
||||||
|
|
||||||
|
let toggleTitle = sleepManager.isActive ? "비활성화" : "활성화 (무제한)"
|
||||||
|
let toggleItem = NSMenuItem(title: toggleTitle, action: #selector(toggle), keyEquivalent: "t")
|
||||||
|
toggleItem.target = self
|
||||||
|
menu.addItem(toggleItem)
|
||||||
|
|
||||||
|
let timerMenu = NSMenu()
|
||||||
|
let durations: [(String, TimeInterval)] = [
|
||||||
|
("15분", 15 * 60),
|
||||||
|
("30분", 30 * 60),
|
||||||
|
("1시간", 60 * 60),
|
||||||
|
("2시간", 2 * 60 * 60),
|
||||||
|
("4시간", 4 * 60 * 60)
|
||||||
|
]
|
||||||
|
for (title, duration) in durations {
|
||||||
|
let item = NSMenuItem(title: title, action: #selector(activateTimer(_:)), keyEquivalent: "")
|
||||||
|
item.target = self
|
||||||
|
item.representedObject = duration
|
||||||
|
timerMenu.addItem(item)
|
||||||
|
}
|
||||||
|
let timerRoot = NSMenuItem(title: "타이머로 활성화", action: nil, keyEquivalent: "")
|
||||||
|
timerRoot.submenu = timerMenu
|
||||||
|
menu.addItem(timerRoot)
|
||||||
|
|
||||||
|
menu.addItem(.separator())
|
||||||
|
|
||||||
|
let displayItem = NSMenuItem(title: "디스플레이 절전 방지", action: #selector(toggleDisplay), keyEquivalent: "")
|
||||||
|
displayItem.target = self
|
||||||
|
displayItem.state = sleepManager.preventDisplaySleep ? .on : .off
|
||||||
|
menu.addItem(displayItem)
|
||||||
|
|
||||||
|
let systemItem = NSMenuItem(title: "시스템 절전 방지", action: #selector(toggleSystem), keyEquivalent: "")
|
||||||
|
systemItem.target = self
|
||||||
|
systemItem.state = sleepManager.preventSystemSleep ? .on : .off
|
||||||
|
menu.addItem(systemItem)
|
||||||
|
|
||||||
|
let batteryItem = NSMenuItem(title: "배터리 사용 시 자동 해제", action: #selector(toggleBatteryAware), keyEquivalent: "")
|
||||||
|
batteryItem.target = self
|
||||||
|
batteryItem.state = batteryAwareDisable ? .on : .off
|
||||||
|
menu.addItem(batteryItem)
|
||||||
|
|
||||||
|
let launchItem = NSMenuItem(title: "로그인 시 자동 활성화", action: #selector(toggleActivateOnLaunch), keyEquivalent: "")
|
||||||
|
launchItem.target = self
|
||||||
|
launchItem.state = defaults.bool(forKey: "activateOnLaunch") ? .on : .off
|
||||||
|
menu.addItem(launchItem)
|
||||||
|
|
||||||
|
menu.addItem(.separator())
|
||||||
|
|
||||||
|
let powerInfo = SleepManager.isOnACPower() ? "전원 어댑터 연결됨" : "배터리 사용 중"
|
||||||
|
let powerItem = NSMenuItem(title: powerInfo, action: nil, keyEquivalent: "")
|
||||||
|
powerItem.isEnabled = false
|
||||||
|
menu.addItem(powerItem)
|
||||||
|
|
||||||
|
menu.addItem(.separator())
|
||||||
|
|
||||||
|
let quitItem = NSMenuItem(title: "KPopBird 종료", action: #selector(quit), keyEquivalent: "q")
|
||||||
|
quitItem.target = self
|
||||||
|
menu.addItem(quitItem)
|
||||||
|
|
||||||
|
statusItem.menu = menu
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Actions
|
||||||
|
|
||||||
|
@objc private func toggle() {
|
||||||
|
if sleepManager.isActive {
|
||||||
|
deactivate()
|
||||||
|
} else {
|
||||||
|
activateIndefinitely()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func activateTimer(_ sender: NSMenuItem) {
|
||||||
|
guard let duration = sender.representedObject as? TimeInterval else { return }
|
||||||
|
activate(for: duration)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func toggleDisplay() {
|
||||||
|
sleepManager.preventDisplaySleep.toggle()
|
||||||
|
savePreferences()
|
||||||
|
if sleepManager.isActive { activateIndefinitely() }
|
||||||
|
rebuildMenu()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func toggleSystem() {
|
||||||
|
sleepManager.preventSystemSleep.toggle()
|
||||||
|
savePreferences()
|
||||||
|
if sleepManager.isActive { activateIndefinitely() }
|
||||||
|
rebuildMenu()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func toggleBatteryAware() {
|
||||||
|
batteryAwareDisable.toggle()
|
||||||
|
savePreferences()
|
||||||
|
rebuildMenu()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func toggleActivateOnLaunch() {
|
||||||
|
let current = defaults.bool(forKey: "activateOnLaunch")
|
||||||
|
defaults.set(!current, forKey: "activateOnLaunch")
|
||||||
|
rebuildMenu()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func quit() {
|
||||||
|
NSApp.terminate(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Activation
|
||||||
|
|
||||||
|
private func activateIndefinitely() {
|
||||||
|
expireTimer?.invalidate()
|
||||||
|
expireTimer = nil
|
||||||
|
expireDate = nil
|
||||||
|
sleepManager.activate()
|
||||||
|
updateStatusIcon()
|
||||||
|
rebuildMenu()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func activate(for duration: TimeInterval) {
|
||||||
|
sleepManager.activate()
|
||||||
|
expireDate = Date().addingTimeInterval(duration)
|
||||||
|
expireTimer?.invalidate()
|
||||||
|
expireTimer = Timer.scheduledTimer(withTimeInterval: duration, repeats: false) { [weak self] _ in
|
||||||
|
self?.deactivate()
|
||||||
|
}
|
||||||
|
updateStatusIcon()
|
||||||
|
rebuildMenu()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func deactivate() {
|
||||||
|
sleepManager.deactivate()
|
||||||
|
expireTimer?.invalidate()
|
||||||
|
expireTimer = nil
|
||||||
|
expireDate = nil
|
||||||
|
updateStatusIcon()
|
||||||
|
rebuildMenu()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Ticking
|
||||||
|
|
||||||
|
private func startTickingTimer() {
|
||||||
|
timer = Timer.scheduledTimer(withTimeInterval: 5, repeats: true) { [weak self] _ in
|
||||||
|
guard let self = self else { return }
|
||||||
|
if self.batteryAwareDisable, !SleepManager.isOnACPower(), self.sleepManager.isActive {
|
||||||
|
self.deactivate()
|
||||||
|
}
|
||||||
|
self.rebuildMenu()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func formatDuration(_ seconds: Int) -> String {
|
||||||
|
let s = max(0, seconds)
|
||||||
|
let h = s / 3600
|
||||||
|
let m = (s % 3600) / 60
|
||||||
|
let sec = s % 60
|
||||||
|
if h > 0 { return String(format: "%d시간 %d분", h, m) }
|
||||||
|
if m > 0 { return String(format: "%d분 %d초", m, sec) }
|
||||||
|
return String(format: "%d초", sec)
|
||||||
|
}
|
||||||
|
}
|
||||||
68
Sources/KPopBird/SleepManager.swift
Normal file
68
Sources/KPopBird/SleepManager.swift
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import Foundation
|
||||||
|
import IOKit
|
||||||
|
import IOKit.pwr_mgt
|
||||||
|
import IOKit.ps
|
||||||
|
|
||||||
|
final class SleepManager {
|
||||||
|
private var displayAssertionID: IOPMAssertionID = 0
|
||||||
|
private var systemAssertionID: IOPMAssertionID = 0
|
||||||
|
private(set) var isDisplayActive = false
|
||||||
|
private(set) var isSystemActive = false
|
||||||
|
private(set) var activatedAt: Date?
|
||||||
|
|
||||||
|
var preventDisplaySleep = true
|
||||||
|
var preventSystemSleep = true
|
||||||
|
|
||||||
|
func activate() {
|
||||||
|
deactivate()
|
||||||
|
|
||||||
|
if preventDisplaySleep {
|
||||||
|
let reason = "KPopBird: preventing display sleep" as CFString
|
||||||
|
let result = IOPMAssertionCreateWithName(
|
||||||
|
kIOPMAssertionTypePreventUserIdleDisplaySleep as CFString,
|
||||||
|
IOPMAssertionLevel(kIOPMAssertionLevelOn),
|
||||||
|
reason,
|
||||||
|
&displayAssertionID
|
||||||
|
)
|
||||||
|
isDisplayActive = (result == kIOReturnSuccess)
|
||||||
|
}
|
||||||
|
|
||||||
|
if preventSystemSleep {
|
||||||
|
let reason = "KPopBird: preventing system sleep" as CFString
|
||||||
|
let result = IOPMAssertionCreateWithName(
|
||||||
|
kIOPMAssertionTypePreventUserIdleSystemSleep as CFString,
|
||||||
|
IOPMAssertionLevel(kIOPMAssertionLevelOn),
|
||||||
|
reason,
|
||||||
|
&systemAssertionID
|
||||||
|
)
|
||||||
|
isSystemActive = (result == kIOReturnSuccess)
|
||||||
|
}
|
||||||
|
|
||||||
|
activatedAt = Date()
|
||||||
|
}
|
||||||
|
|
||||||
|
func deactivate() {
|
||||||
|
if isDisplayActive {
|
||||||
|
IOPMAssertionRelease(displayAssertionID)
|
||||||
|
isDisplayActive = false
|
||||||
|
displayAssertionID = 0
|
||||||
|
}
|
||||||
|
if isSystemActive {
|
||||||
|
IOPMAssertionRelease(systemAssertionID)
|
||||||
|
isSystemActive = false
|
||||||
|
systemAssertionID = 0
|
||||||
|
}
|
||||||
|
activatedAt = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var isActive: Bool {
|
||||||
|
isDisplayActive || isSystemActive
|
||||||
|
}
|
||||||
|
|
||||||
|
static func isOnACPower() -> Bool {
|
||||||
|
guard let info = IOPSCopyPowerSourcesInfo()?.takeRetainedValue(),
|
||||||
|
let providing = IOPSGetProvidingPowerSourceType(info)?.takeUnretainedValue()
|
||||||
|
else { return true }
|
||||||
|
return (providing as String) == kIOPMACPowerKey
|
||||||
|
}
|
||||||
|
}
|
||||||
6
Sources/KPopBird/main.swift
Normal file
6
Sources/KPopBird/main.swift
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import AppKit
|
||||||
|
|
||||||
|
let app = NSApplication.shared
|
||||||
|
let delegate = AppDelegate()
|
||||||
|
app.delegate = delegate
|
||||||
|
app.run()
|
||||||
Reference in New Issue
Block a user