Currency Converter with Live Rates

 import json

import os

import time

from datetime import datetime

from pathlib import Path

from collections import defaultdict


try:

    import requests

    REQUESTS_OK = True

except ImportError:

    REQUESTS_OK = False


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

# CONFIGURATION

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


# Free API — no key needed (up to 1500 requests/month)

BASE_URL      = "https://api.exchangerate-api.com/v4/latest/"

CACHE_FILE    = "exchange_rates_cache.json"

HISTORY_FILE  = "conversion_history.json"

CACHE_EXPIRY  = 3600   # seconds (1 hour)


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

# ALL SUPPORTED CURRENCIES WITH NAMES

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


CURRENCY_NAMES = {

    "USD": "US Dollar",

    "EUR": "Euro",

    "GBP": "British Pound",

    "INR": "Indian Rupee",

    "JPY": "Japanese Yen",

    "AUD": "Australian Dollar",

    "CAD": "Canadian Dollar",

    "CHF": "Swiss Franc",

    "CNY": "Chinese Yuan",

    "SGD": "Singapore Dollar",

    "AED": "UAE Dirham",

    "SAR": "Saudi Riyal",

    "MYR": "Malaysian Ringgit",

    "HKD": "Hong Kong Dollar",

    "NZD": "New Zealand Dollar",

    "SEK": "Swedish Krona",

    "NOK": "Norwegian Krone",

    "DKK": "Danish Krone",

    "ZAR": "South African Rand",

    "MXN": "Mexican Peso",

    "BRL": "Brazilian Real",

    "RUB": "Russian Ruble",

    "KRW": "South Korean Won",

    "TRY": "Turkish Lira",

    "IDR": "Indonesian Rupiah",

    "THB": "Thai Baht",

    "PKR": "Pakistani Rupee",

    "BDT": "Bangladeshi Taka",

    "LKR": "Sri Lankan Rupee",

    "NPR": "Nepalese Rupee",

    "PHP": "Philippine Peso",

    "VND": "Vietnamese Dong",

    "EGP": "Egyptian Pound",

    "NGN": "Nigerian Naira",

    "KWD": "Kuwaiti Dinar",

    "BHD": "Bahraini Dinar",

    "OMR": "Omani Rial",

    "QAR": "Qatari Riyal",

    "ILS": "Israeli Shekel",

    "PLN": "Polish Zloty",

    "CZK": "Czech Koruna",

    "HUF": "Hungarian Forint",

    "RON": "Romanian Leu",

    "HRK": "Croatian Kuna",

    "BGN": "Bulgarian Lev",

    "ISK": "Icelandic Krona",

    "ARS": "Argentine Peso",

    "CLP": "Chilean Peso",

    "COP": "Colombian Peso",

    "PEN": "Peruvian Sol",

    "TWD": "Taiwan Dollar",

}


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

# CACHE MANAGEMENT

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


def load_cache():

    if Path(CACHE_FILE).exists():

        try:

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

                return json.load(f)

        except:

            pass

    return {}



def save_cache(data):

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

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



def is_cache_valid(cache, base):

    if base not in cache:

        return False

    ts = cache[base].get("timestamp", 0)

    return (time.time() - ts) < CACHE_EXPIRY



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

# FETCH RATES FROM API

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


def fetch_rates(base="USD"):

    """

    Fetch live exchange rates for a base currency.

    Returns dict of rates or None on failure.

    """

    if not REQUESTS_OK:

        print("  requests library not installed.")

        return None


    cache = load_cache()


    # Return cached rates if still fresh

    if is_cache_valid(cache, base):

        age = int(time.time() - cache[base]["timestamp"])

        print(f"  Using cached rates (age: {age}s)")

        return cache[base]["rates"]


    # Fetch from API

    print(f"  Fetching live rates for {base}...")

    try:

        resp = requests.get(BASE_URL + base, timeout=10)

        resp.raise_for_status()

        data  = resp.json()

        rates = data.get("rates", {})


        # Save to cache

        cache[base] = {

            "rates":     rates,

            "timestamp": time.time(),

            "date":      data.get("date", "")

        }

        save_cache(cache)


        print(f"  Live rates fetched successfully! ({len(rates)} currencies)")

        return rates


    except requests.exceptions.ConnectionError:

        print("  No internet connection. Checking for cached rates...")

        if base in cache:

            print("  Using expired cache as fallback.")

            return cache[base]["rates"]

        return None


    except Exception as e:

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

        if base in cache:

            return cache[base]["rates"]

        return None



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

