Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ python main.py complete "Math Homework"
# Calculate your GPA
python main.py gpa

# Set a pomodoro timer for efficient studies
python main.py pomodoro

# View statistics
python main.py stats
```
Expand Down Expand Up @@ -83,6 +86,7 @@ python -m unittest tests/test_utils.py
```
demo/
├── main.py # Entry point
├── pomodoro.py # Pomodoro Timer feature
├── utils.py # Utility functions
├── student_manager.py # Core functionality
├── tests/ # Test files
Expand Down
16 changes: 14 additions & 2 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,22 @@
import argparse
from student_manager import StudentManager
from utils import format_date, save_to_json, load_from_json
from pomodoro import PomodoroTimer


def main():
parser = argparse.ArgumentParser(description='StudentHub - Manage your academic life')

parser.add_argument('command', choices=['add-assignment', 'list', 'complete', 'gpa', 'stats'],
parser.add_argument('command', choices=['add-assignment', 'list', 'complete', 'gpa', 'stats','pomodoro'],
help='Command to execute')
parser.add_argument('value', nargs='?', help='Value for the command')
parser.add_argument('--deadline', help='Deadline in YYYY-MM-DD format')
parser.add_argument('--subject', help='Subject name')
parser.add_argument('--sessions', type=int, default=4)
parser.add_argument('--work',type=int,default=25)
parser.add_argument('--break-minutes',type=int,default=5)
parser.add_argument('--no-sound',action='store_true')


args = parser.parse_args()

Expand Down Expand Up @@ -54,7 +60,13 @@ def main():
print(f"Completed: {stats['completed']}")
print(f"Pending: {stats['pending']}")
print(f"GPA: {stats['gpa']:.2f}")

elif args.command == 'pomodoro':
timer = PomodoroTimer(
work_minutes=args.work,
break_minutes=args.break_minutes,
sound=not args.no_sound
)
timer.start(interactive=True)

if __name__ == '__main__':
main()
191 changes: 191 additions & 0 deletions pomodoro.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
"""
Pomodoro timer for StudentHub with ASCII splash and interactive menu.
"""
import time
import sys
import subprocess
import shutil
import threading
import platform
import os
from datetime import timedelta

PLATFORM = platform.system().lower()

ASCII_SPLASH = r"""
__________ .___
\______ \____ _____ ____ __| _/___________ ____
| ___/ _ \ / \ / _ \ / __ |/ _ \_ __ \/ _ \
| | ( <_> ) Y Y ( <_> ) /_/ ( <_> ) | \( <_> )
|____| \____/|__|_| /\____/\____ |\____/|__| \____/
\/ \/
even einstein used to follow the pomodoro technique
just saying

"""

class PomodoroTimer:
"""
Pomodoro timer with optional interactive menu and ASCII art splash.
"""
def __init__(self, work_minutes=25, break_minutes=5, sound=True, notifier_cmd=None):
"""Initialize timer settings and notifier."""
self.work_minutes = int(work_minutes)
self.break_minutes = int(break_minutes)
self.sound = bool(sound)
self._stop_requested = False
self.notifier_cmd = notifier_cmd or self._detect_notifier()
def _detect_notifier(self):
if PLATFORM == "linux" and shutil.which("notify-send"): return "notify-send"
if PLATFORM == "darwin" and shutil.which("osascript"): return "osascript"
if PLATFORM == "windows" and shutil.which("powershell"): return "powershell"
return None
def _notify(self, title, message):
try:
if PLATFORM == "linux" and self.notifier_cmd == "notify-send":
subprocess.Popen(["notify-send", title, message])
elif PLATFORM == "darwin" and self.notifier_cmd == "osascript":
subprocess.Popen(["osascript","-e",f'display notification "{message}" with title "{title}"'])
elif PLATFORM == "windows" and self.notifier_cmd == "powershell":
ps='Add-Type -AssemblyName PresentationFramework;[System.Windows.MessageBox]::Show("{}","{}")'.format(message,title)
subprocess.Popen(["powershell","-NoProfile","-Command",ps],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
except:
pass
print(f"\n=== {title} ===\n{message}\a\n",flush=True)
def _play_sound(self):
if not self.sound: return
try:
if PLATFORM == "linux":
for p in ("paplay","aplay","play"):
if shutil.which(p):
path="/usr/share/sounds/freedesktop/stereo/complete.oga"
if os.path.exists(path):
subprocess.Popen([p,path],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
return
sys.stdout.write("\a"); sys.stdout.flush()
elif PLATFORM == "darwin":
if shutil.which("afplay"):
for p in ("/System/Library/Sounds/Glass.aiff","/System/Library/Sounds/Pop.aiff"):
if os.path.exists(p):
subprocess.Popen(["afplay",p],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
return
sys.stdout.write("\a"); sys.stdout.flush()
elif PLATFORM == "windows":
try:
import winsound
winsound.MessageBeep(winsound.MB_ICONASTERISK)
except:
try:
winsound.Beep(1000,300)
except:
sys.stdout.write("\a"); sys.stdout.flush()
else:
sys.stdout.write("\a"); sys.stdout.flush()
except:
sys.stdout.write("\a"); sys.stdout.flush()
def _format_mmss(self, s):
if s < 0: s = 0
m, s = divmod(int(s), 60)
return f"{m:02d}:{s:02d}"
def _countdown(self, total_seconds, label="Working"):
end = time.time() + float(total_seconds)
try:
while time.time() < end:
if self._stop_requested: return False
remaining = end - time.time()
mmss = self._format_mmss(remaining)
sys.stdout.write(f"\r{label} — {mmss} remaining ")
sys.stdout.flush()
time.sleep(1)
sys.stdout.write("\r" + " " * 60 + "\r")
sys.stdout.flush()
return True
except KeyboardInterrupt:
self._stop_requested = True
print("\nTimer cancelled by user.")
return False
def _show_splash(self):
print(ASCII_SPLASH)
def _interactive_menu(self):
sessions = self.work_minutes and 4 or 4
work = self.work_minutes
brk = self.break_minutes
sound = self.sound
while True:
os.system('cls' if platform.system().lower()=="windows" else 'clear')
self._show_splash()
print(f"Current: sessions = {sessions}, work = {work} minutes, break = {brk} minutes, sound = {'on' if sound else 'off'}")
print("Options:")
print(" 1) Start now")
print(" 2) Configure sessions/work/break")
print(" 3) Toggle sound on/off")
print(" 4) Quick test (6s work / 4s break)")
print(" 5) Quit")
try:
choice = input("Choose [1-5]: ").strip()
except KeyboardInterrupt:
print("\nInterrupted by user, shoulda thought before starting heh...")
return None
if choice == "1":
return {"sessions": sessions, "work": work, "break": brk, "sound": sound}
if choice == "2":
try:
ns = input(f"Sessions ({sessions}): ").strip()
if ns: sessions = int(ns)
nw = input(f"Work minutes ({work}): ").strip()
if nw: work = float(nw)
nb = input(f"Break minutes ({brk}): ").strip()
if nb: brk = float(nb)
except ValueError:
input("Invalid number, press Enter to continue...")
continue
if choice == "3":
sound = not sound
if choice == "4":
return {"sessions":1,"work":0.1,"break":0.0667,"sound":sound}
if choice == "5":
return None
def start(self, sessions=4, work_minutes=None, break_minutes=None, interactive=False):
if interactive:
opts = self._interactive_menu()
if not opts:
print("Cancelled.")
return
sessions = int(opts["sessions"])
work_minutes = opts["work"]
break_minutes = opts["break"]
self.sound = bool(opts["sound"])
if work_minutes is not None: self.work_minutes = int(work_minutes) if float(work_minutes).is_integer() else work_minutes
if break_minutes is not None: self.break_minutes = int(break_minutes) if float(break_minutes).is_integer() else break_minutes
sessions = int(sessions)
print(f"Starting Pomodoro: {sessions} sessions — {self.work_minutes}m work / {self.break_minutes}m break")
try:
for s in range(1, sessions + 1):
if self._stop_requested: break
print(f"\nSession {s} — Work ({self.work_minutes} minutes)")
if not self._countdown(self.work_minutes * 60, f"Work (Session {s}/{sessions})"): break
self._play_sound(); self._notify("Work done","Time for a break!")
if s == sessions:
print("All sessions complete. Good job!"); self._notify("Pomodoro","All sessions complete!"); break
print(f"Break — {self.break_minutes} minutes")
if not self._countdown(self.break_minutes * 60, f"Break (Session {s}/{sessions})"): break
self._play_sound(); self._notify("Break finished","Next session starting")
except KeyboardInterrupt:
print("\nPomodoro interrupted by user.")
finally:
print("Pomodoro stopped.")
def start_in_thread(self, *args, **kwargs):
t = threading.Thread(target=self.start, args=args, kwargs=kwargs, daemon=True)
t.start()
return t

if __name__ == "__main__":
import argparse
p = argparse.ArgumentParser()
p.add_argument("--sessions","-s",type=int,default=1)
p.add_argument("--work",type=float,default=0.1)
p.add_argument("--break",dest="brk",type=float,default=0.1)
p.add_argument("--no-sound",action="store_true")
args = p.parse_args()
timer = PomodoroTimer(work_minutes=args.work, break_minutes=args.brk, sound=not args.no_sound)
timer.start(sessions=args.sessions, work_minutes=args.work, break_minutes=args.brk, interactive=True)