Unit Converter

 


import os

import json

from datetime import datetime

from pathlib import Path

from collections import defaultdict


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

# CONVERSION DATA

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


CATEGORIES = {


    # ── LENGTH ──────────────────────────────────────────────

    "length": {

        "base":  "meter",

        "units": {

            "meter":      1,

            "kilometer":  1000,

            "centimeter": 0.01,

            "millimeter": 0.001,

            "micrometer": 1e-6,

            "nanometer":  1e-9,

            "mile":       1609.344,

            "yard":       0.9144,

            "foot":       0.3048,

            "inch":       0.0254,

            "nautical mile": 1852,

            "light year": 9.461e15,

        }

    },


    # ── WEIGHT / MASS ────────────────────────────────────────

    "weight": {

        "base":  "kilogram",

        "units": {

            "kilogram":     1,

            "gram":         0.001,

            "milligram":    1e-6,

            "microgram":    1e-9,

            "metric ton":   1000,

            "pound":        0.453592,

            "ounce":        0.0283495,

            "stone":        6.35029,

            "quintal":      100,

            "carat":        0.0002,

        }

    },


    # ── TEMPERATURE ──────────────────────────────────────────

    "temperature": {

        "base":  "celsius",

        "units": {

            "celsius":    None,   # special handling

            "fahrenheit": None,

            "kelvin":     None,

            "rankine":    None,

        }

    },


    # ── VOLUME ───────────────────────────────────────────────

    "volume": {

        "base":  "liter",

        "units": {

            "liter":          1,

            "milliliter":     0.001,

            "cubic meter":    1000,

            "cubic centimeter": 0.001,

            "cubic inch":     0.0163871,

            "cubic foot":     28.3168,

            "gallon (US)":    3.78541,

            "gallon (UK)":    4.54609,

            "quart (US)":     0.946353,

            "pint (US)":      0.473176,

            "cup (US)":       0.236588,

            "fluid ounce":    0.0295735,

            "tablespoon":     0.0147868,

            "teaspoon":       0.00492892,

        }

    },


    # ── SPEED ────────────────────────────────────────────────

    "speed": {

        "base":  "meter/second",

        "units": {

            "meter/second":    1,

            "kilometer/hour":  0.277778,

            "mile/hour":       0.44704,

            "foot/second":     0.3048,

            "knot":            0.514444,

            "mach":            340.29,

            "light speed":     299792458,

        }

    },


    # ── AREA ─────────────────────────────────────────────────

    "area": {

        "base":  "square meter",

        "units": {

            "square meter":      1,

            "square kilometer":  1e6,

            "square centimeter": 1e-4,

            "square millimeter": 1e-6,

            "square mile":       2589988.11,

            "square yard":       0.836127,

            "square foot":       0.092903,

            "square inch":       6.4516e-4,

            "hectare":           10000,

            "acre":              4046.86,

            "cent":              40.4686,    # Indian unit

            "bigha":             2508.38,    # common Indian unit

        }

    },


    # ── TIME ─────────────────────────────────────────────────

    "time": {

        "base":  "second",

        "units": {

            "second":       1,

            "millisecond":  0.001,

            "microsecond":  1e-6,

            "nanosecond":   1e-9,

            "minute":       60,

            "hour":         3600,

            "day":          86400,

            "week":         604800,

            "month":        2628000,   # avg 30.44 days

            "year":         31536000,

            "decade":       315360000,

            "century":      3153600000,

        }

    },


    # ── DATA / STORAGE ───────────────────────────────────────

    "data": {

        "base":  "byte",

        "units": {

            "bit":       0.125,

            "byte":      1,

            "kilobyte":  1024,

            "megabyte":  1024**2,

            "gigabyte":  1024**3,

            "terabyte":  1024**4,

            "petabyte":  1024**5,

            "kibibyte":  1024,

            "mebibyte":  1024**2,

            "gibibyte":  1024**3,

        }

    },


    # ── PRESSURE ─────────────────────────────────────────────

    "pressure": {

        "base":  "pascal",

        "units": {

            "pascal":      1,

            "kilopascal":  1000,

            "megapascal":  1e6,

            "bar":         100000,

            "millibar":    100,

            "atmosphere":  101325,

            "psi":         6894.76,

            "mmHg":        133.322,

            "torr":        133.322,

            "inHg":        3386.39,

        }

    },


    # ── ENERGY ───────────────────────────────────────────────

    "energy": {

        "base":  "joule",

        "units": {

            "joule":          1,

            "kilojoule":      1000,

            "megajoule":      1e6,

            "calorie":        4.184,

            "kilocalorie":    4184,

            "watt-hour":      3600,

            "kilowatt-hour":  3.6e6,

            "electronvolt":   1.602e-19,

            "BTU":            1055.06,

            "therm":          105480400,

            "foot-pound":     1.35582,

        }

    },


    # ── FUEL EFFICIENCY ──────────────────────────────────────

    "fuel": {

        "base":  "km/l",

        "units": {

            "km/l":    1,

            "l/100km": None,   # inverse — special handling

            "mpg (US)": 0.425144,

            "mpg (UK)": 0.354006,

            "miles/l":  1.60934,

        }

    },


}


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

# HISTORY FILE

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


HISTORY_FILE = "unit_converter_history.json"


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_history(entry):

    history = load_history()

    history.append(entry)

    history = history[-50:]

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

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



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

# CONVERSION LOGIC

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


def convert_temperature(value, from_unit, to_unit):

    """Special handling for temperature conversions."""

    # Convert to Celsius first

    if from_unit == "celsius":

        celsius = value

    elif from_unit == "fahrenheit":

        celsius = (value - 32) * 5 / 9

    elif from_unit == "kelvin":

        celsius = value - 273.15

    elif from_unit == "rankine":

        celsius = (value - 491.67) * 5 / 9

    else:

        return None


    # Convert from Celsius to target

    if to_unit == "celsius":

        return celsius

    elif to_unit == "fahrenheit":

        return celsius * 9 / 5 + 32

    elif to_unit == "kelvin":

        return celsius + 273.15

    elif to_unit == "rankine":

        return (celsius + 273.15) * 9 / 5

    return None



