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()