Skip to content

EClinick/juice

Repository files navigation

Juice

A macOS menu bar app that shows what is eating your battery.

Swift 6 macOS 14+ License: MIT Tests

Juice popover showing battery status, top energy users with per-app watt-hours, a charge timeline, and insights

Live battery status, per-app energy rankings, charge history, and plain-English insights - one click away in the menu bar.

Why Juice

macOS records detailed per-app energy accounting around the clock, but it never shows you the history. Activity Monitor displays a rolling 12-hour number, and the battery menu lists at most a few "apps using significant energy" right now. The real data lives in a hidden root-only database (/var/db/powerlog), where the OS meters every app's CPU, GPU, and Neural Engine energy in nanojoules.

Juice surfaces that data: which apps used how many watt-hours today, over the last 3 days, or the last week, how fast your battery is draining right now, and why a given app used as much energy as it did.

Features

  • Menu bar live readout: battery percent and live watts drawn (or charging wattage), updated continuously.
  • Top energy users: per-app watt-hours for Today, 3 Days, Week, or All Time, with real app icons.
  • Per-app detail: click any app to see where its energy went (CPU vs GPU vs Neural Engine), an hour-by-hour usage chart, and a plain-English explanation of the usage pattern.
  • Charge timeline: battery level over the last 24 hours, sampled locally every minute, with on-AC periods highlighted.
  • Insights: drain-rate anomalies measured against your own 7-day baseline, apps using far more than their typical energy, the energy hog of the week, and charging-habit observations.
  • Stats window: the full app table (not just the top 8) plus a 7-day charge chart and battery health.
  • In-app updates: choose automatic download-and-install updates, or keep updates manual and use “Check for Updates…” whenever you want. Homebrew installs update directly from Juice's signed release feed.
  • Honest charts: axes are pinned to the real time window, recording gaps show as gaps, and partial data is labeled as such - the charts never stretch or interpolate data to look fuller than it is.
  • Private by default: no telemetry, system profile, or accounts. Juice only contacts its release feed when you ask it to check for updates or enable automatic updates.

Per-app detail window showing VS Code energy breakdown: 100 percent CPU, hourly bar chart, and explanation bullets

Stats window showing the full app energy table and a 7-day charge chart with a recording-since annotation

Architecture

The interesting problem: the energy data sits in a root-only SQLite database, but a menu bar app should not run as root. Juice splits in two.

flowchart TB
  subgraph system["macOS system data"]
    PL[("powerlog database<br/>root-only, per-app energy<br/>~3 days retained by macOS")]
    IOK["IOKit AppleSmartBattery<br/>live watts, percent, health<br/>no special permissions"]
  end

  subgraph helper["JuiceHelper (root daemon, ~350 lines)"]
    SNAP["Read-only snapshot queries"]
  end

  subgraph app["Juice.app (user space)"]
    XPC["XPC client"]
    STORE[("Local store (GRDB/SQLite)<br/>~/Library/Application Support/Juice<br/>history kept beyond macOS retention")]
    INS["Insights engine"]
    UI["SwiftUI MenuBarExtra"]
  end

  PL -- "snapshot copy, read-only SQL" --> SNAP
  SNAP -- "XPC, code-signing validated" --> XPC
  IOK --> UI
  XPC --> STORE
  STORE --> INS
  INS --> UI
  STORE --> UI
Loading

Security model for the privileged helper:

  • The helper exposes a version handshake plus time-bounded energy and battery-history queries, and is small enough to audit directly.
  • It validates the connecting client's code signature on every message and rejects root clients.
  • It never opens the live system database in place; it snapshot-copies the database and reads the copy read-only, so it cannot contend with or corrupt the system's writer.
  • If you decline to approve it, the app still works with live battery data; per-app energy and powerlog backfill remain unavailable.

Requirements

  • macOS 14 (Sonoma) or later, Apple Silicon or Intel.
  • A Swift 6 toolchain (Xcode 16 or later) to build from source.

Install (from source)

Juice can be packaged as a normal macOS application bundle for local use. A Developer ID certificate is required to distribute a notarized release to other people.

git clone https://github.com/EClinick/juice.git
cd juice

# Install the privileged helper (asks for sudo, see below for what it does)
make dev-helper-install

# Build and sign the isolated dev app so the helper accepts its XPC connections
make dev-app-sign

# Run
./.build/debug/Juice

Build a macOS app bundle

Create an isolated development menu-bar app at dist/Juice Dev.app:

DEV_SIGNING_IDENTITY="Developer ID Application: Your Name (TEAMID)" make app
ditto "dist/Juice Dev.app" "/Applications/Juice Dev.app"
open "/Applications/Juice Dev.app"

