This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
VellumFE is a modern terminal client for GemStone IV (text-based MUD) built in Rust. It supports both TUI (ratatui) and GUI (egui, future) frontends with a shared core architecture.
# Standard build
cargo build
# Release build (optimized with LTO)
cargo build --release
# Check for errors without building
cargo check
# Run with debug logging
RUST_LOG=debug cargo run -- --port 8000
# Run all tests
cargo test
# Run a single test by name
cargo test test_name
# Run tests in a specific module
cargo test parser::
# Run tests with output shown
cargo test -- --nocapture
# Run with single thread (for tests with shared state)
cargo test -- --test-threads=1src/
├── main.rs # CLI entry point (clap)
├── config.rs # Configuration loading (TOML files)
├── parser.rs # Wrayth XML protocol parser (facade; ≤1100 lines, enforced)
├── parser/ # Parser internals: text.rs (entities/tags/streams glue),
│ # handlers.rs (tag handlers), dialogs.rs, links.rs,
│ # builders.rs (in-flight multi-line captures), tests.rs
├── network.rs # TCP/TLS connections (Lich proxy or direct eAccess)
│
├── core/ # Business logic layer (NO frontend imports)
│ ├── app_core/ # Main application state
│ │ ├── state.rs # AppCore - central state manager
│ │ ├── layout.rs # Window layout management
│ │ └── commands.rs # Dot-command processing (.menu, .addwindow, etc.)
│ ├── messages.rs # Message processing pipeline
│ └── input_router.rs # Input routing logic
│
├── data/ # Pure data structures (NO frontend imports)
│ ├── widget.rs # Widget data types (TextSpan, ActiveEffect, etc.)
│ ├── ui_state.rs # UI state (InputMode, PopupMenu, etc.)
│ ├── input.rs # Input event types (KeyCode, KeyEvent, MouseEvent)
│ └── window.rs # Window state
│
└── frontend/
├── mod.rs # Frontend trait definition
├── common/ # Shared frontend types (Color, Rect, TextInput)
└── tui/ # Ratatui terminal UI
├── mod.rs # TuiFrontend struct
├── input.rs # Keyboard/mouse event handling
├── input_handlers.rs # Extracted input handler methods
├── widget_manager.rs # Widget cache synchronization
└── [widgets] # progress_bar.rs, countdown.rs, compass.rs, etc.
- Core layer has NO frontend imports -
core/anddata/modules must not import fromfrontend/(enforced bytests/architecture.rs; input event types live indata/input.rsfor this reason) - Frontend reads from data layer - Frontends render by reading
AppCore.ui_stateandAppCore.game_state - Widget data vs rendering -
data/widget.rsdefines data,frontend/tui/*.rshandles rendering
Network (TCP) → Parser (XML) → Core (AppCore) → Data Layer → Frontend (TUI)
↑
User Input ←────────────────────┘
Widgets are defined in layout.toml and rendered based on type:
| Widget Type | File | Purpose |
|---|---|---|
text |
text_window.rs | Scrollable text (main, thoughts, combat) |
tabbedtext |
tabbed_text_window.rs | Multi-tab text window |
progress |
progress_bar.rs | Health/mana/stamina bars |
countdown |
countdown.rs | RT/CT timers |
compass |
compass.rs | Navigation compass |
hand |
hand.rs | Left/right hand items |
indicator |
indicator.rs | Status indicators (kneeling, hidden) |
dashboard |
dashboard.rs | Character stats grid |
spacer |
spacer.rs | Layout spacing (1x1 minimum) |
hotkeybar |
hotkey_bar.rs | Command buttons with condition-driven styling + countdowns (hotbars.toml, .hotbars editor) |
- Create
frontend/tui/new_widget.rswith render function - Add to
frontend/tui/mod.rsexports - Register in
config.rswidget templates (list_window_templates,get_window_template) - Add to
widget_min_size()inlayout.rsif special constraints needed
Config files stored in ~/.vellum-fe/ (or VELLUM_FE_DIR env var):
config.toml- Main settings (connection, UI options)layout.toml- Window positions and sizeskeybinds.toml- Key bindingscontroller.toml- Gamepad binds, wheels, rumble, tuning (global base + per-character override)hotbars.toml- Hotkey bar definitions (bars of command buttons)highlights.toml- Text highlighting rulescolors.toml- Color palette
Defaults embedded from defaults/ directory via include_dir crate.
| Task | Files |
|---|---|
| Window definitions | config.rs (get_window_template, list_window_templates) |
| Layout positioning | core/app_core/layout.rs |
| Min/max window sizes | widget_min_size() in layout.rs |
| Menu building | frontend/tui/menu_builders.rs, core/app_core/state/menus.rs (build_*_menu) |
| Menu actions | frontend/tui/menu_actions.rs, core/menu_actions.rs |
| Keyboard input | frontend/tui/input.rs, input_handlers.rs |
| Dot-commands | core/app_core/commands.rs |
| Color parsing | frontend/tui/colors.rs (parse_color_to_ratatui) |
The Wrayth XML parser (src/parser.rs + src/parser/) has behavior contracts
pinned by a golden snapshot; know them before touching it:
- Golden snapshot workflow:
tests/parser_characterization.rsruns every fixture through the parser and compares againsttests/data/parser_golden.snap. ANY behavior change must show up as a reviewed diff: regenerate withUPDATE_PARSER_GOLDEN=1 cargo test --test parser_characterization, review, commit the diff with the change. (Window templates have the same workflow:UPDATE_GOLDEN=1 cargo test --test template_characterization.) - Nothing is silently dropped: unknown tag names render as literal text
with a
warn!; known-but-unhandled tags (sortedKNOWN_WIRE_TAGSset inparser/text.rs) swallow with a debug log. New wire tags must be added to the set. - Streams are a stack:
pushStreamnests; a pop restores the enclosing stream viaParsedElement::StreamResume(re-route WITHOUT arrival side effects).<prompt>force-closes any stream still open (warn = eaten popStream upstream) and resets all style/bold/mono state. - Multi-line captures: dialogData/openDialog/component/compDef/
worldEvent/compass may close on a later line (
PairedCaptureinparser/builders.rs); prompt mid-capture discards with a warn; 256KiB cap. prompt/left/right/spell/inv are same-line-only by policy. - Mangled markup: a
<preceded by$is broken server escaping — rendered as literal text, never interpreted. - Entities: 5 named + numeric (
&#nnn;/&#xhh;), decoded exactly once (decode_entities_stableis for double-encoded display titles ONLY). - Facade limit:
parser.rsmust stay ≤1100 lines (architecture test); new code goes in theparser/submodules. - Live instrumentation:
grep -a "\[parser\]" ~/.vellum-fe/vellum-fe.logafter a session shows unknown tags, eaten popStreams, and torn captures.
Direct mode connects to GemStone IV without Lich proxy. Implementation in src/network.rs:
- TLS Handshake:
eaccess.play.net:7910vianative-tls(OS-native stack) with SNI disabled. The server only speaks TLS 1.2 with static-RSA key exchange (AES128-GCM-SHA256), so rustls cannot be used — it refuses to implement non-forward-secret suites. - Challenge-Response: Send "K", receive 32-byte hash key, obfuscate password:
((password[i] - 32) ^ hashkey[i]) + 32 - Session: Login payload
A\t{account}\t{encoded_password}\n
Critical: The send_line function must send message + newline in a single TLS write (not two separate writes). This is essential for the protocol to work.
# Direct connection
vellum-fe --direct --account ACCOUNT --password PASS --game prime --character NAME
# Via Lich proxy
vellum-fe --port 8000 --character NAMETLS uses the OS-native stack via native-tls (SChannel on Windows, Security.framework on macOS) — no OpenSSL, Perl, or vcpkg needed there. Linux statically links a vendored OpenSSL (needs Perl, universal on Linux) so release binaries have no libssl runtime dependency.
- Authentication fails: Check
~/.vellum-fe/vellum-fe.log, delete~/.vellum-fe/simu.pemto re-download cert - Layout issues: Check
widget_min_size()in layout.rs for constraint conflicts - Menu items missing: Check
get_visible_templates_by_category()in config.rs for filtering logic