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 serious projects.

Architecture

Telix is primarily a TUI interface for MUDs and BBSs over Telnet, WebSocket and SSH. For MUDs, Telix provides a nice TUI for automations and GMCP data, and “line mode” appropriate for those. For BBSs, it provides color correction, automatic encoding translations, and era-accurate retrocomputing colors and fonts using modern terminal graphics protocols like kitty or sixel.

Telix uses sub-processes to launch sessions, and further, uses subprocesses to launch TUI’s from the MUD/linemode REPL. Inter-process communication is achieved by modification of shared files, like .current-room-<hash> and .fasttravel-<hash>. This is a complication for developer convenience, by launching new sub-processes, the latest code is automatically loaded without requiring a full restart of Telix.

Dependencies

textual, blessed, and wcwidth is used for the User Interface. telnetlib3, asyncssh, websockets for networking.

wcwidth is depended on by each of Telix, telnetlib3, blessed, and rich for string operations related to measuring the width of strings containing sequences and complex unicode like emojis.

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 on windows for keyboard support in its win32 client shell, the same client shell integrated with by Telix.

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. numpy provides vectorized image processing for the sixel and kitty graphics renderers.

numpy and pyte is used for graphics rendering of retrocomputing fonts.

File overview

The following is auto-generated as a convenience, of the first line of python docstring of each file.

