Add MTTS capability parsing module with client color detection
Parses MTTS bitfield values from telnetlib3 ttype3 into a ClientCaps dataclass. Includes color_depth property that returns the best available color mode (truecolor, 256, or 16) based on client capabilities.
This commit is contained in:
parent
269026259c
commit
388e693f8c
3 changed files with 227 additions and 6 deletions
|
|
@ -43,12 +43,26 @@ for MUD clients — they prefer MTTS.
|
|||
|
||||
meanwhile, in the same negotiation, tintin++ already told us via MTTS:
|
||||
|
||||
ttype3: MTTS 2825
|
||||
bit 0 (1) = ANSI color
|
||||
bit 3 (8) = UTF-8
|
||||
bit 8 (256) = 256 colors
|
||||
bit 9 (512) = OSC color palette
|
||||
bit 11 (2048) = true color
|
||||
ttype3: MTTS 2825 = 0b101100001001
|
||||
bit 0 (1) = ANSI
|
||||
bit 1 (2) = VT100
|
||||
bit 2 (4) = UTF-8
|
||||
bit 3 (8) = 256 COLORS [SET]
|
||||
bit 4 (16) = MOUSE TRACKING
|
||||
bit 5 (32) = OSC COLOR PALETTE
|
||||
bit 6 (64) = SCREEN READER
|
||||
bit 7 (128) = PROXY
|
||||
bit 8 (256) = TRUECOLOR [SET]
|
||||
bit 9 (512) = MNES [SET]
|
||||
bit 10 (1024) = MSLP
|
||||
bit 11 (2048) = SSL [SET]
|
||||
|
||||
for MTTS 2825: bits 0, 3, 8, 9, 11 are set
|
||||
|
||||
NOTE: The original version of this doc had incorrect MTTS bit mappings (copied
|
||||
from an unreliable source). This was corrected on 2026-02-07 to match the
|
||||
actual MTTS spec from tintin.mudhalla.net/protocols/mtts/. The wrong values
|
||||
caused a bug in caps.py that misinterpreted client capabilities.
|
||||
|
||||
two protocols, one answer. MTTS (via TTYPE round 3) is what the MUD ecosystem
|
||||
uses. RFC 2066 CHARSET is technically correct but practically ignored.
|
||||
|
|
|
|||
90
src/mudlib/caps.py
Normal file
90
src/mudlib/caps.py
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
"""MTTS (Mud Terminal Type Standard) capability parsing.
|
||||
|
||||
Parses MTTS bitfield values from telnetlib3's ttype3 into a structured dataclass.
|
||||
MTTS is advertised via TTYPE round 3 as "MTTS <number>" where the number is a
|
||||
bitfield of client capabilities.
|
||||
|
||||
See docs/lessons/charset-vs-mtts.txt for protocol details.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Literal
|
||||
|
||||
|
||||
@dataclass
|
||||
class ClientCaps:
|
||||
"""Parsed client capabilities from MTTS bitfield."""
|
||||
|
||||
ansi: bool = False
|
||||
vt100: bool = False
|
||||
utf8: bool = False
|
||||
colors_256: bool = False
|
||||
mouse_tracking: bool = False
|
||||
osc_color_palette: bool = False
|
||||
screen_reader: bool = False
|
||||
proxy: bool = False
|
||||
truecolor: bool = False
|
||||
mnes: bool = False
|
||||
mslp: bool = False
|
||||
ssl: bool = False
|
||||
|
||||
@property
|
||||
def color_depth(self) -> Literal["truecolor", "256", "16"]:
|
||||
"""Return best available color mode: truecolor, 256, or 16."""
|
||||
if self.truecolor:
|
||||
return "truecolor"
|
||||
if self.colors_256:
|
||||
return "256"
|
||||
return "16"
|
||||
|
||||
|
||||
def parse_mtts(ttype3: str | None) -> ClientCaps:
|
||||
"""Parse MTTS capability string into ClientCaps dataclass.
|
||||
|
||||
Args:
|
||||
ttype3: String from writer.get_extra_info("ttype3"), e.g., "MTTS 2825"
|
||||
|
||||
Returns:
|
||||
ClientCaps with parsed capabilities. Returns defaults for
|
||||
None/empty/invalid input.
|
||||
|
||||
Bit values from MTTS spec (tintin.mudhalla.net/protocols/mtts/):
|
||||
Bit 0 (1) = ANSI
|
||||
Bit 1 (2) = VT100
|
||||
Bit 2 (4) = UTF-8
|
||||
Bit 3 (8) = 256 COLORS
|
||||
Bit 4 (16) = MOUSE TRACKING
|
||||
Bit 5 (32) = OSC COLOR PALETTE
|
||||
Bit 6 (64) = SCREEN READER
|
||||
Bit 7 (128) = PROXY
|
||||
Bit 8 (256) = TRUECOLOR
|
||||
Bit 9 (512) = MNES
|
||||
Bit 10 (1024) = MSLP
|
||||
Bit 11 (2048) = SSL
|
||||
"""
|
||||
if not ttype3:
|
||||
return ClientCaps()
|
||||
|
||||
parts = ttype3.split()
|
||||
if len(parts) != 2 or parts[0] != "MTTS":
|
||||
return ClientCaps()
|
||||
|
||||
try:
|
||||
value = int(parts[1])
|
||||
except ValueError:
|
||||
return ClientCaps()
|
||||
|
||||
return ClientCaps(
|
||||
ansi=bool(value & 1),
|
||||
vt100=bool(value & 2),
|
||||
utf8=bool(value & 4),
|
||||
colors_256=bool(value & 8),
|
||||
mouse_tracking=bool(value & 16),
|
||||
osc_color_palette=bool(value & 32),
|
||||
screen_reader=bool(value & 64),
|
||||
proxy=bool(value & 128),
|
||||
truecolor=bool(value & 256),
|
||||
mnes=bool(value & 512),
|
||||
mslp=bool(value & 1024),
|
||||
ssl=bool(value & 2048),
|
||||
)
|
||||
117
tests/test_caps.py
Normal file
117
tests/test_caps.py
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
"""Tests for MTTS capability parsing."""
|
||||
|
||||
from mudlib.caps import parse_mtts
|
||||
|
||||
|
||||
def test_parse_mtts_tintin():
|
||||
"""Parse tintin++ MTTS 2825 correctly.
|
||||
|
||||
2825 in binary = 0b101100001001
|
||||
Bits set: 0, 3, 8, 9, 11
|
||||
= ANSI(1) + 256colors(8) + truecolor(256) + MNES(512) + SSL(2048)
|
||||
"""
|
||||
caps = parse_mtts("MTTS 2825")
|
||||
assert caps.ansi is True
|
||||
assert caps.vt100 is False
|
||||
assert caps.utf8 is False
|
||||
assert caps.colors_256 is True
|
||||
assert caps.mouse_tracking is False
|
||||
assert caps.osc_color_palette is False
|
||||
assert caps.screen_reader is False
|
||||
assert caps.proxy is False
|
||||
assert caps.truecolor is True
|
||||
assert caps.mnes is True
|
||||
assert caps.mslp is False
|
||||
assert caps.ssl is True
|
||||
assert caps.color_depth == "truecolor"
|
||||
|
||||
|
||||
def test_parse_mtts_basic():
|
||||
"""Parse MTTS 137 (ANSI + 256colors + proxy)."""
|
||||
# 137 = 1 (ANSI) + 8 (256colors) + 128 (proxy)
|
||||
caps = parse_mtts("MTTS 137")
|
||||
assert caps.ansi is True
|
||||
assert caps.utf8 is False
|
||||
assert caps.colors_256 is True
|
||||
assert caps.proxy is True
|
||||
assert caps.truecolor is False
|
||||
assert caps.color_depth == "256"
|
||||
|
||||
|
||||
def test_parse_mtts_zero():
|
||||
"""Parse MTTS 0 (nothing supported)."""
|
||||
caps = parse_mtts("MTTS 0")
|
||||
assert caps.ansi is False
|
||||
assert caps.utf8 is False
|
||||
assert caps.colors_256 is False
|
||||
assert caps.truecolor is False
|
||||
assert caps.color_depth == "16"
|
||||
|
||||
|
||||
def test_parse_mtts_256_colors_only():
|
||||
"""Parse MTTS with JUST 256 colors (no truecolor)."""
|
||||
# 9 = 1 (ANSI) + 8 (256colors)
|
||||
caps = parse_mtts("MTTS 9")
|
||||
assert caps.ansi is True
|
||||
assert caps.utf8 is False
|
||||
assert caps.colors_256 is True
|
||||
assert caps.truecolor is False
|
||||
assert caps.color_depth == "256"
|
||||
|
||||
|
||||
def test_parse_mtts_utf8_no_colors():
|
||||
"""Parse MTTS with UTF-8 but no extended colors."""
|
||||
# 5 = 1 (ANSI) + 4 (UTF-8)
|
||||
caps = parse_mtts("MTTS 5")
|
||||
assert caps.ansi is True
|
||||
assert caps.utf8 is True
|
||||
assert caps.colors_256 is False
|
||||
assert caps.truecolor is False
|
||||
assert caps.color_depth == "16"
|
||||
|
||||
|
||||
def test_parse_mtts_empty():
|
||||
"""Handle empty string gracefully."""
|
||||
caps = parse_mtts("")
|
||||
assert caps.ansi is False
|
||||
assert caps.utf8 is False
|
||||
assert caps.color_depth == "16"
|
||||
|
||||
|
||||
def test_parse_mtts_none():
|
||||
"""Handle None gracefully."""
|
||||
caps = parse_mtts(None)
|
||||
assert caps.ansi is False
|
||||
assert caps.utf8 is False
|
||||
assert caps.color_depth == "16"
|
||||
|
||||
|
||||
def test_parse_mtts_non_mtts():
|
||||
"""Handle non-MTTS ttype3 strings (e.g., UNKNOWN-TERMINAL)."""
|
||||
caps = parse_mtts("UNKNOWN-TERMINAL")
|
||||
assert caps.ansi is False
|
||||
assert caps.utf8 is False
|
||||
assert caps.color_depth == "16"
|
||||
|
||||
|
||||
def test_parse_mtts_malformed():
|
||||
"""Handle malformed MTTS strings."""
|
||||
caps = parse_mtts("MTTS")
|
||||
assert caps.ansi is False
|
||||
assert caps.utf8 is False
|
||||
assert caps.color_depth == "16"
|
||||
|
||||
|
||||
def test_color_depth_priority():
|
||||
"""Verify color_depth returns best available mode."""
|
||||
# truecolor wins
|
||||
caps = parse_mtts("MTTS 2825")
|
||||
assert caps.color_depth == "truecolor"
|
||||
|
||||
# 256 wins over 16
|
||||
caps = parse_mtts("MTTS 9")
|
||||
assert caps.color_depth == "256"
|
||||
|
||||
# fallback to 16
|
||||
caps = parse_mtts("MTTS 5") # 1 (ANSI) + 4 (UTF-8)
|
||||
assert caps.color_depth == "16"
|
||||
Loading…
Reference in a new issue