def convert_fuel(value, from_unit, to_unit):

    """Special handling for fuel efficiency (l/100km is inverse)."""

    # Convert to km/l first

    if from_unit == "l/100km":

        kml = 100 / value if value != 0 else 0

    else:

        factor = CATEGORIES["fuel"]["units"].get(from_unit, 1)

        kml    = value * factor


    # Convert from km/l to target

    if to_unit == "l/100km":

        return 100 / kml if kml != 0 else 0

    else:

        factor = CATEGORIES["fuel"]["units"].get(to_unit, 1)

        return kml / factor



def convert(value, from_unit, to_unit, category):

    """

    Universal conversion function.

    Returns converted value or None.

    """

    from_unit = from_unit.lower().strip()

    to_unit   = to_unit.lower().strip()


    if from_unit == to_unit:

        return value


    cat_data = CATEGORIES.get(category)

    if not cat_data:

        return None


    # Special categories

    if category == "temperature":

        return convert_temperature(value, from_unit, to_unit)

    if category == "fuel":

        return convert_fuel(value, from_unit, to_unit)


    units = cat_data["units"]


    if from_unit not in units or to_unit not in units:

        return None


    # Convert via base unit

    value_in_base = value * units[from_unit]

    result        = value_in_base / units[to_unit]

    return result



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

# FORMAT RESULT

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


def fmt_result(value):

    """Smart formatting: remove unnecessary decimals."""

    if value is None:

        return "N/A"

    if abs(value) >= 1e12 or (abs(value) < 1e-6 and value != 0):

        return f"{value:.6e}"

    if value == int(value):

        return f"{int(value):,}"

    if abs(value) >= 100:

        return f"{value:,.4f}"

    if abs(value) >= 1:

        return f"{value:.6f}".rstrip("0").rstrip(".")

    return f"{value:.10f}".rstrip("0").rstrip(".")



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

# DISPLAY UNIT LIST

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


def list_units(category):

    cat_data = CATEGORIES.get(category)

    if not cat_data:

        print(f"  Unknown category: {category}")

        return


    units = list(cat_data["units"].keys())

    print(f"\n  Units in '{category}':")

    print("  " + "-"*40)

    for i, unit in enumerate(units, 1):

        print(f"  {i:>3}. {unit}")

    print("  " + "-"*40)



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

# QUICK CONVERT (ALL TO ONE)

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


def convert_one_to_all(value, from_unit, category):

    """Show value converted to ALL units in the category."""

    cat_data = CATEGORIES.get(category)

    if not cat_data:

        return


    print(f"\n  {fmt_result(value)} {from_unit}  =")

    print("  " + "-"*45)


    for unit in cat_data["units"].keys():

        if unit == from_unit.lower():

            continue

        result = convert(value, from_unit, unit, category)

        if result is not None:

            print(f"  {fmt_result(result):<20}  {unit}")


    print("  " + "-"*45)



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

# INTERACTIVE CONVERSION

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


def do_convert(category):

    list_units(category)


    from_unit = input(f"\n  From unit: ").strip().lower()

    to_unit   = input(f"  To unit  : ").strip().lower()


    cat_data = CATEGORIES.get(category, {})

    units    = cat_data.get("units", {})


    # Fuzzy match

    def fuzzy_match(u, unit_list):

        if u in unit_list:

            return u

        matches = [k for k in unit_list if u in k or k in u]

        return matches[0] if matches else None


    from_unit = fuzzy_match(from_unit, units) or from_unit

    to_unit   = fuzzy_match(to_unit, units) or to_unit


    if from_unit not in units:

        print(f"  Unknown unit: {from_unit}")

        return

    if to_unit not in units:

        print(f"  Unknown unit: {to_unit}")

        return


    try:

        value = float(input(f"  Value     : ").strip())

    except ValueError:

        print("  Invalid value.")

        return


    result = convert(value, from_unit, to_unit, category)


    if result is None:

        print("  Conversion failed.")

        return


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

    print(f"  {fmt_result(value)} {from_unit}")

    print(f"  =  {fmt_result(result)} {to_unit}")

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


    # Show all option

    show_all = input("\n  Show conversion to ALL units? (y/n): ").strip().lower()

    if show_all == "y":

        convert_one_to_all(value, from_unit, category)


    # Save to history

    save_history({

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

        "category": category,

        "value":    value,

        "from":     from_unit,

        "result":   result,

        "to":       to_unit,

    })



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

# QUICK CONVERTER (COMMON PAIRS)

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


def quick_convert():

    print("\n  QUICK CONVERSIONS")

    print("  " + "-"*40)


    quick_pairs = [

        ("1",  "km",       "mile",       "length"),

        ("2",  "kg",       "pound",      "weight"),

        ("3",  "celsius",  "fahrenheit", "temperature"),

        ("4",  "liter",    "gallon (US)","volume"),

        ("5",  "meter/second", "km/hour","speed"),

        ("6",  "hectare",  "acre",       "area"),

        ("7",  "gigabyte", "megabyte",   "data"),

        ("8",  "bar",      "psi",        "pressure"),

        ("9",  "kilocalorie","joule",    "energy"),

        ("10", "km/l",     "mpg (US)",   "fuel"),

    ]


    for num, f, t, cat in quick_pairs:

        result = convert(1, f, t, cat)

        print(f"  {num:>3}. 1 {f:<18} = {fmt_result(result)} {t}")


    print("  " + "-"*40)

    print("\n  Enter a number to use that conversion,")

    choice = input("  or press Enter to go back: ").strip()


    if not choice:

        return


    # Find the pair

    selected = None

    for num, f, t, cat in quick_pairs:

        if choice == num:

            selected = (f, t, cat)

            break


    if not selected:

        return


    from_unit, to_unit, category = selected


    try:

        value = float(input(f"\n  Enter value in {from_unit}: ").strip())

    except ValueError:

        print("  Invalid value.")

        return


    result = convert(value, from_unit, to_unit, category)

    print(f"\n  {fmt_result(value)} {from_unit}  =  {fmt_result(result)} {to_unit}")


    save_history({

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

        "category": category,

        "value":    value,

        "from":     from_unit,

        "result":   result,

        "to":       to_unit,

    })



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

