-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathclient.py
More file actions
168 lines (146 loc) · 5.37 KB
/
client.py
File metadata and controls
168 lines (146 loc) · 5.37 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
import socket
import threading
from prompt_toolkit import Application
from prompt_toolkit.buffer import Buffer
from prompt_toolkit.layout.containers import HSplit, VSplit, Window, WindowAlign
from prompt_toolkit.layout.controls import BufferControl, FormattedTextControl
from prompt_toolkit.layout.layout import Layout
from prompt_toolkit.key_binding import KeyBindings
from prompt_toolkit.styles import Style
from prompt_toolkit.application.current import get_app
HOST = input("Enter server IP: ")
PORT = 5555
nickname = input("Choose your nickname: ").strip() or "Anonymous"
# ─── socket setup ───────────────────────────────────────────
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
sock.connect((HOST, PORT))
except ConnectionRefusedError:
print("Server is not reachable.")
exit(1)
sock.sendall(nickname.encode('utf-8'))
# ─── global state ───────────────────────────────────────────
messages = [] # each element: (text, is_system)
users = [] # list of nicknames
stop_event = threading.Event()
def add_message(text, is_system=False):
"""Append a message to the history and refresh UI."""
messages.append((text, is_system))
if len(messages) > 1000:
messages.pop(0)
get_app().invalidate()
def update_users(new_list):
global users
users = new_list
get_app().invalidate()
# ─── receiving thread ───────────────────────────────────────
def receiver():
while not stop_event.is_set():
try:
data = sock.recv(1024)
if not data:
add_message("[Connection lost]", is_system=True)
stop_event.set()
break
text = data.decode('utf-8')
if text.startswith("##users##"):
# update user list
userlist = text[9:].split(",") if len(text) > 9 else []
update_users([u for u in userlist if u]) # filter empty
else:
add_message(text)
except (ConnectionResetError, BrokenPipeError, OSError):
add_message("[Connection lost]", is_system=True)
stop_event.set()
break
threading.Thread(target=receiver, daemon=True).start()
# ─── TUI layout ─────────────────────────────────────────────
# Message history pane (top left)
class MessageHistory:
def __pt_formatted_text__(self):
fragments = []
for text, is_system in messages:
if is_system:
fragments.append(("class:system", f"• {text}\n"))
else:
fragments.append(("", f"{text}\n"))
return fragments
message_control = FormattedTextControl(MessageHistory())
message_window = Window(
content=message_control,
wrap_lines=True,
style="bg:#222222 #ffffff",
)
# User list pane (right side)
def get_user_list_text():
if not users:
return [("class:userlist", " (no one)\n")]
lines = []
for u in users:
lines.append(("class:userlist", f" {u}\n"))
return lines
user_list_control = FormattedTextControl(get_user_list_text)
user_list_window = Window(
content=user_list_control,
width=20,
style="bg:#333333 #aaaaaa",
align=WindowAlign.LEFT,
)
# Input field (bottom)
input_buffer = Buffer()
input_window = Window(
height=1,
content=BufferControl(buffer=input_buffer),
style="bg:#444444 #ffffff",
)
# Combine everything
root_container = HSplit([
VSplit([
message_window,
user_list_window,
]),
input_window,
])
# ─── key bindings ───────────────────────────────────────────
kb = KeyBindings()
@kb.add("enter")
def _(event):
text = input_buffer.text
if text:
if text == "/quit":
stop_event.set()
sock.close()
event.app.exit()
else:
try:
sock.sendall(text.encode('utf-8'))
except (BrokenPipeError, ConnectionResetError):
add_message("Cannot send message – disconnected", is_system=True)
input_buffer.text = ""
@kb.add("c-c")
def _(event):
stop_event.set()
sock.close()
event.app.exit()
# ─── styling ────────────────────────────────────────────────
style = Style([
("system", "fg:#888888 italic"),
("userlist", "fg:#88ff88"),
])
# ─── build the application ──────────────────────────────────
app = Application(
layout=Layout(root_container, focused_element=input_window),
key_bindings=kb,
style=style,
full_screen=True,
)
def run_tui():
try:
app.run()
except KeyboardInterrupt:
pass
finally:
stop_event.set()
sock.close()
if __name__ == "__main__":
run_tui()