Focus session log with today's total
The timer now saves each finished focus session to a spreadsheet-style file in your home folder. It shows how much you've focused today when it starts and again when you stop.
What it does
- Each time a focus session runs to completion, append one row to a CSV log file. Breaks and sessions stopped early with Ctrl+C are not logged.
- Default log location is `pomodoro_log.csv` in the user's home folder. Add a `--log FILE` option (short form `-l`) to use a different file.
- The CSV has a header row `finished_at,minutes`. If the file is missing or empty, write the header first. Each row has the finish time as local `YYYY-MM-DD HH:MM:SS` and the session length in minutes (e.g. `25`).
- When the timer starts, after the intro line and before "Press Ctrl+C to stop.", print `Today so far: <N> focus session(s), <H> hour(s) <M> minute(s) in total.` Example: `Today so far: 3 focus sessions, 1 hour 15 minutes in total.`
- Totals count only rows whose finish time starts with today's date. Rows that are malformed or have a non-numeric minutes value are skipped. A missing or unreadable file counts as 0 sessions, 0 minutes.
- Formatting: use "session" when the count is 1, otherwise "sessions". Use "hour"/"hours" and "minute"/"minutes" correctly. Omit the hours part when it is 0. Omit the minutes part when it is 0 and hours is nonzero. Show "0 minutes" when the total is 0. Round total minutes to the nearest whole minute.
- When the timer stops, after "Focus sessions completed: N. Nice work!", print `Today's total: <same format>.`
- If the log cannot be written, print a short note like `(Couldn't save this session to the log file <path>: <reason>)` and keep the timer running.
- Update the README with a "Your focus log" section and a `--log FILE` row in the options table.
Safety check ✓ Safe
The change only reads and writes a local CSV file, either in the home folder or at a path the user chooses. It adds no network access, no new dependencies, and nothing unrelated to the request. Only Python standard library modules are used.
Conversation that created this patch
@lena
Keep a log of finished focus sessions and show today's total when the timer starts.
AI
I've added a focus log to the timer. Each focus session you finish is now saved, and 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." It also shows the updated total when you stop. I ran some quick test sessions and the log and daily totals worked.
Things to know:
- **Where it's saved:** a file called `pomodoro_log.csv` in your home folder. It has one row per session, with the time it finished and its length in minutes. You can open it in Excel, Numbers or Google Sheets.
- **What counts:** only sessions that run all the way to the end. If you stop one early with Ctrl+C, it isn't counted.
- **If the file can't be saved:** the timer shows a short note and keeps running.
- **Saving it elsewhere:** there's a new `--log` option to save the log in a different file, for example a separate one for work.
- **Starting fresh:** delete the log file.
I also added a section about the log to the README.
Code changes (for the curious, 2 files) · raw
README.md +17 −0
@@ -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: | |
|
pomodoro.py +70 −0
@@ -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 | |
| |
|