When the user clicks on the lb(Kg) button the execution field should show Pound() and for the Kg(lb) the field should show Kilogram(). Python 3.12 code: import math
import re
from typing import Any, Union, Callable
from PySide6.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QLineEdit, QTextEdit, QScrollBar, QPushButton, QLabel, QFrame,
QGridLayout
)
from PySide6.QtGui import QFont
from PySide6.QtCore import Qt

def convert_angle(func: Callable[[float], float], x: float, use_degrees: bool) -> float:
return func(math.radians(x)) if use_degrees else func(x)

def kilograms_to_pounds(kg: float) -> float:
return kg / 0.45359237

def pounds_to_kilograms(lb: float) -> float:
return lb * 0.45359237

def celsius_to_kelvin(c: float) -> float:
return c + 273.15

def kelvin_to_celsius(k: float) -> float:
return k - 273.15

def celsius_to_fahrenheit(c: float) -> float:
return c * 9 / 5 + 32

def fahrenheit_to_celsius(f: float) -> float:
return (f - 32) * 5 / 9

def capitalize_sentence(sentence: str) -> str:
return ' '.join(word.capitalize() for word in sentence.split())

def remove_duplicates(text: str) -> str:
words = ' '.join(line.strip() for line in text.splitlines() if line.strip()).split()
return ' '.join(dict.fromkeys(words))

def get_safe_globals(use_degrees: bool, variables: dict) -> dict:
trig_funcs = {'sin', 'cos', 'tan', 'asin', 'acos', 'atan'}
safe_globals = {
"sqrt": math.sqrt,
"log": math.log10,
"ln": math.log,
"pi": math.pi,
"e": math.e,
"lb": kilograms_to_pounds,
"kg": pounds_to_kilograms,
"K": celsius_to_kelvin,
"C": kelvin_to_celsius,
"F": celsius_to_fahrenheit,
"CF": fahrenheit_to_celsius,
"capitalize": capitalize_sentence,
"duplicates": remove_duplicates,
}
for func in trig_funcs:
safe_globals[func] = lambda x, f=func: convert_angle(getattr(math, f), x, use_degrees)
safe_globals.update(variables)
return safe_globals

def preprocess_text_functions(expression: str) -> str:
"""Preprocess text functions with argument quotes."""
def add_quotes(match):
func, args = match.groups()
args = args.strip()
if not (args.startswith('"') and args.endswith('"')) and not (args.startswith("'") and args.endswith("'")):
args = f'"{args}"'
return f'{func}({args})'
return re.sub(r'\b(capitalize|duplicates)\s*(\s*([^)]+)\s*)', add_quotes, expression)

def validate_inverse_trig(expression: str, safe_globals: dict) -> Union[str, None]:
"""Check inverse trig function domain validity."""
for func, arg in re.findall(r'\b(asin|acos)\s*(\s*([^)]+)\s*)', expression):
try:
value = eval(arg, {"builtins": None}, safe_globals)
if abs(value) > 1:
return f"Error: {func}({arg}) is undefined for |{arg}| > 1"
except Exception as e:
return f"Error: {e}"
return None

def safe_evaluate(expression: str, safe_globals: dict) -> Union[str, Any]:
try:
result = eval(expression, {"builtins": None}, safe_globals)
if isinstance(result, (int, float)):
if isinstance(result, float):
abs_result = abs(result)
if (abs_result >= 1e9) or (abs_result != 0 and abs_result < 1e-6):
result = f"{result:.3e}"
else:
result = f"{result:.3f}"
else:
if len(str(abs(result))) > 9:
result = f"{result:.3e}"
else:
result = str(result)
return result
else:
return result
except Exception as e:
return f"Error: {e}"

def evaluate_expression(expression: str, variables: dict, use_degrees: bool = False) -> Union[float, str]:
assignment_match = re.match(r'^\s*([a-zA-Z_]\w*)\s*=\s*(.+)$', expression)
expression = preprocess_text_functions(expression)
safe_globals = get_safe_globals(use_degrees, variables)

if assignment_match:
    var_name, var_expr = assignment_match.groups()
    var_value = safe_evaluate(var_expr, safe_globals)
    try:
        if isinstance(var_value, str) and not var_value.startswith("Error"):
            if '.' in var_value:
                var_value_converted = float(var_value)
            else:
                var_value_converted = int(var_value)
            variables[var_name] = var_value_converted
            return f"{var_name} = {var_value_converted}"
        else:
            return var_value
    except ValueError:
        return f"Error: Cannot assign non-numeric value to variable '{var_name}'"
else:
    error = validate_inverse_trig(expression, safe_globals)
    if error:
        return error
    return safe_evaluate(expression, safe_globals)

