Auto Screenshot Scheduler

import os

import json

import time

import threading

from datetime import datetime

from pathlib import Path


try:

    import pyautogui

    PYAUTOGUI_OK = True

except ImportError:

    PYAUTOGUI_OK = False


try:

    from PIL import Image, ImageDraw, ImageFont

    PILLOW_OK = True

except ImportError:

    PILLOW_OK = False


# ============================================================

# CONFIGURATION

# ============================================================


CONFIG_FILE   = "screenshot_config.json"

DEFAULT_DIR   = "screenshots"

DEFAULT_INTERVAL = 60       # seconds between screenshots

DEFAULT_FORMAT   = "png"    # png or jpg

MAX_SCREENSHOTS  = 0        # 0 = unlimited


# ============================================================

# LOAD & SAVE CONFIG

# ============================================================


def load_config():

    if Path(CONFIG_FILE).exists():

        try:

            with open(CONFIG_FILE, "r") as f:

                return json.load(f)

        except:

            pass

    return {

        "save_dir":    DEFAULT_DIR,

        "interval":    DEFAULT_INTERVAL,

        "format":      DEFAULT_FORMAT,

        "max_count":   MAX_SCREENSHOTS,

        "add_timestamp_overlay": False,

        "prefix":      "screenshot",

        "quality":     95

    }



def save_config(config):

    with open(CONFIG_FILE, "w") as f:

        json.dump(config, f, indent=2)

    print("  Config saved.")



# ============================================================

# GENERATE FILENAME

# ============================================================


def generate_filename(save_dir, prefix, fmt):

    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")

    filename  = f"{prefix}_{timestamp}.{fmt}"

    return Path(save_dir) / filename



# ============================================================

# ADD TIMESTAMP OVERLAY TO IMAGE

# ============================================================


def add_timestamp_overlay(img_path):

    """Burn timestamp text into the bottom-right corner of the image."""

    if not PILLOW_OK:

        return


    try:

        img   = Image.open(img_path).convert("RGBA")

        draw  = ImageDraw.Draw(img)

        ts    = datetime.now().strftime("%d-%m-%Y  %H:%M:%S")

        w, h  = img.size


        # Draw background box

        margin  = 10

        box_h   = 28

        box_w   = len(ts) * 8 + margin * 2

        box_x   = w - box_w - margin

        box_y   = h - box_h - margin


        draw.rectangle(

            [box_x - 4, box_y - 4, box_x + box_w, box_y + box_h],

            fill=(0, 0, 0, 160)

        )

        draw.text(

            (box_x, box_y),

            ts,

            fill=(255, 255, 255, 255)

        )


        img = img.convert("RGB")

        img.save(img_path)

    except Exception as e:

        pass   # overlay is optional — don't crash on failure



# ============================================================

# TAKE A SINGLE SCREENSHOT

# ============================================================


def take_screenshot(config):

    """

    Capture the screen and save it.

    Returns (filepath, success).

    """

    save_dir = config["save_dir"]

    prefix   = config.get("prefix", "screenshot")

    fmt      = config.get("format", "png").lower()

    quality  = config.get("quality", 95)

    overlay  = config.get("add_timestamp_overlay", False)


    Path(save_dir).mkdir(parents=True, exist_ok=True)

    filepath = generate_filename(save_dir, prefix, fmt)


    try:

        if not PYAUTOGUI_OK:

            print("  pyautogui not installed — cannot take screenshot.")

            return None, False


        screenshot = pyautogui.screenshot()


        if fmt == "jpg" or fmt == "jpeg":

            screenshot = screenshot.convert("RGB")

            screenshot.save(str(filepath), "JPEG", quality=quality)

        else:

            screenshot.save(str(filepath), "PNG")


        if overlay and PILLOW_OK:

            add_timestamp_overlay(str(filepath))


        size_str = _format_size(filepath.stat().st_size)

        print(f"  Saved: {filepath.name}  ({size_str})")

        return filepath, True


    except Exception as e:

        print(f"  Screenshot error: {e}")

        return None, False



# ============================================================

# SCHEDULER CLASS

# ============================================================