# CONVERT CURRENCY

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


def convert(amount, from_cur, to_cur, rates):

    """

    Convert amount from from_cur to to_cur using rates

    (rates are relative to their base currency).

    """

    from_cur = from_cur.upper()

    to_cur   = to_cur.upper()


    if from_cur not in rates:

        print(f"  Currency not found: {from_cur}")

        return None

    if to_cur not in rates:

        print(f"  Currency not found: {to_cur}")

        return None


    # Convert: amount / from_rate * to_rate

    result = (amount / rates[from_cur]) * rates[to_cur]

    return round(result, 4)



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

# MULTI-CURRENCY CONVERSION (1 to Many)

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


def convert_to_many(amount, from_cur, targets, rates):

    """Convert one amount to multiple currencies at once."""

    results = {}

    for to_cur in targets:

        result = convert(amount, from_cur, to_cur, rates)

        if result is not None:

            results[to_cur] = result

    return results



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

# HISTORY MANAGEMENT

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


def load_history():

    if Path(HISTORY_FILE).exists():

        try:

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

                return json.load(f)

        except:

            pass

    return []



def save_to_history(entry):

    history = load_history()

    history.append(entry)

    history = history[-100:]   # keep last 100

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

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



def display_history():

    history = load_history()

    if not history:

        print("\n  No conversion history yet.")

        return


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

    print(f"  CONVERSION HISTORY  (last {len(history)} entries)")

    print("="*58)

    print(f"  {'TIME':<20} {'FROM':<18} {'TO':<18} RATE")

    print("  " + "-"*54)


    for h in reversed(history[-20:]):

        from_str = f"{h['amount']} {h['from']}"

        to_str   = f"{h['result']} {h['to']}"

        print(f"  {h['time']:<20} {from_str:<18} {to_str:<18} "

              f"1 {h['from']} = {h['rate']} {h['to']}")

    print("="*58)



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

# DISPLAY RATE TABLE

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


def display_rate_table(base, rates, filter_list=None):

    currencies = filter_list if filter_list else list(CURRENCY_NAMES.keys())

    available  = [(c, rates[c]) for c in currencies if c in rates and c != base]


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

    print(f"  EXCHANGE RATES  (Base: {base} — {CURRENCY_NAMES.get(base, '')})")

    print("="*55)

    print(f"  {'CODE':<6} {'CURRENCY NAME':<25} {'RATE':>12}")

    print("  " + "-"*44)


    for code, rate in sorted(available, key=lambda x: x[0]):

        name = CURRENCY_NAMES.get(code, code)

        print(f"  {code:<6} {name:<25} {rate:>12.4f}")


    print("="*55)

    print(f"  Rates relative to 1 {base}")



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

# CURRENCY SEARCH

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


def search_currency(query):

    query = query.lower()

    matches = [

        (code, name) for code, name in CURRENCY_NAMES.items()

        if query in code.lower() or query in name.lower()

    ]

    if not matches:

        print(f"\n  No currencies found for: '{query}'")

    else:

        print(f"\n  Found {len(matches)} match(es):")

        for code, name in matches:

            print(f"  {code:<6} {name}")



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

# POPULAR PAIRS QUICK VIEW

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