Development builds use com.eclinick.juice.dev, the helper/Mach service com.eclinick.juice.dev.helper, and a separate defaults suite, so they can coexist with the released Juice app without replacing its Login Item.

The app bundle includes its development launch daemon and registers it through SMAppService. Juice intentionally registers only while installed under /Applications; launching from dist or directly from the DMG shows an install-location instruction instead of creating an ejectable daemon path. macOS then requires approval under System Settings → General → Login Items before the daemon can run. The packaged development target uses your Developer ID identity and the same Team ID-pinned trust model as production. For packaging inspection on a machine without that certificate, make app-adhoc creates an artifact whose privileged helper is not expected to launch on systems enforcing signed launch constraints.

Publishing a release

Public releases are automated as one guarded transaction. On the configured release Mac, run a read-only preflight first, then publish:

DRY_RUN=1 make publish VERSION=0.1.2
make publish VERSION=0.1.2

The publisher derives the next build number from the latest appcast, requires a clean master synchronized with GitHub, runs the test suite, signs the app and helper, notarizes and staples the app and DMG, signs the Sparkle appcast, creates the GitHub release, updates EClinick/homebrew-tap, and verifies the public artifacts and checksums. It will not create a tag or release if the local validation stages fail.

The release Mac must have GitHub CLI access to both repositories, the Developer ID identity Developer ID Application: Ethan Clinick (U2MBGTFZM5), the case-sensitive notarytool Keychain profile JuiceNotary, and Juice's Sparkle private EdDSA key in Keychain. These credentials stay in Keychain and are never stored in the repository. See docs/releasing.md for initial setup and recovery details, and AGENT.md for the repository rule future coding agents should follow.

The lower-level make release-cask and make appcast targets are retained for diagnostics and local inspection; routine public releases should use make publish.

make dev-helper-install builds the isolated development helper, copies it to /Library/PrivilegedHelperTools/com.eclinick.juice.dev.helper, installs a launchd daemon plist at /Library/LaunchDaemons/com.eclinick.juice.dev.helper.plist, and bootstraps it. The daemon starts on demand when the app connects and is idle otherwise. Remove everything cleanly with make dev-helper-uninstall. Always remove that legacy daemon before testing the packaged development build: it uses the same development Mach service and can hide a broken development SMAppService registration. It does not overlap the production helper.

The legacy development install uses an ad-hoc signature with an identifier-only client check, which is fine for a machine you control but is not the production trust model. Packaged Developer ID releases use SMAppService and require the helper and app to share the same Team ID.

How the numbers work

  • Energy figures come from macOS's own per-coalition accounting in the powerlog database: CPU, GPU, and Neural Engine energy in nanojoules, converted to watt-hours (Wh = nJ / 3.6e12).
  • A "coalition" is an app plus all its helper processes, which is why the numbers map to apps the way you would expect.
  • macOS retains only about 3 days of this data; Juice's local store accumulates daily rollups indefinitely and keeps battery samples for 90 days, so your history grows beyond what the OS keeps.
  • Rollup rebuilds only replace days the source data fully covers, so macOS purging its own retention window can never erase Juice's stored history.
  • The displayed values have been audited against the raw database with independent SQL: stored per-app watt-hours matched the source to floating-point precision.

Development

swift build

# Tests (uses the full Xcode toolchain rather than Command Line Tools)
make test

# End-to-end XPC test against the installed helper
make dev-probe

# Per-app breakdown end-to-end (exactly what the detail window computes)
./.build/debug/JuiceXPCProbe --app com.microsoft.VSCode

Layout: Sources/Juice is the menu bar app, Sources/JuiceHelper is the root daemon, Sources/JuiceXPCShared is the XPC protocol, and Sources/JuiceCore holds the pure logic (store, rollups, insights, breakdowns) that the test suite covers.

Privacy

Everything stays on your Mac. Juice reads the system's power accounting and battery state, stores derived data in ~/Library/Application Support/Juice, and sends nothing anywhere.

Roadmap

  • Backfilling older history from the powerlog archive files macOS keeps on disk.

Status

Early development. The powerlog schema is undocumented Apple internals and can change in any macOS release; Juice probes the schema first and degrades gracefully (live battery data keeps working) rather than guessing.

License

MIT. See LICENSE.

About

macOS menu bar app: see what is eating your battery - per-app energy history, live drain watts, and plain-English insights

Topics

Resources

License

Contributing

Stars

1 star

Watchers

0 watching

Forks

Packages

 
 
 

Contributors