commit 78c9f8831e94f1716ad154a798330c2d5dcd7d7b Author: nico Date: Fri Feb 27 18:11:04 2026 +0000 Subir archivos a "/" diff --git a/Proyecto.py b/Proyecto.py new file mode 100644 index 0000000..56cca71 --- /dev/null +++ b/Proyecto.py @@ -0,0 +1,1910 @@ +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() \ No newline at end of file diff --git a/alarm.mp3 b/alarm.mp3 new file mode 100644 index 0000000..36abcb3 Binary files /dev/null and b/alarm.mp3 differ diff --git a/backup_script.sh b/backup_script.sh new file mode 100644 index 0000000..d161693 --- /dev/null +++ b/backup_script.sh @@ -0,0 +1,44 @@ +#!/bin/bash +# backup_script.sh +# Script de Bash para simular una copia de seguridad en Linux. + +# Directorio de destino para el backup y logs +BACKUP_DIR="/tmp/BackupData" +mkdir -p $BACKUP_DIR + +# Obtener la fecha y hora actual +TIMESTAMP=$(date +"%Y%m%d_%H%M%S") +LOG_FILE="$BACKUP_DIR/backup_log_$TIMESTAMP.txt" + +echo "--- Script de Copia de Seguridad Iniciado ($TIMESTAMP) ---" > $LOG_FILE +echo "Creando directorio de destino: $BACKUP_DIR" >> $LOG_FILE + +# Simular el proceso de copia de seguridad (ejemplo: comprimir /var/log) +# Nota: Esto puede requerir permisos de sudo si /var/log no es accesible para tu usuario. +# Usaremos /tmp para un ejemplo más seguro. +echo "Simulando compresión de archivos de ejemplo..." >> $LOG_FILE +# Creamos un archivo de ejemplo para copiar si no existe +if [ ! -f "/tmp/archivos_a_copiar.txt" ]; then + echo "Contenido de prueba" > /tmp/archivos_a_copiar.txt +fi + +# Simulación de la copia real (usando tar para crear un archivo) +tar -czf "$BACKUP_DIR/backup_$TIMESTAMP.tar.gz" /tmp/archivos_a_copiar.txt >> $LOG_FILE 2>&1 + +# Simular la tarea con una pausa +sleep 3 + +# Verificar si el archivo de backup se creó +if [ -f "$BACKUP_DIR/backup_$TIMESTAMP.tar.gz" ]; then + echo "Copia de seguridad completada con éxito." >> $LOG_FILE + EXIT_CODE=0 +else + echo "ERROR: Falló la creación del archivo de copia de seguridad." >> $LOG_FILE + EXIT_CODE=1 +fi + +echo "Log guardado en: $LOG_FILE" >> $LOG_FILE +echo "--- Script de Copia de Seguridad Finalizado ---" >> $LOG_FILE + +# Devolver el código de salida +exit $EXIT_CODE \ No newline at end of file diff --git a/chat_client.py b/chat_client.py new file mode 100644 index 0000000..37d9fe9 --- /dev/null +++ b/chat_client.py @@ -0,0 +1,240 @@ +import sys +import os +import tkinter as tk +from tkinter import ttk +import datetime + +# --------------------------------------------------------------------------- +# 1. LECTURA DE ARGUMENTOS +# --------------------------------------------------------------------------- +if len(sys.argv) < 7: + print("Uso: python chat_client.py ") + sys.exit(1) + +MY_NAME = sys.argv[1] # Nombre de este cliente (ej: "Cliente A") +OTHER_NAME = sys.argv[2] # Nombre del otro cliente (ej: "Cliente B") +FILE_OUT = sys.argv[3] # Fichero donde YO escribo mis mensajes +FILE_IN = sys.argv[4] # Fichero donde YO leo los mensajes del otro +X_POS = int(sys.argv[5]) # Posición horizontal de la ventana +Y_POS = int(sys.argv[6]) # Posición vertical de la ventana + +# --------------------------------------------------------------------------- +# 2. COMUNICACIÓN IPC POR FICHERO +# --------------------------------------------------------------------------- + +# Contador de líneas ya leídas. Así en cada polling solo procesamos +# las líneas NUEVAS, no releemos todo el fichero desde el principio. +lines_read = 0 + +def write_message(text): + """ + Escribe un mensaje en el fichero de salida (el que lee el otro cliente). + Formato de cada línea: HH:MM|texto\n + Usamos modo "a" (append) para no borrar mensajes anteriores. + """ + with open(FILE_OUT, "a", encoding="utf-8") as f: + timestamp = datetime.datetime.now().strftime("%H:%M") + f.write(f"{timestamp}|{text}\n") + +def read_new_messages(): + """ + Lee el fichero de entrada y devuelve SOLO las líneas nuevas + desde la última vez que se llamó a esta función. + Devuelve una lista de tuplas (timestamp, texto). + """ + global lines_read + new = [] + try: + with open(FILE_IN, "r", encoding="utf-8") as f: + all_lines = f.readlines() + # Solo procesamos desde la línea lines_read en adelante + for line in all_lines[lines_read:]: + line = line.strip() + if line and "|" in line: + ts, msg = line.split("|", 1) # Separamos timestamp del texto + new.append((ts, msg)) + # Actualizamos el contador para la próxima llamada + lines_read = len(all_lines) + except Exception: + pass # Si el fichero no existe aún, simplemente no hay mensajes + return new + +# --------------------------------------------------------------------------- +# 3. INTERFAZ GRÁFICA (Tkinter) +# --------------------------------------------------------------------------- + +COLOR_BG = "#F0F4F8" # Fondo general (gris claro) +COLOR_HEADER = "#2C3E50" # Cabecera (azul oscuro) +COLOR_MY_BG = "#DCF8C6" # Burbujas propias (verde tipo WhatsApp) +COLOR_OTHER_BG = "#FFFFFF" # Burbujas del otro (blanco) +COLOR_SEND_BTN = "#25D366" # Botón enviar (verde) +FONT_MSG = ("Segoe UI", 10) +FONT_HEADER = ("Segoe UI", 12, "bold") + +root = tk.Tk() +root.title(f"Chat — {MY_NAME}") +root.geometry(f"500x700+{X_POS}+{Y_POS}") # Tamaño y posición inicial +root.minsize(360, 400) # Tamaño mínimo para que no se deforme +root.resizable(True, True) +root.configure(bg=COLOR_BG) + +# --- Layout con grid: 3 filas --- +# fila 0 = cabecera (altura fija, weight=0) +# fila 1 = mensajes (crece/encoge, weight=1) +# fila 2 = input (altura fija, weight=0) +# Usar weight=0 en las filas del input y cabecera garantiza que +# siempre tienen su espacio reservado sin importar el tamaño de la ventana. +root.grid_rowconfigure(0, weight=0) +root.grid_rowconfigure(1, weight=1) +root.grid_rowconfigure(2, weight=0) +root.grid_columnconfigure(0, weight=1) + +# --- FILA 0: Cabecera --- +header = tk.Frame(root, bg=COLOR_HEADER, pady=10) +header.grid(row=0, column=0, sticky="ew") + +tk.Label( + header, + text=f"💬 {MY_NAME} → {OTHER_NAME}", + bg=COLOR_HEADER, fg="white", + font=FONT_HEADER, anchor="w", padx=15 +).pack(side="left") + +tk.Label( + header, text="● Conectado", + bg=COLOR_HEADER, fg="#2ECC71", + font=("Segoe UI", 9) +).pack(side="right", padx=15) + +# --- FILA 1: Área de mensajes (Canvas con scroll) --- +# Usamos un Canvas porque tk.Frame no tiene scrollbar nativa. +# El truco es poner un Frame dentro del Canvas y ajustar su ancho +# al del Canvas cada vez que este cambia de tamaño (on_canvas_configure). +messages_outer = tk.Frame(root, bg=COLOR_BG) +messages_outer.grid(row=1, column=0, sticky="nsew", padx=8, pady=(8, 4)) +messages_outer.grid_rowconfigure(0, weight=1) +messages_outer.grid_columnconfigure(0, weight=1) + +canvas_msgs = tk.Canvas(messages_outer, bg=COLOR_BG, highlightthickness=0) +canvas_msgs.grid(row=0, column=0, sticky="nsew") + +scrollbar = ttk.Scrollbar(messages_outer, command=canvas_msgs.yview) +scrollbar.grid(row=0, column=1, sticky="ns") +canvas_msgs.configure(yscrollcommand=scrollbar.set) + +# Frame real donde se añaden las burbujas de mensaje +messages_frame = tk.Frame(canvas_msgs, bg=COLOR_BG) +canvas_window = canvas_msgs.create_window((0, 0), window=messages_frame, anchor="nw") + +def on_frame_configure(event): + """Actualiza la zona de scroll cuando el frame de mensajes cambia de tamaño.""" + canvas_msgs.configure(scrollregion=canvas_msgs.bbox("all")) + +def on_canvas_configure(event): + """Ajusta el ancho del frame interior al ancho del canvas (responsivo).""" + canvas_msgs.itemconfig(canvas_window, width=event.width) + +messages_frame.bind("", on_frame_configure) +canvas_msgs.bind("", on_canvas_configure) + +# Mensaje de sistema al inicio del chat +tk.Label( + messages_frame, + text=f"── Inicio del chat con {OTHER_NAME} ──", + bg=COLOR_BG, fg="#AAAAAA", + font=("Segoe UI", 9, "italic") +).pack(pady=(10, 5)) + +def add_bubble(text, is_mine, timestamp=None): + """ + Añade una burbuja de mensaje al área de chat. + - is_mine=True → burbuja verde a la derecha (mensaje propio) + - is_mine=False → burbuja blanca a la izquierda (mensaje recibido) + """ + if timestamp is None: + timestamp = datetime.datetime.now().strftime("%H:%M") + + bg_color = COLOR_MY_BG if is_mine else COLOR_OTHER_BG + side = "right" if is_mine else "left" + anchor = "e" if is_mine else "w" + label = "Tú" if is_mine else OTHER_NAME + + # Fila contenedora (ocupa todo el ancho para poder alinear la burbuja) + row = tk.Frame(messages_frame, bg=COLOR_BG) + row.pack(fill="x", pady=3, padx=8) + + # Burbuja real (solo tan ancha como su contenido) + bubble = tk.Frame(row, bg=bg_color, padx=10, pady=6) + bubble.pack(side=side, anchor=anchor) + + tk.Label(bubble, text=label, bg=bg_color, fg="#888888", + font=("Segoe UI", 8, "bold")).pack(anchor="w") + tk.Label(bubble, text=text, bg=bg_color, fg="#222222", + font=FONT_MSG, wraplength=280, justify="left").pack(anchor="w") + tk.Label(bubble, text=timestamp, bg=bg_color, fg="#AAAAAA", + font=("Segoe UI", 7)).pack(anchor="e") + + # Scroll automático al fondo al recibir/enviar un mensaje + root.after(50, lambda: canvas_msgs.yview_moveto(1.0)) + +# --- FILA 2: Área de entrada de texto --- +input_frame = tk.Frame(root, bg="#E8ECF0", pady=8, padx=8) +input_frame.grid(row=2, column=0, sticky="ew") +input_frame.grid_columnconfigure(0, weight=1) + +text_entry = tk.Text( + input_frame, + height=3, # 3 líneas de alto + font=FONT_MSG, + bg="#FFFFFF", fg="#222222", + relief="flat", wrap="word", + padx=8, pady=6, + bd=1, + highlightbackground="#CCCCCC", + highlightthickness=1 +) +text_entry.grid(row=0, column=0, sticky="ew", padx=(0, 8)) + +def send_message(event=None): + """ + Recoge el texto del entry, lo escribe en el fichero de salida + y añade la burbuja propia en pantalla. + Devuelve "break" para que el Enter no inserte un salto de línea. + """ + msg = text_entry.get("1.0", "end-1c").strip() + if not msg: + return "break" + write_message(msg) # Escribe en el fichero de cola + add_bubble(msg, is_mine=True) # Muestra la burbuja en nuestra ventana + text_entry.delete("1.0", "end") # Limpia el campo de texto + return "break" + +# Enter envía el mensaje; Shift+Enter inserta salto de línea +text_entry.bind("", send_message) +text_entry.bind("", lambda e: None) + +tk.Button( + input_frame, + text="Enviar ➤", + bg=COLOR_SEND_BTN, fg="white", + font=("Segoe UI", 10, "bold"), + relief="flat", padx=12, pady=8, + cursor="hand2", + command=send_message +).grid(row=0, column=1) + +# --------------------------------------------------------------------------- +# 4. POLLING DE MENSAJES ENTRANTES +# --------------------------------------------------------------------------- +def poll_incoming(): + new_msgs = read_new_messages() + for ts, msg in new_msgs: + add_bubble(msg, is_mine=False, timestamp=ts) # Burbuja del otro + root.after(300, poll_incoming) # Volvemos a llamarnos en 300ms + +root.after(300, poll_incoming) # Primera llamada al arrancar + +# --------------------------------------------------------------------------- +# 5. BUCLE PRINCIPAL +# --------------------------------------------------------------------------- +root.mainloop() \ No newline at end of file diff --git a/music.mp3 b/music.mp3 new file mode 100644 index 0000000..55958b7 Binary files /dev/null and b/music.mp3 differ