def popular_pairs(rates):

    pairs = [

        ("USD", "EUR"), ("USD", "GBP"), ("USD", "INR"),

        ("USD", "JPY"), ("USD", "AED"), ("EUR", "GBP"),

        ("EUR", "INR"), ("GBP", "INR"), ("USD", "CNY"),

        ("USD", "AUD"), ("USD", "CAD"), ("USD", "SAR"),

    ]


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

    print("  POPULAR CURRENCY PAIRS")

    print("="*45)

    print(f"  {'PAIR':<10} {'RATE':>12}  {'REVERSE':>12}")

    print("  " + "-"*40)


    for fr, to in pairs:

        if fr in rates and to in rates:

            rate     = round((1 / rates[fr]) * rates[to], 4)

            rev_rate = round((1 / rates[to]) * rates[fr], 4)

            print(f"  {fr}/{to:<7} {rate:>12.4f}  {rev_rate:>12.4f}")


    print("="*45)

    print("  All rates vs USD base")



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

# MAIN MENU

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


def print_menu():

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

    print("  CURRENCY CONVERTER  (Live Rates)")

    print("-"*48)

    print("  1. Convert currency")

    print("  2. Convert to multiple currencies")

    print("  3. View exchange rate table")

    print("  4. Popular currency pairs")

    print("  5. Search currency by name/code")

    print("  6. View conversion history")

    print("  7. Refresh rates (clear cache)")

    print("  0. Exit")

    print("-"*48)



def main():

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

    print("     CURRENCY CONVERTER  —  Live Rates")

    print("="*55)


    if not REQUESTS_OK:

        print("\n  requests library not installed!")

        print("  Install it:  pip install requests")

        return


    # Load initial USD rates

    print("\n  Loading exchange rates...")

    rates = fetch_rates("USD")


    if not rates:

        print("\n  Could not load rates. Check your internet connection.")

        return


    # Show cache info

    cache = load_cache()

    if "USD" in cache:

        ts = datetime.fromtimestamp(cache["USD"]["timestamp"])

        print(f"  Rates date: {cache['USD'].get('date','')}  "

              f"(cached at {ts.strftime('%H:%M:%S')})")


    while True:

        print_menu()

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


        # --------------------------------------------------

        if choice == "1":

            print("\n  Single Currency Conversion")

            print(f"  (Type currency code e.g. USD, INR, EUR)")


            from_cur = input("  From currency: ").strip().upper()

            to_cur   = input("  To currency  : ").strip().upper()


            if from_cur not in rates:

                print(f"  Unknown currency: {from_cur}. Use option 5 to search.")

                continue

            if to_cur not in rates:

                print(f"  Unknown currency: {to_cur}. Use option 5 to search.")

                continue


            try:

                amount = float(input("  Amount       : ").strip())

            except ValueError:

                print("  Invalid amount.")

                continue


            result = convert(amount, from_cur, to_cur, rates)

            if result is not None:

                rate_val = round((1 / rates[from_cur]) * rates[to_cur], 6)

                from_name = CURRENCY_NAMES.get(from_cur, from_cur)

                to_name   = CURRENCY_NAMES.get(to_cur, to_cur)


                print(f"\n  {'='*45}")

                print(f"  {amount:,.2f} {from_cur} ({from_name})")

                print(f"  =  {result:,.4f} {to_cur} ({to_name})")

                print(f"  Rate: 1 {from_cur} = {rate_val} {to_cur}")

                print(f"  {'='*45}")


                save_to_history({

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

                    "amount": amount,

                    "from":   from_cur,

                    "to":     to_cur,

                    "result": result,

                    "rate":   rate_val

                })


        # --------------------------------------------------

        elif choice == "2":

            print("\n  Convert to Multiple Currencies")

            from_cur = input("  From currency: ").strip().upper()


            if from_cur not in rates:

                print(f"  Unknown currency: {from_cur}")

                continue


            try:

                amount = float(input("  Amount: ").strip())

            except ValueError:

                print("  Invalid amount.")

                continue


            print("  Target currencies (space-separated, e.g. EUR GBP INR JPY):")

            targets_input = input("  > ").strip().upper().split()


            if not targets_input:

                # Default popular targets

                targets_input = ["USD", "EUR", "GBP", "INR", "JPY",

                                 "AUD", "CAD", "AED", "SGD", "CHF"]


            results = convert_to_many(amount, from_cur, targets_input, rates)


            print(f"\n  {amount:,.2f} {from_cur} = ")

            print("  " + "-"*40)

            for cur, val in results.items():

                name = CURRENCY_NAMES.get(cur, cur)

                print(f"  {val:>14,.4f}  {cur}  ({name})")


        # --------------------------------------------------

        elif choice == "3":

            base = input("\n  Base currency (default USD): ").strip().upper() or "USD"

            if base != "USD":

                rates = fetch_rates(base)

                if not rates:

                    print("  Could not fetch rates for that base.")

                    continue

            display_rate_table(base, rates)


        # --------------------------------------------------

        elif choice == "4":

            popular_pairs(rates)


        # --------------------------------------------------

        elif choice == "5":

            query = input("\n  Search (name or code): ").strip()

            search_currency(query)


        # --------------------------------------------------

        elif choice == "6":

            display_history()


        # --------------------------------------------------

        elif choice == "7":

            if Path(CACHE_FILE).exists():

                os.remove(CACHE_FILE)

                print("\n  Cache cleared.")

            print("  Fetching fresh rates...")

            rates = fetch_rates("USD")

            if rates:

                print("  Rates updated successfully!")


        # --------------------------------------------------

        elif choice == "0":

            print("\n  Goodbye!\n")

            break


        else:

            print("  Invalid choice.")



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

