Add habits and time tracking - habits.py: Daily habit tracking with streaks - track.py: Time tracking with projects - Updated ws CLI with new commands - Data stored in data/ directory
This commit is contained in:
219
tools/track.py
Executable file
219
tools/track.py
Executable file
@ -0,0 +1,219 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
track - Simple time tracking
|
||||
|
||||
Log what you're working on and track time spent.
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
WORKSPACE = Path("/home/wdjones/.openclaw/workspace")
|
||||
TRACK_FILE = WORKSPACE / "data" / "timetrack.json"
|
||||
|
||||
def load_data() -> dict:
|
||||
"""Load tracking data."""
|
||||
TRACK_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
if TRACK_FILE.exists():
|
||||
with open(TRACK_FILE) as f:
|
||||
return json.load(f)
|
||||
return {'current': None, 'entries': []}
|
||||
|
||||
def save_data(data: dict):
|
||||
"""Save tracking data."""
|
||||
with open(TRACK_FILE, 'w') as f:
|
||||
json.dump(data, f, indent=2)
|
||||
|
||||
def start(task: str, project: str = None):
|
||||
"""Start tracking a task."""
|
||||
data = load_data()
|
||||
|
||||
# Stop current task if running
|
||||
if data['current']:
|
||||
stop_current(data)
|
||||
|
||||
data['current'] = {
|
||||
'task': task,
|
||||
'project': project,
|
||||
'start': datetime.now().isoformat(),
|
||||
}
|
||||
save_data(data)
|
||||
|
||||
print(f"⏱️ Started: {task}")
|
||||
if project:
|
||||
print(f" Project: {project}")
|
||||
|
||||
def stop():
|
||||
"""Stop the current task."""
|
||||
data = load_data()
|
||||
|
||||
if not data['current']:
|
||||
print("Nothing being tracked")
|
||||
return
|
||||
|
||||
stop_current(data)
|
||||
save_data(data)
|
||||
|
||||
def stop_current(data: dict):
|
||||
"""Stop and log current task."""
|
||||
current = data['current']
|
||||
start = datetime.fromisoformat(current['start'])
|
||||
end = datetime.now()
|
||||
duration = (end - start).total_seconds() / 60 # minutes
|
||||
|
||||
entry = {
|
||||
'task': current['task'],
|
||||
'project': current.get('project'),
|
||||
'start': current['start'],
|
||||
'end': end.isoformat(),
|
||||
'duration_min': round(duration, 1),
|
||||
}
|
||||
data['entries'].append(entry)
|
||||
data['current'] = None
|
||||
|
||||
print(f"⏹️ Stopped: {current['task']}")
|
||||
print(f" Duration: {format_duration(duration)}")
|
||||
|
||||
def format_duration(minutes: float) -> str:
|
||||
"""Format minutes as human readable."""
|
||||
if minutes < 60:
|
||||
return f"{minutes:.0f}m"
|
||||
hours = int(minutes // 60)
|
||||
mins = int(minutes % 60)
|
||||
return f"{hours}h {mins}m"
|
||||
|
||||
def status():
|
||||
"""Show current tracking status."""
|
||||
data = load_data()
|
||||
|
||||
if data['current']:
|
||||
current = data['current']
|
||||
start = datetime.fromisoformat(current['start'])
|
||||
elapsed = (datetime.now() - start).total_seconds() / 60
|
||||
|
||||
print(f"\n⏱️ Currently tracking:")
|
||||
print(f" Task: {current['task']}")
|
||||
if current.get('project'):
|
||||
print(f" Project: {current['project']}")
|
||||
print(f" Elapsed: {format_duration(elapsed)}")
|
||||
else:
|
||||
print("\n⏸️ Not tracking anything")
|
||||
|
||||
# Today's summary
|
||||
today = datetime.now().strftime("%Y-%m-%d")
|
||||
today_entries = [e for e in data['entries'] if e['start'].startswith(today)]
|
||||
|
||||
if today_entries:
|
||||
total = sum(e['duration_min'] for e in today_entries)
|
||||
print(f"\n📊 Today: {format_duration(total)} tracked")
|
||||
|
||||
# Group by project
|
||||
by_project = {}
|
||||
for e in today_entries:
|
||||
proj = e.get('project') or 'No project'
|
||||
by_project[proj] = by_project.get(proj, 0) + e['duration_min']
|
||||
|
||||
for proj, mins in sorted(by_project.items(), key=lambda x: -x[1]):
|
||||
print(f" {proj}: {format_duration(mins)}")
|
||||
|
||||
def report(days: int = 7):
|
||||
"""Show time tracking report."""
|
||||
data = load_data()
|
||||
|
||||
cutoff = datetime.now() - timedelta(days=days)
|
||||
entries = [e for e in data['entries']
|
||||
if datetime.fromisoformat(e['start']) > cutoff]
|
||||
|
||||
if not entries:
|
||||
print(f"No entries in the last {days} days")
|
||||
return
|
||||
|
||||
print(f"\n📊 Time Report (last {days} days)")
|
||||
print("=" * 40)
|
||||
|
||||
total = sum(e['duration_min'] for e in entries)
|
||||
print(f"Total: {format_duration(total)}")
|
||||
|
||||
# By project
|
||||
by_project = {}
|
||||
for e in entries:
|
||||
proj = e.get('project') or 'No project'
|
||||
by_project[proj] = by_project.get(proj, 0) + e['duration_min']
|
||||
|
||||
print(f"\nBy Project:")
|
||||
for proj, mins in sorted(by_project.items(), key=lambda x: -x[1]):
|
||||
pct = mins / total * 100
|
||||
bar = "█" * int(pct / 5)
|
||||
print(f" {proj:20} {format_duration(mins):>8} {bar}")
|
||||
|
||||
# By day
|
||||
by_day = {}
|
||||
for e in entries:
|
||||
day = e['start'][:10]
|
||||
by_day[day] = by_day.get(day, 0) + e['duration_min']
|
||||
|
||||
print(f"\nBy Day:")
|
||||
for day, mins in sorted(by_day.items()):
|
||||
bar = "█" * int(mins / 30) # 30 min per block
|
||||
print(f" {day} {format_duration(mins):>8} {bar}")
|
||||
|
||||
def log_entry(task: str, minutes: int, project: str = None):
|
||||
"""Log a past entry manually."""
|
||||
data = load_data()
|
||||
|
||||
end = datetime.now()
|
||||
start = end - timedelta(minutes=minutes)
|
||||
|
||||
entry = {
|
||||
'task': task,
|
||||
'project': project,
|
||||
'start': start.isoformat(),
|
||||
'end': end.isoformat(),
|
||||
'duration_min': minutes,
|
||||
}
|
||||
data['entries'].append(entry)
|
||||
save_data(data)
|
||||
|
||||
print(f"✓ Logged: {task} ({format_duration(minutes)})")
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage:")
|
||||
print(" track start <task> [project] - Start tracking")
|
||||
print(" track stop - Stop current task")
|
||||
print(" track status - Show current status")
|
||||
print(" track report [days] - Show time report")
|
||||
print(" track log <task> <mins> [proj] - Log past entry")
|
||||
print("")
|
||||
print("Examples:")
|
||||
print(" track start 'Writing docs' docs")
|
||||
print(" track stop")
|
||||
print(" track log 'Meeting' 30 work")
|
||||
status()
|
||||
return
|
||||
|
||||
cmd = sys.argv[1]
|
||||
|
||||
if cmd == 'start' and len(sys.argv) > 2:
|
||||
task = sys.argv[2]
|
||||
project = sys.argv[3] if len(sys.argv) > 3 else None
|
||||
start(task, project)
|
||||
elif cmd == 'stop':
|
||||
stop()
|
||||
elif cmd == 'status':
|
||||
status()
|
||||
elif cmd == 'report':
|
||||
days = int(sys.argv[2]) if len(sys.argv) > 2 else 7
|
||||
report(days)
|
||||
elif cmd == 'log' and len(sys.argv) > 3:
|
||||
task = sys.argv[2]
|
||||
minutes = int(sys.argv[3])
|
||||
project = sys.argv[4] if len(sys.argv) > 4 else None
|
||||
log_entry(task, minutes, project)
|
||||
else:
|
||||
print("Unknown command. Run 'track' for help.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user