class ScreenshotScheduler:

    def __init__(self, config):

        self.config    = config

        self.running   = False

        self.count     = 0

        self.log       = []

        self._thread   = None


    def _run(self):

        interval  = self.config.get("interval", DEFAULT_INTERVAL)

        max_count = self.config.get("max_count", 0)


        print(f"\n  Scheduler started.")

        print(f"  Interval : every {interval}s")

        print(f"  Save to  : {self.config['save_dir']}")

        print(f"  Max shots: {'unlimited' if max_count == 0 else max_count}")

        print(f"  Press Ctrl+C or use menu to stop.\n")


        self.running = True


        while self.running:

            filepath, ok = take_screenshot(self.config)


            if ok:

                self.count += 1

                self.log.append({

                    "index":    self.count,

                    "file":     str(filepath),

                    "time":     datetime.now().strftime("%d-%m-%Y %H:%M:%S"),

                    "size":     _format_size(filepath.stat().st_size)

                })


            # Stop if max count reached

            if max_count > 0 and self.count >= max_count:

                print(f"\n  Reached max {max_count} screenshots. Stopping.")

                self.running = False

                break


            # Wait interval (in 1s ticks so we can stop cleanly)

            for _ in range(interval):

                if not self.running:

                    break

                time.sleep(1)


    def start(self):

        if self.running:

            print("\n  Scheduler is already running.")

            return

        self._thread = threading.Thread(target=self._run, daemon=True)

        self._thread.start()


    def stop(self):

        self.running = False

        print("\n  Scheduler stopped.")


    def status(self):

        status_str = "RUNNING" if self.running else "STOPPED"

        print(f"\n  Status   : {status_str}")

        print(f"  Captured : {self.count} screenshot(s)")

        print(f"  Save dir : {self.config['save_dir']}")

        if self.log:

            last = self.log[-1]

            print(f"  Last shot: {last['file']} at {last['time']}")


    def show_log(self):

        if not self.log:

            print("\n  No screenshots taken yet.")

            return


        print("\n" + "="*60)

        print(f"  SCREENSHOT LOG  ({len(self.log)} captured)")

        print("="*60)

        print(f"  {'#':<5} {'TIME':<22} {'SIZE':<10} FILE")

        print("  " + "-"*55)


        for entry in self.log[-20:]:   # show last 20

            fname = Path(entry["file"]).name

            print(f"  {entry['index']:<5} {entry['time']:<22} "

                  f"{entry['size']:<10} {fname}")


        if len(self.log) > 20:

            print(f"  ... and {len(self.log) - 20} more.")

        print("="*60)


    def folder_summary(self):

        save_dir = Path(self.config["save_dir"])

        if not save_dir.exists():

            print("\n  Save folder does not exist yet.")

            return


        fmt    = self.config.get("format", "png")

        files  = list(save_dir.glob(f"*.{fmt}")) + list(save_dir.glob(f"*.jpg"))

        total  = sum(f.stat().st_size for f in files)


        print(f"\n  Folder   : {save_dir}")

        print(f"  Files    : {len(files)}")

        print(f"  Total    : {_format_size(total)}")


        if files:

            oldest = min(files, key=lambda f: f.stat().st_mtime)

            newest = max(files, key=lambda f: f.stat().st_mtime)

            print(f"  Oldest   : {oldest.name}")

            print(f"  Newest   : {newest.name}")



# ============================================================

# SETTINGS EDITOR

# ============================================================


def edit_settings(config):

    print("\n" + "="*50)

    print("  SETTINGS")

    print("="*50)

    print(f"  Current settings:")

    print(f"  1. Save directory   : {config['save_dir']}")

    print(f"  2. Interval (secs)  : {config['interval']}")

    print(f"  3. Format           : {config['format']}")

    print(f"  4. Max screenshots  : {config['max_count']} (0=unlimited)")

    print(f"  5. Filename prefix  : {config['prefix']}")

    print(f"  6. JPEG quality     : {config['quality']} (1-100)")

    print(f"  7. Timestamp overlay: {config['add_timestamp_overlay']}")

    print(f"  8. Save & return")

    print("="*50)


    while True:

        sub = input("\n  Edit setting (1-8): ").strip()


        if sub == "1":

            val = input(f"  Save directory [{config['save_dir']}]: ").strip()

            if val:

                config["save_dir"] = val


        elif sub == "2":

            val = input(f"  Interval in seconds [{config['interval']}]: ").strip()

            if val.isdigit():

                config["interval"] = max(1, int(val))


        elif sub == "3":

            val = input("  Format (png/jpg) [png]: ").strip().lower()

            if val in ["png", "jpg", "jpeg"]:

                config["format"] = val


        elif sub == "4":

            val = input(f"  Max screenshots (0=unlimited) [{config['max_count']}]: ").strip()

            if val.isdigit():

                config["max_count"] = int(val)


        elif sub == "5":

            val = input(f"  Prefix [{config['prefix']}]: ").strip()

            if val:

                config["prefix"] = val


        elif sub == "6":

            val = input(f"  JPEG quality 1-100 [{config['quality']}]: ").strip()

            if val.isdigit():

                config["quality"] = max(1, min(100, int(val)))


        elif sub == "7":

            val = input("  Timestamp overlay (y/n): ").strip().lower()

            config["add_timestamp_overlay"] = val == "y"


        elif sub == "8":

            save_config(config)

            break


        else:

            print("  Invalid option.")


    return config



# ============================================================

# HELPER

# ============================================================