# SHOW HISTORY

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


def show_history():

    history = load_history()

    if not history:

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

        return


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

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

    print("="*65)

    print(f"  {'TIME':<18} {'CAT':<14} {'FROM':<25} TO")

    print("  " + "-"*60)


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

        from_str = f"{fmt_result(h['value'])} {h['from']}"

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

        print(f"  {h['time']:<18} {h['category']:<14} "

              f"{from_str:<25} {to_str}")

    print("="*65)



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

# MAIN MENU

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


def print_menu():

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

    print("  UNIT CONVERTER")

    print("-"*50)

    print("  1.  Length")

    print("  2.  Weight / Mass")

    print("  3.  Temperature")

    print("  4.  Volume")

    print("  5.  Speed")

    print("  6.  Area")

    print("  7.  Time")

    print("  8.  Data / Storage")

    print("  9.  Pressure")

    print("  10. Energy")

    print("  11. Fuel Efficiency")

    print("  12. Quick conversions (common pairs)")

    print("  13. Conversion history")

    print("  0.  Exit")

    print("-"*50)



MENU_MAP = {

    "1":  "length",

    "2":  "weight",

    "3":  "temperature",

    "4":  "volume",

    "5":  "speed",

    "6":  "area",

    "7":  "time",

    "8":  "data",

    "9":  "pressure",

    "10": "energy",

    "11": "fuel",

}



def main():

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

    print("     UNIT CONVERTER")

    print("="*55)

    print(f"\n  {len(CATEGORIES)} categories  |  "

          f"{sum(len(v['units']) for v in CATEGORIES.values())}+ units")

    print("  Length, Weight, Temperature, Volume, Speed,")

    print("  Area, Time, Data, Pressure, Energy, Fuel")


    while True:

        print_menu()

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


        if choice in MENU_MAP:

            do_convert(MENU_MAP[choice])


        elif choice == "12":

            quick_convert()


        elif choice == "13":

            show_history()


        elif choice == "0":

            print("\n  Goodbye!\n")

            break


        else:

            print("  Invalid choice.")



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

# RUN

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


if __name__ == "__main__":

    main()

Contact Book Manager

 import json

import csv

import os

import re

from datetime import datetime

from pathlib import Path


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

# CONFIGURATION

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


DATA_FILE   = "contacts.json"

BACKUP_FILE = "contacts_backup.json"


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

# LOAD & SAVE

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


def load_contacts():

    if Path(DATA_FILE).exists():

        try:

            with open(DATA_FILE, "r", encoding="utf-8") as f:

                return json.load(f)

        except:

            pass

    return {"contacts": [], "groups": []}



def save_contacts(data):

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

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



def backup_contacts(data):

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

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

    print(f"  Backup saved: {BACKUP_FILE}")



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

# VALIDATION

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


def validate_phone(phone):

    """Accept digits, spaces, +, -, (, )"""

    return bool(re.match(r"^[\d\s\+\-\(\)]{7,20}$", phone.strip()))



def validate_email(email):

    return bool(re.match(r"^[\w\.\+\-]+@[\w\-]+\.[a-zA-Z]{2,}$", email.strip()))



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

# ADD CONTACT

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


def add_contact(data):

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

    print("  ADD NEW CONTACT")

    print("="*50)


    # Required

    name = input("  Full Name*    : ").strip()

    if not name:

        print("  Name is required.")

        return


    # Check duplicate

    if any(c["name"].lower() == name.lower() for c in data["contacts"]):

        overwrite = input(f"  '{name}' already exists. Add anyway? (y/n): ").strip().lower()

        if overwrite != "y":

            return


    # Optional fields

    phone = input("  Phone Number  : ").strip()

    if phone and not validate_phone(phone):

        print("  Invalid phone format. Saving as-is.")


    email = input("  Email         : ").strip()

    if email and not validate_email(email):

        print("  Invalid email format. Saving as-is.")


    address   = input("  Address       : ").strip()

    birthday  = input("  Birthday (DD-MM-YYYY): ").strip()

    company   = input("  Company       : ").strip()

    notes     = input("  Notes         : ").strip()


    # Groups

    if data["groups"]:

        print(f"\n  Available groups: {', '.join(data['groups'])}")

    group = input("  Group (or new group name): ").strip()

    if group and group not in data["groups"]:

        data["groups"].append(group)


    contact = {

        "id":         len(data["contacts"]) + 1,

        "name":       name,

        "phone":      phone,

        "email":      email,

        "address":    address,

        "birthday":   birthday,

        "company":    company,

        "notes":      notes,

        "group":      group,

        "favourite":  False,

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

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

    }


    data["contacts"].append(contact)

    save_contacts(data)

    print(f"\n  Contact saved: {name}")



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

# VIEW CONTACT

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


def view_contact(contact):

    w = 50

    fav = " ★" if contact.get("favourite") else ""

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

    print(f"  {contact['name']}{fav}")

    print("="*w)


    fields = [

        ("Phone",    contact.get("phone",    "")),

        ("Email",    contact.get("email",    "")),

        ("Company",  contact.get("company",  "")),

        ("Address",  contact.get("address",  "")),

        ("Birthday", contact.get("birthday", "")),

        ("Group",    contact.get("group",    "")),

        ("Notes",    contact.get("notes",    "")),

        ("Added",    contact.get("created_at", "")),

        ("Updated",  contact.get("updated_at", "")),

    ]

    for label, value in fields:

        if value:

            print(f"  {label:<10}: {value}")


    print("="*w)



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

# LIST CONTACTS

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


