Collect other apps' menu bar items into a hamburger panel

The menu bar icon is now a hamburger whose panel gathers every other
app's status item via the Accessibility API, so tray icons no longer
need to be hunted down across the menu bar. Items can be opened from
the panel, and a best-effort toggle pushes them off the real menu bar
so they only live there.

💘 Generated with Crush

Assisted-by: Crush:deepseek-v4.1-flash
This commit is contained in:
2026-09-26 23:02:28 +08:00
parent 41a5917b42
commit 373fda738e
7 changed files with 365 additions and 12 deletions
+99 -3
View File
@@ -1,9 +1,11 @@
import SwiftUI
import AppKit
/// The top bar panel shown from the menu bar icon.
/// The top bar panel shown from the menu bar hamburger icon.
struct TopBarView: View {
@ObservedObject var model: AppListModel
@ObservedObject var menuBar: MenuBarItemManager
@ObservedObject var hider: MenuBarHider
var body: some View {
VStack(spacing: 0) {
@@ -11,6 +13,10 @@ struct TopBarView: View {
Divider()
ScrollView {
VStack(alignment: .leading, spacing: 0) {
menuBarSection
Divider().padding(.vertical, 4)
section(title: "Apps", apps: model.regularApps, emptyText: "No apps running")
Divider().padding(.vertical, 4)
@@ -19,23 +25,28 @@ struct TopBarView: View {
}
.padding(.vertical, 4)
}
.frame(maxHeight: 360)
.frame(maxHeight: 420)
Divider()
footer
}
.frame(width: 320)
.onAppear {
model.refresh()
menuBar.refresh()
}
}
private var header: some View {
HStack(spacing: 8) {
Image(systemName: "menubar.rectangle")
Image(systemName: "line.3.horizontal")
.foregroundStyle(.secondary)
Text("SimpleTopBar")
.font(.headline)
Spacer()
Button {
model.refresh()
menuBar.refresh()
} label: {
Image(systemName: "arrow.clockwise")
}
@@ -46,6 +57,69 @@ struct TopBarView: View {
.padding(.vertical, 8)
}
/// The hamburger tray: every other app's menu bar item lives here.
private var menuBarSection: some View {
VStack(alignment: .leading, spacing: 0) {
HStack(spacing: 6) {
Text("MENU BAR")
.font(.caption2)
.fontWeight(.semibold)
.foregroundStyle(.secondary)
Spacer()
Text("\(menuBar.items.count)")
.font(.caption2)
.foregroundStyle(.tertiary)
.padding(.horizontal, 6)
.padding(.vertical, 1)
.background(Capsule().fill(Color.secondary.opacity(0.15)))
}
.padding(.horizontal, 12)
.padding(.top, 4)
.padding(.bottom, 4)
if !menuBar.authorized {
VStack(alignment: .leading, spacing: 4) {
Text("Grant Accessibility to collect and open menu bar items.")
.font(.caption)
.foregroundStyle(.secondary)
Button("Grant Accessibility…") {
AccessibilityPermission.request()
menuBar.refresh()
}
.buttonStyle(.borderedProminent)
.controlSize(.small)
}
.padding(.horizontal, 12)
.padding(.vertical, 4)
} else if menuBar.items.isEmpty {
Text("No menu bar items found")
.font(.caption)
.foregroundStyle(.tertiary)
.padding(.horizontal, 12)
.padding(.vertical, 4)
} else {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 34), spacing: 6)], spacing: 6) {
ForEach(menuBar.items) { item in
MenuBarTile(item: item, menuBar: menuBar)
}
}
.padding(.horizontal, 12)
}
if menuBar.authorized {
Toggle(isOn: $hider.enabled) {
Text("Hide items from the menu bar")
.font(.caption)
}
.toggleStyle(.switch)
.controlSize(.mini)
.padding(.horizontal, 12)
.padding(.top, 6)
.help("Best-effort: pushes the real items off the menu bar so they only live here. Reliable when SimpleTopBar starts at login.")
}
}
}
private func section(title: String, apps: [RunningApp], emptyText: String) -> some View {
VStack(alignment: .leading, spacing: 0) {
Text(title.uppercased())
@@ -163,6 +237,28 @@ struct TopBarView: View {
}
}
private struct MenuBarTile: View {
let item: MenuBarItem
@ObservedObject var menuBar: MenuBarItemManager
var body: some View {
Button {
menuBar.press(item)
} label: {
Image(nsImage: item.appIcon)
.resizable()
.frame(width: 20, height: 20)
.padding(6)
.background(
RoundedRectangle(cornerRadius: 6)
.fill(Color.secondary.opacity(0.12))
)
}
.buttonStyle(.plain)
.help("\(item.appName) — \(item.title)")
}
}
private struct AppRow: View {
let app: RunningApp
@ObservedObject var model: AppListModel