def _format_size(size_bytes):

    for unit in ["B", "KB", "MB", "GB"]:

        if size_bytes < 1024:

            return f"{size_bytes:.1f} {unit}"

        size_bytes /= 1024

    return f"{size_bytes:.1f} GB"



# ============================================================

# DEPENDENCY CHECK

# ============================================================


def check_deps():

    print("\n  Dependency check:")

    print(f"  pyautogui : {'OK' if PYAUTOGUI_OK else 'MISSING  ->  pip install pyautogui'}")

    print(f"  Pillow    : {'OK' if PILLOW_OK    else 'MISSING  ->  pip install Pillow'}")

    if not PYAUTOGUI_OK:

        print("\n  pyautogui is required to take screenshots.")

        return False

    return True



# ============================================================

# MAIN MENU

# ============================================================


def print_menu(scheduler):

    status = "RUNNING" if scheduler.running else "STOPPED"

    count  = scheduler.count

    print("\n" + "-"*48)

    print(f"  AUTO SCREENSHOT SCHEDULER  [{status}] [{count} taken]")

    print("-"*48)

    print("  1. Start scheduler")

    print("  2. Stop scheduler")

    print("  3. Take one screenshot NOW")

    print("  4. View screenshot log")

    print("  5. Folder summary")

    print("  6. Settings")

    print("  7. Check dependencies")

    print("  0. Exit")

    print("-"*48)



def main():

    print("\n" + "="*55)

    print("     AUTO SCREENSHOT SCHEDULER")

    print("="*55)


    config    = load_config()

    scheduler = ScreenshotScheduler(config)


    print(f"\n  Save dir : {config['save_dir']}")

    print(f"  Interval : every {config['interval']}s")

    print(f"  Format   : {config['format'].upper()}")


    while True:

        print_menu(scheduler)

        choice = input("  > ").strip()


        if choice == "1":

            if not PYAUTOGUI_OK:

                print("\n  pyautogui not installed. Run option 7 for install help.")

            else:

                scheduler.config = config

                scheduler.start()


        elif choice == "2":

            scheduler.stop()


        elif choice == "3":

            if not PYAUTOGUI_OK:

                print("\n  pyautogui not installed. Run option 7 for install help.")

            else:

                take_screenshot(config)


        elif choice == "4":

            scheduler.show_log()


        elif choice == "5":

            scheduler.folder_summary()


        elif choice == "6":

            config = edit_settings(config)

            scheduler.config = config


        elif choice == "7":

            check_deps()


        elif choice == "0":

            scheduler.stop()

            scheduler.status()

            print("\n  Goodbye!\n")

            break


        else:

            print("  Invalid choice.")



# ============================================================

# RUN

# ============================================================


if __name__ == "__main__":

    main()

Folder Size Analyzer

import os

import json

import shutil

from pathlib import Path

from collections import defaultdict

from datetime import datetime


# ============================================================

# CONFIGURATION

# ============================================================


REPORT_FILE = "folder_size_report.json"

BAR_WIDTH   = 30    # width of ASCII bar chart

TOP_N       = 10    # default top N items to show


# ============================================================

# FORMAT FILE SIZE

# ============================================================


def format_size(size_bytes):

    if size_bytes < 0:

        return "0 B"

    for unit in ["B", "KB", "MB", "GB", "TB"]:

        if size_bytes < 1024:

            return f"{size_bytes:.1f} {unit}"

        size_bytes /= 1024

    return f"{size_bytes:.1f} PB"



# ============================================================

# DRAW ASCII BAR

# ============================================================


def draw_bar(value, max_value, width=BAR_WIDTH):

    if max_value == 0:

        return ""

    filled = int((value / max_value) * width)

    return "█" * filled + "░" * (width - filled)



# ============================================================

# GET FOLDER SIZE (Recursive)

# ============================================================


def get_folder_size(path):

    """Recursively compute total size of a folder."""

    total = 0

    try:

        for entry in os.scandir(path):

            try:

                if entry.is_file(follow_symlinks=False):

                    total += entry.stat().st_size

                elif entry.is_dir(follow_symlinks=False):

                    total += get_folder_size(entry.path)

            except (PermissionError, OSError):

                continue

    except (PermissionError, OSError):

        pass

    return total



# ============================================================

# SCAN: TOP-LEVEL BREAKDOWN

# ============================================================