# RUN

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


if __name__ == "__main__":

    main()

System Health Monitor

import os

import json

import time

import threading

from datetime import datetime, timedelta

from pathlib import Path

from collections import deque


try:

    import psutil

    PSUTIL_OK = True

except ImportError:

    PSUTIL_OK = False


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

# CONFIGURATION

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


LOG_FILE        = "system_health_log.json"

REFRESH_INTERVAL = 2      # seconds between live updates

HISTORY_LEN      = 60     # data points to keep in history

ALERT_CPU        = 85.0   # % CPU alert threshold

ALERT_RAM        = 85.0   # % RAM alert threshold

ALERT_DISK       = 90.0   # % Disk alert threshold

ALERT_TEMP       = 80.0   # °C CPU temp alert


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

# HELPERS

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


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"



def format_uptime(seconds):

    td = timedelta(seconds=int(seconds))

    days    = td.days

    hours   = td.seconds // 3600

    minutes = (td.seconds % 3600) // 60

    secs    = td.seconds % 60

    if days:

        return f"{days}d {hours}h {minutes}m"

    elif hours:

        return f"{hours}h {minutes}m {secs}s"

    else:

        return f"{minutes}m {secs}s"



def draw_bar(value, max_val=100, width=30, fill="█", empty="░"):

    filled = int((min(value, max_val) / max_val) * width)

    return fill * filled + empty * (width - filled)



def color_bar(value, width=30):

    """Return bar with level indicator based on value."""

    bar = draw_bar(value, width=width)

    if value >= 90:

        level = "CRITICAL"

    elif value >= 75:

        level = "HIGH    "

    elif value >= 50:

        level = "MEDIUM  "

    else:

        level = "OK      "

    return bar, level



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

# DATA COLLECTORS

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


