From d313af12e2c1350c20bef5ff2222ea287c055f83 Mon Sep 17 00:00:00 2001 From: Leonardo Dino Date: Sun, 4 Jun 2023 00:44:05 +0100 Subject: [PATCH] :sparkles: add "Launch at login" item for macOS 13+ --- BeezyLight.xcodeproj/project.pbxproj | 4 +++ README.md | 1 - Sources/AboutWindow.swift | 6 ++++ Sources/LaunchAtLogin.swift | 46 ++++++++++++++++++++++++++++ Sources/StatusItem.swift | 38 +++++++++++------------ 5 files changed, 75 insertions(+), 20 deletions(-) create mode 100644 Sources/LaunchAtLogin.swift diff --git a/BeezyLight.xcodeproj/project.pbxproj b/BeezyLight.xcodeproj/project.pbxproj index f224b7b..f8832f4 100644 --- a/BeezyLight.xcodeproj/project.pbxproj +++ b/BeezyLight.xcodeproj/project.pbxproj @@ -13,6 +13,7 @@ 965A05CF29C5224000559377 /* AudioSystemObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 965A05CE29C5224000559377 /* AudioSystemObject.swift */; }; 965A05D129C522C000559377 /* AudioObjectProperty.swift in Sources */ = {isa = PBXBuildFile; fileRef = 965A05D029C522C000559377 /* AudioObjectProperty.swift */; }; 965A05D329C524F100559377 /* AudioBufferListWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 965A05D229C524F100559377 /* AudioBufferListWrapper.swift */; }; + 966385472A2BF0420092FC62 /* LaunchAtLogin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 966385462A2BF0420092FC62 /* LaunchAtLogin.swift */; }; 96B8901227F4E78D00C39B4A /* AboutWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96B8901127F4E78D00C39B4A /* AboutWindow.swift */; }; 96C2A1E227C5632D00768B18 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96C2A1E127C5632D00768B18 /* AppDelegate.swift */; }; 96C2A1E627C5632E00768B18 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 96C2A1E527C5632E00768B18 /* Assets.xcassets */; }; @@ -30,6 +31,7 @@ 965A05CE29C5224000559377 /* AudioSystemObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioSystemObject.swift; sourceTree = ""; }; 965A05D029C522C000559377 /* AudioObjectProperty.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioObjectProperty.swift; sourceTree = ""; }; 965A05D229C524F100559377 /* AudioBufferListWrapper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioBufferListWrapper.swift; sourceTree = ""; }; + 966385462A2BF0420092FC62 /* LaunchAtLogin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchAtLogin.swift; sourceTree = ""; }; 96A53DDC27C572B9002DA809 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 96B8901127F4E78D00C39B4A /* AboutWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutWindow.swift; sourceTree = ""; }; 96B9AA4828774C1A00175C2A /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; @@ -77,6 +79,7 @@ 96C2A1E127C5632D00768B18 /* AppDelegate.swift */, 962377FD27F4ECD8002D718F /* StatusItem.swift */, 96B8901127F4E78D00C39B4A /* AboutWindow.swift */, + 966385462A2BF0420092FC62 /* LaunchAtLogin.swift */, 96C2A1F327C567C300768B18 /* BlinkStick.swift */, 96D5B44727C57CE300C4FFCA /* Debouncer.swift */, 96D07DC529D7974B009C2C23 /* AudioInput.swift */, @@ -209,6 +212,7 @@ 965A05CF29C5224000559377 /* AudioSystemObject.swift in Sources */, 96C2A1F527C567C300768B18 /* BlinkStick.swift in Sources */, 96C2A1E227C5632D00768B18 /* AppDelegate.swift in Sources */, + 966385472A2BF0420092FC62 /* LaunchAtLogin.swift in Sources */, 965A05CB29C5212400559377 /* AudioDevice.swift in Sources */, 965A05CD29C5214000559377 /* AudioObject.swift in Sources */, 962377FE27F4ECD8002D718F /* StatusItem.swift in Sources */, diff --git a/README.md b/README.md index 3e89c3d..915c6da 100644 --- a/README.md +++ b/README.md @@ -17,4 +17,3 @@ #### ✦ install: 1. download lastest version [**here**](https://github.com/leonardodino/BeezyLight/releases/latest/download/BeezyLight.zip) 2. unzip and move `BeezyLight.app` to `/Applications` -3. add to [**Login Items**](https://support.apple.com/en-gb/guide/mac-help/mh15189/mac) to launch it automatically after login in **(optional)** diff --git a/Sources/AboutWindow.swift b/Sources/AboutWindow.swift index 07764f1..498ee16 100644 --- a/Sources/AboutWindow.swift +++ b/Sources/AboutWindow.swift @@ -28,6 +28,12 @@ extension NSApplication.AboutPanelOptionKey { class AboutWindow { static let shared = AboutWindow() + let menuItem: NSMenuItem + private init() { + let appName = Bundle.main.object(forInfoDictionaryKey: "CFBundleName") ?? "" + menuItem = NSMenuItem(title: "About \(appName)", action: #selector(AboutWindow.show), keyEquivalent: "") + menuItem.target = self + } @objc func show() { NSApp.orderFrontStandardAboutPanel(options: [ .copyright: "", diff --git a/Sources/LaunchAtLogin.swift b/Sources/LaunchAtLogin.swift new file mode 100644 index 0000000..23570d5 --- /dev/null +++ b/Sources/LaunchAtLogin.swift @@ -0,0 +1,46 @@ +import Cocoa +import ServiceManagement + +@available(macOS 13.0, *) +final class LaunchAtLogin { + static let shared = LaunchAtLogin() + private(set) var menuItem: NSMenuItem + + private init() { + menuItem = NSMenuItem(title: "Launch at login", action: #selector(LaunchAtLogin.toggle), keyEquivalent: "") + menuItem.target = self + menuItem.state = state + } + + private func set(_ launch: Bool) { + if launch { + if SMAppService.mainApp.status == .enabled { + try? SMAppService.mainApp.unregister() + } + try? SMAppService.mainApp.register() + } else { + try? SMAppService.mainApp.unregister() + } + } + + private var state: NSControl.StateValue { + switch SMAppService.mainApp.status { + case .notRegistered: return .off + case .enabled: return .on + case .requiresApproval: return .mixed + case .notFound: return .off + @unknown default: return .off + } + } + + @objc + func toggle() { + switch state { + case .on: set(false) + case .off: set(true) + default: SMAppService.openSystemSettingsLoginItems() + } + menuItem.state = state + } +} + diff --git a/Sources/StatusItem.swift b/Sources/StatusItem.swift index c6d5c12..8d006dc 100644 --- a/Sources/StatusItem.swift +++ b/Sources/StatusItem.swift @@ -1,4 +1,11 @@ import Cocoa +import ServiceManagement + +fileprivate extension NSApplication { + var quitMenuItem: NSMenuItem { + NSMenuItem(title: "Quit", action: #selector(terminate), keyEquivalent: "q") + } +} class StatusItem { enum StateIcon { @@ -17,29 +24,22 @@ class StatusItem { private let instance = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength) + private lazy var menu: NSMenu = { + let menu = NSMenu() + menu.addItem(AboutWindow.shared.menuItem) + if #available(macOS 13.0, *) { + menu.addItem(LaunchAtLogin.shared.menuItem) + menu.addItem(NSMenuItem.separator()) + } + menu.addItem(NSApplication.shared.quitMenuItem) + return menu + }() + init() { - let appName = Bundle.main.object(forInfoDictionaryKey: "CFBundleName") ?? "" - instance.menu = NSMenu([ - NSMenuItem("About \(appName)", action: #selector(AboutWindow.show), target: AboutWindow.shared), - NSMenuItem("Quit", action: #selector(NSApp.terminate), keyEquivalent: "q"), - ]) + instance.menu = menu } func setIcon(_ icon: StateIcon) { instance.button?.image = icon.image } } - -fileprivate extension NSMenuItem { - convenience init(_ title: String, action: Selector, target: AnyObject? = nil, keyEquivalent: String = "") { - self.init(title: title, action: action, keyEquivalent: keyEquivalent) - self.target = target - } -} - -fileprivate extension NSMenu { - convenience init(_ items: [NSMenuItem]) { - self.init() - items.forEach { self.addItem($0) } - } -}