def scan_top_level(root_path):

    """

    Returns size of each immediate child (file or folder)

    inside root_path.

    """

    root  = Path(root_path)

    items = []


    entries = sorted(root.iterdir(), key=lambda e: e.name)

    total   = len(entries)


    print(f"\n  Scanning {total} item(s) in '{root_path}'...")


    for i, entry in enumerate(entries, 1):

        print(f"  [{i}/{total}] {entry.name[:40]}...", end="\r")

        try:

            if entry.is_file(follow_symlinks=False):

                size     = entry.stat().st_size

                is_dir   = False

                modified = datetime.fromtimestamp(

                    entry.stat().st_mtime).strftime("%d-%m-%Y")

            elif entry.is_dir(follow_symlinks=False):

                size     = get_folder_size(entry)

                is_dir   = True

                modified = datetime.fromtimestamp(

                    entry.stat().st_mtime).strftime("%d-%m-%Y")

            else:

                continue


            items.append({

                "name":     entry.name,

                "path":     str(entry),

                "size":     size,

                "is_dir":   is_dir,

                "modified": modified

            })

        except (PermissionError, OSError):

            continue


    print(" " * 60, end="\r")  # clear progress line

    return sorted(items, key=lambda x: x["size"], reverse=True)



# ============================================================

# SCAN: FILE EXTENSION BREAKDOWN

# ============================================================


def scan_by_extension(root_path, recursive=True):

    """

    Group total size by file extension across the folder.

    """

    ext_map   = defaultdict(lambda: {"size": 0, "count": 0})

    root      = Path(root_path)

    all_files = root.rglob("*") if recursive else root.glob("*")


    for f in all_files:

        if f.is_file():

            try:

                ext  = f.suffix.lower() or "(no extension)"

                size = f.stat().st_size

                ext_map[ext]["size"]  += size

                ext_map[ext]["count"] += 1

            except (PermissionError, OSError):

                continue


    return dict(sorted(ext_map.items(),

                        key=lambda x: x[1]["size"], reverse=True))



# ============================================================

# SCAN: LARGEST FILES

# ============================================================


def find_largest_files(root_path, top_n=20, recursive=True):

    """Find the top N largest individual files."""

    root      = Path(root_path)

    all_files = root.rglob("*") if recursive else root.glob("*")

    files     = []


    for f in all_files:

        if f.is_file():

            try:

                size = f.stat().st_size

                mtime = datetime.fromtimestamp(

                    f.stat().st_mtime).strftime("%d-%m-%Y")

                files.append((size, str(f), mtime))

            except (PermissionError, OSError):

                continue


    return sorted(files, reverse=True)[:top_n]



# ============================================================

# SCAN: OLDEST / LARGEST FILES COMBO

# ============================================================


def find_old_large_files(root_path, days_old=365, min_size_mb=10):

    """Find files older than N days AND larger than min_size_mb."""

    root       = Path(root_path)

    cutoff     = datetime.now().timestamp() - (days_old * 86400)

    min_bytes  = min_size_mb * 1024 * 1024

    results    = []


    for f in root.rglob("*"):

        if f.is_file():

            try:

                stat = f.stat()

                if stat.st_mtime < cutoff and stat.st_size >= min_bytes:

                    results.append({

                        "path":     str(f),

                        "size":     stat.st_size,

                        "modified": datetime.fromtimestamp(

                            stat.st_mtime).strftime("%d-%m-%Y")

                    })

            except (PermissionError, OSError):

                continue


    return sorted(results, key=lambda x: x["size"], reverse=True)



# ============================================================

# DISPLAY: TOP-LEVEL BREAKDOWN

# ============================================================


def display_top_level(items, top_n=TOP_N):

    if not items:

        print("\n  Folder is empty.")

        return


    shown    = items[:top_n]

    max_size = shown[0]["size"] if shown else 1

    total    = sum(i["size"] for i in items)


    print("\n" + "="*70)

    print(f"  FOLDER SIZE BREAKDOWN  (Top {min(top_n, len(items))} of {len(items)} items)")

    print("="*70)

    print(f"  {'NAME':<28} {'SIZE':>10}  {'%':>5}  VISUAL")

    print("  " + "-"*66)


    for item in shown:

        name     = item["name"]

        icon     = "D" if item["is_dir"] else "F"

        pct      = (item["size"] / total * 100) if total > 0 else 0

        bar      = draw_bar(item["size"], max_size)

        size_str = format_size(item["size"])

        # Truncate long names

        display_name = f"[{icon}] {name}"

        if len(display_name) > 28:

            display_name = display_name[:25] + "..."


        print(f"  {display_name:<28} {size_str:>10}  {pct:>4.1f}%  {bar}")


    print("  " + "-"*66)

    print(f"  {'TOTAL':<28} {format_size(total):>10}")

    print("="*70)


    # Disk usage summary

    try:

        usage = shutil.disk_usage(items[0]["path"].rsplit(os.sep, 1)[0])

        print(f"\n  Disk Total : {format_size(usage.total)}")

        print(f"  Disk Used  : {format_size(usage.used)}  "

              f"({usage.used/usage.total*100:.1f}%)")

        print(f"  Disk Free  : {format_size(usage.free)}")

    except Exception:

        pass



# ============================================================

# DISPLAY: EXTENSION BREAKDOWN

# ============================================================


