Compare commits

...

2 Commits

44 changed files with 605 additions and 0 deletions

8
.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

14
.idea/Final.iml Normal file
View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/venv" />
</content>
<orderEntry type="jdk" jdkName="Python 3.13 (pythonProject)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="PyDocumentationSettings">
<option name="format" value="PLAIN" />
<option name="myDocStringFormat" value="Plain" />
</component>
</module>

View File

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

7
.idea/misc.xml Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.11.2 WSL (Debian): (/home/santi/.virtualenvs/Final/bin/python)" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.13 (pythonProject)" project-jdk-type="Python SDK" />
</project>

8
.idea/modules.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/Final.iml" filepath="$PROJECT_DIR$/.idea/Final.iml" />
</modules>
</component>
</project>

8
.idea/vcs.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$/app" vcs="Git" />
</component>
</project>

83
app/ConfigMgr.py Normal file
View File

@ -0,0 +1,83 @@
import tkinter as tk
from tkinter import ttk
import os
import configparser
class ConfigMgr:
def __init__(self, top_level, config_changed_listener=None):
self.top_level = top_level
self.config_window = None
self.config = configparser.ConfigParser()
self.__load_config()
self.config_changed_listener=config_changed_listener
self.config.read('config.ini')
def __load_config(self):
if os.path.exists('config.ini'):
self.config.read('config.ini')
else:
print("Config file not found, creating default config file")
with open('config.ini', 'w') as f:
self.__write_default_config(f)
self.config.read('config.ini')
def __write_default_config(self, file):
chat_config = ("[Chat]\n"
"server=http://localhost:2020\n"
"name=User\n")
file.write(chat_config)
def display_config_window(self):
if (self.config_window is None
or not tk.Toplevel.winfo_exists(self.config_window)):
self.config_window = self.__build_config_window()
else:
self.config_window.lift()
def __build_config_window(self):
config_window = tk.Toplevel(self.top_level)
config_window.title("Config")
config_window.geometry("400x300")
notebook = ttk.Notebook(config_window)
notebook.pack(expand=True, fill="both")
chat_tab = ttk.Frame(notebook)
notebook.add(chat_tab, text="Chat Config")
chat_server_label = tk.Label(chat_tab, text="Chat Server URL")
chat_server_label.pack()
self.chat_server_variable = tk.StringVar()
try:
self.chat_server_variable.set(self.config["Chat"]["server"])
except KeyError:
self.chat_server_variable.set("")
chat_server_input = tk.Entry(chat_tab, textvariable=self.chat_server_variable)
chat_server_input.pack()
chat_name_label = tk.Label(chat_tab, text="Name in the Chat")
chat_name_label.pack()
self.chat_name_variable = tk.StringVar()
try:
self.chat_name_variable.set(self.config["Chat"]["name"])
except KeyError:
self.chat_name_variable.set("")
chat_name_input = tk.Entry(chat_tab, textvariable=self.chat_name_variable)
chat_name_input.pack()
self.save_button = tk.Button(config_window, text="Save", command=self.save_config)
self.save_button.pack(pady=10)
return config_window
def save_config(self):
self.config["Chat"] = {"server": self.chat_server_variable.get(),
"name": self.chat_name_variable.get()}
with open('config.ini', 'w') as configfile:
self.config.write(configfile)
self.config_changed_listener()
# Close window
self.config_window.destroy()

0
app/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

89
app/chat_server/Server.py Normal file
View File

