3 Commits
Author SHA1 Message Date
TZGyn 5eb2d0f661 Stop listing Control Center's off-screen module duplicates
Control Center reports every module it owns, so the tray filled with
identical tiles for items that are not actually in the menu bar. Keep
only items whose accessibility position is in the bar and de-duplicate
by title, so each tray icon appears once.

💘 Generated with Crush

Assisted-by: Crush:deepseek-v4.1-flash
2026-09-27 02:11:57 +08:00
TZGyn 5a4e2fbda2 Keep Wi-Fi visible when hiding other menu bar icons
Hiding pushed Wi-Fi off along with the third-party app icons. Tune the
spacer boundary to sit just left of Wi-Fi so apps disappear while the
Apple Wi-Fi, Spotlight, Clock, Battery and Control Center icons remain.

💘 Generated with Crush

Assisted-by: Crush:deepseek-v4.1-flash
2026-09-27 00:39:00 +08:00
TZGyn 92a57308b7 Explain which menu bar items can be hidden
Hiding other apps' icons worked, but Apple pins Spotlight, Clock, Battery
and Control Center to the right of the menu bar, so a third-party item can
never be placed after them to push them away. Document the real limit so
the behaviour is not mistaken for a bug.

💘 Generated with Crush

Assisted-by: Crush:deepseek-v4.1-flash
2026-09-27 00:35:05 +08:00
6 changed files with 74 additions and 7 deletions
+22
View File
@@ -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.
@@ -112,6 +116,24 @@ no control over this, so the app creates its own `NSStatusItem` and:
`/tmp/simpletopbar-items.txt`; use it to verify the icon is not under the notch `/tmp/simpletopbar-items.txt`; use it to verify the icon is not under the notch
(x should be ~1200, not ~765). (x should be ~1200, not ~765).
## Hiding other menu bar items
`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
preferred position controls where the hidden/kept boundary lands:
- Seed **250** (default) puts the boundary just left of Wi-Fi, so every
third-party app icon goes to x < 0 (cmux, Kapture, moomoo, WireGuard,
TextInputMenuAgent, Now Playing) while **Wi-Fi (x≈1173), Spotlight, Clock,
Battery, Control Center and our hamburger stay visible**.
- Verified with `--diag-hide` across positions 100–400. Below ~200 Wi-Fi gets
hidden too; above ~300 app icons (Now Playing, TextInput) leak back.
- `debug.spacerPos` (Double) overrides the seed for tuning without rebuilding.
- **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
right, so the spacer has nothing to push them with. This is a macOS
constraint, not a bug.
## Gotchas ## Gotchas
- `AppListModel` is `@MainActor`; the `NSWorkspace` observers hop back with - `AppListModel` is `@MainActor`; the `NSWorkspace` observers hop back with
+19 -5
View File
@@ -27,11 +27,25 @@ 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 items to its left created at launch whose length is expanded to push the other icons off-screen so
off-screen. This is reliable only when SimpleTopBar starts at login (its spacer they only live in this panel. It is off by default and always undone on quit; if
then sits to the right of later-launched apps' items) and may need re-toggling the app dies, macOS removes the spacer automatically.
after items move around. It is off by default and always undone on quit; if the
app dies, macOS removes the spacer automatically. What it can and cannot hide:
- It hides **other third-party apps' icons** (cmux, Kapture, WireGuard, input
source, Now Playing, …) so only your apps' icons leave the bar.
- It **keeps Wi-Fi, Spotlight, Clock, Battery and Control Center**. The spacer's
boundary is tuned (preferred position 250) to fall just left of Wi-Fi, so the
Apple items on the right stay visible. macOS reserves that far-right area: a
third-party item can never be placed to the right of it, so those icons cannot
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
automatically.
- Advanced: set `defaults write dev.tzgyn.simpletopbar debug.spacerPos -float <n>`
to move the boundary (larger = keep more icons; > ~400 starts leaking app
icons back into the bar).
## Install with Homebrew ## Install with Homebrew
+8
View File
@@ -53,6 +53,14 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
return 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 // The tray is empty without Accessibility permission, so surface the
// system prompt once at launch instead of waiting for the user to open // system prompt once at launch instead of waiting for the user to open
// the panel and find the button. // the panel and find the button.
+23
View File
@@ -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,
@@ -165,7 +177,18 @@ final class MenuBarHider: ObservableObject {
/// 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() {
guard spacer == nil else { return } guard spacer == nil else { return }
let name = "SimpleTopBarHider"
let key = "NSStatusItem Preferred Position \(name)"
// 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
// Center/Spotlight stay visible. `debug.spacerPos` overrides the seed.
if let override = UserDefaults.standard.object(forKey: "debug.spacerPos") as? Double {
UserDefaults.standard.set(override, forKey: key)
} else if UserDefaults.standard.object(forKey: key) == nil {
UserDefaults.standard.set(250.0, forKey: key)
}
let item = NSStatusBar.system.statusItem(withLength: 0) let item = NSStatusBar.system.statusItem(withLength: 0)
item.autosaveName = name
item.button?.isHidden = true item.button?.isHidden = true
spacer = item spacer = item
} }
+1 -1
View File
@@ -115,7 +115,7 @@ struct TopBarView: View {
.controlSize(.mini) .controlSize(.mini)
.padding(.horizontal, 12) .padding(.horizontal, 12)
.padding(.top, 6) .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.") .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.")
} }
} }
} }
+1 -1
View File
@@ -2,7 +2,7 @@
set -euo pipefail set -euo pipefail
cd "$(dirname "$0")" cd "$(dirname "$0")"
VERSION="${1:-0.2.2}" VERSION="${1:-0.2.3}"
swift build -c release swift build -c release