def get_cpu_info():

    data = {

        "percent":      psutil.cpu_percent(interval=0.5),

        "per_core":     psutil.cpu_percent(interval=0.5, percpu=True),

        "count_logical": psutil.cpu_count(logical=True),

        "count_physical": psutil.cpu_count(logical=False),

        "freq":         None,

        "temp":         None,

    }

    try:

        freq = psutil.cpu_freq()

        if freq:

            data["freq"] = {

                "current": round(freq.current, 1),

                "min":     round(freq.min, 1),

                "max":     round(freq.max, 1)

            }

    except:

        pass


    try:

        temps = psutil.sensors_temperatures()

        if temps:

            for key in ["coretemp", "cpu_thermal", "k10temp", "acpitz"]:

                if key in temps and temps[key]:

                    data["temp"] = round(temps[key][0].current, 1)

                    break

    except:

        pass


    return data



def get_ram_info():

    vm  = psutil.virtual_memory()

    swp = psutil.swap_memory()

    return {

        "total":        vm.total,

        "available":    vm.available,

        "used":         vm.used,

        "percent":      vm.percent,

        "swap_total":   swp.total,

        "swap_used":    swp.used,

        "swap_percent": swp.percent,

    }



def get_disk_info():

    disks = []

    for part in psutil.disk_partitions(all=False):

        try:

            usage = psutil.disk_usage(part.mountpoint)

            disks.append({

                "device":     part.device,

                "mountpoint": part.mountpoint,

                "fstype":     part.fstype,

                "total":      usage.total,

                "used":       usage.used,

                "free":       usage.free,

                "percent":    usage.percent,

            })

        except (PermissionError, OSError):

            continue

    return disks



def get_network_info():

    net     = psutil.net_io_counters()

    addrs   = psutil.net_if_addrs()

    stats   = psutil.net_if_stats()


    active_ifaces = []

    for iface, stat in stats.items():

        if stat.isup and iface in addrs:

            for addr in addrs[iface]:

                if addr.family.name == "AF_INET":

                    active_ifaces.append({

                        "name":  iface,

                        "ip":    addr.address,

                        "speed": stat.speed

                    })

                    break


    return {

        "bytes_sent":   net.bytes_sent,

        "bytes_recv":   net.bytes_recv,

        "packets_sent": net.packets_sent,

        "packets_recv": net.packets_recv,

        "interfaces":   active_ifaces,

    }



def get_battery_info():

    try:

        bat = psutil.sensors_battery()

        if bat:

            return {

                "percent":    round(bat.percent, 1),

                "plugged":    bat.power_plugged,

                "secs_left":  bat.secsleft if bat.secsleft != psutil.POWER_TIME_UNLIMITED else -1,

            }

    except:

        pass

    return None



def get_top_processes(top_n=10):

    procs = []

    for p in psutil.process_iter(["pid", "name", "cpu_percent",

                                   "memory_percent", "status"]):

        try:

            procs.append(p.info)

        except (psutil.NoSuchProcess, psutil.AccessDenied):

            continue

    # Sort by CPU then memory

    return sorted(procs,

                  key=lambda x: (x.get("cpu_percent") or 0),

                  reverse=True)[:top_n]



def get_system_info():

    boot_time = psutil.boot_time()

    uptime    = time.time() - boot_time

    return {

        "platform":   os.name,

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

        "uptime_sec": uptime,

        "uptime_str": format_uptime(uptime),

        "hostname":   os.uname().nodename if hasattr(os, "uname") else os.environ.get("COMPUTERNAME", "N/A"),

        "user":       os.environ.get("USER") or os.environ.get("USERNAME", "N/A"),

    }



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

# DISPLAY SECTIONS

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


def clear_screen():

    os.system("cls" if os.name == "nt" else "clear")



def display_header():

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

    print("=" * 65)

    print(f"   SYSTEM HEALTH MONITOR          {now}")

    print("=" * 65)



