5 Commits
Author SHA1 Message Date
TZGyn b0671abed8 Set default release version to 0.4.1 2026-09-27 10:14:00 +08:00
TZGyn 569abe967f Revert "Hide menu bar icons with a cover instead of moving them off screen"
This reverts commit 3e57efe312.
2026-09-27 10:13:10 +08:00
TZGyn 3e57efe312 Hide menu bar icons with a cover instead of moving them off screen
Sliding the icons off the screen meant bringing them all back just to
open one menu, which flickered. Draw a frosted window over the other
apps' icons instead: they keep their real positions, so tapping a tile
opens its menu in place with no reveal, and the cover leaves a gap so
the hamburger is never hidden.

💘 Generated with Crush

Assisted-by: Crush:deepseek-v4.1-flash
2026-09-27 03:06:15 +08:00
TZGyn 2312e09105 Shorten the icon reveal when opening a tray menu
The menu is its own window anchored where the item was revealed, so it
survives if the bar re-hides immediately. Revealing for ~0.3s instead of
over a second makes the flash far less noticeable while still opening
the menu in view.

💘 Generated with Crush

Assisted-by: Crush:deepseek-v4.1-flash
2026-09-27 02:53:41 +08:00
TZGyn 9c5416d27e Reveal tray icons only for the moment a menu is opened
Keeping every icon on the menu bar for as long as the panel was open
looked cluttered. Reveal them just long enough to open the clicked
item's menu, then hide again, so the panel stays clean.

💘 Generated with Crush