@ -0,0 +1,89 @@
import threading
import time
import flask
from werkzeug.serving import make_server
class Server:
"""
This server does NOT use Flask's built-in development server.
Instead, it uses Werkzeug's make_server to create a WSGI server.
This server is thread-safe and can be stopped by setting the stop_event.
This server works as a chat server by polling, not by using websockets (to keep it simple).
"""
def __init__(self, host: str, port: int, stop_event: threading.Event):
self.host = host
self.port = port
self.stop_event = stop_event
# Flask definition
self.flask = flask.Flask(__name__)
self.flask.add_url_rule('/status', methods=['POST'], view_func=self.status)
self.flask.add_url_rule('/send_message', methods=['POST'], view_func=self.send_message)
self.flask.add_url_rule('/get_messages', methods=['POST'], view_func=self.get_messages)
self.server = make_server(self.host, self.port, self.flask)
self.watcher_thread = threading.Thread(target=self.__watcher)
self.server_thread = threading.Thread(target=self.server.serve_forever)
self.server_thread.start()
self.watcher_thread.start()
# Message initialization
self.message_id = 0
self.messages = self.__init_messages()
def __watcher(self):
while not self.stop_event.is_set():
time.sleep(1)
self.shutdown()
def shutdown(self):
self.__persist_messages()
self.server.shutdown()
def __init_messages(self):
messages = []
try:
with open('chat_server/messages.txt', 'r') as f:
for line in f:
parts = line.strip().split('|')
messages.append({
'id': int(parts[0]),
'sender': parts[1],
'content': parts[2]
})
self.message_id = messages[-1]['id'] if messages else 0
except FileNotFoundError:
with open('chat_server/messages.txt', 'w+') as f:
pass
return messages
def __persist_messages(self):
with open('chat_server/messages.txt', 'w') as f:
for message in self.messages:
f.write(f"{message['id']}|{message['sender']}|{message['content']}\n")
def status(self):
return 'OK'
def send_message(self):
sender = flask.request.json['sender']
content = flask.request.json['content']
self.message_id += 1
message = {
'id': self.message_id,
'sender': sender,
'content': content
}
self.messages.append(message)
return {'id': self.message_id}
def get_messages(self):
try: last_id = flask.request.json.get('last_id')
# last_id is a mandatory parameter
except AttributeError: return flask.Response('Last ID not specified', status=400)
new_messages = [msg for msg in self.messages if msg['id'] > last_id]
return {'messages': new_messages}

View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,2 @@
1|Santi|Hola como estas?
2|Santi|Andres no me copies el TO DO

4
app/config.ini Normal file
View File

@ -0,0 +1,4 @@
[Chat]
server = http://localhost:2020
name = Santi

130
app/main.py Normal file
View File