def display_cpu(cpu):

    bar, level = color_bar(cpu["percent"])

    print(f"\n  CPU Usage   : {cpu['percent']:>5.1f}%  {bar}  {level}")

    if cpu.get("freq"):

        f = cpu["freq"]

        print(f"  CPU Freq    : {f['current']} MHz  "

              f"(min: {f['min']}  max: {f['max']})")

    print(f"  Cores       : {cpu['count_physical']} physical / "

          f"{cpu['count_logical']} logical")

    if cpu.get("temp"):

        temp_bar, _ = color_bar(cpu["temp"], width=20)

        print(f"  Temperature : {cpu['temp']}°C  {temp_bar}")


    # Per-core bars

    if cpu.get("per_core") and len(cpu["per_core"]) <= 16:

        print(f"  Per-Core    :", end="")

        for i, pct in enumerate(cpu["per_core"]):

            mini_bar = draw_bar(pct, width=8)

            print(f"  C{i}: {pct:4.1f}% {mini_bar}", end="")

            if (i + 1) % 2 == 0:

                print()

                print("              ", end="")

        print()



def display_ram(ram):

    bar, level = color_bar(ram["percent"])

    print(f"\n  RAM Usage   : {ram['percent']:>5.1f}%  {bar}  {level}")

    print(f"  Used / Total: {format_size(ram['used'])} / {format_size(ram['total'])}")

    print(f"  Available   : {format_size(ram['available'])}")


    if ram["swap_total"] > 0:

        sbar, slevel = color_bar(ram["swap_percent"])

        print(f"  Swap Usage  : {ram['swap_percent']:>5.1f}%  {sbar}  {slevel}")

        print(f"  Swap Used   : {format_size(ram['swap_used'])} / "

              f"{format_size(ram['swap_total'])}")



def display_disk(disks):

    print(f"\n  DISK USAGE")

    print("  " + "-"*60)

    for d in disks:

        bar, level = color_bar(d["percent"], width=20)

        print(f"  {d['mountpoint']:<12} {d['percent']:>5.1f}%  {bar}  {level}")

        print(f"  {'':12} Used: {format_size(d['used'])} / "

              f"{format_size(d['total'])}  "

              f"Free: {format_size(d['free'])}")

        print(f"  {'':12} FS: {d['fstype']}  Device: {d['device']}")



def display_network(net):

    print(f"\n  NETWORK")

    print("  " + "-"*60)

    print(f"  Sent         : {format_size(net['bytes_sent'])}")

    print(f"  Received     : {format_size(net['bytes_recv'])}")

    print(f"  Packets Sent : {net['packets_sent']:,}")

    print(f"  Packets Recv : {net['packets_recv']:,}")

    if net["interfaces"]:

        print(f"  Interfaces   :")

        for iface in net["interfaces"]:

            speed = f"{iface['speed']} Mbps" if iface["speed"] else "N/A"

            print(f"    {iface['name']:<15} IP: {iface['ip']:<18} Speed: {speed}")



def display_battery(bat):

    if not bat:

        return

    print(f"\n  BATTERY")

    print("  " + "-"*60)

    bar, level = color_bar(bat["percent"])

    plug = "Plugged In" if bat["plugged"] else "On Battery"

    print(f"  Charge      : {bat['percent']:>5.1f}%  {bar}  {level}")

    print(f"  Status      : {plug}")

    if bat["secs_left"] > 0 and not bat["plugged"]:

        print(f"  Time Left   : {format_uptime(bat['secs_left'])}")



def display_processes(procs):

    print(f"\n  TOP PROCESSES  (by CPU%)")

    print("  " + "-"*60)

    print(f"  {'PID':<8} {'NAME':<22} {'CPU%':>6}  {'MEM%':>6}  STATUS")

    print("  " + "-"*55)

    for p in procs[:10]:

        name   = (p.get("name") or "?")[:20]

        cpu    = p.get("cpu_percent") or 0.0

        mem    = p.get("memory_percent") or 0.0

        status = p.get("status") or "?"

        print(f"  {p['pid']:<8} {name:<22} {cpu:>5.1f}%  {mem:>5.1f}%  {status}")



def display_system(sys_info):

    print(f"\n  SYSTEM INFO")

    print("  " + "-"*60)

    print(f"  Hostname    : {sys_info['hostname']}")

    print(f"  User        : {sys_info['user']}")

    print(f"  Boot Time   : {sys_info['boot_time']}")

    print(f"  Uptime      : {sys_info['uptime_str']}")



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

