Pomo is a local-first Pomodoro timer for Android, with a Chrome extension and a thin desktop client. The phone owns the live clock, Room history, and Crew. Chrome has its own timer engine and local history. Desktop, NodeMCU, and the Omarchy plugin talk to the phone LAN API. Desk and Omarchy already run locally when the phone is gone, then import history and adopt a live timer on reconnect.
The UI is a focus instrument: large live time, dense stats, local history, and direct controls.
- Phone-owned Pomodoro, short break, and long break timer state.
- Foreground service for resilient timing across app restarts.
- Room-backed completed session history and daily stats.
- Local-calendar history handling, including sessions that cross midnight.
- Timer, Stats, History, Settings, notification, and home-screen widget updates from the same phone-owned state.
- Optional local HTTP/WebSocket API for desktop display and control.
- Pairing-token protection for remote commands.
- Thin TypeScript desktop client for terminal, Waybar, QR, and service flows.
- Optional Omarchy shell plugin with local fallback timing and phone sync.
- JDK 17+
- Android SDK, preferably at
~/Android/Sdk - A connected Android device or emulator for install/run checks
Set ANDROID_HOME if your shell does not already provide it:
export ANDROID_HOME="$HOME/Android/Sdk"Fast dev build:
./gradlew assembleDevDebugBuilder script, useful when this checkout needs to prepare its local SDK first:
./build_apk.shDebug APK:
app/build/outputs/apk/dev/debug/app-dev-debug.apk
Production release build:
./gradlew assembleProdReleaseProduction APK:
app/build/outputs/apk/prod/release/app-prod-release-unsigned.apk
Install and launch the dev build:
adb install -r -g app/build/outputs/apk/dev/debug/app-dev-debug.apk
adb shell am start -n com.pomo/.MainActivityUseful logs:
adb logcat -s PomodoroService PhoneServer- Open Pomo on the phone.
- Open Settings.
- Tap "Pair desktop client".
- Use the displayed JSON payload or QR code in the desktop client.
{
"url": "http://<phone-ip>:9876",
"token": "<pairing-token>"
}The phone and desktop must be on the same network. The default API port is
9876, configurable in Settings.
The desktop client stores pairing details, sends commands to the phone API, and keeps a best-effort stale cache for offline display.
npm --prefix desktop-client install
npm --prefix desktop-client run build
node desktop-client/dist/cli.js pair-json '{"url":"http://<phone-ip>:9876","token":"<pairing-token>"}'
node desktop-client/dist/cli.js status
node desktop-client/dist/cli.js toggle
node desktop-client/dist/cli.js qrBackground cache refresh service:
node desktop-client/dist/cli.js service install
node desktop-client/dist/cli.js service start
node desktop-client/dist/cli.js service statusSee docs/desktop-client.md for service paths, Waybar output, QR commands, and failure behavior.
The omarchy-plugin/ directory contains the raja.pomo Omarchy shell plugin
and the shared pomo-link engine. Omarchy gets a bar widget and panel. Waybar
runs the same engine as a user daemon (pomo-link --daemon) and streams JSON
from a status file. Both follow the phone on the LAN and run a local timer
while the phone is gone. See omarchy-plugin/README.md
for installation, pairing, and the Waybar module snippet.
app/src/main/java/com/pomo/
├── MainActivity.kt
├── service/ # PomodoroService, notifications, command receivers
├── timer/ # TimerState and OfflineTimer
├── db/ # Room database, sessions, daily stats
├── network/ # Embedded Ktor HTTP/WebSocket API
├── ui/ # Timer, Stats, History, Settings, About
├── util/ # Preferences, date logic, sound helpers
└── widget/ # Home-screen widget
State flow:
User, notification, widget, or API command
↓
PomodoroService
↓
OfflineTimer + Room history
↓
State broadcast
↓
UI, notification, widget, and WebSocket clients
PomodoroService is the write boundary. Read-only status paths must not mutate
timer state. Room is the canonical history store. The embedded API exposes phone
state over the local network; it does not merge state from a desktop process.
- docs/architecture.md: deeper implementation map.
- docs/protocol.md: HTTP/WebSocket API, authentication, and payloads.
- docs/desktop-client.md: CLI, service, cache, and Waybar behavior.
- omarchy-plugin/README.md: Omarchy installation, bar display settings, hover behavior, pairing, and Waybar.
Run the unit and build checks:
./run_tests.sh
./gradlew assembleDevDebugManual checks worth doing on device:
- App launches without any laptop/server process.
- Start, pause, resume, skip, reset, and extend mutate phone state.
- Completed focus sessions appear in Today, Stats, History, notification, and widget.
- A session crossing midnight is split across local calendar days, with seconds rounded up to minutes per day segment.
- Restarting the app restores stopped, paused, and running timer state sensibly.
GET /api/statusrejects missing tokens and returns state with a valid token./wsaccepts a valid hello token and streams state updates.- Desktop
status --waybarshows fresh phone state when reachable and stale offline state when not.
Releases are automated from main.
When a PR is merged, .github/workflows/version-bump.yml inspects the commit
messages in that push, bumps versionCode and versionName in
app/build.gradle.kts, commits the version bump back to main, and creates a
tag like v1.12.0.
The bump type follows Conventional Commits:
feat:creates a minor release.fix:orperf:creates a patch release.!orBREAKING CHANGE:creates a major release.- Anything else defaults to a patch release.
When a v* tag is pushed, .github/workflows/release.yml builds the dev debug
and unsigned prod release APKs, uploads them as workflow artifacts, and
publishes a GitHub Release with generated release notes.
- Android app package:
com.pomo - Minimum SDK: 26
- Target SDK: 34
- Current app version in this checkout:
1.12.0 - App-initiated cleartext traffic remains disabled; the embedded phone API is local-network HTTP protected by the pairing token.
- Pairing tokens are stored in dedicated non-backed-up shared preferences.


