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 <noreply@anthropic.com>
This commit is contained in:
anas-rashid 2026-04-01 00:18:02 +02:00
commit 05fdd13cd6
2 changed files with 402 additions and 0 deletions

39
README.md Normal file
View File

@ -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 |

363
calculator.py Normal file
View File

@ -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("<Button-1>", 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("<Button-1>", 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")),
("", 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")),
("", 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("<Enter>", lambda _e, b=btn, c=bg: b.config(bg=self._lighten(c)))
btn.bind("<Leave>", lambda _e, b=btn, c=bg: b.config(bg=c))
# keyboard bindings
self.bind("<Key>", self._on_key)
self.bind("<Return>", lambda _: self._evaluate())
self.bind("<KP_Enter>", lambda _: self._evaluate())
self.bind("<BackSpace>", lambda _: self._backspace())
self.bind("<Escape>", lambda _: self._clear_all())
self.bind("<Delete>", lambda _: self._clear_all())
self.bind("<Control-c>", 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"(?<![0-9Ee])e(?![0-9+\-])", str(math.e), expr)
rad = self.angle_mode == "RAD"
def _to_rad(x): return x if rad else math.radians(x)
def _from_rad(x): return x if rad else math.degrees(x)
safe_ns = {
"__builtins__": {},
"sin": lambda x: math.sin(_to_rad(x)),
"cos": lambda x: math.cos(_to_rad(x)),
"tan": lambda x: math.tan(_to_rad(x)),
"asin": lambda x: _from_rad(math.asin(x)),
"acos": lambda x: _from_rad(math.acos(x)),
"atan": lambda x: _from_rad(math.atan(x)),
"sqrt": math.sqrt,
"log": math.log10,
"ln": math.log,
"fact": math.factorial,
"abs": abs,
}
return eval(expr, safe_ns) # noqa: S307
if __name__ == "__main__":
app = Calculator()
app.mainloop()