Skip to content

Guarantee menu bar reachability, fix reconnect state, and add quality-of-life features - #30

Open
distractedhero wants to merge 11 commits into
DParent10:mainfrom
distractedhero:feature/quality-of-life
Open

distractedhero wants to merge 11 commits into
DParent10:mainfrom
distractedhero:feature/quality-of-life

Conversation

@distractedhero

Copy link
Copy Markdown
Contributor

Summary

Two open issues (#9, #11) report the app running with no visible menu bar icon and no other way in. I reproduced this live, including on a fresh copy of the official notarized v0.2.2 release — not a build issue on my end — with no third-party menu-bar manager involved and a normal, single-display notched MacBook. AppKit's own visibility flags (statusItem.isVisible, button.window?.isVisible) reported the item as present in the same session where it plainly wasn't rendering, so a fix that depends on detecting the failure isn't trustworthy. This PR stops trying to detect it and guarantees reachability unconditionally instead, then adds a handful of small, contained quality-of-life features on top.

Reliability fixes

  • Guaranteed Dock icon + window (LSUIElementfalse): the app always has a Dock icon now, and always shows its main window on launch and on reopen (double-click, open, clicking the Dock icon) — independent of whether the menu bar item renders. The status item is still created as a convenience for the (apparently more common) case where it works.
  • Stable dev-build code signing: Scripts/build_app.sh now prefers any local codesigning identity already in the keychain over ad-hoc (codesign --sign -). Ad-hoc signing derives a fresh, content-based identity on every build, so a rebuild silently revoked previously-granted Accessibility/Input Monitoring — this fix keeps grants stable across rebuilds. (Doesn't affect your release signing, which is already Developer ID.)
  • Device-reconnect state reset: HIDListener registered a device-matching callback but never a removal one, so lastDPI/lastDPIDirection and in-flight button-press state survived a Bluetooth drop, sleep/wake, or dongle reseat. The user guide already claims a fresh baseline gets recorded "after launching the app or reconnecting the mouse" — that was only ever true for launch. Now actually true for reconnects too, and a button physically held at disconnect no longer gets stuck "down."

Quality-of-life additions

  • Launch at Login — a checkbox in the main window using SMAppService (macOS 13+, already the stated minimum), no helper app needed.
  • Remap-enable nudge — granting both permissions has never meant remapping is actually on; that's a separate switch, easy to forget once permissions are sorted (this tripped me up twice while testing). Now offers to turn it on the moment both go green.
  • Test button in the mapping editor — runs the currently-configured action once, immediately, without saving or physically pressing the mouse button. Useful for shell commands, app launches, and macros especially.
  • Hypershift on-screen cue — a brief HUD when Hypershift toggles. The user guide already documents the toggle state as easy to lose track of (that's why the long-hold force-deactivate exists); this is the other half of that fix.
  • Update check — checks the GitHub releases API at most once a day and shows a banner in the main window if a newer tag exists. No auto-download, no auto-install — deliberately not Sparkle, which felt like a lot of dependency/maintenance surface for what a version-string comparison mostly solves.
  • Import safety warning — profiles are importable from any JSON file, and a systemCommand action runs with no per-press confirmation. A shared profiles.json found online could otherwise silently bind a button to arbitrary shell code. Now warns with a count before committing an import that contains one.

Testing

  • All existing unit tests pass, plus 5 new ones covering the version-comparison logic and the shell-command-detection scan (22 total).
  • Verified via a real launch: SMAppService status query and the update-check network call both fire correctly in the unified log, no crashes or faults.
  • README has a "Changes on this branch" section documenting all of the above for easy review.

Happy to split this into smaller PRs if that's easier to review — the reliability fixes and the QoL additions are reasonably separable, they just happened to land in one investigation session.

distractedhero and others added 11 commits September 18, 2026 00:39
The status item can silently fail to show up in the menu bar even when
there is free space (no overflow chevron, not crowded) -- confirmed by
screenshot while diagnosing this against upstream DParent10#9 and DParent10#11, which
also both report the icon simply never appearing.

- Give the status item a stable autosaveName so AppKit can persist and
  restore its slot across launches instead of treating it as a fresh,
  position-less item every time. This is the maintainer's own leading
  theory in both linked issues.
- Replace the one-shot, UserDefaults-gated first-launch alert with a
  check that runs on every launch. If the item is genuinely visible,
  show the popover once per install as before. If it isn't, guarantee
  a way in: temporarily switch to .regular activation policy and open
  a real window hosting the same content, instead of an alert that a
  process with no Dock icon and no visible menu bar item may never be
  able to reliably present. The activation policy reverts to
  .accessory when that window is closed.
Once the one-time first-launch popover had fired, double-clicking
NagaController again (or `open`-ing it, or clicking a Dock icon) did
nothing visible — the process was already running and had no other way
to surface UI, which reads as "the app doesn't open."

Implement applicationShouldHandleReopen to bring the popover (or the
fallback window, if the status item never rendered) forward every time
the app is reopened, not just on the very first launch.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ad-hoc signing (`codesign --sign -`) derives the code identity from the
binary's own hash, so it's different on every single build. macOS ties
Accessibility and Input Monitoring grants to that identity, so every
rebuild silently revoked both and forced a re-grant in System Settings
before the app could read the mouse's HID input again.

Prefer any local codesigning identity already in the keychain (a real
certificate, tied to a Team ID rather than the binary's content) and
only fall back to ad-hoc when none exists. Override via
NAGA_CODESIGN_IDENTITY.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ailure

The status item can silently fail to render with no icon and no overflow
chevron, reproduced live on a stock, notarized v0.2.2 release. AppKit's
own visibility flags (statusItem.isVisible, button.window?.isVisible)
don't reliably reflect that failure — they reported the item as visible
in the same session where it plainly wasn't on screen. A fix built on
detecting the failure is therefore not trustworthy.

Stop trying to detect it. Switch LSUIElement to false so the app always
has a Dock icon, and always show the main window on launch and on
reopen (double-click, `open`, clicking the Dock icon), independent of
whatever the menu bar is doing. The status item is still created as a
convenience for the (apparently more common) case where it does work,
but it's no longer the only way in.

This is a bigger behavioral change than a bug fix — a persistent Dock
icon changes the app's default feel from "background menu-bar utility"
to "regular app" — so it's not proposed upstream yet pending discussion
with the maintainer.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Every comparable menu-bar utility has this; NagaController didn't.
Uses SMAppService (macOS 13+, already the app's stated minimum), so no
helper app or legacy SMLoginItem bookkeeping is needed. A checkbox next
to the existing remapping toggle mirrors SMAppService.mainApp.status on
open and registers/unregisters on change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The user guide already admits the toggle-mode Hypershift state is easy
to lose track of — that's the documented reason the long-hold
force-deactivate safety valve exists. A HUD is the other half of that
fix: something visible at the moment it toggles, not just a menu bar
title text change that requires already knowing to look for it (and
which the current status-item rendering issues make an unreliable
place to put state anyway).

HUDNotifier is a small, dependency-free borderless window, reused
singleton-style so a second call just replaces what's showing rather
than stacking.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Verifying a shell command, app launch, or macro mapping meant saving
it and then physically pressing the mouse button — awkward mid-edit,
and impossible for triggers not yet learned. Test runs
buildActionFromUI()'s result once through ButtonMapper immediately,
unsaved, the same builder Save already uses.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
IOHIDManagerRegisterDeviceMatchingCallback was registered without its
counterpart, IOHIDManagerRegisterDeviceRemovalCallback, so nothing
cleared lastDPI/lastDPIDirection, the synthetic button-down states, or
the pointer router across a Bluetooth drop, sleep/wake, or dongle
reseat. The user guide already claims a fresh DPI baseline gets
recorded "after launching the app or reconnecting the mouse" — that
was only ever true for launch. A button physically held down at the
moment of disconnect also never got its release, leaving it stuck.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Profiles are importable from any JSON file, and a systemCommand action
runs on the mouse button press with no per-press confirmation. A
shared profiles.json (someone's public "Naga setup", found online)
could carry one silently. Split the decode step out of importProfiles
so the caller can inspect a file before committing to it, and warn
with a count when it contains shell-command actions.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Every version bump otherwise requires noticing a new GitHub release by
hand. Hits the releases API at most once a day, compares semver tags,
and posts a notification MainViewController's banner listens for — no
auto-download, no auto-install, just visibility. Deliberately not
Sparkle: a full auto-update framework is a lot of dependency and
maintenance surface for what a one-line version check mostly solves.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A "Changes on this branch" section, separate from the general Features
list, so anyone looking at this fork can see at a glance what's here
beyond upstream v0.2.2 and why — without digging through commit
messages.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant