A complete modernization of karpathy/ulogme for modern macOS (Apple Silicon compatible).
📜 Looking for the original legacy version? See old/README.md
ulogme is a personal activity tracker that runs in the background and logs:
- Active window titles — What apps and windows you're using
- Keystroke counts — How much you're typing (not what you type!)
- Browser URLs — Optional URL tracking for browsers
All data stays on your machine in a local DuckDB database. There are no cloud services or network calls.
- 🍎 Native macOS support (Apple Silicon & Intel)
- 🔒 Privacy-first: only counts keystrokes, never logs key values
- 📊 Beautiful React dashboard with charts and visualizations
- 📅 Logical day boundaries (late night sessions count as previous day)
- 📝 Add notes and daily blog entries
- ⚡ Fast DuckDB storage with powerful analytics queries
# Install Python dependencies
uv sync
# Start the tracker (runs in foreground)
uv run python -m tracker start
# With debug output to see what's being tracked
uv run python -m tracker start --verboseThe first time you run the tracker, macOS will prompt you to grant Accessibility permission for keystroke monitoring. Go to:
- System Settings → Privacy & Security → Accessibility
- Add your terminal app (Terminal, iTerm, etc.)
To have the tracker start automatically when you log in:
uv run python -m tracker installImportant for launchd service: You need to grant Accessibility permission to the Python binary itself:
- System Settings → Privacy & Security → Accessibility
- Click + and press Cmd+Shift+G
- Navigate to:
~/.local/share/uv/python/cpython-3.13.5-macos-aarch64-none/bin/ - Select
python3.13and click Open
To uninstall:
uv run python -m tracker uninstallcd site-template/
# Install dependencies
bun install
# Start the development server
bun run devOpen http://localhost:5173 to view your activity data.
# Start tracker in foreground
uv run python -m tracker start
# Stop running tracker
uv run python -m tracker stop
# Check status
uv run python -m tracker status
# Install as launchd service (auto-start on login)
uv run python -m tracker install
# Remove launchd service
uv run python -m tracker uninstallcd site-template/
# Development mode with hot reload
bun run dev
# Production build
bun run build
bun run prodEdit ulogme.toml to customize:
[tracking]
window_titles = true # Track window titles
browser_tabs = true # Track browser tab titles
browser_urls = true # Track full URLs (enables URL-based categorization)
keystrokes = true # Count keystrokes
window_poll_interval = 2 # Seconds between window checks
keystroke_window = 9 # Seconds to aggregate keystrokes
[day_boundary]
hour = 7 # Day starts at 7am (late night = previous day)
[hacking]
# Categories considered "focused work" for hacking streak
categories = ["Coding", "Terminal", "Dev", "AI"]Categories are assigned via regex patterns in order (first match wins). URL-based rules let you distinguish browser activity:
# AI assistants
[[category_mappings.rules]]
pattern = "chatgpt\\.com|claude\\.ai|perplexity\\.ai"
category = "AI"
# Browser URLs (matched against window title which contains the URL)
[[category_mappings.rules]]
pattern = "mail\\.google\\.com|outlook\\.live\\.com"
category = "Email"
[[category_mappings.rules]]
pattern = "github\\.com|stackoverflow\\.com"
category = "Coding"
[[category_mappings.rules]]
pattern = "localhost|127\\.0\\.0\\.1"
category = "Dev"
[[category_mappings.rules]]
pattern = "twitter\\.com|x\\.com|reddit\\.com"
category = "Social"
[[category_mappings.rules]]
pattern = "youtube\\.com|netflix\\.com"
category = "Video"
# Native apps (fallback after URL rules)
[[category_mappings.rules]]
pattern = "VS Code|Cursor|PyCharm"
category = "Coding"
[[category_mappings.rules]]
pattern = "Terminal|iTerm|Warp"
category = "Terminal"
# Generic browser (catch-all, should be last)
[[category_mappings.rules]]
pattern = "Google Chrome|Safari|Arc"
category = "Browser"Categories are applied dynamically at display time — updating the config immediately affects all historical data (no backfill needed).
ulogme/
├── tracker/ # Python daemon
│ ├── daemon.py # Main entry point
│ ├── window.py # Window tracking (PyObjC)
│ ├── keyboard.py # Keystroke counting
│ ├── storage.py # DuckDB operations
│ ├── config.py # Configuration loading
│ └── launchd.py # macOS service integration
├── site-template/ # React dashboard
│ ├── server.ts # Hono API server
│ ├── backend-lib/
│ │ ├── ulogme-db.ts # DuckDB queries
│ │ └── config.ts # TOML config & categorization
│ └── src/
│ ├── pages/ # React pages (DayView, Overview, Settings)
│ └── components/ # UI components (charts, panels)
├── data/ # Database and logs
│ └── ulogme.duckdb
├── old/ # Legacy Python 2.7 version
│ └── README.md # Original documentation
├── pyproject.toml # Python dependencies
├── ulogme.toml # Configuration (edit this!)
└── REWRITE_PLAN.md # Architecture documentation
All data is stored in data/ulogme.duckdb, a local DuckDB database file.
window_events— Active window changes with timestampskey_events— Keystroke counts per time windownotes— User annotationsdaily_blog— Daily journal entriessettings— User preferences
# Quick check of recent data
uv run python -c "
import duckdb
conn = duckdb.connect('data/ulogme.duckdb', read_only=True)
print('Window events:', conn.execute('SELECT COUNT(*) FROM window_events').fetchone()[0])
print('Key events:', conn.execute('SELECT COUNT(*) FROM key_events').fetchone()[0])
print('Latest:', conn.execute('SELECT timestamp, app_name FROM window_events ORDER BY timestamp DESC LIMIT 3').fetchall())
conn.close()
"The web dashboard exposes a REST API:
| Endpoint | Description |
|---|---|
GET /api/ulogme/dates |
List all dates with data |
GET /api/ulogme/day/:date |
Get all data for a specific date |
GET /api/ulogme/day/:date/categories |
Get category breakdown with durations |
GET /api/ulogme/day/:date/apps |
Get app usage breakdown |
GET /api/ulogme/overview |
Get aggregated stats across dates |
GET /api/ulogme/config |
Get category rules and colors |
POST /api/ulogme/note |
Add a note |
PUT /api/ulogme/blog/:date |
Save daily blog entry |
-
Check if it's running:
uv run python -m tracker status
-
Run in verbose mode to debug:
uv run python -m tracker start --verbose
You should see
[HH:MM:SS] poll tickevery 2 seconds and window/keystroke events. -
Check for errors in logs:
cat data/tracker.log cat data/tracker.error.log
Keystroke monitoring requires Accessibility permission:
- For foreground mode: Grant permission to your terminal app
- For launchd service: Grant permission to the Python binary itself
If you see "Conflicting lock" errors, make sure only one process is writing:
- Stop the web server before running database queries
- The tracker briefly locks the DB for writes every few seconds
Check the launchd status:
launchctl list | grep ulogmeExit code 0 = running, any other number = check logs.
- Keystroke counting only — We never log actual key characters
- Local storage only — All data stays on your machine
- No network calls — The tracker daemon is fully offline
- Accessibility permission — Required for global key monitoring
| Layer | Technology |
|---|---|
| Package Managers | uv (Python), Bun (TypeScript) |
| Database | DuckDB (embedded, columnar, analytics-optimized) |
| Backend | Hono on Bun |
| Frontend | React 19 + Vite + Tailwind CSS + shadcn/ui |
| Charts | Recharts via shadcn/ui ChartContainer |
| Tracker | Python 3.13 + PyObjC (native macOS APIs) |
Original project: karpathy/ulogme by Andrej Karpathy
This is a complete rewrite with modern tooling while preserving the core concepts.