def display_extensions(ext_map, top_n=15):

    if not ext_map:

        print("\n  No files found.")

        return


    items    = list(ext_map.items())[:top_n]

    max_size = items[0][1]["size"] if items else 1

    total    = sum(v["size"] for v in ext_map.values())


    print("\n" + "="*65)

    print(f"  SIZE BY FILE TYPE  (Top {min(top_n, len(ext_map))} of {len(ext_map)} types)")

    print("="*65)

    print(f"  {'EXT':<14} {'SIZE':>10}  {'COUNT':>7}  {'%':>5}  VISUAL")

    print("  " + "-"*60)


    for ext, data in items:

        pct      = (data["size"] / total * 100) if total > 0 else 0

        bar      = draw_bar(data["size"], max_size, width=20)

        size_str = format_size(data["size"])

        print(f"  {ext:<14} {size_str:>10}  {data['count']:>7}  "

              f"{pct:>4.1f}%  {bar}")


    print("  " + "-"*60)

    print(f"  {'TOTAL':<14} {format_size(total):>10}  "

          f"{sum(v['count'] for v in ext_map.values()):>7}")

    print("="*65)



# ============================================================

# DISPLAY: LARGEST FILES

# ============================================================


def display_largest_files(files, top_n=20):

    if not files:

        print("\n  No files found.")

        return


    max_size = files[0][0] if files else 1


    print("\n" + "="*70)

    print(f"  TOP {len(files)} LARGEST FILES")

    print("="*70)

    print(f"  {'#':<4} {'SIZE':>10}  {'MODIFIED':<12}  FILE")

    print("  " + "-"*65)


    for i, (size, path, mtime) in enumerate(files, 1):

        bar      = draw_bar(size, max_size, width=12)

        size_str = format_size(size)

        name     = Path(path).name

        # Truncate long paths

        display_path = path if len(path) <= 45 else "..." + path[-42:]

        print(f"  {i:<4} {size_str:>10}  {mtime:<12}  {display_path}")


    print("="*70)



# ============================================================

# DISPLAY: OLD + LARGE FILES

# ============================================================


def display_old_large(results, days_old, min_size_mb):

    if not results:

        print(f"\n  No files found older than {days_old} days "

              f"and larger than {min_size_mb} MB.")

        return


    total_size = sum(r["size"] for r in results)


    print("\n" + "="*70)

    print(f"  OLD + LARGE FILES  (>{days_old} days old, >{min_size_mb} MB)")

    print("="*70)

    print(f"  Found {len(results)} file(s) using {format_size(total_size)} total\n")


    for i, r in enumerate(results, 1):

        print(f"  [{i:02d}] {format_size(r['size']):>10}  "

              f"Modified: {r['modified']}  {r['path']}")


    print("\n" + "="*70)

    print(f"  Potential space to reclaim: {format_size(total_size)}")

    print("="*70)



# ============================================================

# SAVE REPORT

# ============================================================


def save_report(data, report_type, folder):

    report = {

        "report_type":   report_type,

        "folder":        folder,

        "generated_at":  datetime.now().strftime("%d-%m-%Y %H:%M:%S"),

        "data":          data

    }

    with open(REPORT_FILE, "w", encoding="utf-8") as f:

        json.dump(report, f, indent=2, default=str)

    print(f"\n  Report saved: {REPORT_FILE}")



# ============================================================

# MAIN MENU

# ============================================================


def print_menu():

    print("\n" + "-"*48)

    print("  FOLDER SIZE ANALYZER")

    print("-"*48)

    print("  1. Top-level size breakdown")

    print("  2. Breakdown by file type/extension")

    print("  3. Find largest files")

    print("  4. Find old + large files (cleanup hints)")

    print("  5. Full analysis (all of the above)")

    print("  6. Save last report to JSON")

    print("  0. Exit")

    print("-"*48)



