Compare commits
6 Commits
a03b4b0b93
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 530c12d9c0 | |||
| 993ed4f051 | |||
| 42b958acdb | |||
| f1ed9c3d2c | |||
| 4ac9432db9 | |||
| fbaa4564e4 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -5,3 +5,4 @@ venv3/
|
||||
cvarlist.txt
|
||||
curztest.py
|
||||
rconpw.txt
|
||||
CLAUDE*.md
|
||||
|
||||
9
main.py
9
main.py
@ -367,6 +367,9 @@ def main_loop(screen):
|
||||
# Shutdown flag
|
||||
shutdown = False
|
||||
|
||||
# Timer refresh tracking (update UI once per second)
|
||||
last_ui_update = 0
|
||||
|
||||
# Setup JSON logging if requested
|
||||
json_logger = None
|
||||
if args.json_log:
|
||||
@ -525,6 +528,12 @@ def main_loop(screen):
|
||||
formatted_msg, attributes = format_message(message)
|
||||
ui.print_message(formatted_msg)
|
||||
|
||||
# Update server info panel every second (for live timer display)
|
||||
current_time = time.time()
|
||||
if current_time - last_ui_update >= 1.0:
|
||||
ui.update_server_info(game_state)
|
||||
last_ui_update = current_time
|
||||
|
||||
finally:
|
||||
# Clean up resources
|
||||
logger.info("Shutting down...")
|
||||
|
||||
234
test_ui.py
Executable file
234
test_ui.py
Executable file
@ -0,0 +1,234 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test mode for QLPyCon - Simulates game events to test UI
|
||||
Run with: python3 test_ui.py
|
||||
"""
|
||||
|
||||
import curses
|
||||
import time
|
||||
import json
|
||||
import logging
|
||||
import queue
|
||||
import threading
|
||||
import sys
|
||||
|
||||
from config import VERSION
|
||||
from state import GameState
|
||||
from parser import EventParser
|
||||
from ui import UIManager
|
||||
|
||||
logger = logging.getLogger('test_ui')
|
||||
|
||||
def simulate_game_events(game_state, parser, ui):
|
||||
"""Simulate game events for testing"""
|
||||
|
||||
# Wait for UI to initialize
|
||||
time.sleep(1)
|
||||
|
||||
ui.print_message("=== TEST MODE - SIMULATING GAME EVENTS ===\n")
|
||||
ui.print_message(f"QLPyCon {VERSION} Test Mode\n\n")
|
||||
|
||||
# Event 1: Match start
|
||||
time.sleep(2)
|
||||
event = {
|
||||
"TYPE": "MATCH_STARTED",
|
||||
"DATA": {
|
||||
"TIME": 0,
|
||||
"PLAYERS": [
|
||||
{"NAME": "^1RedWarrior"},
|
||||
{"NAME": "^4BlueSniper"}
|
||||
]
|
||||
}
|
||||
}
|
||||
result = parser.parse_event(json.dumps(event))
|
||||
if result:
|
||||
ui.print_message(result)
|
||||
ui.update_server_info(game_state)
|
||||
|
||||
# Event 2: Players join teams
|
||||
time.sleep(2)
|
||||
event = {
|
||||
"TYPE": "PLAYER_SWITCHTEAM",
|
||||
"DATA": {
|
||||
"TIME": 5,
|
||||
"KILLER": {
|
||||
"NAME": "^1RedWarrior",
|
||||
"TEAM": "RED",
|
||||
"OLD_TEAM": "SPECTATOR"
|
||||
}
|
||||
}
|
||||
}
|
||||
result = parser.parse_event(json.dumps(event))
|
||||
if result:
|
||||
ui.print_message(result)
|
||||
ui.update_server_info(game_state)
|
||||
|
||||
time.sleep(1)
|
||||
event = {
|
||||
"TYPE": "PLAYER_SWITCHTEAM",
|
||||
"DATA": {
|
||||
"TIME": 6,
|
||||
"KILLER": {
|
||||
"NAME": "^4BlueSniper",
|
||||
"TEAM": "BLUE",
|
||||
"OLD_TEAM": "SPECTATOR"
|
||||
}
|
||||
}
|
||||
}
|
||||
result = parser.parse_event(json.dumps(event))
|
||||
if result:
|
||||
ui.print_message(result)
|
||||
ui.update_server_info(game_state)
|
||||
|
||||
# Event 3: Some kills
|
||||
for i in range(10):
|
||||
time.sleep(3)
|
||||
match_time = 10 + (i * 5)
|
||||
|
||||
event = {
|
||||
"TYPE": "PLAYER_DEATH",
|
||||
"DATA": {
|
||||
"TIME": match_time,
|
||||
"KILLER": {
|
||||
"NAME": "^1RedWarrior",
|
||||
"TEAM": "RED",
|
||||
"WEAPON": ["ROCKET", "RAILGUN", "LIGHTNING"][i % 3],
|
||||
"HEALTH": 100 - (i * 10)
|
||||
},
|
||||
"VICTIM": {
|
||||
"NAME": "^4BlueSniper",
|
||||
"TEAM": "BLUE"
|
||||
}
|
||||
}
|
||||
}
|
||||
result = parser.parse_event(json.dumps(event))
|
||||
if result:
|
||||
ui.print_message(result)
|
||||
ui.update_server_info(game_state)
|
||||
|
||||
# Counter-kill
|
||||
time.sleep(2)
|
||||
event = {
|
||||
"TYPE": "PLAYER_DEATH",
|
||||
"DATA": {
|
||||
"TIME": match_time + 2,
|
||||
"KILLER": {
|
||||
"NAME": "^4BlueSniper",
|
||||
"TEAM": "BLUE",
|
||||
"WEAPON": ["PLASMA", "GRENADE", "SHOTGUN"][i % 3],
|
||||
"HEALTH": 50
|
||||
},
|
||||
"VICTIM": {
|
||||
"NAME": "^1RedWarrior",
|
||||
"TEAM": "RED"
|
||||
}
|
||||
}
|
||||
}
|
||||
result = parser.parse_event(json.dumps(event))
|
||||
if result:
|
||||
ui.print_message(result)
|
||||
ui.update_server_info(game_state)
|
||||
|
||||
# Event 4: Chat messages
|
||||
time.sleep(2)
|
||||
ui.print_message("^1RedWarrior^7: ^2gg!\n")
|
||||
time.sleep(1)
|
||||
ui.print_message("^4BlueSniper^7: ^2nice shots\n")
|
||||
|
||||
# Event 5: Medal
|
||||
time.sleep(2)
|
||||
event = {
|
||||
"TYPE": "PLAYER_MEDAL",
|
||||
"DATA": {
|
||||
"TIME": 120,
|
||||
"NAME": "^1RedWarrior",
|
||||
"MEDAL": "EXCELLENT"
|
||||
}
|
||||
}
|
||||
result = parser.parse_event(json.dumps(event))
|
||||
if result:
|
||||
ui.print_message(result)
|
||||
|
||||
# Event 6: Match end
|
||||
time.sleep(3)
|
||||
event = {
|
||||
"TYPE": "MATCH_REPORT",
|
||||
"DATA": {
|
||||
"TIME": 180,
|
||||
"TSCORE0": "12",
|
||||
"TSCORE1": "8"
|
||||
}
|
||||
}
|
||||
result = parser.parse_event(json.dumps(event))
|
||||
if result:
|
||||
ui.print_message(result)
|
||||
ui.update_server_info(game_state)
|
||||
|
||||
ui.print_message("\n=== TEST COMPLETE ===\n")
|
||||
ui.print_message("Press Ctrl-C twice to quit\n")
|
||||
|
||||
def test_main(screen):
|
||||
"""Main test loop"""
|
||||
|
||||
# Initialize UI
|
||||
ui = UIManager(screen, "tcp://127.0.0.1:27961 [TEST MODE]")
|
||||
|
||||
# Setup logging
|
||||
log_handler = ui.setup_logging()
|
||||
logger.addHandler(log_handler)
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
# Initialize game state
|
||||
game_state = GameState()
|
||||
|
||||
# Set some initial server info
|
||||
game_state.server_info.hostname = "^3Test Server [Demo Mode]"
|
||||
game_state.server_info.map = "bloodrun"
|
||||
game_state.server_info.gametype = "Team Deathmatch"
|
||||
game_state.server_info.timelimit = "10"
|
||||
game_state.server_info.fraglimit = "50"
|
||||
game_state.server_info.maxclients = "16"
|
||||
|
||||
ui.update_server_info(game_state)
|
||||
|
||||
# Create parser
|
||||
parser = EventParser(game_state)
|
||||
|
||||
# Display startup message
|
||||
ui.print_message(f"*** QL pyCon Version {VERSION} - TEST MODE ***\n")
|
||||
ui.print_message("This mode simulates game events to test the UI\n")
|
||||
ui.print_message("Watch the timer tick in real-time!\n\n")
|
||||
|
||||
# Start event simulation in background thread
|
||||
sim_thread = threading.Thread(
|
||||
target=simulate_game_events,
|
||||
args=(game_state, parser, ui),
|
||||
daemon=True
|
||||
)
|
||||
sim_thread.start()
|
||||
|
||||
# Setup input queue (even though we won't use it much)
|
||||
input_queue = ui.setup_input_queue()
|
||||
|
||||
# Main loop - just keep updating UI
|
||||
try:
|
||||
while True:
|
||||
# Update server info display (this will show live timer)
|
||||
ui.update_server_info(game_state)
|
||||
|
||||
# Process any user input
|
||||
while not input_queue.empty():
|
||||
command = input_queue.get()
|
||||
ui.print_message(f">>> {command}\n")
|
||||
ui.print_message("[Test mode: commands not sent to server]\n")
|
||||
|
||||
time.sleep(0.1) # Update 10 times per second
|
||||
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
curses.wrapper(test_main)
|
||||
except KeyboardInterrupt:
|
||||
print("\nTest mode exited.")
|
||||
98
ui.py
98
ui.py
@ -46,7 +46,10 @@ def print_colored(window, message, attributes=0):
|
||||
^0 = reset, ^1 = red, ^2 = green, ^3 = yellow, ^4 = blue, ^5 = cyan, ^6 = magenta, ^7 = white, ^8 = bold, ^9 = underline
|
||||
"""
|
||||
if not curses.has_colors:
|
||||
try:
|
||||
window.addstr(message)
|
||||
except curses.error:
|
||||
pass
|
||||
return
|
||||
|
||||
color = 0
|
||||
@ -69,13 +72,19 @@ def print_colored(window, message, attributes=0):
|
||||
elif ord('1') <= val <= ord('6'):
|
||||
color = val - ord('0')
|
||||
else:
|
||||
try:
|
||||
window.addch('^', curses.color_pair(color) | (curses.A_BOLD if bold else 0) | (curses.A_UNDERLINE if underline else 0) | attributes)
|
||||
window.addch(ch, curses.color_pair(color) | (curses.A_BOLD if bold else 0) | (curses.A_UNDERLINE if underline else 0) | attributes)
|
||||
except curses.error:
|
||||
return
|
||||
parse_color = False
|
||||
elif ch == '^':
|
||||
parse_color = True
|
||||
else:
|
||||
try:
|
||||
window.addch(ch, curses.color_pair(color) | (curses.A_BOLD if bold else 0) | (curses.A_UNDERLINE if underline else 0) | attributes)
|
||||
except curses.error:
|
||||
return
|
||||
|
||||
def update_autocomplete_display(window, current_input, first_word, words, ends_with_space):
|
||||
"""
|
||||
@ -200,6 +209,7 @@ class UIManager:
|
||||
self.input_queue = None
|
||||
self.command_history = []
|
||||
self.history_index = -1
|
||||
self.cursor_pos = 0 # Track cursor position in input
|
||||
|
||||
self._init_curses()
|
||||
self._create_windows()
|
||||
@ -229,6 +239,10 @@ class UIManager:
|
||||
"""Create all UI windows"""
|
||||
maxy, maxx = self.screen.getmaxyx()
|
||||
|
||||
# Minimum terminal size check
|
||||
if maxy < 20 or maxx < 80:
|
||||
return False
|
||||
|
||||
# Server info window (top)
|
||||
self.info_window = curses.newwin(
|
||||
INFO_WINDOW_HEIGHT,
|
||||
@ -284,12 +298,60 @@ class UIManager:
|
||||
|
||||
self.screen.noutrefresh()
|
||||
curses.doupdate()
|
||||
return True
|
||||
|
||||
def handle_resize(self):
|
||||
"""Handle terminal resize event"""
|
||||
try:
|
||||
# Get new terminal dimensions
|
||||
maxy, maxx = self.screen.getmaxyx()
|
||||
|
||||
# Minimum size check
|
||||
if maxy < 20 or maxx < 80:
|
||||
return False
|
||||
|
||||
# Update screen
|
||||
curses.update_lines_cols()
|
||||
self.screen.clear()
|
||||
self.screen.addstr(0, 0, f"Quake Live PyCon: {self.host}")
|
||||
self.screen.noutrefresh()
|
||||
|
||||
# Recreate windows with new dimensions
|
||||
self.info_window.resize(INFO_WINDOW_HEIGHT, maxx - 4)
|
||||
self.info_window.mvwin(INFO_WINDOW_Y, 2)
|
||||
|
||||
self.output_window.resize(maxy - 17, maxx - 4)
|
||||
self.output_window.mvwin(OUTPUT_WINDOW_Y, 2)
|
||||
|
||||
self.divider_window.resize(1, maxx - 4)
|
||||
self.divider_window.mvwin(maxy - 3, 2)
|
||||
self.divider_window.clear()
|
||||
self.divider_window.hline(curses.ACS_HLINE, maxx - 4)
|
||||
|
||||
self.input_window.resize(INPUT_WINDOW_HEIGHT, maxx - 6)
|
||||
self.input_window.mvwin(maxy - 2, 4)
|
||||
|
||||
self.screen.addstr(maxy - 2, 2, '$ ')
|
||||
|
||||
# Refresh all windows
|
||||
self.info_window.noutrefresh()
|
||||
self.output_window.noutrefresh()
|
||||
self.divider_window.noutrefresh()
|
||||
self.input_window.noutrefresh()
|
||||
self.screen.noutrefresh()
|
||||
curses.doupdate()
|
||||
|
||||
return True
|
||||
|
||||
except curses.error:
|
||||
return False
|
||||
|
||||
def setup_input_queue(self):
|
||||
"""Setup threaded input queue with command history and autocomplete"""
|
||||
def wait_stdin(q, window, manager):
|
||||
current_input = ""
|
||||
cursor_pos = 0
|
||||
manager.cursor_pos = 0 # Keep manager in sync
|
||||
temp_history_index = -1
|
||||
temp_input = "" # Temp storage when navigating history
|
||||
quit_confirm = False
|
||||
@ -306,6 +368,17 @@ class UIManager:
|
||||
if key == -1: # No input
|
||||
continue
|
||||
|
||||
# Handle terminal resize
|
||||
if key == curses.KEY_RESIZE:
|
||||
manager.handle_resize()
|
||||
# Redraw input
|
||||
window.erase()
|
||||
window.addstr(0, 0, current_input)
|
||||
window.move(0, cursor_pos)
|
||||
window.noutrefresh()
|
||||
curses.doupdate()
|
||||
continue
|
||||
|
||||
# Tab key - cycle through suggestions
|
||||
if key == ord('\t') or key == 9:
|
||||
if suggestions:
|
||||
@ -325,6 +398,7 @@ class UIManager:
|
||||
words[-1] = suggestions[suggestion_index]
|
||||
current_input = ' '.join(words)
|
||||
cursor_pos = len(current_input)
|
||||
manager.cursor_pos = cursor_pos
|
||||
|
||||
# Update display
|
||||
window.erase()
|
||||
@ -387,6 +461,7 @@ class UIManager:
|
||||
q.put(current_input)
|
||||
current_input = ""
|
||||
cursor_pos = 0
|
||||
manager.cursor_pos = cursor_pos
|
||||
temp_history_index = -1
|
||||
temp_input = ""
|
||||
suggestions = []
|
||||
@ -407,6 +482,7 @@ class UIManager:
|
||||
temp_history_index -= 1
|
||||
current_input = manager.command_history[temp_history_index]
|
||||
cursor_pos = len(current_input)
|
||||
manager.cursor_pos = cursor_pos
|
||||
suggestions = []
|
||||
suggestion_index = -1
|
||||
original_word = ""
|
||||
@ -427,6 +503,7 @@ class UIManager:
|
||||
current_input = manager.command_history[temp_history_index]
|
||||
|
||||
cursor_pos = len(current_input)
|
||||
manager.cursor_pos = cursor_pos
|
||||
suggestions = []
|
||||
suggestion_index = -1
|
||||
original_word = ""
|
||||
@ -438,6 +515,7 @@ class UIManager:
|
||||
elif key == curses.KEY_LEFT:
|
||||
if cursor_pos > 0:
|
||||
cursor_pos -= 1
|
||||
manager.cursor_pos = cursor_pos
|
||||
window.move(0, cursor_pos)
|
||||
window.noutrefresh()
|
||||
|
||||
@ -445,6 +523,7 @@ class UIManager:
|
||||
elif key == curses.KEY_RIGHT:
|
||||
if cursor_pos < len(current_input):
|
||||
cursor_pos += 1
|
||||
manager.cursor_pos = cursor_pos
|
||||
window.move(0, cursor_pos)
|
||||
window.noutrefresh()
|
||||
|
||||
@ -453,6 +532,7 @@ class UIManager:
|
||||
if cursor_pos > 0:
|
||||
current_input = current_input[:cursor_pos-1] + current_input[cursor_pos:]
|
||||
cursor_pos -= 1
|
||||
manager.cursor_pos = cursor_pos
|
||||
temp_history_index = -1 # Exit history mode
|
||||
|
||||
window.erase()
|
||||
@ -477,6 +557,7 @@ class UIManager:
|
||||
char = chr(key)
|
||||
current_input = current_input[:cursor_pos] + char + current_input[cursor_pos:]
|
||||
cursor_pos += 1
|
||||
manager.cursor_pos = cursor_pos
|
||||
temp_history_index = -1 # Exit history mode
|
||||
|
||||
window.erase()
|
||||
@ -521,7 +602,9 @@ class UIManager:
|
||||
"""Print formatted message to output window"""
|
||||
print_colored(self.output_window, message, attributes)
|
||||
self.output_window.noutrefresh()
|
||||
self.input_window.move(0, 0)
|
||||
# Restore cursor to input window at current position
|
||||
self.input_window.move(0, self.cursor_pos)
|
||||
self.input_window.noutrefresh()
|
||||
curses.doupdate()
|
||||
|
||||
def update_server_info(self, game_state):
|
||||
@ -579,7 +662,10 @@ class UIManager:
|
||||
f"{limit_display}^0\n", 0)
|
||||
|
||||
# Blank lines to fill
|
||||
try:
|
||||
self.info_window.addstr("\n")
|
||||
except curses.error:
|
||||
pass
|
||||
|
||||
# Line 3: Team headers and player lists
|
||||
teams = game_state.player_tracker.get_players_by_team()
|
||||
@ -694,7 +780,10 @@ class UIManager:
|
||||
print_colored(self.info_window, line, 0)
|
||||
|
||||
# Blank lines to fill
|
||||
try:
|
||||
self.info_window.addstr("\n")
|
||||
except curses.error:
|
||||
pass
|
||||
|
||||
# List spectators on one line
|
||||
spec_list = " ".join(spec_players)
|
||||
@ -702,12 +791,17 @@ class UIManager:
|
||||
print_colored(self.info_window, line, 0)
|
||||
|
||||
# Blank lines to fill
|
||||
try:
|
||||
self.info_window.addstr("\n")
|
||||
except curses.error:
|
||||
pass
|
||||
|
||||
# Separator
|
||||
separator = "^7" + "═" * (max_x - 1) + "^7"
|
||||
print_colored(self.info_window, separator, 0)
|
||||
|
||||
self.info_window.noutrefresh()
|
||||
|
||||
# Restore cursor to input window at current position
|
||||
self.input_window.move(0, self.cursor_pos)
|
||||
self.input_window.noutrefresh()
|
||||
curses.doupdate()
|
||||
|
||||
Reference in New Issue
Block a user