From 9fdc7b9cad409ab0ee03d9f60ef2d1bce2dd03a1 Mon Sep 17 00:00:00 2001 From: Jared Miller Date: Wed, 11 Feb 2026 23:37:54 -0500 Subject: [PATCH] Add client command to show protocol and terminal info --- src/mudlib/commands/help.py | 76 +++++++++++++++++++++- tests/test_help.py | 122 ++++++++++++++++++++++++++++++++++++ 2 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 tests/test_help.py diff --git a/src/mudlib/commands/help.py b/src/mudlib/commands/help.py index b9b1d60..64f3b26 100644 --- a/src/mudlib/commands/help.py +++ b/src/mudlib/commands/help.py @@ -260,6 +260,69 @@ async def cmd_skills(player: Player, args: str) -> None: await player.send(" ".join(names) + "\r\n") +async def cmd_client(player: Player, args: str) -> None: + """Show client protocol negotiation and terminal capabilities.""" + lines = ["client protocols"] + + # Protocol status + gmcp = "active" if player.gmcp_enabled else "not active" + msdp = "active" if player.msdp_enabled else "not active" + lines.append(f" GMCP: {gmcp}") + lines.append(f" MSDP: {msdp}") + + # Terminal info + lines.append("terminal") + + # Terminal type from TTYPE negotiation + ttype = None + if player.writer is not None: + ttype = player.writer.get_extra_info("TERM") or None + lines.append(f" type: {ttype or 'unknown'}") + + # Terminal size from NAWS + cols, rows = 80, 24 + if player.writer is not None: + cols = player.writer.get_extra_info("cols") or 80 + rows = player.writer.get_extra_info("rows") or 24 + lines.append(f" size: {cols}x{rows}") + + # Color depth + lines.append(f" colors: {player.color_depth}") + + # MTTS capabilities + caps = player.caps + mtts_flags = [] + if caps.ansi: + mtts_flags.append("ANSI") + if caps.vt100: + mtts_flags.append("VT100") + if caps.utf8: + mtts_flags.append("UTF-8") + if caps.colors_256: + mtts_flags.append("256 colors") + if caps.truecolor: + mtts_flags.append("truecolor") + if caps.mouse_tracking: + mtts_flags.append("mouse tracking") + if caps.screen_reader: + mtts_flags.append("screen reader") + if caps.proxy: + mtts_flags.append("proxy") + if caps.mnes: + mtts_flags.append("MNES") + if caps.mslp: + mtts_flags.append("MSLP") + if caps.ssl: + mtts_flags.append("SSL") + + if mtts_flags: + lines.append(f" MTTS: {', '.join(mtts_flags)}") + else: + lines.append(" MTTS: none detected") + + await player.send("\r\n".join(lines) + "\r\n") + + # Register the commands command register( CommandDefinition( @@ -282,6 +345,17 @@ register( ) ) +# Register the client command +register( + CommandDefinition( + "client", + cmd_client, + aliases=[], + mode="*", + help="show client protocol and terminal info", + ) +) + async def cmd_help(player: Player, args: str) -> None: """Show help for a command or skill. @@ -293,7 +367,7 @@ async def cmd_help(player: Player, args: str) -> None: args = args.strip() if not args: await player.send( - "type help for details. see also: commands, skills\r\n" + "type help for details. see also: commands, skills, client\r\n" ) return await _show_command_detail(player, args) diff --git a/tests/test_help.py b/tests/test_help.py new file mode 100644 index 0000000..5ec997c --- /dev/null +++ b/tests/test_help.py @@ -0,0 +1,122 @@ +"""Tests for client command.""" + +from unittest.mock import AsyncMock, MagicMock + +import pytest + +from mudlib.caps import ClientCaps +from mudlib.commands.help import cmd_client +from mudlib.player import Player, players + + +@pytest.fixture(autouse=True) +def clear_state(): + players.clear() + yield + players.clear() + + +@pytest.fixture +def mock_writer(): + writer = MagicMock() + writer.write = MagicMock() + writer.drain = AsyncMock() + writer.send_gmcp = MagicMock() + writer.send_msdp = MagicMock() + writer.local_option = MagicMock() + writer.local_option.enabled = MagicMock(return_value=True) + writer.remote_option = MagicMock() + writer.remote_option.enabled = MagicMock(return_value=True) + writer.get_extra_info = MagicMock( + side_effect=lambda key: { + "TERM": "TINTIN", + "cols": 191, + "rows": 54, + }.get(key) + ) + return writer + + +@pytest.mark.asyncio +async def test_client_shows_protocols(mock_writer): + """Test client command shows GMCP and MSDP status.""" + p = Player(name="Test", writer=mock_writer) + p.caps = ClientCaps(ansi=True, truecolor=True) + await cmd_client(p, "") + + output = mock_writer.write.call_args[0][0] + assert "GMCP: active" in output + assert "MSDP: active" in output + + +@pytest.mark.asyncio +async def test_client_shows_terminal_info(mock_writer): + """Test client command shows terminal type and size.""" + p = Player(name="Test", writer=mock_writer) + p.caps = ClientCaps(ansi=True) + await cmd_client(p, "") + + output = mock_writer.write.call_args[0][0] + assert "type: TINTIN" in output + assert "size: 191x54" in output + + +@pytest.mark.asyncio +async def test_client_shows_color_depth(mock_writer): + """Test client command shows color depth.""" + p = Player(name="Test", writer=mock_writer) + p.caps = ClientCaps(ansi=True, truecolor=True, colors_256=True) + await cmd_client(p, "") + + output = mock_writer.write.call_args[0][0] + assert "colors: truecolor" in output + + +@pytest.mark.asyncio +async def test_client_shows_mtts_flags(mock_writer): + """Test client command shows MTTS capabilities.""" + p = Player(name="Test", writer=mock_writer) + p.caps = ClientCaps(ansi=True, vt100=True, utf8=True, colors_256=True) + await cmd_client(p, "") + + output = mock_writer.write.call_args[0][0] + assert "ANSI" in output + assert "VT100" in output + assert "UTF-8" in output + assert "256 colors" in output + + +@pytest.mark.asyncio +async def test_client_not_negotiated(mock_writer): + """Test client command when protocols not negotiated.""" + mock_writer.local_option.enabled.return_value = False + mock_writer.remote_option.enabled.return_value = False + + p = Player(name="Test", writer=mock_writer) + p.caps = ClientCaps() + await cmd_client(p, "") + + output = mock_writer.write.call_args[0][0] + assert "GMCP: not active" in output + assert "MSDP: not active" in output + + +@pytest.mark.asyncio +async def test_client_no_mtts(): + """Test client command with no MTTS capabilities.""" + writer = MagicMock() + writer.write = MagicMock() + writer.drain = AsyncMock() + writer.local_option = MagicMock() + writer.local_option.enabled = MagicMock(return_value=False) + writer.remote_option = MagicMock() + writer.remote_option.enabled = MagicMock(return_value=False) + writer.get_extra_info = MagicMock(return_value=None) + + p = Player(name="Test", writer=writer) + p.caps = ClientCaps() + await cmd_client(p, "") + + output = writer.write.call_args[0][0] + assert "type: unknown" in output + assert "MTTS: none detected" in output