import os
import json
import time
import threading
from datetime import datetime, date, timedelta
from pathlib import Path
from collections import defaultdict
try:
import matplotlib.pyplot as plt
MATPLOTLIB_OK = True
except ImportError:
MATPLOTLIB_OK = False
try:
import winsound
SOUND_ENGINE = "winsound"
except ImportError:
try:
import subprocess
SOUND_ENGINE = "subprocess"
except:
SOUND_ENGINE = None
# ============================================================
# CONFIGURATION
# ============================================================
DATA_FILE = "pomodoro_data.json"
DEFAULT_CONFIG = {
"work_minutes": 25,
"short_break": 5,
"long_break": 15,
"sessions_before_long": 4,
"auto_start_break": False,
"sound_enabled": True,
"daily_goal": 8, # pomodoros per day
}
# ============================================================
# HELPERS
# ============================================================
def clear():
os.system("cls" if os.name == "nt" else "clear")
def fmt_time(seconds):
m = seconds // 60
s = seconds % 60
return f"{m:02d}:{s:02d}"
def draw_bar(value, max_val, width=30, fill="█", empty="░"):
if max_val == 0:
return ""
filled = int((min(value, max_val) / max_val) * width)
return fill * filled + empty * (width - filled)
def beep(times=1):
"""Play a beep sound — cross platform."""
for _ in range(times):
if SOUND_ENGINE == "winsound":
try:
winsound.Beep(1000, 400)
except:
print("\a")
elif SOUND_ENGINE == "subprocess":
try:
subprocess.run(["paplay", "/usr/share/sounds/freedesktop/stereo/complete.oga"],
capture_output=True)
except:
print("\a")
else:
print("\a")
time.sleep(0.2)
# ============================================================
# LOAD & SAVE DATA
# ============================================================
def load_data():
if Path(DATA_FILE).exists():
try:
with open(DATA_FILE, "r") as f:
return json.load(f)
except:
pass
return {
"config": DEFAULT_CONFIG.copy(),
"sessions": [],
"tasks": []
}
def save_data(data):
with open(DATA_FILE, "w") as f:
json.dump(data, f, indent=2)
# ============================================================
# TIMER ENGINE
# ============================================================
class PomodoroTimer:
def __init__(self, data):
self.data = data
self.config = data.get("config", DEFAULT_CONFIG.copy())
self.running = False
self.paused = False
self.current_type = "work" # work / short_break / long_break
self.seconds_left = 0
self.session_count = 0 # completed work sessions today
self.total_today = self._count_today()
self.current_task = ""
self._thread = None
self._pause_event = threading.Event()
self._pause_event.set()
def _count_today(self):
today = str(date.today())
return sum(1 for s in self.data["sessions"]
if s.get("date") == today and s.get("type") == "work")
def _duration(self, session_type):
if session_type == "work":
return self.config["work_minutes"] * 60
elif session_type == "short_break":
return self.config["short_break"] * 60
else:
return self.config["long_break"] * 60
def start(self, session_type="work", task=""):
if self.running:
print(" Timer already running.")
return
self.current_type = session_type
self.seconds_left = self._duration(session_type)
self.current_task = task
self.running = True
self.paused = False
self._pause_event.set()
self._thread = threading.Thread(
target=self._countdown, daemon=True
)
self._thread.start()
def _countdown(self):
start_time = datetime.now()
while self.seconds_left > 0 and self.running:
self._pause_event.wait() # blocks if paused
if not self.running:
break
self._display_timer()
time.sleep(1)
if not self.paused:
self.seconds_left -= 1
if self.running and self.seconds_left <= 0:
self._on_complete(start_time)
self.running = False
def _display_timer(self):
clear()
icons = {"work": "WORK", "short_break": "SHORT BREAK", "long_break": "LONG BREAK"}
total = self._duration(self.current_type)
elapsed = total - self.seconds_left
pct = elapsed / total * 100 if total else 0
bar = draw_bar(elapsed, total, width=35)
print("\n" + "="*55)
print(f" POMODORO TIMER — {icons[self.current_type]}")
print("="*55)
print(f"\n {fmt_time(self.seconds_left)} remaining")
print(f"\n {bar} {pct:.0f}%\n")
if self.current_task:
print(f" Task: {self.current_task}")
print(f"\n Sessions today : {self.total_today + (1 if self.current_type == 'work' else 0)}"
f" / {self.config['daily_goal']} goal")
print(f" Session # : {self.session_count + 1}")
status = "PAUSED" if self.paused else "RUNNING"
print(f"\n [{status}] Ctrl+C or menu to stop")
print("="*55)
def _on_complete(self, start_time):
end_time = datetime.now()
if self.config.get("sound_enabled"):
beep(3 if self.current_type == "work" else 1)
# Log session
session = {
"type": self.current_type,
"date": str(date.today()),
"start": start_time.strftime("%H:%M:%S"),
"end": end_time.strftime("%H:%M:%S"),
"duration": self._duration(self.current_type) // 60,
"task": self.current_task,
"completed": True,
}
self.data["sessions"].append(session)
save_data(self.data)
if self.current_type == "work":
self.session_count += 1
self.total_today += 1
clear()
print("\n" + "="*55)
icon = "WORK SESSION COMPLETE!" if self.current_type == "work" else "BREAK OVER!"
print(f" {icon}")
print("="*55)
if self.current_type == "work":
print(f"\n Great work! Session {self.session_count} done.")
if self.total_today >= self.config["daily_goal"]:
print(f" DAILY GOAL REACHED! {self.total_today} pomodoros today!")
# Determine next break
if self.session_count % self.config["sessions_before_long"] == 0:
print(f" Next: LONG BREAK ({self.config['long_break']} min)")
else:
print(f" Next: SHORT BREAK ({self.config['short_break']} min)")
else:
print(f"\n Break done. Ready for the next work session!")
print()
def pause(self):
if not self.running:
return
self.paused = True
self._pause_event.clear()
print(" Timer paused.")
def resume(self):
if not self.running:
return
self.paused = False
self._pause_event.set()
print(" Timer resumed.")
def stop(self):
self.running = False
self._pause_event.set() # unblock if paused
print(" Timer stopped.")
def status(self):
if not self.running:
print("\n No timer running.")
return
state = "PAUSED" if self.paused else "RUNNING"
icons = {"work": "Work", "short_break": "Short Break",
"long_break": "Long Break"}
print(f"\n [{state}] {icons[self.current_type]}: "
f"{fmt_time(self.seconds_left)} remaining")
# ============================================================
# STATS & REPORTS
# ============================================================
def stats_today(data):
today = str(date.today())
sessions = [s for s in data["sessions"] if s.get("date") == today]
work = [s for s in sessions if s.get("type") == "work"]
breaks = [s for s in sessions if s.get("type") != "work"]
goal = data["config"].get("daily_goal", 8)
print("\n" + "="*55)
print(f" TODAY'S STATS — {today}")
print("="*55)
print(f" Pomodoros : {len(work)} / {goal}")
bar = draw_bar(len(work), goal)
print(f" Progress : {bar}")
print(f" Work time : {len(work) * data['config']['work_minutes']} min")
print(f" Breaks : {len(breaks)}")
if work:
tasks = [s["task"] for s in work if s.get("task")]
if tasks:
print(f"\n Tasks worked on:")
for t in set(tasks):
count = tasks.count(t)
print(f" • {t} ({count} pomodoro{'s' if count > 1 else ''})")
if len(work) >= goal:
print(f"\n DAILY GOAL REACHED!")
else:
remaining = goal - len(work)
print(f"\n {remaining} more pomodoro(s) to reach daily goal.")
print("="*55)
def stats_weekly(data):
sessions = data["sessions"]
today = date.today()
# Last 7 days
daily = {}
for i in range(6, -1, -1):
d = str(today - timedelta(days=i))
daily[d] = 0
for s in sessions:
d = s.get("date", "")
if d in daily and s.get("type") == "work":
daily[d] += 1
max_count = max(daily.values()) if daily else 1
goal = data["config"].get("daily_goal", 8)
print("\n" + "="*55)
print(" WEEKLY STATS (Last 7 Days)")
print("="*55)
total_week = 0
for d, count in daily.items():
try:
label = datetime.strptime(d, "%Y-%m-%d").strftime("%a %d %b")
except:
label = d
bar = draw_bar(count, max(max_count, goal), width=25)
goal_marker = " GOAL!" if count >= goal else ""
print(f" {label:<12} {count:>3} {bar}{goal_marker}")
total_week += count
avg = total_week / 7
print("="*55)
print(f" Total this week : {total_week} pomodoros")
print(f" Daily average : {avg:.1f}")
print(f" Daily goal : {goal}")
print("="*55)
def stats_all_time(data):
sessions = data["sessions"]
work = [s for s in sessions if s.get("type") == "work"]
if not work:
print("\n No sessions recorded yet.")
return
total_pomodoros = len(work)
total_minutes = sum(s.get("duration", 25) for s in work)
total_hours = total_minutes / 60
# Most productive day
day_counts = defaultdict(int)
for s in work:
day_counts[s.get("date", "")] += 1
best_day = max(day_counts, key=day_counts.get)
best_count = day_counts[best_day]
# Most worked task
task_counts = defaultdict(int)
for s in work:
t = s.get("task", "")
if t:
task_counts[t] += 1
print("\n" + "="*55)
print(" ALL-TIME STATS")
print("="*55)
print(f" Total pomodoros : {total_pomodoros:,}")
print(f" Total work time : {total_hours:.1f} hours ({total_minutes:,} min)")
print(f" Days tracked : {len(day_counts)}")
print(f" Avg per day : {total_pomodoros / max(len(day_counts), 1):.1f}")
print(f" Best day : {best_day} ({best_count} pomodoros)")
if task_counts:
top_task = max(task_counts, key=task_counts.get)
print(f" Most worked on : {top_task} ({task_counts[top_task]} sessions)")
print("="*55)
def recent_sessions(data, n=10):
sessions = data["sessions"]
if not sessions:
print("\n No sessions yet.")
return
print("\n" + "="*65)
print(f" RECENT SESSIONS (last {n})")
print("="*65)
print(f" {'DATE':<12} {'TYPE':<14} {'START':<10} {'DUR':>5} TASK")
print(" " + "-"*60)
for s in reversed(sessions[-n:]):
stype = s.get("type", "").replace("_", " ").upper()
dur = f"{s.get('duration', '?')}m"
task = s.get("task", "")[:25]
print(f" {s.get('date',''):<12} {stype:<14} "
f"{s.get('start',''):<10} {dur:>5} {task}")
print("="*65)
# ============================================================
# MATPLOTLIB CHART
# ============================================================
def plot_weekly_chart(data):
if not MATPLOTLIB_OK:
print("\n matplotlib not installed.")
print(" Install: pip install matplotlib")
return
sessions = data["sessions"]
today = date.today()
daily = {}
for i in range(13, -1, -1):
d = str(today - timedelta(days=i))
daily[d] = 0
for s in sessions:
d = s.get("date", "")
if d in daily and s.get("type") == "work":
daily[d] += 1
labels = []
values = list(daily.values())
for d in daily.keys():
try:
labels.append(datetime.strptime(d, "%Y-%m-%d").strftime("%d %b"))
except:
labels.append(d)
goal = data["config"].get("daily_goal", 8)
colors = ["#2ecc71" if v >= goal else "#3498db" for v in values]
fig, ax = plt.subplots(figsize=(13, 5))
bars = ax.bar(labels, values, color=colors, alpha=0.85, width=0.6)
# Goal line
ax.axhline(y=goal, color="#e74c3c", linestyle="--",
linewidth=1.5, label=f"Daily goal ({goal})")
# Value labels on bars
for bar, val in zip(bars, values):
if val > 0:
ax.text(bar.get_x() + bar.get_width() / 2,
bar.get_height() + 0.1,
str(val), ha="center", va="bottom", fontsize=9)
ax.set_title("Pomodoro Sessions — Last 14 Days",
fontsize=13, fontweight="bold")
ax.set_ylabel("Pomodoros")
ax.set_xlabel("Date")
ax.legend()
ax.grid(axis="y", alpha=0.3)
plt.xticks(rotation=45, ha="right")
plt.tight_layout()
from io import BytesIO
buf = BytesIO()
plt.savefig("pomodoro_chart.png", dpi=120, bbox_inches="tight")
print("\n Chart saved: pomodoro_chart.png")
plt.show()
# ============================================================
# SETTINGS
# ============================================================
def edit_settings(data):
cfg = data["config"]
print("\n" + "="*50)
print(" SETTINGS")
print("="*50)
print(f" 1. Work duration : {cfg['work_minutes']} min")
print(f" 2. Short break : {cfg['short_break']} min")
print(f" 3. Long break : {cfg['long_break']} min")
print(f" 4. Sessions before long break: {cfg['sessions_before_long']}")
print(f" 5. Daily goal : {cfg['daily_goal']} pomodoros")
print(f" 6. Sound enabled : {cfg['sound_enabled']}")
print(f" 7. Save & return")
print("="*50)
while True:
sub = input("\n Edit (1-7): ").strip()
if sub == "1":
v = input(f" Work minutes [{cfg['work_minutes']}]: ").strip()
if v.isdigit(): cfg["work_minutes"] = int(v)
elif sub == "2":
v = input(f" Short break [{cfg['short_break']}]: ").strip()
if v.isdigit(): cfg["short_break"] = int(v)
elif sub == "3":
v = input(f" Long break [{cfg['long_break']}]: ").strip()
if v.isdigit(): cfg["long_break"] = int(v)
elif sub == "4":
v = input(f" Sessions before long [{cfg['sessions_before_long']}]: ").strip()
if v.isdigit(): cfg["sessions_before_long"] = int(v)
elif sub == "5":
v = input(f" Daily goal [{cfg['daily_goal']}]: ").strip()
if v.isdigit(): cfg["daily_goal"] = int(v)
elif sub == "6":
cfg["sound_enabled"] = not cfg["sound_enabled"]
print(f" Sound: {cfg['sound_enabled']}")
elif sub == "7":
data["config"] = cfg
save_data(data)
print(" Settings saved.")
break
else:
print(" Invalid option.")
# ============================================================
# MAIN MENU
# ============================================================
def print_menu(timer, data):
cfg = data["config"]
today = str(date.today())
today_count = sum(1 for s in data["sessions"]
if s.get("date") == today and s.get("type") == "work")
goal = cfg["daily_goal"]
status = "RUNNING" if timer.running else "IDLE"
print("\n" + "-"*50)
print(f" POMODORO TIMER [{status}] "
f"Today: {today_count}/{goal}")
print("-"*50)
print(f" Work: {cfg['work_minutes']}m | "
f"Short: {cfg['short_break']}m | "
f"Long: {cfg['long_break']}m")
print("-"*50)
print(" 1. Start WORK session")
print(" 2. Start SHORT BREAK")
print(" 3. Start LONG BREAK")
print(" 4. Pause / Resume timer")
print(" 5. Stop timer")
print(" 6. Today's stats")
print(" 7. Weekly stats")
print(" 8. All-time stats")
print(" 9. Recent sessions")
print(" 10. Plot chart (14-day)")
print(" 11. Settings")
print(" 0. Exit")
print("-"*50)
def main():
print("\n" + "="*55)
print(" POMODORO TIMER WITH STATS")
print("="*55)
print("\n The Pomodoro Technique:")
print(" Work 25 min → Short break 5 min → Repeat x4 → Long break 15 min")
data = load_data()
timer = PomodoroTimer(data)
today = str(date.today())
today_count = sum(1 for s in data["sessions"]
if s.get("date") == today and s.get("type") == "work")
print(f"\n Today so far: {today_count} pomodoro(s)")
while True:
print_menu(timer, data)
choice = input(" > ").strip()
if choice == "1":
task = input(" What are you working on? (Enter to skip): ").strip()
if timer.running:
print(" Stop current timer first (option 5).")
else:
timer.start("work", task)
print(f"\n Work session started! ({data['config']['work_minutes']} min)")
print(" Return to menu anytime — timer runs in background.")
elif choice == "2":
if timer.running:
print(" Stop current timer first.")
else:
timer.start("short_break")
print(f"\n Short break started! ({data['config']['short_break']} min)")
elif choice == "3":
if timer.running:
print(" Stop current timer first.")
else:
timer.start("long_break")
print(f"\n Long break started! ({data['config']['long_break']} min)")
elif choice == "4":
if timer.paused:
timer.resume()
else:
timer.pause()
elif choice == "5":
timer.stop()
elif choice == "6":
stats_today(data)
elif choice == "7":
stats_weekly(data)
elif choice == "8":
stats_all_time(data)
elif choice == "9":
n = input(" Show last N sessions (default 10): ").strip()
n = int(n) if n.isdigit() else 10
recent_sessions(data, n)
elif choice == "10":
plot_weekly_chart(data)
elif choice == "11":
edit_settings(data)
timer.config = data["config"]
elif choice == "0":
timer.stop()
print("\n Goodbye! Keep up the great work!\n")
break
else:
print(" Invalid choice.")
# ============================================================
# RUN
# ============================================================
if __name__ == "__main__":
main()
No comments:
Post a Comment