""" Currency Converter — customtkinter edition Rounded corners, dark/light mode toggle. Dependencies: pip install requests pyperclip customtkinter """ import customtkinter as ctk import threading import time import re import platform try: import requests HAS_REQUESTS = True except ImportError: HAS_REQUESTS = False try: import pyperclip HAS_PYPERCLIP = True except ImportError: HAS_PYPERCLIP = False IS_MAC = platform.system() == "Darwin" ctk.set_appearance_mode("dark") ctk.set_default_color_theme("blue") FONT = "SF Pro Text" if IS_MAC else "Helvetica" FONT_D = "SF Pro Display" if IS_MAC else "Helvetica" ACCENT = "#0A84FF" GREEN = "#30D158" # Dynamic colors per mode — (dark, light) def T(dark, light): """Return a tuple customtkinter uses for dark/light.""" return (dark, light) # Color pairs C_BG = T("#1C1C1E", "#F2F2F7") C_PANEL = T("#2C2C2E", "#FFFFFF") C_PILL = T("#3A3A3C", "#E5E5EA") C_PILLH = T("#555558", "#D1D1D6") C_WHITE = T("#FFFFFF", "#000000") C_GREY = T("#8E8E93", "#6C6C70") C_GREY2 = T("#636366", "#8E8E93") C_SEP = T("#38383A", "#C6C6C8") FALLBACK_RATES = { "USD": 1.0, "VND": 25450.0, "EUR": 0.92, "GBP": 0.79, "JPY": 149.5, "KRW": 1340.0, "CNY": 7.24, "SGD": 1.35, "THB": 35.5, "MYR": 4.72, "AUD": 1.54, "CAD": 1.37, "CHF": 0.90, "HKD": 7.82, "INR": 83.2, "NZD": 1.65, "SEK": 10.5, "NOK": 10.8, "DKK": 6.89, "BRL": 4.97, "MXN": 17.2, "ZAR": 18.6, "TRY": 32.1, "RUB": 90.5, "AED": 3.67, "SAR": 3.75, "ILS": 3.72, "PLN": 3.98, "CZK": 23.1, "HUF": 358.0, "RON": 4.59, "IDR": 15750.0, "PHP": 56.8, "PKR": 278.0, "BDT": 110.0, "NGN": 1550.0, } CURRENCIES = sorted(FALLBACK_RATES.keys()) SYMBOLS = { "USD":"$", "VND":"₫", "EUR":"€", "GBP":"£", "JPY":"¥", "KRW":"₩", "CNY":"¥", "SGD":"S$", "THB":"฿", "MYR":"RM", "AUD":"A$", "CAD":"C$", "CHF":"Fr", "HKD":"HK$","INR":"₹", "NZD":"NZ$","SEK":"kr", "NOK":"kr", "DKK":"kr", "BRL":"R$", "MXN":"$", "ZAR":"R", "TRY":"₺", "RUB":"₽", "AED":"د.إ", "SAR":"﷼", "ILS":"₪", "PLN":"zł", "CZK":"Kč", "HUF":"Ft", "RON":"lei","IDR":"Rp", "PHP":"₱", "PKR":"₨", "BDT":"৳", "NGN":"₦", } FALLBACK_RATES class RateCache: def __init__(self): self.rates = dict(FALLBACK_RATES) self._lock = threading.Lock() def fetch(self, cb=None): if not HAS_REQUESTS: if cb: cb(False); return try: r = requests.get("https://open.er-api.com/v6/latest/USD", timeout=8) d = r.json() if d.get("result") == "success": with self._lock: self.rates = d["rates"] if cb: cb(True) else: if cb: cb(False) except Exception: if cb: cb(False) def convert(self, amt, frm, to): with self._lock: r = self.rates return amt / r[frm] * r[to] def rate_str(self, frm, to): with self._lock: r = self.rates v = r[to] / r[frm] return f"1 {frm} = {v:,.4f} {to}" if v >= 1 else f"1 {frm} = {v:.6f} {to}" # ── App ─────────────────────────────────────────────────────────────────────── class App(ctk.CTk): def __init__(self): super().__init__() self.cache = RateCache() self._result_str = "" self._build_window() self._build_ui() threading.Thread(target=self.cache.fetch, kwargs={"cb": self._on_rates}, daemon=True).start() self._live_convert() def _build_window(self): self.title("Currency") self.configure(fg_color=C_BG) self.resizable(False, False) W, H = 320, 420 sw, sh = self.winfo_screenwidth(), self.winfo_screenheight() self.geometry(f"{W}x{H}+{(sw-W)//2}+{(sh-H)//2}") def _build_ui(self): self.from_var = ctk.StringVar(value="USD") self.to_var = ctk.StringVar(value="VND") # ── Currency selector row ───────────────────────────────────────── top = ctk.CTkFrame(self, fg_color=C_BG) top.pack(fill="x", padx=16, pady=(16, 0)) top.columnconfigure(0, weight=1) top.columnconfigure(2, weight=1) # FROM dropdown self._from_dd = ctk.CTkOptionMenu( top, variable=self.from_var, values=CURRENCIES, width=110, height=32, corner_radius=8, fg_color=C_PILL, button_color=C_PILL, button_hover_color=C_PILLH, text_color=C_WHITE, dropdown_fg_color=C_PANEL, dropdown_hover_color=C_PILL, font=(FONT, 13, "bold"), command=lambda _: self._on_from_change()) self._from_dd.grid(row=0, column=0, sticky="ew") # Swap button swap_btn = ctk.CTkButton( top, text="⇄", width=36, height=32, corner_radius=8, fg_color=C_PILL, hover_color=C_PILLH, text_color=C_GREY, font=(FONT, 14, "bold"), command=self._swap) swap_btn.grid(row=0, column=1, padx=8) # TO dropdown self._to_dd = ctk.CTkOptionMenu( top, variable=self.to_var, values=CURRENCIES, width=110, height=32, corner_radius=8, fg_color=C_PILL, button_color=C_PILL, button_hover_color=C_PILLH, text_color=C_WHITE, dropdown_fg_color=C_PANEL, dropdown_hover_color=C_PILL, font=(FONT, 13, "bold"), command=lambda _: self._live_convert()) self._to_dd.grid(row=0, column=2, sticky="ew") # ── Rate label ──────────────────────────────────────────────────── self._rate_lbl = ctk.CTkLabel(self, text="", font=(FONT, 11), text_color=C_GREY) self._rate_lbl.pack(pady=(10, 0)) # Separator ctk.CTkFrame(self, height=1, fg_color=C_SEP, corner_radius=0)\ .pack(fill="x", padx=16, pady=(10, 0)) # ── INPUT card ──────────────────────────────────────────────────── in_card = ctk.CTkFrame(self, fg_color=C_PANEL, corner_radius=12) in_card.pack(fill="x", padx=16, pady=(12, 0)) in_hdr = ctk.CTkFrame(in_card, fg_color=C_PANEL) in_hdr.pack(fill="x", padx=14, pady=(10, 0)) self._in_tag = ctk.CTkLabel(in_hdr, text="USD $", font=(FONT, 10, "bold"), text_color=ACCENT) self._in_tag.pack(side="left") self._copy_in_btn = ctk.CTkButton( in_hdr, text="⎘", width=28, height=22, corner_radius=6, fg_color="transparent", hover_color=C_PILL, text_color=C_GREY2, font=(FONT, 11), command=self._copy_input) self._copy_in_btn.pack(side="right") self.amount_var = ctk.StringVar(value="1") self._entry = ctk.CTkEntry( in_card, textvariable=self.amount_var, font=(FONT_D, 38, "bold"), fg_color=C_PANEL, border_width=0, text_color=C_WHITE, justify="center", width=280, height=56) self._entry.pack(pady=(2, 12), padx=14) self._entry.bind("", lambda e: self._live_convert()) self._entry.bind("", lambda e: self._live_convert()) # ── Swap vertical ───────────────────────────────────────────────── mid = ctk.CTkFrame(self, fg_color=C_BG) mid.pack(pady=8) ctk.CTkButton(mid, text="↕", width=32, height=32, corner_radius=16, fg_color=C_PILL, hover_color=C_PILLH, text_color=C_GREY, font=(FONT, 13), command=self._swap).pack() # ── OUTPUT card ─────────────────────────────────────────────────── out_card = ctk.CTkFrame(self, fg_color=C_PANEL, corner_radius=12) out_card.pack(fill="x", padx=16, pady=(0, 12)) out_hdr = ctk.CTkFrame(out_card, fg_color=C_PANEL) out_hdr.pack(fill="x", padx=14, pady=(10, 0)) self._out_tag = ctk.CTkLabel(out_hdr, text="VND ₫", font=(FONT, 10, "bold"), text_color=ACCENT) self._out_tag.pack(side="left") self._copy_out_btn = ctk.CTkButton( out_hdr, text="⎘", width=28, height=22, corner_radius=6, fg_color="transparent", hover_color=C_PILL, text_color=C_GREY2, font=(FONT, 11), command=self._copy_output) self._copy_out_btn.pack(side="right") self._result_lbl = ctk.CTkLabel( out_card, text="—", font=(FONT_D, 38, "bold"), text_color=C_WHITE) self._result_lbl.pack(pady=(2, 12)) # ── Status bar ──────────────────────────────────────────────────── bot = ctk.CTkFrame(self, fg_color=C_BG) bot.pack(fill="x", padx=16, pady=(0, 12)) self._status_lbl = ctk.CTkLabel(bot, text="● connecting…", font=(FONT, 9), text_color=C_GREY2) self._status_lbl.pack(side="left") # Dark / Light toggle self._mode = "dark" self._mode_btn = ctk.CTkButton( bot, text="☀", width=28, height=22, corner_radius=6, fg_color="transparent", hover_color=C_PILL, text_color=C_GREY2, font=(FONT, 13), command=self._toggle_mode) self._mode_btn.pack(side="right", padx=(6, 0)) ctk.CTkLabel(bot, text="Currency Converter", font=(FONT, 9), text_color=C_GREY2).pack(side="right") # ── Logic ───────────────────────────────────────────────────────────────── def _toggle_mode(self): if self._mode == "dark": self._mode = "light" ctk.set_appearance_mode("light") self._mode_btn.configure(text="🌙") else: self._mode = "dark" ctk.set_appearance_mode("dark") self._mode_btn.configure(text="☀") def _on_from_change(self): self._refresh_tags() self._live_convert() def _refresh_tags(self): f, t = self.from_var.get(), self.to_var.get() self._in_tag.configure(text=f"{f} {SYMBOLS.get(f,'')}") self._out_tag.configure(text=f"{t} {SYMBOLS.get(t,'')}") def _get_amount(self): raw = self.amount_var.get().strip().replace(",", "") m = re.findall(r"[\d.]+", raw) if not m: return None try: return float(m[0]) except ValueError: return None def _fmt(self, v): if v >= 1000: return f"{v:,.2f}" if v >= 1: return f"{v:.4f}" return f"{v:.6f}" def _live_convert(self): self._refresh_tags() amt = self._get_amount() if amt is None: return frm, to = self.from_var.get(), self.to_var.get() try: result = self.cache.convert(amt, frm, to) self._result_str = self._fmt(result) self._result_lbl.configure(text=self._result_str, text_color=C_WHITE) self._rate_lbl.configure(text=self.cache.rate_str(frm, to)) except Exception: pass def _swap(self): a, b = self.from_var.get(), self.to_var.get() res = self._result_str.replace(",", "") if self._result_str else None self.from_var.set(b) self.to_var.set(a) if res: try: self.amount_var.set(res) except Exception: pass self._refresh_tags() self._live_convert() def _do_copy(self, btn, value): if not value: return try: if HAS_PYPERCLIP: pyperclip.copy(value) else: self.clipboard_clear(); self.clipboard_append(value) except Exception: pass btn.configure(text="✓", text_color=GREEN) self.after(1200, lambda: btn.configure(text="⎘", text_color=C_GREY2)) def _copy_input(self): self._do_copy(self._copy_in_btn, self.amount_var.get()) def _copy_output(self): self._do_copy(self._copy_out_btn, self._result_str) def _on_rates(self, live): def _u(): self._status_lbl.configure( text="● live rates" if live else "● offline", text_color=GREEN if live else C_GREY2) self._live_convert() self.after(0, _u) def main(): print("💱 Currency | ⌘+Shift+T") App().mainloop() if __name__ == "__main__": main()