Add setting to hide the startup picker with a one-time menu-bar hint
Lets someone skip the startup picker entirely; the first time it would've shown with nothing running, a small popover on the menu bar icon points there instead, then never again. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2 files changed
+81
-4
No files matched your search
@@ -41,6 +41,30 @@ enum MenuClickBehavior {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Whether the startup picker (see AppDelegate.showStartupPicker) should
|
||||||
|
// appear at all when nothing's running at launch. Off by default, so the
|
||||||
|
// existing picker keeps showing up unless someone opts out of it.
|
||||||
|
enum StartupBehavior {
|
||||||
|
private static let hidePickerKey = "HideStartupPicker"
|
||||||
|
|
||||||
|
static var hidePicker: Bool {
|
||||||
|
get { UserDefaults.standard.bool(forKey: hidePickerKey) }
|
||||||
|
set { UserDefaults.standard.set(newValue, forKey: hidePickerKey) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A person who's opted out of the startup picker still needs to discover
|
||||||
|
// the menu bar icon somehow the very first time — this fires once, ever,
|
||||||
|
// the first time that situation actually arises, then never again.
|
||||||
|
private enum MenuBarHintShown {
|
||||||
|
private static let key = "MenuBarHintShown"
|
||||||
|
|
||||||
|
static var value: Bool {
|
||||||
|
get { UserDefaults.standard.bool(forKey: key) }
|
||||||
|
set { UserDefaults.standard.set(newValue, forKey: key) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate, LiveStateDelegate {
|
final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate, LiveStateDelegate {
|
||||||
let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
|
let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
|
||||||
let menu = NSMenu()
|
let menu = NSMenu()
|
||||||
@@ -53,6 +77,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate, LiveSt
|
|||||||
private var accessibilityRecheckScheduled = false
|
private var accessibilityRecheckScheduled = false
|
||||||
private var activationObserver: NSObjectProtocol?
|
private var activationObserver: NSObjectProtocol?
|
||||||
private var appearanceObserver: NSKeyValueObservation?
|
private var appearanceObserver: NSKeyValueObservation?
|
||||||
|
private var menuBarHintPopover: NSPopover?
|
||||||
|
|
||||||
func applicationDidFinishLaunching(_ notification: Notification) {
|
func applicationDidFinishLaunching(_ notification: Notification) {
|
||||||
menu.delegate = self
|
menu.delegate = self
|
||||||
@@ -91,9 +116,15 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate, LiveSt
|
|||||||
|
|
||||||
// Launching this app fresh with nothing running means there's no
|
// Launching this app fresh with nothing running means there's no
|
||||||
// menu-bar title/dots to click yet — offer a picker up front
|
// menu-bar title/dots to click yet — offer a picker up front
|
||||||
// instead of leaving the user to discover the dropdown.
|
// instead of leaving the user to discover the dropdown, unless
|
||||||
|
// that picker's been turned off, in which case a one-time hint
|
||||||
|
// takes its place (see MenuBarHintShown).
|
||||||
if currentInfos.filter(\.running).isEmpty {
|
if currentInfos.filter(\.running).isEmpty {
|
||||||
showStartupPicker(currentInfos)
|
if StartupBehavior.hidePicker {
|
||||||
|
showMenuBarHintIfNeeded()
|
||||||
|
} else {
|
||||||
|
showStartupPicker(currentInfos)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateChecker.checkAutomaticallyIfDue()
|
UpdateChecker.checkAutomaticallyIfDue()
|
||||||
@@ -403,6 +434,41 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate, LiveSt
|
|||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in self?.liveState.reconcile() }
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in self?.liveState.reconcile() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stands in for showStartupPicker when that's been turned off — but
|
||||||
|
// only the very first time nothing's running at launch, ever. After
|
||||||
|
// that, someone who opted out of the picker gets silence, same as
|
||||||
|
// launching with something already running.
|
||||||
|
private func showMenuBarHintIfNeeded() {
|
||||||
|
guard !MenuBarHintShown.value, let button = statusItem.button else { return }
|
||||||
|
MenuBarHintShown.value = true
|
||||||
|
|
||||||
|
let label = NSTextField(wrappingLabelWithString: "Claude isn't running — click here to start a profile.")
|
||||||
|
label.font = .systemFont(ofSize: 13)
|
||||||
|
label.isEditable = false
|
||||||
|
label.isBordered = false
|
||||||
|
label.drawsBackground = false
|
||||||
|
label.preferredMaxLayoutWidth = 200
|
||||||
|
|
||||||
|
let container = NSView()
|
||||||
|
container.addSubview(label)
|
||||||
|
label.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
label.topAnchor.constraint(equalTo: container.topAnchor, constant: 12),
|
||||||
|
label.bottomAnchor.constraint(equalTo: container.bottomAnchor, constant: -12),
|
||||||
|
label.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 14),
|
||||||
|
label.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -14),
|
||||||
|
])
|
||||||
|
|
||||||
|
let viewController = NSViewController()
|
||||||
|
viewController.view = container
|
||||||
|
|
||||||
|
let popover = NSPopover()
|
||||||
|
popover.contentViewController = viewController
|
||||||
|
popover.behavior = .transient
|
||||||
|
menuBarHintPopover = popover
|
||||||
|
popover.show(relativeTo: button.bounds, of: button, preferredEdge: .maxY)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Actions
|
// MARK: - Actions
|
||||||
|
|
||||||
@objc func pick(_ sender: NSMenuItem) {
|
@objc func pick(_ sender: NSMenuItem) {
|
||||||
|
|||||||
@@ -508,6 +508,8 @@ private final class SettingsViewController: NSViewController {
|
|||||||
private let autoUpdateCheckbox = NSButton(
|
private let autoUpdateCheckbox = NSButton(
|
||||||
checkboxWithTitle: "Automatically check for updates", target: nil, action: nil)
|
checkboxWithTitle: "Automatically check for updates", target: nil, action: nil)
|
||||||
private let launchAtLoginCheckbox = NSButton(checkboxWithTitle: "Launch at Login", target: nil, action: nil)
|
private let launchAtLoginCheckbox = NSButton(checkboxWithTitle: "Launch at Login", target: nil, action: nil)
|
||||||
|
private let hideStartupPickerCheckbox = NSButton(
|
||||||
|
checkboxWithTitle: "Hide the startup picker when no profiles are running", target: nil, action: nil)
|
||||||
private let checkNowButton = NSButton(title: "Check for Updates\u{2026}", target: nil, action: nil)
|
private let checkNowButton = NSButton(title: "Check for Updates\u{2026}", target: nil, action: nil)
|
||||||
private let tagStyleLabel = NSTextField(labelWithString: "Window tag:")
|
private let tagStyleLabel = NSTextField(labelWithString: "Window tag:")
|
||||||
private let tagStylePopUp = NSPopUpButton(frame: .zero, pullsDown: false)
|
private let tagStylePopUp = NSPopUpButton(frame: .zero, pullsDown: false)
|
||||||
@@ -564,12 +566,17 @@ private final class SettingsViewController: NSViewController {
|
|||||||
autoUpdateCheckbox.action = #selector(toggleAutoUpdate)
|
autoUpdateCheckbox.action = #selector(toggleAutoUpdate)
|
||||||
autoUpdateCheckbox.state = UpdateChecker.automaticCheckEnabled ? .on : .off
|
autoUpdateCheckbox.state = UpdateChecker.automaticCheckEnabled ? .on : .off
|
||||||
|
|
||||||
|
hideStartupPickerCheckbox.target = self
|
||||||
|
hideStartupPickerCheckbox.action = #selector(toggleHideStartupPicker)
|
||||||
|
hideStartupPickerCheckbox.state = StartupBehavior.hidePicker ? .on : .off
|
||||||
|
|
||||||
checkNowButton.target = self
|
checkNowButton.target = self
|
||||||
checkNowButton.action = #selector(checkNow)
|
checkNowButton.action = #selector(checkNow)
|
||||||
checkNowButton.bezelStyle = .rounded
|
checkNowButton.bezelStyle = .rounded
|
||||||
|
|
||||||
stack = NSStackView(views: [
|
stack = NSStackView(views: [
|
||||||
launchAtLoginCheckbox, clickActionRow, tagStyleRow, autoUpdateCheckbox, checkNowButton,
|
launchAtLoginCheckbox, clickActionRow, tagStyleRow, hideStartupPickerCheckbox,
|
||||||
|
autoUpdateCheckbox, checkNowButton,
|
||||||
])
|
])
|
||||||
stack.orientation = .vertical
|
stack.orientation = .vertical
|
||||||
stack.alignment = .leading
|
stack.alignment = .leading
|
||||||
@@ -610,7 +617,7 @@ private final class SettingsViewController: NSViewController {
|
|||||||
clickActionLabel.intrinsicContentSize.width
|
clickActionLabel.intrinsicContentSize.width
|
||||||
+ clickActionPopUp.intrinsicContentSize.width + clickActionRow.spacing,
|
+ clickActionPopUp.intrinsicContentSize.width + clickActionRow.spacing,
|
||||||
]
|
]
|
||||||
let widest = ([launchAtLoginCheckbox, autoUpdateCheckbox, checkNowButton]
|
let widest = ([launchAtLoginCheckbox, hideStartupPickerCheckbox, autoUpdateCheckbox, checkNowButton]
|
||||||
.map(\.intrinsicContentSize.width) + rowWidths)
|
.map(\.intrinsicContentSize.width) + rowWidths)
|
||||||
.max() ?? 0
|
.max() ?? 0
|
||||||
preferredContentSize = NSSize(width: widest + 40, height: stack.fittingSize.height)
|
preferredContentSize = NSSize(width: widest + 40, height: stack.fittingSize.height)
|
||||||
@@ -648,6 +655,10 @@ private final class SettingsViewController: NSViewController {
|
|||||||
UpdateChecker.automaticCheckEnabled = autoUpdateCheckbox.state == .on
|
UpdateChecker.automaticCheckEnabled = autoUpdateCheckbox.state == .on
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc private func toggleHideStartupPicker() {
|
||||||
|
StartupBehavior.hidePicker = hideStartupPickerCheckbox.state == .on
|
||||||
|
}
|
||||||
|
|
||||||
@objc private func checkNow() {
|
@objc private func checkNow() {
|
||||||
UpdateChecker.checkManually { result in
|
UpdateChecker.checkManually { result in
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
|
|||||||
Reference in new issue
Block a user