feat: add launch-at-login settings and Sunset styling

This commit is contained in:
2026-10-12 13:59:40 +02:00
parent 37f6cab256
commit fa4cf9c154
2 changed files with 89 additions and 6 deletions
+4 -2
View File
@@ -55,9 +55,9 @@ Activation uses the native macOS hotkey API, without an Accessibility permission
## Launch at login
Add `/Applications/Conjure.app` in **System Settings → General → Login Items & Extensions → Open at Login**. Conjure recognizes the native [login-launch Apple event](https://developer.apple.com/documentation/coreservices/1556410-launch_apple_event_constants) and starts without showing a window or taking focus, while still registering its saved global shortcut. Opening it manually from Finder still shows the launcher, including when it is already running.
Open Settings with `⌘,` and enable **Launch at login**. Conjure uses macOS's native login-item service and reads its actual registration status rather than storing a separate preference. If macOS requires approval, Settings explains this and offers **Open Login Items Settings…**. Registration errors are shown beside the control, and the checkbox returns to the actual system state. Install Conjure in `/Applications` before enabling it; disable the toggle to unregister it.
If shortcut registration fails at login, the error is retained and shown in Settings when you next open Conjure manually. Normal manual launches continue to report startup errors immediately. Conjure does not manage the login-item setting itself.
Conjure recognizes the native [login-launch Apple event](https://developer.apple.com/documentation/coreservices/1556410-launch_apple_event_constants) and starts without showing a window or taking focus, while still registering its saved global shortcut. Opening it manually from Finder still shows the launcher, including when it is already running. If shortcut registration fails at login, the error is retained and shown in Settings when you next open Conjure manually. Normal manual launches continue to report startup errors immediately.
## Discovery and search
@@ -79,6 +79,8 @@ The launcher keeps its Raycast-inspired 750 × 474-point, titlebar-free rounded
The application icon uses the same orange `sparkles` SF Symbol on a self-contained, rounded dark-brown tile. `Scripts/icon.swift` renders every standard and Retina icon size, and `make build` packages the resulting `.icns` in the signed bundle for Finder and other system icon surfaces. Conjure remains an accessory app; adding an application icon does not add a Dock or app-switcher entry.
Settings shares the launcher's dark appearance, frosted HUD material, Sunset background glows, cream headings, and orange control accents. Native window controls and shortcut recording behavior remain available.
The brown tint is 35% opaque over an active, behind-window `NSVisualEffectView` using the native HUD material. This exposes macOS's blurred backdrop while retaining the warm palette and glows; the system material supplies an opaque fallback when Reduce Transparency is enabled. Both the panel and launch pill use native rounded alpha masks alongside layer clipping, keeping material inside its rounded outline and making the window shadow follow the same shape.
AppKit still provides text editing, table navigation, scrolling, application icons, controls, menus, fonts, and visual-effect materials. The background uses cached native radial gradients that fade to transparent: static soft blobs, without animations, timers, image assets, blur filters, or third-party UI dependencies. Decorative drawing does not intercept clicks. Increased-contrast settings strengthen the selection outline, and text colors are tuned for readability across the glows and selection.
+85 -4
View File
@@ -1,5 +1,6 @@
import AppKit
import ConjureKit
import ServiceManagement
@MainActor
final class SettingsWindowController: NSWindowController, NSWindowDelegate {
@@ -8,16 +9,24 @@ final class SettingsWindowController: NSWindowController, NSWindowDelegate {
private let clearButton = NSButton(title: "Clear", target: nil, action: nil)
private let statusLabel = NSTextField(wrappingLabelWithString: "")
private let errorLabel = NSTextField(wrappingLabelWithString: "")
private let loginToggle = NSButton(checkboxWithTitle: "Launch at login", target: nil, action: nil)
private let loginStatus = NSTextField(wrappingLabelWithString: "")
private let loginSettings = NSButton(title: "Open Login Items Settings…", target: nil, action: nil)
init(shortcut: GlobalShortcut) {
self.shortcut = shortcut
let window = ShortcutSettingsWindow(
contentRect: NSRect(x: 0, y: 0, width: 580, height: 520),
contentRect: NSRect(x: 0, y: 0, width: 620, height: 640),
styleMask: [.titled, .closable, .miniaturizable],
backing: .buffered,
defer: false
)
window.title = "Conjure Settings"
window.appearance = NSAppearance(named: .darkAqua)
window.titlebarAppearsTransparent = true
window.backgroundColor = .clear
window.isOpaque = false
window.isMovableByWindowBackground = true
window.isReleasedWhenClosed = false
window.center()
super.init(window: window)
@@ -43,6 +52,10 @@ final class SettingsWindowController: NSWindowController, NSWindowDelegate {
window?.makeKeyAndOrderFront(nil)
}
func windowDidBecomeKey(_ notification: Notification) {
refreshLogin()
}
func windowDidResignKey(_ notification: Notification) {
cancelRecording()
}
@@ -94,15 +107,44 @@ final class SettingsWindowController: NSWindowController, NSWindowDelegate {
)
keyboardSettings.bezelStyle = .rounded
loginToggle.target = self
loginToggle.action = #selector(toggleLogin)
loginSettings.target = self
loginSettings.action = #selector(openLoginSettings)
loginSettings.bezelStyle = .rounded
loginStatus.setAccessibilityLabel("Launch at login status")
heading.font = .systemFont(ofSize: 20, weight: .semibold)
heading.textColor = SunsetPalette.text
for label in [instructions, statusLabel, spotlight, loginStatus] {
label.textColor = SunsetPalette.secondaryText
}
for button in [recorder, commandSpaceButton, clearButton, keyboardSettings, loginToggle, loginSettings] {
button.contentTintColor = SunsetPalette.accent
}
let loginHeading = NSTextField(labelWithString: "Startup")
loginHeading.font = .systemFont(ofSize: 20, weight: .semibold)
loginHeading.textColor = SunsetPalette.text
let divider = NSBox()
divider.boxType = .separator
let stack = NSStackView(views: [
heading, controls, instructions, statusLabel, errorLabel, spotlight, keyboardSettings
heading, controls, instructions, statusLabel, errorLabel, spotlight, keyboardSettings,
divider, loginHeading, loginToggle, loginStatus, loginSettings
])
stack.orientation = .vertical
stack.alignment = .leading
stack.spacing = 12
stack.translatesAutoresizingMaskIntoConstraints = false
let content = NSView()
let content = NSVisualEffectView()
content.material = .hudWindow
content.blendingMode = .behindWindow
content.state = .active
window.contentView = content
let background = LauncherBackground(frame: content.bounds)
background.autoresizingMask = [.width, .height]
background.setAccessibilityElement(false)
content.addSubview(background)
content.addSubview(stack)
NSLayoutConstraint.activate([
stack.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 24),
@@ -111,12 +153,51 @@ final class SettingsWindowController: NSWindowController, NSWindowDelegate {
stack.bottomAnchor.constraint(lessThanOrEqualTo: content.bottomAnchor, constant: -24),
recorder.widthAnchor.constraint(greaterThanOrEqualToConstant: 190)
])
for label in [instructions, statusLabel, errorLabel, spotlight] {
for label in [instructions, statusLabel, errorLabel, spotlight, loginStatus] {
label.widthAnchor.constraint(equalTo: stack.widthAnchor).isActive = true
}
}
private func refreshLogin() {
let status = SMAppService.mainApp.status
loginToggle.state = (status == .enabled || status == .requiresApproval) ? .on : .off
loginSettings.isHidden = status != .requiresApproval
switch status {
case .enabled:
loginStatus.stringValue = "Conjure starts in the background when you log in."
case .requiresApproval:
loginStatus.stringValue = "Allow Conjure in System Settings → General → Login Items & Extensions."
case .notRegistered:
loginStatus.stringValue = "Start Conjure automatically so your global shortcut is available."
case .notFound:
loginStatus.stringValue = "Install Conjure in Applications before enabling launch at login."
@unknown default:
loginStatus.stringValue = "Launch at login status is unavailable."
}
}
@objc private func toggleLogin() {
cancelRecording()
do {
if loginToggle.state == .on {
try SMAppService.mainApp.register()
} else {
try SMAppService.mainApp.unregister()
}
refreshLogin()
} catch {
refreshLogin()
loginStatus.stringValue = "Could not change launch at login: \(error.localizedDescription)"
}
}
@objc private func openLoginSettings() {
cancelRecording()
SMAppService.openSystemSettingsLoginItems()
}
private func refresh() {
refreshLogin()
let name = shortcut.shortcut?.displayName
recorder.title = recorder.isRecording ? "Type shortcut…" : (name ?? "Record Shortcut…")
recorder.setAccessibilityValue(name ?? "Not set")