# ALERTS

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


def check_alerts(cpu, ram, disks, bat, temp):

    alerts = []

    if cpu["percent"] >= ALERT_CPU:

        alerts.append(f"HIGH CPU: {cpu['percent']:.1f}% (threshold: {ALERT_CPU}%)")

    if ram["percent"] >= ALERT_RAM:

        alerts.append(f"HIGH RAM: {ram['percent']:.1f}% (threshold: {ALERT_RAM}%)")

    for d in disks:

        if d["percent"] >= ALERT_DISK:

            alerts.append(f"DISK FULL: {d['mountpoint']} at "

                          f"{d['percent']:.1f}% (threshold: {ALERT_DISK}%)")

    if cpu.get("temp") and cpu["temp"] >= ALERT_TEMP:

        alerts.append(f"HIGH TEMP: {cpu['temp']}°C (threshold: {ALERT_TEMP}°C)")

    if bat and not bat["plugged"] and bat["percent"] <= 15:

        alerts.append(f"LOW BATTERY: {bat['percent']}% — please plug in!")

    return alerts



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

# LIVE DASHBOARD

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


def live_dashboard(duration_secs=0):

    """

    Show a live-refreshing dashboard.

    duration_secs=0 runs until user presses Ctrl+C.

    """

    print("\n  Starting live dashboard... Press Ctrl+C to stop.\n")

    time.sleep(1)


    start      = time.time()

    prev_net   = psutil.net_io_counters()

    prev_time  = time.time()


    try:

        while True:

            clear_screen()

            display_header()


            cpu     = get_cpu_info()

            ram     = get_ram_info()

            disks   = get_disk_info()

            net     = get_network_info()

            bat     = get_battery_info()

            procs   = get_top_processes()

            sysinfo = get_system_info()


            # Network speed (bytes per second)

            now_net   = psutil.net_io_counters()

            now_time  = time.time()

            elapsed   = now_time - prev_time or 1

            dl_speed  = (now_net.bytes_recv - prev_net.bytes_recv) / elapsed

            ul_speed  = (now_net.bytes_sent - prev_net.bytes_sent) / elapsed

            prev_net  = now_net

            prev_time = now_time


            display_system(sysinfo)

            display_cpu(cpu)

            display_ram(ram)

            display_disk(disks)


            # Network with live speed

            print(f"\n  NETWORK  (Live speed)")

            print("  " + "-"*60)

            print(f"  Download Speed : {format_size(dl_speed)}/s")

            print(f"  Upload Speed   : {format_size(ul_speed)}/s")

            print(f"  Total Sent     : {format_size(net['bytes_sent'])}")

            print(f"  Total Received : {format_size(net['bytes_recv'])}")

            if net["interfaces"]:

                for iface in net["interfaces"]:

                    print(f"  {iface['name']:<15} {iface['ip']}")


            display_battery(bat)

            display_processes(procs)


            # Alerts

            alerts = check_alerts(cpu, ram, disks, bat, cpu.get("temp"))

            if alerts:

                print(f"\n  *** ALERTS ***")

                for a in alerts:

                    print(f"  ! {a}")


            print(f"\n  Refreshing every {REFRESH_INTERVAL}s  |  Ctrl+C to stop")


            if duration_secs > 0 and (time.time() - start) >= duration_secs:

                break


            time.sleep(REFRESH_INTERVAL)


    except KeyboardInterrupt:

        print("\n\n  Dashboard stopped.")



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

# SNAPSHOT REPORT

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