__init__.py                    Telix: a TUI telnet and MUD client.
chat.py                        Chat message persistence for GMCP ``Comm.Channel.Text``.
client_repl.py                 REPL and TUI components for linemode telnet client sessions.
client_repl_color.py           HSV/RGB colour math and vital-bar flash animation helpers.
client_repl_commands.py        Command expansion, queuing, chained command sending, and macro execution.
client_repl_dialogs.py         TUI thread-based management: confirmation dialogs, help screen, editor launchers.
client_repl_render.py          Vital-bar rendering, toolbar layout, and display helpers.
client_repl_sextant.py         Sextant block character table and password scrambling.
client_repl_travel.py          Movement and pathfinding: travel, autodiscover, randomwalk.
client_shell.py                Telix client shell -- wraps telnetlib3's terminal handling with REPL support.
client_tui.py                  Textual TUI session manager for telix -- re-export hub.
client_tui_app.py              Main Textual application and entry point for the telix TUI.
client_tui_bars.py             Progress bar and theme editor panes and screens for the telix TUI.
client_tui_base.py             Foundation layer for the Textual TUI editor infrastructure.
client_tui_captures.py         Highlight captures and chat viewer screens for the telix TUI.
client_tui_dialogs.py          Confirmation dialogs, walk dialogs, and the tabbed editor screen.
client_tui_editors.py          Standalone entry points for the Textual TUI editor panes.
client_tui_highlights.py       Highlight editor pane and screen for the telix TUI.
client_tui_macros.py           Macro editor pane and screen for the telix TUI.
client_tui_rooms.py            Room browser, picker, and graph editor screens for the telix TUI.
client_tui_session_manager.py  Session management layer for the Textual TUI.
client_tui_triggers.py         Trigger editor pane and screen for the telix TUI.
color_filter.py                ANSI color palette translation for MUD/BBS client output.
directory.py                   Load the bundled MUD/BBS directory and convert to session configs.
fonts/font_registry.py         Bitmap font registry -- auto-generated by tools/build_fonts.py.
gmcp_snapshot.py               Rolling GMCP data snapshot persistence.
graphics_renderer.py           Sixel and Kitty graphics protocol encoders.
graphics_writer.py             Graphics writer: pyte virtual terminal rendered via sixel/kitty graphics.
highlighter.py                 Output text highlighting engine for MUD client sessions.
macros.py                      Macro key binding support for the REPL client.
main.py                        Entry point for the telix CLI.
mslp.py                        MSLP (Mud Server Link Protocol) keyboard navigation.
mtts.py                        MTTS and MNES protocol support.
paths.py                       Consolidated XDG Base Directory paths for telix.
progressbars.py                Progress bar configuration model for the GMCP vitals toolbar.
raw_client.py                  Raw TCP client for telix.
raw_transport.py               Raw TCP reader/writer adapters for telix sessions.
repl_theme.py                  Resolve the user's Textual theme into concrete hex colors for the blessed REPL.
rooms.py                       Room graph tracking, BFS pathfinding, and SQLite persistence for GMCP Room.Info data.
scripts.py                     Async Python scripting engine for telix.
session_context.py             Per-connection session state for MUD client sessions.
ssh_client.py                  SSH client for telix.
ssh_transport.py               SSH reader/writer adapters for telix sessions.
telix_config.py                Telix-specific CLI configuration threaded through the call chain to client shells.
terminal.py                    Platform dispatcher for terminal operations.
terminal_unix.py               Unix-specific terminal operations for the telix REPL.
terminal_win32.py              Windows terminal operations for the telix REPL (stubs).
trigger.py                     Server output pattern matching and automatic reply engine.
util.py                        Small shared utility functions.
ws_client.py                   WebSocket client for telix.
ws_transport.py                WebSocket reader/writer adapters for MUD client sessions.

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 as obj.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._enabled behind @property enabled)

    • External library private attributes (e.g. widget._label, parser._actions)

  • Import style: import module everywhere, access via module.name. Internal imports use from . import module. Never from X import Y except from typing import TYPE_CHECKING and inside if 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/except blocks that swallow errors. Let exceptions propagate unless there is a specific reason to handle them. Never catch broad Exception or OSError just to log and return None. Acceptable uses: except ImportError for optional dependencies, cleanup in finally blocks, 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 calls ws_client.run_ws_client(), which connects via websockets.connect() using the gmcp.mudstandards.org subprotocol and invokes ws_client_shell(reader, writer) with WebSocketReader/WebSocketWriter adapters.

  • Plain host – injects --shell=telix.client_shell.telix_client_shell into sys.argv and calls telnetlib3.client.run_client(), which parses all remaining CLI arguments and opens the Telnet connection. The shell is a drop-in replacement for telnetlib3.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) -> str

  • raw_modeNone (auto-detect), True, or False

  • ascii_eolbool

  • input_filterInputFilter or None

  • trigger_engine – trigger engine or None

  • trigger_wait_fn – async callable or None

  • typescript_file – open file handle or None

  • gmcp_datadict[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 for Comm.Channel.Text

  • on_chat_channels – called for Comm.Channel.List

  • on_room_info – called for Room.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:

  1. In-memory (for REPL commands like randomwalk, autodiscover, and fast-travel): ctx.room.current, ctx.room.previous, ctx.room.changed, and ctx.room.graph (a RoomStore backed by a SQLite database at ctx.room.file).

  2. File-based (for TUI subprocesses like the Alt+R room browser): ctx.room.current_file contains the current room number as plain text, read by rooms.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._exception and queues Rich tracebacks in app._exit_renderables. In non-pilot mode Textual never calls print_error_renderables() itself, so EditorApp overrides 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 flag subprocess_needs_rearm is set, and the main event loop calls rearm_after_subprocess() after the post-action render is complete. That method flushes stale terminal input (termios.tcflush), records the current terminal size (to suppress on_resize_repaint), and only then re-enables mode 2048.

  • Traceback display. run_editor_app() wraps the Textual app.run() call. On crash it writes TERMINAL_CLEANUP (which includes cursor-home and clear-screen) and calls restore_opost() to re-enable the terminal’s OPOST flag so \n maps 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:

  1. Telnet parsingtelnetlib3 strips IAC sequences and decodes bytes to text. IAC-only segments produce no data; the reader stays blocked.

  2. Output transformtransform_output() normalises line endings and applies the color filter.

  3. Line holdLineHoldBuffer.add(text) splits the text at the last \n. Complete lines go to emit_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).

  4. Prompt signal – If the server sends IAC GA or IAC EOR, the on_prompt_signal callback sets prompt_pending = True. The main loop flushes held text immediately when it sees a pending prompt (flush_for_prompt).

  5. Highlight engineemit_now lines are run through the highlight engine before display; held-back text flushed by the timer is written raw (no highlights). Rules with captured=True extract regex groups into ctx.captures (for when conditions) and log matched lines to ctx.capture_log (for the Capture Window).

  6. Screen output – The REPL saves/restores the cursor position via VT100 DECSC (\x1b7) / DECRC (\x1b8), writes to stdout (an asyncio.StreamWriter connected to the PTY master FD via connect_write_pipe), and re-renders the input line and toolbar after each write.

  7. Scroll regionScrollRegion confines 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:

  1. telix_client_shell is called by telnetlib3 after connection.

  2. want_repl() decides the mode (line vs. kludge/raw).

  3. repl_event_loop sets up the scroll region, registers IAC callbacks, and starts read_server + read_input as concurrent tasks via run_repl_tasks.

  4. 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.

Graphics rendering pipeline

When --graphics-font auto is active and the terminal supports kitty or sixel, the raw-mode output path uses GraphicsWriter (graphics_writer.py) instead of ColorFilteredWriter.

GraphicsWriter decodes server output, feeds it through a pyte virtual terminal, and renders the result as a pixel image encoded in the terminal’s native graphics protocol (kitty or sixel). This can be thought of as “tmux for retrocomputing”.

