Introduction
The Pomodoro technique — 25 minutes of focus, 5 minutes of break, repeat — is simple, and that's exactly why a digital version should be simple too. This build extends the countdown-timer idea into a cycling app: work sessions flow into breaks automatically, completed pomodoros accumulate, and a daily stats row shows your focus totals. It is the deadline mechanics of the countdown timer plus the session logging of the expense tracker.
Features
Prerequisites
pip install streamlit streamlit-autorefresh pandasStep 1: Create the Script
Save as pomodoro.py:
import streamlit as st
import pandas as pd
from datetime import datetime, timedelta
from streamlit_autorefresh import st_autorefresh
import os
LOG = "pomodoros.csv"
WORK, BREAK = 25, 5
st.set_page_config(page_title="Pomodoro", page_icon="🍅")
st.title("🍅 Pomodoro Timer")
running = "deadline" in st.session_state
if running and not st.session_state.get("paused"):
st_autorefresh(interval=1000, key="tick")
def start_phase(phase):
mins = WORK if phase == "work" else BREAK
st.session_state.phase = phase
st.session_state.deadline = datetime.now() + timedelta(minutes=mins)
st.session_state.duration = mins * 60
st.session_state.paused = False
c1, c2 = st.columns(2)
if c1.button("▶️ Start work session", type="primary"):
start_phase("work")
st.rerun()
if c2.button("⏭ Skip phase"):
start_phase("break" if st.session_state.get("phase") == "work" else "work")
st.rerun()
if running:
remaining = (st.session_state.deadline - datetime.now()).total_seconds()
if st.session_state.get("paused"):
remaining = st.session_state["paused_remaining"]
mins, secs = divmod(max(int(remaining), 0), 60)
phase_label = "🍅 Focus" if st.session_state.phase == "work" else "☕ Break"
st.metric(phase_label, f"{mins:02d}:{secs:02d}")
st.progress(max(0, 1 - remaining / st.session_state.duration))
p1, p2 = st.columns(2)
if p1.button("⏸ Pause" if not st.session_state.get("paused") else "▶️ Resume"):
if st.session_state.get("paused"):
st.session_state.deadline = datetime.now() + timedelta(seconds=st.session_state["paused_remaining"])
st.session_state.paused = False
else:
st.session_state.paused = True
st.session_state["paused_remaining"] = remaining
st.rerun()
if remaining <= 0:
if st.session_state.phase == "work":
row = pd.DataFrame([{
"date": datetime.now().strftime("%Y-%m-%d"),
"time": datetime.now().strftime("%H:%M"), "minutes": WORK,
}])
row.to_csv(LOG, mode="a", header=not os.path.exists(LOG), index=False)
st.balloons()
start_phase("break")
else:
start_phase("work")
st.rerun()
if os.path.exists(LOG):
log = pd.read_csv(LOG)
today = log[log["date"] == datetime.now().strftime("%Y-%m-%d")]
m1, m2 = st.columns(2)
m1.metric("Pomodoros today", len(today))
m2.metric("Focus minutes today", int(today["minutes"].sum()))
with st.expander("Session log"):
st.dataframe(log.tail(10), hide_index=True)Step 2: Run the App
streamlit run pomodoro.pyStart a session, work until the balloons, and the app drops you straight into a break — your tally grows in the stats row.
How It Works
The app is a two-state machine (work ↔ break) wearing a timer's clothes. start_phase() is the single transition function: it sets the phase name, builds a fresh deadline, and resets pause state. Completion detection — remaining <= 0 — is what triggers the flip, and because the work branch logs to CSV *before* transitioning, no completed pomodoro is ever lost to a refresh.
The CSV append uses mode="a" with header=not os.path.exists(LOG) — write headers only when creating the file. This one-liner log pattern is deliberately minimal; if you outgrow it, the expense tracker's read-modify-write handles edits.
Autorefresh runs only while the timer is unpaused, exactly as in the countdown timer — idle screens cost zero requests.
Common Errors & Fixes
st.rerun(); without it, the UI shows the old phase until your next click.remaining <= 0; transition the phase immediately after logging so the condition can't repeat.%Y-%m-%d both sides, or they won't match).Key Concepts
What to Try Next
st.bar_chart on the grouped log.st.audio, like the countdown's alert idea.FAQ
Why 25 and 5 minutes?
Francesco Cirillo's original technique — long enough to make progress, short enough to stay urgent. The constants are variables; tune them to your rhythm.
Where is my data stored?
In pomodoros.csv next to the script — one row per completed session. Copy it to back up, open it in Excel, or point the app at a shared drive.
Does the timer survive a page refresh?
Yes — the deadline is server-side, so refreshing shows the correct remaining time, exactly like the countdown timer.