Keep an eye on your connection from the macOS menu bar — battery, signal, and how much data you've used, without opening a web panel. Works with ZTE 5G modems, ASUS routers, and an iPhone Personal Hotspot.
The icon shows up only while you're actually on that network, and stays out of the way the rest of the time.
A regular router (ASUS) — network status, this Mac's address, and live transfer with a 24-hour chart:
A mobile modem (ZTE) — the same, plus signal quality, network type, and a battery with its own history:
An iPhone Personal Hotspot — the tether read from the Mac's own interface, with the link medium and traffic since the connection came up:
brew tap RadnoK/tap
brew install --cask router-menuOr grab the .zip from Releases
and drag Router Menu.app into /Applications.
The app is signed and notarized by Apple, so it opens without Gatekeeper warnings. It runs natively on both Apple Silicon and Intel Macs.
At a glance in the menu bar — the icon fills up as your signal gets stronger, so you can tell reception from across the room.
Click for the details:
- Battery level, and whether the modem is charging
- Signal strength and network type (5G, LTE, …)
- Current up and down speed
- Data used this month and in total
- How long the connection has been up
- Small charts of battery and data over the last 24 hours
Optional signal diagnostics — RSRP and SINR, two numbers the modem reports that tell you how clean the cellular signal is. Useful when you're hunting for the best spot to put the modem; easy to ignore otherwise. You can hide them, along with any other group of stats, in Settings.
A widget in Notification Center — battery, signal and transfer at a glance without opening anything. Three sizes: the small one leads with the battery level, the medium one adds RSRP/SINR and current speeds, and the large one draws the last 24 hours of battery history. The widget shows the last reading the app took, and says how old it is once that reading stops being current.
Speaks your language — English and Polish. Follows your Mac's language, or you can pick one in Settings.
-
macOS 26 (Tahoe) or newer — the popover is a translucent, rounded panel that reads through to what is behind it, which needs Tahoe's AppKit.
-
One of:
- A ZTE 5G modem with a web panel at
192.168.0.1— developed against the ZTE U50, and likely to work with related models that share the same web panel. - An ASUS router running Asuswrt.
- An iPhone (or Android) Personal Hotspot — no setup on the phone, the Mac reads its own end of the tether. Note that iOS does not share the phone's battery or cellular signal with a Mac, so those are not shown.
If you try another device, let me know how it goes.
- A ZTE 5G modem with a web panel at
Most of it works the moment you install it. Two things are worth knowing.
The app needs to know when you're on the modem's network, and there are two ways it can tell:
- By Wi-Fi name — matches your network name. macOS only lets apps read the Wi-Fi name if you grant location permission, so you'll see that prompt on first launch. Nothing about your location is read, sent, or stored; it's just the gate macOS puts in front of the network name.
- By modem address — checks whether the modem answers at its IP. No location permission needed.
Pick whichever you prefer in Settings, under General.
Battery, signal, and current speed work right away.
The monthly and total counters are different: the modem only reports them to a logged-in session. Put your modem's web panel password into Settings under Account, and the counters start filling in. The password goes into your macOS keychain and is only ever sent to the modem on your own network.
Don't want to bother? Leave it empty. Everything else keeps working.
The app updates itself. You can turn that off, change how often it checks, or check right now — all under Updates in Settings. Updates are cryptographically signed and verified before anything is installed.
The app talks to your modem and nothing else.
- No analytics, no telemetry, no external servers — all traffic stays on your local network.
- Your modem password lives in the macOS keychain.
- The 24-hour history is a local file in
~/Library/Application Support/zte-menu/. - Location permission, if you grant it, is used for exactly one thing: reading your Wi-Fi network name. Your location is never read or transmitted.
If you use a menu bar manager like Bartender or Ice, it may have tucked the new icon into the hidden section. Check there and mark Router Menu as always visible.
Otherwise, remember the icon is hidden by design when you're not connected to the modem — that's usually the explanation.
Bug reports and pull requests are welcome. If something's broken or your modem isn't supported, open an issue.
You'll need a Swift 6 toolchain (ships with recent Xcode) and xcodegen
(brew install xcodegen).
swift test # run the test suite
./scripts/build-app.sh # generate the project, build, package into a .app
open "dist/Router Menu.app"Dependencies and the test suite still live in Package.swift; swift test
needs nothing else. The Xcode project is generated from project.yml and
is not checked in — it exists because the widget is an app extension, and only
xcodebuild can build a nested .appex, embed it and sign the bundle
inside-out.
build-app.sh produces a universal binary (arm64 + x86_64), assembles the app
with the widget in Contents/PlugIns/, and ad-hoc signs it so it runs locally.
The widget needs a real signature. It reads the app's data through the App Group
7S3F9767BM.io.8lines.router-menu. The Team ID prefix is what lets the sandbox verify the group against the signing team, so no provisioning profile is needed — but an ad-hoc signature has no team, so a local build installs the widget and it finds no data. The app itself is unaffected.
Resources/Info.plist, Resources/Widget-Info.plist and both entitlement
files are generated from project.yml — edit the manifest, not the plists,
or the next xcodegen generate will overwrite them.
The code is layered so the interesting parts can be unit tested without a modem attached: parsing, formatting, state, and history are plain Swift with no system dependencies, while the pieces that touch the network, the keychain, and Core Location sit behind thin injected wrappers. SwiftUI views read from an observable store and stay free of business logic.
Talking to the modem was worked out by watching its own web panel: logging in
hashes the password, and the resulting session cookie is replayed on later
requests. ModemClient and ZTEAuth hold that logic, and the tests pin it
against captured responses.
Bump MARKETING_VERSION in project.yml (and CURRENT_PROJECT_VERSION, which
must stay purely numeric — Apple rejects a pre-release suffix there), then push
a matching tag (the release script refuses a mismatch):
git tag v0.4.0 && git push origin main v0.4.0GitHub Actions takes it from there — builds, signs, notarizes, publishes the release, updates the appcast, and bumps the Homebrew cask.
To cut a release from your own machine instead, run ./scripts/release.sh <version>. It picks up the Developer ID certificate from your keychain on its
own; if you hold more than one, it stops and asks you to name the one to use:
SIGN_IDENTITY=<sha1-hash> ./scripts/release.sh 0.6.3security find-identity -v -p codesigning lists the hashes.
A tag with a pre-release suffix (v0.7.0-beta.1) is published on Sparkle's
beta channel and marked as a pre-release on GitHub. Only users who pick
Beta under Settings → Updates are offered it; everyone else keeps seeing
stable releases only, and the Homebrew cask is left untouched. Beta is
additive — a beta user still receives every stable release.
MIT © Konrad Alfaro



