commit 05fdd13cd6863083e263368fbe6195d14cdf4991 Author: anas-rashid Date: Wed Apr 1 00:18:02 2026 +0200 Add scientific calculator with modern Catppuccin Mocha UI Features: basic arithmetic, trig/inverse-trig, log/ln, factorial, memory (MC/MR/M+/M−), DEG/RAD toggle, auto-resizing display, hover effects, keyboard bindings, and clipboard copy. Co-Authored-By: Claude Sonnet 4.6 diff --git a/README.md b/README.md new file mode 100644 index 0000000..69bd607 --- /dev/null +++ b/README.md @@ -0,0 +1,39 @@ +# Scientific Calculator + +A desktop scientific calculator built with Python and Tkinter. + +## Features + +**Basic arithmetic** +- Addition, subtraction, multiplication, division +- Modulo (`%`), power (`xʸ`), parentheses +- Negate (`±`), decimal point + +**Scientific functions** +- Trigonometry: `sin`, `cos`, `tan` (degrees) +- `√x` — square root +- `log` — base-10 logarithm +- `ln` — natural logarithm +- `x!` — factorial +- Constants: `π` and `e` + +## Requirements + +- Python 3.8+ (Tkinter is included in the standard library) + +## Run + +```bash +python calculator.py +``` + +## Keyboard shortcuts + +| Key | Action | +|-----|--------| +| `0-9`, `.` | Digits | +| `+`, `-`, `*`, `/`, `%` | Operators | +| `(`, `)` | Parentheses | +| `Enter` | Evaluate | +| `Backspace` | Delete last character | +| `Esc` | Clear all | diff --git a/calculator.py b/calculator.py new file mode 100644 index 0000000..014c383 --- /dev/null +++ b/calculator.py @@ -0,0 +1,363 @@ +import tkinter as tk +import math +import re + +# ── Catppuccin Mocha palette ────────────────────────────────────────────────── +BG = "#1e1e2e" +BASE = "#313244" +OVERLAY = "#45475a" +TEXT = "#cdd6f4" +SUBTEXT = "#6c7086" +BLUE = "#89b4fa" +PURPLE = "#cba6f7" +GREEN = "#a6e3a1" +RED = "#f38ba8" +YELLOW = "#f9e2af" +TEAL = "#94e2d5" + + +class Calculator(tk.Tk): + def __init__(self): + super().__init__() + self.title("Scientific Calculator") + self.resizable(False, False) + self.configure(bg=BG) + + self.expression = "" + self.result_shown = False + self.memory = 0.0 + self.angle_mode = "DEG" # or "RAD" + + self._build_display() + self._build_buttons() + self._center_window() + + # ── window centering ────────────────────────────────────────────────────── + + def _center_window(self): + self.update_idletasks() + w, h = self.winfo_width(), self.winfo_height() + sw, sh = self.winfo_screenwidth(), self.winfo_screenheight() + self.geometry(f"+{(sw - w) // 2}+{(sh - h) // 2}") + + # ── display ─────────────────────────────────────────────────────────────── + + def _build_display(self): + display_frame = tk.Frame(self, bg=BG, pady=12, padx=14) + display_frame.pack(fill="x") + + # top bar: memory indicator (left) + angle-mode toggle (right) + top_bar = tk.Frame(display_frame, bg=BG) + top_bar.pack(fill="x") + + self.mem_indicator = tk.Label( + top_bar, text="", bg=BG, fg=TEAL, + font=("Consolas", 10, "bold"), anchor="w", + ) + self.mem_indicator.pack(side="left") + + self.angle_lbl = tk.Label( + top_bar, text="DEG", bg=OVERLAY, fg=BLUE, + font=("Consolas", 10, "bold"), + padx=10, pady=3, cursor="hand2", + ) + self.angle_lbl.pack(side="right") + self.angle_lbl.bind("", lambda _: self._toggle_angle()) + + # history line + self.history_var = tk.StringVar(value="") + tk.Label( + display_frame, + textvariable=self.history_var, + bg=BG, fg=SUBTEXT, + font=("Consolas", 12), + anchor="e", height=1, + ).pack(fill="x") + + # main display — font shrinks automatically for long text + self.display_var = tk.StringVar(value="0") + self.display_lbl = tk.Label( + display_frame, + textvariable=self.display_var, + bg=BG, fg=TEXT, + font=("Consolas", 32, "bold"), + anchor="e", height=2, + ) + self.display_lbl.pack(fill="x") + + # click display to copy result + self.display_lbl.bind("", lambda _: self._copy_result()) + self.display_lbl.config(cursor="hand2") + + # divider + tk.Frame(self, bg=OVERLAY, height=1).pack(fill="x") + + # ── buttons ─────────────────────────────────────────────────────────────── + + def _build_buttons(self): + btn_frame = tk.Frame(self, bg=BG, padx=8, pady=8) + btn_frame.pack() + + C = { + "digit": (BASE, TEXT), + "op": (OVERLAY, PURPLE), + "fn": (BASE, BLUE), + "equal": (PURPLE, BG), + "clear": (RED, BG), + "back": (OVERLAY, RED), + "const": (BASE, GREEN), + "mem": (BASE, TEAL), + } + + layout = [ + # ── Row 0 · memory ────────────────────────────────────────────── + ("MC", 0, 0, 1, "mem", self._mem_clear), + ("MR", 0, 1, 1, "mem", self._mem_recall), + ("M+", 0, 2, 1, "mem", self._mem_add), + ("M−", 0, 3, 1, "mem", self._mem_sub), + # ── Row 1 · basic trig ────────────────────────────────────────── + ("sin", 1, 0, 1, "fn", lambda: self._insert_fn("sin")), + ("cos", 1, 1, 1, "fn", lambda: self._insert_fn("cos")), + ("tan", 1, 2, 1, "fn", lambda: self._insert_fn("tan")), + ("√x", 1, 3, 1, "fn", lambda: self._insert_fn("sqrt")), + # ── Row 2 · inverse trig + power ──────────────────────────────── + ("asin", 2, 0, 1, "fn", lambda: self._insert_fn("asin")), + ("acos", 2, 1, 1, "fn", lambda: self._insert_fn("acos")), + ("atan", 2, 2, 1, "fn", lambda: self._insert_fn("atan")), + ("xʸ", 2, 3, 1, "fn", lambda: self._append("**")), + # ── Row 3 · logarithms + factorial + square ────────────────────── + ("log", 3, 0, 1, "fn", lambda: self._insert_fn("log")), + ("ln", 3, 1, 1, "fn", lambda: self._insert_fn("ln")), + ("x!", 3, 2, 1, "fn", lambda: self._insert_fn("fact")), + ("x²", 3, 3, 1, "fn", lambda: self._append("**2")), + # ── Row 4 · parens + constants ─────────────────────────────────── + ("(", 4, 0, 1, "op", lambda: self._append("(")), + (")", 4, 1, 1, "op", lambda: self._append(")")), + ("π", 4, 2, 1, "const", lambda: self._append("π")), + ("e", 4, 3, 1, "const", lambda: self._append("e")), + # ── Row 5 · clear / back ───────────────────────────────────────── + ("AC", 5, 0, 1, "clear", self._clear_all), + ("CE", 5, 1, 1, "back", self._clear_entry), + ("⌫", 5, 2, 1, "back", self._backspace), + ("%", 5, 3, 1, "op", lambda: self._append("%")), + # ── Row 6 ──────────────────────────────────────────────────────── + ("7", 6, 0, 1, "digit", lambda: self._append("7")), + ("8", 6, 1, 1, "digit", lambda: self._append("8")), + ("9", 6, 2, 1, "digit", lambda: self._append("9")), + ("÷", 6, 3, 1, "op", lambda: self._append("/")), + # ── Row 7 ──────────────────────────────────────────────────────── + ("4", 7, 0, 1, "digit", lambda: self._append("4")), + ("5", 7, 1, 1, "digit", lambda: self._append("5")), + ("6", 7, 2, 1, "digit", lambda: self._append("6")), + ("×", 7, 3, 1, "op", lambda: self._append("*")), + # ── Row 8 ──────────────────────────────────────────────────────── + ("1", 8, 0, 1, "digit", lambda: self._append("1")), + ("2", 8, 1, 1, "digit", lambda: self._append("2")), + ("3", 8, 2, 1, "digit", lambda: self._append("3")), + ("−", 8, 3, 1, "op", lambda: self._append("-")), + # ── Row 9 ──────────────────────────────────────────────────────── + ("±", 9, 0, 1, "op", self._negate), + ("0", 9, 1, 1, "digit", lambda: self._append("0")), + (".", 9, 2, 1, "digit", lambda: self._append(".")), + ("+", 9, 3, 1, "op", lambda: self._append("+")), + # ── Row 10 · equals ────────────────────────────────────────────── + ("=", 10, 0, 4, "equal", self._evaluate), + ] + + for (label, row, col, span, ckey, cmd) in layout: + bg, fg = C[ckey] + btn = tk.Button( + btn_frame, + text=label, + bg=bg, fg=fg, + activebackground=self._lighten(bg), + activeforeground=fg, + font=("Consolas", 14, "bold"), + relief="flat", bd=0, + width=5 if span == 1 else 23, + height=2, + cursor="hand2", + command=cmd, + ) + btn.grid(row=row, column=col, columnspan=span, + padx=4, pady=3, sticky="ew") + # hover highlight + btn.bind("", lambda _e, b=btn, c=bg: b.config(bg=self._lighten(c))) + btn.bind("", lambda _e, b=btn, c=bg: b.config(bg=c)) + + # keyboard bindings + self.bind("", self._on_key) + self.bind("", lambda _: self._evaluate()) + self.bind("", lambda _: self._evaluate()) + self.bind("", lambda _: self._backspace()) + self.bind("", lambda _: self._clear_all()) + self.bind("", lambda _: self._clear_all()) + self.bind("", lambda _: self._copy_result()) + + # ── helpers ─────────────────────────────────────────────────────────────── + + @staticmethod + def _lighten(hex_color: str) -> str: + h = hex_color.lstrip("#") + r, g, b = (int(h[i:i+2], 16) for i in (0, 2, 4)) + return f"#{min(255, r+35):02x}{min(255, g+35):02x}{min(255, b+35):02x}" + + def _set_display(self, text: str): + text = text or "0" + n = len(text) + size = 18 if n > 20 else 22 if n > 14 else 26 if n > 9 else 32 + self.display_lbl.config(font=("Consolas", size, "bold")) + self.display_var.set(text) + + def _append(self, char: str): + if self.result_shown: + if char in "0123456789.": + self.expression = "" + self.result_shown = False + self.expression += char + self._set_display(self.expression) + + def _insert_fn(self, fn: str): + self.result_shown = False + self.expression += fn + "(" + self._set_display(self.expression) + + def _clear_all(self): + self.expression = "" + self.history_var.set("") + self.result_shown = False + self._set_display("") + + def _clear_entry(self): + self.expression = "" + self.result_shown = False + self._set_display("") + + def _backspace(self): + if self.result_shown: + self._clear_all() + return + self.expression = self.expression[:-1] + self._set_display(self.expression) + + def _negate(self): + if not self.expression or self.expression == "0": + return + if self.expression.startswith("-"): + self.expression = self.expression[1:] + else: + self.expression = "-" + self.expression + self._set_display(self.expression) + + def _toggle_angle(self): + if self.angle_mode == "DEG": + self.angle_mode = "RAD" + self.angle_lbl.config(text="RAD", fg=YELLOW) + else: + self.angle_mode = "DEG" + self.angle_lbl.config(text="DEG", fg=BLUE) + + def _copy_result(self): + self.clipboard_clear() + self.clipboard_append(self.display_var.get()) + + # ── memory ──────────────────────────────────────────────────────────────── + + def _update_mem_indicator(self): + self.mem_indicator.config( + text=f"M = {self.memory:.6g}" if self.memory != 0 else "" + ) + + def _mem_clear(self): + self.memory = 0.0 + self._update_mem_indicator() + + def _mem_recall(self): + val = f"{self.memory:.10g}" + if self.result_shown or not self.expression: + self.expression = val + else: + self.expression += val + self.result_shown = False + self._set_display(self.expression) + + def _mem_add(self): + try: + self.memory += float(self._safe_eval(self.expression)) + self._update_mem_indicator() + except Exception: + pass + + def _mem_sub(self): + try: + self.memory -= float(self._safe_eval(self.expression)) + self._update_mem_indicator() + except Exception: + pass + + # ── keyboard ────────────────────────────────────────────────────────────── + + def _on_key(self, event: tk.Event): + char = event.char + if char in "0123456789.+-*/()%": + self._append(char) + elif char == "^": + self._append("**") + + # ── evaluate ────────────────────────────────────────────────────────────── + + def _evaluate(self): + if not self.expression: + return + try: + result = self._safe_eval(self.expression) + self.history_var.set(self.expression + " =") + if isinstance(result, float) and result.is_integer(): + display = str(int(result)) + else: + display = f"{result:.10g}" + self.expression = display + self._set_display(display) + self.result_shown = True + except ZeroDivisionError: + self._show_error("Error: ÷ 0") + except ValueError as exc: + self._show_error(f"Error: {exc}") + except Exception: + self._show_error("Error") + + def _show_error(self, msg: str): + self._set_display(msg) + self.expression = "" + self.result_shown = False + + def _safe_eval(self, expr: str): + expr = expr.replace("π", str(math.pi)) + # Replace standalone 'e' constant but not 'e' in scientific notation (e.g. 1e5) + expr = re.sub(r"(?