Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 0 additions & 26 deletions Sources/TermIMS/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,9 @@ class AppDelegate: NSObject, NSApplicationDelegate {
private var settingsWC: SettingsWindowController?
private var permissionWC: PermissionWindowController?
private var enabledItem: NSMenuItem!
private var loginItem: NSMenuItem!
private var permissionPollTimer: Timer?
private var wasTrusted = false

private var launchAgentPath: String {
NSHomeDirectory() + "/Library/LaunchAgents/top.cuiko.termims.plist"
}

func applicationDidFinishLaunching(_ n: Notification) {
Log.debug("=== TermIMS started ===")
installEditMenu()
Expand Down Expand Up @@ -174,12 +169,6 @@ class AppDelegate: NSObject, NSApplicationDelegate {
let settings = NSMenuItem(title: "Settings\u{2026}", action: #selector(showSettings), keyEquivalent: ",")
settings.target = self; menu.addItem(settings)

menu.addItem(.separator())
loginItem = NSMenuItem(title: "Launch at Login", action: #selector(toggleLogin), keyEquivalent: "")
loginItem.target = self
loginItem.state = FileManager.default.fileExists(atPath: launchAgentPath) ? .on : .off
menu.addItem(loginItem)

menu.addItem(.separator())
let quit = NSMenuItem(title: "Quit TermIMS", action: #selector(quitApp), keyEquivalent: "q")
quit.target = self; menu.addItem(quit)
Expand All @@ -198,20 +187,5 @@ class AppDelegate: NSObject, NSApplicationDelegate {
if settingsWC == nil { settingsWC = SettingsWindowController() }
settingsWC?.showWindow(nil); NSApp.activate(ignoringOtherApps: true)
}
@objc private func toggleLogin() {
let fm = FileManager.default
if fm.fileExists(atPath: launchAgentPath) {
try? fm.removeItem(atPath: launchAgentPath)
} else {
try? fm.createDirectory(atPath: NSHomeDirectory() + "/Library/LaunchAgents", withIntermediateDirectories: true)
let plist: NSDictionary = [
"Label": "top.cuiko.termims",
"ProgramArguments": [Bundle.main.executablePath ?? ""],
"RunAtLoad": true,
]
plist.write(toFile: launchAgentPath, atomically: true)
}
loginItem.state = fm.fileExists(atPath: launchAgentPath) ? .on : .off
}
@objc private func quitApp() { monitor.stop(); NSApp.terminate(nil) }
}
39 changes: 39 additions & 0 deletions Sources/TermIMS/LaunchAtLogin.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import Foundation
import ServiceManagement

/// Launch-at-login via `SMAppService.mainApp` (macOS 13+). Registering the
/// app bundle this way makes it appear in System Settings → General → Login
/// Items under "Open at Login" with the real app icon and name — unlike the
/// legacy LaunchAgent plist, which lands in "Allow in the Background" as a
/// raw `exec` entry from an "unidentified developer".
enum LaunchAtLogin {
static var isEnabled: Bool {
SMAppService.mainApp.status == .enabled
}

static func setEnabled(_ enabled: Bool) {
// Drop any agent left by older plist-based builds so the user isn't
// launched twice (once by the stale agent, once by SMAppService).
removeLegacyAgent()
do {
if enabled {
if SMAppService.mainApp.status != .enabled {
try SMAppService.mainApp.register()
}
} else {
if SMAppService.mainApp.status == .enabled {
try SMAppService.mainApp.unregister()
}
}
} catch {
Log.debug("LaunchAtLogin set(\(enabled)) failed: \(error)")
}
}

private static func removeLegacyAgent() {
let path = NSHomeDirectory() + "/Library/LaunchAgents/top.cuiko.termims.plist"
if FileManager.default.fileExists(atPath: path) {
try? FileManager.default.removeItem(atPath: path)
}
}
}
29 changes: 27 additions & 2 deletions Sources/TermIMS/SettingsWindowController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ final class DragHandleView: NSView {
class SettingsWindowController: NSWindowController, NSTableViewDataSource, NSTableViewDelegate {
private var appTableView: NSTableView!
private var termTableView: NSTableView!
private var loginCheck: NSButton?
private let inputSources = listInputSources()

convenience init() {
Expand All @@ -90,6 +91,19 @@ class SettingsWindowController: NSWindowController, NSTableViewDataSource, NSTab
w.isReleasedWhenClosed = false
self.init(window: w)
buildUI()
// The General tab is built once and the controller is cached, so a
// launch-at-login change made elsewhere (the user toggling our entry
// in System Settings → Login Items) would leave the checkbox stale.
// macOS sends us no notification for that, so re-read the live
// SMAppService status every time the window comes forward.
NotificationCenter.default.addObserver(self, selector: #selector(syncExternalState),
name: NSWindow.didBecomeKeyNotification, object: w)
}

deinit { NotificationCenter.default.removeObserver(self) }

@objc private func syncExternalState() {
loginCheck?.state = LaunchAtLogin.isEnabled ? .on : .off
}

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

let loginCheck = NSButton(checkboxWithTitle: "Launch at login",
target: self, action: #selector(launchAtLoginToggled(_:)))
loginCheck.translatesAutoresizingMaskIntoConstraints = false
loginCheck.state = LaunchAtLogin.isEnabled ? .on : .off
self.loginCheck = loginCheck

let hideCheck = NSButton(checkboxWithTitle: "Hide menu bar icon (reopen app to show Settings)",
target: self, action: #selector(hideIconToggled(_:)))
hideCheck.translatesAutoresizingMaskIntoConstraints = false
Expand All @@ -181,7 +201,7 @@ class SettingsWindowController: NSWindowController, NSTableViewDataSource, NSTab
clearBtn.controlSize = .regular

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

appLabel.topAnchor.constraint(equalTo: sep2.bottomAnchor, constant: 16),
appLabel.leadingAnchor.constraint(equalTo: v.leadingAnchor, constant: 16),
hideCheck.topAnchor.constraint(equalTo: appLabel.bottomAnchor, constant: 10),
loginCheck.topAnchor.constraint(equalTo: appLabel.bottomAnchor, constant: 10),
loginCheck.leadingAnchor.constraint(equalTo: v.leadingAnchor, constant: 16),
hideCheck.topAnchor.constraint(equalTo: loginCheck.bottomAnchor, constant: 8),
hideCheck.leadingAnchor.constraint(equalTo: v.leadingAnchor, constant: 16),

sep3.topAnchor.constraint(equalTo: hideCheck.bottomAnchor, constant: 20),
Expand Down Expand Up @@ -242,6 +264,9 @@ class SettingsWindowController: NSWindowController, NSTableViewDataSource, NSTab
@objc private func indicatorPosChanged(_ sender: NSPopUpButton) {
RuleStore.shared.indicatorPosition = IndicatorPosition.allCases[sender.indexOfSelectedItem]
}
@objc private func launchAtLoginToggled(_ sender: NSButton) {
LaunchAtLogin.setEnabled(sender.state == .on)
}
@objc private func hideIconToggled(_ sender: NSButton) {
RuleStore.shared.hideMenuBarIcon = sender.state == .on
}
Expand Down
Loading