The hamburger was landing among the system icons. Move it to the left
edge of the visible group so it reads as the entry point rather than one
more status item.
💘 Generated with Crush
Assisted-by: Crush:deepseek-v4.1-flash
185 lines
6.8 KiB
Swift
185 lines
6.8 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. Kept just below the hider's
|
|
// preferred position so the hamburger lands at the left edge of the
|
|
// visible icons. `debug.hamburgerPos` overrides it.
|
|
if let override = UserDefaults.standard.object(forKey: "debug.hamburgerPos") as? Double {
|
|
UserDefaults.standard.set(override, forKey: positionKey)
|
|
} else if UserDefaults.standard.object(forKey: positionKey) == nil {
|
|
UserDefaults.standard.set(290.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 and give up focus,
|
|
// otherwise the panel stays on top of the menu and our app stays
|
|
// frontmost, which stops transient items (Spotlight's search field)
|
|
// from appearing at all.
|
|
Task { @MainActor [weak self] in
|
|
MenuBarItemManager.shared.onWillPress = {
|
|
self?.popover?.performClose(nil)
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
|
|
NSApp.deactivate()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@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
|
|
}
|
|
}
|
|
}
|