@ -0,0 +1,130 @@
import tkinter as tk
from tkinter import Menu
from tkinter import ttk
import threading
from app.chat_server.Server import Server
from app.widgets import ClockLabel
from app.ConfigMgr import ConfigMgr
from app.widgets.ChatTab import ChatTab
from app.widgets.UsageLabels import CPULabel, RAMLabel, BatteryLabel, NetworkLabel
# Evento para detener threads de manera segura
stop_event = threading.Event()
def on_closing():
"""Gestiona el cierre de la aplicación de manera segura."""
# Detiene todos los threads relacionados con stop_event
stop_event.set()
# Cierra la ventana principal
root.quit()
root.destroy()
def on_config_changed():
"""Actualiza la configuración del servidor y nombre de usuario en el chat."""
chat_frame.change_server_url(config_manager.config["Chat"]["server"])
chat_frame.change_sender_name(config_manager.config["Chat"]["name"])
# Crea la ventana principal
root = tk.Tk()
root.title("Responsive Window") # Título de la ventana
root.geometry("1150x700") # Tamaño inicial de la ventana
# Inicializa el gestor de configuración
config_manager = ConfigMgr(root, config_changed_listener=on_config_changed)
# Inicializa el servidor de chat
server = Server(host="localhost", port=2020, stop_event=stop_event)
# Configura la ventana principal para que sea responsive
root.columnconfigure(0, weight=0) # Columna izquierda con tamaño fijo
root.columnconfigure(1, weight=1) # Columna central ajustable
root.columnconfigure(2, weight=0) # Columna derecha con tamaño fijo
root.rowconfigure(0, weight=1) # Fila principal ajustable
root.rowconfigure(1, weight=0) # Barra de estado con tamaño fijo
# Crea el menú superior
menu_bar = Menu(root)
# Menú Archivo
file_menu = Menu(menu_bar, tearoff=0)
file_menu.add_command(label="New") # Comando Nuevo
file_menu.add_command(label="Open") # Comando Abrir
file_menu.add_separator() # Separador visual
file_menu.add_command(label="Config", command=config_manager.display_config_window) # Configuración
file_menu.add_command(label="Exit", command=on_closing) # Salir
# Menú Edición
edit_menu = Menu(menu_bar, tearoff=0)
edit_menu.add_command(label="Copy") # Comando Copiar
edit_menu.add_command(label="Paste") # Comando Pegar
# Añade los menús al menú principal
menu_bar.add_cascade(label="File", menu=file_menu)
menu_bar.add_cascade(label="Edit", menu=edit_menu)
# Asigna el menú a la ventana principal
root.config(menu=menu_bar)
# Crea los marcos laterales y central
frame_left = tk.Frame(root, bg="lightblue", width=200) # Marco izquierdo
frame_center = tk.Frame(root, bg="white") # Marco central
chat_frame = ChatTab(root, chat_server_url=config_manager.config["Chat"]["server"],
sender_name=config_manager.config["Chat"]["name"],
stop_event=stop_event, width=200, bg="lightgreen") # Marco derecho para el chat
# Coloca los marcos en la cuadrícula
frame_left.grid(row=0, column=0, sticky="ns") # Marco izquierdo
frame_center.grid(row=0, column=1, sticky="nsew") # Marco central
chat_frame.grid(row=0, column=2, sticky="ns") # Marco derecho
# Configura tamaños fijos para los marcos laterales
frame_left.grid_propagate(False)
chat_frame.grid_propagate(False)
# Divide el marco central en dos partes (superior ajustable e inferior fijo)
frame_center.rowconfigure(0, weight=1) # Parte superior ajustable
frame_center.rowconfigure(1, weight=0) # Parte inferior fija
frame_center.columnconfigure(0, weight=1) # Ancho ajustable
# Crea sub-marcos dentro del marco central
frame_top = tk.Frame(frame_center, bg="lightyellow") # Parte superior
frame_bottom = tk.Frame(frame_center, bg="lightgray", height=100) # Parte inferior
# Coloca los sub-marcos en el marco central
frame_top.grid(row=0, column=0, sticky="nsew") # Parte superior
frame_bottom.grid(row=1, column=0, sticky="ew") # Parte inferior
# Fija el tamaño de la parte inferior
frame_bottom.grid_propagate(False)
# Crea la barra de estado
status_bar = tk.Label(root, text="Status bar", bg="lightgray", anchor="w") # Barra de estado
status_bar.grid(row=1, column=0, columnspan=3, sticky="ew")
# Configura un cuaderno (notebook) para widgets
style = ttk.Style()
style.configure("CustomNotebook.TNotebook.Tab", font=("Arial", 12, "bold"))
notebook = ttk.Notebook(frame_top, style="CustomNotebook.TNotebook")
notebook.pack(fill="both", expand=True)
# Añade etiquetas de uso del sistema
label_cpu = CPULabel(status_bar, bg="lightgreen", font=("Helvetica", 10), relief="groove", anchor="center", width=10, stop_event=stop_event) # Uso de CPU
label_ram = RAMLabel(status_bar, bg="lightcoral", font=("Helvetica", 10), relief="groove", anchor="center", width=10, stop_event=stop_event) # Uso de RAM
label_battery = BatteryLabel(status_bar, bg="lightblue", font=("Helvetica", 10), relief="groove", anchor="center", width=20, stop_event=stop_event) # Batería
label_net = NetworkLabel(status_bar, text="Network", bg="lightpink", font=("Helvetica", 10), relief="groove", anchor="center", width=20, stop_event=stop_event) # Red
label_time = ClockLabel(status_bar, font=("Helvetica", 12), bd=1, fg="darkblue", relief="sunken", anchor="center", width=20, stop_event=stop_event) # Reloj
# Coloca las etiquetas en la barra de estado
label_cpu.pack(side="left", fill="both", expand=True)
label_ram.pack(side="left", fill="both", expand=True)
label_battery.pack(side="left", fill="both", expand=True)
label_net.pack(side="left", fill="both", expand=True)
label_time.pack(side="right", fill="both", expand=True)
# Configura la acción para el cierre de la ventana
root.protocol("WM_DELETE_WINDOW", on_closing)
# Inicia la aplicación
root.mainloop()

