Inital Code

This commit is contained in:
2025-11-24 15:39:05 +00:00
parent b7c419758a
commit 30b7308b6a
13 changed files with 1558 additions and 2 deletions

489
chatapp/ui.py Normal file
View File

@@ -0,0 +1,489 @@
"""Terminal UI components using Textual framework."""
import asyncio
from datetime import datetime
from typing import List
from textual.app import App, ComposeResult
from textual.containers import Container, Vertical, Horizontal, ScrollableContainer
from textual.widgets import Header, Footer, Input, Static, RichLog
from textual.binding import Binding
from .constants import COLORS
class ChatHistory(RichLog):
"""Custom RichLog widget for chat history display."""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs, highlight=True, markup=True)
self.can_focus = False
class TerminalChatApp(App):
"""Main terminal chat application UI."""
CSS = """
Screen {
background: #1a1a1a;
}
Header {
background: #2d2d2d;
color: #00ff00;
text-style: bold;
}
Footer {
background: #2d2d2d;
}
#main-container {
height: 1fr;
layout: horizontal;
background: #1a1a1a;
}
#chat-panel {
width: 3fr;
height: 1fr;
layout: vertical;
}
#chat-container {
height: 1fr;
border: heavy #00ff00;
background: #0a0a0a;
padding: 0;
}
ChatHistory {
background: #0a0a0a;
color: #ffffff;
padding: 1 2;
border: none;
height: 1fr;
}
#input-container {
height: auto;
padding: 1 2;
background: #1a1a1a;
border-top: heavy #00ff00;
}
#user-input {
width: 1fr;
background: #2d2d2d;
border: solid #00ff00;
}
#user-input:focus {
border: heavy #00ff00;
}
#sidebar {
width: 22;
height: 1fr;
layout: vertical;
margin-left: 1;
}
#channels-panel {
height: 1fr;
border: heavy #00ff00;
background: #0a0a0a;
padding: 1;
margin-bottom: 1;
}
#users-panel {
height: 1fr;
border: heavy #00ff00;
background: #0a0a0a;
padding: 1;
}
.panel-title {
color: #00ff00;
text-style: bold;
text-align: center;
margin-bottom: 1;
background: #1a1a1a;
padding: 1;
border-bottom: solid #00ff00;
}
.panel-list {
height: 1fr;
padding: 1 0;
}
.list-item {
padding: 0 1;
margin: 0 0 1 0;
}
#status-bar {
dock: bottom;
height: 1;
background: #2d2d2d;
color: #00ff00;
padding: 0 2;
text-style: bold;
}
#help-bar {
height: auto;
background: #1a1a1a;
color: #888888;
padding: 1 2;
border-top: solid #444444;
text-align: center;
}
"""
BINDINGS = [
Binding("ctrl+c", "quit", "Quit", show=False),
Binding("ctrl+d", "quit", "Quit", show=False),
Binding("ctrl+h", "show_help", "Help", show=True),
]
TITLE = "Terminal Chat"
def __init__(self, username: str, message_queue: asyncio.Queue, chat_server):
super().__init__()
self.username = username
self.message_queue = message_queue
self.chat_server = chat_server
self.current_view = 'global' # Can be channel name or 'dm:username'
self.users_list = []
def compose(self) -> ComposeResult:
yield Header()
with Container(id="main-container"):
with Vertical(id="chat-panel"):
with Container(id="chat-container"):
yield ChatHistory(id="chat-history")
with Horizontal(id="input-container"):
yield Input(
placeholder="Type a message or /help for commands...",
id="user-input"
)
with Vertical(id="sidebar"):
with Vertical(id="channels-panel"):
yield Static("CHANNELS", classes="panel-title")
yield ScrollableContainer(id="channels-list", classes="panel-list")
with Vertical(id="users-panel"):
yield Static("ONLINE USERS", classes="panel-title")
yield ScrollableContainer(id="users-list", classes="panel-list")
yield Static(
"/dm <user> - DM | /create <channel> - create | /join <channel> - join | /help - help",
id="help-bar"
)
yield Static(f"Connected as: {self.username} | View: #global", id="status-bar")
yield Footer()
def on_mount(self) -> None:
self.query_one("#user-input").focus()
self.display_info_message(f"Welcome to Terminal Chat, {self.username}!")
self.display_info_message("Type /help to see available commands")
self.set_interval(0.1, self.check_messages)
self.update_channels_list()
def update_status_bar(self):
view_text = f"#{self.current_view}" if not self.current_view.startswith('dm:') else f"DM: {self.current_view[3:]}"
self.query_one("#status-bar", Static).update(f"Connected as: {self.username} | View: {view_text}")
def update_channels_list(self):
channels_container = self.query_one("#channels-list", ScrollableContainer)
channels_container.remove_children()
user_channels = self.chat_server.get_user_channels(self.username)
for channel_name in sorted(user_channels):
prefix = "> " if channel_name == self.current_view else " "
color = COLORS['channel'] if channel_name == self.current_view else "#888888"
channel_widget = Static(f"{prefix}[{color}]#{channel_name}[/{color}]", classes="list-item", markup=True)
channels_container.mount(channel_widget)
def display_user_message(self, username: str, message: str, timestamp: str, color: str, channel: str = None):
chat_history = self.query_one(ChatHistory)
chat_history.write(f"[dim]{timestamp}[/dim] [{color}]{username}[/{color}]: {message}")
def display_system_message(self, message: str, timestamp: str):
chat_history = self.query_one(ChatHistory)
chat_history.write(f"[dim]{timestamp}[/dim] [{COLORS['system']}][SYSTEM][/{COLORS['system']}] {message}")
def display_error_message(self, message: str, timestamp: str = None):
if not timestamp:
timestamp = datetime.now().strftime("%H:%M:%S")
chat_history = self.query_one(ChatHistory)
chat_history.write(f"[dim]{timestamp}[/dim] [{COLORS['error']}][ERROR][/{COLORS['error']}] {message}")
def display_info_message(self, message: str):
timestamp = datetime.now().strftime("%H:%M:%S")
chat_history = self.query_one(ChatHistory)
chat_history.write(f"[dim]{timestamp}[/dim] [{COLORS['info']}][INFO][/{COLORS['info']}] {message}")
def display_success_message(self, message: str):
timestamp = datetime.now().strftime("%H:%M:%S")
chat_history = self.query_one(ChatHistory)
chat_history.write(f"[dim]{timestamp}[/dim] [{COLORS['success']}][SUCCESS][/{COLORS['success']}] {message}")
def display_dm(self, from_user: str, message: str, timestamp: str, color: str):
chat_history = self.query_one(ChatHistory)
chat_history.write(f"[dim]{timestamp}[/dim] [{color}]{from_user}[/{color}]: {message}")
def switch_view(self, view: str):
self.current_view = view
self.update_status_bar()
self.update_channels_list()
chat_history = self.query_one(ChatHistory)
chat_history.clear()
if view.startswith('dm:'):
other_user = view[3:]
dm = self.chat_server.get_or_create_dm(self.username, other_user)
for msg in dm.messages:
self.display_dm(msg['from'], msg['message'], msg['timestamp'], msg['color'])
else:
if view in self.chat_server.channels:
channel = self.chat_server.channels[view]
for msg in channel.messages:
if msg['type'] == 'system':
self.display_system_message(msg['message'], msg['timestamp'])
elif msg['type'] == 'message':
self.display_user_message(msg['username'], msg['message'], msg['timestamp'], msg['color'])
def check_messages(self):
try:
while not self.message_queue.empty():
msg_data = self.message_queue.get_nowait()
# Only display if in the right view
if msg_data['type'] == 'message':
if msg_data.get('channel') == self.current_view:
self.display_user_message(
msg_data['username'],
msg_data['message'],
msg_data['timestamp'],
msg_data.get('color', '#FFFFFF')
)
elif msg_data['type'] == 'dm':
other_user = msg_data['from'] if msg_data['from'] != self.username else msg_data['to']
if self.current_view == f"dm:{other_user}":
self.display_dm(
msg_data['from'],
msg_data['message'],
msg_data['timestamp'],
msg_data.get('color', '#FFFFFF')
)
elif msg_data['type'] == 'system':
if self.current_view == 'global':
self.display_system_message(msg_data['message'], msg_data['timestamp'])
elif msg_data['type'] == 'error':
self.display_error_message(msg_data['message'], msg_data['timestamp'])
elif msg_data['type'] == 'info':
self.display_info_message(msg_data['message'])
elif msg_data['type'] == 'user_list':
self.update_user_list(msg_data['users'])
self.update_channels_list()
except:
pass
def update_user_list(self, users: list):
self.users_list = users
users_container = self.query_one("#users-list", ScrollableContainer)
users_container.remove_children()
for user_data in sorted(users, key=lambda x: x['username']):
username = user_data['username']
color = user_data['color']
prefix = "> " if username == self.username else " "
user_widget = Static(f"{prefix}[{color}]{username}[/{color}]", classes="list-item", markup=True)
users_container.mount(user_widget)
def process_command(self, message: str) -> bool:
if not message.startswith('/'):
return False
parts = message.split(' ', 2)
command = parts[0].lower()
if command == '/help' or command == '/h':
self.show_help_message()
return True
elif command == '/dm' or command == '/d':
if len(parts) < 2:
self.display_error_message("Usage: /dm <username> [message]")
return True
target_user = parts[1]
if target_user == self.username:
self.display_error_message("You cannot DM yourself")
return True
if target_user not in self.chat_server.users:
self.display_error_message(f"User '{target_user}' is not online")
return True
if len(parts) == 3:
self.chat_server.send_dm(self.username, target_user, parts[2])
else:
self.switch_view(f"dm:{target_user}")
return True
elif command == '/create' or command == '/new':
if len(parts) < 2:
self.display_error_message("Usage: /create <channel_name> [private]")
return True
channel_name = parts[1].lower()
is_private = len(parts) > 2 and parts[2].lower() == 'private'
if self.chat_server.create_channel(channel_name, self.username, is_private):
self.display_success_message(f"Created channel #{channel_name}")
self.switch_view(channel_name)
else:
self.display_error_message(f"Channel #{channel_name} already exists")
return True
elif command == '/join' or command == '/j':
if len(parts) < 2:
self.display_error_message("Usage: /join <channel_name>")
return True
channel_name = parts[1].lower()
success, msg = self.chat_server.join_channel(channel_name, self.username)
if success:
self.display_success_message(msg)
self.switch_view(channel_name)
else:
self.display_error_message(msg)
return True
elif command == '/leave' or command == '/l':
if len(parts) < 2:
self.display_error_message("Usage: /leave <channel_name>")
return True
channel_name = parts[1].lower()
success, msg = self.chat_server.leave_channel(channel_name, self.username)
if success:
self.display_success_message(msg)
self.switch_view('global')
else:
self.display_error_message(msg)
return True
elif command == '/invite' or command == '/inv':
if len(parts) < 3:
self.display_error_message("Usage: /invite <channel_name> <username>")
return True
channel_name = parts[1].lower()
invitee = parts[2]
success, msg = self.chat_server.invite_to_channel(channel_name, self.username, invitee)
if success:
self.display_success_message(msg)
else:
self.display_error_message(msg)
return True
elif command == '/delete' or command == '/del':
if len(parts) < 2:
self.display_error_message("Usage: /delete <channel_name>")
return True
channel_name = parts[1].lower()
if self.chat_server.delete_channel(channel_name, self.username):
self.display_success_message(f"Deleted channel #{channel_name}")
self.switch_view('global')
else:
self.display_error_message(f"Cannot delete #{channel_name}. You must be the creator")
return True
elif command == '/switch' or command == '/s':
if len(parts) < 2:
self.display_error_message("Usage: /switch <channel_name>")
return True
channel_name = parts[1].lower()
if channel_name in self.chat_server.channels and self.chat_server.channels[channel_name].has_member(self.username):
self.switch_view(channel_name)
else:
self.display_error_message(f"You are not a member of #{channel_name}")
return True
elif command == '/channels' or command == '/ch':
channels = self.chat_server.get_user_channels(self.username)
self.display_info_message(f"Your channels: {', '.join(['#' + c for c in channels])}")
return True
elif command == '/users' or command == '/u':
usernames = [u['username'] for u in self.users_list]
self.display_info_message(f"Online users ({len(usernames)}): {', '.join(sorted(usernames))}")
return True
elif command == '/clear' or command == '/c':
chat_history = self.query_one(ChatHistory)
chat_history.clear()
self.display_info_message("Chat history cleared locally")
return True
elif command == '/quit' or command == '/q':
self.exit()
return True
else:
self.display_error_message(f"Unknown command: {command}. Type /help for available commands")
return True
def show_help_message(self):
self.display_info_message("Available commands:")
chat_history = self.query_one(ChatHistory)
chat_history.write(f" [{COLORS['help']}]/dm <user> [msg][/{COLORS['help']}] or [{COLORS['command']}]/d[/{COLORS['command']}] - DM a user")
chat_history.write(f" [{COLORS['help']}]/create <name> [private][/{COLORS['help']}] or [{COLORS['command']}]/new[/{COLORS['command']}] - Create a channel")
chat_history.write(f" [{COLORS['help']}]/join <name>[/{COLORS['help']}] or [{COLORS['command']}]/j[/{COLORS['command']}] - Join a channel")
chat_history.write(f" [{COLORS['help']}]/leave <name>[/{COLORS['help']}] or [{COLORS['command']}/]/l[/{COLORS['command']}] - Leave a channel")
chat_history.write(f" [{COLORS['help']}]/invite <channel> <user>[/{COLORS['help']}] or [{COLORS['command']}]/inv[/{COLORS['command']}] - Invite user to channel")
chat_history.write(f" [{COLORS['help']}]/delete <name>[/{COLORS['help']}] or [{COLORS['command']}]/del[/{COLORS['command']}] - Delete a channel you created")
chat_history.write(f" [{COLORS['help']}]/switch <name>[/{COLORS['help']}] or [{COLORS['command']}]/s[/{COLORS['command']}] - Switch to a channel")
chat_history.write(f" [{COLORS['help']}]/channels[/{COLORS['help']}] or [{COLORS['command']}]/ch[/{COLORS['command']}] - List your channels")
chat_history.write(f" [{COLORS['help']}]/users[/{COLORS['help']}] or [{COLORS['command']}]/u[/{COLORS['command']}] - List online users")
chat_history.write(f" [{COLORS['help']}]/clear[/{COLORS['help']}] or [{COLORS['command']}]/c[/{COLORS['command']}] - Clear chat history locally")
chat_history.write(f" [{COLORS['help']}]/quit[/{COLORS['help']}] or [{COLORS['command']}]/q[/{COLORS['command']}] - Quit the chat application")
def on_input_submitted(self, event: Input.Submitted) -> None:
message = event.value.strip()
event.input.value = ""
if not message:
return
if self.process_command(message):
return
# Regular message
if self.current_view.startswith('dm:'):
target_user = self.current_view[3:]
self.chat_server.send_dm(self.username, target_user, message)
else:
self.chat_server.send_channel_message(self.current_view, self.username, message)