4 Commits
Author SHA1 Message Date
TZGyn dec8d60aa3 Hide other menu bar icons by default
The point of the app is a single hamburger that replaces the tray, so
hiding the other icons should be the out-of-the-box state. Remember the
user's choice, and create the hamburger before the hider so it is not
itself pushed off-screen.

💘 Generated with Crush

Assisted-by: Crush:deepseek-v4.1-flash
2026-09-27 02:15:01 +08:00
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 97 additions and 16 deletions
+26
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
`enumerate()` routine is `nonisolated static` so `--self-test` can run it.
- 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
Recording or Apple Events usage.
@@ -112,6 +116,28 @@ 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
(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. It is
**on by default** (`hideMenuBarItems` defaults to true, then persisted). 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.
- **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
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
- `AppListModel` is `@MainActor`; the `NSWorkspace` observers hop back with
+20 -5
View File
@@ -27,11 +27,26 @@ still works.
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
created at launch whose length is expanded to push the items to its left
off-screen. This is reliable only when SimpleTopBar starts at login (its spacer
then sits to the right of later-launched apps' items) and may need re-toggling
after items move around. It is off by default and always undone on quit; if the
app dies, macOS removes the spacer automatically.
created at launch whose length is expanded to push the other icons off-screen so
they only live in this panel. It is **on by default** (the choice is
remembered), always undone on quit, and 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
+11 -1
View File
@@ -43,8 +43,10 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
AppListModel.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()
MenuBarHider.shared.prepare()
if CommandLine.arguments.contains("--diag") {
DispatchQueue.main.asyncAfter(deadline: .now() + 2.5) {
@@ -53,6 +55,14 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
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.
+38 -8
View File
@@ -68,11 +68,23 @@ final class MenuBarItemManager: ObservableObject {
guard let bar = extrasMenuBar(of: axApp),
let children = children(of: bar) else { continue }
var seenTitles = Set<String>()
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)
?? stringAttribute(child, kAXTitleAttribute)
?? app.localizedName
?? "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(
id: "\(pid)-\(index)",
pid: pid,
@@ -139,10 +151,10 @@ final class MenuBarItemManager: ObservableObject {
}
/// 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
/// panel. Reliable only when SimpleTopBar starts at login (its spacer then sits
/// to the right of later-launched apps' items). Always restored on quit.
/// panel. On by default; the choice is persisted. Always restored on quit, and
/// a crash cleans up automatically because status items die with the process.
@MainActor
final class MenuBarHider: ObservableObject {
static let shared = MenuBarHider()
@@ -150,37 +162,55 @@ final class MenuBarHider: ObservableObject {
/// Width that pushes the neighbouring items off the visible menu bar.
static let hiddenLength: CGFloat = 10_000
private static let defaultsKey = "hideMenuBarItems"
@Published private(set) var isHiding = false
@Published var enabled = false {
@Published var enabled: Bool {
didSet {
guard enabled != oldValue else { return }
UserDefaults.standard.set(enabled, forKey: Self.defaultsKey)
apply(enabled)
}
}
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.
func prepare() {
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)
item.autosaveName = name
item.button?.isHidden = true
spacer = item
apply(enabled)
}
private func apply(_ hiding: Bool) {
prepare()
spacer?.length = hiding ? Self.hiddenLength : 0
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() {
spacer?.length = 0
isHiding = false
enabled = false
}
}
+1 -1
View File
@@ -115,7 +115,7 @@ struct TopBarView: View {
.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.")
.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
cd "$(dirname "$0")"
VERSION="${1:-0.2.2}"
VERSION="${1:-0.2.3}"
swift build -c release