import tkinter as tk from tkinter import Menu, filedialog, simpledialog, messagebox from tkinter import ttk import threading import time import datetime import webbrowser import subprocess import os import collections import random import urllib.request import re import urllib.error # --- Importar y configurar Pygame para la música/alarma --- try: import pygame # Intentar inicializar Pygame Mixer. Si falla, deshabilitar la música. try: pygame.mixer.init() MUSIC_AVAILABLE = True except pygame.error as e: MUSIC_AVAILABLE = False print(f"Error al inicializar pygame mixer: {e}. Funcionalidad de audio deshabilitada.") MUSIC_FILE = "music.mp3" # Archivo música de fondo ALARM_FILE = "alarm.mp3" # <--- ¡NUEVO ARCHIVO DE ALARMA! except ImportError: MUSIC_AVAILABLE = False print("Pygame no está instalado. La funcionalidad de audio no estará disponible.") # --- Importación específica para el sonido de alarma (Solo Windows) --- try: import winsound WINSOUND_AVAILABLE = True except ImportError: WINSOUND_AVAILABLE = False # --------------------------------------------------------------------- # --- IMPORTACIONES PARA GRÁFICOS DE RECURSOS --- import psutil import matplotlib.pyplot as plt from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg # ----------------------------------------------- # --- HILOS GLOBALES PARA CONTROL --- cpu_monitor = None memory_monitor = None disk_monitor = None net_monitor = None network_thread = None clock_thread = None status_bar_time_thread = None alarm_stop_event = threading.Event() countdown_after_id = None # --- VARIABLES GLOBALES DEL JUEGO DE COCHES --- race_threads = [] race_lock = threading.Lock() winner = None race_stop_event = threading.Event() # --- VARIABLES GLOBALES DE MÚSICA --- music_state = False # False = parada, True = sonando # --------------------------------------------- # --- CLASE DE GESTIÓN DE LA BARRA DE ESTADO --- class StatusBarManager: """Gestiona los tres labels dinámicos de la barra inferior (más los dos nuevos).""" def __init__(self, parent_frame, root): self.parent_frame = parent_frame self.root = root self.labels = {} self._initialize_labels() def _initialize_labels(self): label_configs = [ ("correos", "Correos sin leer (0)", "yellow"), ("temperatura", "Temperatura local (0°C)", "orange"), ("status_1", "Estado 1 (Libre)", "green"), ("status_2", "Estado 2 (Listo)", "blue"), ("status_3", "Estado 3", "cyan"), ] for name, initial_text, bg_color in label_configs[2:]: label = tk.Label( self.parent_frame, text=initial_text, bg=bg_color, anchor="w", width=20, bd=1, relief="sunken" ) label.pack(side="left", fill="x", expand=True) self.labels[name] = label for name, initial_text, bg_color in label_configs[:2]: label = tk.Label( self.parent_frame, text=initial_text, bg=bg_color, anchor="w", width=25, bd=1, relief="flat", font=("Helvetica", 10) ) label.pack(side="right", fill="x", padx=5) self.labels[name] = label def set_status(self, label_name, text, bg_color=None): if label_name in self.labels: label = self.labels[label_name] config = {"text": text} if bg_color: config["bg"] = bg_color self.root.after(0, label.config, config) else: print(f"Error: Label '{label_name}' no encontrado.") # --- CLASE PARA MONITORIZAR Y GRAFICAR RECURSOS --- class SystemMonitor: def __init__(self, master, resource_type, width=4, height=3): self.master = master self.resource_type = resource_type self.update_interval = 1000 if resource_type == 'net': self.data_history_sent = collections.deque([0] * 30, maxlen=30) self.data_history_recv = collections.deque([0] * 30, maxlen=30) self._last_net_io = psutil.net_io_counters() else: self.data_history = collections.deque([0] * 30, maxlen=30) self.fig, self.ax = plt.subplots(figsize=(width, height), dpi=100) self.canvas = FigureCanvasTkAgg(self.fig, master=master) self.canvas_widget = self.canvas.get_tk_widget() self.canvas_widget.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) self.setup_plot() self.stop_event = threading.Event() self.monitor_thread = threading.Thread(target=self._data_collection_loop, daemon=True) self.monitor_thread.start() def setup_plot(self): title_map = {'cpu': "Uso de CPU", 'memory': "Uso de RAM", 'disk': "Uso de Disco (I/O)", 'net': "Tasa de Red (Kb/s)"} ylabel_map = {'cpu': "% Uso", 'memory': "% Uso", 'disk': "% Disco", 'net': "Kb/s"} self.ax.set_title(title_map.get(self.resource_type, "Recurso"), fontsize=10) self.ax.set_ylim(0, 100 if self.resource_type != 'net' else 500) self.ax.set_ylabel(ylabel_map.get(self.resource_type, "Valor"), fontsize=8) self.ax.set_xlabel("Tiempo (s)", fontsize=8) self.ax.tick_params(axis='both', which='major', labelsize=7) if self.resource_type == 'net': self.line_sent, = self.ax.plot(self.data_history_sent, label='Enviado (Kb)', color='red') self.line_recv, = self.ax.plot(self.data_history_recv, label='Recibido (Kb)', color='blue') self.ax.legend(loc='upper right', fontsize=7) else: self.line, = self.ax.plot(self.data_history, label=f'{self.resource_type.upper()} %', color='blue') self.ax.legend(loc='upper right', fontsize=7) self.fig.tight_layout(pad=1.0) def _get_data(self): if self.resource_type == 'cpu': return psutil.cpu_percent(interval=0.1) elif self.resource_type == 'memory': return psutil.virtual_memory().percent elif self.resource_type == 'disk': return psutil.disk_usage('/').percent elif self.resource_type == 'net': current_net_io = psutil.net_io_counters() time_diff = self.update_interval / 1000.0 bytes_sent = current_net_io.bytes_sent - self._last_net_io.bytes_sent bytes_recv = current_net_io.bytes_recv - self._last_net_io.bytes_recv kbs_sent = (bytes_sent / 1024) / time_diff kbs_recv = (bytes_recv / 1024) / time_diff self._last_net_io = current_net_io return kbs_sent, kbs_recv return 0 def _data_collection_loop(self): while not self.stop_event.is_set(): data_point = self._get_data() if self.resource_type == 'net': kbs_sent, kbs_recv = data_point self.data_history_sent.append(kbs_sent) self.data_history_recv.append(kbs_recv) else: self.data_history.append(data_point) self.master.after(0, self._update_gui) time.sleep(self.update_interval / 1000.0) def _update_gui(self): if self.resource_type == 'net': self.line_sent.set_ydata(self.data_history_sent) self.line_recv.set_ydata(self.data_history_recv) latest_sent = self.data_history_sent[-1] latest_recv = self.data_history_recv[-1] self.ax.set_title(f"Tasa de Red: S: {latest_sent:.1f} Kb/s, R: {latest_recv:.1f} Kb/s", fontsize=10) else: self.line.set_ydata(self.data_history) latest_value = self.data_history[-1] self.ax.set_title(f"Uso de {self.resource_type.upper()}: {latest_value:.1f}%", fontsize=10) self.canvas.draw_idle() def stop(self): self.stop_event.set() # --- GESTOR DE HILO DE RED --- class NetworkTrafficThread: """Hilo que monitorea y actualiza el tráfico total de red.""" def __init__(self, root, label_in, label_out): self.root = root self.label_in = label_in self.label_out = label_out self.update_interval = 1000 # 1 segundo self.stop_event = threading.Event() self.initial_net_io = psutil.net_io_counters() self.thread = threading.Thread(target=self._monitor_loop, daemon=True) self.thread.start() def _monitor_loop(self): while not self.stop_event.is_set(): current_net_io = psutil.net_io_counters() bytes_sent_total = current_net_io.bytes_sent - self.initial_net_io.bytes_sent bytes_recv_total = current_net_io.bytes_recv - self.initial_net_io.bytes_recv kb_sent_total = bytes_sent_total / 1024 kb_recv_total = bytes_recv_total / 1024 self.root.after(0, self._update_gui, kb_sent_total, kb_recv_total) time.sleep(self.update_interval / 1000.0) def _update_gui(self, kb_sent, kb_recv): self.label_in.config(text=f"KB Recibidos (Input): {kb_recv:,.2f} KB") self.label_out.config(text=f"KB Enviados (Output): {kb_sent:,.2f} KB") def stop(self): self.stop_event.set() # --- FUNCIONES DE MÚSICA --- def initialize_music(): """Carga el archivo de música. Llamar una vez al inicio o al cambiar a T2.""" global MUSIC_AVAILABLE # Solo intentamos cargar si Pygame está disponible y si no hay música ya cargada if MUSIC_AVAILABLE and not pygame.mixer.music.get_busy() and pygame.mixer.get_init(): try: pygame.mixer.music.load(MUSIC_FILE) status_manager.set_status("status_3", f"Música cargada: {MUSIC_FILE}", "purple") except pygame.error as e: messagebox.showerror("Error de Audio", f"No se pudo cargar el archivo '{MUSIC_FILE}'. Asegúrate de que existe y es compatible. Error: {e}") MUSIC_AVAILABLE = False status_manager.set_status("status_3", "Error al cargar música", "magenta") def toggle_music(button): """Inicia o detiene la reproducción de música en bucle.""" global music_state, MUSIC_AVAILABLE if not MUSIC_AVAILABLE: messagebox.showwarning("Advertencia", "La funcionalidad de música no está disponible (Pygame o archivo no encontrado).") return if not music_state: # Asegurarse de que el archivo esté cargado antes de intentar reproducir if pygame.mixer.music.get_pos() == 0 and not pygame.mixer.music.get_busy(): initialize_music() if not MUSIC_AVAILABLE: return # 1. Iniciar la música try: # -1 para reproducir en bucle (infinitamente) pygame.mixer.music.play(-1) music_state = True button.config(text="STOP MÚSICA (En Bucle)", style="Red.TButton") status_manager.set_status("status_3", "Música sonando en bucle", "purple") except pygame.error as e: messagebox.showerror("Error de Reproducción", f"Error al reproducir: {e}") MUSIC_AVAILABLE = False else: # 2. Detener la música pygame.mixer.music.stop() music_state = False button.config(text="MÚSICA (Play)", style="Green.TButton") status_manager.set_status("status_3", "Música detenida", "cyan") def stop_music_clean(): """Detiene la música de forma limpia al salir de la app o cambiar de pestaña.""" global music_state, MUSIC_AVAILABLE if MUSIC_AVAILABLE and music_state: pygame.mixer.music.stop() music_state = False # --- FIN DE FUNCIONES DE MÚSICA --- # --- FUNCIONES PARA LA PESTAÑA T2: SCRAPING (Implementación Books To Scrape) --- def run_simple_scraper(label_resultado): """ Inicia el scraping de la página de práctica de libros (books.toscrape.com). """ URL = "http://books.toscrape.com/" status_manager.set_status("status_3", "Iniciando scraping de libros...", "orange") label_resultado.config(text="Buscando títulos...", fg="orange") # 2. Hilo para no bloquear la GUI threading.Thread(target=_scrape_thread, args=(URL, label_resultado), daemon=True).start() def _scrape_thread(URL, label_resultado): """Ejecuta la lógica de scraping de libros dentro de un hilo.""" global root, status_manager # User-Agent no es estrictamente necesario aquí, pero es una buena práctica USER_AGENT = 'Mozilla/5.0' req = urllib.request.Request(URL, headers={'User-Agent': USER_AGENT}) try: # 1. Obtener la página with urllib.request.urlopen(req, timeout=10) as response: html = response.read().decode('utf-8') # 2. Buscamos todos los títulos de libros. # El patrón busca la etiqueta HTML: title="[TÍTULO DEL LIBRO]" title_matches = re.findall(r'title="(.*?)"', html) # Filtramos los títulos que no son de libros, buscando títulos largos # Esto es una heurística; el primer libro tiene un título corto ("A Light in the Attic") book_titles = [t for t in title_matches if len(t) > 30 and t != "A Light in the Attic"] if book_titles: resultado_text = "✅ **Títulos de Libros Encontrados (books.toscrape.com):**\n\n" for i, title in enumerate(book_titles[:20]): # Limitar a 20 resultados resultado_text += f"[{i+1:02d}] {title}\n" color = "green" status_text = f"Scraping OK: {len(book_titles)} títulos encontrados" else: resultado_text = "⚠️ Advertencia: No se pudo encontrar el patrón o el HTML ha cambiado." color = "blue" status_text = "Scraping parcial/patrón fallido" except urllib.error.HTTPError as e: resultado_text = f"❌ Error HTTP: El servidor denegó el acceso (Error {e.code})." color = "red" status_text = "Error HTTP (4xx/5xx)" except Exception as e: resultado_text = f"❌ Error de Conexión/Scraping: {e}" color = "red" status_text = "Error de Red/Otro" # 4. Actualizar la GUI desde el hilo principal root.after(0, label_resultado.config, {"text": resultado_text, "fg": color, "font": ("Consolas", 10)}) status_manager.set_status("status_3", status_text, color) def setup_scraping_tab(tab_frame): """Configura la interfaz para la pestaña de Scraping.""" for widget in tab_frame.winfo_children(): widget.destroy() main_frame = tk.Frame(tab_frame) main_frame.pack(expand=True, padx=20, pady=20, fill="both") tk.Label(main_frame, text="Web Scraping de Práctica (Books to Scrape)", font=("Arial", 16, "bold")).pack(pady=10) tk.Label( main_frame, text="Esto ejecuta un hilo que descarga la página de prueba 'books.toscrape.com' y extrae los 20 primeros títulos.", wraplength=500, justify="left" ).pack(pady=5) # Etiqueta para mostrar el resultado label_resultado = tk.Label( main_frame, text="Pulse el botón 'Scrapear' para comenzar...", font=("Consolas", 10), fg="gray", justify="left" ) label_resultado.pack(pady=20, padx=10) # Botón de ejecución ttk.Button( main_frame, text="SCRAPEAR LIBROS DE PRÁCTICA", style="Green.TButton", command=lambda: run_simple_scraper(label_resultado) ).pack(pady=15) # --- FIN DE FUNCIONES DE SCRAPING --- # --- FUNCIONES DE LA GUI GENERAL --- def update_time(status_bar, stop_event): """Bucle que actualiza la fecha y hora en la barra de estado.""" while not stop_event.is_set(): now = datetime.datetime.now() day_of_week = now.strftime("%A") time_str = now.strftime("%H:%M:%S") date_str = now.strftime("%Y-%m-%d") label_text = f"{day_of_week}, {date_str} - {time_str}" status_bar.after(1000, status_bar.config, {"text": label_text}) time.sleep(1) def stop_status_bar_time_thread(): """Detiene el hilo de la hora de la barra de estado.""" global status_bar_time_thread if status_bar_time_thread and hasattr(status_bar_time_thread, 'stop_event'): status_bar_time_thread.stop_event.set() status_bar_time_thread = None def open_external_url(url): status_manager.set_status("status_2", f"Abriendo URL...", "orange") webbrowser.open(url) root.after(2000, lambda: status_manager.set_status("status_2", "URL lanzada.", "blue")) def run_bash_backup(): def execute_script(): status_manager.set_status("status_3", "Copia en curso (Bash)...", "red") script_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "backup_script.sh") command = [script_path] try: subprocess.run(command, capture_output=True, text=True, check=True) status_manager.set_status("status_3", "Copia OK: ¡Éxito!", "lime") except subprocess.CalledProcessError as e: status_manager.set_status("status_3", f"Copia ¡FALLO! Código: {e.returncode}", "magenta") except FileNotFoundError: status_manager.set_status("status_3", "Error: Script no encontrado", "black") root.after(5000, lambda: status_manager.set_status("status_3", "Esperando tarea", "cyan")) threading.Thread(target=execute_script, daemon=True).start() def save_text_file(text_widget): text_content = text_widget.get("1.0", "end-1c") file_path = filedialog.asksaveasfilename( defaultextension=".txt", filetypes=[ ("Archivos de Texto", "*.txt"), ("Todos los archivos", "*.*") ], title="Guardar documento del editor" ) if file_path: try: with open(file_path, "w", encoding="utf-8") as file: file.write(text_content) status_manager.set_status("status_3", f"Archivo guardado: {os.path.basename(file_path)}", "lime") root.after(3000, lambda: status_manager.set_status("status_3", "Estado 3", "cyan")) except Exception as e: status_manager.set_status("status_3", f"Error al guardar: {e}", "magenta") root.after(3000, lambda: status_manager.set_status("status_3", "Estado 3", "cyan")) # --- CONTROL DE HILOS Y PESTAÑAS T1 --- class SystemMonitor: def __init__(self, master, resource_type, width=4, height=3): self.master = master self.resource_type = resource_type self.update_interval = 1000 if resource_type == 'net': self.data_history_sent = collections.deque([0] * 30, maxlen=30) self.data_history_recv = collections.deque([0] * 30, maxlen=30) self._last_net_io = psutil.net_io_counters() else: self.data_history = collections.deque([0] * 30, maxlen=30) self.fig, self.ax = plt.subplots(figsize=(width, height), dpi=100) self.canvas = FigureCanvasTkAgg(self.fig, master=master) self.canvas_widget = self.canvas.get_tk_widget() self.canvas_widget.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) self.setup_plot() self.stop_event = threading.Event() self.monitor_thread = threading.Thread(target=self._data_collection_loop, daemon=True) self.monitor_thread.start() def setup_plot(self): title_map = {'cpu': "Uso de CPU", 'memory': "Uso de RAM", 'disk': "Uso de Disco (I/O)", 'net': "Tasa de Red (Kb/s)"} ylabel_map = {'cpu': "% Uso", 'memory': "% Uso", 'disk': "% Disco", 'net': "Kb/s"} self.ax.set_title(title_map.get(self.resource_type, "Recurso"), fontsize=10) self.ax.set_ylim(0, 100 if self.resource_type != 'net' else 500) self.ax.set_ylabel(ylabel_map.get(self.resource_type, "Valor"), fontsize=8) self.ax.set_xlabel("Tiempo (s)", fontsize=8) self.ax.tick_params(axis='both', which='major', labelsize=7) if self.resource_type == 'net': self.line_sent, = self.ax.plot(self.data_history_sent, label='Enviado (Kb)', color='red') self.line_recv, = self.ax.plot(self.data_history_recv, label='Recibido (Kb)', color='blue') self.ax.legend(loc='upper right', fontsize=7) else: self.line, = self.ax.plot(self.data_history, label=f'{self.resource_type.upper()} %', color='blue') self.ax.legend(loc='upper right', fontsize=7) self.fig.tight_layout(pad=1.0) def _get_data(self): if self.resource_type == 'cpu': return psutil.cpu_percent(interval=0.1) elif self.resource_type == 'memory': return psutil.virtual_memory().percent elif self.resource_type == 'disk': return psutil.disk_usage('/').percent elif self.resource_type == 'net': current_net_io = psutil.net_io_counters() time_diff = self.update_interval / 1000.0 bytes_sent = current_net_io.bytes_sent - self._last_net_io.bytes_sent bytes_recv = current_net_io.bytes_recv - self._last_net_io.bytes_recv kbs_sent = (bytes_sent / 1024) / time_diff kbs_recv = (bytes_recv / 1024) / time_diff self._last_net_io = current_net_io return kbs_sent, kbs_recv return 0 def _data_collection_loop(self): while not self.stop_event.is_set(): data_point = self._get_data() if self.resource_type == 'net': kbs_sent, kbs_recv = data_point self.data_history_sent.append(kbs_sent) self.data_history_recv.append(kbs_recv) else: self.data_history.append(data_point) self.master.after(0, self._update_gui) time.sleep(self.update_interval / 1000.0) def _update_gui(self): if self.resource_type == 'net': self.line_sent.set_ydata(self.data_history_sent) self.line_recv.set_ydata(self.data_history_recv) latest_sent = self.data_history_sent[-1] latest_recv = self.data_history_recv[-1] self.ax.set_title(f"Tasa de Red: S: {latest_sent:.1f} Kb/s, R: {latest_recv:.1f} Kb/s", fontsize=10) else: self.line.set_ydata(self.data_history) latest_value = self.data_history[-1] self.ax.set_title(f"Uso de {self.resource_type.upper()}: {latest_value:.1f}%", fontsize=10) self.canvas.draw_idle() def stop(self): self.stop_event.set() class NetworkTrafficThread: """Hilo que monitorea y actualiza el tráfico total de red.""" def __init__(self, root, label_in, label_out): self.root = root self.label_in = label_in self.label_out = label_out self.update_interval = 1000 # 1 segundo self.stop_event = threading.Event() self.initial_net_io = psutil.net_io_counters() self.thread = threading.Thread(target=self._monitor_loop, daemon=True) self.thread.start() def _monitor_loop(self): while not self.stop_event.is_set(): current_net_io = psutil.net_io_counters() bytes_sent_total = current_net_io.bytes_sent - self.initial_net_io.bytes_sent bytes_recv_total = current_net_io.bytes_recv - self.initial_net_io.bytes_recv kb_sent_total = bytes_sent_total / 1024 kb_recv_total = bytes_recv_total / 1024 self.root.after(0, self._update_gui, kb_sent_total, kb_recv_total) time.sleep(self.update_interval / 1000.0) def _update_gui(self, kb_sent, kb_recv): self.label_in.config(text=f"KB Recibidos (Input): {kb_recv:,.2f} KB") self.label_out.config(text=f"KB Enviados (Output): {kb_sent:,.2f} KB") def stop(self): self.stop_event.set() def setup_monitors_tab(tab_frame): tk.Label(tab_frame, text="Monitorización de Recursos del Sistema", font=("Arial", 14, "bold")).pack(pady=10) monitor_grid_frame = tk.Frame(tab_frame) monitor_grid_frame.pack(fill="both", expand=True, padx=10, pady=10) monitor_grid_frame.columnconfigure(0, weight=1) monitor_grid_frame.columnconfigure(1, weight=1) monitor_grid_frame.rowconfigure(0, weight=1) monitor_grid_frame.rowconfigure(1, weight=1) global cpu_monitor, memory_monitor, disk_monitor, net_monitor cpu_frame = tk.Frame(monitor_grid_frame, bd=1, relief="solid") cpu_frame.grid(row=0, column=0, sticky="nsew", padx=5, pady=5) cpu_monitor = SystemMonitor(cpu_frame, 'cpu') memory_frame = tk.Frame(monitor_grid_frame, bd=1, relief="solid") memory_frame.grid(row=0, column=1, sticky="nsew", padx=5, pady=5) memory_monitor = SystemMonitor(memory_frame, 'memory') disk_frame = tk.Frame(monitor_grid_frame, bd=1, relief="solid") disk_frame.grid(row=1, column=0, sticky="nsew", padx=5, pady=5) disk_monitor = SystemMonitor(disk_frame, 'disk') net_frame = tk.Frame(monitor_grid_frame, bd=1, relief="solid") net_frame.grid(row=1, column=1, sticky="nsew", padx=5, pady=5) net_monitor = SystemMonitor(net_frame, 'net') def setup_editor_tab(tab_frame): tk.Label(tab_frame, text="Editor de Texto Simple", font=("Arial", 14, "bold")).pack(pady=10) editor_frame = tk.Frame(tab_frame) editor_frame.pack(fill="both", expand=True, padx=10, pady=10) editor_scrollbar = ttk.Scrollbar(editor_frame) editor_scrollbar.pack(side="right", fill="y") text_area = tk.Text( editor_frame, wrap="word", undo=True, font=("Consolas", 11), yscrollcommand=editor_scrollbar.set ) text_area.pack(fill="both", expand=True) editor_scrollbar.config(command=text_area.yview) ttk.Button( tab_frame, text="Guardar Documento", command=lambda: save_text_file(text_area) ).pack(pady=5) def setup_network_thread_tab(tab_frame): """Configura la pestaña 'Hilo' con el contador de tráfico de red, centrado y ampliado.""" global network_thread center_frame = tk.Frame(tab_frame) center_frame.pack(expand=True) tk.Label( center_frame, text="Monitorización Total de Tráfico de Red (KB)", font=("Arial", 18, "bold") ).pack(pady=20) label_in = tk.Label( center_frame, text="KB Recibidos (Input): -- KB", font=("Consolas", 20, "bold"), fg="blue" ) label_in.pack(pady=15, padx=50) label_out = tk.Label( center_frame, text="KB Enviados (Output): -- KB", font=("Consolas", 20, "bold"), fg="red" ) label_out.pack(pady=15, padx=50) tk.Label( center_frame, text="El conteo se reinicia al navegar fuera de la pestaña T1.", fg="gray", font=("Arial", 10) ).pack(pady=30) if not network_thread: network_thread = NetworkTrafficThread(root, label_in, label_out) def stop_network_thread(): global network_thread if 'network_thread' in globals() and network_thread: network_thread.stop() network_thread = None def setup_t1_processes(tab_frame, root_instance): """Configura el Notebook interno para la pestaña T1. Procesos.""" for widget in tab_frame.winfo_children(): widget.destroy() internal_notebook = ttk.Notebook(tab_frame) internal_notebook.pack(fill="both", expand=True, padx=5, pady=5) estado_tab = ttk.Frame(internal_notebook) internal_notebook.add(estado_tab, text="Estado") setup_monitors_tab(estado_tab) editor_tab = ttk.Frame(internal_notebook) internal_notebook.add(editor_tab, text="Editor de texto") setup_editor_tab(editor_tab) hilo_tab = ttk.Frame(internal_notebook) internal_notebook.add(hilo_tab, text="Hilo") def handle_internal_tab_change(event): selected_index = internal_notebook.index(internal_notebook.select()) current_tab_title = internal_notebook.tab(selected_index, "text") if internal_notebook.tab(0, "text") != "Estado": stop_monitors() if current_tab_title == "Hilo": if not network_thread: setup_network_thread_tab(hilo_tab) else: stop_network_thread() internal_notebook.bind("<>", handle_internal_tab_change) def stop_monitors(): """Detiene todos los monitores y el hilo de red de T1.""" global cpu_monitor, memory_monitor, disk_monitor, net_monitor if 'cpu_monitor' in globals() and cpu_monitor: cpu_monitor.stop() cpu_monitor = None if 'memory_monitor' in globals() and memory_monitor: memory_monitor.stop() memory_monitor = None if 'disk_monitor' in globals() and disk_monitor: disk_monitor.stop() disk_monitor = None if 'net_monitor' in globals() and net_monitor: net_monitor.stop() net_monitor = None stop_network_thread() # --- FUNCIONES PARA LA PESTAÑA T2: RELOJ Y ALARMA --- def stop_clock_thread(): """Detiene el hilo del reloj digital y el hilo de la alarma.""" global clock_thread if 'clock_thread' in globals() and clock_thread and hasattr(clock_thread, 'stop_event'): clock_thread.stop_event.set() clock_thread = None stop_alarm_countdown() def stop_alarm_countdown(): """Detiene la cuenta atrás de la alarma si está activa.""" global countdown_after_id, alarm_stop_event alarm_stop_event.set() if countdown_after_id: root.after_cancel(countdown_after_id) countdown_after_id = None def update_digital_clock(label_clock, stop_event): """Función de bucle para actualizar la hora digital en un hilo.""" while not stop_event.is_set(): now = datetime.datetime.now() time_str = now.strftime("%H:%M:%S") date_str = now.strftime("%Y-%m-%d") display_text = f"{date_str}\n{time_str}" root.after(0, label_clock.config, {"text": display_text}) time.sleep(1) def play_alarm_sound(): """Produce una notificación sonora.""" global MUSIC_AVAILABLE if MUSIC_AVAILABLE: # Detenemos la música de fondo para que la alarma se escuche clara pygame.mixer.music.stop() try: # Cargamos y reproducimos el archivo de alarma una sola vez alarm_sound = pygame.mixer.Sound(ALARM_FILE) alarm_sound.play() except pygame.error as e: messagebox.showerror("Error de Audio", f"No se pudo reproducir {ALARM_FILE}. Error: {e}") if WINSOUND_AVAILABLE: # Fallback de Windows si Pygame no funciona bien winsound.Beep(1000, 1000) winsound.MessageBeep(winsound.MB_ICONEXCLAMATION) else: print("\n*** ALARMA DISPARADA (Sin sonido de sistema) ***\n") def alarm_loop(wait_seconds, alarm_time_str, label_status): """Hilo de la alarma que espera y se dispara.""" global alarm_stop_event time.sleep(wait_seconds) if alarm_stop_event.is_set(): return root.after(0, play_alarm_sound) root.after(0, lambda: messagebox.showinfo("ALARMA", f"¡ALARMA TERMINADA! Son las {alarm_time_str}")) root.after(0, lambda: label_status.config(text="ALARMA TERMINADA", fg="red")) root.after(0, stop_alarm_countdown) def start_alarm_thread(alarm_time_str, label_status, label_countdown): """Calcula el tiempo de espera, inicia el hilo de alarma y la cuenta atrás.""" global alarm_stop_event try: alarm_hour, alarm_minute = map(int, alarm_time_str.split(':')) if not (0 <= alarm_hour <= 23 and 0 <= alarm_minute <= 59): raise ValueError("Hora fuera de rango.") except ValueError: messagebox.showerror("Error de Formato", "Por favor, introduce la hora en formato HH:MM (ej: 07:30)") return now = datetime.datetime.now() alarm_dt = now.replace(hour=alarm_hour, minute=alarm_minute, second=0, microsecond=0) if alarm_dt <= now: alarm_dt += datetime.timedelta(days=1) time_difference = alarm_dt - now wait_seconds = time_difference.total_seconds() stop_alarm_countdown() alarm_stop_event.clear() label_status.config(text=f"ALARMA ACTIVA: {alarm_time_str} (Hacia adelante)", fg="orange") update_countdown_label(alarm_dt, label_countdown, label_status) threading.Thread(target=alarm_loop, args=(wait_seconds, alarm_time_str, label_status), daemon=True).start() def update_countdown_label(alarm_dt, label_countdown, label_status): """Actualiza la etiqueta de cuenta atrás cada segundo.""" global countdown_after_id, alarm_stop_event now = datetime.datetime.now() if alarm_stop_event.is_set() or alarm_dt <= now: label_countdown.config(text="00:00:00") label_status.config(text="ALARMA INACTIVA", fg="gray") return time_remaining = alarm_dt - now total_seconds = int(time_remaining.total_seconds()) hours, remainder = divmod(total_seconds, 3600) minutes, seconds = divmod(remainder, 60) countdown_str = f"{hours:02d}:{minutes:02d}:{seconds:02d}" label_countdown.config(text=countdown_str) countdown_after_id = root.after(1000, lambda: update_countdown_label(alarm_dt, label_countdown, label_status)) def set_alarm(entry_hour, label_status, label_countdown): """Recoge la hora de entrada y lanza el hilo de alarma.""" alarm_time = entry_hour.get() if alarm_time: start_alarm_thread(alarm_time, label_status, label_countdown) entry_hour.delete(0, tk.END) def setup_clock_tab(tab_frame): """Configura la interfaz de la subpestaña Reloj/Alarma.""" for widget in tab_frame.winfo_children(): widget.destroy() center_frame = tk.Frame(tab_frame) center_frame.pack(expand=True, pady=20) tk.Label(center_frame, text="Reloj Digital y Alarma", font=("Arial", 16, "bold")).pack(pady=10) label_clock = tk.Label( center_frame, text="00:00:00", font=("Consolas", 60, "bold"), bg="black", fg="#00FF00", relief="sunken", padx=20, pady=10 ) label_clock.pack(pady=20) global clock_thread stop_clock_thread() stop_event = threading.Event() clock_thread = threading.Thread( target=update_digital_clock, args=(label_clock, stop_event), daemon=True ) clock_thread.stop_event = stop_event clock_thread.start() alarm_frame = ttk.LabelFrame(center_frame, text="Configurar Alarma", padding=10) alarm_frame.pack(pady=30, padx=20, fill="x") tk.Label(alarm_frame, text="Hora (HH:MM):", font=("Arial", 12)).pack(side="left", padx=5) entry_hour = ttk.Entry(alarm_frame, width=8, font=("Arial", 12)) entry_hour.pack(side="left", padx=5) label_alarm_status = tk.Label(alarm_frame, text="ALARMA INACTIVA", fg="gray", font=("Arial", 12, "italic")) label_alarm_status.pack(side="right", padx=15) label_countdown = tk.Label(alarm_frame, text="00:00:00", fg="red", font=("Consolas", 14, "bold")) label_countdown.pack(side="right", padx=10) ttk.Button( alarm_frame, text="Establecer Alarma", command=lambda: set_alarm(entry_hour, label_alarm_status, label_countdown) ).pack(side="right", padx=10) ttk.Button( alarm_frame, text="Cancelar", command=lambda: stop_alarm_countdown() or label_alarm_status.config(text="ALARMA CANCELADA", fg="gray") ).pack(side="right", padx=10) # --- FUNCIONES PARA LA PESTAÑA T2: COCHES --- def stop_race(): """Detiene la carrera actual, si está en curso.""" global race_stop_event, race_threads, winner if not race_stop_event.is_set(): race_stop_event.set() for thread in race_threads: if thread.is_alive(): pass race_threads = [] winner = None def car_movement(canvas, car_id, car_color, label_result, start_x, goal_x): """Hilo que mueve un coche de color.""" global winner, race_lock current_x = start_x delay = random.randint(10, 50) / 1000.0 step = 5 while current_x < goal_x and not race_stop_event.is_set(): current_x += step canvas.after(0, canvas.coords, car_id, current_x, canvas.coords(car_id)[1], current_x + 50, canvas.coords(car_id)[3]) time.sleep(delay) if not race_stop_event.is_set(): with race_lock: if winner is None: winner = car_color race_stop_event.set() root.after(0, label_result.config, {"text": f"🎉 ¡Ha ganado el coche {car_color.upper()}! 🎉", "fg": car_color, "font": ("Arial", 18, "bold")}) status_manager.set_status("status_3", f"Ganador: {car_color}", car_color) else: pass def start_race(canvas, cars, label_result): """Inicia la carrera, lanzando un hilo para cada coche.""" stop_race() global race_threads, winner, race_stop_event winner = None race_stop_event.clear() label_result.config(text="¡CARRERA EN CURSO!", fg="black", font=("Arial", 14, "italic")) START_X = 10 GOAL_X = canvas.winfo_width() - 60 for car_color, car_id in cars: canvas.coords(car_id, START_X, canvas.coords(car_id)[1], START_X + 50, canvas.coords(car_id)[3]) thread = threading.Thread( target=car_movement, args=(canvas, car_id, car_color, label_result, START_X, GOAL_X), daemon=True ) race_threads.append(thread) thread.start() status_manager.set_status("status_3", "Carrera iniciada...", "yellow") def setup_race_game(tab_frame): """Configura la interfaz del juego de la carrera de coches.""" for widget in tab_frame.winfo_children(): widget.destroy() control_frame = tk.Frame(tab_frame) control_frame.pack(pady=10, fill="x") tk.Label(control_frame, text="Simulación de Carrera Multihilo (Coches)", font=("Arial", 16, "bold")).pack(side="left", padx=15) label_result = tk.Label(control_frame, text="Presiona 'START' para comenzar", fg="black", font=("Arial", 14)) label_result.pack(side="right", padx=15) canvas_frame = tk.Frame(tab_frame, bd=2, relief="sunken") canvas_frame.pack(fill="both", expand=True, padx=10, pady=5) canvas = tk.Canvas(canvas_frame, bg="lightgray") canvas.pack(fill="both", expand=True) CARS_CONFIG = [ ("red", 50), ("blue", 150), ("green", 250) ] cars_data = [] def draw_cars_and_goal(): canvas_width = canvas.winfo_width() if canvas_width < 100: root.after(100, draw_cars_and_goal) return GOAL_X = canvas_width - 50 canvas.create_line(GOAL_X, 0, GOAL_X, canvas.winfo_height(), width=3, fill="black", dash=(5, 5)) canvas.create_text(GOAL_X - 20, 15, text="META", fill="black") for color, y_pos in CARS_CONFIG: car_id = canvas.create_rectangle(10, y_pos, 60, y_pos + 50, fill=color, outline="black") cars_data.append((color, car_id)) canvas.bind("", lambda event: draw_cars_and_goal() if not cars_data else None) button_frame = tk.Frame(tab_frame) button_frame.pack(pady=10) ttk.Button( button_frame, text="START RACE (Hilos)", command=lambda: start_race(canvas, cars_data, label_result) ).pack(side="left", padx=10) ttk.Button( button_frame, text="STOP/RESET", command=lambda: stop_race() or label_result.config(text="Carrera detenida y reiniciada.", fg="gray") ).pack(side="left", padx=10) def setup_t2_threads(tab_frame): """Configura el Notebook interno para la pestaña T2. Threads.""" for widget in tab_frame.winfo_children(): widget.destroy() internal_notebook = ttk.Notebook(tab_frame) internal_notebook.pack(fill="both", expand=True, padx=5, pady=5) clock_tab = ttk.Frame(internal_notebook) internal_notebook.add(clock_tab, text="Reloj / Alarma") race_tab = ttk.Frame(internal_notebook) internal_notebook.add(race_tab, text="Coches") # --- PESTAÑA DE SCRAPING --- scraping_tab = ttk.Frame(internal_notebook) internal_notebook.add(scraping_tab, text="Scraping") # --------------------------------- def handle_internal_tab_change_t2(event): selected_index = internal_notebook.index(internal_notebook.select()) current_tab_title = internal_notebook.tab(selected_index, "text") # Detener hilos al cambiar de pestaña if current_tab_title != "Reloj / Alarma": stop_clock_thread() else: setup_clock_tab(clock_tab) if current_tab_title != "Coches": stop_race() else: setup_race_game(race_tab) if current_tab_title == "Scraping": setup_scraping_tab(scraping_tab) internal_notebook.bind("<>", handle_internal_tab_change_t2) # Configuración inicial de todas las pestañas setup_clock_tab(clock_tab) setup_race_game(race_tab) setup_scraping_tab(scraping_tab) # --- FUNCIONES PARA LA PESTAÑA T3: CHAT IPC (subprocess + ficheros de cola) --- # # Arquitectura: # - Proyecto.py crea dos ficheros temporales: cola_a.txt y cola_b.txt # - Lanza dos subprocesos de chat_client.py, pasando los paths por argv # - Cliente A escribe en cola_a.txt y lee de cola_b.txt (y viceversa) # - Cada cliente hace polling del fichero de entrada cada 300ms # - No hay multiprocessing, no hay semáforos, no hay re-importación del proyecto def launch_chat_windows(): """Crea los ficheros de cola temporales y lanza los dos clientes de chat.""" import tempfile import sys # --- Localizar chat_client.py de forma robusta --- candidates = [] try: candidates.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), "chat_client.py")) except Exception: pass candidates.append(os.path.join(os.getcwd(), "chat_client.py")) client_script = None for path in candidates: if os.path.isfile(path): client_script = path break if not client_script: messagebox.showerror( "Error - Chat", "No se encontro chat_client.py.\n\n" "Asegurate de que esta en la misma carpeta que Proyecto.py.\n\n" "Buscado en:\n" + "\n".join(candidates) ) return # --- Crear ficheros de cola temporales --- tmp_dir = tempfile.mkdtemp(prefix="chat_ipc_") cola_a = os.path.join(tmp_dir, "cola_a.txt") cola_b = os.path.join(tmp_dir, "cola_b.txt") open(cola_a, "w").close() open(cola_b, "w").close() # Buscar el intérprete Python correcto (el del sistema, con tkinter) import shutil python = None for candidate in [sys.executable, "python3", "python"]: path = shutil.which(candidate) if not os.path.isabs(candidate) else candidate if path and os.path.isfile(path): python = path break if not python: messagebox.showerror("Error - Chat", "No se encontró un intérprete Python válido.") return log_a = os.path.join(tmp_dir, "chat_a.log") log_b = os.path.join(tmp_dir, "chat_b.log") # Heredar el entorno completo (incluye DISPLAY, XAUTHORITY, etc.) import copy env = copy.copy(os.environ) try: subprocess.Popen( [python, client_script, "Cliente A", "Cliente B", cola_a, cola_b, "100", "150"], stdout=open(log_a, "w"), stderr=open(log_a, "w"), env=env ) subprocess.Popen( [python, client_script, "Cliente B", "Cliente A", cola_b, cola_a, "560", "150"], stdout=open(log_b, "w"), stderr=open(log_b, "w"), env=env ) # Esperar 1.5s y comprobar si los procesos arrancaron correctamente def check_logs(): for log_path, nombre in [(log_a, "Cliente A"), (log_b, "Cliente B")]: try: with open(log_path, "r") as lf: contenido = lf.read().strip() if contenido: messagebox.showerror( f"Error - {nombre}", f"El proceso de chat falló al arrancar:\n\n{contenido[:500]}" ) return except Exception: pass status_manager.set_status("status_3", "Chat IPC activo", "#25D366") root.after(1500, check_logs) except Exception as e: messagebox.showerror("Error - Chat", f"No se pudieron lanzar los clientes de chat:\n{e}") # ── GESTOR DE CORREO ──────────────────────────────────────────────────────── import imaplib import poplib import smtplib import email from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart from email.header import decode_header MAIL_SERVER = "10.10.0.101" SMTP_PORT = 25 IMAP_PORT = 143 POP_PORT = 110 # Estado de sesión de correo mail_session = { "user": None, "password": None, "connected": False, } def decode_str(value): """Decodifica cabeceras de correo que pueden venir en base64 u otros encodings.""" if value is None: return "" parts = decode_header(value) result = [] for part, enc in parts: if isinstance(part, bytes): result.append(part.decode(enc or "utf-8", errors="replace")) else: result.append(str(part)) return " ".join(result) def fetch_inbox_imap(user, password): """Devuelve lista de dicts con los correos de la bandeja de entrada via IMAP.""" msgs = [] conn = None try: conn = imaplib.IMAP4(MAIL_SERVER, IMAP_PORT) conn.login(user, password) conn.select("INBOX") _, data = conn.search(None, "ALL") ids = data[0].split()[-20:] # últimos 20 for uid in reversed(ids): _, msg_data = conn.fetch(uid, "(RFC822)") raw = msg_data[0][1] msg = email.message_from_bytes(raw) body = "" if msg.is_multipart(): for part in msg.walk(): if part.get_content_type() == "text/plain": body = part.get_payload(decode=True).decode("utf-8", errors="replace") break else: body = msg.get_payload(decode=True).decode("utf-8", errors="replace") msgs.append({ "uid": uid.decode(), "from": decode_str(msg.get("From", "")), "subject": decode_str(msg.get("Subject", "(sin asunto)")), "date": decode_str(msg.get("Date", "")), "body": body, }) except Exception as e: raise RuntimeError(f"IMAP error: {e}") finally: # Cierre blindado: pase lo que pase, la conexión se cierra siempre if conn is not None: try: conn.logout() except Exception: try: conn.shutdown() except Exception: pass return msgs def send_smtp(user, password, to_addr, subject, body): """Envía un correo via SMTP (servidor interno sin autenticación).""" msg = MIMEMultipart() msg["From"] = user msg["To"] = to_addr msg["Subject"] = subject msg.attach(MIMEText(body, "plain", "utf-8")) try: with smtplib.SMTP(MAIL_SERVER, SMTP_PORT, timeout=10) as s: s.ehlo() # Servidor interno de clase: no requiere autenticación s.sendmail(user, [to_addr], msg.as_string()) except Exception as e: raise RuntimeError(f"SMTP error: {e}") def setup_mail_compose(parent_frame, to_addr="", subject="", body=""): """Muestra el formulario de redacción de correo dentro de parent_frame.""" for w in parent_frame.winfo_children(): w.destroy() parent_frame.grid_rowconfigure(0, weight=0) parent_frame.grid_rowconfigure(1, weight=1) parent_frame.grid_columnconfigure(0, weight=1) # Cabecera del formulario hdr = tk.Frame(parent_frame, bg="#2C3E50", pady=8) hdr.grid(row=0, column=0, sticky="ew") tk.Label(hdr, text="✉ Nuevo correo", bg="#2C3E50", fg="white", font=("Segoe UI", 12, "bold"), padx=12).pack(side="left") form = tk.Frame(parent_frame, bg="white", padx=15, pady=10) form.grid(row=1, column=0, sticky="nsew") form.grid_columnconfigure(1, weight=1) fields = [("Para:", to_addr), ("Asunto:", subject)] entries = {} for i, (lbl, val) in enumerate(fields): tk.Label(form, text=lbl, bg="white", font=("Segoe UI", 10, "bold"), anchor="w").grid(row=i, column=0, sticky="w", pady=4, padx=(0,8)) e = ttk.Entry(form, font=("Segoe UI", 10)) e.grid(row=i, column=1, sticky="ew", pady=4) e.insert(0, val) entries[lbl] = e tk.Label(form, text="Mensaje:", bg="white", font=("Segoe UI", 10, "bold"), anchor="w").grid( row=2, column=0, sticky="nw", pady=(8, 4)) body_text = tk.Text(form, font=("Segoe UI", 10), wrap="word", relief="flat", bd=1, highlightbackground="#CCCCCC", highlightthickness=1) body_text.grid(row=2, column=1, sticky="nsew", pady=(8, 4)) body_text.insert("1.0", body) form.grid_rowconfigure(2, weight=1) btn_frame = tk.Frame(form, bg="white") btn_frame.grid(row=3, column=0, columnspan=2, sticky="ew", pady=(8, 0)) status_lbl = tk.Label(btn_frame, text="", bg="white", font=("Segoe UI", 9), fg="gray") status_lbl.pack(side="left") def do_send(): to = entries["Para:"].get().strip() subj = entries["Asunto:"].get().strip() msg_body = body_text.get("1.0", "end-1c").strip() if not to or not msg_body: status_lbl.config(text="⚠ Rellena Para y Mensaje.", fg="orange") return status_lbl.config(text="Enviando...", fg="gray") parent_frame.update() try: send_smtp(mail_session["user"], mail_session["password"], to, subj, msg_body) status_lbl.config(text="✅ Correo enviado.", fg="green") except RuntimeError as e: status_lbl.config(text=f"❌ {e}", fg="red") tk.Button(btn_frame, text=" ✈ Enviar ", bg="#25D366", fg="white", font=("Segoe UI", 10, "bold"), relief="flat", padx=10, pady=6, cursor="hand2", command=do_send).pack(side="right") def setup_mail_read(parent_frame, msg_dict, inbox_callback): """Muestra el cuerpo de un correo y permite responder.""" for w in parent_frame.winfo_children(): w.destroy() parent_frame.grid_rowconfigure(1, weight=1) parent_frame.grid_columnconfigure(0, weight=1) # Barra superior top = tk.Frame(parent_frame, bg="#2C3E50", pady=8) top.grid(row=0, column=0, sticky="ew") tk.Button(top, text="← Volver", bg="#2C3E50", fg="white", font=("Segoe UI", 9), relief="flat", cursor="hand2", command=inbox_callback).pack(side="left", padx=10) tk.Label(top, text=msg_dict["subject"][:60], bg="#2C3E50", fg="white", font=("Segoe UI", 11, "bold")).pack(side="left", padx=4) body_frame = tk.Frame(parent_frame, bg="white", padx=15, pady=10) body_frame.grid(row=1, column=0, sticky="nsew") body_frame.grid_rowconfigure(3, weight=1) body_frame.grid_columnconfigure(0, weight=1) tk.Label(body_frame, text=f"De: {msg_dict['from']}", bg="white", font=("Segoe UI", 9), anchor="w", fg="#555").grid( row=0, column=0, sticky="ew") tk.Label(body_frame, text=f"Fecha: {msg_dict['date']}", bg="white", font=("Segoe UI", 9), anchor="w", fg="#555").grid( row=1, column=0, sticky="ew") ttk.Separator(body_frame).grid(row=2, column=0, sticky="ew", pady=8) txt = tk.Text(body_frame, font=("Consolas", 10), wrap="word", relief="flat", bg="#FAFAFA", highlightbackground="#EEEEEE", highlightthickness=1) txt.grid(row=3, column=0, sticky="nsew") txt.insert("1.0", msg_dict["body"]) txt.config(state="disabled") # esto btn_bar = tk.Frame(body_frame, bg="white") btn_bar.grid(row=4, column=0, sticky="ew", pady=(10, 0)) reply_subj = ("Re: " + msg_dict["subject"]) if not msg_dict["subject"].startswith("Re:") else msg_dict["subject"] reply_body = f"\n\n--- Mensaje original de {msg_dict['from']} ---\n{msg_dict['body']}" tk.Button(btn_bar, text="↩ Responder", bg="#3498DB", fg="white", font=("Segoe UI", 10, "bold"), relief="flat", padx=10, pady=6, cursor="hand2", command=lambda: setup_mail_compose( parent_frame, msg_dict["from"], reply_subj, reply_body )).pack(side="left", padx=(0, 8)) def setup_mail_inbox(parent_frame): """Carga y muestra la bandeja de entrada.""" for w in parent_frame.winfo_children(): w.destroy() parent_frame.grid_rowconfigure(1, weight=1) parent_frame.grid_columnconfigure(0, weight=1) # Cabecera hdr = tk.Frame(parent_frame, bg="#2C3E50", pady=8) hdr.grid(row=0, column=0, sticky="ew") tk.Label(hdr, text="📥 Bandeja de entrada", bg="#2C3E50", fg="white", font=("Segoe UI", 12, "bold"), padx=12).pack(side="left") refresh_btn = tk.Button(hdr, text="↻ Actualizar", bg="#2C3E50", fg="#AAD4F5", font=("Segoe UI", 9), relief="flat", cursor="hand2") refresh_btn.pack(side="right", padx=10) # Lista de correos list_frame = tk.Frame(parent_frame, bg="white") list_frame.grid(row=1, column=0, sticky="nsew") list_frame.grid_rowconfigure(0, weight=1) list_frame.grid_columnconfigure(0, weight=1) columns = ("from", "subject", "date") tree = ttk.Treeview(list_frame, columns=columns, show="headings", selectmode="browse") tree.heading("from", text="De") tree.heading("subject", text="Asunto") tree.heading("date", text="Fecha") tree.column("from", width=180, minwidth=100) tree.column("subject", width=300, minwidth=150) tree.column("date", width=160, minwidth=100) vsb = ttk.Scrollbar(list_frame, orient="vertical", command=tree.yview) tree.configure(yscrollcommand=vsb.set) tree.grid(row=0, column=0, sticky="nsew") vsb.grid(row=0, column=1, sticky="ns") status = tk.Label(parent_frame, text="Cargando correos...", bg="#F0F4F8", fg="gray", font=("Segoe UI", 9), pady=4) status.grid(row=2, column=0, sticky="ew") msgs_cache = [] def load_inbox(): status.config(text="⏳ Conectando al servidor IMAP...", fg="gray") parent_frame.update() try: msgs = fetch_inbox_imap(mail_session["user"], mail_session["password"]) msgs_cache.clear() msgs_cache.extend(msgs) tree.delete(*tree.get_children()) for m in msgs: tree.insert("", "end", iid=m["uid"], values=(m["from"][:40], m["subject"][:60], m["date"][:25])) status.config(text=f"✅ {len(msgs)} correos cargados.", fg="green") except RuntimeError as e: status.config(text=f"❌ {e}", fg="red") def on_select(event): sel = tree.selection() if not sel: return uid = sel[0] msg = next((m for m in msgs_cache if m["uid"] == uid), None) if msg: setup_mail_read(parent_frame, msg, inbox_callback=lambda: setup_mail_inbox(parent_frame)) tree.bind("<>", on_select) refresh_btn.config(command=lambda: threading.Thread( target=load_inbox, daemon=True).start()) threading.Thread(target=load_inbox, daemon=True).start() def setup_mail_login(parent_frame): """Pantalla de login del gestor de correo.""" for w in parent_frame.winfo_children(): w.destroy() parent_frame.grid_rowconfigure(0, weight=1) parent_frame.grid_columnconfigure(0, weight=1) center = tk.Frame(parent_frame, bg="white") center.grid(row=0, column=0) tk.Label(center, text="📧", bg="white", font=("Segoe UI", 48)).pack(pady=(0, 8)) tk.Label(center, text="Gestor de Correo", bg="white", fg="#2C3E50", font=("Segoe UI", 16, "bold")).pack() tk.Label(center, text=f"Servidor: {MAIL_SERVER} | IMAP:{IMAP_PORT} SMTP:{SMTP_PORT}", bg="white", fg="#7F8C8D", font=("Segoe UI", 9)).pack(pady=(4, 20)) form = tk.Frame(center, bg="white") form.pack() form.grid_columnconfigure(1, weight=1) tk.Label(form, text="Usuario:", bg="white", font=("Segoe UI", 10, "bold")).grid(row=0, column=0, sticky="w", pady=6, padx=(0, 10)) entry_user = ttk.Entry(form, font=("Segoe UI", 11), width=24) entry_user.grid(row=0, column=1, sticky="ew") tk.Label(form, text="Contraseña:", bg="white", font=("Segoe UI", 10, "bold")).grid(row=1, column=0, sticky="w", pady=6, padx=(0, 10)) entry_pass = ttk.Entry(form, font=("Segoe UI", 11), width=24, show="•") entry_pass.grid(row=1, column=1, sticky="ew") status_lbl = tk.Label(center, text="", bg="white", font=("Segoe UI", 9), fg="red") status_lbl.pack(pady=(10, 0)) def do_login(): user = entry_user.get().strip() pwd = entry_pass.get().strip() if not user or not pwd: status_lbl.config(text="Introduce usuario y contraseña.") return status_lbl.config(text="Conectando...", fg="gray") parent_frame.update() conn = None try: # Verificar credenciales con IMAP conn = imaplib.IMAP4(MAIL_SERVER, IMAP_PORT) conn.login(user, pwd) mail_session["user"] = user mail_session["password"] = pwd mail_session["connected"] = True except Exception as e: status_lbl.config(text=f"❌ Login fallido: {e}", fg="red") return finally: # Cierre blindado: siempre se libera la conexión de verificación if conn is not None: try: conn.logout() except Exception: try: conn.shutdown() except Exception: pass # Login OK -> cargar bandeja setup_mail_inbox(parent_frame) entry_pass.bind("", lambda e: do_login()) tk.Button(center, text=" Entrar ", bg="#2C3E50", fg="white", font=("Segoe UI", 11, "bold"), relief="flat", padx=16, pady=8, cursor="hand2", command=do_login).pack(pady=(16, 0)) # ── SETUP PESTAÑA T3 ───────────────────────────────────────────────────────── def setup_t3_sockets(tab_frame): """Configura la pestaña T3: notebook interno con Chat y Correo.""" for widget in tab_frame.winfo_children(): widget.destroy() nb = ttk.Notebook(tab_frame) nb.pack(fill="both", expand=True, padx=5, pady=5) # --- Sub-pestaña Chat --- chat_tab = ttk.Frame(nb) nb.add(chat_tab, text="Chat") outer = tk.Frame(chat_tab, bg="white") outer.pack(fill="both", expand=True) center = tk.Frame(outer, bg="white") center.place(relx=0.5, rely=0.45, anchor="center") tk.Label(center, text="💬", bg="white", font=("Segoe UI", 52)).pack(pady=(0, 10)) tk.Label(center, text="Chat entre procesos", bg="white", fg="#2C3E50", font=("Segoe UI", 18, "bold")).pack() tk.Label(center, text="Comunicación IPC mediante ficheros de cola compartidos\nDos subprocesos independientes, cada uno con su ventana Tkinter.", bg="white", fg="#7F8C8D", font=("Segoe UI", 10), justify="center").pack(pady=(6, 30)) tk.Button(center, text=" ▶ Abrir Chat ", bg="#25D366", fg="white", font=("Segoe UI", 13, "bold"), relief="flat", padx=20, pady=12, cursor="hand2", command=launch_chat_windows).pack() tk.Label(center, text="Se abrirán dos ventanas — una por cada \'cliente\'.", bg="white", fg="#AAAAAA", font=("Segoe UI", 9, "italic")).pack(pady=(12, 0)) # --- Sub-pestaña Correo --- mail_tab = ttk.Frame(nb) nb.add(mail_tab, text="Correo") mail_tab.grid_rowconfigure(0, weight=1) mail_tab.grid_columnconfigure(0, weight=1) mail_inner = tk.Frame(mail_tab, bg="white") mail_inner.grid(row=0, column=0, sticky="nsew") mail_inner.grid_rowconfigure(0, weight=1) mail_inner.grid_columnconfigure(0, weight=1) # Barra de acciones (siempre visible encima del contenido) action_bar = tk.Frame(mail_tab, bg="#ECF0F1", pady=4, padx=8) action_bar.grid(row=1, column=0, sticky="ew") def show_compose(): setup_mail_compose(mail_inner) def show_inbox(): if mail_session["connected"]: setup_mail_inbox(mail_inner) else: setup_mail_login(mail_inner) def show_logout(): mail_session["user"] = None mail_session["password"] = None mail_session["connected"] = False setup_mail_login(mail_inner) tk.Button(action_bar, text="📥 Bandeja", bg="#ECF0F1", relief="flat", font=("Segoe UI", 9), cursor="hand2", command=show_inbox).pack(side="left", padx=4) tk.Button(action_bar, text="✉ Nuevo correo", bg="#ECF0F1", relief="flat", font=("Segoe UI", 9), cursor="hand2", command=lambda: setup_mail_compose(mail_inner) if mail_session["connected"] else messagebox.showwarning("Correo", "Inicia sesión primero.")).pack( side="left", padx=4) tk.Button(action_bar, text="⏏ Cerrar sesión", bg="#ECF0F1", relief="flat", font=("Segoe UI", 9), cursor="hand2", command=show_logout).pack(side="right", padx=4) # Arrancar con el login setup_mail_login(mail_inner) # --- FUNCIÓN DE NAVEGACIÓN PRINCIPAL --- def navigate_to_tab(event): """Maneja el cambio de pestaña principal (T1, T2, T3...).""" selected_index = notebook.index(notebook.select()) tab_name = notebook.tab(selected_index, "text") # 1. Limpiar el frame izquierdo (Botones) for widget in frame_izquierdo.winfo_children(): widget.destroy() # 2. Detener todos los hilos activos de otras pestañas stop_monitors() stop_clock_thread() stop_race() stop_music_clean() # Detener la música al cambiar de pestaña # 3. Configurar la pestaña principal y los botones laterales if selected_index == 0: # T1. Procesos setup_t1_processes(tabs[0], root) setup_buttons_t1(frame_izquierdo) elif selected_index == 1: # T2. Threads setup_t2_threads(tabs[1]) setup_buttons_t2(frame_izquierdo) initialize_music() # Cargar la música solo cuando entramos en T2 elif selected_index == 2: # T3. Sockets / Chat IPC setup_t3_sockets(tabs[2]) setup_buttons_default(frame_izquierdo, tab_name) else: setup_buttons_default(frame_izquierdo, tab_name) status_manager.set_status("status_1", f"Navegando a: {tab_name}", "yellow") root.after(2000, lambda: status_manager.set_status("status_1", "Navegación completa", "green")) # --- FUNCIONES DE SETUP PARA BOTONES CONTEXTUALES --- def create_section(parent, title): """Crea un label de título de sección estilizado.""" tk.Label(parent, text=title, bg="#AAAAAA", fg="black", font=("Arial", 10, "bold"), anchor="w").pack(side="top", fill="x", pady=(10, 2), padx=5) def setup_buttons_t1(frame): """Configura los botones para la pestaña T1. Procesos.""" URL_GOOGLE = "https://www.google.com" URL_YOUTUBE = "https://www.youtube.com" URL_FILMIN = "https://www.filmin.es/" create_section(frame, "Aplicaciones") ttk.Button(frame, text="Google", command=lambda: open_external_url(URL_GOOGLE)).pack(pady=3, padx=10, fill="x") ttk.Button(frame, text="YouTube", command=lambda: open_external_url(URL_YOUTUBE)).pack(pady=3, padx=10, fill="x") ttk.Button(frame, text="Filmin", command=lambda: open_external_url(URL_FILMIN)).pack(pady=3, padx=10, fill="x") create_section(frame, "Procesos batch") ttk.Button( frame, text="Copias de seguridad", command=run_bash_backup ).pack(pady=3, padx=10, fill="x") def setup_buttons_t2(frame): """Configura los botones para la pestaña T2. Threads (Incluyendo el de Música).""" create_section(frame, "Hilos de Control") # Crear un botón con estilo y pasarlo a la función de toggle global music_state # Definir el estilo inicial initial_text = "MÚSICA (Play)" if not music_state else "STOP MÚSICA (En Bucle)" initial_style = "Green.TButton" if not music_state else "Red.TButton" music_button = ttk.Button( frame, text=initial_text, style=initial_style ) # Pasar el botón a la función toggle_music para que pueda cambiar su propio texto y estilo music_button.config(command=lambda: toggle_music(music_button)) music_button.pack(pady=10, padx=10, fill="x") create_section(frame, "Configuración General") tk.Label(frame, text="Controles de T2...", fg="gray", font=("Arial", 10)).pack(pady=10) def setup_buttons_default(frame, tab_title): """Configura botones genéricos para pestañas no implementadas.""" create_section(frame, f"Acciones para {tab_title}") tk.Label(frame, text="Funcionalidad pendiente...", fg="gray", font=("Arial", 10)).pack(pady=10) ttk.Button(frame, text="Ejecutar Acción Genérica").pack(pady=3, padx=10, fill="x") # --- CONFIGURACIÓN PRINCIPAL --- root = tk.Tk() root.title("Ventana Responsive - Proyecto Nico") root.geometry("1200x800") root.columnconfigure(0, weight=0) root.columnconfigure(1, weight=1) root.rowconfigure(0, weight=1) root.rowconfigure(1, weight=0) # Menú menu_bar = Menu(root) file_menu = Menu(menu_bar, tearoff=0) file_menu.add_command(label="Nuevo") file_menu.add_command(label="Abrir") file_menu.add_separator() file_menu.add_command(label="Salir", command=root.quit) edit_menu = Menu(menu_bar, tearoff=0) edit_menu.add_command(label="Copiar") edit_menu.add_command(label="Pegar") help_menu = Menu(menu_bar, tearoff=0) help_menu.add_command(label="Acerca de") menu_config = Menu(menu_bar, tearoff=0) menu_config.add_command(label="Preferencias") menu_bar.add_cascade(label="Archivo", menu=file_menu) menu_bar.add_cascade(label="Editar", menu=edit_menu) menu_bar.add_cascade(label="Configuración", menu=menu_config) menu_bar.add_cascade(label="Ayuda", menu=help_menu) root.config(menu=menu_bar) # Estilos personalizados style = ttk.Style() style.configure("CustomNotebook.TNotebook.Tab", font=("Arial", 12, "bold")) style.configure("Green.TButton", background="#4CAF50", foreground="black") style.map("Green.TButton", background=[('active', '#66BB6A')], foreground=[('active', 'black')] ) style.configure("Red.TButton", background="#F44336", foreground="black") style.map("Red.TButton", background=[('active', '#E57373')], foreground=[('active', 'black')] ) # Estructura de Frames frame_izquierdo = tk.Frame(root, bg="#DDDDDD", width=250) frame_central = tk.Frame(root, bg="white") frame_izquierdo.grid(row=0, column=0, sticky="ns") frame_central.grid(row=0, column=1, sticky="nsew") frame_izquierdo.grid_propagate(False) frame_central.rowconfigure(0, weight=1) frame_central.rowconfigure(1, weight=0) frame_central.columnconfigure(0, weight=1) frame_superior = tk.Frame(frame_central, bg="#F0F0F0") frame_inferior = tk.Frame(frame_central, bg="#D0FFD0", height=100) frame_superior.grid(row=0, column=0, sticky="nsew") frame_inferior.grid(row=1, column=0, sticky="ew") frame_inferior.grid_propagate(False) tk.Label(frame_inferior, text="Panel para notas informativas y mensajes sobre la ejecución de los hilos.", bg="#D0FFD0", fg="#333333", justify="left", padx=10, pady=5).pack(fill="both", expand=True) # Barra de estado barra_estado = tk.Frame(root, bg="lightgray") barra_estado.grid(row=1, column=0, columnspan=2, sticky="ew") barra_estado.grid_columnconfigure(0, weight=1) status_manager = StatusBarManager(barra_estado, root) label_fecha_hora = tk.Label(barra_estado, text="Hilo fecha-hora", font=("Helvetica", 10, "bold"), bd=1, fg="blue", relief="raised", anchor="center", width=25, padx=5) label_fecha_hora.pack(side="right", fill="x", padx=5) # INICIALIZACIÓN DEL HILO DE LA BARRA DE ESTADO stop_event_status_bar = threading.Event() status_bar_time_thread = threading.Thread(target=update_time, args=(label_fecha_hora, stop_event_status_bar), daemon=True) status_bar_time_thread.stop_event = stop_event_status_bar status_bar_time_thread.start() # Notebook principal notebook = ttk.Notebook(frame_superior, style="CustomNotebook.TNotebook") notebook.pack(fill="both", expand=True) TAB_TITLES = ["T1. Procesos", "T2. Threads", "T3. Sockets", "T4. Servicios", "T5. Seguridad"] tabs = [] for i, title in enumerate(TAB_TITLES): tab = ttk.Frame(notebook) tabs.append(tab) notebook.add(tab, text=title, padding=4) if i == 0: pass elif i == 1: pass else: ttk.Label(tab, text=f"Contenido de {title}", font=("Arial", 14)).pack(pady=20) notebook.bind("<>", navigate_to_tab) # Configuración inicial (inicia en T1) root.after(100, lambda: setup_t1_processes(tabs[0], root)) root.after(100, lambda: setup_buttons_t1(frame_izquierdo)) # FUNCIÓN DE CIERRE DE VENTANA def on_closing(): stop_monitors() stop_clock_thread() stop_race() stop_music_clean() stop_status_bar_time_thread() root.destroy() root.protocol("WM_DELETE_WINDOW", on_closing) root.mainloop()