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:
Jared Miller 2026-02-07 22:18:46 -05:00
parent 269026259c
commit 388e693f8c
Signed by: shmup
GPG key ID: 22B5C6D66A38B06C
3 changed files with 227 additions and 6 deletions

View file

@ -43,12 +43,26 @@ for MUD clients — they prefer MTTS.
meanwhile, in the same negotiation, tintin++ already told us via MTTS: meanwhile, in the same negotiation, tintin++ already told us via MTTS:
ttype3: MTTS 2825 ttype3: MTTS 2825 = 0b101100001001
bit 0 (1) = ANSI color bit 0 (1) = ANSI
bit 3 (8) = UTF-8 bit 1 (2) = VT100
bit 8 (256) = 256 colors bit 2 (4) = UTF-8
bit 9 (512) = OSC color palette bit 3 (8) = 256 COLORS [SET]
bit 11 (2048) = true color 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 two protocols, one answer. MTTS (via TTYPE round 3) is what the MUD ecosystem
uses. RFC 2066 CHARSET is technically correct but practically ignored. uses. RFC 2066 CHARSET is technically correct but practically ignored.

90
src/mudlib/caps.py Normal file
View 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
View 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"