def list_contacts(data, group_filter=None, favourites_only=False,

                  sort_by="name"):

    contacts = data["contacts"]


    if group_filter:

        contacts = [c for c in contacts

                    if c.get("group", "").lower() == group_filter.lower()]

    if favourites_only:

        contacts = [c for c in contacts if c.get("favourite")]


    if not contacts:

        print("\n  No contacts found.")

        return []


    # Sort

    if sort_by == "name":

        contacts = sorted(contacts, key=lambda c: c["name"].lower())

    elif sort_by == "company":

        contacts = sorted(contacts, key=lambda c: c.get("company", "").lower())

    elif sort_by == "group":

        contacts = sorted(contacts, key=lambda c: c.get("group", "").lower())

    elif sort_by == "recent":

        contacts = sorted(contacts, key=lambda c: c.get("updated_at", ""),

                          reverse=True)


    label = ""

    if group_filter:

        label = f"  Group: {group_filter}"

    elif favourites_only:

        label = "  Favourites"


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

    print(f"  CONTACTS  ({len(contacts)} total){label}")

    print("="*65)

    print(f"  {'#':<5} {'NAME':<22} {'PHONE':<16} "

          f"{'EMAIL':<22} {'GRP'}")

    print("  " + "-"*60)


    for i, c in enumerate(contacts, 1):

        fav   = "★" if c.get("favourite") else " "

        name  = c["name"][:20]

        phone = c.get("phone", "")[:14]

        email = c.get("email", "")[:20]

        group = c.get("group", "")[:8]

        print(f"  {fav}{i:<4} {name:<22} {phone:<16} "

              f"{email:<22} {group}")


    print("="*65)

    return contacts



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

# SEARCH CONTACTS

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


def search_contacts(data, query):

    query = query.lower().strip()

    results = []


    for c in data["contacts"]:

        if (query in c["name"].lower()

                or query in c.get("phone", "").lower()

                or query in c.get("email", "").lower()

                or query in c.get("company", "").lower()

                or query in c.get("address", "").lower()

                or query in c.get("notes", "").lower()):

            results.append(c)


    if not results:

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

        return []


    print(f"\n  Found {len(results)} result(s) for '{query}':")

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

        fav   = "★" if c.get("favourite") else " "

        phone = c.get("phone", "N/A")

        email = c.get("email", "")

        print(f"  {fav}[{i}] {c['name']}  |  {phone}  |  {email}")


    return results



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

# EDIT CONTACT

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


def edit_contact(data, contact):

    print(f"\n  Editing: {contact['name']}")

    print("  (Press Enter to keep current value)\n")


    fields = [

        ("name",     "Full Name",    contact.get("name", "")),

        ("phone",    "Phone",        contact.get("phone", "")),

        ("email",    "Email",        contact.get("email", "")),

        ("company",  "Company",      contact.get("company", "")),

        ("address",  "Address",      contact.get("address", "")),

        ("birthday", "Birthday",     contact.get("birthday", "")),

        ("notes",    "Notes",        contact.get("notes", "")),

        ("group",    "Group",        contact.get("group", "")),

    ]


    for key, label, current in fields:

        new_val = input(f"  {label:<12} [{current}]: ").strip()

        if new_val:

            contact[key] = new_val


    # Favourite toggle

    fav = input(f"  Favourite? (y/n) [{'+' if contact.get('favourite') else '-'}]: ").strip().lower()

    if fav == "y":

        contact["favourite"] = True

    elif fav == "n":

        contact["favourite"] = False


    contact["updated_at"] = datetime.now().strftime("%d-%m-%Y %H:%M")


    # Update in data

    for i, c in enumerate(data["contacts"]):

        if c["id"] == contact["id"]:

            data["contacts"][i] = contact

            break


    # Add new group if needed

    if contact["group"] and contact["group"] not in data["groups"]:

        data["groups"].append(contact["group"])


    save_contacts(data)

    print(f"\n  Contact updated: {contact['name']}")



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

# DELETE CONTACT

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


def delete_contact(data, contact):

    confirm = input(f"\n  Delete '{contact['name']}'? (yes/no): ").strip().lower()

    if confirm == "yes":

        data["contacts"] = [c for c in data["contacts"]

                            if c["id"] != contact["id"]]

        save_contacts(data)

        print(f"  Deleted: {contact['name']}")

    else:

        print("  Cancelled.")



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

# BIRTHDAY REMINDERS

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


def birthday_reminders(data):

    today     = datetime.today()

    upcoming  = []


    for c in data["contacts"]:

        bday = c.get("birthday", "")

        if not bday:

            continue

        try:

            bday_dt = datetime.strptime(bday, "%d-%m-%Y")

            # This year's birthday

            this_year = bday_dt.replace(year=today.year)

            if this_year < today:

                this_year = bday_dt.replace(year=today.year + 1)

            days_left = (this_year - today).days

            age = today.year - bday_dt.year

            if this_year.year > today.year:

                age -= 1

            upcoming.append((days_left, c["name"], bday, age + 1))

        except:

            continue


    upcoming.sort(key=lambda x: x[0])


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

    print("  UPCOMING BIRTHDAYS")

    print("="*52)


    if not upcoming:

        print("  No birthdays found. Add birthdays when creating contacts.")

        return


    for days, name, bday, age in upcoming[:15]:

        if days == 0:

            when = "TODAY!"

        elif days == 1:

            when = "Tomorrow"

        elif days <= 7:

            when = f"In {days} days"

        elif days <= 30:

            when = f"In {days} days"

        else:

            when = f"In {days} days"


        print(f"  {when:<15} {name:<25} {bday}  (turns {age})")


    print("="*52)



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

# EXPORT TO CSV

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


def export_csv(data, filename=None):

    contacts = data["contacts"]

    if not contacts:

        print("  No contacts to export.")

        return


    if not filename:

        filename = f"contacts_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"


    fields = ["name", "phone", "email", "company",

              "address", "birthday", "group", "notes"]


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

        writer = csv.DictWriter(f, fieldnames=fields, extrasaction="ignore")

        writer.writeheader()

        for c in contacts:

            writer.writerow({k: c.get(k, "") for k in fields})


    print(f"\n  Exported {len(contacts)} contact(s) to: {filename}")



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

# IMPORT FROM CSV

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