13
app/requirements.txt Normal file
View File

@ -0,0 +1,13 @@
blinker==1.9.0
certifi==2024.8.30
charset-normalizer==3.4.0
click==8.1.7
Flask==3.1.0
idna==3.10
itsdangerous==2.2.0
Jinja2==3.1.4
MarkupSafe==3.0.2
psutil==6.1.0
requests==2.32.3
urllib3==2.2.3
Werkzeug==3.1.3

111
app/widgets/ChatTab.py Normal file
View File

@ -0,0 +1,111 @@
import time
import tkinter as tk
from tkinter import ttk
from tkinter.ttk import Notebook
from tkinter import Tk
import requests
from app.widgets.abc import ThreadedTab
class ChatTab(ThreadedTab):
def __init__(self, root: Notebook | Tk, chat_server_url: str, sender_name: str, **kwargs):
self.chat_server_url = chat_server_url
self.sender_name = sender_name
self.conn = False
self.last_msg = 0
super().__init__(root, **kwargs)
def task(self):
try:
self.get_messages()
if not self.conn:
self.connected()
self.conn = True
except requests.ConnectionError:
self.disconnected()
time.sleep(1)
def build(self):
# Create the main frame for the chat interface
self.chat_frame = tk.Frame(self)
self.chat_frame.pack(fill="both", expand=True)
# Create the status label
self.status_label = tk.Label(self.chat_frame, text="", font=("Helvetica", 16))
self.status_label.pack(fill="x")
# Create the history frame with a scrollbar
self.history_frame = tk.Frame(self.chat_frame)
self.history_frame.pack(fill="both", expand=True)
self.history_canvas = tk.Canvas(self.history_frame)
self.history_canvas.pack(side="left", fill="both", expand=True)
self.scrollbar = ttk.Scrollbar(self.history_frame, orient="vertical", command=self.history_canvas.yview)
self.scrollbar.pack(side="right", fill="y")
self.history_canvas.configure(yscrollcommand=self.scrollbar.set)
self.history_canvas.bind('<Configure>', lambda e: self.history_canvas.configure(scrollregion=self.history_canvas.bbox("all")))
self.history_container = tk.Frame(self.history_canvas)
self.history_container.bind("<Configure>", self.on_frame_configure)
self.history_canvas.create_window((0, 0), window=self.history_container, anchor="nw")
# Create the input frame at the bottom
self.input_frame = tk.Frame(self.chat_frame)
self.input_frame.pack(fill="x")
self.message_entry = tk.Entry(self.input_frame)
self.message_entry.pack(side="left", fill="x", expand=True, padx=5, pady=5)
self.message_entry.bind("<Return>", lambda event: self.send_message()) # Bind Enter key to send_message
self.send_button = tk.Button(self.input_frame, text="Send", command=self.send_message)
self.send_button.pack(side="right", padx=5, pady=5)
def on_frame_configure(self, event):
self.history_canvas.configure(scrollregion=self.history_canvas.bbox("all"))
def send_message(self):
message = self.message_entry.get()
if message and self.conn:
response = requests.post(f"{self.chat_server_url}/send_message", json={"content": message, "sender": self.sender_name})
self.last_msg = response.json().get("id")
self.display_message(self.sender_name, message)
self.message_entry.delete(0, tk.END)
def display_message(self, sender, message):
message_frame = tk.Frame(self.history_container, bg="lightgray", pady=5)
message_frame.pack(fill="x", padx=5, pady=5)
message_label = tk.Label(message_frame, text=f"{sender}: {message}", anchor="w", justify="left", wraplength=300)
message_label.pack(fill="x")
self.history_canvas.update_idletasks()
self.history_canvas.yview_moveto(1)
def get_messages(self):
response = requests.post(f"{self.chat_server_url}/get_messages", json={"last_id": self.last_msg})
messages = response.json().get("messages")
for message in messages:
self.display_message(message["sender"], message["content"])
self.last_msg = messages[-1]["id"] if messages else self.last_msg
def connected(self):
self.conn = True
self.status_label.config(text="Connected to chat", fg="green")
self.send_button.config(state="normal")
def disconnected(self):
self.conn = False
self.status_label.config(text="Disconnected from Chat", fg="red")
self.send_button.config(state="disabled")
def change_sender_name(self, new_name: str):
self.sender_name = new_name
def change_server_url(self, new_url: str):
self.chat_server_url = new_url