Input stage

  1. Raw bytes from the server arrive via write(data: bytes), called synchronously by telnetlib3’s _raw_event_loop.

  2. Pre-processing: Form Feed bytes (0x0C) are replaced with clear-screen sequences if the ff-clears-screen option is on. A cursor-home sequence is injected before clear-screen if clear-homes-cursor is on (CTerm compatibility).

  3. Decoding: bytes are re-encoded per the active font’s wire encoding (e.g. cp437, iso-8859-1) to map server bytes to font glyph indices.

  4. Font switching: SyncTERM CSI Ps1 ; Ps2 SP D sequences are intercepted.

    The font and wire encoding switch to the requested font ID. CSI sequences with intermediate bytes that pyte cannot handle are stripped, like DCS terminal queries (XTGETTCAP).

  5. Color filter: ANSI SGR color codes are translated through the configured hardware palette (VGA, xterm, etc.) into 24-bit RGB values using color_filter.py. This ensures the colors displayed match the era-accurate palette the BBS artist intended, regardless of the terminal’s color palette, which is very often customized as something else.

Virtual terminal

  1. pyte: the cleaned text is fed to an in-memory pyte.Screen (80x25 by default, or forced size via --graphics-columns / --graphics-rows). The screen maintains per-cell character and color attributes with full DEC VT100/VT220 emulation.

    BBSScreen (a pyte.Screen subclass) applies two BBS compatibility adjustments: DECAWM (auto-wrap) is permanently disabled to match raw BBS connections sends CR+LF as line endings. With DECAWM on, pyte injects extra line breaks causing doubled spacing. ED 2 (Erase in Display) also homes the cursor, matching SyncTERM/CTerm behavior of BBS software.

Rendering

The rendering pipeline contains many performance enhancements to improve latency and reduce CPU usage. It is also required to create a “software cursor” when using the graphics pipeline, and, special attention to preference of integer (“non-blurry”) scaling.

  1. Diff: only cells in pyte’s screen.dirty set are re-rendered. A full redraw is forced when the font changes, the terminal resizes, or _needs_full_redraw is set.

  2. Glyph cache: each font’s bitmap data is pre-rasterized into a (nglyphs, height, width) numpy boolean array. The cache is built once per font switch.

  3. Pixel buffer: for each changed cell, the character’s glyph index is resolved via _char_to_code() (mapping from the font’s encoding back to a byte index). The glyph bitmap is stamped into a (rows * fh, columns * fw, 3) numpy float32 array using the cell’s foreground and background RGB colors. Colors are resolved through pyte_color_to_rgb() which handles named colors, xterm-256, and #RRGGBB values.

  4. Cursor – the block cursor is drawn by inverting (1.0 - color) the pixel region at the screen cursor position. The cursor shape (block, underline, I-beam) is parsed from DECSCUSR sequences stripped from the text stream. Blink timing is managed by a repeating 500ms timer; the cursor is visible during the first half of each cycle and hidden during the second half.

  5. Scaling – if --graphics-columns / --graphics-rows specify a cell pixel size larger than the font’s native dimensions, the pixel buffer is integer-scaled via np.repeat.

Protocol encoding

Kitty graphics is preferred when a terminal supports both Kitty and Sixel. Although C bindings to “libsixel” could significantly reduce CPU usage, a numpy-based solution is used for sixel to maximize compatibility and compilation troubles. kitty graphics has less overhead because the encoding is very straight-forward.

  1. Kitty – the pixel array is encoded as an APC sequence (ESC _ G). RGBA data is deflate-compressed and base64-encoded in 4096-byte chunks. The frame specifies a=T,f=32,s=<w>,v=<h> (RGBA transmission, 32-bit format). The entire frame is wrapped in DEC 2026 synchronized output brackets (ESC [?2026h / ESC [?2026l) for tear-free display.

  2. Sixel – the pixel array is quantized per-row into color registers and transmitted as a DCS sequence (ESC P q). Each sixel band encodes a row of pixels; repeated pixels use run-length encoding. DEC 2026 sync brackets are NOT used with sixel because they produce blank output in foot and xterm. Instead, each frame is preceded by a clear-screen sequence (ESC [H ESC [2J).

Output bridge

  1. Sync-to-asyncwrite() runs in telnetlib3’s synchronous _raw_event_loop. Rendering is dispatched to the asyncio event loop via loop.create_task(self._render_frame()). _render_frame() calls _do_render() (builds the pixel buffer and encodes the protocol sequence) then await self.inner.drain() to flush output atomically.

  2. Rate limiting – renders are capped at ~30 fps (MIN_RENDER_INTERVAL = 0.033). Multiple write() calls within the interval coalesce dirty cells into a single frame. If the _rendering guard is already set, subsequent writes skip scheduling and let the inflight render complete.

Encoding-only path

When graphics font is not active but a color palette is configured, raw-mode output uses ColorFilteredWriter – a simpler writer that applies the color filter and encoding translation directly to server bytes without the pyte virtual terminal or graphics rendering stages.