A menu-bar-only macOS app that tracks your keystrokes locally and shows you
juicy stats about them. Nothing leaves your machine — it's a local SQLite file at
~/Library/Application Support/KeyStats/keystats.sqlite, no networking code
exists in the app at all.
- Per-key counts — which letters/keys you press most (A, S, Backspace, etc.)
- Modifier key usage — how often you hit Cmd, Shift, Option, Control, Fn on their own
- Keybind / combo usage — e.g.
Cmd+Tab,Cmd+S,Control+C,Cmd+Shift+Z, counted as canonical combos regardless of press order - Hourly activity — bar chart of keystrokes for the last 24 hours, useful for spotting your most productive coding hours
- Per-app activity — which app was frontmost when you were typing (handy to see how much time goes to your editor vs. terminal vs. Slack)
- Daily totals + backspace ratio — rough proxy for how much you're correcting yourself while coding
- Daily goal + streak — a progress ring against a daily keystroke goal (15,000 by default), and a streak of consecutive days you've hit it. Once a day is credited it stays credited even if you change the goal later — see "Customizing it" below for how to change the goal itself
It intentionally does not store the sequence/content of what you typed — only aggregate counts. That keeps it useful without turning into an actual keylogger that could leak passwords if the DB were ever read by someone else.
- macOS 14+ (Sonoma or later — needed for the
SectorMarkpie chart in the dashboard) - Xcode 15+ (for Swift 5.9 toolchain) — either Xcode itself, or just the
Command Line Tools (
xcode-select --install) if you'd rather build from the terminal withswift build.
There's no downloadable build yet (see "Getting a real .app" below), so for
now you build it yourself. Two ways to do that:
This builds a real /Applications/KeyStatsApp.app, the same way a
downloadable release eventually will work: proper menu bar icon, survives
reboots, and — unlike ad-hoc builds — keeps its Accessibility permission
across rebuilds.
cd KeyStats
./make-signing-cert.sh # one-time per machine: creates a local "KeyStats Local Signing"
# identity in your keychain, so rebuilds don't lose Accessibility
# permission (ad-hoc signing has no stable identity, so macOS keys
# the grant to the binary's hash, which changes on every build)
./build-app.sh # builds, signs, installs to /Applications, and launches itRe-run ./build-app.sh any time you pull new changes — it's idempotent,
backs up the SQLite database first, and quits/relaunches the running app for
you. ./make-signing-cert.sh only ever needs to run once (it's a no-op if
the identity already exists); the one exception is the very first time you
switch an existing ad-hoc install over to this signed one, which still needs
one final manual Accessibility re-grant.
cd KeyStats
swift build -c release
swift run -c releaseOr open the folder in Xcode (File > Open on Package.swift) and hit Run.
Faster to iterate with, but permission tends to attach to the parent process
(Terminal/Xcode) rather than a stable app identity, so it's easy to lose
between runs — use Option A if you want the grant to actually stick.
macOS requires Accessibility permission for any app that wants to observe
keystrokes globally (this is the same permission used by apps like Rectangle
or Karabiner). On first launch a system prompt appears — click Open System
Settings, go to Privacy & Security → Accessibility, and enable the
toggle for KeyStats (or Terminal/Xcode if running via swift run).
The app polls for permission every 2 seconds and starts capturing
automatically once granted — no need to relaunch.
If permission is ever lost or was never granted, the dashboard shows a banner, the menu bar icon changes, and Preferences → General shows the status live — each with a button straight to the Accessibility pane and, if needed, instructions to remove and re-add KeyStats there:
The app has no Dock icon — look for the keyboard icon in your menu bar. Click it and you'll see a greeting (by name, if you've set one), today's keystroke count against your daily goal, then Open Dashboard for the full stats, Preferences… for theme/goal settings, or Quit KeyStats to stop tracking and exit. The dashboard's gear icon opens the same Preferences window.
Preferences has a General tab (a display name that shows up in the menu bar
greeting, plus Accessibility permission status with the same "Open
Accessibility Settings" button as the dashboard banner), an Appearance tab
(a font-size scale from XS to Large, a font picker with 8 paired UI+mono
presets — 3 of them bundled developer fonts — and 15 themes, 12 dark and 3
light, click a card to switch — same list as znotes/themes.json), and a
Goals tab (daily goal slider + presets, a "count weekends toward streak"
toggle off by default so a quiet Saturday or Sunday can't break a streak
built on weekdays, and your longest streak with its date range). Changing
the goal only ever affects today — past days keep whatever goal_met
they were written with, permanently; raising the goal above what you've
already typed today un-earns just today's credit, and lowering it back
re-earns it.
Every tunable number in the app — window size, the daily goal's default/
range/presets, refresh intervals, chart sizing, query limits — lives in one
file, Sources/KeyStats/AppConfig.swift. Edit a constant there and rebuild
rather than hunting a number down across files.
The daily goal, weekend toggle, font size, and font preset are also
reachable via defaults write if you'd rather script them than use
Preferences:
defaults write com.joby.KeyStatsApp dailyGoal 20000
defaults write com.joby.KeyStatsApp countWeekendsTowardStreak -bool true
defaults write com.joby.KeyStatsApp fontScale large
defaults write com.joby.KeyStatsApp fontPresetID modern-dev
# back to the defaults (15,000 / weekends don't count / Regular / Default)
defaults delete com.joby.KeyStatsApp dailyGoal
defaults delete com.joby.KeyStatsApp countWeekendsTowardStreak
defaults delete com.joby.KeyStatsApp fontScale
defaults delete com.joby.KeyStatsApp fontPresetID(If you're running via swift run rather than the packaged .app, these
land in a different defaults domain — the SwiftPM executable's, not
com.joby.KeyStatsApp — since they're keyed by bundle identifier.)
- The keycode → name map in
KeyCodeMap.swiftcovers a standard US ANSI keyboard. If you use a different layout, some keys may show asKey#NN— just add the missing codes to the map. - Combos are canonicalized in a fixed order (
Control+Option+Shift+Cmd+Fn+Key) soShift+Cmd+ZandCmd+Shift+Zcount as the same keybind. - If you'd rather not track which app is frontmost (e.g. for extra privacy),
delete the
recordFrontmostApp()calls inEventTapManager.swift. - To reset all stats, quit the app and delete
~/Library/Application Support/KeyStats/keystats.sqlite. - If you need to change the SQLite schema, don't edit the
CREATE TABLEstatements directly —Storage.swifthas a numbered-migration system (migrate()/addColumnIfMissing) specifically so an update doesn't break installs that already have data. See the comment block abovemigrate()for the rules (append-only steps, always provide aDEFAULTforNOT NULLcolumns).
There's no downloadable release yet — for now, building it yourself via
./make-signing-cert.sh + ./build-app.sh (see "Running it" above) is the
only way to get the packaged .app. A proper downloadable build (signed
with a real Developer ID and notarized, rather than the local self-signed
identity these scripts use) is something I'd like to put together
eventually — hopefully stay tuned.
