import customtkinter as ctk # GUI Configuration ctk.set_appearance_mode("Dark") ctk.set_default_color_theme("blue") class AppGUI(ctk.CTk): def __init__(self, controller): super().__init__() self.controller = controller self.title("Secure SMS - Raspberry Pi") self.geometry("800x480") # Typical Pi touchscreen resolution # Current state self.current_contact = None self.setup_ui() self.load_contacts() def setup_ui(self): # Grid Layout (1x2) - Sidebar | Main Content self.grid_rowconfigure(0, weight=1) self.grid_columnconfigure(1, weight=1) # --- Sidebar --- self.sidebar_frame = ctk.CTkFrame(self, width=200, corner_radius=0) self.sidebar_frame.grid(row=0, column=0, sticky="nsew") self.sidebar_frame.grid_rowconfigure(4, weight=1) self.logo_label = ctk.CTkLabel(self.sidebar_frame, text="Secure SMS", font=ctk.CTkFont(size=20, weight="bold")) self.logo_label.grid(row=0, column=0, padx=20, pady=(20, 10)) self.contacts_scrollable_frame = ctk.CTkScrollableFrame(self.sidebar_frame, label_text="Contacts") self.contacts_scrollable_frame.grid(row=1, column=0, padx=10, pady=10, sticky="nsew", rowspan=4) # Settings / Add Contact Button self.settings_btn = ctk.CTkButton(self.sidebar_frame, text="Settings / Add Contact", command=self.open_settings) self.settings_btn.grid(row=5, column=0, padx=10, pady=20) # --- Main Chat Area --- self.main_frame = ctk.CTkFrame(self, corner_radius=0) self.main_frame.grid(row=0, column=1, sticky="nsew") self.main_frame.grid_rowconfigure(1, weight=1) self.main_frame.grid_columnconfigure(0, weight=1) # Chat Header self.chat_header_var = ctk.StringVar(value="Select a contact") self.chat_header = ctk.CTkLabel(self.main_frame, textvariable=self.chat_header_var, font=ctk.CTkFont(size=18, weight="bold")) self.chat_header.grid(row=0, column=0, padx=20, pady=10, sticky="w") # Secured Status Label Indicator self.header_secure_status = ctk.CTkLabel(self.main_frame, text="Current Mode: Normal", text_color="yellow") self.header_secure_status.grid(row=0, column=0, padx=20, pady=10, sticky="e") # Messages Display self.msg_display = ctk.CTkTextbox(self.main_frame, state="disabled", wrap="word") self.msg_display.grid(row=1, column=0, padx=20, pady=(0, 10), sticky="nsew") self.msg_display.tag_config('sent_normal', foreground='lightgray', justify='right') self.msg_display.tag_config('sent_secure', foreground='lightgreen', justify='right') self.msg_display.tag_config('recv_normal', foreground='white', justify='left') self.msg_display.tag_config('recv_secure', foreground='mediumspringgreen', justify='left') self.msg_display.tag_config('system', foreground='red', justify='center') # Bottom Input Area self.input_frame = ctk.CTkFrame(self.main_frame, height=50) self.input_frame.grid(row=2, column=0, padx=20, pady=10, sticky="nsew") self.input_frame.grid_columnconfigure(0, weight=1) self.msg_entry = ctk.CTkEntry(self.input_frame, placeholder_text="Type a message...") self.msg_entry.grid(row=0, column=0, padx=(10, 5), pady=10, sticky="nsew") self.msg_entry.bind("", lambda e: self.send_message()) # Toggle Secure SMS self.secure_mode_var = ctk.StringVar(value="off") self.secure_switch = ctk.CTkSwitch( self.input_frame, text="Secure", command=self.toggle_mode, variable=self.secure_mode_var, onvalue="on", offvalue="off" ) self.secure_switch.grid(row=0, column=1, padx=5, pady=10) self.send_btn = ctk.CTkButton(self.input_frame, text="Send", width=80, command=self.send_message) self.send_btn.grid(row=0, column=2, padx=(5, 10), pady=10) def toggle_mode(self): mode = "Secure" if self.secure_mode_var.get() == "on" else "Normal" color = "mediumspringgreen" if mode == "Secure" else "yellow" self.header_secure_status.configure(text=f"Current Mode: {mode}", text_color=color) def load_contacts(self): # Clear sidebar list for widget in self.contacts_scrollable_frame.winfo_children(): widget.destroy() contacts = self.controller.get_contacts() for idx, contact in enumerate(contacts): # contact = (name, phone, pubkey) name, phone, _ = contact btn = ctk.CTkButton( self.contacts_scrollable_frame, text=f"{name}\n{phone}", command=lambda p=phone: self.select_contact(p), fg_color="transparent", border_width=2, text_color=("gray10", "#DCE4EE") ) btn.grid(row=idx, column=0, padx=5, pady=5, sticky="ew") def select_contact(self, phone): self.current_contact = self.controller.get_contact_info(phone) name = self.current_contact[0] self.chat_header_var.set(f"Chat with {name} ({phone})") # Determine if we can do secure has_key = self.current_contact[2] is not None if not has_key: self.secure_switch.deselect() self.secure_switch.configure(state="disabled") self.toggle_mode() self.append_system_msg("This contact has no public key. Secure mode disabled.") else: self.secure_switch.configure(state="normal") self.refresh_messages() def refresh_messages(self): self.msg_display.configure(state="normal") self.msg_display.delete("1.0", "end") if self.current_contact: messages = self.controller.get_messages(self.current_contact[1]) for msg in messages: # msg = (id, phone, text, date, is_secure, status) text = msg[2] date = msg[3] is_secure = msg[4] status = msg[5] # sent / recv tag = f"{status}_{'secure' if is_secure else 'normal'}" prefix = "🔒 " if is_secure else "" display_text = f"[{date}]\n{prefix}{text}\n\n" self.msg_display.insert("end", display_text, tag) self.msg_display.configure(state="disabled") self.msg_display.yview("end") def append_system_msg(self, text): self.msg_display.configure(state="normal") self.msg_display.insert("end", f"--- {text} ---\n\n", "system") self.msg_display.configure(state="disabled") self.msg_display.yview("end") def send_message(self): if not self.current_contact: return text = self.msg_entry.get().strip() if not text: return is_secure = self.secure_mode_var.get() == "on" phone = self.current_contact[1] # Ask controller to send self.msg_entry.delete(0, 'end') self.append_system_msg("Sending...") self.update() # force UI refresh safely success = self.controller.send_message(phone, text, is_secure) if success: self.refresh_messages() else: self.append_system_msg("Failed to send SMS!") def open_settings(self): SettingsWindow(self, self.controller) class SettingsWindow(ctk.CTkToplevel): def __init__(self, master, controller): super().__init__(master) self.controller = controller self.title("Settings") self.geometry("500x500") # Bring to front self.attributes('-topmost', 1) self.grid_columnconfigure(0, weight=1) # My Public Key lbl1 = ctk.CTkLabel(self, text="My Public Key (Share this to chat securely):") lbl1.grid(row=0, column=0, padx=20, pady=(20, 5), sticky="w") self.my_key_box = ctk.CTkTextbox(self, height=100) self.my_key_box.grid(row=1, column=0, padx=20, pady=5, sticky="ew") self.my_key_box.insert("1.0", self.controller.get_my_public_key()) self.my_key_box.configure(state="disabled") # Add / Update Contact lbl_add = ctk.CTkLabel(self, text="Add/Update Contact:", font=ctk.CTkFont(weight="bold")) lbl_add.grid(row=2, column=0, padx=20, pady=(30, 5), sticky="w") self.name_entry = ctk.CTkEntry(self, placeholder_text="Contact Name") self.name_entry.grid(row=3, column=0, padx=20, pady=5, sticky="ew") self.phone_entry = ctk.CTkEntry(self, placeholder_text="Phone Number (+989...)") self.phone_entry.grid(row=4, column=0, padx=20, pady=5, sticky="ew") lbl2 = ctk.CTkLabel(self, text="Contact's Public Key (Optional but required for secure SMS):") lbl2.grid(row=5, column=0, padx=20, pady=5, sticky="w") self.contact_key_box = ctk.CTkTextbox(self, height=100) self.contact_key_box.grid(row=6, column=0, padx=20, pady=5, sticky="ew") self.save_btn = ctk.CTkButton(self, text="Save Contact", command=self.save_contact) self.save_btn.grid(row=7, column=0, padx=20, pady=20) def save_contact(self): name = self.name_entry.get().strip() phone = self.phone_entry.get().strip() pubkey = self.contact_key_box.get("1.0", 'end-1c').strip() if not name or not phone: return if not pubkey: pubkey = None self.controller.save_contact(name, phone, pubkey) self.master.load_contacts() self.destroy() if __name__ == "__main__": # Mock Controller for standalone GUI test class MockController: def get_contacts(self): return [("Alice", "+1234", "key"), ("Bob", "+5678", None)] def get_contact_info(self, phone): return ("Alice", phone, "key") if phone == "+1234" else ("Bob", phone, None) def get_messages(self, phone): return [(1, phone, "Hi", "2023-01-01", 0, "recv"), (2, phone, "Secret", "2023-01-01", 1, "sent")] def get_my_public_key(self): return "MY_PEM_MOCK_DATA" def send_message(self, *args): return True def save_contact(self, *args): pass app = AppGUI(MockController()) app.mainloop()