Compare commits
9 Commits
f75adac97a
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 530c12d9c0 | |||
| 993ed4f051 | |||
| 42b958acdb | |||
| f1ed9c3d2c | |||
| 4ac9432db9 | |||
| fbaa4564e4 | |||
| a03b4b0b93 | |||
| e6e32033ad | |||
| 16b5209338 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -5,3 +5,4 @@ venv3/
|
|||||||
cvarlist.txt
|
cvarlist.txt
|
||||||
curztest.py
|
curztest.py
|
||||||
rconpw.txt
|
rconpw.txt
|
||||||
|
CLAUDE*.md
|
||||||
|
|||||||
33
cvars.py
33
cvars.py
@ -119,6 +119,39 @@ COMMANDS = [
|
|||||||
'killserver',
|
'killserver',
|
||||||
'quit',
|
'quit',
|
||||||
'team',
|
'team',
|
||||||
|
# Match control
|
||||||
|
'readyall',
|
||||||
|
'allready',
|
||||||
|
'abort',
|
||||||
|
'pause',
|
||||||
|
'unpause',
|
||||||
|
'lock',
|
||||||
|
'unlock',
|
||||||
|
'timeout',
|
||||||
|
'timein',
|
||||||
|
# Player management
|
||||||
|
'shuffle',
|
||||||
|
'put',
|
||||||
|
'mute',
|
||||||
|
'unmute',
|
||||||
|
'slap',
|
||||||
|
'slay',
|
||||||
|
# Server control
|
||||||
|
'restart',
|
||||||
|
'endgame',
|
||||||
|
'nextmap',
|
||||||
|
'forcemap',
|
||||||
|
# QLX commands
|
||||||
|
'qlx',
|
||||||
|
'elo',
|
||||||
|
'balance',
|
||||||
|
'teams',
|
||||||
|
'scores',
|
||||||
|
# Info commands
|
||||||
|
'serverinfo',
|
||||||
|
'players',
|
||||||
|
'maplist',
|
||||||
|
'configstrings',
|
||||||
]
|
]
|
||||||
|
|
||||||
# Bot names for addbot command (complete list)
|
# Bot names for addbot command (complete list)
|
||||||
|
|||||||
9
main.py
9
main.py
@ -367,6 +367,9 @@ def main_loop(screen):
|
|||||||
# Shutdown flag
|
# Shutdown flag
|
||||||
shutdown = False
|
shutdown = False
|
||||||
|
|
||||||
|
# Timer refresh tracking (update UI once per second)
|
||||||
|
last_ui_update = 0
|
||||||
|
|
||||||
# Setup JSON logging if requested
|
# Setup JSON logging if requested
|
||||||
json_logger = None
|
json_logger = None
|
||||||
if args.json_log:
|
if args.json_log:
|
||||||
@ -525,6 +528,12 @@ def main_loop(screen):
|
|||||||
formatted_msg, attributes = format_message(message)
|
formatted_msg, attributes = format_message(message)
|
||||||
ui.print_message(formatted_msg)
|
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:
|
finally:
|
||||||
# Clean up resources
|
# Clean up resources
|
||||||
logger.info("Shutting down...")
|
logger.info("Shutting down...")
|
||||||
|
|||||||
@ -94,6 +94,7 @@ class EventParser:
|
|||||||
# Get Match Time
|
# Get Match Time
|
||||||
if 'TIME' in data:
|
if 'TIME' in data:
|
||||||
self.game_state.server_info.match_time = int(data['TIME'])
|
self.game_state.server_info.match_time = int(data['TIME'])
|
||||||
|
self.game_state.server_info.match_time_last_sync = time.time()
|
||||||
|
|
||||||
if 'KILLER' not in data:
|
if 'KILLER' not in data:
|
||||||
return None
|
return None
|
||||||
@ -138,6 +139,7 @@ class EventParser:
|
|||||||
# Get Match Time
|
# Get Match Time
|
||||||
if 'TIME' in data:
|
if 'TIME' in data:
|
||||||
self.game_state.server_info.match_time = int(data['TIME'])
|
self.game_state.server_info.match_time = int(data['TIME'])
|
||||||
|
self.game_state.server_info.match_time_last_sync = time.time()
|
||||||
|
|
||||||
if 'VICTIM' not in data:
|
if 'VICTIM' not in data:
|
||||||
return None
|
return None
|
||||||
@ -244,6 +246,7 @@ class EventParser:
|
|||||||
# Get Match Time
|
# Get Match Time
|
||||||
if 'TIME' in data:
|
if 'TIME' in data:
|
||||||
self.game_state.server_info.match_time = int(data['TIME'])
|
self.game_state.server_info.match_time = int(data['TIME'])
|
||||||
|
self.game_state.server_info.match_time_last_sync = time.time()
|
||||||
|
|
||||||
team_won = data.get('TEAM_WON')
|
team_won = data.get('TEAM_WON')
|
||||||
round_num = data.get('ROUND', 0)
|
round_num = data.get('ROUND', 0)
|
||||||
@ -265,6 +268,7 @@ class EventParser:
|
|||||||
# Get Match Time
|
# Get Match Time
|
||||||
if 'TIME' in data:
|
if 'TIME' in data:
|
||||||
self.game_state.server_info.match_time = int(data['TIME'])
|
self.game_state.server_info.match_time = int(data['TIME'])
|
||||||
|
self.game_state.server_info.match_time_last_sync = time.time()
|
||||||
|
|
||||||
name = data.get('NAME', 'Unknown')
|
name = data.get('NAME', 'Unknown')
|
||||||
medal = data.get('MEDAL', 'UNKNOWN')
|
medal = data.get('MEDAL', 'UNKNOWN')
|
||||||
@ -299,6 +303,7 @@ class EventParser:
|
|||||||
# Get Match Time
|
# Get Match Time
|
||||||
if 'TIME' in data:
|
if 'TIME' in data:
|
||||||
self.game_state.server_info.match_time = int(data['TIME'])
|
self.game_state.server_info.match_time = int(data['TIME'])
|
||||||
|
self.game_state.server_info.match_time_last_sync = time.time()
|
||||||
|
|
||||||
if self.game_state.server_info.is_team_mode():
|
if self.game_state.server_info.is_team_mode():
|
||||||
return f"^8^2[GAME ON]^0 ^7Match has started - ^1^8RED ^0^7vs. ^4^8BLUE\n"
|
return f"^8^2[GAME ON]^0 ^7Match has started - ^1^8RED ^0^7vs. ^4^8BLUE\n"
|
||||||
@ -320,6 +325,7 @@ class EventParser:
|
|||||||
# Get Match Time
|
# Get Match Time
|
||||||
if 'TIME' in data:
|
if 'TIME' in data:
|
||||||
self.game_state.server_info.match_time = int(data['TIME'])
|
self.game_state.server_info.match_time = int(data['TIME'])
|
||||||
|
self.game_state.server_info.match_time_last_sync = time.time()
|
||||||
|
|
||||||
if not self.game_state.server_info.is_team_mode():
|
if not self.game_state.server_info.is_team_mode():
|
||||||
return None
|
return None
|
||||||
|
|||||||
1
state.py
1
state.py
@ -34,6 +34,7 @@ class ServerInfo:
|
|||||||
self.dead_players = {}
|
self.dead_players = {}
|
||||||
self.round_end_time = None
|
self.round_end_time = None
|
||||||
self.match_time = 0
|
self.match_time = 0
|
||||||
|
self.match_time_last_sync = 0 # Timestamp of last TIME update from server
|
||||||
|
|
||||||
def is_team_mode(self):
|
def is_team_mode(self):
|
||||||
"""Check if current gametype is a team mode"""
|
"""Check if current gametype is a team mode"""
|
||||||
|
|||||||
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.")
|
||||||
124
ui.py
124
ui.py
@ -9,6 +9,7 @@ import curses.textpad
|
|||||||
import threading
|
import threading
|
||||||
import queue
|
import queue
|
||||||
import logging
|
import logging
|
||||||
|
import time
|
||||||
from config import COLOR_PAIRS, INFO_WINDOW_HEIGHT, INFO_WINDOW_Y, OUTPUT_WINDOW_Y, INPUT_WINDOW_HEIGHT, TEAM_MODES, MAX_COMMAND_HISTORY
|
from config import COLOR_PAIRS, INFO_WINDOW_HEIGHT, INFO_WINDOW_Y, OUTPUT_WINDOW_Y, INPUT_WINDOW_HEIGHT, TEAM_MODES, MAX_COMMAND_HISTORY
|
||||||
from cvars import autocomplete, COMMAND_SIGNATURES, get_signature_with_highlight, get_argument_suggestions, COMMAND_ARGUMENTS
|
from cvars import autocomplete, COMMAND_SIGNATURES, get_signature_with_highlight, get_argument_suggestions, COMMAND_ARGUMENTS
|
||||||
|
|
||||||
@ -45,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
|
^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:
|
if not curses.has_colors:
|
||||||
|
try:
|
||||||
window.addstr(message)
|
window.addstr(message)
|
||||||
|
except curses.error:
|
||||||
|
pass
|
||||||
return
|
return
|
||||||
|
|
||||||
color = 0
|
color = 0
|
||||||
@ -68,13 +72,19 @@ def print_colored(window, message, attributes=0):
|
|||||||
elif ord('1') <= val <= ord('6'):
|
elif ord('1') <= val <= ord('6'):
|
||||||
color = val - ord('0')
|
color = val - ord('0')
|
||||||
else:
|
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('^', 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)
|
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
|
parse_color = False
|
||||||
elif ch == '^':
|
elif ch == '^':
|
||||||
parse_color = True
|
parse_color = True
|
||||||
else:
|
else:
|
||||||
|
try:
|
||||||
window.addch(ch, 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
|
||||||
|
|
||||||
def update_autocomplete_display(window, current_input, first_word, words, ends_with_space):
|
def update_autocomplete_display(window, current_input, first_word, words, ends_with_space):
|
||||||
"""
|
"""
|
||||||
@ -105,7 +115,7 @@ def update_autocomplete_display(window, current_input, first_word, words, ends_w
|
|||||||
else:
|
else:
|
||||||
window.addstr(1, x_pos, arg_text, curses.A_DIM)
|
window.addstr(1, x_pos, arg_text, curses.A_DIM)
|
||||||
x_pos += len(arg_text) + 1
|
x_pos += len(arg_text) + 1
|
||||||
except:
|
except curses.error:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
# User is typing arguments
|
# User is typing arguments
|
||||||
@ -134,7 +144,7 @@ def update_autocomplete_display(window, current_input, first_word, words, ends_w
|
|||||||
match_line = f'<{arg_type}>: {" ".join(display_suggestions)}{more_indicator}'
|
match_line = f'<{arg_type}>: {" ".join(display_suggestions)}{more_indicator}'
|
||||||
try:
|
try:
|
||||||
window.addstr(1, 0, match_line, curses.A_DIM)
|
window.addstr(1, 0, match_line, curses.A_DIM)
|
||||||
except:
|
except curses.error:
|
||||||
pass
|
pass
|
||||||
suggestions = arg_suggestions # Store for Tab cycling
|
suggestions = arg_suggestions # Store for Tab cycling
|
||||||
suggestion_index = -1
|
suggestion_index = -1
|
||||||
@ -151,7 +161,7 @@ def update_autocomplete_display(window, current_input, first_word, words, ends_w
|
|||||||
else:
|
else:
|
||||||
window.addstr(1, x_pos, arg_text, curses.A_DIM)
|
window.addstr(1, x_pos, arg_text, curses.A_DIM)
|
||||||
x_pos += len(arg_text) + 1
|
x_pos += len(arg_text) + 1
|
||||||
except:
|
except curses.error:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
elif first_word in COMMAND_SIGNATURES and COMMAND_SIGNATURES[first_word]:
|
elif first_word in COMMAND_SIGNATURES and COMMAND_SIGNATURES[first_word]:
|
||||||
@ -166,7 +176,7 @@ def update_autocomplete_display(window, current_input, first_word, words, ends_w
|
|||||||
else:
|
else:
|
||||||
window.addstr(1, x_pos, arg_text, curses.A_DIM)
|
window.addstr(1, x_pos, arg_text, curses.A_DIM)
|
||||||
x_pos += len(arg_text) + 1
|
x_pos += len(arg_text) + 1
|
||||||
except:
|
except curses.error:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@ -180,7 +190,7 @@ def update_autocomplete_display(window, current_input, first_word, words, ends_w
|
|||||||
match_line = ' '.join(suggestions)
|
match_line = ' '.join(suggestions)
|
||||||
try:
|
try:
|
||||||
window.addstr(1, 0, match_line, curses.A_DIM)
|
window.addstr(1, 0, match_line, curses.A_DIM)
|
||||||
except:
|
except curses.error:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return suggestions, suggestion_index, original_word
|
return suggestions, suggestion_index, original_word
|
||||||
@ -199,6 +209,7 @@ class UIManager:
|
|||||||
self.input_queue = None
|
self.input_queue = None
|
||||||
self.command_history = []
|
self.command_history = []
|
||||||
self.history_index = -1
|
self.history_index = -1
|
||||||
|
self.cursor_pos = 0 # Track cursor position in input
|
||||||
|
|
||||||
self._init_curses()
|
self._init_curses()
|
||||||
self._create_windows()
|
self._create_windows()
|
||||||
@ -228,6 +239,10 @@ class UIManager:
|
|||||||
"""Create all UI windows"""
|
"""Create all UI windows"""
|
||||||
maxy, maxx = self.screen.getmaxyx()
|
maxy, maxx = self.screen.getmaxyx()
|
||||||
|
|
||||||
|
# Minimum terminal size check
|
||||||
|
if maxy < 20 or maxx < 80:
|
||||||
|
return False
|
||||||
|
|
||||||
# Server info window (top)
|
# Server info window (top)
|
||||||
self.info_window = curses.newwin(
|
self.info_window = curses.newwin(
|
||||||
INFO_WINDOW_HEIGHT,
|
INFO_WINDOW_HEIGHT,
|
||||||
@ -283,12 +298,60 @@ class UIManager:
|
|||||||
|
|
||||||
self.screen.noutrefresh()
|
self.screen.noutrefresh()
|
||||||
curses.doupdate()
|
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):
|
def setup_input_queue(self):
|
||||||
"""Setup threaded input queue with command history and autocomplete"""
|
"""Setup threaded input queue with command history and autocomplete"""
|
||||||
def wait_stdin(q, window, manager):
|
def wait_stdin(q, window, manager):
|
||||||
current_input = ""
|
current_input = ""
|
||||||
cursor_pos = 0
|
cursor_pos = 0
|
||||||
|
manager.cursor_pos = 0 # Keep manager in sync
|
||||||
temp_history_index = -1
|
temp_history_index = -1
|
||||||
temp_input = "" # Temp storage when navigating history
|
temp_input = "" # Temp storage when navigating history
|
||||||
quit_confirm = False
|
quit_confirm = False
|
||||||
@ -305,6 +368,17 @@ class UIManager:
|
|||||||
if key == -1: # No input
|
if key == -1: # No input
|
||||||
continue
|
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
|
# Tab key - cycle through suggestions
|
||||||
if key == ord('\t') or key == 9:
|
if key == ord('\t') or key == 9:
|
||||||
if suggestions:
|
if suggestions:
|
||||||
@ -324,6 +398,7 @@ class UIManager:
|
|||||||
words[-1] = suggestions[suggestion_index]
|
words[-1] = suggestions[suggestion_index]
|
||||||
current_input = ' '.join(words)
|
current_input = ' '.join(words)
|
||||||
cursor_pos = len(current_input)
|
cursor_pos = len(current_input)
|
||||||
|
manager.cursor_pos = cursor_pos
|
||||||
|
|
||||||
# Update display
|
# Update display
|
||||||
window.erase()
|
window.erase()
|
||||||
@ -367,7 +442,7 @@ class UIManager:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
window.addstr(1, 0, display_line, curses.A_DIM)
|
window.addstr(1, 0, display_line, curses.A_DIM)
|
||||||
except:
|
except curses.error:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
window.move(0, cursor_pos)
|
window.move(0, cursor_pos)
|
||||||
@ -386,6 +461,7 @@ class UIManager:
|
|||||||
q.put(current_input)
|
q.put(current_input)
|
||||||
current_input = ""
|
current_input = ""
|
||||||
cursor_pos = 0
|
cursor_pos = 0
|
||||||
|
manager.cursor_pos = cursor_pos
|
||||||
temp_history_index = -1
|
temp_history_index = -1
|
||||||
temp_input = ""
|
temp_input = ""
|
||||||
suggestions = []
|
suggestions = []
|
||||||
@ -406,6 +482,7 @@ class UIManager:
|
|||||||
temp_history_index -= 1
|
temp_history_index -= 1
|
||||||
current_input = manager.command_history[temp_history_index]
|
current_input = manager.command_history[temp_history_index]
|
||||||
cursor_pos = len(current_input)
|
cursor_pos = len(current_input)
|
||||||
|
manager.cursor_pos = cursor_pos
|
||||||
suggestions = []
|
suggestions = []
|
||||||
suggestion_index = -1
|
suggestion_index = -1
|
||||||
original_word = ""
|
original_word = ""
|
||||||
@ -426,6 +503,7 @@ class UIManager:
|
|||||||
current_input = manager.command_history[temp_history_index]
|
current_input = manager.command_history[temp_history_index]
|
||||||
|
|
||||||
cursor_pos = len(current_input)
|
cursor_pos = len(current_input)
|
||||||
|
manager.cursor_pos = cursor_pos
|
||||||
suggestions = []
|
suggestions = []
|
||||||
suggestion_index = -1
|
suggestion_index = -1
|
||||||
original_word = ""
|
original_word = ""
|
||||||
@ -437,6 +515,7 @@ class UIManager:
|
|||||||
elif key == curses.KEY_LEFT:
|
elif key == curses.KEY_LEFT:
|
||||||
if cursor_pos > 0:
|
if cursor_pos > 0:
|
||||||
cursor_pos -= 1
|
cursor_pos -= 1
|
||||||
|
manager.cursor_pos = cursor_pos
|
||||||
window.move(0, cursor_pos)
|
window.move(0, cursor_pos)
|
||||||
window.noutrefresh()
|
window.noutrefresh()
|
||||||
|
|
||||||
@ -444,6 +523,7 @@ class UIManager:
|
|||||||
elif key == curses.KEY_RIGHT:
|
elif key == curses.KEY_RIGHT:
|
||||||
if cursor_pos < len(current_input):
|
if cursor_pos < len(current_input):
|
||||||
cursor_pos += 1
|
cursor_pos += 1
|
||||||
|
manager.cursor_pos = cursor_pos
|
||||||
window.move(0, cursor_pos)
|
window.move(0, cursor_pos)
|
||||||
window.noutrefresh()
|
window.noutrefresh()
|
||||||
|
|
||||||
@ -452,6 +532,7 @@ class UIManager:
|
|||||||
if cursor_pos > 0:
|
if cursor_pos > 0:
|
||||||
current_input = current_input[:cursor_pos-1] + current_input[cursor_pos:]
|
current_input = current_input[:cursor_pos-1] + current_input[cursor_pos:]
|
||||||
cursor_pos -= 1
|
cursor_pos -= 1
|
||||||
|
manager.cursor_pos = cursor_pos
|
||||||
temp_history_index = -1 # Exit history mode
|
temp_history_index = -1 # Exit history mode
|
||||||
|
|
||||||
window.erase()
|
window.erase()
|
||||||
@ -476,6 +557,7 @@ class UIManager:
|
|||||||
char = chr(key)
|
char = chr(key)
|
||||||
current_input = current_input[:cursor_pos] + char + current_input[cursor_pos:]
|
current_input = current_input[:cursor_pos] + char + current_input[cursor_pos:]
|
||||||
cursor_pos += 1
|
cursor_pos += 1
|
||||||
|
manager.cursor_pos = cursor_pos
|
||||||
temp_history_index = -1 # Exit history mode
|
temp_history_index = -1 # Exit history mode
|
||||||
|
|
||||||
window.erase()
|
window.erase()
|
||||||
@ -496,8 +578,7 @@ class UIManager:
|
|||||||
curses.doupdate() # Immediate screen update
|
curses.doupdate() # Immediate screen update
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
import logging
|
logger.error(f'Input error: {e}')
|
||||||
logging.getLogger('ui').error(f'Input error: {e}')
|
|
||||||
# Log but continue - input thread should stay alive
|
# Log but continue - input thread should stay alive
|
||||||
|
|
||||||
window.move(0, cursor_pos)
|
window.move(0, cursor_pos)
|
||||||
@ -521,7 +602,9 @@ class UIManager:
|
|||||||
"""Print formatted message to output window"""
|
"""Print formatted message to output window"""
|
||||||
print_colored(self.output_window, message, attributes)
|
print_colored(self.output_window, message, attributes)
|
||||||
self.output_window.noutrefresh()
|
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()
|
curses.doupdate()
|
||||||
|
|
||||||
def update_server_info(self, game_state):
|
def update_server_info(self, game_state):
|
||||||
@ -536,8 +619,14 @@ class UIManager:
|
|||||||
|
|
||||||
timer_display = ""
|
timer_display = ""
|
||||||
if server_info.match_time > 0 and not server_info.warmup:
|
if server_info.match_time > 0 and not server_info.warmup:
|
||||||
mins = server_info.match_time // 60
|
# Calculate live time: add elapsed seconds since last server update
|
||||||
secs = server_info.match_time % 60
|
current_time = server_info.match_time
|
||||||
|
if server_info.match_time_last_sync > 0:
|
||||||
|
elapsed = int(time.time() - server_info.match_time_last_sync)
|
||||||
|
current_time += elapsed
|
||||||
|
|
||||||
|
mins = current_time // 60
|
||||||
|
secs = current_time % 60
|
||||||
timer_display = f"^3^0Time:^8^7 {mins}:{secs:02d}^0"
|
timer_display = f"^3^0Time:^8^7 {mins}:{secs:02d}^0"
|
||||||
else:
|
else:
|
||||||
timer_display = "^3^0Time:^8^7 0:00^0"
|
timer_display = "^3^0Time:^8^7 0:00^0"
|
||||||
@ -573,7 +662,10 @@ class UIManager:
|
|||||||
f"{limit_display}^0\n", 0)
|
f"{limit_display}^0\n", 0)
|
||||||
|
|
||||||
# Blank lines to fill
|
# Blank lines to fill
|
||||||
|
try:
|
||||||
self.info_window.addstr("\n")
|
self.info_window.addstr("\n")
|
||||||
|
except curses.error:
|
||||||
|
pass
|
||||||
|
|
||||||
# Line 3: Team headers and player lists
|
# Line 3: Team headers and player lists
|
||||||
teams = game_state.player_tracker.get_players_by_team()
|
teams = game_state.player_tracker.get_players_by_team()
|
||||||
@ -688,7 +780,10 @@ class UIManager:
|
|||||||
print_colored(self.info_window, line, 0)
|
print_colored(self.info_window, line, 0)
|
||||||
|
|
||||||
# Blank lines to fill
|
# Blank lines to fill
|
||||||
|
try:
|
||||||
self.info_window.addstr("\n")
|
self.info_window.addstr("\n")
|
||||||
|
except curses.error:
|
||||||
|
pass
|
||||||
|
|
||||||
# List spectators on one line
|
# List spectators on one line
|
||||||
spec_list = " ".join(spec_players)
|
spec_list = " ".join(spec_players)
|
||||||
@ -696,12 +791,17 @@ class UIManager:
|
|||||||
print_colored(self.info_window, line, 0)
|
print_colored(self.info_window, line, 0)
|
||||||
|
|
||||||
# Blank lines to fill
|
# Blank lines to fill
|
||||||
|
try:
|
||||||
self.info_window.addstr("\n")
|
self.info_window.addstr("\n")
|
||||||
|
except curses.error:
|
||||||
|
pass
|
||||||
|
|
||||||
# Separator
|
# Separator
|
||||||
separator = "^7" + "═" * (max_x - 1) + "^7"
|
separator = "^7" + "═" * (max_x - 1) + "^7"
|
||||||
print_colored(self.info_window, separator, 0)
|
print_colored(self.info_window, separator, 0)
|
||||||
|
|
||||||
self.info_window.noutrefresh()
|
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()
|
curses.doupdate()
|
||||||
|
|||||||
Reference in New Issue
Block a user