#!/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()