Files
simpletopbar/Sources/SimpleTopBar/App.swift
T
TZGyn e465acba12 Close the panel when a tray item is selected
Selecting a tile opened the app's menu while SimpleTopBar's own panel
stayed on top of it, and could leave menus such as Spotlight stranded
behind it. Dismiss the panel first so the selected menu is unobstructed.

💘 Generated with Crush

Assisted-by: Crush:deepseek-v4.1-flash
2026-09-27 10:26:09 +08:00

176 lines
6.2 KiB
Swift

import SwiftUI
import AppKit
import ServiceManagement
@main
struct SimpleTopBarApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
var body: some Scene {
// The app is a menu bar extra, so it has no real windows.
Settings { EmptyView() }
}
}
/// Owns the menu bar status item directly (instead of `MenuBarExtra`) so the
/// item can be given an `autosaveName` and a persisted preferred position. On a
/// full menu bar, a brand new item lands under the notch and is invisible;
/// writing `NSStatusItem Preferred Position <name>` into defaults puts it in a
/// visible slot, and the autosave name lets the user's own drag persist.
final class AppDelegate: NSObject, NSApplicationDelegate {
private var statusItem: NSStatusItem?
private var popover: NSPopover?
func applicationDidFinishLaunching(_ notification: Notification) {
if CommandLine.arguments.contains("--self-test") {
runSelfTest()
return
}
if CommandLine.arguments.contains("--dump-items") {
dumpMenuBarItems()
return
}
// Single instance: stale copies keep a menu bar icon around after an
// update and swallow clicks meant for the new instance.
let myPid = ProcessInfo.processInfo.processIdentifier
let others = NSRunningApplication.runningApplications(withBundleIdentifier: "dev.tzgyn.simpletopbar")
.filter { $0.processIdentifier != myPid }
for other in others {
other.forceTerminate()
}
AppListModel.shared.start()
MenuBarItemManager.shared.start()
// Create the hamburger before the hider so it is placed in the visible
// part of the bar; the hider then contracts the space around it.
setupStatusItem()
MenuBarHider.shared.prepare()
if CommandLine.arguments.contains("--diag-press") {
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
MenuBarHider.shared.suspend()
}
DispatchQueue.main.asyncAfter(deadline: .now() + 2.25) {
if let item = MenuBarItemManager.enumerate().first(where: {
$0.title.lowercased().contains("wireguard")
}) {
NSLog("diag-press: pressing \(item.appName) after brief reveal")
_ = AXUIElementPerformAction(item.element, kAXPressAction as CFString)
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + 2.45) {
MenuBarHider.shared.resume()
}
DispatchQueue.main.asyncAfter(deadline: .now() + 4.5) {
dumpWindows()
}
return
}
if CommandLine.arguments.contains("--diag") {
DispatchQueue.main.asyncAfter(deadline: .now() + 2.5) {
dumpMenuBarItems()
}
return
}
if CommandLine.arguments.contains("--diag-hide") {
MenuBarHider.shared.enabled = true
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
dumpMenuBarItems()
}
return
}
// The tray is empty without Accessibility permission, so surface the
// system prompt once at launch instead of waiting for the user to open
// the panel and find the button.
if !AccessibilityPermission.isGranted,
!UserDefaults.standard.bool(forKey: "didRequestAccessibility") {
UserDefaults.standard.set(true, forKey: "didRequestAccessibility")
AccessibilityPermission.request()
}
}
func applicationWillTerminate(_ notification: Notification) {
// Never leave the menu bar resized behind us.
MenuBarHider.shared.restore()
}
private func setupStatusItem() {
let name = "SimpleTopBar"
let positionKey = "NSStatusItem Preferred Position \(name)"
// Only seed the position on first run so the user's own drag wins and
// persists through the autosave name.
if UserDefaults.standard.object(forKey: positionKey) == nil {
UserDefaults.standard.set(100.0, forKey: positionKey)
}
let item = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
item.autosaveName = name
if let button = item.button {
let image = NSImage(systemSymbolName: "line.3.horizontal", accessibilityDescription: name)
image?.isTemplate = true
button.image = image
button.target = self
button.action = #selector(togglePopover)
button.setAccessibilityLabel("SimpleTopBar")
}
statusItem = item
let popover = NSPopover()
popover.behavior = .transient
popover.animates = false
popover.contentViewController = NSHostingController(
rootView: TopBarView(
model: AppListModel.shared,
menuBar: MenuBarItemManager.shared,
hider: MenuBarHider.shared
)
)
self.popover = popover
// Selecting a tray item must dismiss our panel, otherwise it stays on
// top of the menu that opens (and can strand menus behind it).
Task { @MainActor [weak self] in
MenuBarItemManager.shared.onWillPress = {
self?.popover?.performClose(nil)
}
}
}
@objc private func togglePopover() {
guard let popover, let button = statusItem?.button else { return }
if popover.isShown {
popover.performClose(nil)
} else {
NSApp.activate(ignoringOtherApps: true)
popover.show(relativeTo: button.bounds, of: button, preferredEdge: .minY)
popover.contentViewController?.view.window?.makeKey()
}
}
}
enum LoginItem {
static var isEnabled: Bool {
SMAppService.mainApp.status == .enabled
}
static func toggle() -> Bool {
let service = SMAppService.mainApp
do {
if service.status == .enabled {
try service.unregister()
} else {
try service.register()
}
return true
} catch {
return false
}
}
}