Assisted-by: Crush:deepseek-v4.1-flash
2026-09-27 02:47:20 +08:00
5 changed files with 82 additions and 27 deletions
+11 -6
View File
@@ -134,12 +134,17 @@ 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.
- Pressing a tray tile calls `MenuBarItemManager.press` (`kAXPressAction`, then - Pressing a tray tile calls `MenuBarItemManager.press`. A hidden item sits
`kAXShowMenuAction`). Because a hidden item sits off-screen, its menu would off-screen, and its menu opens off-screen too (verified with `--diag-press`:
open off-screen; `AppDelegate` therefore calls `MenuBarHider.suspend()` the menu window lands at negative x). So press briefly `suspend()`s the hider,
(`popoverWillShow`) to reveal items while the panel is open and `resume()` waits ~0.2s for the layout to settle, performs the accessibility action
0.6s after `popoverDidClose`. `suspend`/`resume` do not change the user's (`kAXPressAction`, then `kAXShowMenuAction`), and `resume()`s ~0.1s later. The
`enabled` preference. menu is its own window anchored at the item's on-screen spot, so it survives
re-hiding.
- **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
need either per-item ordering control (not available) or covering the icons
with an overlay window instead of moving them off-screen.
- **Order matters**: `AppDelegate` must call `setupStatusItem()` *before* - **Order matters**: `AppDelegate` must call `setupStatusItem()` *before*
`MenuBarHider.shared.prepare()`. If the hider contracts the bar first, the `MenuBarHider.shared.prepare()`. If the hider contracts the bar first, the
hamburger is created into the collapsed space and lands off-screen. hamburger is created into the collapsed space and lands off-screen.
+22 -16
View File
@@ -17,7 +17,7 @@ struct SimpleTopBarApp: App {
/// full menu bar, a brand new item lands under the notch and is invisible; /// full menu bar, a brand new item lands under the notch and is invisible;
/// writing `NSStatusItem Preferred Position <name>` into defaults puts it in a /// writing `NSStatusItem Preferred Position <name>` into defaults puts it in a
/// visible slot, and the autosave name lets the user's own drag persist. /// visible slot, and the autosave name lets the user's own drag persist.
final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate { final class AppDelegate: NSObject, NSApplicationDelegate {
private var statusItem: NSStatusItem? private var statusItem: NSStatusItem?
private var popover: NSPopover? private var popover: NSPopover?
@@ -48,6 +48,27 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
setupStatusItem() setupStatusItem()
MenuBarHider.shared.prepare() MenuBarHider.shared.prepare()
if CommandLine.arguments.contains("--diag-press") {
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
MenuBarHider.shared.suspend()
}
DispatchQueue.main.asyncAfter(deadline: .now() + 2.25) {
if let item = MenuBarItemManager.enumerate().first(where: {
$0.title.lowercased().contains("wireguard")
}) {
NSLog("diag-press: pressing \(item.appName) after brief reveal")
_ = AXUIElementPerformAction(item.element, kAXPressAction as CFString)
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + 2.45) {
MenuBarHider.shared.resume()
}
DispatchQueue.main.asyncAfter(deadline: .now() + 4.5) {
dumpWindows()
}
return
}
if CommandLine.arguments.contains("--diag") { if CommandLine.arguments.contains("--diag") {
DispatchQueue.main.asyncAfter(deadline: .now() + 2.5) { DispatchQueue.main.asyncAfter(deadline: .now() + 2.5) {
dumpMenuBarItems() dumpMenuBarItems()
@@ -103,7 +124,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
let popover = NSPopover() let popover = NSPopover()
popover.behavior = .transient popover.behavior = .transient
popover.animates = false popover.animates = false
popover.delegate = self
popover.contentViewController = NSHostingController( popover.contentViewController = NSHostingController(
rootView: TopBarView( rootView: TopBarView(
model: AppListModel.shared, model: AppListModel.shared,
@@ -114,20 +134,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
self.popover = popover self.popover = popover
} }
// Reveal the hidden menu bar items while the panel is open so pressing a
// tray icon opens that app's menu at its real, on-screen position.
func popoverWillShow(_ notification: Notification) {
MenuBarHider.shared.suspend()
}
func popoverDidClose(_ notification: Notification) {
// Small delay so a menu just opened from a tray tile is fully on screen
// before the bar contracts again.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) {
MenuBarHider.shared.resume()
}
}
@objc private func togglePopover() { @objc private func togglePopover() {
guard let popover, let button = statusItem?.button else { return } guard let popover, let button = statusItem?.button else { return }
if popover.isShown { if popover.isShown {
+25 -4
View File
@@ -103,10 +103,31 @@ final class MenuBarItemManager: ObservableObject {
} }
} }
/// Opens the real menu bar item by pressing its accessibility element, so /// Opens the real menu bar item, so its menu/popover appears.
/// its menu/popover appears at its true screen location. ///
@discardableResult /// A hidden item sits off-screen, and its menu would open off-screen too.
func press(_ item: MenuBarItem) -> Bool { /// So briefly reveal the items, let the layout settle, press the item, then
/// hide again shortly after. The reveal is momentary rather than lasting the
/// whole time the panel is open.
func press(_ item: MenuBarItem) {
let hider = MenuBarHider.shared
let needsReveal = hider.isHiding
guard needsReveal else {
_ = performPress(item)
return
}
hider.suspend()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
_ = self.performPress(item)
// The menu is its own window, positioned at the item's on-screen
// spot, so the bar can be re-hidden almost immediately.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
hider.resume()
}
}
}
private func performPress(_ item: MenuBarItem) -> Bool {
if AXUIElementPerformAction(item.element, kAXPressAction as CFString) == .success { if AXUIElementPerformAction(item.element, kAXPressAction as CFString) == .success {
return true return true
} }
+23
View File
@@ -1,5 +1,28 @@
import AppKit import AppKit
import ApplicationServices import ApplicationServices
import CoreGraphics
/// Diagnostic used by `open SimpleTopBar.app --args --diag-press`. Presses a
/// hidden item's accessibility element without revealing it and dumps the
/// resulting high-level windows, to see where its menu actually opens.
func dumpWindows() {
var lines: [String] = []
if let list = CGWindowListCopyWindowInfo([], kCGNullWindowID) as? [[String: Any]] {
for window in list {
let layer = window[kCGWindowLayer as String] as? Int ?? -1
guard layer > 20 else { continue }
let owner = window[kCGWindowOwnerName as String] as? String ?? "?"
let bounds = window[kCGWindowBounds as String] as? [String: Any] ?? [:]
let x = bounds["X"] as? Double ?? 0
let y = bounds["Y"] as? Double ?? 0
let width = bounds["Width"] as? Double ?? 0
let height = bounds["Height"] as? Double ?? 0
lines.append("\(owner) layer=\(layer) frame=\(Int(x)),\(Int(y)) \(Int(width))x\(Int(height))")
}
}
try? lines.joined(separator: "\n").write(toFile: "/tmp/simpletopbar-windows.txt", atomically: true, encoding: .utf8)
exit(0)
}
/// Diagnostic used by `open SimpleTopBar.app --args --dump-items`. Writes the /// Diagnostic used by `open SimpleTopBar.app --args --dump-items`. Writes the
/// live Accessibility view of the menu bar to /tmp so it can be inspected /// live Accessibility view of the menu bar to /tmp so it can be inspected
+1 -1
View File
@@ -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.1}"
swift build -c release swift build -c release