Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30d5aff906 | ||
|
|
6f55861be7 | ||
|
|
ccc172d218 | ||
|
|
4bc6b333dc | ||
|
|
abb0b548bf | ||
|
|
8d795d0a28 | ||
|
|
e465acba12 | ||
|
|
29157c77fb | ||
|
|
b0671abed8 | ||
|
|
569abe967f | ||
|
|
3e57efe312 |
@@ -103,6 +103,13 @@ wants it in the final answer every time. Keep it to the cask upgrade commands
|
|||||||
(see the README release section for the exact block), because the app is
|
(see the README release section for the exact block), because the app is
|
||||||
installed through the tap.
|
installed through the tap.
|
||||||
|
|
||||||
|
Notarization: `release.sh` submits to Apple when the `SIMPLETOPBAR_NOTARY_*`
|
||||||
|
env vars are set (provider, `.p8` path, key id, issuer id). The credentials are
|
||||||
|
the same App Store Connect key used by Kapture (`~/.config/kapture/AuthKey_*.p8`,
|
||||||
|
team `RX6767S6BW`); the issuer id is a non-secret UUID from App Store Connect.
|
||||||
|
Verify with `spctl -a -vv SimpleTopBar.app` → `source=Notarized Developer ID`.
|
||||||
|
Without the vars it still builds, zips and prints the sha, just unnotarized.
|
||||||
|
|
||||||
## Status item placement (why not MenuBarExtra)
|
## Status item placement (why not MenuBarExtra)
|
||||||
|
|
||||||
On a full menu bar, a newly created status item is placed leftmost, which on a
|
On a full menu bar, a newly created status item is placed leftmost, which on a
|
||||||
@@ -141,6 +148,9 @@ preferred position controls where the hidden/kept boundary lands:
|
|||||||
(`kAXPressAction`, then `kAXShowMenuAction`), and `resume()`s ~0.1s later. The
|
(`kAXPressAction`, then `kAXShowMenuAction`), and `resume()`s ~0.1s later. The
|
||||||
menu is its own window anchored at the item's on-screen spot, so it survives
|
menu is its own window anchored at the item's on-screen spot, so it survives
|
||||||
re-hiding.
|
re-hiding.
|
||||||
|
- `press` first calls `onWillPress`, which `AppDelegate` wires to close the
|
||||||
|
popover **and** `NSApp.deactivate()`. Closing alone left our app frontmost, so
|
||||||
|
transient items (Spotlight's search field) still would not appear.
|
||||||
- **Cannot reveal a single icon.** The spacer hides a contiguous block to its
|
- **Cannot reveal a single icon.** The spacer hides a contiguous block to its
|
||||||
left, so a press can only reveal the whole block. Isolating one item would
|
left, so a press can only reveal the whole block. Isolating one item would
|
||||||
need either per-item ordering control (not available) or covering the icons
|
need either per-item ordering control (not available) or covering the icons
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ collects every other app's menu bar (tray) icons, plus every app running on your
|
|||||||
Mac, into three sections:
|
Mac, into three sections:
|
||||||
|
|
||||||
- **Menu Bar** — a tray of all other apps' menu bar items, discovered through
|
- **Menu Bar** — a tray of all other apps' menu bar items, discovered through
|
||||||
the Accessibility API. Click one to open its real menu. Optionally **hide the
|
the Accessibility API. It is split into **Apps** and **System** sections.
|
||||||
items from the menu bar** so they only live inside this panel (best-effort;
|
Click one to open its real menu. Optionally **hide the items from the menu
|
||||||
see below).
|
bar** so they only live inside this panel (best-effort; see below).
|
||||||
- **Apps** — regular apps that live in the Dock.
|
- **Apps** — regular apps that live in the Dock.
|
||||||
- **Background Apps** — accessory/agent apps without a Dock icon. This section
|
- **Background Apps** — accessory/agent apps without a Dock icon. This section
|
||||||
is a **dropdown**: it stays collapsed to a single header, and when you expand
|
is a **dropdown**: it stays collapsed to a single header, and when you expand
|
||||||
@@ -34,14 +34,12 @@ automatically.
|
|||||||
|
|
||||||
What it can and cannot hide:
|
What it can and cannot hide:
|
||||||
|
|
||||||
- It hides **other third-party apps' icons** (cmux, Kapture, WireGuard, input
|
- It hides **other third-party apps' icons** (cmux, Kapture, moomoo, WireGuard).
|
||||||
source, Now Playing, …) so only your apps' icons leave the bar.
|
- It **keeps Wi-Fi, Now Playing, the input-source indicator, Spotlight, Clock,
|
||||||
- It **keeps Wi-Fi, Spotlight, Clock, Battery and Control Center**. The spacer's
|
Battery and Control Center** visible, with the **hamburger at the left edge of
|
||||||
boundary is tuned (preferred position 250) to fall just left of Wi-Fi, so the
|
that group**. The spacer's boundary (preferred position 330) falls just left
|
||||||
Apple items on the right stay visible. macOS reserves that far-right area: a
|
of the input-source indicator. macOS reserves the far-right area, so an item
|
||||||
third-party item can never be placed to the right of it, so those icons cannot
|
can never be placed to the right of the system icons.
|
||||||
be pushed away even in principle. (Bartender and Ice hit the same wall and
|
|
||||||
cover the area with their own overlay bar instead.)
|
|
||||||
- Newly launched apps are placed to the left of the spacer, so they are hidden
|
- Newly launched apps are placed to the left of the spacer, so they are hidden
|
||||||
automatically.
|
automatically.
|
||||||
- Advanced: set `defaults write dev.tzgyn.simpletopbar debug.spacerPos -float <n>`
|
- Advanced: set `defaults write dev.tzgyn.simpletopbar debug.spacerPos -float <n>`
|
||||||
@@ -57,7 +55,8 @@ brew install --cask simpletopbar
|
|||||||
```
|
```
|
||||||
|
|
||||||
The tap is hosted on Gitea, so the full URL is required. Homebrew may print an
|
The tap is hosted on Gitea, so the full URL is required. Homebrew may print an
|
||||||
"Unsupported tap" warning — it's harmless.
|
"Unsupported tap" warning — it's harmless. Releases from 0.4.8 on are notarized,
|
||||||
|
so the app opens without a Gatekeeper warning.
|
||||||
|
|
||||||
## Upgrade
|
## Upgrade
|
||||||
|
|
||||||
|
|||||||
@@ -103,9 +103,13 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
|||||||
let name = "SimpleTopBar"
|
let name = "SimpleTopBar"
|
||||||
let positionKey = "NSStatusItem Preferred Position \(name)"
|
let positionKey = "NSStatusItem Preferred Position \(name)"
|
||||||
// Only seed the position on first run so the user's own drag wins and
|
// Only seed the position on first run so the user's own drag wins and
|
||||||
// persists through the autosave name.
|
// persists through the autosave name. Kept just below the hider's
|
||||||
if UserDefaults.standard.object(forKey: positionKey) == nil {
|
// preferred position so the hamburger lands at the left edge of the
|
||||||
UserDefaults.standard.set(100.0, forKey: positionKey)
|
// 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(320.0, forKey: positionKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
let item = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
|
let item = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
|
||||||
@@ -132,6 +136,19 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
self.popover = popover
|
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() {
|
@objc private func togglePopover() {
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ struct MenuBarItem: Identifiable, Equatable {
|
|||||||
let appName: String
|
let appName: String
|
||||||
let title: String
|
let title: String
|
||||||
let icon: NSImage
|
let icon: NSImage
|
||||||
|
/// True for Apple's own items (Control Center, Spotlight, …).
|
||||||
|
let isSystem: Bool
|
||||||
|
/// Frame in Accessibility coordinates (origin top-left of the main screen).
|
||||||
|
let frame: CGRect?
|
||||||
let element: AXUIElement
|
let element: AXUIElement
|
||||||
|
|
||||||
static func == (lhs: MenuBarItem, rhs: MenuBarItem) -> Bool {
|
static func == (lhs: MenuBarItem, rhs: MenuBarItem) -> Bool {
|
||||||
@@ -28,6 +32,9 @@ final class MenuBarItemManager: ObservableObject {
|
|||||||
|
|
||||||
private var observers: [NSObjectProtocol] = []
|
private var observers: [NSObjectProtocol] = []
|
||||||
|
|
||||||
|
/// Called just before pressing an item so the owner can close its panel.
|
||||||
|
var onWillPress: (() -> Void)?
|
||||||
|
|
||||||
private init() {}
|
private init() {}
|
||||||
|
|
||||||
func start() {
|
func start() {
|
||||||
@@ -70,10 +77,11 @@ final class MenuBarItemManager: ObservableObject {
|
|||||||
|
|
||||||
var seenTitles = Set<String>()
|
var seenTitles = Set<String>()
|
||||||
for (index, child) in children.enumerated() {
|
for (index, child) in children.enumerated() {
|
||||||
|
let position = pointAttribute(child, kAXPositionAttribute)
|
||||||
// Control Center reports every module it owns, including the
|
// Control Center reports every module it owns, including the
|
||||||
// ones not in the menu bar, which are parked off-screen around
|
// ones not in the menu bar, which are parked off-screen around
|
||||||
// y=982. Keep only items actually sitting in the bar.
|
// y=982. Keep only items actually sitting in the bar.
|
||||||
if let position = pointAttribute(child, kAXPositionAttribute), position.y > 60 {
|
if let position, position.y > 60 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,12 +93,22 @@ final class MenuBarItemManager: ObservableObject {
|
|||||||
// "Control Center"; show each distinct title once per app.
|
// "Control Center"; show each distinct title once per app.
|
||||||
if !seenTitles.insert(title).inserted { continue }
|
if !seenTitles.insert(title).inserted { continue }
|
||||||
|
|
||||||
|
let size = sizeAttribute(child, kAXSizeAttribute)
|
||||||
|
let frame: CGRect?
|
||||||
|
if let position, let size {
|
||||||
|
frame = CGRect(origin: position, size: size)
|
||||||
|
} else {
|
||||||
|
frame = nil
|
||||||
|
}
|
||||||
|
|
||||||
found.append(MenuBarItem(
|
found.append(MenuBarItem(
|
||||||
id: "\(pid)-\(index)",
|
id: "\(pid)-\(index)",
|
||||||
pid: pid,
|
pid: pid,
|
||||||
appName: app.localizedName ?? app.bundleIdentifier ?? "Unknown",
|
appName: app.localizedName ?? app.bundleIdentifier ?? "Unknown",
|
||||||
title: title,
|
title: title,
|
||||||
icon: Self.icon(forTitle: title, app: app),
|
icon: Self.icon(forTitle: title, app: app),
|
||||||
|
isSystem: Self.isSystemBundle(app.bundleIdentifier),
|
||||||
|
frame: frame,
|
||||||
element: child
|
element: child
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
@@ -110,6 +128,10 @@ final class MenuBarItemManager: ObservableObject {
|
|||||||
/// hide again shortly after. The reveal is momentary rather than lasting the
|
/// hide again shortly after. The reveal is momentary rather than lasting the
|
||||||
/// whole time the panel is open.
|
/// whole time the panel is open.
|
||||||
func press(_ item: MenuBarItem) {
|
func press(_ item: MenuBarItem) {
|
||||||
|
// Close our own panel first, otherwise it stays open on top of the
|
||||||
|
// menu that is about to appear (and can leave menus behind it).
|
||||||
|
onWillPress?()
|
||||||
|
|
||||||
let hider = MenuBarHider.shared
|
let hider = MenuBarHider.shared
|
||||||
let needsReveal = hider.isHiding
|
let needsReveal = hider.isHiding
|
||||||
guard needsReveal else {
|
guard needsReveal else {
|
||||||
@@ -117,23 +139,48 @@ final class MenuBarItemManager: ObservableObject {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
hider.suspend()
|
hider.suspend()
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
|
||||||
_ = self.performPress(item)
|
_ = self.performPress(item)
|
||||||
// The menu is its own window, positioned at the item's on-screen
|
// The menu is its own window, positioned at the item's on-screen
|
||||||
// spot, so the bar can be re-hidden almost immediately.
|
// spot, so the bar can be re-hidden shortly after.
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
|
||||||
hider.resume()
|
hider.resume()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func performPress(_ item: MenuBarItem) -> Bool {
|
private func performPress(_ item: MenuBarItem) -> Bool {
|
||||||
|
// Spotlight's status item accepts the accessibility press action but
|
||||||
|
// does nothing with it; it needs a genuine click.
|
||||||
|
if item.appName.lowercased().contains("spotlight"), let frame = item.frame {
|
||||||
|
postClick(at: CGPoint(x: frame.midX, y: frame.midY))
|
||||||
|
return true
|
||||||
|
}
|
||||||
if AXUIElementPerformAction(item.element, kAXPressAction as CFString) == .success {
|
if AXUIElementPerformAction(item.element, kAXPressAction as CFString) == .success {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return AXUIElementPerformAction(item.element, kAXShowMenuAction as CFString) == .success
|
return AXUIElementPerformAction(item.element, kAXShowMenuAction as CFString) == .success
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Posts a real left click at a point in Accessibility/global coordinates.
|
||||||
|
private func postClick(at point: CGPoint) {
|
||||||
|
let source = CGEventSource(stateID: .hidSystemState)
|
||||||
|
let down = CGEvent(mouseEventSource: source, mouseType: .leftMouseDown, mouseCursorPosition: point, mouseButton: .left)
|
||||||
|
let up = CGEvent(mouseEventSource: source, mouseType: .leftMouseUp, mouseCursorPosition: point, mouseButton: .left)
|
||||||
|
down?.post(tap: .cghidEventTap)
|
||||||
|
up?.post(tap: .cghidEventTap)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Classification
|
||||||
|
|
||||||
|
/// Apple's own menu bar items (Control Center, Spotlight, the input-source
|
||||||
|
/// agent, …) live under `com.apple.*` bundles; everything else is a third
|
||||||
|
/// party app.
|
||||||
|
nonisolated static func isSystemBundle(_ bundleID: String?) -> Bool {
|
||||||
|
guard let bundleID else { return false }
|
||||||
|
return bundleID.hasPrefix("com.apple.")
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Icons
|
// MARK: - Icons
|
||||||
|
|
||||||
/// Picks an icon for a menu bar item. Control Center owns many unrelated
|
/// Picks an icon for a menu bar item. Control Center owns many unrelated
|
||||||
@@ -215,6 +262,16 @@ final class MenuBarItemManager: ObservableObject {
|
|||||||
guard AXValueGetValue(value as! AXValue, .cgPoint, &point) else { return nil }
|
guard AXValueGetValue(value as! AXValue, .cgPoint, &point) else { return nil }
|
||||||
return point
|
return point
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nonisolated static func sizeAttribute(_ element: AXUIElement, _ attribute: String) -> CGSize? {
|
||||||
|
var value: CFTypeRef?
|
||||||
|
guard AXUIElementCopyAttributeValue(element, attribute as CFString, &value) == .success,
|
||||||
|
let value,
|
||||||
|
CFGetTypeID(value) == AXValueGetTypeID() else { return nil }
|
||||||
|
var size = CGSize.zero
|
||||||
|
guard AXValueGetValue(value as! AXValue, .cgSize, &size) else { return nil }
|
||||||
|
return size
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Best-effort menu bar item hiding. A zero-width spacer status item is created
|
/// Best-effort menu bar item hiding. A zero-width spacer status item is created
|
||||||
@@ -256,10 +313,15 @@ final class MenuBarHider: ObservableObject {
|
|||||||
// The boundary between "hidden" and "kept" falls just left of Wi-Fi at
|
// The boundary between "hidden" and "kept" falls just left of Wi-Fi at
|
||||||
// ~250, so the other apps hide while Apple's Wi-Fi/Clock/Battery/Control
|
// ~250, so the other apps hide while Apple's Wi-Fi/Clock/Battery/Control
|
||||||
// Center/Spotlight stay visible. `debug.spacerPos` overrides the seed.
|
// Center/Spotlight stay visible. `debug.spacerPos` overrides the seed.
|
||||||
|
// The boundary sits just left of the input-source indicator, so every
|
||||||
|
// system item (input source, Now Playing, Wi-Fi, …) stays visible while
|
||||||
|
// the apps hide. It must stay *above* the hamburger's preferred position
|
||||||
|
// (see AppDelegate) for the hamburger to remain on the visible side.
|
||||||
|
// `debug.spacerPos` overrides the seed.
|
||||||
if let override = UserDefaults.standard.object(forKey: "debug.spacerPos") as? Double {
|
if let override = UserDefaults.standard.object(forKey: "debug.spacerPos") as? Double {
|
||||||
UserDefaults.standard.set(override, forKey: key)
|
UserDefaults.standard.set(override, forKey: key)
|
||||||
} else if UserDefaults.standard.object(forKey: key) == nil {
|
} else if UserDefaults.standard.object(forKey: key) == nil {
|
||||||
UserDefaults.standard.set(250.0, forKey: key)
|
UserDefaults.standard.set(330.0, forKey: key)
|
||||||
}
|
}
|
||||||
let item = NSStatusBar.system.statusItem(withLength: 0)
|
let item = NSStatusBar.system.statusItem(withLength: 0)
|
||||||
item.autosaveName = name
|
item.autosaveName = name
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ func dumpMenuBarItems() {
|
|||||||
lines.append("count=\(items.count)")
|
lines.append("count=\(items.count)")
|
||||||
for item in items {
|
for item in items {
|
||||||
let pos = MenuBarItemManager.pointAttribute(item.element, kAXPositionAttribute)
|
let pos = MenuBarItemManager.pointAttribute(item.element, kAXPositionAttribute)
|
||||||
lines.append("\(item.appName) | \(item.title) | pid=\(item.pid) | id=\(item.id) | pos=\(pos.map { "\(Int($0.x)),\(Int($0.y))" } ?? "?")")
|
lines.append("\(item.appName) | \(item.title) | pid=\(item.pid) | id=\(item.id) | system=\(item.isSystem) | pos=\(pos.map { "\(Int($0.x)),\(Int($0.y))" } ?? "?")")
|
||||||
}
|
}
|
||||||
let own = MenuBarItemManager.enumerate(includeOwn: true).filter {
|
let own = MenuBarItemManager.enumerate(includeOwn: true).filter {
|
||||||
$0.pid == ProcessInfo.processInfo.processIdentifier
|
$0.pid == ProcessInfo.processInfo.processIdentifier
|
||||||
@@ -103,9 +103,9 @@ func runSelfTest() {
|
|||||||
check(MenuBarItemManager.children(of: systemWide) == nil, "no children on a non-app element")
|
check(MenuBarItemManager.children(of: systemWide) == nil, "no children on a non-app element")
|
||||||
|
|
||||||
let element = AXUIElementCreateSystemWide()
|
let element = AXUIElementCreateSystemWide()
|
||||||
let first = MenuBarItem(id: "1-0", pid: 1, appName: "A", title: "T", icon: NSImage(), element: element)
|
let first = MenuBarItem(id: "1-0", pid: 1, appName: "A", title: "T", icon: NSImage(), isSystem: false, frame: nil, element: element)
|
||||||
let same = MenuBarItem(id: "1-0", pid: 1, appName: "A", title: "T", icon: NSImage(), element: element)
|
let same = MenuBarItem(id: "1-0", pid: 1, appName: "A", title: "T", icon: NSImage(), isSystem: false, frame: nil, element: element)
|
||||||
let different = MenuBarItem(id: "1-1", pid: 1, appName: "A", title: "T", icon: NSImage(), element: element)
|
let different = MenuBarItem(id: "1-1", pid: 1, appName: "A", title: "T", icon: NSImage(), isSystem: false, frame: nil, element: element)
|
||||||
check(first == same, "menu bar items compare by id and title")
|
check(first == same, "menu bar items compare by id and title")
|
||||||
check(first != different, "different ids are not equal")
|
check(first != different, "different ids are not equal")
|
||||||
|
|
||||||
@@ -117,6 +117,11 @@ func runSelfTest() {
|
|||||||
check(MenuBarItemManager.symbolName(for: "Search") == "magnifyingglass", "search maps to magnifier symbol")
|
check(MenuBarItemManager.symbolName(for: "Search") == "magnifyingglass", "search maps to magnifier symbol")
|
||||||
check(MenuBarItemManager.symbolName(for: "cmux") == nil, "unknown titles fall back to the app icon")
|
check(MenuBarItemManager.symbolName(for: "cmux") == nil, "unknown titles fall back to the app icon")
|
||||||
|
|
||||||
|
// Tray sections: Apple bundles are "system", everything else is an app.
|
||||||
|
check(MenuBarItemManager.isSystemBundle("com.apple.controlcenter"), "apple bundles classify as system")
|
||||||
|
check(!MenuBarItemManager.isSystemBundle("com.cmux.app"), "third-party bundles classify as apps")
|
||||||
|
check(!MenuBarItemManager.isSystemBundle(nil), "unknown bundles are treated as apps")
|
||||||
|
|
||||||
let authorized = AXIsProcessTrusted()
|
let authorized = AXIsProcessTrusted()
|
||||||
print("accessibility authorized=\(authorized)")
|
print("accessibility authorized=\(authorized)")
|
||||||
if authorized {
|
if authorized {
|
||||||
|
|||||||
@@ -60,23 +60,6 @@ struct TopBarView: View {
|
|||||||
/// The hamburger tray: every other app's menu bar item lives here.
|
/// The hamburger tray: every other app's menu bar item lives here.
|
||||||
private var menuBarSection: some View {
|
private var menuBarSection: some View {
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
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 {
|
if !menuBar.authorized {
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
Text("Grant Accessibility to collect and open menu bar items.")
|
Text("Grant Accessibility to collect and open menu bar items.")
|
||||||
@@ -98,12 +81,9 @@ struct TopBarView: View {
|
|||||||
.padding(.horizontal, 12)
|
.padding(.horizontal, 12)
|
||||||
.padding(.vertical, 4)
|
.padding(.vertical, 4)
|
||||||
} else {
|
} else {
|
||||||
LazyVGrid(columns: [GridItem(.adaptive(minimum: 34), spacing: 6)], spacing: 6) {
|
trayGroup(title: "Apps", items: appTrayItems)
|
||||||
ForEach(menuBar.items) { item in
|
Divider().padding(.vertical, 2)
|
||||||
MenuBarTile(item: item, menuBar: menuBar)
|
trayGroup(title: "System", items: systemTrayItems)
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 12)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if menuBar.authorized {
|
if menuBar.authorized {
|
||||||
@@ -115,7 +95,53 @@ struct TopBarView: View {
|
|||||||
.controlSize(.mini)
|
.controlSize(.mini)
|
||||||
.padding(.horizontal, 12)
|
.padding(.horizontal, 12)
|
||||||
.padding(.top, 6)
|
.padding(.top, 6)
|
||||||
.help("Best-effort: pushes other apps' icons off the menu bar so they only live here. Apple's Wi-Fi, Spotlight, Clock, Battery and Control Center stay visible; macOS won't let any third-party app move them.")
|
.help("Best-effort: pushes other apps' icons off the menu bar so they only live here. Apple's system items (Wi-Fi, Now Playing, Clock, Battery, Control Center, Spotlight) stay visible; macOS won't let any third-party app move them.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Third-party apps' menu bar icons.
|
||||||
|
private var appTrayItems: [MenuBarItem] {
|
||||||
|
menuBar.items.filter { !$0.isSystem }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apple's own menu bar items (Control Center, Spotlight, …).
|
||||||
|
private var systemTrayItems: [MenuBarItem] {
|
||||||
|
menuBar.items.filter { $0.isSystem }
|
||||||
|
}
|
||||||
|
|
||||||
|
private func trayGroup(title: String, items: [MenuBarItem]) -> some View {
|
||||||
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
|
HStack(spacing: 6) {
|
||||||
|
Text(title.uppercased())
|
||||||
|
.font(.caption2)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
Spacer()
|
||||||
|
Text("\(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 items.isEmpty {
|
||||||
|
Text("None")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.tertiary)
|
||||||
|
.padding(.horizontal, 12)
|
||||||
|
.padding(.bottom, 4)
|
||||||
|
} else {
|
||||||
|
LazyVGrid(columns: [GridItem(.adaptive(minimum: 34), spacing: 6)], spacing: 6) {
|
||||||
|
ForEach(items) { item in
|
||||||
|
MenuBarTile(item: item, menuBar: menuBar)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 12)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -2,7 +2,7 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
cd "$(dirname "$0")"
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
VERSION="${1:-0.2.3}"
|
VERSION="${1:-0.4.8}"
|
||||||
|
|
||||||
swift build -c release
|
swift build -c release
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ chmod +x scripts/make-icon.sh
|
|||||||
|
|
||||||
if [ -n "$IDENT" ]; then
|
if [ -n "$IDENT" ]; then
|
||||||
security unlock-keychain "$HOME/Library/Keychains/login.keychain-db" 2>/dev/null || true
|
security unlock-keychain "$HOME/Library/Keychains/login.keychain-db" 2>/dev/null || true
|
||||||
codesign --force --options runtime --sign "$IDENT" "$APP"
|
codesign --force --options runtime --timestamp --sign "$IDENT" "$APP"
|
||||||
echo "Built $APP ${VERSION} (signed with Developer ID '$IDENT') — open $APP"
|
echo "Built $APP ${VERSION} (signed with Developer ID '$IDENT') — open $APP"
|
||||||
else
|
else
|
||||||
codesign --force --sign - "$APP"
|
codesign --force --sign - "$APP"
|
||||||
|
|||||||
Reference in New Issue
Block a user