import json
import os
import time
import threading
from datetime import datetime, timedelta
from pathlib import Path
# Try importing notification libraries
try:
from plyer import notification
NOTIF_ENGINE = "plyer"
except ImportError:
NOTIF_ENGINE = None
# Fallback for Windows
if NOTIF_ENGINE is None:
try:
from win10toast import ToastNotifier
NOTIF_ENGINE = "win10toast"
toaster = ToastNotifier()
except ImportError:
NOTIF_ENGINE = None
# ============================================================
# CONFIGURATION
# ============================================================
REMINDERS_FILE = "reminders.json"
APP_NAME = "Python Reminder"
APP_ICON = None # Set path to a .ico file if desired e.g. "icon.ico"
# ============================================================
# LOAD & SAVE REMINDERS
# ============================================================
def load_reminders():
if Path(REMINDERS_FILE).exists():
try:
with open(REMINDERS_FILE, "r") as f:
return json.load(f)
except:
return []
return []
def save_reminders(reminders):
with open(REMINDERS_FILE, "w") as f:
json.dump(reminders, f, indent=2)
# ============================================================
# SEND DESKTOP NOTIFICATION
# ============================================================
def send_notification(title, message, timeout=10):
"""
Send a desktop pop-up notification.
Tries plyer first, then win10toast, then terminal fallback.
"""
print(f"\n [REMINDER] {title}: {message}")
if NOTIF_ENGINE == "plyer":
try:
notification.notify(
title=title,
message=message,
app_name=APP_NAME,
app_icon=APP_ICON,
timeout=timeout
)
return True
except Exception as e:
print(f" plyer error: {e}")
elif NOTIF_ENGINE == "win10toast":
try:
toaster.show_toast(
title,
message,
duration=timeout,
threaded=True
)
return True
except Exception as e:
print(f" win10toast error: {e}")
# Terminal bell fallback
print(f"\n {'='*50}")
print(f" *** REMINDER ***")
print(f" Title : {title}")
print(f" Message: {message}")
print(f" Time : {datetime.now().strftime('%d-%m-%Y %H:%M:%S')}")
print(f" {'='*50}")
print("\a") # Terminal bell
return True
# ============================================================
# REMINDER CLASS
# ============================================================
class ReminderManager:
def __init__(self):
self.reminders = load_reminders()
self.running = False
self.fired = set() # IDs of already-fired one-time reminders
# ----------------------------------------------------------
# Add Reminder
# ----------------------------------------------------------
def add_reminder(self, title, message, remind_type,
remind_at=None, interval_mins=None,
repeat_times=1):
reminder = {
"id": datetime.now().strftime("%Y%m%d%H%M%S%f"),
"title": title,
"message": message,
"type": remind_type, # once / interval / daily / weekly
"remind_at": remind_at, # "HH:MM" or "DD-MM-YYYY HH:MM"
"interval_mins": interval_mins,
"repeat_times": repeat_times,
"fired_count": 0,
"active": True,
"created_at": datetime.now().strftime("%d-%m-%Y %H:%M:%S")
}
self.reminders.append(reminder)
save_reminders(self.reminders)
print(f"\n Reminder saved: [{title}]")
return reminder
# ----------------------------------------------------------
# List Reminders
# ----------------------------------------------------------
def list_reminders(self):
active = [r for r in self.reminders if r.get("active")]
inactive = [r for r in self.reminders if not r.get("active")]
print("\n" + "="*58)
print(f" REMINDERS ({len(active)} active, {len(inactive)} done/disabled)")
print("="*58)
if not self.reminders:
print("\n No reminders set yet.")
return
if active:
print("\n ACTIVE:")
for i, r in enumerate(active, 1):
rtype = r.get("type", "")
at = r.get("remind_at", "")
mins = r.get("interval_mins")
fired = r.get("fired_count", 0)
repeat = r.get("repeat_times", 1)
if rtype == "once":
schedule_str = f"Once at {at}"
elif rtype == "interval":
schedule_str = f"Every {mins} min(s)"
elif rtype == "daily":
schedule_str = f"Daily at {at}"
elif rtype == "weekly":
schedule_str = f"Weekly on {at}"
else:
schedule_str = at
print(f"\n [{i}] {r['title']}")
print(f" Message : {r['message']}")
print(f" Schedule : {schedule_str}")
print(f" Fired : {fired}/{repeat if repeat > 0 else 'unlimited'}")
print(f" Created : {r['created_at']}")
if inactive:
print("\n COMPLETED / DISABLED:")
for r in inactive[-5:]:
print(f" - [{r['title']}] fired {r.get('fired_count',0)} time(s)")
print("="*58)
# ----------------------------------------------------------
# Delete / Disable Reminder
# ----------------------------------------------------------
def delete_reminder(self, index):
active = [r for r in self.reminders if r.get("active")]
if index < 1 or index > len(active):
print(" Invalid index.")
return
target = active[index - 1]
target["active"] = False
save_reminders(self.reminders)
print(f"\n Disabled: [{target['title']}]")
def delete_all(self):
confirm = input("\n Delete ALL reminders? (yes/no): ").strip().lower()
if confirm == "yes":
self.reminders = []
self.fired = set()
save_reminders(self.reminders)
print(" All reminders cleared.")
# ----------------------------------------------------------
# Core Check Loop
# ----------------------------------------------------------
def _check_loop(self):
print(" Reminder monitor running in background...")
self.running = True
# Track interval next-fire times
interval_next = {}
for r in self.reminders:
if r.get("type") == "interval" and r.get("active"):
interval_next[r["id"]] = datetime.now() + timedelta(
minutes=r.get("interval_mins", 1)
)
while self.running:
now = datetime.now()
now_hhmm = now.strftime("%H:%M")
now_str = now.strftime("%d-%m-%Y %H:%M")
for r in self.reminders:
if not r.get("active"):
continue
rid = r["id"]
rtype = r.get("type")
repeat = r.get("repeat_times", 1)
fired = r.get("fired_count", 0)
should_fire = False
# --- One-time ---
if rtype == "once":
if rid not in self.fired:
try:
fire_dt = datetime.strptime(r["remind_at"], "%d-%m-%Y %H:%M")
if now >= fire_dt:
should_fire = True
self.fired.add(rid)
except:
pass
# --- Interval ---
elif rtype == "interval":
next_fire = interval_next.get(rid, now)
if now >= next_fire:
should_fire = True
interval_next[rid] = now + timedelta(
minutes=r.get("interval_mins", 1)
)
# --- Daily ---
elif rtype == "daily":
fire_key = f"{rid}_{now.strftime('%Y%m%d')}"
if now_hhmm == r.get("remind_at") and fire_key not in self.fired:
should_fire = True
self.fired.add(fire_key)
# --- Weekly ---
elif rtype == "weekly":
# remind_at format: "Monday 09:00"
try:
day_name, hhmm = r["remind_at"].split(" ", 1)
if now.strftime("%A") == day_name and now_hhmm == hhmm:
fire_key = f"{rid}_{now.strftime('%Y%W')}"
if fire_key not in self.fired:
should_fire = True
self.fired.add(fire_key)
except:
pass
# Fire it!
if should_fire:
send_notification(r["title"], r["message"])
r["fired_count"] = fired + 1
# Deactivate if repeat limit reached
if repeat > 0 and r["fired_count"] >= repeat:
if rtype in ["once", "interval"]:
r["active"] = False
save_reminders(self.reminders)
time.sleep(15) # Check every 15 seconds
def start(self):
t = threading.Thread(target=self._check_loop, daemon=True)
t.start()
def stop(self):
self.running = False
# ============================================================
# QUICK ADD HELPERS
# ============================================================
def add_once(manager):
print("\n One-time Reminder")
title = input(" Title: ").strip()
message = input(" Message: ").strip()
at_str = input(" When? (DD-MM-YYYY HH:MM): ").strip()
try:
datetime.strptime(at_str, "%d-%m-%Y %H:%M")
manager.add_reminder(title, message, "once",
remind_at=at_str, repeat_times=1)
except ValueError:
print(" Invalid date/time format.")
def add_interval(manager):
print("\n Interval Reminder")
title = input(" Title: ").strip()
message = input(" Message: ").strip()
try:
mins = int(input(" Repeat every how many minutes? ").strip())
repeats = input(" How many times? (0 = unlimited): ").strip()
repeats = int(repeats) if repeats.isdigit() else 0
manager.add_reminder(title, message, "interval",
interval_mins=mins, repeat_times=repeats)
except ValueError:
print(" Invalid number.")
def add_daily(manager):
print("\n Daily Reminder")
title = input(" Title: ").strip()
message = input(" Message: ").strip()
at_str = input(" Daily at (HH:MM): ").strip()
manager.add_reminder(title, message, "daily",
remind_at=at_str, repeat_times=0)
def add_weekly(manager):
print("\n Weekly Reminder")
title = input(" Title: ").strip()
message = input(" Message: ").strip()
days = ["Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday"]
print(" Days:", ", ".join(days))
day = input(" Day: ").strip().capitalize()
if day not in days:
print(" Invalid day.")
return
time_str = input(" Time (HH:MM): ").strip()
at_str = f"{day} {time_str}"
manager.add_reminder(title, message, "weekly",
remind_at=at_str, repeat_times=0)
def add_quick(manager):
print("\n Quick Reminder (in X minutes from now)")
title = input(" Title: ").strip()
message = input(" Message: ").strip()
try:
mins = int(input(" Remind in how many minutes? ").strip())
fire_at = (datetime.now() + timedelta(minutes=mins)).strftime("%d-%m-%Y %H:%M")
manager.add_reminder(title, message, "once",
remind_at=fire_at, repeat_times=1)
print(f" Will remind at: {fire_at}")
except ValueError:
print(" Invalid number.")
# ============================================================
# MAIN MENU
# ============================================================
def print_menu(manager):
active_count = sum(1 for r in manager.reminders if r.get("active"))
running_str = "Running" if manager.running else "Stopped"
print("\n" + "-"*48)
print(f" DESKTOP REMINDER [{running_str}] [{active_count} active]")
print("-"*48)
print(" 1. Add one-time reminder (specific date & time)")
print(" 2. Add quick reminder (in X minutes)")
print(" 3. Add interval reminder (every N minutes)")
print(" 4. Add daily reminder (every day at HH:MM)")
print(" 5. Add weekly reminder (day + time)")
print(" 6. View all reminders")
print(" 7. Disable a reminder")
print(" 8. Clear all reminders")
print(" 9. Start monitor (background)")
print(" 0. Exit")
print("-"*48)
def main():
print("\n" + "="*55)
print(" DESKTOP NOTIFICATION REMINDER")
print("="*55)
# Show notification engine status
if NOTIF_ENGINE == "plyer":
print("\n Notification engine : plyer (desktop pop-ups)")
elif NOTIF_ENGINE == "win10toast":
print("\n Notification engine : win10toast (Windows toast)")
else:
print("\n No notification library found.")
print(" Install one: pip install plyer")
print(" pip install win10toast (Windows)")
print(" Falling back to terminal alerts.\n")
manager = ReminderManager()
if manager.reminders:
active = sum(1 for r in manager.reminders if r.get("active"))
print(f"\n Loaded {len(manager.reminders)} reminder(s), {active} active.")
# Auto-start monitor if there are active reminders
if any(r.get("active") for r in manager.reminders):
manager.start()
print(" Monitor auto-started for existing reminders.")
while True:
print_menu(manager)
choice = input(" > ").strip()
if choice == "1":
add_once(manager)
elif choice == "2":
add_quick(manager)
elif choice == "3":
add_interval(manager)
elif choice == "4":
add_daily(manager)
elif choice == "5":
add_weekly(manager)
elif choice == "6":
manager.list_reminders()
elif choice == "7":
manager.list_reminders()
try:
idx = int(input("\n Enter reminder number to disable: ").strip())
manager.delete_reminder(idx)
except ValueError:
print(" Invalid input.")
elif choice == "8":
manager.delete_all()
elif choice == "9":
if manager.running:
print("\n Monitor is already running.")
else:
manager.start()
print("\n Monitor started! Checks every 15 seconds.")
print(" Keep this window open to receive reminders.")
elif choice == "0":
manager.stop()
print("\n Goodbye! Reminders will not fire after exit.\n")
break
else:
print(" Invalid choice.")
# ============================================================
# RUN
# ============================================================
if __name__ == "__main__":
main()