feat(macos): launcher identity, Dock tile, hide-to-tray, native menu-bar item#168
Merged
Merged
Conversation
eb1a2cf to
44072df
Compare
44072df to
ec9a76f
Compare
A bare `python main_qml.py` launch surfaces "python" in the Dock tile, Cmd+Tab caption, Force Quit, and Activity Monitor because macOS reads the Mach-O image basename at exec time and there is no in-process API to rename it after the fact. The application icon comes from the same bundle identity, so the Dock also shows the generic Python launcher icon instead of Mouser's branding. None of this affects PyInstaller `.app` bundles -- only source and venv launches. The fix layers a few small pieces in main_qml.py. `_maybe_relaunch_with_mouser_process_name` re-execs the current interpreter through a stable symlink whose basename is `Mouser`, staged atomically via os.replace() to avoid a TOCTOU window where a concurrent launch could observe the launcher path missing. The symlink lives next to the venv's python shim so pyvenv.cfg discovery still resolves site-packages after the re-exec. It returns immediately on PyInstaller-frozen bundles (where the Mach-O is already correctly named), on non-macOS platforms, and when the env-var guard shows we already relaunched. `_rename_macos_bundle_for_dock` mutates [NSBundle mainBundle]'s display-name keys so the application menu, Force Quit, and notification banners read "Mouser" as well. `_install_macos_dock_icon` replaces NSApplication's icon with `images/logo_icon.png`. The decoded NSImage is cached at module scope so repeated invocations (one per Regular promotion) only re-issue the cheap setApplicationIconImage_ syscall instead of re-decoding the 1024px PNG. `_app_icon` similarly hands QIcon the full-resolution PNG directly so the Dock and Cmd+Tab tiles render crisply at native size instead of the previous upscaled-256px blur. `_configure_macos_app_mode` and `_set_macos_activation_policy` launch the app as NSApplicationActivationPolicyAccessory (menu-bar only, no Dock entry), then promote to Regular when a window becomes visible. The visibilityChanged hook flips back to Accessory on hide so the Dock collapses cleanly. The policy setter is idempotent against rapid visibilityChanged storms by skipping the AppKit round-trip when the requested state matches the last-applied one. `_check_accessibility` fails closed on exception now (returns False) so future callers can rely on the docstring contract, and `_parse_cli_args` returns a 4-tuple so callers can branch on the new `--show-window` flag.
ec9a76f to
1626dc3
Compare
Collaborator
|
Thanks @hughesyadaddy. I pushed four follow-up commits on top of your original commit to tighten the macOS app-shell behavior before merge. Your original commit is preserved. What changed:
Validation run on my Mac:
Manual notched-MacBook smoke passed after the follow-up commits:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
A first-time macOS install of Mouser has four rough edges that make
the app feel half-finished even though every feature works:
Quit dialog the app appears as
python(orPython), which isindistinguishable from any random script and makes "what's using
my mouse?" diagnostics annoying.
images/AppIcon.icnsships to PyInstaller, but the runningprocess's executable identity does not match the bundle, so AppKit
falls back to the framework default at runtime.
menu-bar tray; default Qt close behaviour kills the engine along
with the window. A user who hits Cmd-Q out of habit has to relaunch
to get scroll inversion / button remapping back.
MacBooks. Qt 6's
QSystemTrayIconallocates a fixed-lengthNSStatusItemand places it left-of-notch, where macOS Sonoma /Sequoia refuse to composite items competing with the notch
cut-out — the result is a phantom item that exists in the status
bar's geometry but never paints. On a clean install of Mouser on a
14"/16" MacBook Pro, the tray icon is simply not there. Right-
click also fails since AppKit cannot deliver clicks to an item it
refuses to draw.
None of these block functionality, but together they cost the app
roughly 30 seconds of credibility every time someone installs it,
and (4) is a hard-blocker for hide-to-tray on any new MacBook.
What changed
main_qml.pyis the only file touched. All changes are macOS-only(gated on
sys.platform == "darwin") and inert on Windows / Linux.Process identity & Dock presence
_macos_named_executable_path()+_maybe_relaunch_with_mouser_process_name()— on a source-checkout launch, re-execs the interpreter through a
stable
Mouser-named symlink in~/Library/Application Support/Mouser/. Activity Monitor,ps,and the Force Quit dialog then show the process as
Mouser. Thesymlink is created with
os.symlinkand rotated withos.replaceso a stale link can't break a re-exec. Guarded on
sys.frozensopackaged
.appbundles skip the dance (the bundle'sInfo.plistalready pins the name).
_rename_macos_bundle_for_dock()— writesMouserto theprocess's runtime
CFBundleNameviaNSBundle.localizedInfoDictionaryso the Dock menu's app-name label matches the process name.
_install_macos_dock_icon()— decodesimages/AppIcon.icnsintoan
NSImageexactly once and assigns it viaNSApplication.setApplicationIconImage_. Module-scope cache sovisibility transitions don't re-decode the 1024 PNG.
_app_icon()— verifies the icon path exists before handing itto
QIconso a missing asset surfaces in logs rather than silentlyfalling back to the generic icon.
Window-lifecycle <-> Dock tile reconciliation
_set_macos_activation_policy(regular: bool)— flipsNSApp.activationPolicybetweenNSApplicationActivationPolicyRegular(Dock tile visible, Cmd-Tab eligible) and
NSApplicationActivationPolicyAccessory(menu-bar-only) in responseto
QWindow.visibilityChanged. Idempotency check inside so we don'tpoke AppKit on every redraw.
_on_window_visibility_changed(visibility)— Qt signal handlerthat calls the policy setter. Critical fix: the handler is now
invoked with the window's initial visibility immediately after
connecting, because the window was created
visible: !launchHiddenby QML before the handler was wired, which meant the initial
show-transition fired with no listener and the policy stayed
Accessory(no Dock tile) on a--show-windowlaunch. Withoutthis reconcile, a user starting Mouser with the window open would
never see a Dock tile.
Native
NSStatusItem(menu-bar tray icon)This is the biggest piece in the diff and the reason hide-to-tray is
actually usable on macOS 14+.
_install_native_macos_status_item(qmenu, on_left_click)—installs an
NSStatusItemvia PyObjC directly, bypassingQSystemTrayIcon:NSVariableStatusItemLengthinstead ofNSSquareStatusItemLength. Variable-length items are the onlyones macOS Sonoma+ will composite right-of-notch on a notched
display, so the icon ends up visible instead of stranded in the
left-of-notch dead zone Qt's
QSystemTrayIconlands in.SVG the in-app sidebar brand mark uses, paints it through
QSvgRendererinto aQPixmap, exports PNG bytes, hands them toNSImage, setstemplate = True. macOS then tints the icon tomatch the menu bar (light / dark / accent) without a per-mode
asset.
(
_StatusItemTarget(NSObject)): single click →on_left_click()(which shows the main window); any modifier or right-click →
pop up the existing Qt
QMenuat the cursor.QSystemTrayIconis kept alive but hidden: it remainsavailable for
showMessage()notifications (which use a differentAppKit path under the hood, separate from
NSStatusItem) so the"Mouser is hidden -- click the menu-bar icon to bring it back"
banner still fires.
it the moment the install function returns.
TCC
_check_accessibility()— fails closed: a TCC exception or aFalsereturn fromAXIsProcessTrustednow triggers the systemprompt rather than silently assuming permission was granted.
Previously a bare
except:swallowed the error and the engine ranwith no event-tap input, which presents to the user as "Mouser is
running but nothing works".
Why a native
NSStatusIteminstead of fixing QtQSystemTrayIconcalls[NSStatusBar systemStatusBar statusItemWithLength: NSSquareStatusItemLength]internally (verifiedagainst Qt 6.7 source). That length value is the root cause: macOS
Sonoma changed the menu-bar layout to position fixed-square items
left-of-notch on notched displays, where the system refuses to draw
them. There is no Qt API to override the length parameter — the only
fix is to stop using
QSystemTrayIconfor the icon surface and callAppKit directly with
NSVariableStatusItemLength. We keep Qt'sQSystemTrayIconalive in parallel because it owns thenotification-banner path, which works correctly regardless of the
status-item length.
Scope
(it backs the existing event-tap and HID code).
upstream/master). The Dock-tile art refresh is in feat(brand): macOS-spec app icon and reproducible build pipeline #177 andlands independently of this PR.
Info.plistchanges — this PR drives everything from the runningprocess so the source-checkout and bundle paths share one code
path.
Test plan
psshow theprocess as
Mouser. The Dock tile shows the Mouser icon. Aboutpanel surfaces the bundle identity, build mode, HEAD commit,
and launch path.
.applaunch:_maybe_relaunch_with_mouser_process_nameis skipped (
sys.frozenguard); identity comes from the bundle.flips between Regular and Accessory; Dock tile appears /
disappears in lockstep.
--show-windowlaunch on a clean profile (window startsvisible): Dock tile is present immediately (regression test
for the initial-visibility reconcile fix).
icon renders right-of-notch. Single click reopens the window;
right-click and Option-click pop up the Qt menu.
behaviour identical.
(regression test for the silent-failure path).
pytest tests/-- 406 tests, 155 subtests, green.Screenshot
About panel from the sidebar info button -- confirms the running
bundle identity, the
Source checkoutbuild mode, the HEAD commit,and the runtime launch path (all wired through the renamed launcher
machinery):