From 94566aa82078d6ed6a5ceea3e0fc21a7b7103d17 Mon Sep 17 00:00:00 2001 From: Florian Egger Date: Wed, 2 Sep 2026 12:29:01 +0200 Subject: [PATCH] Add Waybar time tracker with README, styles, and config Includes a dependency-free Python CLI, Waybar JSON output, persistent JSON storage, and Markdown timesheet generation grouped by project and day. Also adds example Waybar configuration and CSS. --- README.md | 63 ++++++ __pycache__/time_track.cpython-314.pyc | Bin 0 -> 13893 bytes style.css | 17 ++ time_track.py | 290 +++++++++++++++++++++++++ waybar-config.json | 12 + 5 files changed, 382 insertions(+) create mode 100644 README.md create mode 100644 __pycache__/time_track.cpython-314.pyc create mode 100644 style.css create mode 100755 time_track.py create mode 100644 waybar-config.json diff --git a/README.md b/README.md new file mode 100644 index 0000000..3468ccf --- /dev/null +++ b/README.md @@ -0,0 +1,63 @@ +# Waybar Time Tracker + +A small, dependency-free Python time tracker for Waybar with a start/stop button and timesheet generation. + +## Features + +- **Start / stop button** in Waybar: click the module to toggle tracking. +- **Live elapsed time** shown in the bar while running. +- **Persistent JSON storage** in `~/.local/share/time_track/data.json` (override with `TIME_TRACK_DIR`). +- **Timesheet generation** in Markdown, grouped by project and by day. + +## Files + +| File | Purpose | +|------|---------| +| `time_track.py` | Main CLI and Waybar interface | +| `waybar-config.json` | Example Waybar custom module configuration | +| `style.css` | Example CSS for the running/idle states | + +## Installation + +1. Make sure `time_track.py` is executable: + + ```sh + chmod +x /path/to/time_track/time_track.py + ``` + +2. Add the custom module from `waybar-config.json` to your Waybar config (`~/.config/waybar/config`). +3. Add the CSS from `style.css` to your Waybar stylesheet (`~/.config/waybar/style.css`). +4. Update the paths in `waybar-config.json` to point to wherever you place `time_track.py`. + +## CLI usage + +```sh +# Toggle tracking for the default project +./time_track.py toggle + +# Toggle tracking for a specific project +./time_track.py toggle "client-a" + +# Emit Waybar JSON (used by the module) +./time_track.py status + +# Show the active session +./time_track.py current + +# Stop the active session +./time_track.py stop + +# Show recent entries +./time_track.py log +./time_track.py log 25 + +# Generate a Markdown timesheet +./time_track.py timesheet week +./time_track.py timesheet month +``` + +## Waybar behavior + +- **Left click**: start or stop the current session. +- **Right click**: print the current week's Markdown timesheet to stdout (useful bound to a notification or terminal). +- The module updates every 5 seconds while running. diff --git a/__pycache__/time_track.cpython-314.pyc b/__pycache__/time_track.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..08b73b9e5c9e4fc5344b3aa6a4b687940c39d960 GIT binary patch literal 13893 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 literal 0 HcmV?d00001 diff --git a/style.css b/style.css new file mode 100644 index 0000000..1a7defb --- /dev/null +++ b/style.css @@ -0,0 +1,17 @@ +/* Waybar CSS for the time tracker module */ +#custom-timetrack { + padding: 0 12px; + border-radius: 4px; + margin: 4px 2px; + font-weight: bold; +} + +#custom-timetrack.idle { + background-color: #3b3b3b; + color: #a0a0a0; +} + +#custom-timetrack.running { + background-color: #2e7d32; + color: #ffffff; +} diff --git a/time_track.py b/time_track.py new file mode 100755 index 0000000..976bb17 --- /dev/null +++ b/time_track.py @@ -0,0 +1,290 @@ +#!/usr/bin/env python3 +"""Time tracker for Waybar. + +Usage: + time_track.py toggle [project_name] -- start or stop a session + time_track.py status -- emit Waybar JSON + time_track.py log [n] -- show last n entries (default 10) + time_track.py timesheet [week|month] -- generate a Markdown timesheet + time_track.py current -- show the active session, if any + time_track.py stop -- stop the active session +""" + +import argparse +import json +import os +import sys +from datetime import datetime, timedelta +from pathlib import Path + +DATA_DIR = Path( + os.environ.get("TIME_TRACK_DIR", Path.home() / ".local" / "share" / "time_track") +) +DATA_FILE = DATA_DIR / "data.json" +DEFAULT_PROJECT = "default" + + +def load_data(): + DATA_DIR.mkdir(parents=True, exist_ok=True) + if DATA_FILE.exists(): + try: + with DATA_FILE.open("r", encoding="utf-8") as f: + return json.load(f) + except (json.JSONDecodeError, OSError): + pass + return {"active": False, "current": None, "entries": []} + + +def save_data(data): + DATA_DIR.mkdir(parents=True, exist_ok=True) + with DATA_FILE.open("w", encoding="utf-8") as f: + json.dump(data, f, indent=2) + + +def now_iso(): + return datetime.now().isoformat(timespec="seconds") + + +def parse_iso(value): + return datetime.fromisoformat(value) + + +def format_duration(seconds): + sign = "-" if seconds < 0 else "" + seconds = abs(int(seconds)) + hours, remainder = divmod(seconds, 3600) + minutes, secs = divmod(remainder, 60) + parts = [] + if hours: + parts.append(f"{hours}h") + if minutes: + parts.append(f"{minutes}m") + if not hours and not minutes: + parts.append(f"{secs}s") + return sign + " ".join(parts) + + +def format_time(dt): + return dt.strftime("%H:%M") + + +def format_date(dt): + return dt.strftime("%Y-%m-%d") + + +def toggle(project=None): + data = load_data() + project = project or DEFAULT_PROJECT + + if data["active"]: + start = parse_iso(data["current"]["start"]) + end = datetime.now() + duration = int((end - start).total_seconds()) + entry = { + "project": data["current"]["project"], + "start": data["current"]["start"], + "end": end.isoformat(timespec="seconds"), + "duration": duration, + } + data["entries"].append(entry) + data["active"] = False + data["current"] = None + save_data(data) + print( + f"Stopped: {entry['project']} @ {format_time(end)} " + f"({format_duration(duration)})", + file=sys.stderr, + ) + else: + data["active"] = True + data["current"] = { + "project": project, + "start": now_iso(), + } + save_data(data) + print( + f"Started: {project} @ {format_time(datetime.now())}", + file=sys.stderr, + ) + + +def stop(): + data = load_data() + if not data["active"]: + print("No active session.", file=sys.stderr) + return + toggle() + + +def status(): + data = load_data() + if data["active"]: + start = parse_iso(data["current"]["start"]) + elapsed = int((datetime.now() - start).total_seconds()) + elapsed_text = format_duration(elapsed) + waybar = { + "text": f"⏱ {elapsed_text}", + "tooltip": ( + f"Project: {data['current']['project']}\n" + f"Started: {format_time(start)} ({format_date(start)})\n" + f"Elapsed: {elapsed_text}" + ), + "class": "running", + } + else: + last_entry = data["entries"][-1] if data["entries"] else None + if last_entry: + tooltip = ( + f"Last: {last_entry['project']} " + f"@ {format_time(parse_iso(last_entry['end']))} " + f"({format_duration(last_entry['duration'])}). Click to start." + ) + else: + tooltip = "Click to start tracking." + waybar = { + "text": "⏱ --:--", + "tooltip": tooltip, + "class": "idle", + } + print(json.dumps(waybar)) + + +def current(): + data = load_data() + if data["active"]: + start = parse_iso(data["current"]["start"]) + elapsed = int((datetime.now() - start).total_seconds()) + print( + f"Project: {data['current']['project']}\n" + f"Started: {start.isoformat(' ', timespec='seconds')}\n" + f"Elapsed: {format_duration(elapsed)}" + ) + else: + print("No active session.") + + +def log_cmd(count=10): + data = load_data() + entries = data["entries"][-count:] + if not entries: + print("No logged entries yet.") + return + print(f"{'Start':20} {'End':20} {'Project':12} {'Duration':10}") + print("-" * 65) + for e in entries: + start = parse_iso(e["start"]) + end = parse_iso(e["end"]) + print( + f"{start.isoformat(' ', timespec='seconds'):20} " + f"{end.isoformat(' ', timespec='seconds'):20} " + f"{e['project']:12} {format_duration(e['duration']):>10}" + ) + + +def timesheet(period="week"): + data = load_data() + today = datetime.now().date() + + if period == "week": + start_date = today - timedelta(days=today.weekday()) + end_date = start_date + timedelta(days=6) + elif period == "month": + start_date = today.replace(day=1) + next_month = (today.replace(day=28) + timedelta(days=4)).replace(day=1) + end_date = (next_month - timedelta(days=1)) + else: + start_date = today - timedelta(days=7) + end_date = today + + entries = [ + e for e in data["entries"] + if start_date <= parse_iso(e["start"]).date() <= end_date + ] + + if not entries: + print(f"No entries for {period} ({start_date} → {end_date}).") + return + + by_project: dict[str, int] = {} + by_date: dict[str, int] = {} + rows = [] + + for e in entries: + start = parse_iso(e["start"]) + end = parse_iso(e["end"]) + duration = e["duration"] + date_key = format_date(start) + by_project[e["project"]] = by_project.get(e["project"], 0) + duration + by_date[date_key] = by_date.get(date_key, 0) + duration + rows.append( + f"| {format_date(start)} | {format_time(start)} | " + f"{format_time(end)} | {e['project']} | " + f"{format_duration(duration)} |" + ) + + total = sum(by_project.values()) + + print(f"# Timesheet: {period} ({start_date} → {end_date})") + print() + print("| Date | Start | End | Project | Duration |") + print("|------|-------|-----|---------|----------|") + for row in rows: + print(row) + print() + print("## Summary by project") + print() + print("| Project | Total |") + print("|---------|-------|") + for project, seconds in sorted(by_project.items()): + print(f"| {project} | {format_duration(seconds)} |") + print(f"| **Total** | **{format_duration(total)}** |") + print() + print("## Summary by day") + print() + print("| Day | Total |") + print("|-----|-------|") + for date_key in sorted(by_date): + print(f"| {date_key} | {format_duration(by_date[date_key])} |") + + +def main(): + parser = argparse.ArgumentParser(description="Waybar time tracker") + subparsers = parser.add_subparsers(dest="command", required=True) + + p_toggle = subparsers.add_parser("toggle", help="start or stop a session") + p_toggle.add_argument("project", nargs="?", default=DEFAULT_PROJECT) + + subparsers.add_parser("status", help="emit Waybar JSON") + subparsers.add_parser("current", help="show the active session") + subparsers.add_parser("stop", help="stop the active session") + + p_log = subparsers.add_parser("log", help="show recent entries") + p_log.add_argument("count", nargs="?", type=int, default=10) + + p_sheet = subparsers.add_parser("timesheet", help="generate a Markdown timesheet") + p_sheet.add_argument( + "period", + nargs="?", + choices=["week", "month"], + default="week", + help="time period to summarize", + ) + + args = parser.parse_args() + + if args.command == "toggle": + toggle(args.project) + elif args.command == "status": + status() + elif args.command == "current": + current() + elif args.command == "stop": + stop() + elif args.command == "log": + log_cmd(args.count) + elif args.command == "timesheet": + timesheet(args.period) + + +if __name__ == "__main__": + main() diff --git a/waybar-config.json b/waybar-config.json new file mode 100644 index 0000000..c9ee5cb --- /dev/null +++ b/waybar-config.json @@ -0,0 +1,12 @@ +{ + "modules-center": ["custom/timetrack"], + "custom/timetrack": { + "exec": "/home/fegger/Code/projects/time_track/time_track.py status", + "interval": 5, + "format": "{}", + "return-type": "json", + "on-click": "/home/fegger/Code/projects/time_track/time_track.py toggle", + "on-click-right": "/home/fegger/Code/projects/time_track/time_track.py timesheet week", + "tooltip": true + } +}