Skip to content

Instantly share code, notes, and snippets.

@tcard
Created September 6, 2021 15:06
Show Gist options
  • Save tcard/b754e7922b6a3605050ffd9ce3347e58 to your computer and use it in GitHub Desktop.
Save tcard/b754e7922b6a3605050ffd9ce3347e58 to your computer and use it in GitHub Desktop.
Poor man's time tracker for projects
#!/usr/bin/env python
# LICENSE: See the bottom of this file.
# README
#
# Put this script in a directory. Make it executable. Edit the projects list
# below.
#
# The script will let you pick a project, then keep track of time spent on it
# in the current week using plain text files in this directory. An archive of
# data from past weeks is kept too.
#
# While running, it will keep printing the time tracked this week.
#
# Terminate the process (Ctrl+C works) to stop the timer.
#
# /README
projects = [
# List your projects here, as strings (that are valid file names too).
]
import os
import sys
import signal
import time
from datetime import datetime, timedelta, date
script_path = os.path.abspath(__file__)
script_dir = os.path.dirname(script_path)
def project_file(project):
return f"{script_dir}/{project}"
def elapsed(lines):
sum = timedelta()
try:
while True:
on = datetime.fromisoformat(next(lines).split(" ")[0])
off = datetime.fromisoformat(next(lines).split(" ")[0])
sum += off - on
except StopIteration:
pass
return sum, on
def format_elapsed(delta):
hours = int(delta.total_seconds() / 60 / 60)
rest = delta - timedelta(seconds=hours * 60 * 60)
rest = rest - timedelta(microseconds=rest.microseconds)
return f"{hours} hours + {rest}"
def menu():
signal.signal(signal.SIGINT, signal.SIG_DFL)
rows = []
for i, project in enumerate(projects):
e = ''
try:
with open(project_file(project)) as f:
sum, _ = elapsed(f)
e = format_elapsed(sum)
except FileNotFoundError:
pass
rows.append((str(i + 1), project, e))
widths = [max(map(len, col)) for col in zip(*rows)]
for row in rows:
print(" ".join((val.ljust(width) for val, width in zip(row, widths))))
selected = projects[int(input('which? ')) - 1]
signal.signal(signal.SIGINT, menu)
os.system(f"{script_path} {selected}")
if len(sys.argv) <= 1:
menu()
sys.exit()
project = sys.argv[1]
file_path = f"{script_dir}/{project}"
try:
lines = None
with open(file_path) as file:
lines = [l.rstrip('\n') for l in file]
last_off_date, state = lines[-1].split(' ')
if state != 'OFF':
print('Already on!')
sys.exit(1)
last_off_date = datetime.fromisoformat(last_off_date).date()
last_off_week = last_off_date.isocalendar().week
current_week = date.today().isocalendar().week
if last_off_week != current_week:
os.makedirs(f"{script_dir}/past/{project}", exist_ok=True)
with open(f"{script_dir}/past/{project}/{last_off_date.year}_{last_off_week}", 'w') as f:
f.write('\n'.join(lines) + '\n')
with open(f"{script_dir}/past/{project}/summary", 'a') as f:
first_on_date, _ = lines[0].split(' ')
first_on_date = datetime.fromisoformat(first_on_date).date()
sum, _ = elapsed(iter(lines))
week_summary = f"{first_on_date} -> {last_off_date}: {format_elapsed(sum)}"
print(f"Finished week: {week_summary}")
f.write(week_summary + '\n')
os.remove(file_path)
except FileNotFoundError:
pass
with open(file_path, 'a') as file:
file.write(f"{datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%S')} ON\n")
def turn_off():
with open(file_path, 'a') as file:
file.write(f"{datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%S')} OFF\n")
import atexit
atexit.register(turn_off)
while True:
with open(file_path) as f:
sum, on = elapsed(f)
total = datetime.utcnow() - on + sum
print(f"\r{sys.argv[1]}\t{format_elapsed(total)}", end="")
time.sleep(1)
# Copyright (c) 2021 Toni Cárdenas
#
# Permission is hereby granted, free of charge, to any person obtaining a copy of
# this software and associated documentation files (the "Software"), to deal in
# the Software without restriction, including without limitation the rights to
# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
# of the Software, and to permit persons to whom the Software is furnished to do
# so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment