Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dec8d60aa3 | ||
|
|
5eb2d0f661 |
@@ -38,6 +38,10 @@ executable SwiftPM package, no external dependencies (all Apple frameworks).
|
|||||||
or per-element, so every helper returns `nil` instead of force-unwrapping. The
|
or per-element, so every helper returns `nil` instead of force-unwrapping. The
|
||||||
`enumerate()` routine is `nonisolated static` so `--self-test` can run it.
|
`enumerate()` routine is `nonisolated static` so `--self-test` can run it.
|
||||||
- Items are keyed by `pid-index`; `MenuBarItem` equality ignores the AX element.
|
- Items are keyed by `pid-index`; `MenuBarItem` equality ignores the AX element.
|
||||||
|
- Control Center is special: its extras menu bar lists **every module it owns**,
|
||||||
|
including ones not in the bar, which are parked off-screen at `y≈982`.
|
||||||
|
`enumerate()` drops items whose AX position `y > 60` and de-duplicates by
|
||||||
|
title per app, otherwise the tray fills with identical "Control Center" tiles.
|
||||||
- Reading another app's UI is the only TCC permission needed; there is no Screen
|
- Reading another app's UI is the only TCC permission needed; there is no Screen
|
||||||
Recording or Apple Events usage.
|
Recording or Apple Events usage.
|
||||||
|
|
||||||
@@ -115,7 +119,8 @@ no control over this, so the app creates its own `NSStatusItem` and:
|
|||||||
## Hiding other menu bar items
|
## Hiding other menu bar items
|
||||||
|
|
||||||
`MenuBarHider` creates a zero-width spacer `NSStatusItem` at launch and grows its
|
`MenuBarHider` creates a zero-width spacer `NSStatusItem` at launch and grows its
|
||||||
length to `hiddenLength` (10_000) to push the icons on its left off-screen. Its
|
length to `hiddenLength` (10_000) to push the icons on its left off-screen. It is
|
||||||
|
**on by default** (`hideMenuBarItems` defaults to true, then persisted). Its
|
||||||
preferred position controls where the hidden/kept boundary lands:
|
preferred position controls where the hidden/kept boundary lands:
|
||||||
|
|
||||||
- Seed **250** (default) puts the boundary just left of Wi-Fi, so every
|
- Seed **250** (default) puts the boundary just left of Wi-Fi, so every
|
||||||
@@ -125,6 +130,9 @@ preferred position controls where the hidden/kept boundary lands:
|
|||||||
- Verified with `--diag-hide` across positions 100–400. Below ~200 Wi-Fi gets
|
- Verified with `--diag-hide` across positions 100–400. Below ~200 Wi-Fi gets
|
||||||
hidden too; above ~300 app icons (Now Playing, TextInput) leak back.
|
hidden too; above ~300 app icons (Now Playing, TextInput) leak back.
|
||||||
- `debug.spacerPos` (Double) overrides the seed for tuning without rebuilding.
|
- `debug.spacerPos` (Double) overrides the seed for tuning without rebuilding.
|
||||||
|
- **Order matters**: `AppDelegate` must call `setupStatusItem()` *before*
|
||||||
|
`MenuBarHider.shared.prepare()`. If the hider contracts the bar first, the
|
||||||
|
hamburger is created into the collapsed space and lands off-screen.
|
||||||
- **Wi-Fi/Spotlight/Clock/Battery/Control Center cannot be hidden**: macOS pins
|
- **Wi-Fi/Spotlight/Clock/Battery/Control Center cannot be hidden**: macOS pins
|
||||||
them to the far right and a third-party item can never be inserted to their
|
them to the far right and a third-party item can never be inserted to their
|
||||||
right, so the spacer has nothing to push them with. This is a macOS
|
right, so the spacer has nothing to push them with. This is a macOS
|
||||||
|
|||||||
@@ -28,8 +28,9 @@ still works.
|
|||||||
macOS has no public API to move another app's status item. The **Hide items from
|
macOS has no public API to move another app's status item. The **Hide items from
|
||||||
the menu bar** toggle uses the spacer technique: a zero-width status item is
|
the menu bar** toggle uses the spacer technique: a zero-width status item is
|
||||||
created at launch whose length is expanded to push the other icons off-screen so
|
created at launch whose length is expanded to push the other icons off-screen so
|
||||||
they only live in this panel. It is off by default and always undone on quit; if
|
they only live in this panel. It is **on by default** (the choice is
|
||||||
the app dies, macOS removes the spacer automatically.
|
remembered), always undone on quit, and if the app dies macOS removes the spacer
|
||||||
|
automatically.
|
||||||
|
|
||||||
What it can and cannot hide:
|
What it can and cannot hide:
|
||||||
|
|
||||||
|
|||||||
@@ -43,8 +43,10 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
|||||||
|
|
||||||
AppListModel.shared.start()
|
AppListModel.shared.start()
|
||||||
MenuBarItemManager.shared.start()
|
MenuBarItemManager.shared.start()
|
||||||
MenuBarHider.shared.prepare()
|
// 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()
|
setupStatusItem()
|
||||||
|
MenuBarHider.shared.prepare()
|
||||||
|
|
||||||
if CommandLine.arguments.contains("--diag") {
|
if CommandLine.arguments.contains("--diag") {
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2.5) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 2.5) {
|
||||||
|
|||||||
@@ -68,11 +68,23 @@ final class MenuBarItemManager: ObservableObject {
|
|||||||
guard let bar = extrasMenuBar(of: axApp),
|
guard let bar = extrasMenuBar(of: axApp),
|
||||||
let children = children(of: bar) else { continue }
|
let children = children(of: bar) else { continue }
|
||||||
|
|
||||||
|
var seenTitles = Set<String>()
|
||||||
for (index, child) in children.enumerated() {
|
for (index, child) in children.enumerated() {
|
||||||
|
// Control Center reports every module it owns, including the
|
||||||
|
// ones not in the menu bar, which are parked off-screen around
|
||||||
|
// y=982. Keep only items actually sitting in the bar.
|
||||||
|
if let position = pointAttribute(child, kAXPositionAttribute), position.y > 60 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
let title = stringAttribute(child, kAXDescriptionAttribute)
|
let title = stringAttribute(child, kAXDescriptionAttribute)
|
||||||
?? stringAttribute(child, kAXTitleAttribute)
|
?? stringAttribute(child, kAXTitleAttribute)
|
||||||
?? app.localizedName
|
?? app.localizedName
|
||||||
?? "Item"
|
?? "Item"
|
||||||
|
// Several Control Center modules share the generic title
|
||||||
|
// "Control Center"; show each distinct title once per app.
|
||||||
|
if !seenTitles.insert(title).inserted { continue }
|
||||||
|
|
||||||
found.append(MenuBarItem(
|
found.append(MenuBarItem(
|
||||||
id: "\(pid)-\(index)",
|
id: "\(pid)-\(index)",
|
||||||
pid: pid,
|
pid: pid,
|
||||||
@@ -139,10 +151,10 @@ final class MenuBarItemManager: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 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
|
||||||
/// at launch; expanding its length to a huge value pushes the items to its left
|
/// at launch; expanding its length to a huge value pushes the other apps' icons
|
||||||
/// off the visible menu bar, so they can only be reached through SimpleTopBar's
|
/// off the visible menu bar, so they can only be reached through SimpleTopBar's
|
||||||
/// panel. Reliable only when SimpleTopBar starts at login (its spacer then sits
|
/// panel. On by default; the choice is persisted. Always restored on quit, and
|
||||||
/// to the right of later-launched apps' items). Always restored on quit.
|
/// a crash cleans up automatically because status items die with the process.
|
||||||
@MainActor
|
@MainActor
|
||||||
final class MenuBarHider: ObservableObject {
|
final class MenuBarHider: ObservableObject {
|
||||||
static let shared = MenuBarHider()
|
static let shared = MenuBarHider()
|
||||||
@@ -150,17 +162,24 @@ final class MenuBarHider: ObservableObject {
|
|||||||
/// Width that pushes the neighbouring items off the visible menu bar.
|
/// Width that pushes the neighbouring items off the visible menu bar.
|
||||||
static let hiddenLength: CGFloat = 10_000
|
static let hiddenLength: CGFloat = 10_000
|
||||||
|
|
||||||
|
private static let defaultsKey = "hideMenuBarItems"
|
||||||
|
|
||||||
@Published private(set) var isHiding = false
|
@Published private(set) var isHiding = false
|
||||||
@Published var enabled = false {
|
@Published var enabled: Bool {
|
||||||
didSet {
|
didSet {
|
||||||
guard enabled != oldValue else { return }
|
guard enabled != oldValue else { return }
|
||||||
|
UserDefaults.standard.set(enabled, forKey: Self.defaultsKey)
|
||||||
apply(enabled)
|
apply(enabled)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var spacer: NSStatusItem?
|
private var spacer: NSStatusItem?
|
||||||
|
|
||||||
private init() {}
|
private init() {
|
||||||
|
// Hide by default: the menu bar then shows only SimpleTopBar's hamburger
|
||||||
|
// plus the Apple items macOS will not let us move.
|
||||||
|
enabled = UserDefaults.standard.object(forKey: Self.defaultsKey) as? Bool ?? true
|
||||||
|
}
|
||||||
|
|
||||||
/// Create the spacer early (at launch) so it lands as far right as possible.
|
/// Create the spacer early (at launch) so it lands as far right as possible.
|
||||||
func prepare() {
|
func prepare() {
|
||||||
@@ -179,19 +198,19 @@ final class MenuBarHider: ObservableObject {
|
|||||||
item.autosaveName = name
|
item.autosaveName = name
|
||||||
item.button?.isHidden = true
|
item.button?.isHidden = true
|
||||||
spacer = item
|
spacer = item
|
||||||
|
apply(enabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func apply(_ hiding: Bool) {
|
private func apply(_ hiding: Bool) {
|
||||||
prepare()
|
|
||||||
spacer?.length = hiding ? Self.hiddenLength : 0
|
spacer?.length = hiding ? Self.hiddenLength : 0
|
||||||
isHiding = hiding
|
isHiding = hiding
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Put the menu bar back the way we found it. Called on quit.
|
/// Put the menu bar back the way we found it. Called on quit. Leaves the
|
||||||
|
/// user's preference intact.
|
||||||
func restore() {
|
func restore() {
|
||||||
spacer?.length = 0
|
spacer?.length = 0
|
||||||
isHiding = false
|
isHiding = false
|
||||||
enabled = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user