def import_csv(data, filename):

    if not Path(filename).exists():

        print(f"  File not found: {filename}")

        return


    imported = 0

    skipped  = 0


    with open(filename, "r", encoding="utf-8", newline="") as f:

        reader = csv.DictReader(f)

        for row in reader:

            name = row.get("name", "").strip()

            if not name:

                skipped += 1

                continue


            # Skip if already exists

            if any(c["name"].lower() == name.lower()

                   for c in data["contacts"]):

                skipped += 1

                continue


            contact = {

                "id":         len(data["contacts"]) + 1,

                "name":       name,

                "phone":      row.get("phone", ""),

                "email":      row.get("email", ""),

                "address":    row.get("address", ""),

                "birthday":   row.get("birthday", ""),

                "company":    row.get("company", ""),

                "notes":      row.get("notes", ""),

                "group":      row.get("group", ""),

                "favourite":  False,

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

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

            }

            data["contacts"].append(contact)


            # Add group

            if contact["group"] and contact["group"] not in data["groups"]:

                data["groups"].append(contact["group"])


            imported += 1


    save_contacts(data)

    print(f"\n  Imported: {imported}  |  Skipped (duplicate/empty): {skipped}")



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

# GROUPS

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


def manage_groups(data):

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

    print("  GROUPS")

    print("="*45)


    if not data["groups"]:

        print("  No groups yet.")

    else:

        for i, g in enumerate(data["groups"], 1):

            count = sum(1 for c in data["contacts"]

                        if c.get("group") == g)

            print(f"  [{i}] {g}  ({count} contacts)")


    print("\n  1. View contacts in a group")

    print("  2. Rename a group")

    print("  3. Delete a group")

    print("  4. Back")


    sub = input("\n  > ").strip()


    if sub == "1":

        group = input("  Group name: ").strip()

        list_contacts(data, group_filter=group)


    elif sub == "2":

        old = input("  Current group name: ").strip()

        new = input("  New group name    : ").strip()

        if old and new:

            for c in data["contacts"]:

                if c.get("group") == old:

                    c["group"] = new

            if old in data["groups"]:

                data["groups"].remove(old)

            if new not in data["groups"]:

                data["groups"].append(new)

            save_contacts(data)

            print(f"  Renamed '{old}' to '{new}'")


    elif sub == "3":

        group = input("  Group to delete: ").strip()

        confirm = input(f"  Delete group '{group}'? Contacts will be ungrouped. (yes/no): ").strip().lower()

        if confirm == "yes":

            for c in data["contacts"]:

                if c.get("group") == group:

                    c["group"] = ""

            if group in data["groups"]:

                data["groups"].remove(group)

            save_contacts(data)

            print(f"  Group deleted: {group}")



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

# STATS

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


def show_stats(data):

    contacts = data["contacts"]

    total    = len(contacts)


    if not total:

        print("\n  No contacts yet.")

        return


    with_phone  = sum(1 for c in contacts if c.get("phone"))

    with_email  = sum(1 for c in contacts if c.get("email"))

    with_bday   = sum(1 for c in contacts if c.get("birthday"))

    favourites  = sum(1 for c in contacts if c.get("favourite"))


    group_counts = {}

    for c in contacts:

        g = c.get("group") or "Ungrouped"

        group_counts[g] = group_counts.get(g, 0) + 1


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

    print("  CONTACT BOOK STATISTICS")

    print("="*48)

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

    print(f"  With phone      : {with_phone}")

    print(f"  With email      : {with_email}")

    print(f"  With birthday   : {with_bday}")

    print(f"  Favourites      : {favourites}")

    print(f"\n  By group:")

    for g, count in sorted(group_counts.items(), key=lambda x: -x[1]):

        bar = "█" * min(count, 20)

        print(f"  {g:<20} {count:>4}  {bar}")

    print("="*48)



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

# SELECT CONTACT HELPER

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


def select_contact(data, prompt="  Search contact: "):

    query = input(prompt).strip()

    if not query:

        return None

    results = search_contacts(data, query)

    if not results:

        return None

    if len(results) == 1:

        return results[0]

    try:

        idx = int(input("  Select number: ").strip()) - 1

        if 0 <= idx < len(results):

            return results[idx]

    except ValueError:

        pass

    return None



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

# MAIN MENU

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


def print_menu(data):

    total = len(data["contacts"])

    favs  = sum(1 for c in data["contacts"] if c.get("favourite"))

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

    print(f"  CONTACT BOOK  [{total} contacts  ★{favs} favourites]")

    print("-"*50)

    print("  1.  Add new contact")

    print("  2.  View all contacts")

    print("  3.  Search contact")

    print("  4.  View contact details")

    print("  5.  Edit contact")

    print("  6.  Delete contact")

    print("  7.  View favourites")

    print("  8.  Birthday reminders")

    print("  9.  Manage groups")

    print("  10. Export to CSV")

    print("  11. Import from CSV")

    print("  12. Statistics")

    print("  13. Backup contacts")

    print("  0.  Exit")

    print("-"*50)



def main():

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

    print("     CONTACT BOOK MANAGER")

    print("="*55)


    data = load_contacts()

    if data["contacts"]:

        print(f"\n  Loaded {len(data['contacts'])} contact(s) from {DATA_FILE}")

    else:

        print("\n  No contacts yet. Start by adding one!")


    while True:

        print_menu(data)

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


        if choice == "1":

            add_contact(data)


        elif choice == "2":

            print("\n  Sort by: 1=Name  2=Company  3=Group  4=Recent")

            sort_opt = input("  Sort (default Name): ").strip()

            sort_map = {"1": "name", "2": "company",

                        "3": "group", "4": "recent"}

            sort_by  = sort_map.get(sort_opt, "name")

            list_contacts(data, sort_by=sort_by)


        elif choice == "3":

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

            search_contacts(data, query)


        elif choice == "4":

            contact = select_contact(data)

            if contact:

                view_contact(contact)


        elif choice == "5":

            contact = select_contact(data)

            if contact:

                edit_contact(data, contact)


        elif choice == "6":

            contact = select_contact(data)

            if contact:

                view_contact(contact)

                delete_contact(data, contact)


        elif choice == "7":

            list_contacts(data, favourites_only=True)


        elif choice == "8":

            birthday_reminders(data)


        elif choice == "9":

            manage_groups(data)


        elif choice == "10":

            fname = input("  Output filename (Enter=auto): ").strip() or None

            export_csv(data, fname)


        elif choice == "11":

            fname = input("  CSV file path: ").strip()

            import_csv(data, fname)


        elif choice == "12":

            show_stats(data)


        elif choice == "13":

            backup_contacts(data)


        elif choice == "0":

            print("\n  Goodbye!\n")

            break


        else:

            print("  Invalid choice.")



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

