Contributing¶
We welcome contributions via GitHub pull requests:
Version API¶
This project uses Semantic Versioning for the scripting API, Commands and configuration files. These are expected to be backwards-compatible; when a breaking change is necessary, the major version is incremented.
This project does not follow semantic versioning for Python functions, classes, modules, or their
signatures – any name can change at any time. It is not recommended to import telix for
use in other projects.
Architecture¶
Telix is primarily a TUI interface for MUD scripting over Telnet. It also includes support for BBS, and the WebSocket and SSH protocol. Telix is mainly a TUI and automation/scripting layer above telnetlib3, blessed, wcwidth, textual, asyncssh, websockets.
wcwidth is depended on by each of Telix, telnetlib3, blessed, and rich. wcwidth is used to measure the width of strings containing sequences and complex unicode. blessed is depended on for general terminal support for access to terminal sequence, feature detection, and keyboard handling, and to provide the REPL for MUD connections. telnetlib3 also requires blessed, and, textual is used for all complex TUIs, which depends on its core library rich. For Windows systems, jinxed is used by both Telix and telnetlib3 for msvcrt keyboard routines.
Telix code file overview:
telix/
├── main.py CLI entry (TUI or direct connect)
├── session_context.py Per-connection mutable state
├── client_shell.py Shell callback (drop-in for telnetlib3)
│
├── ws_transport.py WebSocket reader/writer adapters
├── ws_client.py WebSocket connection core (run_ws_client, build_parser)
│
├── ssh_transport.py SSH reader/writer adapters
├── ssh_client.py SSH connection core (run_ssh_client)
│
├── scripts.py Async Python scripting engine
│
├── client_repl.py blessed LineEditor REPL event loop
├── client_repl_render.py Toolbar / status line rendering
├── client_repl_commands.py Command expansion and backtick dispatch
├── client_repl_dialogs.py Interactive dialogs (confirm, input)
├── client_repl_travel.py Room graph navigation
├── repl_theme.py Textual theme to REPL palette resolution
│
├── client_tui.py Re-export hub (backwards compat)
├── client_tui_base.py TUI foundation: sessions, base editors, app
├── client_tui_editors.py Re-export hub (backwards compat)
├── client_tui_app.py Textual app entry point and session launcher
├── client_tui_macros.py Macro editor
├── client_tui_triggers.py Trigger editor
├── client_tui_highlights.py Highlight editor
├── client_tui_bars.py Progress bar editor
├── client_tui_rooms.py Room browser TUI
├── client_tui_captures.py Capture window (Alt+C)
├── client_tui_dialogs.py Confirmation dialogs, walk dialogs, tabbed editor
│
├── trigger.py Pattern-triggered automatic responses
├── macros.py Key-bound macro definitions
├── highlighter.py Regex-based output highlighting + captures
├── rooms.py GMCP Room.Info graph store (SQLite)
├── chat.py GMCP Comm.Channel.Text persistence
├── directory.py Bundled MUD/BBS directory loader
├── progressbars.py Progress bar config loading/saving
├── gmcp_snapshot.py GMCP snapshot persistence
│
├── color_filter.py ANSI/PETSCII/ATASCII color palette translation
├── terminal.py Terminal abstraction (Unix/Win32 dispatch)
├── terminal_unix.py Unix terminal operations (termios, PTY)
├── terminal_win32.py Windows terminal operations (msvcrt)
├── mtts.py MTTS terminal type standard bitvector
├── mslp.py MSLP multiline softlink parser
│
├── paths.py XDG base directory resolution
├── util.py Small internal helpers
└── help/ Markdown help files loaded at runtime
Developing¶
Development requires Python 3.10+. Install in editable mode:
pip install -e .
Any changes made in this project folder are then made available to the Python interpreter as the
telix CLI command and python module regardless of the current working directory.
Running tests¶
pytest is the test runner. Install and run using tox:
pip install --upgrade tox
tox
Run a single test file:
tox -e py314 -- telix/tests/test_chat.py -x -v
Code formatting¶
This project uses ruff for code formatting and linting:
tox -e format
You can also set up a pre-commit hook:
pip install pre-commit
pre-commit install --install-hooks
Run all linters:
tox -e lint
Run individual linters:
tox -e ruff
tox -e ruff_format
tox -e pydocstyle
tox -e codespell
Style¶
Do not use
getattr(obj, "attr", default)as defensive noise when the attribute is always present. If the call site owns the invariant, access it directly asobj.attr.Do not use single-underscore prefixes on names (functions, classes, constants, methods, or attributes). This project has no public Python API – all names are internal. Exceptions:
Unused variables in unpacking (e.g.
for _s, _e, name in spans:)Property backing attributes (e.g.
self._enabledbehind@property enabled)External library private attributes (e.g.
widget._label,parser._actions)
Import style:
import moduleeverywhere, access viamodule.name. Internal imports usefrom . import module. Neverfrom X import Yexceptfrom typing import TYPE_CHECKINGand insideif TYPE_CHECKING:blocks.Omit type annotations rather than use ambiguous types or
# type: ignore. Tests must not use type annotations; tests are excluded from type checking.Do not write Unicode em-dash, arrows, or similar characters in code or documentation.
Use tox to run tests, linters, and formatters.
Max line length: 120 characters.
Sphinx-style reStructuredText docstrings.
TUI and REPL modules should have basic coverage for data-handling and validation logic; only interactive rendering and layout is excluded.
Write tests first when fixing bugs (TDD).
Do not use section dividers or markers in code.
Tests should be self-documenting: no assertion messages, no explanatory comments, no description parameters in parametrized tests. Docstrings should be brief factual statements.
Do not write defensive
try/exceptblocks that swallow errors. Let exceptions propagate unless there is a specific reason to handle them. Never catch broadExceptionorOSErrorjust to log and returnNone. Acceptable uses:except ImportErrorfor optional dependencies, cleanup infinallyblocks, and boundary code that must not crash (e.g. top-level CLI).
Workflow¶
Review whether tests can be simplified: join related tests, use parametrized testing, and reduce line count while keeping the same coverage.
After larger changes, review for unnecessary complexity: reduce duplication, use walrus operators or context managers, and lower McCabe complexity.
Integration boundaries¶
telix.main is the single CLI entry point. It inspects the first
positional argument and routes to one of three paths:
No argument – launches the Textual TUI session manager.
``ws://`` or ``wss://`` URL – parses WS-specific flags via
ws_client.build_parser()and callsws_client.run_ws_client(), which connects viawebsockets.connect()using thegmcp.mudstandards.orgsubprotocol and invokesws_client_shell(reader, writer)withWebSocketReader/WebSocketWriteradapters.Plain host – injects
--shell=telix.client_shell.telix_client_shellintosys.argvand callstelnetlib3.client.run_client(), which parses all remaining CLI arguments and opens the Telnet connection. The shell is a drop-in replacement fortelnetlib3.client_shell.telnet_client_shell.
Before routing, main() checks for --bbs or --mud and removes
the flag from sys.argv. The flag injects preset arguments
(BBS_TELNET_FLAGS / MUD_TELNET_FLAGS) that mirror the TUI session
editor presets. For BBS, the telix shell is not injected (REPL disabled);
for WebSocket connections, --bbs sets no_repl=True.
The TUI launches connection subprocesses via subprocess.Popen. Both
transports use the same python -c "from telix.main import main; main()"
invocation – the URL or host argument in the subprocess command determines
which path main takes.
Every TelnetWriter (or WebSocketWriter) has a .ctx
attribute that defaults to a TelnetSessionContext. Telix’s
SessionContext subclasses TelnetSessionContext, adding
MUD-specific state (rooms, macros, highlights, chat, etc.). The
shell callback creates a SessionContext and assigns it to
writer.ctx.
Telix’s SessionContext also provides captures (a flat
dict[str, int] of captured variables) and capture_log (a
dict[str, list[dict]] of per-channel capture history), populated
by the highlight engine and consumed by the when condition checker
and the Capture Window (Alt+C).
TelnetSessionContext (defined in telnetlib3/session_context.py)
provides the attributes that telnetlib3.client_shell uses:
color_filter– object with.filter(str) -> strraw_mode–None(auto-detect),True, orFalseascii_eol–boolinput_filter–InputFilterorNonetrigger_engine– trigger engine orNonetrigger_wait_fn– async callable orNonetypescript_file– open file handle orNonegmcp_data–dict[str, Any]of raw GMCP package data
GMCP data flow¶
GMCP (Generic MUD Communication Protocol) data arrives as telnet
sub-negotiation and is parsed by telnetlib3 into package/data pairs.
TelnetClient.on_gmcp() stores each package in ctx.gmcp_data
(merging dict updates for the same package key).
Telix overrides the GMCP ext callback in telix_client_shell to
wrap the base on_gmcp with package-specific dispatch to callbacks
on SessionContext:
on_chat_text– called forComm.Channel.Texton_chat_channels– called forComm.Channel.Liston_room_info– called forRoom.Info
These callback attributes are defined on Telix’s SessionContext
and wired up in client_shell.load_configs(). Access them as
regular attributes – do not use getattr().
Room tracking¶
Room state lives in two parallel systems:
In-memory (for REPL commands like randomwalk, autodiscover, and fast-travel):
ctx.room.current,ctx.room.previous,ctx.room.changed, andctx.room.graph(aRoomStorebacked by a SQLite database atctx.room.file).File-based (for TUI subprocesses like the Alt+R room browser):
ctx.room.current_filecontains the current room number as plain text, read byrooms.read_current_room(). The rooms SQLite DB is shared between both systems.
The on_room_info callback bridges these: when a Room.Info
GMCP message arrives, it updates ctx.room.current, calls
room_graph.update_room() to persist the room and its exits to
SQLite, and writes ctx.room.current_file so TUI subprocesses
see the change.
TUI editor subprocesses¶
Pressing editor keys (Alt+H, Alt+M, Alt+A, etc.) launches Textual-based editor screens in a
child subprocess via launch_tui_editor() in
client_repl_dialogs.py. Key constraints:
Never pipe stderr (
stderr=subprocess.PIPE). Textual renders its TUI to stderr. Piping it redirects Textual’s output to a pipe instead of the terminal, freezing the app because stderr is no longer a TTY.Error display. Textual stores unhandled exceptions in
app._exceptionand queues Rich tracebacks inapp._exit_renderables. In non-pilot mode Textual never callsprint_error_renderables()itself, soEditorAppoverrides it to write to stdout (not stderr) after the alt screen exits.run_editor_app()calls it explicitly on non-zero return codes.Blocking fds. The parent’s asyncio event loop sets stdin non-blocking. Since stdin/stdout/stderr share the same PTY file descriptor, the child inherits non-blocking mode.
restore_blocking_fds()must run before Textual starts.In-band resize (DEC mode 2048). The REPL enables DEC private mode 2048 so the terminal sends resize notifications as escape sequences instead of (or in addition to) SIGWINCH. Textual also supports this mode and disables it on
stop_application_mode().restore_after_subprocess()must NOT re-enable mode 2048 immediately – the terminal responds with a resize notification that arrives before the REPL event loop is ready, causing a storm of redundant full-screen repaints. Instead, the module-level flagsubprocess_needs_rearmis set, and the main event loop callsrearm_after_subprocess()after the post-action render is complete. That method flushes stale terminal input (termios.tcflush), records the current terminal size (to suppresson_resize_repaint), and only then re-enables mode 2048.Traceback display.
run_editor_app()wraps the Textualapp.run()call. On crash it writesTERMINAL_CLEANUP(which includes cursor-home and clear-screen) and callsrestore_opost()to re-enable the terminal’sOPOSTflag so\nmaps to\r\n– without this, tracebacks render with staircase output because the terminal is still in raw mode.
REPL output pipeline¶
The REPL reads server data in read_server (client_repl.py)
using await telnet_reader.read(). Incoming text flows through
several stages before reaching the terminal:
Telnet parsing –
telnetlib3strips IAC sequences and decodes bytes to text. IAC-only segments produce no data; the reader stays blocked.Output transform –
transform_output()normalises line endings and applies the color filter.Line hold –
LineHoldBuffer.add(text)splits the text at the last\n. Complete lines go toemit_now; the trailing fragment (e.g. a prompt without\n) is held back.schedule_line_hold_flush()starts a 150 ms debounce timer (LINE_HOLD_TIMEOUT).Prompt signal – If the server sends IAC GA or IAC EOR, the
on_prompt_signalcallback setsprompt_pending = True. The main loop flushes held text immediately when it sees a pending prompt (flush_for_prompt).Highlight engine –
emit_nowlines are run through the highlight engine before display; held-back text flushed by the timer is written raw (no highlights). Rules withcaptured=Trueextract regex groups intoctx.captures(forwhenconditions) and log matched lines toctx.capture_log(for the Capture Window).Screen output – The REPL saves/restores the cursor position via VT100 DECSC (
\x1b7) / DECRC (\x1b8), writes tostdout(anasyncio.StreamWriterconnected to the PTY master FD viaconnect_write_pipe), and re-renders the input line and toolbar after each write.Scroll region –
ScrollRegionconfines server output to the top portion of the terminal using DECSTBM (change_scroll_region). The input line and toolbar sit below the scroll boundary.grow_reserve()expands the reserved area when the GMCP toolbar first appears. It scrolls existing content up by emitting newlines at the scroll-region bottom, then adjusts the saved cursor position by the same amount so that subsequent restore/save pairs stay consistent.
Connection lifecycle¶
The shell callback (client_shell.py) drives the outer
REPL/raw-mode loop:
telix_client_shellis called by telnetlib3 after connection.want_repl()decides the mode (line vs. kludge/raw).repl_event_loopsets up the scroll region, registers IAC callbacks, and startsread_server+read_inputas concurrent tasks viarun_repl_tasks.When the server switches to kludge mode or the connection closes, the REPL returns and the outer loop re-evaluates.
Data arriving before the REPL event loop starts is buffered in
the telnet reader’s internal buffer and consumed by the first
read() call in read_server.