240 lines
7.2 KiB
Markdown
240 lines
7.2 KiB
Markdown
# Zed Hours Tracker
|
|
|
|
A lightweight, local-first, background time tracker for Zed editor sessions, with automatic git commit logging. No cloud, no accounts, no manual timers.
|
|
|
|
Works on **Arch Linux + Wayland** and is designed to be portable to other projects.
|
|
|
|
---
|
|
|
|
## What it does
|
|
|
|
1. **Detects when you are working in Zed** on this project by monitoring the active window title (Wayland) and filesystem activity.
|
|
2. **Writes heartbeats** to `.zed-hours/heartbeats.jsonl`:
|
|
- timestamp
|
|
- project path
|
|
- focused file (if detectable)
|
|
- idle status
|
|
3. **Logs git commits** automatically via `post-commit` hook to `.zed-hours/commits.jsonl`.
|
|
4. **Generates markdown reports** combining heartbeats + commits into daily summaries.
|
|
|
|
---
|
|
|
|
## Files
|
|
|
|
| File | Purpose |
|
|
|---|---|
|
|
| `install.py` | Install git hook and create `.zed-hours/` data directory |
|
|
| `log_commit.py` | Called by git hook; do not run directly usually |
|
|
| `zed_tracker.py` | Background tracker for Zed activity |
|
|
| `generate_report.py` | Generate a markdown hours report |
|
|
| `sync.py` | Opt-in incremental sync of local logs to a Zed Hours server |
|
|
| `README.md` | This file |
|
|
|
|
---
|
|
|
|
## Installation
|
|
|
|
Run from the project root:
|
|
|
|
```bash
|
|
python3 tools/time_tracking/install.py
|
|
```
|
|
|
|
This will:
|
|
- create `.zed-hours/` in the project root;
|
|
- install a `post-commit` git hook that logs commits;
|
|
- create a default config file.
|
|
|
|
---
|
|
|
|
## Running the tracker
|
|
|
|
The tracker is intentionally a simple script you run in the background.
|
|
|
|
### Option A: One-off test
|
|
|
|
```bash
|
|
python3 tools/time_tracking/zed_tracker.py --project-root $(pwd)
|
|
```
|
|
|
|
Press `Ctrl+C` to stop.
|
|
|
|
### Option B: Zed tasks (run from the editor)
|
|
|
|
This project includes Zed tasks so you can trigger tracker actions with the task runner shortcut (`alt+shift+t` or `ctrl+shift+t` depending on your keymap). The default working directory for Zed tasks is the project root, so the commands use relative paths.
|
|
|
|
| Task | Action |
|
|
|---|---|
|
|
| `Zed Hours: Install tracker` | Install git hook, config, and systemd service |
|
|
| `Zed Hours: Start tracker (foreground)` | Start the tracker in a Zed terminal panel |
|
|
| `Zed Hours: Generate report (today)` | Generate today's report |
|
|
| `Zed Hours: Generate report (yesterday)` | Generate yesterday's report |
|
|
| `Zed Hours: Generate report (last 14 days)` | Generate a 14-day report |
|
|
| `Zed Hours: Open latest report` | Print path to latest report |
|
|
| `Zed Hours: Status` | Show heartbeat/commit/report counts |
|
|
|
|
### Option C: Background via systemd user service (recommended for daily use)
|
|
|
|
A sample service file is generated by `install.py` at:
|
|
|
|
```
|
|
~/.config/systemd/user/zed-hours-gem360.service
|
|
```
|
|
|
|
Enable it:
|
|
|
|
```bash
|
|
systemctl --user daemon-reload
|
|
systemctl --user enable --now zed-hours-gem360.service
|
|
systemctl --user status zed-hours-gem360.service
|
|
```
|
|
|
|
Logs:
|
|
|
|
```bash
|
|
journalctl --user -u zed-hours-gem360.service -f
|
|
```
|
|
|
|
To stop:
|
|
|
|
```bash
|
|
systemctl --user stop zed-hours-gem360.service
|
|
```
|
|
|
|
### Option C: `tmux` / `screen`
|
|
|
|
```bash
|
|
tmux new -s zed-hours -d 'python3 tools/time_tracking/zed_tracker.py --project-root $(pwd)'
|
|
```
|
|
|
|
---
|
|
|
|
## Automatic sync (systemd user timer)
|
|
|
|
`sync_all.py` syncs every configured project in one run. Configuration lives
|
|
in `~/.config/zed-hours-sync/`:
|
|
|
|
| File | Purpose |
|
|
|---|---|
|
|
| `env` | `ZED_HOURS_SERVER_URL`, `ZED_HOURS_SERVER_TOKEN`, `ZED_HOURS_DEVICE_ID` (keep mode 600) |
|
|
| `projects.txt` | One project per line: `PATH [SLUG]`; slug defaults to the directory name |
|
|
|
|
A systemd user pair drives it:
|
|
|
|
- `zed-hours-sync.service` (oneshot; runs `sync_all.py` with the env file)
|
|
- `zed-hours-sync.timer` (every 15 minutes, `Persistent=true` to catch up after sleep)
|
|
|
|
Useful commands:
|
|
|
|
```sh
|
|
systemctl --user start zed-hours-sync.service # run a sync now
|
|
systemctl --user list-timers zed-hours-sync.timer # next scheduled run
|
|
journalctl --user -u zed-hours-sync.service -f # live logs
|
|
```
|
|
|
|
---
|
|
|
|
## Generating a report
|
|
|
|
```bash
|
|
python3 tools/time_tracking/generate_report.py --from 2026-08-03 --to yesterday
|
|
```
|
|
|
|
Output is written to `.zed-hours/reports/YYYY-MM-DD_to_YYYY-MM-DD.md`.
|
|
|
|
You can also run:
|
|
|
|
```bash
|
|
python3 tools/time_tracking/generate_report.py --today
|
|
```
|
|
|
|
---
|
|
|
|
## Optional server sync
|
|
|
|
The tracker remains local-first. To opt in to sending the locally recorded heartbeats and commits to a compatible server, set these environment variables:
|
|
|
|
```bash
|
|
export ZED_HOURS_SERVER_URL="https://hours.example.com"
|
|
export ZED_HOURS_SERVER_TOKEN="your-bearer-token"
|
|
export ZED_HOURS_DEVICE_ID="laptop-1"
|
|
```
|
|
|
|
Then run from the tracked project root:
|
|
|
|
```bash
|
|
python3 tools/time_tracking/sync.py
|
|
```
|
|
|
|
The command posts heartbeats to `/api/v1/sync/zed-heartbeats` and commits to `/api/v1/sync/zed-commits`. It sends `device_id`, `project_slug`, and records in batches. The slug defaults to the project root directory name and can be overridden without exposing the absolute local project path:
|
|
|
|
```bash
|
|
python3 tools/time_tracking/sync.py \
|
|
--project-root /path/to/project \
|
|
--project-slug customer-portal
|
|
```
|
|
|
|
Progress is recorded locally in `.zed-hours/sync-state.json`. Retrying after a failed request is safe: each record has a deterministic, device-specific `source_id`, allowing the server to treat duplicate submissions as idempotent.
|
|
|
|
---
|
|
|
|
## How activity detection works on Wayland
|
|
|
|
Wayland does not expose a global active-window API for security reasons. The tracker tries several methods, in order:
|
|
|
|
1. **Hyprland**: `hyprctl activewindow`
|
|
2. **Sway**: `swaymsg -t get_tree`
|
|
3. **GNOME / Mutter**: D-Bus `GetActiveWindow` via `gdbus`
|
|
4. **KDE / KWin**: D-Bus via `qdbus`
|
|
5. **Fallback**: monitor filesystem activity inside the project root using `inotify` or polling recent file access/modification times.
|
|
|
|
If all window-detection methods fail, the fallback still gives you a reliable signal that someone is actively editing files in this project.
|
|
|
|
---
|
|
|
|
## Reusing in another project
|
|
|
|
Copy the `tools/time_tracking/` directory to the new project and run:
|
|
|
|
```bash
|
|
python3 tools/time_tracking/install.py
|
|
```
|
|
|
|
The scripts detect the project root automatically from the current working directory.
|
|
|
|
---
|
|
|
|
## Data format
|
|
|
|
### `.zed-hours/heartbeats.jsonl`
|
|
|
|
```json
|
|
{"timestamp": "2026-08-26T09:15:00", "project": "/home/fegger/Code/ixsol/gem360-git", "file": "README.md", "active": true, "source": "hyprctl"}
|
|
```
|
|
|
|
### `.zed-hours/commits.jsonl`
|
|
|
|
```json
|
|
{"type": "commit", "timestamp": "2026-08-26T09:20:00", "repo": "/home/fegger/Code/ixsol/gem360-git", "branch": "va_module", "sha": "abc12345", "message": "[ADD] feature X", "author": "Florian Egger"}
|
|
```
|
|
|
|
### `.zed-hours/config.json`
|
|
|
|
```json
|
|
{
|
|
"project_root": "/home/fegger/Code/ixsol/gem360-git",
|
|
"heartbeat_interval_seconds": 60,
|
|
"idle_threshold_seconds": 300,
|
|
"log_dir": ".zed-hours"
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Notes
|
|
|
|
- This tool is intentionally simple and local. It is not a replacement for a commercial time tracker, but it is a reliable fallback when you forget to log hours.
|
|
- The tracker does not send data anywhere unless you explicitly run `sync.py` with `ZED_HOURS_SERVER_URL`, `ZED_HOURS_SERVER_TOKEN`, and `ZED_HOURS_DEVICE_ID` configured.
|
|
- Heartbeat granularity is one minute by default. Tune it in `config.json`.
|
|
- Because of Wayland security, window detection may not give the exact filename. The filesystem fallback compensates for this.
|