# RUN

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


if __name__ == "__main__":

    main()

Pomodoro Timer

import os

import json

import time

import threading

from datetime import datetime, date, timedelta

from pathlib import Path

from collections import defaultdict


try:

    import matplotlib.pyplot as plt

    MATPLOTLIB_OK = True

except ImportError:

    MATPLOTLIB_OK = False


try:

    import winsound

    SOUND_ENGINE = "winsound"

except ImportError:

    try:

        import subprocess

        SOUND_ENGINE = "subprocess"

    except:

        SOUND_ENGINE = None


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

# CONFIGURATION

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


DATA_FILE = "pomodoro_data.json"


DEFAULT_CONFIG = {

    "work_minutes":       25,

    "short_break":        5,

    "long_break":         15,

    "sessions_before_long": 4,

    "auto_start_break":   False,

    "sound_enabled":      True,

    "daily_goal":         8,       # pomodoros per day

}


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

# HELPERS

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


def clear():

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



def fmt_time(seconds):

    m = seconds // 60

    s = seconds % 60

    return f"{m:02d}:{s:02d}"



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

    if max_val == 0:

        return ""

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

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



def beep(times=1):

    """Play a beep sound — cross platform."""

    for _ in range(times):

        if SOUND_ENGINE == "winsound":

            try:

                winsound.Beep(1000, 400)

            except:

                print("\a")

        elif SOUND_ENGINE == "subprocess":

            try:

                subprocess.run(["paplay", "/usr/share/sounds/freedesktop/stereo/complete.oga"],

                               capture_output=True)

            except:

                print("\a")

        else:

            print("\a")

        time.sleep(0.2)



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

# LOAD & SAVE DATA

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


def load_data():

    if Path(DATA_FILE).exists():

        try:

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

                return json.load(f)

        except:

            pass

    return {

        "config":   DEFAULT_CONFIG.copy(),

        "sessions": [],

        "tasks":    []

    }



def save_data(data):

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

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



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

# TIMER ENGINE

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


class PomodoroTimer:

    def __init__(self, data):

        self.data          = data

        self.config        = data.get("config", DEFAULT_CONFIG.copy())

        self.running       = False

        self.paused        = False

        self.current_type  = "work"      # work / short_break / long_break

        self.seconds_left  = 0

        self.session_count = 0           # completed work sessions today

        self.total_today   = self._count_today()

        self.current_task  = ""

        self._thread       = None

        self._pause_event  = threading.Event()

        self._pause_event.set()


    def _count_today(self):

        today = str(date.today())

        return sum(1 for s in self.data["sessions"]

                   if s.get("date") == today and s.get("type") == "work")


    def _duration(self, session_type):

        if session_type == "work":

            return self.config["work_minutes"] * 60

        elif session_type == "short_break":

            return self.config["short_break"] * 60

        else:

            return self.config["long_break"] * 60


    def start(self, session_type="work", task=""):

        if self.running:

            print("  Timer already running.")

            return


        self.current_type = session_type

        self.seconds_left = self._duration(session_type)

        self.current_task = task

        self.running      = True

        self.paused       = False

        self._pause_event.set()


        self._thread = threading.Thread(

            target=self._countdown, daemon=True

        )

        self._thread.start()


    def _countdown(self):

        start_time = datetime.now()


        while self.seconds_left > 0 and self.running:

            self._pause_event.wait()   # blocks if paused

            if not self.running:

                break

            self._display_timer()

            time.sleep(1)

            if not self.paused:

                self.seconds_left -= 1


        if self.running and self.seconds_left <= 0:

            self._on_complete(start_time)


        self.running = False


    def _display_timer(self):

        clear()

        icons = {"work": "WORK", "short_break": "SHORT BREAK", "long_break": "LONG BREAK"}

        total = self._duration(self.current_type)

        elapsed = total - self.seconds_left

        pct = elapsed / total * 100 if total else 0

        bar = draw_bar(elapsed, total, width=35)


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

        print(f"   POMODORO TIMER  —  {icons[self.current_type]}")

        print("="*55)

        print(f"\n   {fmt_time(self.seconds_left)}  remaining")

        print(f"\n   {bar}  {pct:.0f}%\n")


        if self.current_task:

            print(f"   Task: {self.current_task}")


        print(f"\n   Sessions today : {self.total_today + (1 if self.current_type == 'work' else 0)}"

              f" / {self.config['daily_goal']} goal")

        print(f"   Session #      : {self.session_count + 1}")


        status = "PAUSED" if self.paused else "RUNNING"

        print(f"\n   [{status}]  Ctrl+C or menu to stop")

        print("="*55)


    def _on_complete(self, start_time):

        end_time = datetime.now()


        if self.config.get("sound_enabled"):

            beep(3 if self.current_type == "work" else 1)


        # Log session

        session = {

            "type":       self.current_type,

            "date":       str(date.today()),

            "start":      start_time.strftime("%H:%M:%S"),

            "end":        end_time.strftime("%H:%M:%S"),

            "duration":   self._duration(self.current_type) // 60,

            "task":       self.current_task,

            "completed":  True,

        }

        self.data["sessions"].append(session)

        save_data(self.data)


        if self.current_type == "work":

            self.session_count += 1

            self.total_today   += 1


        clear()

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

        icon = "WORK SESSION COMPLETE!" if self.current_type == "work" else "BREAK OVER!"

        print(f"   {icon}")

        print("="*55)


        if self.current_type == "work":

            print(f"\n   Great work! Session {self.session_count} done.")

            if self.total_today >= self.config["daily_goal"]:

                print(f"   DAILY GOAL REACHED! {self.total_today} pomodoros today!")

            # Determine next break

            if self.session_count % self.config["sessions_before_long"] == 0:

                print(f"   Next: LONG BREAK ({self.config['long_break']} min)")

            else:

                print(f"   Next: SHORT BREAK ({self.config['short_break']} min)")

        else:

            print(f"\n   Break done. Ready for the next work session!")


        print()


    def pause(self):

        if not self.running:

            return

        self.paused = True

        self._pause_event.clear()

        print("  Timer paused.")


    def resume(self):

        if not self.running:

            return

        self.paused = False

        self._pause_event.set()

        print("  Timer resumed.")


    def stop(self):

        self.running = False

        self._pause_event.set()   # unblock if paused

        print("  Timer stopped.")


    def status(self):

        if not self.running:

            print("\n  No timer running.")

            return

        state = "PAUSED" if self.paused else "RUNNING"

        icons = {"work": "Work", "short_break": "Short Break",

                 "long_break": "Long Break"}

        print(f"\n  [{state}] {icons[self.current_type]}: "

              f"{fmt_time(self.seconds_left)} remaining")



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