class CalculatorGUI(QMainWindow):
def init(self):
super().init()
self.use_degrees = False
self.variables = {}
self.history_entries = []
self.text_functions = ['capitalize', 'duplicates']
self.setup_window()
self.create_widgets()
self.layout_widgets()
self.connect_signals()

def setup_window(self) -> None:
    self.setWindowTitle("Calculator")
    self.setGeometry(100, 100, 1280, 720)

def create_widgets(self) -> None:
    font = QFont()
    font.setPointSize(12)
    font.setFamily('Literata')

    self.entry = QLineEdit()
    self.entry.setFont(font)
    self.entry.setPlaceholderText('Enter expression')
    self.entry.setClearButtonEnabled(True)
    self.entry.returnPressed.connect(self.calculate)

    self.history_text = QTextEdit()
    self.history_text.setFont(font)
    self.history_text.setReadOnly(True)
    self.history_text.setStyleSheet("position: relative;")

    self.scrollbar = QScrollBar(Qt.Vertical)
    self.history_text.setVerticalScrollBar(self.scrollbar)

    # Search Button
    self.search_button = QPushButton("🔍", self.history_text)
    self.search_button.setFixedSize(24, 24)
    self.search_button.setToolTip("Search History")
    self.search_button.setStyleSheet("""
        QPushButton {
            border: none;
            background-color: rgba(255, 255, 255, 200);
            font-size: 14px;
            border-radius: 12px;
        }
        QPushButton:hover {
            background-color: rgba(0, 120, 215, 200);
            color: white;
        }
    """)
    self.search_button.clicked.connect(self.toggle_search)
    self.search_button.raise_()

    # Variables Button
    self.vars_button = QPushButton("🧮", self.history_text)  # Using an abacus emoji as an example
    self.vars_button.setFixedSize(24, 24)
    self.vars_button.setToolTip("Show Variables")
    self.vars_button.setStyleSheet("""
        QPushButton {
            border: none;
            background-color: rgba(255, 255, 255, 200);
            font-size: 14px;
            border-radius: 12px;
        }
        QPushButton:hover {
            background-color: rgba(0, 150, 136, 200);
            color: white;
        }
    """)
    self.vars_button.clicked.connect(self.toggle_vars_overlay)
    self.vars_button.raise_()

    # Delete History Button
    self.delete_button = QPushButton("🗑️", self.history_text)
    self.delete_button.setFixedSize(24, 24)
    self.delete_button.setToolTip("Clear History")
    self.delete_button.setStyleSheet("""
        QPushButton {
            border: none;
            background-color: rgba(255, 255, 255, 200);
            font-size: 14px;
            border-radius: 12px;
        }
        QPushButton:hover {
            background-color: rgba(255, 0, 0, 200);
            color: white;
        }
    """)
    self.delete_button.clicked.connect(self.clear_history)
    self.delete_button.raise_()

    # Variables Overlay
    self.vars_overlay = QLineEdit(self.history_text)
    self.vars_overlay.setReadOnly(True)
    self.vars_overlay.setPlaceholderText("a = 1, b = 85, ...")
    self.vars_overlay.setFixedHeight(24)
    self.vars_overlay.setFixedWidth(300)  # Double the previous assumed width
    self.vars_overlay.setStyleSheet("""
        QLineEdit {
            border: 1px solid gray;
            border-radius: 4px;
            padding: 2px 4px;
            background-color: white;
        }
    """)
    self.vars_overlay.setVisible(False)

    # Delete Variables Button within Variables Overlay
    self.vars_delete_button = QPushButton("🗑️", self.vars_overlay)
    self.vars_delete_button.setFixedSize(24, 24)
    self.vars_delete_button.setToolTip("Clear Variables")
    self.vars_delete_button.setStyleSheet("""
        QPushButton {
            border: none;
            background-color: rgba(255, 255, 255, 200);
            font-size: 14px;
            border-radius: 12px;
        }
        QPushButton:hover {
            background-color: rgba(255, 0, 0, 200);
            color: white;
        }
    """)
    self.vars_delete_button.clicked.connect(self.clear_variables)
    self.vars_delete_button.setVisible(False)

    # Search Bar
    self.search_bar = QLineEdit(self.history_text)
    self.search_bar.setPlaceholderText("Search...")
    self.search_bar.setFixedHeight(24)
    self.search_bar.setFixedWidth(300)  # Double the previous assumed width
    self.search_bar.setVisible(False)
    self.search_bar.setStyleSheet("""
        QLineEdit {
            border: 1px solid gray;
            border-radius: 4px;
            padding: 2px 4px;
            background-color: white;
        }
    """)
    self.search_bar.textChanged.connect(self.filter_history)
    self.search_bar.setClearButtonEnabled(True)

    # Function Buttons
    self.function_buttons = {
        "Functions": ["sqrt", "sin", "cos", "tan", "asin", "acos", "atan", "log", "ln"],
        "Conversions": ["lb", "kg", "K", "C", "F", "CF"],
        "Text": ["capitalize", "duplicates"],
        "Constants": ["pi", "e"],
        "Settings": []
    }
    self.function_frame = QFrame()
    self.create_function_buttons(font)

