Skip to content

Commit 8589f94

Browse files
author
cuiko tang
authored
Merge pull request #5 from cuiko/feat/launch-at-login-setting
feat(settings): Launch at login via SMAppService in Settings
2 parents 3e570db + 7183902 commit 8589f94

3 files changed

Lines changed: 66 additions & 28 deletions

File tree

Sources/TermIMS/AppDelegate.swift

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,9 @@ class AppDelegate: NSObject, NSApplicationDelegate {
88
private var settingsWC: SettingsWindowController?
99
private var permissionWC: PermissionWindowController?
1010
private var enabledItem: NSMenuItem!
11-
private var loginItem: NSMenuItem!
1211
private var permissionPollTimer: Timer?
1312
private var wasTrusted = false
1413

15-
private var launchAgentPath: String {
16-
NSHomeDirectory() + "/Library/LaunchAgents/top.cuiko.termims.plist"
17-
}
18-
1914
func applicationDidFinishLaunching(_ n: Notification) {
2015
Log.debug("=== TermIMS started ===")
2116
installEditMenu()
@@ -174,12 +169,6 @@ class AppDelegate: NSObject, NSApplicationDelegate {
174169
let settings = NSMenuItem(title: "Settings\u{2026}", action: #selector(showSettings), keyEquivalent: ",")
175170
settings.target = self; menu.addItem(settings)
176171

177-
menu.addItem(.separator())
178-
loginItem = NSMenuItem(title: "Launch at Login", action: #selector(toggleLogin), keyEquivalent: "")
179-
loginItem.target = self
180-
loginItem.state = FileManager.default.fileExists(atPath: launchAgentPath) ? .on : .off
181-
menu.addItem(loginItem)
182-
183172
menu.addItem(.separator())
184173
let quit = NSMenuItem(title: "Quit TermIMS", action: #selector(quitApp), keyEquivalent: "q")
185174
quit.target = self; menu.addItem(quit)
@@ -198,20 +187,5 @@ class AppDelegate: NSObject, NSApplicationDelegate {
198187
if settingsWC == nil { settingsWC = SettingsWindowController() }
199188
settingsWC?.showWindow(nil); NSApp.activate(ignoringOtherApps: true)
200189
}
201-
@objc private func toggleLogin() {
202-
let fm = FileManager.default
203-
if fm.fileExists(atPath: launchAgentPath) {
204-
try? fm.removeItem(atPath: launchAgentPath)
205-
} else {
206-
try? fm.createDirectory(atPath: NSHomeDirectory() + "/Library/LaunchAgents", withIntermediateDirectories: true)
207-
let plist: NSDictionary = [
208-
"Label": "top.cuiko.termims",
209-
"ProgramArguments": [Bundle.main.executablePath ?? ""],
210-
"RunAtLoad": true,
211-
]
212-
plist.write(toFile: launchAgentPath, atomically: true)
213-
}
214-
loginItem.state = fm.fileExists(atPath: launchAgentPath) ? .on : .off
215-
}
216190
@objc private func quitApp() { monitor.stop(); NSApp.terminate(nil) }
217191
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import Foundation
2+
import ServiceManagement
3+
4+
/// Launch-at-login via `SMAppService.mainApp` (macOS 13+). Registering the
5+
/// app bundle this way makes it appear in System Settings → General → Login
6+
/// Items under "Open at Login" with the real app icon and name — unlike the
7+
/// legacy LaunchAgent plist, which lands in "Allow in the Background" as a
8+
/// raw `exec` entry from an "unidentified developer".
9+
enum LaunchAtLogin {
10+
static var isEnabled: Bool {
11+
SMAppService.mainApp.status == .enabled
12+
}
13+
14+
static func setEnabled(_ enabled: Bool) {
15+
// Drop any agent left by older plist-based builds so the user isn't
16+
// launched twice (once by the stale agent, once by SMAppService).
17+
removeLegacyAgent()
18+
do {
19+
if enabled {
20+
if SMAppService.mainApp.status != .enabled {
21+
try SMAppService.mainApp.register()
22+
}
23+
} else {
24+
if SMAppService.mainApp.status == .enabled {
25+
try SMAppService.mainApp.unregister()
26+
}
27+
}
28+
} catch {
29+
Log.debug("LaunchAtLogin set(\(enabled)) failed: \(error)")
30+
}
31+
}
32+
33+
private static func removeLegacyAgent() {
34+
let path = NSHomeDirectory() + "/Library/LaunchAgents/top.cuiko.termims.plist"
35+
if FileManager.default.fileExists(atPath: path) {
36+
try? FileManager.default.removeItem(atPath: path)
37+
}
38+
}
39+
}

Sources/TermIMS/SettingsWindowController.swift

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ final class DragHandleView: NSView {
7878
class SettingsWindowController: NSWindowController, NSTableViewDataSource, NSTableViewDelegate {
7979
private var appTableView: NSTableView!
8080
private var termTableView: NSTableView!
81+
private var loginCheck: NSButton?
8182
private let inputSources = listInputSources()
8283

8384
convenience init() {
@@ -90,6 +91,19 @@ class SettingsWindowController: NSWindowController, NSTableViewDataSource, NSTab
9091
w.isReleasedWhenClosed = false
9192
self.init(window: w)
9293
buildUI()
94+
// The General tab is built once and the controller is cached, so a
95+
// launch-at-login change made elsewhere (the user toggling our entry
96+
// in System Settings → Login Items) would leave the checkbox stale.
97+
// macOS sends us no notification for that, so re-read the live
98+
// SMAppService status every time the window comes forward.
99+
NotificationCenter.default.addObserver(self, selector: #selector(syncExternalState),
100+
name: NSWindow.didBecomeKeyNotification, object: w)
101+
}
102+
103+
deinit { NotificationCenter.default.removeObserver(self) }
104+
105+
@objc private func syncExternalState() {
106+
loginCheck?.state = LaunchAtLogin.isEnabled ? .on : .off
93107
}
94108

95109
// MARK: Tab Builder
@@ -161,6 +175,12 @@ class SettingsWindowController: NSWindowController, NSTableViewDataSource, NSTab
161175
let appLabel = label("Application")
162176
appLabel.font = .systemFont(ofSize: 13, weight: .semibold)
163177

178+
let loginCheck = NSButton(checkboxWithTitle: "Launch at login",
179+
target: self, action: #selector(launchAtLoginToggled(_:)))
180+
loginCheck.translatesAutoresizingMaskIntoConstraints = false
181+
loginCheck.state = LaunchAtLogin.isEnabled ? .on : .off
182+
self.loginCheck = loginCheck
183+
164184
let hideCheck = NSButton(checkboxWithTitle: "Hide menu bar icon (reopen app to show Settings)",
165185
target: self, action: #selector(hideIconToggled(_:)))
166186
hideCheck.translatesAutoresizingMaskIntoConstraints = false
@@ -181,7 +201,7 @@ class SettingsWindowController: NSWindowController, NSTableViewDataSource, NSTab
181201
clearBtn.controlSize = .regular
182202

183203
for sub in [defLabel, defPopup, sep1, indLabel, indCheck, posLabel, posPopup,
184-
sep2, appLabel, hideCheck,
204+
sep2, appLabel, loginCheck, hideCheck,
185205
sep3, debugLabel, debugCheck, clearBtn] { v.addSubview(sub) }
186206
NSLayoutConstraint.activate([
187207
defLabel.topAnchor.constraint(equalTo: v.topAnchor, constant: 20),
@@ -210,7 +230,9 @@ class SettingsWindowController: NSWindowController, NSTableViewDataSource, NSTab
210230

211231
appLabel.topAnchor.constraint(equalTo: sep2.bottomAnchor, constant: 16),
212232
appLabel.leadingAnchor.constraint(equalTo: v.leadingAnchor, constant: 16),
213-
hideCheck.topAnchor.constraint(equalTo: appLabel.bottomAnchor, constant: 10),
233+
loginCheck.topAnchor.constraint(equalTo: appLabel.bottomAnchor, constant: 10),
234+
loginCheck.leadingAnchor.constraint(equalTo: v.leadingAnchor, constant: 16),
235+
hideCheck.topAnchor.constraint(equalTo: loginCheck.bottomAnchor, constant: 8),
214236
hideCheck.leadingAnchor.constraint(equalTo: v.leadingAnchor, constant: 16),
215237

216238
sep3.topAnchor.constraint(equalTo: hideCheck.bottomAnchor, constant: 20),
@@ -242,6 +264,9 @@ class SettingsWindowController: NSWindowController, NSTableViewDataSource, NSTab
242264
@objc private func indicatorPosChanged(_ sender: NSPopUpButton) {
243265
RuleStore.shared.indicatorPosition = IndicatorPosition.allCases[sender.indexOfSelectedItem]
244266
}
267+
@objc private func launchAtLoginToggled(_ sender: NSButton) {
268+
LaunchAtLogin.setEnabled(sender.state == .on)
269+
}
245270
@objc private func hideIconToggled(_ sender: NSButton) {
246271
RuleStore.shared.hideMenuBarIcon = sender.state == .on
247272
}

0 commit comments

Comments
 (0)