# STATS & REPORTS

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


def stats_today(data):

    today    = str(date.today())

    sessions = [s for s in data["sessions"] if s.get("date") == today]

    work     = [s for s in sessions if s.get("type") == "work"]

    breaks   = [s for s in sessions if s.get("type") != "work"]

    goal     = data["config"].get("daily_goal", 8)


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

    print(f"  TODAY'S STATS  —  {today}")

    print("="*55)

    print(f"  Pomodoros    : {len(work)} / {goal}")

    bar = draw_bar(len(work), goal)

    print(f"  Progress     : {bar}")

    print(f"  Work time    : {len(work) * data['config']['work_minutes']} min")

    print(f"  Breaks       : {len(breaks)}")


    if work:

        tasks = [s["task"] for s in work if s.get("task")]

        if tasks:

            print(f"\n  Tasks worked on:")

            for t in set(tasks):

                count = tasks.count(t)

                print(f"    • {t}  ({count} pomodoro{'s' if count > 1 else ''})")


    if len(work) >= goal:

        print(f"\n  DAILY GOAL REACHED!")

    else:

        remaining = goal - len(work)

        print(f"\n  {remaining} more pomodoro(s) to reach daily goal.")


    print("="*55)



def stats_weekly(data):

    sessions = data["sessions"]

    today    = date.today()


    # Last 7 days

    daily = {}

    for i in range(6, -1, -1):

        d = str(today - timedelta(days=i))

        daily[d] = 0


    for s in sessions:

        d = s.get("date", "")

        if d in daily and s.get("type") == "work":

            daily[d] += 1


    max_count = max(daily.values()) if daily else 1

    goal = data["config"].get("daily_goal", 8)


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

    print("  WEEKLY STATS  (Last 7 Days)")

    print("="*55)


    total_week = 0

    for d, count in daily.items():

        try:

            label = datetime.strptime(d, "%Y-%m-%d").strftime("%a %d %b")

        except:

            label = d

        bar   = draw_bar(count, max(max_count, goal), width=25)

        goal_marker = " GOAL!" if count >= goal else ""

        print(f"  {label:<12} {count:>3} {bar}{goal_marker}")

        total_week += count


    avg = total_week / 7

    print("="*55)

    print(f"  Total this week : {total_week} pomodoros")

    print(f"  Daily average   : {avg:.1f}")

    print(f"  Daily goal      : {goal}")

    print("="*55)



def stats_all_time(data):

    sessions = data["sessions"]

    work     = [s for s in sessions if s.get("type") == "work"]


    if not work:

        print("\n  No sessions recorded yet.")

        return


    total_pomodoros = len(work)

    total_minutes   = sum(s.get("duration", 25) for s in work)

    total_hours     = total_minutes / 60


    # Most productive day

    day_counts = defaultdict(int)

    for s in work:

        day_counts[s.get("date", "")] += 1


    best_day   = max(day_counts, key=day_counts.get)

    best_count = day_counts[best_day]


    # Most worked task

    task_counts = defaultdict(int)

    for s in work:

        t = s.get("task", "")

        if t:

            task_counts[t] += 1


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

    print("  ALL-TIME STATS")

    print("="*55)

    print(f"  Total pomodoros : {total_pomodoros:,}")

    print(f"  Total work time : {total_hours:.1f} hours  ({total_minutes:,} min)")

    print(f"  Days tracked    : {len(day_counts)}")

    print(f"  Avg per day     : {total_pomodoros / max(len(day_counts), 1):.1f}")

    print(f"  Best day        : {best_day}  ({best_count} pomodoros)")


    if task_counts:

        top_task  = max(task_counts, key=task_counts.get)

        print(f"  Most worked on  : {top_task}  ({task_counts[top_task]} sessions)")


    print("="*55)



def recent_sessions(data, n=10):

    sessions = data["sessions"]

    if not sessions:

        print("\n  No sessions yet.")

        return


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

    print(f"  RECENT SESSIONS  (last {n})")

    print("="*65)

    print(f"  {'DATE':<12} {'TYPE':<14} {'START':<10} {'DUR':>5}  TASK")

    print("  " + "-"*60)


    for s in reversed(sessions[-n:]):

        stype = s.get("type", "").replace("_", " ").upper()

        dur   = f"{s.get('duration', '?')}m"

        task  = s.get("task", "")[:25]

        print(f"  {s.get('date',''):<12} {stype:<14} "

              f"{s.get('start',''):<10} {dur:>5}  {task}")


    print("="*65)



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

# MATPLOTLIB CHART

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