def main():

    print("\n" + "="*55)

    print("     FOLDER SIZE ANALYZER")

    print("="*55)

    print("\n  Visualize what's eating your disk space.")

    print("  Uses ASCII bar charts for instant insight.\n")


    last_data   = {}

    last_folder = ""


    while True:

        print_menu()

        choice = input("  > ").strip()


        if choice not in ["0", "6"] and not last_folder or choice in ["1","2","3","4","5"]:

            folder = input("\n  Enter folder path to analyze: ").strip()

            if not os.path.isdir(folder):

                print("  Invalid folder path.")

                continue

            last_folder = folder


        if choice == "1":

            items = scan_top_level(last_folder)

            top_n = input(f"  Show top N items (default {TOP_N}): ").strip()

            top_n = int(top_n) if top_n.isdigit() else TOP_N

            display_top_level(items, top_n)

            last_data = {"items": [

                {**i, "size": format_size(i["size"])} for i in items

            ]}


        elif choice == "2":

            recursive = input("  Include subfolders? (y/n, default y): ").strip().lower()

            recursive = recursive != "n"

            ext_map   = scan_by_extension(last_folder, recursive)

            display_extensions(ext_map)

            last_data = {

                ext: {"size": format_size(v["size"]), "count": v["count"]}

                for ext, v in ext_map.items()

            }


        elif choice == "3":

            top_n = input(f"  How many largest files to show (default 20): ").strip()

            top_n = int(top_n) if top_n.isdigit() else 20

            files = find_largest_files(last_folder, top_n)

            display_largest_files(files, top_n)

            last_data = [

                {"size": format_size(s), "path": p, "modified": m}

                for s, p, m in files

            ]


        elif choice == "4":

            days = input("  Older than how many days? (default 365): ").strip()

            days = int(days) if days.isdigit() else 365

            size = input("  Minimum file size in MB (default 10): ").strip()

            size = int(size) if size.isdigit() else 10

            results = find_old_large_files(last_folder, days, size)

            display_old_large(results, days, size)

            last_data = [

                {**r, "size": format_size(r["size"])} for r in results

            ]


        elif choice == "5":

            print("\n  Running full analysis...\n")


            items = scan_top_level(last_folder)

            display_top_level(items, TOP_N)


            ext_map = scan_by_extension(last_folder)

            display_extensions(ext_map)


            files = find_largest_files(last_folder, 10)

            display_largest_files(files, 10)


            results = find_old_large_files(last_folder, 365, 10)

            display_old_large(results, 365, 10)


            last_data = {

                "top_level":    [

                    {**i, "size": format_size(i["size"])} for i in items

                ],

                "by_extension": {

                    ext: {"size": format_size(v["size"]), "count": v["count"]}

                    for ext, v in ext_map.items()

                },

                "largest_files": [

                    {"size": format_size(s), "path": p, "modified": m}

                    for s, p, m in files

                ],

                "old_large_files": [

                    {**r, "size": format_size(r["size"])} for r in results

                ]

            }


        elif choice == "6":

            if not last_data:

                print("\n  No analysis data yet. Run an analysis first.")

            else:

                save_report(last_data, "folder_analysis", last_folder)


        elif choice == "0":

            print("\n  Goodbye!\n")

            break


        else:

            print("  Invalid choice.")



# ============================================================

# RUN

# ============================================================


if __name__ == "__main__":

    main()

Duplicate File Finder

import os

import hashlib

import json

import shutil

from pathlib import Path

from collections import defaultdict

from datetime import datetime


# ============================================================

# CONFIGURATION

# ============================================================


REPORT_FILE   = "duplicate_report.json"

CHUNK_SIZE    = 8192   # bytes read per chunk for hashing


# ============================================================

# HASH A FILE (MD5)

# ============================================================


def get_file_hash(filepath, algorithm="md5"):

    """

    Compute hash of a file in chunks to handle large files.

    Returns hex digest string or None on error.

    """

    try:

        h = hashlib.new(algorithm)

        with open(filepath, "rb") as f:

            while chunk := f.read(CHUNK_SIZE):

                h.update(chunk)

        return h.hexdigest()

    except (PermissionError, OSError):

        return None



# ============================================================

# QUICK PRE-FILTER: Group by File Size First

# ============================================================


def group_by_size(folder, recursive=True, extensions=None):

    """

    First pass: group files by size.

    Only files sharing the same size are candidates for duplication.

    This avoids hashing every single file.

    """

    size_map = defaultdict(list)


    if recursive:

        all_files = Path(folder).rglob("*")

    else:

        all_files = Path(folder).glob("*")


    for f in all_files:

        if not f.is_file():

            continue

        if extensions:

            if f.suffix.lower() not in extensions:

                continue

        try:

            size = f.stat().st_size

            if size > 0:   # skip empty files

                size_map[size].append(f)

        except OSError:

            continue


    # Keep only groups with 2+ files (potential duplicates)

    return {size: files for size, files in size_map.items() if len(files) > 1}



# ============================================================

# SECOND PASS: Hash Files Within Same-Size Groups

# ============================================================


def find_duplicates(folder, recursive=True, extensions=None, progress=True):

    """

    Full duplicate detection:

    1. Group by size  (fast)

    2. Hash same-size files (thorough)

    Returns dict: hash -> list of duplicate file paths

    """

    print(f"\n  Scanning: {folder}")

    print(f"  Mode    : {'Recursive' if recursive else 'Top-level only'}")

    if extensions:

        print(f"  Filter  : {', '.join(extensions)}")


    # Step 1 — Group by size

    print("\n  Step 1/2: Grouping files by size...")

    size_groups = group_by_size(folder, recursive, extensions)

    candidate_count = sum(len(v) for v in size_groups.values())

    print(f"  Found {candidate_count} candidate file(s) in {len(size_groups)} size group(s)")


    if not size_groups:

        print("\n  No duplicate candidates found.")

        return {}


    # Step 2 — Hash candidates

    print("\n  Step 2/2: Computing file hashes...")

    hash_map  = defaultdict(list)

    processed = 0


    for size, files in size_groups.items():

        for f in files:

            file_hash = get_file_hash(f)

            if file_hash:

                hash_map[file_hash].append(f)

            processed += 1

            if progress and processed % 50 == 0:

                print(f"  Processed {processed}/{candidate_count} files...", end="\r")


    print(f"  Processed {processed}/{candidate_count} files.         ")


    # Keep only actual duplicates (2+ files with same hash)

    duplicates = {h: files for h, files in hash_map.items() if len(files) > 1}

    return duplicates



