240 lines
9.0 KiB
Python
240 lines
9.0 KiB
Python
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 <mi_nombre> <otro_nombre> <f_salida> <f_entrada> <x> <y>")
|
|
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("<Configure>", on_frame_configure)
|
|
canvas_msgs.bind("<Configure>", 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("<Return>", send_message)
|
|
text_entry.bind("<Shift-Return>", 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() |