def plot_weekly_chart(data):

    if not MATPLOTLIB_OK:

        print("\n  matplotlib not installed.")

        print("  Install: pip install matplotlib")

        return


    sessions = data["sessions"]

    today    = date.today()

    daily    = {}


    for i in range(13, -1, -1):

        d = str(today - timedelta(days=i))

        daily[d] = 0


    for s in sessions:

        d = s.get("date", "")

        if d in daily and s.get("type") == "work":

            daily[d] += 1


    labels = []

    values = list(daily.values())

    for d in daily.keys():

        try:

            labels.append(datetime.strptime(d, "%Y-%m-%d").strftime("%d %b"))

        except:

            labels.append(d)


    goal   = data["config"].get("daily_goal", 8)

    colors = ["#2ecc71" if v >= goal else "#3498db" for v in values]


    fig, ax = plt.subplots(figsize=(13, 5))

    bars = ax.bar(labels, values, color=colors, alpha=0.85, width=0.6)


    # Goal line

    ax.axhline(y=goal, color="#e74c3c", linestyle="--",

               linewidth=1.5, label=f"Daily goal ({goal})")


    # Value labels on bars

    for bar, val in zip(bars, values):

        if val > 0:

            ax.text(bar.get_x() + bar.get_width() / 2,

                    bar.get_height() + 0.1,

                    str(val), ha="center", va="bottom", fontsize=9)


    ax.set_title("Pomodoro Sessions — Last 14 Days",

                 fontsize=13, fontweight="bold")

    ax.set_ylabel("Pomodoros")

    ax.set_xlabel("Date")

    ax.legend()

    ax.grid(axis="y", alpha=0.3)

    plt.xticks(rotation=45, ha="right")

    plt.tight_layout()


    from io import BytesIO

    buf = BytesIO()

    plt.savefig("pomodoro_chart.png", dpi=120, bbox_inches="tight")

    print("\n  Chart saved: pomodoro_chart.png")

    plt.show()



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

# SETTINGS

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


def edit_settings(data):

    cfg = data["config"]

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

    print("  SETTINGS")

    print("="*50)

    print(f"  1. Work duration    : {cfg['work_minutes']} min")

    print(f"  2. Short break      : {cfg['short_break']} min")

    print(f"  3. Long break       : {cfg['long_break']} min")

    print(f"  4. Sessions before long break: {cfg['sessions_before_long']}")

    print(f"  5. Daily goal       : {cfg['daily_goal']} pomodoros")

    print(f"  6. Sound enabled    : {cfg['sound_enabled']}")

    print(f"  7. Save & return")

    print("="*50)


    while True:

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

        if sub == "1":

            v = input(f"  Work minutes [{cfg['work_minutes']}]: ").strip()

            if v.isdigit(): cfg["work_minutes"] = int(v)

        elif sub == "2":

            v = input(f"  Short break [{cfg['short_break']}]: ").strip()

            if v.isdigit(): cfg["short_break"] = int(v)

        elif sub == "3":

            v = input(f"  Long break [{cfg['long_break']}]: ").strip()

            if v.isdigit(): cfg["long_break"] = int(v)

        elif sub == "4":

            v = input(f"  Sessions before long [{cfg['sessions_before_long']}]: ").strip()

            if v.isdigit(): cfg["sessions_before_long"] = int(v)

        elif sub == "5":

            v = input(f"  Daily goal [{cfg['daily_goal']}]: ").strip()

            if v.isdigit(): cfg["daily_goal"] = int(v)

        elif sub == "6":

            cfg["sound_enabled"] = not cfg["sound_enabled"]

            print(f"  Sound: {cfg['sound_enabled']}")

        elif sub == "7":

            data["config"] = cfg

            save_data(data)

            print("  Settings saved.")

            break

        else:

            print("  Invalid option.")



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

# MAIN MENU

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


def print_menu(timer, data):

    cfg    = data["config"]

    today  = str(date.today())

    today_count = sum(1 for s in data["sessions"]

                      if s.get("date") == today and s.get("type") == "work")

    goal   = cfg["daily_goal"]

    status = "RUNNING" if timer.running else "IDLE"


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

    print(f"  POMODORO TIMER  [{status}]  "

          f"Today: {today_count}/{goal}")

    print("-"*50)

    print(f"  Work: {cfg['work_minutes']}m  |  "

          f"Short: {cfg['short_break']}m  |  "

          f"Long: {cfg['long_break']}m")

    print("-"*50)

    print("  1. Start WORK session")

    print("  2. Start SHORT BREAK")

    print("  3. Start LONG BREAK")

    print("  4. Pause / Resume timer")

    print("  5. Stop timer")

    print("  6. Today's stats")

    print("  7. Weekly stats")

    print("  8. All-time stats")

    print("  9. Recent sessions")

    print("  10. Plot chart (14-day)")

    print("  11. Settings")

    print("  0. Exit")

    print("-"*50)



def main():

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

    print("     POMODORO TIMER WITH STATS")

    print("="*55)

    print("\n  The Pomodoro Technique:")

    print("  Work 25 min → Short break 5 min → Repeat x4 → Long break 15 min")


    data  = load_data()

    timer = PomodoroTimer(data)


    today = str(date.today())

    today_count = sum(1 for s in data["sessions"]

                      if s.get("date") == today and s.get("type") == "work")

    print(f"\n  Today so far: {today_count} pomodoro(s)")


    while True:

        print_menu(timer, data)

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


        if choice == "1":

            task = input("  What are you working on? (Enter to skip): ").strip()

            if timer.running:

                print("  Stop current timer first (option 5).")

            else:

                timer.start("work", task)

                print(f"\n  Work session started! ({data['config']['work_minutes']} min)")

                print("  Return to menu anytime — timer runs in background.")


        elif choice == "2":

            if timer.running:

                print("  Stop current timer first.")

            else:

                timer.start("short_break")

                print(f"\n  Short break started! ({data['config']['short_break']} min)")


        elif choice == "3":

            if timer.running:

                print("  Stop current timer first.")

            else:

                timer.start("long_break")

                print(f"\n  Long break started! ({data['config']['long_break']} min)")


        elif choice == "4":

            if timer.paused:

                timer.resume()

            else:

                timer.pause()


        elif choice == "5":

            timer.stop()


        elif choice == "6":

            stats_today(data)


        elif choice == "7":

            stats_weekly(data)


        elif choice == "8":

            stats_all_time(data)


        elif choice == "9":

            n = input("  Show last N sessions (default 10): ").strip()

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

            recent_sessions(data, n)


        elif choice == "10":

            plot_weekly_chart(data)


        elif choice == "11":

            edit_settings(data)

            timer.config = data["config"]


        elif choice == "0":

            timer.stop()

            print("\n  Goodbye! Keep up the great work!\n")

            break


        else:

            print("  Invalid choice.")



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

# RUN

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


if __name__ == "__main__":

    main()