Desktop Notification Reminder

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()