# ============================================================

# DISPLAY RESULTS

# ============================================================


def display_results(duplicates):

    if not duplicates:

        print("\n  No duplicates found!")

        return


    total_groups = len(duplicates)

    total_files  = sum(len(v) for v in duplicates.values())

    total_wasted = 0


    print("\n" + "="*60)

    print(f"  DUPLICATE FILES FOUND")

    print("="*60)

    print(f"  Duplicate groups : {total_groups}")

    print(f"  Total duplicates : {total_files}")

    print("="*60)


    for i, (file_hash, files) in enumerate(duplicates.items(), 1):

        size_bytes = files[0].stat().st_size

        size_str   = _format_size(size_bytes)

        wasted     = size_bytes * (len(files) - 1)

        total_wasted += wasted


        print(f"\n  Group {i} | {len(files)} files | {size_str} each | "

              f"Wasted: {_format_size(wasted)}")

        print(f"  Hash: {file_hash[:16]}...")

        print("  " + "-"*54)


        for j, f in enumerate(files):

            try:

                mtime = datetime.fromtimestamp(f.stat().st_mtime).strftime("%d-%m-%Y %H:%M")

            except:

                mtime = "unknown"

            marker = "  [ORIGINAL?]" if j == 0 else "  [DUPLICATE]"

            print(f"  {marker}  {f}")

            print(f"            Modified: {mtime}")


    print("\n" + "="*60)

    print(f"  Total wasted space: {_format_size(total_wasted)}")

    print("="*60)



# ============================================================

# SAVE REPORT

# ============================================================


def save_report(duplicates, folder):

    report = {

        "scanned_folder": str(folder),

        "scan_time":      datetime.now().strftime("%d-%m-%Y %H:%M:%S"),

        "total_groups":   len(duplicates),

        "total_files":    sum(len(v) for v in duplicates.values()),

        "groups": []

    }


    for file_hash, files in duplicates.items():

        group = {

            "hash":  file_hash,

            "size":  _format_size(files[0].stat().st_size),

            "files": [str(f) for f in files]

        }

        report["groups"].append(group)


    with open(REPORT_FILE, "w", encoding="utf-8") as f:

        json.dump(report, f, indent=2)


    print(f"\n  Report saved: {REPORT_FILE}")



# ============================================================

# DELETE DUPLICATES (Keep First / Newest / Oldest)

# ============================================================


def delete_duplicates(duplicates, strategy="keep_first", move_to=None):

    """

    strategy:

      keep_first  - keep the first file in each group, delete rest

      keep_newest - keep most recently modified file

      keep_oldest - keep oldest file

    move_to: if set, move duplicates here instead of deleting

    """

    if not duplicates:

        print("  No duplicates to remove.")

        return


    if move_to:

        Path(move_to).mkdir(parents=True, exist_ok=True)

        action = f"move to '{move_to}'"

    else:

        action = "DELETE PERMANENTLY"


    print(f"\n  Strategy : {strategy}")

    print(f"  Action   : {action}")


    confirm = input(f"\n  Confirm? This will {action} duplicates. (yes/no): ").strip().lower()

    if confirm != "yes":

        print("  Cancelled.")

        return


    removed_count = 0

    freed_bytes   = 0


    for file_hash, files in duplicates.items():

        # Sort by strategy to pick which to keep

        if strategy == "keep_newest":

            files_sorted = sorted(files, key=lambda f: f.stat().st_mtime, reverse=True)

        elif strategy == "keep_oldest":

            files_sorted = sorted(files, key=lambda f: f.stat().st_mtime)

        else:

            files_sorted = files  # keep_first = as found


        keeper    = files_sorted[0]

        to_remove = files_sorted[1:]


        print(f"\n  Keeping : {keeper.name}")


        for dup in to_remove:

            try:

                size = dup.stat().st_size

                if move_to:

                    dest = Path(move_to) / dup.name

                    # Handle name collision in destination

                    if dest.exists():

                        dest = Path(move_to) / f"{dup.stem}_{file_hash[:6]}{dup.suffix}"

                    shutil.move(str(dup), str(dest))

                    print(f"  Moved   : {dup.name} -> {dest}")

                else:

                    dup.unlink()

                    print(f"  Deleted : {dup}")

                removed_count += 1

                freed_bytes   += size

            except Exception as e:

                print(f"  Error   : {dup} -> {e}")


    print(f"\n  Done! {removed_count} file(s) removed. "

          f"Freed: {_format_size(freed_bytes)}")