def create_function_buttons(self, font: QFont) -> None:
    layout = QGridLayout()
    categories = [("Functions", 3), ("Conversions", 2), ("Text", 2), ("Constants", 2), ("Settings", 1)]
    starting_col = 0

    for category, col_span in categories:
        label = QLabel(category)
        label.setFont(font)
        layout.addWidget(label, 0, starting_col, 1, col_span, Qt.AlignCenter)

        buttons = self.function_buttons.get(category, [])
        if category == "Functions":
            for i, func in enumerate(buttons):
                button = QPushButton(func)
                button.setFont(font)
                button.clicked.connect(lambda _, f=func: self.insert_function(f, is_text_function=(f in self.text_functions)))
                layout.addWidget(button, (i // 3) + 1, starting_col + (i % 3))

        elif category == "Settings":
            self.mode_button = QPushButton("Radians")
            self.mode_button.setFont(font)
            self.mode_button.clicked.connect(self.toggle_mode)
            layout.addWidget(self.mode_button, 1, starting_col, 1, 1)

        elif category == "Conversions":
            for idx, func in enumerate(buttons):
                display_text = {
                    "lb": "lb(kg)", "kg": "kg(lb)", "K": "K(°C)",
                    "C": "°C(K)", "F": "F(°C)", "CF": "°C(F)"
                }.get(func, func)
                button = QPushButton(display_text)
                button.setFont(font)
                button.clicked.connect(lambda _, f=func: self.insert_function(f, is_text_function=False))
                row, col = 1 + idx // 2, starting_col + idx % 2
                layout.addWidget(button, row, col)

        elif category == "Text":
            for idx, func in enumerate(buttons):
                button = QPushButton(func)
                button.setFont(font)
                button.clicked.connect(lambda _, f=func: self.insert_function(f, is_text_function=True))
                row, col = 1 + idx // col_span, starting_col + idx % col_span
                layout.addWidget(button, row, col)

        elif category == "Constants":
            for idx, func in enumerate(buttons):
                button = QPushButton(func)
                button.setFont(font)
                button.clicked.connect(lambda _, f=func: self.insert_function(f, is_constant=True))
                row, col = 1 + idx // col_span, starting_col + idx % col_span
                layout.addWidget(button, row, col)

        # Fill empty spaces if needed
        total_rows = math.ceil(len(buttons) / col_span)
        for extra in range(col_span * total_rows - len(buttons)):
            layout.addWidget(QLabel(""), 1 + extra // col_span, starting_col + extra % col_span)

        starting_col += col_span

    self.function_frame.setLayout(layout)

def layout_widgets(self) -> None:
    central_widget = QWidget()
    main_layout = QVBoxLayout()

    main_layout.addWidget(self.entry)

    history_layout = QHBoxLayout()
    history_layout.addWidget(self.history_text)
    history_layout.addWidget(self.scrollbar)
    main_layout.addLayout(history_layout)

    main_layout.addWidget(self.function_frame)

    central_widget.setLayout(main_layout)
    self.setCentralWidget(central_widget)

def connect_signals(self) -> None:
    QApplication.instance().focusChanged.connect(self.on_focus_changed)

def toggle_mode(self) -> None:
    self.use_degrees = not self.use_degrees
    self.mode_button.setText("Degrees" if self.use_degrees else "Radians")

def insert_function(self, function: str, is_constant: bool = False, is_text_function: bool = False) -> None:
    current_text = self.entry.text()
    cursor_position = self.entry.cursorPosition()
    if is_constant:
        new_text = f"{current_text[:cursor_position]}{function} "
        new_cursor_position = cursor_position + len(function) + 1
    elif is_text_function and function in self.text_functions:
        new_text = f'{current_text[:cursor_position]}{function}() {current_text[cursor_position:]}'
        new_cursor_position = cursor_position + len(function) + 1
    else:
        new_text = f"{current_text[:cursor_position]}{function}() {current_text[cursor_position:]}"
        new_cursor_position = cursor_position + len(function) + 1
    self.entry.setText(new_text)
    self.entry.setCursorPosition(new_cursor_position)
    self.entry.setFocus()

def calculate(self) -> None:
    expression = self.entry.text().strip()
    if expression:
        result = evaluate_expression(
            expression, self.variables, use_degrees=self.use_degrees
        )
        self.display_result(expression, result)
        self.entry.clear()

def display_result(self, expression: str, result: Union[float, str]) -> None:
    mode = "D" if self.use_degrees else "R"
    if re.match(r'^\s*[a-zA-Z_]\w*\s*=\s*.+$', expression):
        history_entry = f"[{mode}] {result}"
    else:
        history_entry = f"[{mode}] {expression} = {result}"
    self.history_entries.append(history_entry)
    self.refresh_history()
    self.history_text.verticalScrollBar().setValue(
        self.history_text.verticalScrollBar().maximum()
    )

def toggle_search(self) -> None:
    if self.search_bar.isVisible():
        self.search_bar.setVisible(False)
    else:
        self.search_bar.setVisible(True)
        self.search_bar.setFocus()  # Set focus to the search bar
    self.update_overlay_buttons_position()

def filter_history(self, text: str) -> None:
    self.refresh_history(text)

def refresh_history(self, filter_text: str = "") -> None:
    self.history_text.clear()
    for entry in self.history_entries:
        if filter_text.lower() in entry.lower():
            self.history_text.append(entry)

def resizeEvent(self, event) -> None:
    super().resizeEvent(event)
    self.update_overlay_buttons_position()

def update_overlay_buttons_position(self) -> None:
    size = self.history_text.viewport().size()

    margin_right = 5
    margin_top = 5
    spacing = 5

    # Determine available width for buttons
    available_width = size.width()

    # Calculate and position the search button
    search_offset = self.search_bar.width() + spacing if self.search_bar.isVisible() else 0
    self.search_button.move(
        available_width - search_offset - self.search_button.width() - margin_right,
        margin_top
    )

    # Calculate and position the variables button
    vars_offset = self.vars_overlay.width() + spacing if self.vars_overlay.isVisible() else 0
    vars_button_y = self.search_button.y() + self.search_button.height() + spacing
    self.vars_button.move(
        available_width - vars_offset - self.vars_button.width() - margin_right,
        vars_button_y
    )

    # Position the delete button for history
    self.delete_button.move(
        available_width - self.delete_button.width() - margin_right,
        self.vars_button.y() + self.vars_button.height() + spacing
    )

    # Variables Overlay Position
    if self.vars_overlay.isVisible():
        self.vars_overlay.move(
            available_width - self.vars_overlay.width() - margin_right,
            self.vars_button.y()
        )
        self.vars_delete_button.move(
            self.vars_overlay.width() - self.vars_delete_button.width() - margin_right,
            (self.vars_overlay.height() - self.vars_delete_button.height()) // 2
        )
        self.vars_delete_button.setVisible(True)

    # Search Bar Position
    if self.search_bar.isVisible():
        self.search_bar.move(
            available_width - self.search_bar.width() - margin_right,
            margin_top
        )

def on_focus_changed(self, old: QWidget, new: QWidget) -> None:
    # Hide the variables overlay if focus is lost
    if self.vars_overlay.isVisible() and new not in {self.vars_overlay, self.vars_delete_button, self.vars_button}:
        self.vars_overlay.setVisible(False)
        self.vars_delete_button.setVisible(False)
        self.update_overlay_buttons_position()
    # Hide the search bar if focus is lost and search bar is empty
    if self.search_bar.isVisible() and not self.search_bar.text().strip() and new != self.search_bar:
        self.toggle_search()

def showEvent(self, event) -> None:
    super().showEvent(event)
    self.update_overlay_buttons_position()

def clear_history(self) -> None:
    self.history_entries.clear()
    self.refresh_history()

def toggle_vars_overlay(self) -> None:
    if self.vars_overlay.isVisible():
        self.vars_overlay.setVisible(False)
        self.vars_delete_button.setVisible(False)
    else:
        self.update_vars_overlay()
        self.vars_overlay.setVisible(True)
        self.vars_delete_button.setVisible(True)
    self.update_overlay_buttons_position()

def update_vars_overlay(self) -> None:
    if not self.variables:
        self.vars_overlay.setText("No variables assigned.")
    else:
        vars_text = ", ".join(f"{var} = {value}" for var, value in self.variables.items())
        self.vars_overlay.setText(vars_text)

def clear_variables(self) -> None:
    self.variables.clear()
    self.update_vars_overlay()
    self.vars_overlay.setVisible(True)
    self.vars_delete_button.setVisible(True)

def main() -> None:
app = QApplication([])
calculator = CalculatorGUI()
calculator.show()
app.exec()

if name == "main":
main()