import math try: import tkinter as tk from tkinter import messagebox from tkinter import ttk TK_AVAILABLE = True TK_IMPORT_ERROR = None except Exception as exc: tk = None messagebox = None ttk = None TK_AVAILABLE = False TK_IMPORT_ERROR = exc # ========================= # EDIT VALUES HERE (CONFIG) # ========================= KELP_PRICE = 61 # per kelp item BLAZE_ROD_PRICE = 150 # per blaze rod DRIED_KELP_PRICE = 83.18 # per dried kelp item DRIED_KELP_BLOCK_PRICE = 750.5 # per dried kelp block BUY_KELP = True # True = buy kelp, False = use owned kelp SMOKERS = 64 # amount of smokers KELP_AMOUNT = "1s" # amount of kelp to process (items) # ========================= # GAME CONSTANTS (DON'T EDIT) # ========================= SECONDS_PER_ITEM_SMOKER = 5 ITEMS_PER_BLAZE_ROD = 12 DRIED_KELP_PER_BLOCK = 9 NUMBER_SUFFIXES = { "": 1, "k": 1_000, "m": 1_000_000, "b": 1_000_000_000, } def money(x: float) -> str: return f"{x:,.2f}" def format_duration(total_seconds: float) -> str: """Format seconds as a compact duration like 2d 3h 4m 5s.""" seconds = max(0, int(round(total_seconds))) days, rem = divmod(seconds, 86_400) hours, rem = divmod(rem, 3_600) minutes, secs = divmod(rem, 60) parts = [] if days: parts.append(f"{days}d") if hours: parts.append(f"{hours}h") if minutes: parts.append(f"{minutes}m") if secs or not parts: parts.append(f"{secs}s") return " ".join(parts) def parse_number_with_suffix(value) -> float: """Parse values like 1200, 1.2k, 3m, 4B (case-insensitive).""" if isinstance(value, (int, float)): return float(value) text = str(value).strip() if not text: raise ValueError("Value cannot be empty") suffix = "" if text[-1].isalpha(): suffix = text[-1].lower() text = text[:-1].strip() if suffix not in NUMBER_SUFFIXES: raise ValueError("Invalid suffix. Use k, m, or b") try: base_value = float(text) except ValueError as exc: raise ValueError(f"Invalid numeric value: {value}") from exc return base_value * NUMBER_SUFFIXES[suffix] def parse_int_with_suffix(value, field_name: str) -> int: parsed = parse_number_with_suffix(value) result = int(round(parsed)) if result < 0: raise ValueError(f"{field_name} must be >= 0") return result def parse_kelp_amount(value) -> int: """Parse kelp amount as items; supports k/m/b and shulker forms s/ks/ms/bs.""" text = str(value).strip() if not text: raise ValueError("KELP_AMOUNT cannot be empty") lower_text = text.lower() shulker_multipliers = [ ("bs", 1_000_000_000), ("ms", 1_000_000), ("ks", 1_000), ("s", 1), ] amount = None for suffix, shulker_scale in shulker_multipliers: if lower_text.endswith(suffix): number_part = text[:-len(suffix)].strip() if not number_part: raise ValueError("Invalid shulker amount format") try: shulker_count = float(number_part) except ValueError as exc: raise ValueError("Invalid shulker amount format") from exc amount = int(round(shulker_count * shulker_scale * 1728)) break if amount is None: amount = parse_int_with_suffix(text, "KELP_AMOUNT") if amount < 0: raise ValueError("KELP_AMOUNT must be >= 0") return amount def calculate( kelp_price: float, blaze_rod_price: float, dried_kelp_price: float, dried_kelp_block_price: float, smokers: int, kelp_amount: int, buy_kelp: bool, ) -> dict: if smokers <= 0: raise ValueError("SMOKERS must be >= 1") if kelp_amount < 0: raise ValueError("KELP_AMOUNT must be >= 0") dried_kelp_out = kelp_amount # 1 kelp -> 1 dried kelp # Fuel usage (rounded up to whole rods) blaze_rods_used = math.ceil(kelp_amount / ITEMS_PER_BLAZE_ROD) fuel_cost = blaze_rods_used * blaze_rod_price # Time total_seconds = (kelp_amount * SECONDS_PER_ITEM_SMOKER) / smokers total_minutes = total_seconds / 60 total_hours = total_minutes / 60 # --- Scenario 1: Smelt and sell dried kelp --- kelp_market_value = kelp_amount * kelp_price # In owned mode we still use kelp market value as opportunity cost, # but label it as raw kelp profit in the UI. kelp_cost = kelp_market_value kelp_value_label = "Kelp Cost" if buy_kelp else "Raw Kelp Profit" cost_smelt_only = kelp_cost + fuel_cost revenue_smelt_only = dried_kelp_out * dried_kelp_price profit_smelt_only = revenue_smelt_only - cost_smelt_only # Blocks and leftovers from the smelt output blocks_from_amount = dried_kelp_out // DRIED_KELP_PER_BLOCK leftover_dried = dried_kelp_out % DRIED_KELP_PER_BLOCK # --- Scenario 2: Craft blocks only (buy dried kelp for blocks) --- cost_craft_only = (blocks_from_amount * DRIED_KELP_PER_BLOCK) * dried_kelp_price revenue_craft_only = blocks_from_amount * dried_kelp_block_price profit_craft_only = revenue_craft_only - cost_craft_only # --- Scenario 3: Smelt then craft, sell blocks + leftover dried kelp --- cost_smelt_then_craft = kelp_cost + fuel_cost revenue_smelt_then_craft = (blocks_from_amount * dried_kelp_block_price) + (leftover_dried * dried_kelp_price) profit_smelt_then_craft = revenue_smelt_then_craft - cost_smelt_then_craft print("=== Kelp Smelting / Block Craft Profit Calculator ===") print("\n=== Config ===") print(f"KELP_PRICE: {kelp_price}") print(f"BLAZE_ROD_PRICE: {blaze_rod_price}") print(f"DRIED_KELP_PRICE: {dried_kelp_price}") print(f"DRIED_KELP_BLOCK_PRICE: {dried_kelp_block_price}") print(f"BUY_KELP: {buy_kelp}") print(f"SMOKERS: {smokers}") print(f"KELP_AMOUNT: {kelp_amount}") print("\n=== Results ===") print(f"Kelp processed: {kelp_amount}") print(f"Dried kelp produced: {dried_kelp_out}") print(f"Blocks craftable: {blocks_from_amount} (leftover dried kelp: {leftover_dried})") print("\n--- Smelting logistics ---") print(f"Blaze rods used: {blaze_rods_used} (1 rod smelts {ITEMS_PER_BLAZE_ROD} items)") print(f"Fuel cost: {money(fuel_cost)}") print(f"Time to smelt all: {total_seconds:,.0f} s ({total_minutes:,.2f} min, {total_hours:,.2f} hr)") print("\n--- Profit calculations ---") print(f"Profit (smelt -> sell dried kelp): {money(profit_smelt_only)}") print(f"Profit (craft blocks only): {money(profit_craft_only)}") print(f"Profit (smelt -> craft -> sell blocks + leftover dried): {money(profit_smelt_then_craft)}") print("\n--- Useful unit breakdowns ---") avg_fuel_cost_per_item = blaze_rod_price / ITEMS_PER_BLAZE_ROD smelt_profit_per_kelp = (dried_kelp_price - kelp_price) - avg_fuel_cost_per_item craft_profit_per_block = dried_kelp_block_price - (DRIED_KELP_PER_BLOCK * dried_kelp_price) return { "kelp_price": kelp_price, "blaze_rod_price": blaze_rod_price, "dried_kelp_price": dried_kelp_price, "dried_kelp_block_price": dried_kelp_block_price, "buy_kelp": buy_kelp, "smokers": smokers, "kelp_amount": kelp_amount, "dried_kelp_out": dried_kelp_out, "blocks_from_amount": blocks_from_amount, "leftover_dried": leftover_dried, "blaze_rods_used": blaze_rods_used, "kelp_market_value": kelp_market_value, "kelp_value_label": kelp_value_label, "kelp_cost": kelp_cost, "fuel_cost": fuel_cost, "total_seconds": total_seconds, "total_minutes": total_minutes, "total_hours": total_hours, "profit_smelt_only": profit_smelt_only, "profit_craft_only": profit_craft_only, "profit_smelt_then_craft": profit_smelt_then_craft, "avg_fuel_cost_per_item": avg_fuel_cost_per_item, "smelt_profit_per_kelp": smelt_profit_per_kelp, "craft_profit_per_block": craft_profit_per_block, } def format_results(data: dict) -> str: return ( "=== Kelp Smelting / Block Craft Profit Calculator ===\n\n" "=== Config ===\n" f"KELP_PRICE: {data['kelp_price']}\n" f"BLAZE_ROD_PRICE: {data['blaze_rod_price']}\n" f"DRIED_KELP_PRICE: {data['dried_kelp_price']}\n" f"DRIED_KELP_BLOCK_PRICE: {data['dried_kelp_block_price']}\n" f"BUY_KELP: {data['buy_kelp']}\n" f"SMOKERS: {data['smokers']}\n" f"KELP_AMOUNT: {data['kelp_amount']}\n\n" "=== Results ===\n" f"Kelp processed: {data['kelp_amount']}\n" f"Dried kelp produced: {data['dried_kelp_out']}\n" f"Blocks craftable: {data['blocks_from_amount']} " f"(leftover dried kelp: {data['leftover_dried']})\n\n" "--- Smelting logistics ---\n" f"Blaze rods used: {data['blaze_rods_used']} " f"(1 rod smelts {ITEMS_PER_BLAZE_ROD} items)\n" f"Fuel cost: {money(data['fuel_cost'])}\n" f"Time to smelt all: {format_duration(data['total_seconds'])} " f"({data['total_seconds']:,.0f} s, {data['total_minutes']:,.2f} min, {data['total_hours']:,.2f} hr)\n\n" "--- Profit calculations ---\n" f"Profit (smelt -> sell dried kelp): {money(data['profit_smelt_only'])}\n" f"Profit (craft blocks only): {money(data['profit_craft_only'])}\n" f"Profit (smelt -> craft -> sell blocks + leftover dried): " f"{money(data['profit_smelt_then_craft'])}\n\n" "--- Useful unit breakdowns ---\n" f"Avg fuel cost per smelted item: {money(data['avg_fuel_cost_per_item'])}\n" "Estimated profit per kelp smelted (avg fuel): " f"{money(data['smelt_profit_per_kelp'])}\n" f"Profit per dried kelp block crafted: {money(data['craft_profit_per_block'])}" ) def main(): data = calculate( kelp_price=parse_number_with_suffix(KELP_PRICE), blaze_rod_price=parse_number_with_suffix(BLAZE_ROD_PRICE), dried_kelp_price=parse_number_with_suffix(DRIED_KELP_PRICE), dried_kelp_block_price=parse_number_with_suffix(DRIED_KELP_BLOCK_PRICE), smokers=parse_int_with_suffix(SMOKERS, "SMOKERS"), kelp_amount=parse_kelp_amount(KELP_AMOUNT), buy_kelp=BUY_KELP, ) print(format_results(data)) def launch_ui(): if not TK_AVAILABLE: raise RuntimeError(f"Tkinter is unavailable: {TK_IMPORT_ERROR}") root = tk.Tk() root.title("Kelp Profit Calculator") root.geometry("980x700") root.minsize(900, 620) root.configure(bg="#000000") style = ttk.Style(root) try: style.theme_use("clam") except tk.TclError: pass style.configure("App.TFrame", background="#000000") style.configure("Card.TFrame", background="#111214", relief="flat") style.configure("Header.TLabel", background="#000000", foreground="#f2f3f5", font=("Segoe UI", 21, "bold")) style.configure("SubHeader.TLabel", background="#000000", foreground="#b5bac1", font=("Segoe UI", 10)) style.configure("FieldLabel.TLabel", background="#111214", foreground="#dbdee1", font=("Segoe UI", 10, "bold")) style.configure("KpiLabel.TLabel", background="#111214", foreground="#949ba4", font=("Segoe UI", 9, "bold")) style.configure("KpiValue.TLabel", background="#111214", foreground="#f2f3f5", font=("Segoe UI", 13, "bold")) style.configure("KpiValueBest.TLabel", background="#111214", foreground="#57f287", font=("Segoe UI", 13, "bold")) style.configure("KpiValueProfit.TLabel", background="#111214", foreground="#f2f3f5", font=("Segoe UI", 13, "bold")) style.configure("KpiValueLoss.TLabel", background="#111214", foreground="#ed4245", font=("Segoe UI", 13, "bold")) style.configure("Accent.TButton", font=("Segoe UI", 10, "bold"), padding=8) style.map( "Accent.TButton", background=[("active", "#4752c4"), ("!disabled", "#5865f2")], foreground=[("!disabled", "#ffffff")], ) style.configure( "TEntry", fieldbackground="#1e1f22", foreground="#f2f3f5", bordercolor="#2b2d31", insertcolor="#f2f3f5", ) style.configure("SourceOn.TButton", font=("Segoe UI", 9, "bold"), padding=7) style.map( "SourceOn.TButton", background=[("active", "#4752c4"), ("!disabled", "#5865f2")], foreground=[("!disabled", "#ffffff")], ) style.configure("SourceOff.TButton", font=("Segoe UI", 9, "bold"), padding=7) style.map( "SourceOff.TButton", background=[("active", "#3a3d42"), ("!disabled", "#2b2d31")], foreground=[("!disabled", "#dbdee1")], ) style.configure("Dark.Vertical.TScrollbar", background="#2b2d31", troughcolor="#1e1f22") app = ttk.Frame(root, style="App.TFrame", padding=(22, 18, 22, 18)) app.pack(fill="both", expand=True) app.grid_columnconfigure(0, weight=1) app.grid_columnconfigure(1, weight=1) app.grid_rowconfigure(2, weight=1) ttk.Label(app, text="Kelp Profit Dashboard", style="Header.TLabel").grid( row=0, column=0, columnspan=2, sticky="w" ) ttk.Label( app, text="Smelting, crafting, and profit snapshots in one place", style="SubHeader.TLabel", ).grid(row=1, column=0, columnspan=2, sticky="w", pady=(0, 14)) input_card = ttk.Frame(app, style="Card.TFrame", padding=(16, 16, 16, 16)) input_card.grid(row=2, column=0, sticky="nsew", padx=(0, 10)) input_card.grid_columnconfigure(0, weight=1) input_card.grid_columnconfigure(1, weight=1) results_card = ttk.Frame(app, style="Card.TFrame", padding=(16, 16, 16, 16)) results_card.grid(row=2, column=1, sticky="nsew", padx=(10, 0)) results_card.grid_columnconfigure(0, weight=1) results_card.grid_rowconfigure(4, weight=1) fields = [ ("Kelp Price (k/m/b)", str(KELP_PRICE)), ("Blaze Rod Price (k/m/b)", str(BLAZE_ROD_PRICE)), ("Dried Kelp Price (k/m/b)", str(DRIED_KELP_PRICE)), ("Dried Kelp Block Price (k/m/b)", str(DRIED_KELP_BLOCK_PRICE)), ("Smokers (k/m/b)", str(SMOKERS)), ("Kelp Amount (items k/m/b or s/ks/ms/bs)", str(KELP_AMOUNT)), ] entries = {} buy_kelp_var = tk.BooleanVar(value=BUY_KELP) ttk.Label(input_card, text="Inputs", style="FieldLabel.TLabel").grid( row=0, column=0, columnspan=2, sticky="w", pady=(0, 10) ) for row, (label_text, default_value) in enumerate(fields): label = ttk.Label(input_card, text=label_text, style="FieldLabel.TLabel", anchor="w") label.grid(row=row + 1, column=0, sticky="w", padx=(0, 8), pady=6) entry = ttk.Entry(input_card, width=24) entry.insert(0, default_value) entry.grid(row=row + 1, column=1, sticky="ew", pady=6) entries[label_text] = entry source_row = ttk.Frame(input_card, style="Card.TFrame") source_row.grid(row=len(fields) + 1, column=0, columnspan=2, sticky="ew", pady=(10, 2)) source_row.grid_columnconfigure(1, weight=1) ttk.Label(source_row, text="Kelp Source", style="FieldLabel.TLabel").grid(row=0, column=0, sticky="w", padx=(0, 8)) source_toggle_btn = ttk.Button(source_row) source_toggle_btn.grid(row=0, column=1, sticky="ew") def refresh_source_toggle(): if buy_kelp_var.get(): source_toggle_btn.configure(text="Buying Kelp (click to use owned)", style="SourceOn.TButton") else: source_toggle_btn.configure(text="Using Owned Kelp (click to buy)", style="SourceOff.TButton") def toggle_kelp_source(): buy_kelp_var.set(not buy_kelp_var.get()) refresh_source_toggle() on_calculate() source_toggle_btn.configure(command=toggle_kelp_source) refresh_source_toggle() kpi_row = ttk.Frame(results_card, style="Card.TFrame") kpi_row.grid(row=0, column=0, sticky="ew") kpi_row.grid_columnconfigure(0, weight=1) kpi_row.grid_columnconfigure(1, weight=1) kpi_row.grid_columnconfigure(2, weight=1) kpi_1 = ttk.Frame(kpi_row, style="Card.TFrame", padding=(10, 8, 10, 8)) kpi_1.grid(row=0, column=0, sticky="nsew", padx=(0, 6)) kpi_2 = ttk.Frame(kpi_row, style="Card.TFrame", padding=(10, 8, 10, 8)) kpi_2.grid(row=0, column=1, sticky="nsew", padx=3) kpi_3 = ttk.Frame(kpi_row, style="Card.TFrame", padding=(10, 8, 10, 8)) kpi_3.grid(row=0, column=2, sticky="nsew", padx=(6, 0)) ttk.Label(kpi_1, text="Smelt Profit", style="KpiLabel.TLabel").pack(anchor="w") kpi_1_value = ttk.Label(kpi_1, text="0.00", style="KpiValue.TLabel") kpi_1_value.pack(anchor="w") ttk.Label(kpi_2, text="Craft Profit", style="KpiLabel.TLabel").pack(anchor="w") kpi_2_value = ttk.Label(kpi_2, text="0.00", style="KpiValue.TLabel") kpi_2_value.pack(anchor="w") ttk.Label(kpi_3, text="Hybrid Profit", style="KpiLabel.TLabel").pack(anchor="w") kpi_3_value = ttk.Label(kpi_3, text="0.00", style="KpiValue.TLabel") kpi_3_value.pack(anchor="w") metrics_row = ttk.Frame(results_card, style="Card.TFrame") metrics_row.grid(row=1, column=0, sticky="ew", pady=(10, 0)) metrics_row.grid_columnconfigure(0, weight=1) metrics_row.grid_columnconfigure(1, weight=1) metrics_row.grid_columnconfigure(2, weight=1) smelt_card = ttk.Frame(metrics_row, style="Card.TFrame", padding=(10, 8, 10, 8)) smelt_card.grid(row=0, column=0, sticky="nsew", padx=(0, 6)) fuel_card = ttk.Frame(metrics_row, style="Card.TFrame", padding=(10, 8, 10, 8)) fuel_card.grid(row=0, column=1, sticky="nsew", padx=3) kelp_card = ttk.Frame(metrics_row, style="Card.TFrame", padding=(10, 8, 10, 8)) kelp_card.grid(row=0, column=2, sticky="nsew", padx=(6, 0)) ttk.Label(smelt_card, text="Smelt Time", style="KpiLabel.TLabel").pack(anchor="w") smelt_time_value = ttk.Label(smelt_card, text="0m", style="KpiValueProfit.TLabel") smelt_time_value.pack(anchor="w") ttk.Label(fuel_card, text="Fuel Cost", style="KpiLabel.TLabel").pack(anchor="w") fuel_cost_value = ttk.Label(fuel_card, text="0.00", style="KpiValueProfit.TLabel") fuel_cost_value.pack(anchor="w") kelp_cost_label = ttk.Label(kelp_card, text="Kelp Cost", style="KpiLabel.TLabel") kelp_cost_label.pack(anchor="w") kelp_cost_value = ttk.Label(kelp_card, text="0.00", style="KpiValueProfit.TLabel") kelp_cost_value.pack(anchor="w") ttk.Separator(results_card, orient="horizontal").grid(row=2, column=0, sticky="ew", pady=(12, 10)) ttk.Label(results_card, text="Full Breakdown", style="FieldLabel.TLabel").grid(row=3, column=0, sticky="w", pady=(0, 8)) text_wrap = ttk.Frame(results_card, style="Card.TFrame") text_wrap.grid(row=4, column=0, sticky="nsew") text_wrap.grid_columnconfigure(0, weight=1) text_wrap.grid_rowconfigure(0, weight=1) result_text = tk.Text( text_wrap, wrap="word", height=24, bg="#1e1f22", fg="#f2f3f5", insertbackground="#f2f3f5", relief="flat", padx=10, pady=10, font=("Consolas", 10), ) result_text.grid(row=0, column=0, sticky="nsew") scrollbar = ttk.Scrollbar(text_wrap, orient="vertical", command=result_text.yview, style="Dark.Vertical.TScrollbar") scrollbar.grid(row=0, column=1, sticky="ns") result_text.configure(yscrollcommand=scrollbar.set) result_text.tag_configure("section", foreground="#f2f3f5", font=("Consolas", 10, "bold")) result_text.tag_configure("profit_best", foreground="#57f287", font=("Consolas", 10, "bold")) result_text.tag_configure("profit_other", foreground="#f2f3f5", font=("Consolas", 10, "bold")) result_text.tag_configure("loss", foreground="#ed4245", font=("Consolas", 10, "bold")) button_row = ttk.Frame(input_card, style="Card.TFrame") button_row.grid(row=len(fields) + 2, column=0, columnspan=2, sticky="ew", pady=(14, 2)) button_row.grid_columnconfigure(0, weight=1) calculate_btn = ttk.Button(button_row, text="Calculate", style="Accent.TButton") calculate_btn.grid(row=0, column=0, sticky="ew") def on_calculate(): try: data = calculate( kelp_price=parse_number_with_suffix(entries["Kelp Price (k/m/b)"].get()), blaze_rod_price=parse_number_with_suffix(entries["Blaze Rod Price (k/m/b)"].get()), dried_kelp_price=parse_number_with_suffix(entries["Dried Kelp Price (k/m/b)"].get()), dried_kelp_block_price=parse_number_with_suffix(entries["Dried Kelp Block Price (k/m/b)"].get()), smokers=parse_int_with_suffix(entries["Smokers (k/m/b)"].get(), "SMOKERS"), kelp_amount=parse_kelp_amount(entries["Kelp Amount (items k/m/b or s/ks/ms/bs)"].get()), buy_kelp=buy_kelp_var.get(), ) except ValueError as exc: messagebox.showerror("Invalid input", str(exc)) return kpi_1_value.configure(text=money(data["profit_smelt_only"])) kpi_2_value.configure(text=money(data["profit_craft_only"])) kpi_3_value.configure(text=money(data["profit_smelt_then_craft"])) profits = { "smelt": data["profit_smelt_only"], "craft": data["profit_craft_only"], "hybrid": data["profit_smelt_then_craft"], } max_profit = max(profits.values()) def kpi_style(value: float) -> str: if value < 0: return "KpiValueLoss.TLabel" if value == max_profit: return "KpiValueBest.TLabel" return "KpiValueProfit.TLabel" kpi_1_value.configure(style=kpi_style(profits["smelt"])) kpi_2_value.configure(style=kpi_style(profits["craft"])) kpi_3_value.configure(style=kpi_style(profits["hybrid"])) smelt_time_value.configure(text=format_duration(data["total_seconds"])) fuel_cost_value.configure(text=money(data["fuel_cost"])) kelp_cost_label.configure(text=data["kelp_value_label"]) kelp_cost_value.configure(text=money(data["kelp_cost"])) result_text.delete("1.0", tk.END) result_text.insert(tk.END, format_results(data)) for heading in ["===", "---"]: start = "1.0" while True: idx = result_text.search(heading, start, tk.END) if not idx: break line_end = result_text.index(f"{idx} lineend") result_text.tag_add("section", idx, line_end) start = line_end result_text.tag_remove("profit_best", "1.0", tk.END) result_text.tag_remove("profit_other", "1.0", tk.END) result_text.tag_remove("loss", "1.0", tk.END) line_rules = [ ("Profit (smelt -> sell dried kelp)", profits["smelt"]), ("Profit (craft blocks only)", profits["craft"]), ("Profit (smelt -> craft -> sell blocks + leftover dried)", profits["hybrid"]), ] for label, value in line_rules: idx = result_text.search(label, "1.0", tk.END) if not idx: continue line_end = result_text.index(f"{idx} lineend") if value < 0: result_text.tag_add("loss", idx, line_end) elif value == max_profit: result_text.tag_add("profit_best", idx, line_end) else: result_text.tag_add("profit_other", idx, line_end) calculate_btn.configure(command=on_calculate) on_calculate() root.mainloop() if __name__ == "__main__": try: if TK_AVAILABLE: try: launch_ui() except tk.TclError: main() else: print(f"UI unavailable ({TK_IMPORT_ERROR}). Falling back to CLI output.\n") main() except KeyboardInterrupt: print("\nInterrupted by user. Exiting.")