diff --git a/README.md b/README.md index de5b404..e0b904e 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,22 @@ It's a single Python file with nothing extra to install. The timer keeps going (focus, break, focus, break...) until you press **Ctrl+C**. When you stop, it tells you how many focus sessions you finished. +## Your focus log + +Every focus session you finish is saved to a log file called +`pomodoro_log.csv` in your home folder. When you start the timer it shows +how much you've already focused today, for example: + +``` +Today so far: 3 focus sessions, 1 hour 15 minutes in total. +``` + +and it shows the updated total again when you stop. Sessions you stop +early with Ctrl+C aren't counted. The log is a plain spreadsheet file +(one row per session, with the time it finished and how many minutes it +was), so you can open it in Excel, Numbers or Google Sheets. Delete the file +if you want to start fresh. + ## Options You can change the times if you want: @@ -38,6 +54,7 @@ You can change the times if you want: | `--break MINUTES` | Length of each break (default 5) | `--break 10` | | `--rounds N` | Stop after N focus sessions (default: keep going) | `--rounds 4` | | `--quiet` | Don't beep | `--quiet` | +| `--log FILE` | Save the focus log somewhere else (default: `pomodoro_log.csv` in your home folder) | `--log work.csv` | For example, four rounds of 50 minutes work with 10 minute breaks: diff --git a/pomodoro.py b/pomodoro.py index 331e868..ddbcf15 100644 --- a/pomodoro.py +++ b/pomodoro.py @@ -5,15 +5,24 @@ Alternates between focus sessions (25 minutes by default) and breaks (5 minutes by default), showing a live countdown with a progress bar and ringing the terminal bell when each session ends. +Every finished focus session is written to a small log file (a CSV file in +your home folder by default), and today's total is shown when the timer +starts. + Uses only the Python standard library. """ import argparse +import csv +import datetime +import os import shutil import sys import time BELL = "\a" +DEFAULT_LOG_FILE = os.path.join(os.path.expanduser("~"), "pomodoro_log.csv") +LOG_HEADER = ["finished_at", "minutes"] def pick_bar_chars(): @@ -72,6 +81,59 @@ def run_session(label, minutes): sys.stdout.flush() +def log_session(path, minutes): + """Add one finished focus session to the log file. + + Returns True if it was saved. Problems writing the file are reported but + never stop the timer. + """ + finished_at = datetime.datetime.now().replace(microsecond=0).isoformat(sep=" ") + try: + is_new = not os.path.exists(path) or os.path.getsize(path) == 0 + with open(path, "a", newline="", encoding="utf-8") as f: + writer = csv.writer(f) + if is_new: + writer.writerow(LOG_HEADER) + writer.writerow([finished_at, f"{minutes:g}"]) + return True + except OSError as e: + print(f"(Couldn't save this session to the log file {path}: {e.strerror or e})") + return False + + +def today_totals(path, today=None): + """Return (number of sessions, total minutes) logged for today.""" + today = (today or datetime.date.today()).isoformat() + count = 0 + minutes = 0.0 + try: + with open(path, newline="", encoding="utf-8") as f: + for row in csv.reader(f): + if len(row) < 2 or not row[0].startswith(today): + continue + try: + value = float(row[1]) + except ValueError: + continue + count += 1 + minutes += value + except (OSError, UnicodeDecodeError): + pass + return count, minutes + + +def describe_total(count, minutes): + sessions = "session" if count == 1 else "sessions" + total = int(round(minutes)) + hours, mins = divmod(total, 60) + parts = [] + if hours: + parts.append(f"{hours} hour" + ("" if hours == 1 else "s")) + if mins or not hours: + parts.append(f"{mins} minute" + ("" if mins == 1 else "s")) + return f"{count} focus {sessions}, {' '.join(parts)} in total" + + def ring(times): for i in range(times): sys.stdout.write(BELL) @@ -122,6 +184,11 @@ def parse_args(argv=None): "-q", "--quiet", action="store_true", help="don't ring the terminal bell", ) + parser.add_argument( + "-l", "--log", default=DEFAULT_LOG_FILE, metavar="FILE", + help="file where finished focus sessions are recorded " + "(default: %(default)s)", + ) return parser.parse_args(argv) @@ -135,6 +202,7 @@ def main(argv=None): focus_text = f"{args.focus:g} {plural(args.focus)}" break_text = f"{args.break_minutes:g} {plural(args.break_minutes)}" print(f"Pomodoro timer: {focus_text} focus, {break_text} break.") + print(f"Today so far: {describe_total(*today_totals(args.log))}.") print("Press Ctrl+C to stop.\n") completed = 0 @@ -145,6 +213,7 @@ def main(argv=None): print(f"Round {round_number}{suffix}") run_session("Focus", args.focus) completed += 1 + log_session(args.log, args.focus) ring(ring_times) is_last = args.rounds and round_number >= args.rounds @@ -161,6 +230,7 @@ def main(argv=None): print("\n\nStopped.") print(f"Focus sessions completed: {completed}. Nice work!") + print(f"Today's total: {describe_total(*today_totals(args.log))}.") return 0