16
app/widgets/ClockLabel.py Normal file
View File

@ -0,0 +1,16 @@
import time
from datetime import datetime
from .abc import ThreadedLabel
class ClockLabel(ThreadedLabel):
def task(self):
now = 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}"
try: self.config(text=label_text)
except RuntimeError: pass
time.sleep(0.5)

View File

@ -0,0 +1,50 @@
import time
import psutil
from .abc import ThreadedLabel
class CPULabel(ThreadedLabel):
def task(self, *args):
cpu_percent = psutil.cpu_percent()
try: self.config(text=f'CPU: {cpu_percent}%')
except RuntimeError: pass
time.sleep(1)
class RAMLabel(ThreadedLabel):
def task(self, *args):
memory = psutil.virtual_memory()
try: self.config(text=f'RAM: {memory.percent}%')
except RuntimeError: pass
time.sleep(1)
class BatteryLabel(ThreadedLabel):
def task(self, *args):
battery = psutil.sensors_battery()
if battery is None:
self.config(text='Battery: N/A')
return
battery_percent = battery.percent
is_charging = battery.power_plugged
time_left = battery.secsleft
text = f'Battery: {battery_percent:.0f}%'
if is_charging:
text += ', Plugged in'
else:
text += f', ({time_left // 3600}h {time_left % 3600 // 60}m left)'
try: self.config(text=text)
except RuntimeError: pass # Catch update on closed widget
time.sleep(1)
class NetworkLabel(ThreadedLabel):
def task(self, *args):
network = psutil.net_io_counters()
try: self.config(text=f'Net: {network.bytes_sent / 1024 / 1024:.2f} MB snt,'
f' {network.bytes_recv / 1024 / 1024:.2f} MB rcv')
except RuntimeError: pass # Catch update on closed widget
time.sleep(1)

4
app/widgets/__init__.py Normal file
View File

@ -0,0 +1,4 @@
from .ClockLabel import ClockLabel
from .UsageLabels import CPULabel, RAMLabel
__all__ = ['ClockLabel', 'CPULabel', 'RAMLabel']

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,22 @@
import threading
from abc import ABC, abstractmethod
from threading import Thread
from tkinter import Label
class ThreadedLabel(ABC, Label):
def __init__(self, root: Label, stop_event: threading.Event, **kwargs):
super().__init__(root, **kwargs)
self.stop_event = stop_event
self.declared_thread: Thread = threading.Thread(target=self.__loop)
self.declared_thread.start()
def __loop(self):
while not self.stop_event.is_set():
self.task()
@abstractmethod
def task(self, *args):
pass

View File

@ -0,0 +1,26 @@
import threading
from abc import ABC, abstractmethod
from tkinter import Tk
from tkinter.ttk import Notebook
from tkinter import Frame
class ThreadedTab(ABC, Frame):
def __init__(self, root: Notebook | Tk, stop_event: threading.Event, **kwargs):
super().__init__(root, **kwargs)
self.stop_event = stop_event
self._thread = threading.Thread(target=self.__loop)
self.build()
self._thread.start()
def __loop(self):
while not self.stop_event.is_set():
self.task()
@abstractmethod
def build(self):
pass
@abstractmethod
def task(self, *args):
raise NotImplementedError("Method not implemented")

View File

@ -0,0 +1,4 @@
from .ThreadedLabel import ThreadedLabel
from .ThreadedTab import ThreadedTab
__all__ = ['ThreadedLabel', 'ThreadedTab']

Binary file not shown.

Binary file not shown.