Generates a clean PDF timesheet from a screenshot of time entries. Reads the image, cross-references git commits in the date range, and produces a minimal styled PDF with bullet-point descriptions. Use when the user provides a timesheet screenshot or asks to generate a timesheet PDF.
Turns a screenshot of time-tracking entries into a polished PDF timesheet with commit-derived descriptions.
Run the entire workflow end-to-end without pausing for confirmation. Do not ask the user to approve intermediate steps — just execute everything and present the final result.
Use the Read tool on the provided image path. Extract:
Run:
git log --author="$(git config user.name)" --since="<first_date>" --until="<last_date_plus_1>" --format="%H %ai %s" --all
For each day, turn each commit into a bullet point:
Good bullet examples:
Migrated marketing page from inline styles to Tailwind classesAdded lint-staged with husky pre-commit hookReplaced static agent-signup PNG with interactive Rive animationHeader mask/overlay styling; merged PRs #198, #199Use Python with reportlab. Install if needed: pip3 install --break-system-packages reportlab
The PDF must follow this exact style specification:
from reportlab.lib.pagesizes import letter
from reportlab.lib.units import inch
from reportlab.pdfgen import canvas
from reportlab.lib.colors import HexColor
output = "<project_dir>/colby_thomas_timesheet_<start_date>-<end_date>.pdf" # e.g. colby_thomas_timesheet_mar_26-apr_3.pdf (lowercase 3-letter month, underscore, day)
c = canvas.Canvas(output, pagesize=letter)
w, h = letter
# Colors
black = HexColor("#1a1a1a")
gray = HexColor("#666666")
light_gray = HexColor("#cccccc")
separator = HexColor("#e5e5e5")
# Layout constants
LEFT = 0.75 * inch
RIGHT = w - 0.75 * inch
DESC_X = 1.0 * inch # bullet position
TEXT_X = DESC_X + 10 # text after bullet
BULLET_MAX_W = RIGHT - TEXT_X
BULLET = "\u2022"
def wrap_text(text, font, size, max_w):
words = text.split()
lines, line = [], ""
for word in words:
test = f"{line} {word}".strip()
if c.stringWidth(test, font, size) <= max_w:
line = test
else:
if line: lines.append(line)
line = word
if line: lines.append(line)
return lines
# --- Title ---
y = h - 1 * inch
c.setFont("Helvetica-Bold", 18)
c.setFillColor(black)
c.drawString(LEFT, y, "Colby Thomas")
# --- Subtitle: date range · project ---
y -= 20
c.setFont("Helvetica", 10)
c.setFillColor(gray)
c.drawString(LEFT, y, "<date_range> \u00b7 <project_name>")
# --- Divider under subtitle ---
y -= 12
c.setStrokeColor(light_gray)
c.setLineWidth(0.5)
c.line(LEFT, y, RIGHT, y)
y -= 30
# --- Day rows ---
for day_label, day_total, items in days:
# Estimate height for page break
needed = 18
for item_text, item_sha in items:
sha_suffix = f" ({item_sha})" if item_sha else ""
needed += len(wrap_text(item_text + sha_suffix, "Helvetica", 9, BULLET_MAX_W)) * 13 + 2
needed += 20
if y - needed < 0.75 * inch:
c.showPage()
y = h - 1 * inch
# Day header: bold label left, total right
c.setFont("Helvetica-Bold", 11)
c.setFillColor(black)
c.drawString(LEFT, y, day_label) # e.g. "March 26"
c.setFont("Helvetica", 10)
c.setFillColor(gray)
c.drawRightString(RIGHT, y, day_total) # e.g. "1h 15m"
y -= 18
# Bullet items
for item_text, item_sha in items:
if y < 0.75 * inch:
c.showPage()
y = h - 1 * inch
c.setFont("Helvetica", 9)
c.setFillColor(black)
c.drawString(DESC_X, y, BULLET)
# Append sha to last line if present
sha_suffix = f" ({item_sha})" if item_sha else ""
lines = wrap_text(item_text + sha_suffix, "Helvetica", 9, BULLET_MAX_W)
for i, ln in enumerate(lines):
if item_sha and i == len(lines) - 1 and ln.endswith(f"({item_sha})"):
# Draw text before sha in black, sha portion in gray
prefix = ln[: -len(f"({item_sha})")]
c.setFillColor(black)
c.drawString(TEXT_X, y, prefix)
sha_x = TEXT_X + c.stringWidth(prefix, "Helvetica", 9)
c.setFillColor(gray)
c.drawString(sha_x, y, f"({item_sha})")
else:
c.setFillColor(black)
c.drawString(TEXT_X, y, ln)
y -= 13
y -= 2
y -= 5
# Day separator
c.setStrokeColor(separator)
c.setLineWidth(0.3)
c.line(LEFT, y + 4, RIGHT, y + 4)
y -= 14
# --- Total at bottom ---
if y < 1 * inch:
c.showPage()
y = h - 1 * inch
y -= 5
c.setFont("Helvetica", 10)
c.setFillColor(gray)
c.drawString(LEFT, y, f"Total: {total_h}h {total_m}m")
c.save()
Structure the data as a list of tuples — one tuple per day with a list of (description, sha) tuples. sha is a short (7-char) commit hash string if the bullet was derived from a commit, or None if no commit matches (e.g. meetings, env setup):
days = [
("March 26", "1h 15m", [
("Onboarding call with anon team", None),
("Local dev environment setup (Node.js, Docker, Supabase)", None),
("Ran Prettier across existing codebase; began Tailwind setup", "303896e"),
]),
# ... more days
]
When grouping multiple commits into one bullet, use the SHA of the primary/first commit in the group.
Day totals: format as Xh Ym (e.g. "2h 54m", "0h 27m") — sum of all sessions that day.
Grand total: sum all day totals, format as Xh Ym.
#1a1a1a (black), #666666 (gray), #cccccc (light divider), #e5e5e5 (day separator)colby_thomas_timesheet_<start>-<end>.pdf where start/end are mon_dd (e.g. mar_26, apr_3) — lowercase 3-letter month, underscore, day number. Saved to the current project directory and copied to ~/Documents/Always copy the generated PDF to ~/Documents/:
cp <project_dir>/colby_thomas_timesheet_<start>-<end>.pdf ~/Documents/
Read the generated PDF to confirm it renders correctly, then tell the user both file paths.