def snapshot_report():

    print("\n  Taking system snapshot...")

    cpu     = get_cpu_info()

    ram     = get_ram_info()

    disks   = get_disk_info()

    net     = get_network_info()

    bat     = get_battery_info()

    sysinfo = get_system_info()


    clear_screen()

    display_header()

    display_system(sysinfo)

    display_cpu(cpu)

    display_ram(ram)

    display_disk(disks)

    display_network(net)

    display_battery(bat)


    alerts = check_alerts(cpu, ram, disks, bat, cpu.get("temp"))

    if alerts:

        print(f"\n  *** ALERTS ***")

        for a in alerts:

            print(f"  ! {a}")

    else:

        print("\n  All systems OK. No alerts.")



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

# SAVE SNAPSHOT TO JSON

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


def save_snapshot():

    cpu     = get_cpu_info()

    ram     = get_ram_info()

    disks   = get_disk_info()

    net     = get_network_info()

    bat     = get_battery_info()

    sysinfo = get_system_info()


    snapshot = {

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

        "system":    sysinfo,

        "cpu": {

            "percent": cpu["percent"],

            "freq":    cpu.get("freq"),

            "temp":    cpu.get("temp"),

            "cores":   cpu["count_logical"]

        },

        "ram": {

            "percent":   ram["percent"],

            "used_gb":   round(ram["used"] / 1e9, 2),

            "total_gb":  round(ram["total"] / 1e9, 2),

        },

        "disks": [

            {

                "mount":   d["mountpoint"],

                "percent": d["percent"],

                "free_gb": round(d["free"] / 1e9, 2),

            }

            for d in disks

        ],

        "battery": bat,

        "network": {

            "bytes_sent": net["bytes_sent"],

            "bytes_recv": net["bytes_recv"],

        }

    }


    # Append to log file

    log = []

    if Path(LOG_FILE).exists():

        try:

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

                log = json.load(f)

        except:

            log = []


    log.append(snapshot)

    log = log[-200:]   # keep last 200 snapshots


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

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


    print(f"\n  Snapshot saved to {LOG_FILE}  ({len(log)} total)")



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

# MAIN MENU

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


def print_menu():

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

    print("  SYSTEM HEALTH MONITOR")

    print("-"*48)

    print("  1. Live dashboard (auto-refresh)")

    print("  2. Snapshot report (one-time view)")

    print("  3. Top processes")

    print("  4. Disk usage only")

    print("  5. Network info")

    print("  6. Save snapshot to JSON log")

    print("  7. Alert thresholds info")

    print("  0. Exit")

    print("-"*48)



def main():

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

    print("     SYSTEM HEALTH MONITOR")

    print("="*55)


    if not PSUTIL_OK:

        print("\n  psutil is not installed!")

        print("  Install it with:  pip install psutil")

        return


    while True:

        print_menu()

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


        if choice == "1":

            live_dashboard()


        elif choice == "2":

            snapshot_report()

            input("\n  Press Enter to return to menu...")


        elif choice == "3":

            procs = get_top_processes(15)

            clear_screen()

            display_header()

            display_processes(procs)

            input("\n  Press Enter to return to menu...")


        elif choice == "4":

            disks = get_disk_info()

            clear_screen()

            display_header()

            display_disk(disks)

            input("\n  Press Enter to return to menu...")


        elif choice == "5":

            net = get_network_info()

            clear_screen()

            display_header()

            display_network(net)

            input("\n  Press Enter to return to menu...")


        elif choice == "6":

            save_snapshot()


        elif choice == "7":

            print(f"\n  Current alert thresholds:")

            print(f"  CPU usage   : >= {ALERT_CPU}%")

            print(f"  RAM usage   : >= {ALERT_RAM}%")

            print(f"  Disk usage  : >= {ALERT_DISK}%")

            print(f"  CPU temp    : >= {ALERT_TEMP}°C")

            print(f"  Battery low : <= 15%")

            print(f"\n  Edit ALERT_* constants at the top of the script to change.")


        elif choice == "0":

            print("\n  Goodbye!\n")

            break


        else:

            print("  Invalid choice.")



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

# RUN

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


if __name__ == "__main__":

    main()

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