# ============================================================

# INTERACTIVE MODE: Pick Which to Delete Per Group

# ============================================================


def interactive_delete(duplicates):

    if not duplicates:

        print("  No duplicates to review.")

        return


    print("\n  Interactive mode: Review each group and choose what to delete.")

    print("  Enter file numbers to DELETE (comma separated), or 'skip' to keep all.\n")


    total_freed = 0


    for i, (file_hash, files) in enumerate(duplicates.items(), 1):

        size_str = _format_size(files[0].stat().st_size)

        print(f"\n  Group {i}/{len(duplicates)}  |  {size_str} each")

        print("  " + "-"*50)


        for j, f in enumerate(files, 1):

            try:

                mtime = datetime.fromtimestamp(f.stat().st_mtime).strftime("%d-%m-%Y %H:%M")

            except:

                mtime = "unknown"

            print(f"  [{j}] {f}  (modified: {mtime})")


        choice = input("\n  Delete file numbers (e.g. 2,3) or 'skip': ").strip().lower()


        if choice == "skip" or not choice:

            continue


        try:

            indices = [int(x.strip()) - 1 for x in choice.split(",")]

            for idx in indices:

                if 0 <= idx < len(files):

                    f = files[idx]

                    size = f.stat().st_size

                    f.unlink()

                    print(f"  Deleted: {f}")

                    total_freed += size

                else:

                    print(f"  Invalid index: {idx + 1}")

        except ValueError:

            print("  Invalid input, skipping group.")


    print(f"\n  Interactive cleanup done. Freed: {_format_size(total_freed)}")



# ============================================================

# HELPER: Format File Size

# ============================================================


def _format_size(size_bytes):

    for unit in ["B", "KB", "MB", "GB", "TB"]:

        if size_bytes < 1024:

            return f"{size_bytes:.1f} {unit}"

        size_bytes /= 1024

    return f"{size_bytes:.1f} PB"



# ============================================================

# MAIN MENU

# ============================================================


def print_menu():

    print("\n" + "-"*48)

    print("  DUPLICATE FILE FINDER")

    print("-"*48)

    print("  1. Scan folder for duplicates")

    print("  2. Scan with file type filter")

    print("  3. Auto-delete duplicates (keep first)")

    print("  4. Auto-delete duplicates (keep newest)")

    print("  5. Move duplicates to a folder")

    print("  6. Interactive delete (review each group)")

    print("  7. Save report to JSON")

    print("  0. Exit")

    print("-"*48)



def main():

    print("\n" + "="*55)

    print("     DUPLICATE FILE FINDER")

    print("="*55)

    print("\n  Uses MD5 hashing for accurate duplicate detection.")

    print("  Pre-filters by file size for maximum speed.\n")


    duplicates = {}

    last_folder = ""


    while True:

        print_menu()

        choice = input("  > ").strip()


        if choice in ["1", "2", "3", "4", "5", "6"]:

            if choice in ["1", "2"]:

                folder = input("\n  Enter folder path to scan: ").strip()

                if not os.path.isdir(folder):

                    print("  Invalid folder path.")

                    continue

                last_folder = folder


                recursive = input("  Include subfolders? (y/n, default y): ").strip().lower()

                recursive = recursive != "n"


                extensions = None

                if choice == "2":

                    ext_input = input("  File types (e.g. .jpg .png .pdf): ").strip()

                    extensions = [e.lower() if e.startswith(".") else f".{e.lower()}"

                                  for e in ext_input.split()] if ext_input else None


                duplicates = find_duplicates(folder, recursive, extensions)

                display_results(duplicates)


            elif not duplicates:

                print("\n  Please scan a folder first (Option 1 or 2).")

                continue


            if choice == "3":

                delete_duplicates(duplicates, strategy="keep_first")


            elif choice == "4":

                delete_duplicates(duplicates, strategy="keep_newest")


            elif choice == "5":

                move_path = input("\n  Move duplicates to folder: ").strip()

                delete_duplicates(duplicates, strategy="keep_first", move_to=move_path)


            elif choice == "6":

                interactive_delete(duplicates)


        elif choice == "7":

            if not duplicates:

                print("\n  No scan results to save. Run a scan first.")

            else:

                save_report(duplicates, last_folder)


        elif choice == "0":

            print("\n  Goodbye!\n")

            break


        else:

            print("  Invalid choice.")



# ============================================================

# RUN

# ============================================================


if __name__ == "__main__":

    main()

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