Two-Face
A modern, high-performance multi-frontend client for GemStone IV
What is Two-Face?
Two-Face is a feature-rich terminal client designed specifically for GemStone IV, the legendary text-based MMORPG by Simutronics. Built from the ground up in Rust, Two-Face delivers:
- 60+ FPS rendering with sub-millisecond event processing
- Fully customizable layouts with pixel-perfect window positioning
- Rich theming support with 24-bit true color
- Multiple connection modes (Lich proxy or direct eAccess authentication)
- Modern TUI built on ratatui with planned GUI support via egui
┌─────────────────────────────────────────────────────────────────────────┐
│ Two-Face │
│ │
│ ┌─────────────────────────┐ ┌────────────────────────────────────┐ │
│ │ Main Window │ │ Room Description │ │
│ │ │ │ [Obvious exits: north, east, out] │ │
│ │ A goblin attacks! │ └────────────────────────────────────┘ │
│ │ > attack goblin │ ┌──────────┐ ┌──────────┐ │
│ │ You swing at a goblin! │ │ ◄ N ► │ │ HP: 100% │ │
│ │ │ │ S │ │ MP: 87% │ │
│ └─────────────────────────┘ └──────────┘ └──────────┘ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ > │ │
│ └─────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
Philosophy
Two-Face is built on these core principles:
1. Performance First
Every design decision prioritizes smooth, responsive gameplay. Generation-based change detection, lazy text wrapping, and efficient memory management ensure Two-Face never gets in your way.
2. Customization Without Limits
Your client should look and behave exactly how you want. Every window can be positioned, sized, styled, and configured independently. Create layouts for hunting, merchanting, roleplaying, or anything else.
3. Modern Architecture
A clean separation between Core (game logic), Data (state), and Frontend (rendering) allows for multiple frontends (TUI today, GUI tomorrow) while keeping the codebase maintainable.
4. Developer Friendly
Written in idiomatic Rust with comprehensive documentation. Want to add a new widget type? Extend the parser? Create a custom browser? The architecture supports it.
Feature Highlights
Layouts
Design your perfect interface with TOML-based layout files. Position windows by row/column or pixel coordinates. Nest windows, create tabs, configure borders and colors per-window.
Highlights
Apply colors and styles to game text with regex patterns. Highlight creature names, player speech, spell effects, or anything else. Fast literal matching via Aho-Corasick for high-frequency patterns.
Keybinds
Bind any key combination to game commands, client actions, or macros. Full modifier support (Ctrl, Alt, Shift). Action-based system supports scrolling, navigation, text editing, and custom commands.
Themes
Complete color control with presets, palettes, and per-widget overrides. Ship with dark and light themes, or create your own.
Sound
Audio alerts for game events with configurable triggers. Text-to-speech support for accessibility.
Connection Modes
Connect via Lich for scripting integration, or directly authenticate with eAccess for standalone operation.
Quick Start
# Via Lich (default)
two-face --port 8000
# Direct connection
two-face --direct --account YOUR_ACCOUNT --character CharName
See Installation for detailed setup instructions.
Documentation Structure
This documentation is organized for multiple audiences:
| Section | Audience | Purpose |
|---|---|---|
| Getting Started | New users | Installation, first launch, quick tour |
| Configuration | All users | Config file reference |
| Widgets | All users | Widget types and properties |
| Customization | Power users | Layouts, themes, highlights |
| Tutorials | All users | Step-by-step guides |
| Cookbook | Power users | Quick recipes for specific tasks |
| Architecture | Developers | System design and internals |
| Development | Contributors | Building, testing, contributing |
| Reference | All users | Complete reference tables |
Getting Help
- Issues: GitHub Issues
- Discussions: GitHub Discussions
- In-Game: Find us on the amunet channel
License
Two-Face is open source software. See the repository for license details.
Ready to dive in? Start with Installation.
Getting Started
Welcome to Two-Face! This section will guide you from installation to your first game session.
Overview
Getting Two-Face running involves three steps:
- Installation - Download or build Two-Face for your platform
- First Launch - Connect to GemStone IV
- Quick Tour - Learn the essential controls
Prerequisites
Before you begin, ensure you have:
- A GemStone IV Account: Create one at play.net
- A Modern Terminal: Windows Terminal, iTerm2, or any terminal with 256+ color support
- Optional: Lich: For scripting support, install Lich
Connection Modes
Two-Face offers two ways to connect:
Lich Proxy Mode (Recommended for Script Users)
# Start Lich with your character, then:
two-face --port 8000
- Full Lich script compatibility
- Scripting and automation support
- Most users choose this option
Direct Mode (Standalone)
two-face --direct --account YOUR_ACCOUNT --character CharName --game prime
- No Lich dependency
- Authenticates directly with eAccess
- Ideal for lightweight setups
What’s Next?
| Page | Description |
|---|---|
| Installation | Platform-specific setup instructions |
| First Launch | Connecting and initial configuration |
| Quick Tour | Essential keyboard shortcuts and navigation |
| Upgrading | Updating to new versions |
Ready to install? Continue to Installation.
Installation
This guide covers installing Two-Face on all supported platforms.
Quick Install
Pre-built Binaries
Download the latest release for your platform from GitHub Releases:
| Platform | Download |
|---|---|
| Windows (x64) | two-face-windows-x64.zip |
| Linux (x64) | two-face-linux-x64.tar.gz |
| macOS (Intel) | two-face-macos-x64.tar.gz |
| macOS (Apple Silicon) | two-face-macos-arm64.tar.gz |
Extract and Run
Windows:
# Extract to a folder
Expand-Archive two-face-windows-x64.zip -DestinationPath C:\two-face
# Run
C:\two-face\two-face.exe
Linux/macOS:
# Extract
tar xzf two-face-linux-x64.tar.gz
# Make executable
chmod +x two-face
# Run
./two-face
Building from Source
For the latest features or unsupported platforms, build from source.
Prerequisites
-
Rust Toolchain (1.70+)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -
Platform-Specific Dependencies
Windows:
- Visual Studio Build Tools
- vcpkg with OpenSSL:
git clone https://github.com/microsoft/vcpkg.git C:\vcpkg cd C:\vcpkg .\bootstrap-vcpkg.bat .\vcpkg install openssl:x64-windows $env:VCPKG_ROOT = "C:\vcpkg"
Linux (Debian/Ubuntu):
sudo apt install build-essential pkg-config libssl-dev libasound2-devLinux (Fedora/RHEL):
sudo dnf install gcc pkg-config openssl-devel alsa-lib-develmacOS:
xcode-select --install # OpenSSL usually provided by system
Build Steps
# Clone repository
git clone https://github.com/nisugi/two-face.git
cd two-face
# Build release version
cargo build --release
# Binary is at target/release/two-face
Build Options
# Build without sound support (smaller binary)
cargo build --release --no-default-features
# Build with all features
cargo build --release --features sound
Data Directory
Two-Face stores configuration in a data directory:
| Platform | Default Location |
|---|---|
| Windows | %USERPROFILE%\.two-face\ |
| Linux | ~/.two-face/ |
| macOS | ~/.two-face/ |
Structure
~/.two-face/
├── config.toml # Main configuration
├── layout.toml # Window layout
├── keybinds.toml # Key bindings
├── highlights.toml # Text highlighting
├── colors.toml # Color theme
├── simu.pem # eAccess certificate (auto-downloaded)
├── two-face.log # Debug log
└── profiles/ # Per-character profiles
└── CharName/
├── layout.toml
└── highlights.toml
Custom Data Directory
Override the default location:
# Via command line
two-face --data-dir /path/to/custom/dir
# Via environment variable
export TWO_FACE_DIR=/path/to/custom/dir
two-face
Verifying Installation
-
Check version:
two-face --version -
View help:
two-face --help -
Test terminal colors: Launch Two-Face and check if colors display correctly. If colors look wrong:
# Set truecolor support export COLORTERM=truecolor two-face
Troubleshooting Installation
Windows: “vcruntime140.dll not found”
Install the Visual C++ Redistributable.
Windows: OpenSSL errors
Ensure VCPKG_ROOT is set and OpenSSL is installed via vcpkg.
Linux: “libasound.so not found”
Install ALSA library:
# Debian/Ubuntu
sudo apt install libasound2
# Fedora
sudo dnf install alsa-lib
macOS: “cannot be opened because the developer cannot be verified”
Right-click the binary, select “Open”, then click “Open” in the dialog.
All Platforms: Colors not working
- Use a modern terminal (Windows Terminal, iTerm2, etc.)
- Set
COLORTERM=truecolorenvironment variable - Ensure your terminal supports 24-bit color
Next Steps
With Two-Face installed, continue to First Launch to connect to the game.
See Also
- Building from Source - Detailed build guide for developers
- Configuration Reference - Config file documentation
- Troubleshooting - Common issues and solutions
First Launch
This guide walks you through connecting to GemStone IV for the first time.
Choose Your Connection Mode
Two-Face supports two connection methods:
| Mode | Best For | Requirements |
|---|---|---|
| Lich Proxy | Script users, most players | Lich running |
| Direct | Standalone use, lightweight | Account credentials |
Option A: Lich Proxy Mode
Step 1: Start Lich
Launch Lich with your character. If you’re new to Lich, see the Lich documentation.
# Example Lich launch (varies by setup)
ruby lich.rb --login CharacterName
Lich will start listening on a port (default: 8000).
Step 2: Launch Two-Face
# Connect to Lich on default port
two-face
# Or specify a different port
two-face --port 8001
Step 3: Verify Connection
You should see:
- Two-Face window appears
- Game output starts flowing
- Prompt (“>”) is visible
If the connection fails:
- Ensure Lich is running and logged in
- Check the port number matches
- See Connection Troubleshooting
Option B: Direct Mode
Direct mode connects without Lich, authenticating directly with Simutronics.
Step 1: Launch with Credentials
# Minimal command (will prompt for password)
two-face --direct --account YOUR_ACCOUNT --character CharName
# Specify game world
two-face --direct --account YOUR_ACCOUNT --character CharName --game prime
Game Worlds
| World | Flag | Description |
|---|---|---|
| Prime | --game prime | Main GemStone IV server |
| Platinum | --game platinum | Premium subscription server |
| Shattered | --game shattered | Test/development server |
Step 2: Enter Password
When prompted, enter your account password. The password is not echoed for security.
Password for account YOUR_ACCOUNT: ********
Step 3: Verify Connection
Two-Face will:
- Authenticate with eAccess servers
- Download/verify the server certificate (first time only)
- Connect to the game server
- Display game output
Initial Configuration
On first launch, Two-Face creates default configuration files:
~/.two-face/
├── config.toml # Created with defaults
├── layout.toml # Default window layout
├── keybinds.toml # Default keybindings
├── highlights.toml # Default highlights
└── colors.toml # Default color theme
Character Profiles
To use per-character settings:
two-face --character CharName
This loads settings from ~/.two-face/profiles/CharName/ if they exist, falling back to defaults.
Essential First Steps
Once connected, try these:
1. Test Commands
Type a game command and press Enter:
look
2. Test Scrolling
Use Page Up/Down or scroll wheel to navigate history.
3. Open the Menu
Press Ctrl+M to open the main menu. Navigate with arrow keys.
4. Check Keybinds
Press Ctrl+? to view current keybindings.
First Launch Checklist
- Connected successfully
- Game output appears
- Commands work
- Colors display correctly
- Menu opens with Ctrl+M
Common First-Launch Issues
“Connection refused”
- Lich mode: Ensure Lich is running and logged in
- Direct mode: Check internet connection
No output appears
- Check that you’re connected to the right port
- Ensure Lich/game is actually sending data
Colors look wrong
- Set
COLORTERM=truecolorin your terminal - Use Windows Terminal (not CMD) on Windows
Password not accepted (Direct mode)
- Verify account name and password
- Try logging in via the official client first
- Delete
~/.two-face/simu.pemand retry
Next Steps
Now that you’re connected, take a Quick Tour to learn the essential controls.
See Also
- Quick Tour - Essential keyboard shortcuts
- Configuration - Customize your setup
- Connection Troubleshooting - Detailed connection help
Quick Tour
A 5-minute introduction to Two-Face’s essential controls and features.
The Interface
┌─────────────────────────────────────────────────────────────────────────┐
│ Two-Face v0.1.0 [?] │
├─────────────────────────────────────────────────────────────────────────┤
│ ┌─ Main ───────────────────────────┐ ┌─ Room ─────────────────────────┐ │
│ │ │ │ Town Square Central │ │
│ │ A goblin attacks you! │ │ This is the heart of the town. │ │
│ │ You swing your sword at goblin! │ │ Obvious paths: n, e, s, w, out │ │
│ │ You hit for 45 damage! │ │ │ │
│ │ The goblin falls dead! │ └────────────────────────────────┘ │
│ │ │ ┌─ Vitals ───┐ ┌─ Compass ──────┐ │
│ │ > │ │ HP ████████│ │ [N] │ │
│ │ │ │ MP ██████░░│ │ [W] + [E] │ │
│ │ │ │ ST ████████│ │ [S] │ │
│ └──────────────────────────────────┘ └────────────┘ └────────────────┘ │
│ ┌─ Input ──────────────────────────────────────────────────────────────┐│
│ │ > attack goblin ││
│ └──────────────────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────────────┘
Essential Keybinds
Navigation
| Key | Action |
|---|---|
Page Up / Page Down | Scroll main window |
Home / End | Jump to top/bottom |
Scroll Wheel | Scroll focused window |
Ctrl+Tab | Cycle through windows |
Tab | Focus next tabbed window |
Input
| Key | Action |
|---|---|
Enter | Send command |
Up / Down | Command history |
Ctrl+C | Copy selection |
Ctrl+V | Paste |
Escape | Clear input / Cancel |
Menus & Popups
| Key | Action |
|---|---|
Ctrl+M | Open main menu |
Ctrl+H | Open highlight browser |
Ctrl+K | Open keybind browser |
Ctrl+E | Open window editor |
Ctrl+? | Show keybind help |
Escape | Close popup |
Client Control
| Key | Action |
|---|---|
Ctrl+Q | Quit Two-Face |
F5 | Reload configuration |
Ctrl+L | Clear main window |
Try It Now
1. Send a Command
Type look and press Enter. The room description appears in the main window.
2. Scroll History
Press Page Up to scroll back through game output. Press End to return to the bottom.
3. Use Command History
Press Up Arrow to recall your previous command. Press Down Arrow to go forward.
4. Open the Menu
Press Ctrl+M. Use arrow keys to navigate, Enter to select, Escape to close.
5. Focus a Window
Click on a window or use Ctrl+Tab to cycle focus. The focused window has a highlighted border.
Window Types
Two-Face displays several types of windows:
Text Windows
Scrollable text output (main game feed, thoughts, speech).
Controls:
- Scroll with Page Up/Down or mouse wheel
- Select text by clicking and dragging
- Copy selection with Ctrl+C
Progress Bars
Visual health, mana, stamina, etc.
Display:
HP ████████████████ 100%
MP ██████████░░░░░░ 67%
Compass
Shows available exits with directional indicators.
Hands
Displays items in your left/right hands and prepared spell.
Indicators
Status icons for conditions (stunned, hidden, etc.).
The Main Menu
Press Ctrl+M to access:
┌─ Main Menu ────────────────────────┐
│ ► Window Editor │
│ Highlight Browser │
│ Keybind Browser │
│ Color Browser │
│ ───────────────────────── │
│ Reload Configuration │
│ ───────────────────────── │
│ Quit │
└────────────────────────────────────┘
| Option | Description |
|---|---|
| Window Editor | Modify window positions, sizes, styles |
| Highlight Browser | View and edit text highlighting rules |
| Keybind Browser | View and edit keybindings |
| Color Browser | View and edit color settings |
| Reload Configuration | Apply changes from config files |
| Quit | Exit Two-Face |
Quick Customization
Change a Keybind
- Press
Ctrl+Kto open the keybind browser - Navigate to the keybind you want to change
- Press Enter to edit
- Press the new key combination
- Press Escape to close
Edit Highlights
- Press
Ctrl+Hto open the highlight browser - Find an existing highlight or add a new one
- Edit the pattern, colors, or conditions
- Changes take effect immediately
Adjust a Window
- Press
Ctrl+Eto open the window editor - Select a window from the list
- Modify position, size, border, colors
- Press Escape to close (changes auto-save)
Keyboard Reference Card
Print this for quick reference:
┌─────────────────────────────────────────────────────────────┐
│ TWO-FACE QUICK REFERENCE │
├─────────────────────────────────────────────────────────────┤
│ NAVIGATION │ MENUS │ INPUT │
│ PgUp/PgDn Scroll │ Ctrl+M Main Menu │ Enter Send │
│ Home/End Top/Bot │ Ctrl+H Highlights │ Up/Down History │
│ Ctrl+Tab Windows │ Ctrl+K Keybinds │ Ctrl+C Copy │
│ Tab Tabs │ Ctrl+E Editor │ Ctrl+V Paste │
│ │ Escape Close │ Escape Clear │
├─────────────────────┴────────────────────┴───────────────────┤
│ F5 = Reload Config Ctrl+L = Clear Ctrl+Q = Quit │
└─────────────────────────────────────────────────────────────┘
Next Steps
Now that you know the basics:
- Customize Your Layout - Make it yours
- Set Up Highlights - Color your world
- Configure Keybinds - Optimize your workflow
- Explore Widgets - Learn all widget types
See Also
- Keybind Actions Reference - Complete keybind list
- Configuration Guide - Config file details
- Tutorials - Step-by-step guides
Upgrading
How to update Two-Face to newer versions while preserving your configuration.
Before Upgrading
1. Backup Your Configuration
Your configuration files are precious. Back them up:
# Linux/macOS
cp -r ~/.two-face ~/.two-face.backup
# Windows (PowerShell)
Copy-Item -Recurse $env:USERPROFILE\.two-face $env:USERPROFILE\.two-face.backup
2. Check the Changelog
Review Version History for:
- Breaking changes
- New features
- Deprecated settings
- Migration requirements
Upgrade Methods
Pre-built Binaries
- Download the new version from GitHub Releases
- Replace the old binary with the new one
- Launch Two-Face
Your configuration files in ~/.two-face/ are preserved automatically.
Building from Source
cd two-face
# Pull latest changes
git pull origin master
# Rebuild
cargo build --release
# New binary is at target/release/two-face
Configuration Compatibility
Automatic Migration
Two-Face attempts to handle configuration changes automatically:
- Missing keys use default values
- Deprecated keys are ignored with warnings
- New features are disabled until configured
Manual Migration
For breaking changes, you may need to update config files manually. Check the version-specific notes below.
Version-Specific Upgrade Notes
v0.1.x → v0.2.x
No breaking changes expected during alpha.
Future Versions
Check Version History for detailed migration guides when released.
Troubleshooting Upgrades
Config file errors after upgrade
- Check the error message for the problematic key
- Compare your config with the default files
- Update or remove the problematic setting
Reset to defaults
If needed, reset configuration:
# Backup first!
cp -r ~/.two-face ~/.two-face.backup
# Remove old config (keeps profiles)
rm ~/.two-face/*.toml
# Launch Two-Face to regenerate defaults
two-face
Partial reset
Reset specific files:
# Reset just keybinds
rm ~/.two-face/keybinds.toml
two-face # Regenerates default keybinds
Downgrading
To revert to an older version:
- Download the older release
- Replace the binary
- Restore your backed-up configuration if needed
Note: Config files from newer versions may not work with older versions. Use your backup.
See Also
- Version History - Changelog and migration guides
- Default Files - Default configuration reference
- Troubleshooting - General troubleshooting
Configuration
Two-Face uses TOML files for all configuration. This section provides complete documentation for each configuration file.
Configuration Files
Two-Face loads configuration from these files in your data directory (~/.two-face/):
| File | Purpose | Hot Reload |
|---|---|---|
| config.toml | Main settings, connection, behavior | Yes |
| layout.toml | Window positions, sizes, arrangement | Yes |
| keybinds.toml | Keyboard shortcuts and actions | Yes |
| highlights.toml | Text highlighting patterns | Yes |
| colors.toml | Color theme, presets, palettes | Yes |
Hot Reload: Press F5 to reload all configuration without restarting.
File Locations
Default Location
| Platform | Path |
|---|---|
| Windows | %USERPROFILE%\.two-face\ |
| Linux | ~/.two-face/ |
| macOS | ~/.two-face/ |
Custom Location
Override with --data-dir or TWO_FACE_DIR:
# Command line
two-face --data-dir /custom/path
# Environment variable
export TWO_FACE_DIR=/custom/path
Character Profiles
Per-character configuration lives in ~/.two-face/profiles/<CharName>/:
~/.two-face/
├── config.toml # Global defaults
├── layout.toml
├── keybinds.toml
├── highlights.toml
├── colors.toml
└── profiles/
├── Warrior/ # Character-specific overrides
│ ├── layout.toml
│ └── highlights.toml
└── Wizard/
├── layout.toml
└── highlights.toml
Loading Order
- Load global defaults from
~/.two-face/ - If
--characterspecified, overlay fromprofiles/<CharName>/ - Character files only need to contain overrides, not complete configs
Usage
# Load Warrior's profile
two-face --character Warrior
Configuration Syntax
All config files use TOML syntax:
# Comments start with #
# Simple key-value
setting = "value"
number = 42
enabled = true
# Nested tables
[section]
key = "value"
# Inline tables
point = { x = 10, y = 20 }
# Arrays
list = ["one", "two", "three"]
# Array of tables
[[items]]
name = "first"
[[items]]
name = "second"
Color Values
Colors can be specified as:
# Hex RGB
color = "#FF5500"
# Hex RRGGBB (same as above)
color = "#ff5500"
# Named color (from palette)
color = "bright_red"
# Preset reference
color = "@speech" # Uses the 'speech' preset color
Size Values
# Fixed pixels
width = 40
# Percentage of parent
width = "50%"
Default Files
Two-Face ships with sensible defaults embedded in the binary. On first run, these are written to your data directory if the files don’t exist.
See Default Files Reference for complete default content.
Validation
Two-Face validates configuration on load:
- Errors: Invalid syntax or required fields prevent loading
- Warnings: Unknown keys or deprecated settings log warnings but allow loading
- Missing files: Created from defaults
Check ~/.two-face/two-face.log for configuration warnings.
Quick Reference
Most Common Settings
| Setting | File | Key |
|---|---|---|
| Connection port | config.toml | connection.port |
| Default character | config.toml | connection.character |
| Main window size | layout.toml | [[windows]] with name = "main" |
| Monsterbold color | colors.toml | [presets.monsterbold] |
| Attack keybind | keybinds.toml | Entry with action = "send" |
Reload Configuration
- F5: Reload all config files
- Ctrl+M → Reload: Same via menu
- Restart: Always picks up changes
File Reference
| Page | Contents |
|---|---|
| config.toml | Connection, sound, TTS, general behavior |
| layout.toml | Window definitions, positions, sizes |
| keybinds.toml | Key mappings, actions, modifiers |
| highlights.toml | Pattern matching, colors, conditions |
| colors.toml | Theme colors, presets, UI styling |
| profiles.md | Per-character configuration |
See Also
- Creating Layouts - Layout design guide
- Creating Themes - Theme authoring
- Default Files - Built-in defaults
config.toml Reference
The main configuration file controlling Two-Face’s behavior, connection settings, and general preferences.
Location
~/.two-face/config.toml
Complete Reference
#
# Two-Face Configuration
#
[connection]
# Default host for Lich proxy mode
host = "127.0.0.1"
# Default port for Lich proxy mode
port = 8000
# Default character name (used for profile loading)
character = ""
# Timeout for connection attempts (seconds)
timeout = 30
[interface]
# Enable clickable links in game text
links = true
# Show window borders by default
show_borders = true
# Default border style: "plain", "rounded", "double", "thick"
border_style = "rounded"
# Mouse support
mouse = true
# Scroll speed (lines per scroll event)
scroll_speed = 3
# Command history size
history_size = 1000
[sound]
# Enable sound effects
enabled = true
# Master volume (0.0 - 1.0)
volume = 0.8
# Play startup music
startup_music = true
# Sound file paths (relative to data dir or absolute)
# alert_sound = "sounds/alert.wav"
[tts]
# Enable text-to-speech
enabled = false
# TTS rate (words per minute, platform dependent)
rate = 150
# Which streams to speak
streams = ["main"]
# Speak room descriptions
speak_rooms = true
# Speak player speech
speak_speech = true
[logging]
# Log level: "error", "warn", "info", "debug", "trace"
level = "warn"
# Log file path (relative to data dir)
file = "two-face.log"
# Log to console (for debugging)
console = false
[behavior]
# Auto-scroll to bottom on new text
auto_scroll = true
# Flash window on important events
flash_on_alert = false
# Notification sound on important events
sound_on_alert = false
# Pause input during roundtime (experimental)
pause_on_rt = false
[performance]
# Maximum lines to buffer per text window
max_buffer_lines = 2000
# Frame rate target
target_fps = 60
# Enable performance metrics collection
collect_metrics = true
Section Details
[connection]
Controls how Two-Face connects to the game.
| Key | Type | Default | Description |
|---|---|---|---|
host | string | "127.0.0.1" | Host for Lich proxy mode |
port | integer | 8000 | Port for Lich proxy mode |
character | string | "" | Default character name for profiles |
timeout | integer | 30 | Connection timeout in seconds |
Note: Direct mode (--direct) ignores these settings and uses command-line credentials.
[interface]
Visual and interaction settings.
| Key | Type | Default | Description |
|---|---|---|---|
links | boolean | true | Enable clickable game links |
show_borders | boolean | true | Show window borders by default |
border_style | string | "rounded" | Default border style |
mouse | boolean | true | Enable mouse support |
scroll_speed | integer | 3 | Lines scrolled per mouse wheel tick |
history_size | integer | 1000 | Command history entries to remember |
Border styles:
"plain"- Single line:─│─│┌┐└┘"rounded"- Rounded corners:─│─│╭╮╰╯"double"- Double line:═║═║╔╗╚╝"thick"- Thick line:━┃━┃┏┓┗┛
[sound]
Audio settings.
| Key | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | Master sound toggle |
volume | float | 0.8 | Master volume (0.0 to 1.0) |
startup_music | boolean | true | Play music on launch |
Note: Requires the sound feature to be compiled in.
[tts]
Text-to-speech accessibility features.
| Key | Type | Default | Description |
|---|---|---|---|
enabled | boolean | false | Enable TTS |
rate | integer | 150 | Speech rate (WPM) |
streams | array | ["main"] | Streams to speak |
speak_rooms | boolean | true | Speak room descriptions |
speak_speech | boolean | true | Speak player dialogue |
Supported streams: "main", "speech", "thoughts", "combat"
[logging]
Diagnostic logging configuration.
| Key | Type | Default | Description |
|---|---|---|---|
level | string | "warn" | Minimum log level |
file | string | "two-face.log" | Log file path |
console | boolean | false | Also log to console |
Log levels (from least to most verbose):
"error"- Only errors"warn"- Errors and warnings"info"- Normal operation info"debug"- Debugging details"trace"- Very verbose tracing
[behavior]
Client behavior tweaks.
| Key | Type | Default | Description |
|---|---|---|---|
auto_scroll | boolean | true | Auto-scroll on new text |
flash_on_alert | boolean | false | Flash window on alerts |
sound_on_alert | boolean | false | Play sound on alerts |
pause_on_rt | boolean | false | Pause input during RT |
[performance]
Performance tuning.
| Key | Type | Default | Description |
|---|---|---|---|
max_buffer_lines | integer | 2000 | Max lines per text window |
target_fps | integer | 60 | Target frame rate |
collect_metrics | boolean | true | Enable performance stats |
Examples
Minimal Config
[connection]
port = 8000
character = "MyCharacter"
High-Performance Config
[performance]
max_buffer_lines = 1000
target_fps = 120
collect_metrics = false
[interface]
links = false # Disable link parsing for speed
Accessibility Config
[tts]
enabled = true
rate = 175
streams = ["main", "speech"]
speak_rooms = true
speak_speech = true
[interface]
scroll_speed = 5
Environment Variable Overrides
Some settings can be overridden via environment variables:
| Variable | Overrides |
|---|---|
TWO_FACE_DIR | Data directory location |
RUST_LOG | Logging level |
COLORTERM | Terminal color support |
See Also
- layout.toml - Window layout configuration
- Character Profiles - Per-character settings
- Environment Variables - All environment variables
layout.toml Reference
The layout configuration file defines all windows, their positions, sizes, and visual properties.
Location
~/.two-face/layout.toml
Structure Overview
# Global layout settings
[layout]
columns = 120
rows = 40
# Window definitions
[[windows]]
name = "main"
type = "text"
# ... window properties
[[windows]]
name = "room"
type = "room"
# ... window properties
[layout] Section
Global layout dimensions and defaults.
[layout]
# Terminal grid size
columns = 120 # Total columns
rows = 40 # Total rows
# Default window settings
default_border = true
default_border_style = "rounded"
| Key | Type | Default | Description |
|---|---|---|---|
columns | integer | 120 | Layout width in columns |
rows | integer | 40 | Layout height in rows |
default_border | boolean | true | Default border visibility |
default_border_style | string | "rounded" | Default border style |
[[windows]] Array
Each [[windows]] entry defines a single window.
Common Properties
These properties apply to all window types:
[[windows]]
# Identity
name = "main" # Unique identifier (required)
type = "text" # Widget type (required)
# Position (choose one method)
row = 0 # Row position
col = 0 # Column position
# OR
x = 0 # Pixel X position
y = 0 # Pixel Y position
# Size
width = 80 # Width in columns
height = 30 # Height in rows
# OR percentage
width = "60%" # Percentage of parent width
height = "80%" # Percentage of parent height
# Visual
show_border = true # Show window border
border_style = "rounded" # Border style
border_sides = "all" # Which sides have borders
show_title = true # Show title bar
title = "Main Window" # Custom title (default: name)
transparent_background = false # Transparent background
# Colors (override theme)
border_color = "#5588AA" # Border color
background_color = "#000000" # Background color
text_color = "#CCCCCC" # Text color
Position Properties
| Key | Type | Description |
|---|---|---|
row | integer | Row position (0 = top) |
col | integer | Column position (0 = left) |
x | integer | Pixel X position |
y | integer | Pixel Y position |
Note: Use row/col for grid-based layouts, x/y for pixel-precise positioning.
Size Properties
| Key | Type | Description |
|---|---|---|
width | integer or string | Width in columns or percentage |
height | integer or string | Height in rows or percentage |
Percentage sizing:
width = "50%" # 50% of layout width
height = "100%" # Full layout height
Border Properties
| Key | Type | Default | Description |
|---|---|---|---|
show_border | boolean | true | Show border |
border_style | string | "rounded" | Border style |
border_sides | string | "all" | Which sides |
border_color | string | theme | Border color |
Border sides:
"all"- All four sides"none"- No borders"top"- Top only"bottom"- Bottom only"left"- Left only"right"- Right only"top,bottom"- Specific sides (comma-separated)"horizontal"- Top and bottom"vertical"- Left and right
Window Types
Text Window
Scrollable text display for game output.
[[windows]]
name = "main"
type = "text"
row = 0
col = 0
width = 80
height = 30
# Text-specific options
stream = "main" # Game stream to display
buffer_size = 2000 # Maximum lines to buffer
word_wrap = true # Enable word wrapping
| Key | Type | Default | Description |
|---|---|---|---|
stream | string | same as name | Game stream to display |
buffer_size | integer | 2000 | Max buffered lines |
word_wrap | boolean | true | Enable word wrap |
Common streams: main, speech, thoughts, combat, death, logons, familiar, group
Tabbed Text Window
Multiple streams in tabbed interface.
[[windows]]
name = "channels"
type = "tabbed_text"
row = 0
col = 80
width = 40
height = 30
# Tab configuration
tabs = ["speech", "thoughts", "combat"]
default_tab = "speech"
show_tab_bar = true
| Key | Type | Default | Description |
|---|---|---|---|
tabs | array | [] | List of stream names |
default_tab | string | first tab | Initially active tab |
show_tab_bar | boolean | true | Show tab headers |
Command Input
Text input field for commands.
[[windows]]
name = "input"
type = "command_input"
row = 38
col = 0
width = 120
height = 2
# Input-specific options
prompt = "> "
history = true
| Key | Type | Default | Description |
|---|---|---|---|
prompt | string | "> " | Input prompt |
history | boolean | true | Enable command history |
Progress Bar
Displays a value as a filled bar.
[[windows]]
name = "health"
type = "progress"
row = 0
col = 80
width = 20
height = 1
# Progress-specific options
stat = "health" # Stat to track
show_value = true # Show "425/500"
show_percentage = false # Show "85%"
bar_color = "#00FF00" # Bar fill color
| Key | Type | Default | Description |
|---|---|---|---|
stat | string | required | Stat ID (health, mana, etc.) |
show_value | boolean | true | Show numeric value |
show_percentage | boolean | false | Show percentage |
bar_color | string | theme | Bar fill color |
Stat IDs: health, mana, spirit, stamina, encumbrance
Countdown
Countdown timer display.
[[windows]]
name = "roundtime"
type = "countdown"
row = 2
col = 80
width = 20
height = 1
# Countdown-specific options
countdown_type = "roundtime" # "roundtime" or "casttime"
show_seconds = true
icon = "⏱"
| Key | Type | Default | Description |
|---|---|---|---|
countdown_type | string | required | roundtime or casttime |
show_seconds | boolean | true | Show remaining seconds |
icon | string | "" | Icon to display |
Compass
Directional compass display.
[[windows]]
name = "compass"
type = "compass"
row = 5
col = 100
width = 10
height = 5
# Compass-specific options
style = "graphical" # "graphical" or "text"
show_labels = true # Show N/S/E/W labels
| Key | Type | Default | Description |
|---|---|---|---|
style | string | "graphical" | Display style |
show_labels | boolean | true | Show direction labels |
Hand
Left/right hand or spell display.
[[windows]]
name = "right_hand"
type = "hand"
row = 10
col = 100
width = 15
height = 1
# Hand-specific options
hand_type = "right" # "left", "right", or "spell"
icon = "🤚"
| Key | Type | Default | Description |
|---|---|---|---|
hand_type | string | required | left, right, or spell |
icon | string | "" | Icon to display |
Indicator
Status indicator widget.
[[windows]]
name = "status"
type = "indicator"
row = 12
col = 100
width = 20
height = 1
# Indicator-specific options
indicators = ["stunned", "hidden", "poisoned"]
style = "icons" # "icons" or "text"
| Key | Type | Default | Description |
|---|---|---|---|
indicators | array | all | Which indicators to show |
style | string | "icons" | Display style |
Indicator IDs: stunned, hidden, webbed, poisoned, diseased, bleeding, prone, kneeling, sitting
Room Window
Room description display.
[[windows]]
name = "room"
type = "room"
row = 0
col = 80
width = 40
height = 10
# Room-specific options
show_title = true # Show room name
show_description = true # Show room desc
show_exits = true # Show obvious exits
show_objects = true # Show items/creatures
show_players = true # Show other players
Injury Doll
Body injury display.
[[windows]]
name = "injuries"
type = "injury_doll"
row = 15
col = 100
width = 15
height = 10
# Injury-specific options
show_labels = true
compact = false
Performance Monitor
Performance metrics display.
[[windows]]
name = "performance"
type = "performance"
row = 0
col = 0
width = 30
height = 15
# Which metrics to show
show_fps = true
show_frame_times = true
show_render_times = true
show_net = true
show_parse = true
show_memory = true
show_lines = true
show_uptime = true
Layout Examples
Basic Two-Column Layout
[layout]
columns = 120
rows = 40
[[windows]]
name = "main"
type = "text"
row = 0
col = 0
width = 80
height = 38
[[windows]]
name = "room"
type = "room"
row = 0
col = 80
width = 40
height = 15
[[windows]]
name = "vitals"
type = "progress"
stat = "health"
row = 15
col = 80
width = 40
height = 1
[[windows]]
name = "input"
type = "command_input"
row = 38
col = 0
width = 120
height = 2
Hunting Layout
See Hunting Setup Tutorial for a complete example.
Minimal Layout
[layout]
columns = 80
rows = 24
[[windows]]
name = "main"
type = "text"
row = 0
col = 0
width = 80
height = 22
show_border = false
[[windows]]
name = "input"
type = "command_input"
row = 22
col = 0
width = 80
height = 2
show_border = false
See Also
- Creating Layouts - Layout design guide
- Widget Reference - Detailed widget documentation
- Tutorials - Complete layout examples
keybinds.toml Reference
The keybinds configuration file maps keyboard shortcuts to actions.
Location
~/.two-face/keybinds.toml
Structure Overview
[[keybinds]]
key = "Enter"
action = "send"
[[keybinds]]
key = "Ctrl+Q"
action = "quit"
[[keybinds]]
key = "F1"
action = "send"
argument = "look"
Keybind Properties
Each [[keybinds]] entry defines one key mapping:
[[keybinds]]
# Required
key = "Ctrl+S" # Key combination
action = "send" # Action to perform
# Optional
argument = "save" # Action argument
description = "Save game" # Description for help
context = "input" # When this binding is active
| Property | Type | Required | Description |
|---|---|---|---|
key | string | yes | Key combination |
action | string | yes | Action name |
argument | string | no | Action argument |
description | string | no | Help text |
context | string | no | Active context |
Key Syntax
Basic Keys
key = "A" # Letter A
key = "1" # Number 1
key = "F1" # Function key
key = "Enter" # Enter key
key = "Space" # Space bar
key = "Tab" # Tab key
key = "Escape" # Escape key
Modifier Keys
Combine modifiers with +:
key = "Ctrl+S" # Ctrl + S
key = "Alt+F4" # Alt + F4
key = "Shift+Tab" # Shift + Tab
key = "Ctrl+Shift+Z" # Ctrl + Shift + Z
key = "Ctrl+Alt+Delete" # Multiple modifiers
Available modifiers:
Ctrl- Control keyAlt- Alt keyShift- Shift key
Special Keys
| Key Name | Description |
|---|---|
Enter | Enter/Return |
Space | Space bar |
Tab | Tab key |
Escape | Escape key |
Backspace | Backspace |
Delete | Delete key |
Insert | Insert key |
Home | Home key |
End | End key |
PageUp | Page Up |
PageDown | Page Down |
Up | Up arrow |
Down | Down arrow |
Left | Left arrow |
Right | Right arrow |
F1 - F12 | Function keys |
Numpad Keys
key = "Numpad0" # Numpad 0
key = "Numpad+" # Numpad plus
key = "Numpad*" # Numpad multiply
key = "NumpadEnter" # Numpad enter
Actions
Input Actions
| Action | Argument | Description |
|---|---|---|
send | command | Send command to game |
send_silent | command | Send without echo |
insert | text | Insert text at cursor |
clear_input | - | Clear input field |
history_prev | - | Previous command |
history_next | - | Next command |
Examples:
[[keybinds]]
key = "Enter"
action = "send"
[[keybinds]]
key = "F1"
action = "send"
argument = "attack"
[[keybinds]]
key = "Ctrl+K"
action = "clear_input"
Navigation Actions
| Action | Argument | Description |
|---|---|---|
scroll_up | lines | Scroll up |
scroll_down | lines | Scroll down |
scroll_page_up | - | Page up |
scroll_page_down | - | Page down |
scroll_top | - | Jump to top |
scroll_bottom | - | Jump to bottom |
focus_next | - | Focus next window |
focus_prev | - | Focus previous window |
focus_window | name | Focus specific window |
Examples:
[[keybinds]]
key = "PageUp"
action = "scroll_page_up"
[[keybinds]]
key = "Ctrl+Tab"
action = "focus_next"
[[keybinds]]
key = "Ctrl+1"
action = "focus_window"
argument = "main"
Menu Actions
| Action | Argument | Description |
|---|---|---|
open_menu | - | Open main menu |
open_highlight_browser | - | Open highlight editor |
open_keybind_browser | - | Open keybind editor |
open_color_browser | - | Open color editor |
open_window_editor | - | Open window editor |
close_popup | - | Close current popup |
Examples:
[[keybinds]]
key = "Ctrl+M"
action = "open_menu"
[[keybinds]]
key = "Ctrl+H"
action = "open_highlight_browser"
[[keybinds]]
key = "Escape"
action = "close_popup"
Client Actions
| Action | Argument | Description |
|---|---|---|
quit | - | Exit Two-Face |
reload_config | - | Reload configuration |
clear_window | name | Clear window content |
toggle_links | - | Toggle clickable links |
toggle_border | name | Toggle window border |
copy | - | Copy selection |
paste | - | Paste clipboard |
Examples:
[[keybinds]]
key = "Ctrl+Q"
action = "quit"
[[keybinds]]
key = "F5"
action = "reload_config"
[[keybinds]]
key = "Ctrl+L"
action = "clear_window"
argument = "main"
Game Actions
| Action | Argument | Description |
|---|---|---|
move | direction | Move in direction |
look | - | Look around |
inventory | - | Check inventory |
Examples:
[[keybinds]]
key = "Numpad8"
action = "move"
argument = "north"
[[keybinds]]
key = "Numpad2"
action = "move"
argument = "south"
Contexts
Keybinds can be limited to specific contexts:
| Context | Active When |
|---|---|
global | Always (default) |
input | Input field focused |
menu | Menu/popup open |
browser | Browser popup open |
editor | Editor popup open |
Examples:
# Only active when input is focused
[[keybinds]]
key = "Tab"
action = "autocomplete"
context = "input"
# Only active in menus
[[keybinds]]
key = "Enter"
action = "select"
context = "menu"
Default Keybinds
Two-Face ships with these defaults:
Navigation
[[keybinds]]
key = "PageUp"
action = "scroll_page_up"
[[keybinds]]
key = "PageDown"
action = "scroll_page_down"
[[keybinds]]
key = "Home"
action = "scroll_top"
[[keybinds]]
key = "End"
action = "scroll_bottom"
[[keybinds]]
key = "Ctrl+Tab"
action = "focus_next"
Input
[[keybinds]]
key = "Enter"
action = "send"
[[keybinds]]
key = "Up"
action = "history_prev"
[[keybinds]]
key = "Down"
action = "history_next"
[[keybinds]]
key = "Ctrl+C"
action = "copy"
[[keybinds]]
key = "Ctrl+V"
action = "paste"
Menus
[[keybinds]]
key = "Ctrl+M"
action = "open_menu"
[[keybinds]]
key = "Ctrl+H"
action = "open_highlight_browser"
[[keybinds]]
key = "Ctrl+K"
action = "open_keybind_browser"
[[keybinds]]
key = "Ctrl+E"
action = "open_window_editor"
[[keybinds]]
key = "Escape"
action = "close_popup"
Client
[[keybinds]]
key = "Ctrl+Q"
action = "quit"
[[keybinds]]
key = "F5"
action = "reload_config"
[[keybinds]]
key = "Ctrl+L"
action = "clear_window"
argument = "main"
Example Configurations
Numpad Movement
[[keybinds]]
key = "Numpad8"
action = "send"
argument = "north"
description = "Move north"
[[keybinds]]
key = "Numpad2"
action = "send"
argument = "south"
[[keybinds]]
key = "Numpad4"
action = "send"
argument = "west"
[[keybinds]]
key = "Numpad6"
action = "send"
argument = "east"
[[keybinds]]
key = "Numpad7"
action = "send"
argument = "northwest"
[[keybinds]]
key = "Numpad9"
action = "send"
argument = "northeast"
[[keybinds]]
key = "Numpad1"
action = "send"
argument = "southwest"
[[keybinds]]
key = "Numpad3"
action = "send"
argument = "southeast"
[[keybinds]]
key = "Numpad5"
action = "send"
argument = "out"
Combat Macros
[[keybinds]]
key = "F1"
action = "send"
argument = "attack"
description = "Basic attack"
[[keybinds]]
key = "F2"
action = "send"
argument = "incant 101"
description = "Cast Spirit Warding I"
[[keybinds]]
key = "F3"
action = "send"
argument = "stance defensive"
description = "Defensive stance"
See Also
- Keybind Actions Reference - Complete action list
- Quick Tour - Default keybinds
- Customization - Keybind customization guide
highlights.toml Reference
The highlights configuration file defines text patterns and their visual styling.
Location
~/.two-face/highlights.toml
Structure Overview
[[highlights]]
name = "creatures"
pattern = "(goblin|orc|troll)"
fg = "#FF0000"
bold = true
[[highlights]]
name = "player_speech"
pattern = '^\w+ (says|asks|exclaims),'
fg = "#00FF00"
stream = "main"
Highlight Properties
Each [[highlights]] entry defines one highlighting rule:
[[highlights]]
# Identity
name = "my_highlight" # Unique name (required)
pattern = "text to match" # Regex pattern (required)
# Colors
fg = "#FF0000" # Foreground color
bg = "#000000" # Background color
# Styling
bold = false # Bold text
italic = false # Italic text
underline = false # Underlined text
# Scope
stream = "main" # Limit to stream (optional)
span_type = "Normal" # Only match span type (optional)
# Performance
fast_parse = false # Use literal matching
enabled = true # Enable/disable
priority = 0 # Match priority (higher = first)
Required Properties
| Property | Type | Description |
|---|---|---|
name | string | Unique identifier |
pattern | string | Regex pattern to match |
Color Properties
| Property | Type | Default | Description |
|---|---|---|---|
fg | string | - | Foreground color |
bg | string | - | Background color |
Color formats:
fg = "#FF5500" # Hex RGB
fg = "bright_red" # Palette name
fg = "@speech" # Preset reference
Style Properties
| Property | Type | Default | Description |
|---|---|---|---|
bold | boolean | false | Bold text |
italic | boolean | false | Italic text |
underline | boolean | false | Underline text |
Scope Properties
| Property | Type | Default | Description |
|---|---|---|---|
stream | string | all | Limit to specific stream |
span_type | string | all | Only match span type |
Stream values: main, speech, thoughts, combat, etc.
Span types:
Normal- Regular textLink- Clickable linksMonsterbold- Creature namesSpeech- Player dialogueSpell- Spell names
Performance Properties
| Property | Type | Default | Description |
|---|---|---|---|
fast_parse | boolean | false | Use Aho-Corasick |
enabled | boolean | true | Enable this highlight |
priority | integer | 0 | Match order (higher first) |
Pattern Syntax
Highlights use Rust regex syntax. Key features:
Basic Patterns
# Literal text
pattern = "goblin"
# Case insensitive
pattern = "(?i)goblin"
# Word boundaries
pattern = "\\bgoblin\\b"
# Multiple words
pattern = "goblin|orc|troll"
Character Classes
# Any digit
pattern = "\\d+"
# Any word character
pattern = "\\w+"
# Any whitespace
pattern = "\\s+"
# Custom class
pattern = "[A-Z][a-z]+"
Anchors
# Start of line
pattern = "^You attack"
# End of line
pattern = "falls dead!$"
# Word boundary
pattern = "\\bgoblin\\b"
Groups and Captures
# Non-capturing group
pattern = "(?:goblin|orc)"
# Capturing group (for future use)
pattern = "(\\w+) says,"
Escaping Special Characters
These characters must be escaped with \\:
. * + ? ^ $ { } [ ] ( ) | \
# Match literal period
pattern = "Mr\\. Smith"
# Match literal asterisk
pattern = "\\*\\*emphasis\\*\\*"
Fast Parse Mode
For simple literal patterns, fast_parse = true uses Aho-Corasick algorithm:
[[highlights]]
name = "names"
pattern = "Bob|Alice|Charlie"
fg = "#FFFF00"
fast_parse = true
When to use fast_parse:
- Pattern is literal strings (no regex)
- Pattern has many alternatives (names, items)
- High-frequency matching needed
When NOT to use fast_parse:
- Pattern uses regex features
- Pattern needs word boundaries
- Pattern uses anchors (^, $)
Priority System
When multiple highlights match, priority determines which wins:
# Higher priority wins
[[highlights]]
name = "important"
pattern = "critical"
fg = "#FF0000"
priority = 100
# Lower priority, same match
[[highlights]]
name = "general"
pattern = ".*"
fg = "#808080"
priority = 0
Default priorities:
- Monsterbold spans: Highest
- Link spans: High
- User highlights: Medium (configurable)
- Preset colors: Low
Stream Filtering
Limit highlights to specific streams:
# Only in main window
[[highlights]]
name = "combat"
pattern = "You (hit|miss)"
fg = "#FF0000"
stream = "main"
# Only in thoughts
[[highlights]]
name = "esp"
pattern = "\\[ESP\\]"
fg = "#00FFFF"
stream = "thoughts"
Common Patterns
Creature Names
[[highlights]]
name = "creatures"
pattern = "(?i)\\b(goblin|orc|troll|kobold|giant)s?\\b"
fg = "#FF6600"
bold = true
Player Speech
[[highlights]]
name = "speech"
pattern = '^(\\w+) (says|asks|exclaims|whispers),'
fg = "#00FF00"
Your Character Name
[[highlights]]
name = "my_name"
pattern = "\\bYourCharacterName\\b"
fg = "#FFFF00"
bold = true
Damage Numbers
[[highlights]]
name = "damage"
pattern = "\\b\\d+\\s+points?\\s+of\\s+damage"
fg = "#FF0000"
bold = true
Silver/Gold
[[highlights]]
name = "silver"
pattern = "\\d+\\s+silver"
fg = "#C0C0C0"
bold = true
[[highlights]]
name = "gold"
pattern = "\\d+\\s+gold"
fg = "#FFD700"
bold = true
Room Exits
[[highlights]]
name = "exits"
pattern = "Obvious (paths|exits):"
fg = "#00FFFF"
ESP/Thoughts
[[highlights]]
name = "esp"
pattern = "^\\[.*?\\]"
fg = "#FF00FF"
stream = "thoughts"
Complete Example
# Two-Face Highlights Configuration
# Creature highlighting
[[highlights]]
name = "creatures"
pattern = "(?i)\\b(goblin|orc|troll|kobold|giant rat|wolf|bear)s?\\b"
fg = "#FF6600"
bold = true
priority = 50
# Player names (customize this list)
[[highlights]]
name = "friends"
pattern = "\\b(Alice|Bob|Charlie)\\b"
fg = "#00FF00"
fast_parse = true
priority = 60
# Damage taken
[[highlights]]
name = "damage_taken"
pattern = "strikes you|hits you|bites you"
fg = "#FF0000"
bold = true
priority = 70
# Damage dealt
[[highlights]]
name = "damage_dealt"
pattern = "You (hit|strike|slash)"
fg = "#00FF00"
priority = 70
# Currency
[[highlights]]
name = "silver"
pattern = "\\d+\\s+silvers?"
fg = "#C0C0C0"
[[highlights]]
name = "gold"
pattern = "\\b(gold|golden)\\b"
fg = "#FFD700"
# Important messages
[[highlights]]
name = "roundtime"
pattern = "Roundtime:"
fg = "#FF00FF"
bold = true
[[highlights]]
name = "death"
pattern = "You have died|DEAD"
fg = "#FF0000"
bg = "#400000"
bold = true
priority = 100
See Also
- Highlight Patterns Guide - Advanced patterns
- Colors Reference - Color values and palettes
- Performance - Highlight optimization
colors.toml Reference
The colors configuration file defines the visual theme including presets, palettes, and UI colors.
Location
~/.two-face/colors.toml
Structure Overview
# Named presets (game text styling)
[presets]
speech = { fg = "#53a684" }
monsterbold = { fg = "#a29900" }
# Color palette (named colors)
[palette]
bright_red = "#FF5555"
bright_green = "#55FF55"
# UI element colors
[ui]
border = "#5588AA"
background = "#000000"
# Prompt colors
[prompt]
default = "#808080"
[presets] Section
Presets define colors for game text elements:
[presets]
# Speech/dialogue
speech = { fg = "#53a684" }
# Creature names (monsterbold)
monsterbold = { fg = "#a29900" }
# Clickable links
links = { fg = "#477ab3" }
# Command echoes
commands = { fg = "#477ab3" }
# Whispers
whisper = { fg = "#4682B4" }
# Room name
roomName = { fg = "#9BA2B2", bg = "#395573" }
# Room description
roomDesc = { fg = "#CCCCCC" }
# Thoughts/ESP
thought = { fg = "#FF00FF" }
# Combat messages
combat = { fg = "#FF6600" }
Preset Format
Each preset can have:
[presets]
name = { fg = "#RRGGBB", bg = "#RRGGBB", bold = true }
| Property | Type | Description |
|---|---|---|
fg | string | Foreground color |
bg | string | Background color |
bold | boolean | Bold styling |
Standard Presets
These presets are used by the game protocol:
| Preset | Used For |
|---|---|
speech | Player dialogue |
monsterbold | Creature names |
links | Clickable game objects |
commands | Command echoes |
whisper | Whispered text |
roomName | Room titles |
roomDesc | Room descriptions |
thought | ESP/thoughts stream |
[palette] Section
Named colors for use elsewhere in configuration:
[palette]
# Base colors
black = "#000000"
white = "#FFFFFF"
red = "#AA0000"
green = "#00AA00"
blue = "#0000AA"
yellow = "#AAAA00"
cyan = "#00AAAA"
magenta = "#AA00AA"
# Bright variants
bright_black = "#555555"
bright_white = "#FFFFFF"
bright_red = "#FF5555"
bright_green = "#55FF55"
bright_blue = "#5555FF"
bright_yellow = "#FFFF55"
bright_cyan = "#55FFFF"
bright_magenta = "#FF55FF"
# Custom colors
health_high = "#00FF00"
health_mid = "#FFFF00"
health_low = "#FF0000"
mana_color = "#0088FF"
Using Palette Colors
Reference palette colors in other config files:
# In highlights.toml
[[highlights]]
name = "damage"
pattern = "damage"
fg = "bright_red" # Uses palette.bright_red
# In layout.toml
[[windows]]
name = "health"
type = "progress"
bar_color = "health_high" # Uses palette.health_high
[ui] Section
Colors for UI elements:
[ui]
# Borders
border = "#5588AA"
border_focused = "#88AACC"
# Backgrounds
background = "#000000"
window_background = "#0A0A0A"
# Text
text = "#CCCCCC"
text_dim = "#808080"
text_highlight = "#FFFFFF"
# Selection
selection_bg = "#264F78"
selection_fg = "#FFFFFF"
# Scrollbar
scrollbar_track = "#1A1A1A"
scrollbar_thumb = "#404040"
# Menu
menu_bg = "#1A1A1A"
menu_fg = "#CCCCCC"
menu_selected_bg = "#264F78"
menu_selected_fg = "#FFFFFF"
# Input
input_bg = "#0A0A0A"
input_fg = "#FFFFFF"
input_cursor = "#FFFFFF"
UI Color Reference
| Color | Used For |
|---|---|
border | Window borders |
border_focused | Focused window border |
background | Global background |
window_background | Window backgrounds |
text | Default text |
text_dim | Dimmed/secondary text |
text_highlight | Highlighted text |
selection_bg | Selection background |
selection_fg | Selection foreground |
[prompt] Section
Colors for the game prompt:
[prompt]
# Default prompt color
default = "#808080"
# Combat/RT prompt
combat = "#FF0000"
# Safe prompt
safe = "#00FF00"
[spell] Section
Colors for spell circle indicators:
[spell]
# Major Elemental
major_elemental = "#FF6600"
# Minor Elemental
minor_elemental = "#FFAA00"
# Major Spiritual
major_spiritual = "#00AAFF"
# Minor Spiritual
minor_spiritual = "#0066FF"
# Bard
bard = "#FF00FF"
# Wizard
wizard = "#AA00FF"
# Sorcerer
sorcerer = "#660066"
# Ranger
ranger = "#00AA00"
# Paladin
paladin = "#FFFF00"
# Cleric
cleric = "#FFFFFF"
# Empath
empath = "#00FFAA"
Color Formats
Hex RGB
color = "#FF5500" # 6-digit hex
color = "#F50" # 3-digit hex (expanded to #FF5500)
Named Colors
Reference palette entries:
color = "bright_red" # From [palette]
color = "health_high" # Custom palette entry
Preset References
Reference preset colors with @:
color = "@speech" # Uses presets.speech.fg
color = "@monsterbold" # Uses presets.monsterbold.fg
Complete Theme Example
# Two-Face Dark Theme
[presets]
speech = { fg = "#53a684" }
monsterbold = { fg = "#e8b923", bold = true }
links = { fg = "#477ab3" }
commands = { fg = "#477ab3" }
whisper = { fg = "#4682B4" }
roomName = { fg = "#9BA2B2", bg = "#395573" }
roomDesc = { fg = "#B0B0B0" }
thought = { fg = "#DA70D6" }
[palette]
# Base colors
black = "#000000"
white = "#FFFFFF"
red = "#CC0000"
green = "#00CC00"
blue = "#0066CC"
yellow = "#CCCC00"
cyan = "#00CCCC"
magenta = "#CC00CC"
# Bright variants
bright_red = "#FF5555"
bright_green = "#55FF55"
bright_blue = "#5588FF"
bright_yellow = "#FFFF55"
bright_cyan = "#55FFFF"
bright_magenta = "#FF55FF"
# Custom
health = "#00FF00"
mana = "#0088FF"
spirit = "#00FFFF"
stamina = "#FFFF00"
[ui]
border = "#3A5F7A"
border_focused = "#5588AA"
background = "#0A0A0A"
window_background = "#0F0F0F"
text = "#CCCCCC"
text_dim = "#666666"
text_highlight = "#FFFFFF"
selection_bg = "#264F78"
selection_fg = "#FFFFFF"
menu_bg = "#1A1A1A"
menu_fg = "#CCCCCC"
menu_selected_bg = "#264F78"
menu_selected_fg = "#FFFFFF"
[prompt]
default = "#666666"
combat = "#FF4444"
safe = "#44FF44"
Theme Switching
Two-Face supports hot theme switching:
- Edit
colors.toml - Press
F5to reload - New colors apply immediately
Or create multiple theme files and switch by copying:
cp themes/solarized.toml ~/.two-face/colors.toml
# Then press F5 in Two-Face
See Also
- Creating Themes - Theme authoring guide
- Preset Colors Reference - All preset names
- Color Codes - Hex color reference
Character Profiles
Two-Face supports per-character configuration through profiles, allowing different layouts, highlights, and settings for each character.
Overview
Profiles let you:
- Use different layouts for different playstyles (hunting warrior vs. merchant)
- Customize highlights per character (different friend lists, creature lists)
- Override any global setting on a per-character basis
Profile Structure
Profiles are stored in ~/.two-face/profiles/<CharacterName>/:
~/.two-face/
├── config.toml # Global defaults
├── layout.toml # Global layout
├── keybinds.toml # Global keybinds
├── highlights.toml # Global highlights
├── colors.toml # Global colors
│
└── profiles/
├── Warrior/ # Warrior's profile
│ ├── layout.toml # Warrior's layout
│ └── highlights.toml
│
├── Wizard/ # Wizard's profile
│ ├── layout.toml
│ ├── highlights.toml
│ └── keybinds.toml
│
└── Merchant/ # Merchant's profile
└── layout.toml
Using Profiles
Loading a Profile
Specify the character name when launching:
two-face --character Warrior
Or set a default in config.toml:
[connection]
character = "Warrior"
Profile Loading Order
- Load global config from
~/.two-face/ - If profile exists, overlay files from
profiles/<CharName>/ - Missing profile files fall back to global
Example flow for --character Warrior:
| File | Source |
|---|---|
| config.toml | Global (no profile override) |
| layout.toml | profiles/Warrior/layout.toml |
| keybinds.toml | Global (no profile override) |
| highlights.toml | profiles/Warrior/highlights.toml |
| colors.toml | Global (no profile override) |
Creating a Profile
Method 1: Manual Creation
# Create profile directory
mkdir -p ~/.two-face/profiles/MyCharacter
# Copy files you want to customize
cp ~/.two-face/layout.toml ~/.two-face/profiles/MyCharacter/
cp ~/.two-face/highlights.toml ~/.two-face/profiles/MyCharacter/
# Edit the copies
vim ~/.two-face/profiles/MyCharacter/layout.toml
Method 2: Start from Defaults
Launch with a new character name:
two-face --character NewCharacter
Two-Face uses global defaults. Then copy and customize as needed.
Profile-Specific Settings
Layout Differences
A warrior might want:
# profiles/Warrior/layout.toml
[[windows]]
name = "main"
type = "text"
width = "70%"
height = "80%"
# Large status area for combat
[[windows]]
name = "vitals"
type = "dashboard"
width = "30%"
height = "40%"
A merchant might prefer:
# profiles/Merchant/layout.toml
[[windows]]
name = "main"
type = "text"
width = "100%"
height = "90%"
# No combat widgets, maximize text area
Highlight Differences
Warrior with creature highlights:
# profiles/Warrior/highlights.toml
[[highlights]]
name = "hunting_targets"
pattern = "(?i)\\b(goblin|orc|troll|giant)s?\\b"
fg = "#FF6600"
bold = true
Merchant with item highlights:
# profiles/Merchant/highlights.toml
[[highlights]]
name = "valuable_items"
pattern = "(?i)\\b(gold|silver|gem|diamond)s?\\b"
fg = "#FFD700"
bold = true
[[highlights]]
name = "materials"
pattern = "(?i)\\b(leather|silk|velvet|mithril)\\b"
fg = "#00FFFF"
Keybind Differences
Combat character:
# profiles/Warrior/keybinds.toml
[[keybinds]]
key = "F1"
action = "send"
argument = "attack"
[[keybinds]]
key = "F2"
action = "send"
argument = "feint"
Spellcaster:
# profiles/Wizard/keybinds.toml
[[keybinds]]
key = "F1"
action = "send"
argument = "incant 901"
[[keybinds]]
key = "F2"
action = "send"
argument = "incant 903"
Shared vs. Profile-Specific
Keep Global (Shared)
- colors.toml - Usually same theme everywhere
- config.toml - Connection settings shared
Make Profile-Specific
- layout.toml - Different layouts per playstyle
- highlights.toml - Different creatures/items/friends
- keybinds.toml - Different combat macros
Profile Tips
1. Start Simple
Only create profile overrides for files you need to change. Let others fall back to global.
2. Use Consistent Names
Match your character name exactly (case-sensitive on some systems):
# Good
--character Nisugi
~/.two-face/profiles/Nisugi/
# May not work
--character nisugi
~/.two-face/profiles/Nisugi/ # Case mismatch!
3. Share Common Elements
For settings shared across some (but not all) characters, create a “base” profile and copy from it:
# Create a hunting base
cp -r ~/.two-face/profiles/Warrior ~/.two-face/profiles/hunting-base
# New hunting character
cp -r ~/.two-face/profiles/hunting-base ~/.two-face/profiles/NewHunter
4. Version Control
Consider version controlling your profiles:
cd ~/.two-face
git init
git add .
git commit -m "Initial Two-Face configuration"
Troubleshooting Profiles
Profile Not Loading
- Check character name spelling (case-sensitive)
- Verify directory exists:
ls ~/.two-face/profiles/ - Check file permissions
- Look for errors in
~/.two-face/two-face.log
Wrong Settings Applied
- Check which profile is loaded (shown on startup)
- Verify the file exists in the profile directory
- Check for syntax errors in the profile file
Resetting a Profile
# Remove profile to use global defaults
rm -rf ~/.two-face/profiles/CharName/
# Or reset specific file
rm ~/.two-face/profiles/CharName/layout.toml
Example: Multi-Character Setup
~/.two-face/
├── config.toml
├── layout.toml # Balanced default layout
├── keybinds.toml # Common keybinds
├── highlights.toml # Common highlights
├── colors.toml # Dark theme for all
│
└── profiles/
├── Ranger/
│ ├── layout.toml # Compact hunting layout
│ ├── highlights.toml # Creature + foraging
│ └── keybinds.toml # Ranged combat macros
│
├── Empath/
│ ├── layout.toml # Group-focused layout
│ └── highlights.toml # Wounds + group members
│
└── Bard/
├── layout.toml # RP-friendly layout
└── highlights.toml # Song + speech focus
See Also
- Configuration Overview - All config files
- Creating Layouts - Layout design
- Tutorials - Complete setup examples
Widgets
Widgets are the visual building blocks of Two-Face. Each widget type displays a specific kind of game information.
Overview
Two-Face provides these widget types:
| Widget | Type | Purpose |
|---|---|---|
| Text Window | text | Scrollable game text |
| Tabbed Text | tabbed_text | Multiple streams in tabs |
| Command Input | command_input | Command entry field |
| Progress Bar | progress | Health, mana, etc. |
| Countdown | countdown | RT, cast time |
| Compass | compass | Available exits |
| Hand | hand | Items in hands |
| Indicator | indicator | Status conditions |
| Injury Doll | injury_doll | Body injuries |
| Active Effects | active_effects | Buffs/debuffs |
| Room Window | room | Room description |
| Inventory | inventory | Item list |
| Spells | spells | Known spells |
| Dashboard | dashboard | Composite status |
| Performance | performance | Debug metrics |
Common Properties
All widgets share these properties in layout.toml:
Identity
[[windows]]
name = "my_widget" # Unique identifier (required)
type = "text" # Widget type (required)
Position
# Grid-based positioning
row = 0 # Row (0 = top)
col = 0 # Column (0 = left)
# OR pixel positioning
x = 100 # X coordinate
y = 50 # Y coordinate
Size
# Fixed size
width = 40 # Columns wide
height = 10 # Rows tall
# OR percentage
width = "50%" # Half of layout width
height = "100%" # Full layout height
Borders
show_border = true # Display border
border_style = "rounded" # Style: plain, rounded, double, thick
border_sides = "all" # Which sides: all, none, top, bottom, etc.
border_color = "#5588AA" # Border color
Title
show_title = true # Show title bar
title = "Custom Title" # Override default title
Colors
background_color = "#000000" # Background
text_color = "#CCCCCC" # Default text
transparent_background = false # See-through background
Widget Categories
Text Displays
Widgets that show game text:
- Text Window - Main game output, speech, thoughts
- Tabbed Text - Multiple streams in one widget
- Room Window - Room name, description, exits
Status Displays
Widgets that show character status:
- Progress Bar - Health, mana, stamina, spirit
- Countdown - Roundtime, cast time
- Hand - Left hand, right hand, spell
- Indicator - Stunned, hidden, webbed, etc.
- Injury Doll - Body part injuries
- Active Effects - Spells, buffs, cooldowns
Navigation
Widgets for game navigation:
- Compass - Available exits with directions
Lists
Widgets that display lists:
- Inventory - Items carried
- Spells - Known spells
Composite
Widgets combining multiple elements:
- Dashboard - Configurable status panel
Debug
Developer/debugging widgets:
- Performance - Frame rate, memory, network stats
Stream Mapping
Text-based widgets receive data from game streams:
| Stream | Content | Default Widget |
|---|---|---|
main | Primary game output | Main text window |
speech | Player dialogue | Speech tab/window |
thoughts | ESP/telepathy | Thoughts tab/window |
combat | Combat messages | Combat tab/window |
death | Death messages | Death window |
logons | Login/logout | Arrivals window |
familiar | Familiar messages | Familiar window |
group | Group information | Group window |
room | Room data | Room window |
inv | Inventory data | Inventory window |
Configure stream mapping in layout:
[[windows]]
name = "my_window"
type = "text"
stream = "speech" # Display speech stream
Creating Custom Layouts
Combine widgets to create your ideal interface:
[layout]
columns = 120
rows = 40
# Main game text (left side)
[[windows]]
name = "main"
type = "text"
row = 0
col = 0
width = 80
height = 35
# Channels on right
[[windows]]
name = "channels"
type = "tabbed_text"
tabs = ["speech", "thoughts", "combat"]
row = 0
col = 80
width = 40
height = 20
# Status area
[[windows]]
name = "health"
type = "progress"
stat = "health"
row = 20
col = 80
width = 40
height = 1
# Command input at bottom
[[windows]]
name = "input"
type = "command_input"
row = 38
col = 0
width = 120
height = 2
Widget Interaction
Focus
- Click a widget to focus it
- Use
Ctrl+Tabto cycle focus - Focused widget has highlighted border
Scrolling
Page Up/Page Down- Scroll focused text widget- Mouse wheel - Scroll widget under cursor
Home/End- Jump to top/bottom
Selection
- Click and drag to select text
Ctrl+Cto copy selection- Double-click to select word
Links
If links = true in config:
- Click game objects to interact
- Right-click for context menu
Performance Considerations
Buffer Sizes
Text widgets buffer a limited number of lines:
[[windows]]
name = "main"
type = "text"
buffer_size = 2000 # Max lines (default)
Larger buffers use more memory. Reduce for secondary windows.
Widget Count
Each widget has rendering overhead. For performance:
- Use tabbed windows instead of multiple text windows
- Disable unused widgets
- Reduce buffer sizes on secondary windows
See Also
- layout.toml Reference - Layout syntax
- Creating Layouts - Design guide
- Performance - Optimization tips
Text Windows
Text windows display scrollable game text. They’re the primary widgets for viewing game output.
Overview
Text windows:
- Display one game stream (main, speech, thoughts, etc.)
- Support scrolling through history
- Apply highlights and colors
- Handle clickable links
- Buffer a configurable number of lines
Configuration
[[windows]]
name = "main"
type = "text"
# Position and size
row = 0
col = 0
width = 80
height = 30
# Text-specific options
stream = "main" # Game stream to display
buffer_size = 2000 # Maximum lines to keep
word_wrap = true # Enable word wrapping
# Visual options
show_border = true
border_style = "rounded"
show_title = true
title = "Game" # Custom title
background_color = "#000000"
text_color = "#CCCCCC"
Properties
stream
The game stream to display:
| Stream | Content |
|---|---|
main | Primary game output |
speech | Player dialogue |
thoughts | ESP/telepathy |
combat | Combat messages |
death | Death messages |
logons | Arrivals/departures |
familiar | Familiar messages |
group | Group info |
If not specified, defaults to the widget name.
buffer_size
Maximum lines to keep in memory:
buffer_size = 2000 # Default
buffer_size = 500 # Smaller for secondary windows
buffer_size = 5000 # Large for main window
Older lines are removed when the buffer fills. Larger buffers use more memory.
word_wrap
Whether to wrap long lines:
word_wrap = true # Wrap at window width (default)
word_wrap = false # Allow horizontal scrolling
Interaction
Scrolling
| Input | Action |
|---|---|
Page Up | Scroll up one page |
Page Down | Scroll down one page |
Home | Jump to oldest line |
End | Jump to newest line |
| Mouse wheel | Scroll up/down |
Ctrl+Up | Scroll up one line |
Ctrl+Down | Scroll down one line |
Selection
| Input | Action |
|---|---|
| Click + drag | Select text |
| Double-click | Select word |
| Triple-click | Select line |
Ctrl+C | Copy selection |
Ctrl+A | Select all |
Links
When links = true in config.toml:
| Input | Action |
|---|---|
| Click link | Primary action (look/get) |
| Right-click | Context menu |
| Ctrl+click | Alternative action |
Text Styling
Game Colors
The server sends color information through the XML protocol:
<color fg='...' bg='...'>- Explicit colors<preset id='...'>- Named presets (speech, monsterbold)<pushBold/><popBold/>- Bold/monsterbold
Highlights
Your highlights.toml patterns are applied after game colors:
[[highlights]]
name = "creatures"
pattern = "goblin|orc"
fg = "#FF6600"
bold = true
Priority
Color priority (highest to lowest):
- Explicit
<color>tags - Monsterbold (
<pushBold/>) - User highlights
- Preset colors
- Default text color
Auto-Scroll Behavior
Text windows auto-scroll to show new content when:
- Window is scrolled to the bottom
- New text arrives
Auto-scroll pauses when:
- User scrolls up
- Window is not at bottom
To resume auto-scroll:
- Press
Endto jump to bottom - Scroll to the bottom manually
Configure in config.toml:
[behavior]
auto_scroll = true # Default
Memory Management
Text windows use generation-based change detection for efficient updates:
- Each
add_line()increments a generation counter - Sync only copies lines newer than last sync
- Buffer trimming removes oldest lines when full
This enables smooth performance even with high text throughput.
Examples
Main Game Window
[[windows]]
name = "main"
type = "text"
stream = "main"
row = 0
col = 0
width = "70%"
height = "90%"
buffer_size = 3000
show_title = false
border_style = "rounded"
Speech Window
[[windows]]
name = "speech"
type = "text"
stream = "speech"
row = 0
col = 85
width = 35
height = 15
buffer_size = 500
title = "Speech"
Thoughts Window
[[windows]]
name = "thoughts"
type = "text"
stream = "thoughts"
row = 15
col = 85
width = 35
height = 15
buffer_size = 500
title = "Thoughts"
Minimal (No Border)
[[windows]]
name = "main"
type = "text"
row = 0
col = 0
width = "100%"
height = "100%"
show_border = false
show_title = false
transparent_background = true
Troubleshooting
Text not appearing
- Check
streammatches intended source - Verify window is within layout bounds
- Check
enabled = true(default)
Colors wrong
- Verify
COLORTERM=truecoloris set - Check highlights aren’t overriding game colors
- Review preset colors in
colors.toml
Performance issues
- Reduce
buffer_sizeon secondary windows - Reduce number of highlight patterns
- Use
fast_parse = truefor literal patterns
See Also
- Tabbed Text Windows - Multiple streams in tabs
- Highlight Patterns - Text styling
- Stream Reference - All streams
Tabbed Text Windows
Tabbed text windows combine multiple game streams into a single widget with selectable tabs.
Overview
Tabbed text windows:
- Display multiple streams in one widget
- Switch between tabs to view different content
- Share a single buffer per tab
- Save screen space vs. multiple windows
Configuration
[[windows]]
name = "channels"
type = "tabbed_text"
# Position and size
row = 0
col = 80
width = 40
height = 20
# Tab configuration
tabs = ["speech", "thoughts", "combat"]
default_tab = "speech"
show_tab_bar = true
# Visual options
tab_bar_position = "top" # "top" or "bottom"
buffer_size = 1000 # Per-tab buffer
Properties
tabs (required)
List of streams to display as tabs:
tabs = ["speech", "thoughts", "combat"]
Each tab shows content from the named stream.
default_tab
Initially selected tab:
default_tab = "speech" # Open to speech tab
If not specified, opens to first tab.
show_tab_bar
Whether to display the tab bar:
show_tab_bar = true # Show tabs (default)
show_tab_bar = false # Hide tab bar
tab_bar_position
Position of the tab bar:
tab_bar_position = "top" # Tabs at top (default)
tab_bar_position = "bottom" # Tabs at bottom
buffer_size
Lines to buffer per tab:
buffer_size = 1000 # Default per tab
Tab Bar Display
┌─ Channels ──────────────────────────┐
│ [Speech] [Thoughts] [Combat] │
├─────────────────────────────────────┤
│ Alice says, "Hello everyone!" │
│ Bob asks, "How are you?" │
│ Charlie exclaims, "Great day!" │
│ │
└─────────────────────────────────────┘
Active tab is highlighted, inactive tabs are dimmed.
Interaction
Tab Navigation
| Input | Action |
|---|---|
Tab | Next tab |
Shift+Tab | Previous tab |
| Click tab | Select tab |
1-9 | Select tab by number |
Content Navigation
| Input | Action |
|---|---|
Page Up/Down | Scroll current tab |
Home/End | Top/bottom of current tab |
| Mouse wheel | Scroll current tab |
Text Operations
| Input | Action |
|---|---|
| Click+drag | Select text |
Ctrl+C | Copy selection |
Examples
Communication Channels
[[windows]]
name = "channels"
type = "tabbed_text"
tabs = ["speech", "thoughts", "whisper"]
default_tab = "speech"
row = 0
col = 80
width = 40
height = 20
title = "Channels"
Combat and Notifications
[[windows]]
name = "alerts"
type = "tabbed_text"
tabs = ["combat", "death", "logons"]
default_tab = "combat"
row = 20
col = 80
width = 40
height = 15
title = "Alerts"
All Streams
[[windows]]
name = "all_streams"
type = "tabbed_text"
tabs = ["speech", "thoughts", "combat", "death", "logons", "familiar", "group"]
default_tab = "speech"
row = 0
col = 80
width = 40
height = 35
buffer_size = 500 # Smaller buffer for many tabs
Hidden Tab Bar
[[windows]]
name = "hidden_tabs"
type = "tabbed_text"
tabs = ["speech", "thoughts"]
show_tab_bar = false
row = 0
col = 80
width = 40
height = 20
Use keybinds to switch tabs when bar is hidden.
Tab Indicators
Unread Content
Tabs with new unread content show an indicator:
[Speech*] [Thoughts] [Combat]
The * indicates unread content in that tab.
Activity Highlighting
Active tabs can use different colors:
active_tab_color = "#FFFFFF"
inactive_tab_color = "#808080"
unread_tab_color = "#FFFF00"
Memory Considerations
Each tab maintains its own buffer:
Total memory ≈ buffer_size × number_of_tabs × line_size
For many tabs, reduce buffer_size:
# 7 tabs × 500 lines = 3500 lines total
tabs = ["speech", "thoughts", "combat", "death", "logons", "familiar", "group"]
buffer_size = 500
Stream Reference
Common streams for tabs:
| Stream | Content |
|---|---|
speech | Player dialogue |
thoughts | ESP/telepathy |
combat | Combat messages |
death | Death notifications |
logons | Arrivals/departures |
familiar | Familiar messages |
group | Group information |
whisper | Private whispers |
Troubleshooting
Tab not receiving content
- Verify stream name is correct
- Check game is sending that stream
- Some streams require game features
Tabs not switching
- Ensure widget has focus
- Check keybinds for conflicts
- Try clicking tabs directly
Memory issues with many tabs
- Reduce
buffer_size - Use fewer tabs
- Remove unused streams
See Also
- Text Windows - Single stream windows
- Stream Reference - All streams
- Keybinds - Tab navigation keys
Command Input
The command input widget provides the text entry field for sending commands to the game.
Overview
The command input:
- Accepts keyboard input for game commands
- Maintains command history
- Supports editing operations (cut, copy, paste)
- Can display a custom prompt
Configuration
[[windows]]
name = "input"
type = "command_input"
# Position and size
row = 38
col = 0
width = 120
height = 2
# Input-specific options
prompt = "> " # Input prompt
history = true # Enable command history
history_size = 1000 # Commands to remember
# Visual options
show_border = true
border_style = "rounded"
background_color = "#0A0A0A"
text_color = "#FFFFFF"
cursor_color = "#FFFFFF"
Properties
prompt
The prompt displayed before input:
prompt = "> " # Default
prompt = ">> " # Double arrow
prompt = "[cmd] " # Custom label
prompt = "" # No prompt
history
Enable command history:
history = true # Enable (default)
history = false # Disable
history_size
Number of commands to remember:
history_size = 1000 # Default
history_size = 100 # Smaller history
history_size = 5000 # Large history
Input Operations
Basic Input
| Key | Action |
|---|---|
Enter | Send command |
Escape | Clear input |
| Any text | Insert at cursor |
Cursor Movement
| Key | Action |
|---|---|
Left | Move cursor left |
Right | Move cursor right |
Home | Move to start |
End | Move to end |
Ctrl+Left | Previous word |
Ctrl+Right | Next word |
Editing
| Key | Action |
|---|---|
Backspace | Delete before cursor |
Delete | Delete at cursor |
Ctrl+Backspace | Delete word before |
Ctrl+Delete | Delete word after |
Ctrl+U | Clear line |
Ctrl+K | Delete to end |
Clipboard
| Key | Action |
|---|---|
Ctrl+C | Copy selection |
Ctrl+X | Cut selection |
Ctrl+V | Paste |
Ctrl+A | Select all |
History
| Key | Action |
|---|---|
Up | Previous command |
Down | Next command |
Ctrl+R | Search history (if supported) |
Display
Single Line
> look
Standard single-line input.
Multi-Line
[[windows]]
name = "input"
type = "command_input"
height = 3 # Multiple lines visible
Shows more context:
┌─ Input ────────────────────────────┐
│ > look │
│ │
└────────────────────────────────────┘
No Border
[[windows]]
name = "input"
type = "command_input"
show_border = false
show_title = false
height = 1
Minimal footprint:
> look█
Examples
Standard Input
[[windows]]
name = "input"
type = "command_input"
row = 38
col = 0
width = 120
height = 2
prompt = "> "
history = true
show_border = true
border_style = "rounded"
title = "Command"
Full-Width Minimal
[[windows]]
name = "input"
type = "command_input"
row = 39
col = 0
width = "100%"
height = 1
show_border = false
background_color = "#0A0A0A"
Centered Input
[[windows]]
name = "input"
type = "command_input"
row = 38
col = 20
width = 80
height = 2
prompt = ">> "
border_style = "double"
Command Processing
Command Flow
- User types command
- User presses Enter
- Command added to history
- Command sent to game server
- Input cleared for next command
Special Commands
Two-Face intercepts some commands:
| Command | Action |
|---|---|
;command | Client command (not sent to game) |
/quit | Exit Two-Face |
/reload | Reload configuration |
History Features
Navigation
Upcycles through older commandsDowncycles through newer commands- Current input is preserved when browsing
Persistence
Command history is saved between sessions in:
~/.two-face/history
Duplicate Handling
Consecutive duplicate commands are not added to history.
Focus Behavior
The command input:
- Automatically receives focus on startup
- Maintains focus during normal gameplay
- Returns focus when popups close
- Can be focused with
Ctrl+Ior clicking
Troubleshooting
Commands not sending
- Check input has focus (cursor visible)
- Verify connection is active
- Check for keybind conflicts with Enter
History not working
- Verify
history = true - Check
history_size> 0 - Check file permissions on history file
Cursor not visible
- Check
cursor_coloris visible - Verify widget has focus
- Check width is sufficient
See Also
- Keybinds Configuration - Input shortcuts
- Quick Tour - Basic usage
- Automation - Command automation
Progress Bars
Progress bars display character statistics as visual bars with optional numeric values.
Overview
Progress bars:
- Show health, mana, stamina, spirit, encumbrance
- Update automatically from game data
- Support custom colors and styling
- Can show value, percentage, or both
Configuration
[[windows]]
name = "health"
type = "progress"
# Position and size
row = 0
col = 80
width = 30
height = 1
# Progress-specific options
stat = "health" # Stat to track (required)
show_value = true # Show "425/500"
show_percentage = false # Show "85%"
show_label = true # Show stat name
# Colors
bar_color = "#00FF00" # Bar fill color
bar_empty_color = "#333333" # Empty portion
text_color = "#FFFFFF" # Text color
Properties
stat (required)
The character statistic to display:
| Stat | Description |
|---|---|
health | Health points |
mana | Mana points |
spirit | Spirit points |
stamina | Stamina points |
encumbrance | Encumbrance level |
show_value
Display the numeric value:
show_value = true # Shows "425/500"
show_value = false # Bar only
show_percentage
Display percentage instead of/with value:
show_percentage = true # Shows "85%"
show_percentage = false # Shows value or nothing
show_label
Display the stat name:
show_label = true # Shows "HP: 425/500"
show_label = false # Shows "425/500"
bar_color
Color of the filled portion:
bar_color = "#00FF00" # Green
bar_color = "health" # From palette
bar_color = "@health" # From preset
Dynamic Colors
Use different colors based on value:
[[windows]]
name = "health"
type = "progress"
stat = "health"
# Color thresholds (checked in order)
[windows.color_thresholds]
75 = "#00FF00" # Green above 75%
50 = "#FFFF00" # Yellow 50-75%
25 = "#FF8800" # Orange 25-50%
0 = "#FF0000" # Red below 25%
Display Formats
Compact (1 row)
[[windows]]
name = "health"
type = "progress"
stat = "health"
width = 20
height = 1
show_label = true
show_value = true
Result: HP ████████░░ 80%
Minimal (Bar only)
[[windows]]
name = "health"
type = "progress"
stat = "health"
width = 15
height = 1
show_label = false
show_value = false
Result: ████████████░░░
Vertical Bar
[[windows]]
name = "health"
type = "progress"
stat = "health"
width = 3
height = 10
orientation = "vertical"
Examples
Health Bar (Green)
[[windows]]
name = "health"
type = "progress"
stat = "health"
row = 0
col = 80
width = 25
height = 1
bar_color = "#00FF00"
show_value = true
Mana Bar (Blue)
[[windows]]
name = "mana"
type = "progress"
stat = "mana"
row = 1
col = 80
width = 25
height = 1
bar_color = "#0088FF"
show_value = true
Spirit Bar (Cyan)
[[windows]]
name = "spirit"
type = "progress"
stat = "spirit"
row = 2
col = 80
width = 25
height = 1
bar_color = "#00FFFF"
show_value = true
Stamina Bar (Yellow)
[[windows]]
name = "stamina"
type = "progress"
stat = "stamina"
row = 3
col = 80
width = 25
height = 1
bar_color = "#FFFF00"
show_value = true
Encumbrance Bar
[[windows]]
name = "encumbrance"
type = "progress"
stat = "encumbrance"
row = 4
col = 80
width = 25
height = 1
bar_color = "#888888"
show_percentage = true
Stacked Vitals
# All vitals in one area
[[windows]]
name = "health"
type = "progress"
stat = "health"
row = 0
col = 100
width = 20
height = 1
[[windows]]
name = "mana"
type = "progress"
stat = "mana"
row = 1
col = 100
width = 20
height = 1
[[windows]]
name = "spirit"
type = "progress"
stat = "spirit"
row = 2
col = 100
width = 20
height = 1
[[windows]]
name = "stamina"
type = "progress"
stat = "stamina"
row = 3
col = 100
width = 20
height = 1
Data Source
Progress bars receive data from <progressBar> XML elements:
<progressBar id='health' value='85' text='health 425/500' />
The parser extracts:
id: Stat typevalue: Percentage (0-100)text: Parse current/max values
Troubleshooting
Bar not updating
- Verify
statmatches a valid stat ID - Check game is sending progress bar data
- Ensure widget is enabled
Wrong colors
- Check
bar_colorsyntax - Verify palette/preset references exist
- Check color threshold order
Value display issues
- Ensure
show_valueorshow_percentageis true - Check width is sufficient for text
- Verify text_color is visible against background
See Also
- Dashboard - Composite status widget
- Countdowns - RT/cast time display
- Color Configuration - Color settings
Countdowns
Countdown widgets display roundtime and cast time remaining.
Overview
Countdown widgets:
- Show remaining seconds for roundtime or cast time
- Update every second automatically
- Support visual bar and numeric display
- Customize icons and colors
Configuration
[[windows]]
name = "roundtime"
type = "countdown"
# Position and size
row = 5
col = 80
width = 15
height = 1
# Countdown-specific options
countdown_type = "roundtime" # "roundtime" or "casttime"
show_bar = true # Show progress bar
show_seconds = true # Show numeric seconds
icon = "⏱" # Icon prefix
# Colors
bar_color = "#FF0000" # Progress bar color
text_color = "#FFFFFF" # Seconds text color
background_color = "#000000"
Properties
countdown_type (required)
Which countdown to display:
| Type | Description |
|---|---|
roundtime | Combat/action roundtime |
casttime | Spell casting time |
show_bar
Display a progress bar:
show_bar = true # Show visual bar (default)
show_bar = false # Numbers only
show_seconds
Display numeric seconds remaining:
show_seconds = true # Show "5s" (default)
show_seconds = false # Bar only
icon
Icon displayed before countdown:
icon = "⏱" # Timer emoji
icon = "RT:" # Text prefix
icon = "" # No icon
Display Modes
Bar with Seconds
⏱ ██████░░░░ 6s
Shows visual progress and numeric value.
Bar Only
██████████░░░░░░░░░░
Clean visual indicator.
Seconds Only
RT: 6
Minimal numeric display.
Compact
width = 8
height = 1
show_bar = false
icon = ""
Result: 6s
Visual Behavior
Active Countdown
When time is remaining:
- Bar fills proportionally
- Seconds count down
- Color indicates urgency
Zero/Expired
When countdown reaches zero:
- Bar empty or hidden
- Shows “0” or disappears
- Can trigger alert
Color Progression
Optional color changes based on time:
# High time (>5s)
high_color = "#00FF00"
# Medium time (2-5s)
medium_color = "#FFFF00"
# Low time (<2s)
low_color = "#FF0000"
Examples
Standard Roundtime
[[windows]]
name = "roundtime"
type = "countdown"
countdown_type = "roundtime"
row = 5
col = 80
width = 20
height = 1
icon = "RT:"
show_bar = true
show_seconds = true
bar_color = "#FF4444"
Cast Time
[[windows]]
name = "casttime"
type = "countdown"
countdown_type = "casttime"
row = 6
col = 80
width = 20
height = 1
icon = "CT:"
show_bar = true
show_seconds = true
bar_color = "#4444FF"
Minimal RT
[[windows]]
name = "rt"
type = "countdown"
countdown_type = "roundtime"
row = 0
col = 0
width = 5
height = 1
icon = ""
show_bar = false
show_border = false
text_color = "#FF0000"
Dual Countdowns
# Side by side
[[windows]]
name = "roundtime"
type = "countdown"
countdown_type = "roundtime"
row = 10
col = 80
width = 15
height = 1
icon = "RT"
bar_color = "#FF0000"
[[windows]]
name = "casttime"
type = "countdown"
countdown_type = "casttime"
row = 10
col = 96
width = 15
height = 1
icon = "CT"
bar_color = "#0088FF"
Progress Bar Style
[[windows]]
name = "roundtime"
type = "countdown"
countdown_type = "roundtime"
row = 15
col = 80
width = 30
height = 1
show_seconds = false
show_border = false
bar_color = "#FF6600"
transparent_background = true
Data Source
Countdown widgets receive data from XML elements:
<roundTime value='1234567890'/>
<castTime value='1234567895'/>
The value is a Unix timestamp when the countdown ends. Two-Face calculates remaining seconds.
Accuracy
Countdowns update based on:
- Game server timestamps
- Local system clock
- ~1 second precision
Minor drift from game may occur due to network latency.
Alerts
Configure alerts when countdown ends:
# In config.toml
[alerts]
roundtime_end_sound = "sounds/ding.wav"
roundtime_end_flash = true
Troubleshooting
Countdown not appearing
- Check
countdown_typeis correct - Verify game is sending countdown data
- Actions must trigger roundtime
Time seems wrong
- Check system clock accuracy
- Network latency can cause slight drift
- Verify countdown is for expected action
Bar not showing
- Check
show_bar = true - Verify
widthis sufficient - Check
bar_coloris visible
See Also
- Progress Bars - Static progress display
- Indicators - Status indicators
- Sound Alerts - Alert configuration
Compass
The compass widget displays available room exits as directional indicators.
Overview
The compass:
- Shows all available exits from current room
- Updates automatically when you move
- Supports graphical and text display modes
- Highlights cardinal and special directions
Configuration
[[windows]]
name = "compass"
type = "compass"
# Position and size
row = 0
col = 100
width = 11
height = 5
# Compass-specific options
style = "graphical" # "graphical" or "text"
show_labels = true # Show N/S/E/W labels
compact = false # Compact display mode
# Colors
active_color = "#FFFFFF" # Available exit color
inactive_color = "#333333" # Unavailable direction
special_color = "#00FFFF" # Special exits (out, up, down)
Properties
style
Display style:
| Style | Description |
|---|---|
graphical | Visual compass rose |
text | Text list of exits |
show_labels
Show direction labels:
show_labels = true # Shows N, S, E, W, etc.
show_labels = false # Icons/indicators only
compact
Use compact layout:
compact = false # Full 5x11 compass (default)
compact = true # Smaller 3x7 compass
Direction Mapping
The compass recognizes these directions:
| Direction | Abbreviation | Position |
|---|---|---|
| north | n | Top center |
| south | s | Bottom center |
| east | e | Right center |
| west | w | Left center |
| northeast | ne | Top right |
| northwest | nw | Top left |
| southeast | se | Bottom right |
| southwest | sw | Bottom left |
| up | u | Top (special) |
| down | d | Bottom (special) |
| out | out | Center or special |
Display Modes
Graphical Mode (Default)
[N]
[NW] [NE]
[W] + [E]
[SW] [SE]
[S]
Available exits are highlighted, unavailable are dimmed.
Text Mode
Exits: n, e, sw, out
Simple text list of available directions.
Compact Mode
N
W+E
S
Smaller footprint, cardinal directions only.
Examples
Standard Compass
[[windows]]
name = "compass"
type = "compass"
row = 5
col = 100
width = 11
height = 5
style = "graphical"
show_labels = true
border_style = "rounded"
title = "Exits"
Compact Compass
[[windows]]
name = "compass"
type = "compass"
row = 0
col = 110
width = 7
height = 3
style = "graphical"
compact = true
show_border = false
Text List
[[windows]]
name = "exits"
type = "compass"
row = 10
col = 80
width = 25
height = 1
style = "text"
show_border = false
Colored Compass
[[windows]]
name = "compass"
type = "compass"
row = 5
col = 100
width = 11
height = 5
active_color = "#00FF00" # Green for available
inactive_color = "#1A1A1A" # Very dark for unavailable
special_color = "#FFFF00" # Yellow for up/down/out
Interaction
The compass is display-only by default. With links = true:
| Input | Action |
|---|---|
| Click direction | Move that direction |
| Hover | Highlight direction |
Data Source
The compass receives data from <compass> XML elements:
<compass>
<dir value="n"/>
<dir value="e"/>
<dir value="out"/>
</compass>
The <nav> element provides additional room information:
<nav rm='123456'/>
Troubleshooting
Compass not updating
- Verify you’re receiving compass data (check main window)
- Some areas don’t send compass info
- Check widget is properly configured
Exits showing incorrectly
- The game controls compass data
- Some rooms have unusual exit names
- Special exits may not appear in compass
Display issues
- Check
widthandheightare sufficient - Verify style matches your preference
- Check colors are visible against background
See Also
- Room Window - Full room information
- layout.toml Reference - Layout syntax
- Navigation Tutorial - Layout examples
Hands
Hand widgets display items held in your character’s hands and prepared spells.
Overview
Hand widgets:
- Show left hand, right hand, or spell contents
- Update automatically when items change
- Support clickable links to items
- Display custom icons
Configuration
[[windows]]
name = "right_hand"
type = "hand"
# Position and size
row = 10
col = 100
width = 20
height = 1
# Hand-specific options
hand_type = "right" # "left", "right", or "spell"
# Display options
icon = "🤚" # Icon before text
icon_color = "#FFFFFF" # Icon color
text_color = "#CCCCCC" # Item text color
# Visual options
show_border = true
border_style = "rounded"
Properties
hand_type (required)
Which hand/slot to display:
| Type | Content |
|---|---|
left | Left hand item |
right | Right hand item |
spell | Prepared spell |
icon
Icon displayed before content:
# Hand icons
icon = "🤚" # Generic hand
icon = "👋" # Wave (left)
icon = "✋" # Palm (right)
icon = "✨" # Sparkle (spell)
# Text icons
icon = "[L]" # Left bracket
icon = "[R]" # Right bracket
icon = "[S]" # Spell bracket
# No icon
icon = ""
icon_color
Color of the icon:
icon_color = "#FFFFFF" # White
icon_color = "bright_cyan" # Palette color
icon_color = "@links" # Preset color
text_color
Color of the item text:
text_color = "#CCCCCC" # Default gray
text_color = "@links" # Link color from presets
Display Modes
With Icon
🤚 a gleaming vultite sword
Text Only
[R] a gleaming vultite sword
Compact
show_border = false
icon = ""
width = 25
height = 1
Result: a gleaming vultite sword
Interaction
With links = true in config.toml:
| Input | Action |
|---|---|
| Click item | Primary action |
| Right-click | Context menu |
Actions depend on the item type:
- Weapons: Attack commands
- Containers: Open/search
- General: Look/inspect
Examples
Right Hand
[[windows]]
name = "right_hand"
type = "hand"
hand_type = "right"
row = 5
col = 80
width = 25
height = 1
icon = "✋"
icon_color = "#AAAAAA"
title = "Right"
Left Hand
[[windows]]
name = "left_hand"
type = "hand"
hand_type = "left"
row = 6
col = 80
width = 25
height = 1
icon = "🤚"
icon_color = "#AAAAAA"
title = "Left"
Spell Hand
[[windows]]
name = "spell"
type = "hand"
hand_type = "spell"
row = 7
col = 80
width = 25
height = 1
icon = "✨"
icon_color = "#FFFF00"
title = "Spell"
text_color = "#FF88FF"
Stacked Hands
# All three hands vertically
[[windows]]
name = "right_hand"
type = "hand"
hand_type = "right"
row = 10
col = 100
width = 20
height = 1
icon = "R:"
show_border = false
[[windows]]
name = "left_hand"
type = "hand"
hand_type = "left"
row = 11
col = 100
width = 20
height = 1
icon = "L:"
show_border = false
[[windows]]
name = "spell"
type = "hand"
hand_type = "spell"
row = 12
col = 100
width = 20
height = 1
icon = "S:"
show_border = false
Bordered Group
[[windows]]
name = "hands"
type = "container"
row = 10
col = 100
width = 22
height = 5
title = "Hands"
border_style = "rounded"
[[windows]]
name = "right_hand"
type = "hand"
hand_type = "right"
row = 11
col = 101
width = 20
height = 1
show_border = false
[[windows]]
name = "left_hand"
type = "hand"
hand_type = "left"
row = 12
col = 101
width = 20
height = 1
show_border = false
[[windows]]
name = "spell"
type = "hand"
hand_type = "spell"
row = 13
col = 101
width = 20
height = 1
show_border = false
Data Source
Hand widgets receive data from XML elements:
<right exist="123456" noun="sword">a gleaming vultite sword</right>
<left exist="789012" noun="shield">a battered wooden shield</left>
<spell>Minor Sanctuary</spell>
The exist and noun attributes enable link functionality.
Empty Hands
When a hand is empty:
- Display shows “Empty” or nothing
- Configure empty display:
empty_text = "Empty" # Show "Empty"
empty_text = "-" # Show dash
empty_text = "" # Show nothing
Troubleshooting
Hand not updating
- Check
hand_typeis correct - Verify game is sending hand data
- Check widget is positioned correctly
Links not working
- Enable
links = truein config.toml - Verify item has exist ID
- Check for click handler conflicts
Text truncated
- Increase widget
width - Remove icon to save space
- Use smaller font if available
See Also
- Indicators - Status indicators
- Dashboard - Composite status
- Link Configuration - Link settings
Status Indicators
Status indicator widgets display active character conditions like stunned, hidden, poisoned, etc.
Overview
Indicator widgets:
- Show active status conditions
- Update automatically when conditions change
- Support icon or text display modes
- Can show multiple conditions
Configuration
[[windows]]
name = "status"
type = "indicator"
# Position and size
row = 8
col = 100
width = 20
height = 3
# Indicator-specific options
indicators = ["stunned", "hidden", "webbed", "poisoned"]
style = "icons" # "icons" or "text"
layout = "horizontal" # "horizontal" or "vertical"
show_inactive = false # Show inactive indicators dimmed
# Colors
active_color = "#FF0000"
inactive_color = "#333333"
Properties
indicators
Which conditions to display:
# Show specific indicators
indicators = ["stunned", "hidden", "poisoned"]
# Show all indicators
indicators = "all"
Available indicators:
| ID | Condition |
|---|---|
stunned | Character stunned |
hidden | Character hidden |
webbed | Caught in web |
poisoned | Poisoned |
diseased | Diseased |
bleeding | Bleeding wound |
prone | Lying down |
kneeling | Kneeling |
sitting | Sitting |
dead | Dead |
style
Display style:
style = "icons" # Show icons (default)
style = "text" # Show text labels
style = "both" # Icon and text
layout
Indicator arrangement:
layout = "horizontal" # Side by side (default)
layout = "vertical" # Stacked
layout = "grid" # Grid layout
show_inactive
Whether to show inactive indicators:
show_inactive = false # Hide inactive (default)
show_inactive = true # Show dimmed
Display Modes
Icons Only
⚡ 🕸️ ☠️
Compact, visual indicators.
Text Only
STUNNED WEBBED POISONED
Clear text labels.
Icons with Text
⚡ Stunned 🕸️ Webbed
Combined display.
Vertical
⚡ Stunned
🕸️ Webbed
☠️ Poisoned
Stacked layout.
Indicator Icons
Default icons for each condition:
| Condition | Icon | Description |
|---|---|---|
| stunned | ⚡ | Lightning bolt |
| hidden | 👁️ | Eye |
| webbed | 🕸️ | Spider web |
| poisoned | ☠️ | Skull |
| diseased | 🦠 | Microbe |
| bleeding | 🩸 | Blood drop |
| prone | ⬇️ | Down arrow |
| kneeling | 🧎 | Kneeling |
| sitting | 🪑 | Chair |
| dead | 💀 | Skull |
Custom Icons
Override default icons:
[indicator_icons]
stunned = "STUN"
hidden = "HIDE"
poisoned = "POIS"
Examples
Combat Status Bar
[[windows]]
name = "combat_status"
type = "indicator"
indicators = ["stunned", "prone", "webbed"]
row = 0
col = 80
width = 30
height = 1
style = "icons"
layout = "horizontal"
active_color = "#FF4444"
Full Status Panel
[[windows]]
name = "status"
type = "indicator"
indicators = "all"
row = 10
col = 100
width = 15
height = 10
style = "text"
layout = "vertical"
show_inactive = true
active_color = "#FF0000"
inactive_color = "#333333"
Compact Icons
[[windows]]
name = "status_icons"
type = "indicator"
indicators = ["stunned", "hidden", "poisoned", "diseased"]
row = 5
col = 110
width = 10
height = 1
style = "icons"
show_border = false
Health Conditions
[[windows]]
name = "health_status"
type = "indicator"
indicators = ["poisoned", "diseased", "bleeding"]
row = 4
col = 80
width = 20
height = 1
style = "both"
active_color = "#FF8800"
title = "Conditions"
Condition Colors
Different colors for different severity:
[indicator_colors]
# Immediate threats (red)
stunned = "#FF0000"
prone = "#FF0000"
# Combat conditions (orange)
webbed = "#FF8800"
bleeding = "#FF8800"
# Health conditions (yellow)
poisoned = "#FFFF00"
diseased = "#FFFF00"
# Status (blue)
hidden = "#0088FF"
kneeling = "#0088FF"
sitting = "#0088FF"
Animation
Indicators can flash or pulse:
# Flash active indicators
flash_active = true
flash_rate = 500 # Milliseconds
# Pulse critical conditions
pulse_critical = true
critical_indicators = ["stunned", "dead"]
Data Source
Indicators receive data from XML elements:
<indicator id="IconSTUNNED" visible="y"/>
<indicator id="IconHIDDEN" visible="n"/>
<indicator id="IconPOISONED" visible="y"/>
The visible attribute determines active state.
Troubleshooting
Indicators not showing
- Verify condition is actually active
- Check
indicatorslist includes the condition - Ensure
show_inactive = falseisn’t hiding it
Wrong icons
- Check
stylesetting - Verify icon font support in terminal
- Use text fallbacks if needed
Layout issues
- Adjust
widthandheight - Change
layoutmode - Reduce number of indicators
See Also
- Injury Doll - Body injury display
- Active Effects - Buff/debuff display
- Progress Bars - Health status
Injury Doll
The injury doll widget displays a visual representation of injuries to different body parts.
Overview
Injury doll widgets:
- Show body part injury severity visually
- Display wounds, scars, and blood loss
- Update as injuries change
- Support multiple display styles
Configuration
[[windows]]
name = "injuries"
type = "injury_doll"
# Position and size
row = 0
col = 100
width = 20
height = 12
# Injury-specific options
style = "ascii" # "ascii", "simple", "detailed"
show_labels = true # Show body part names
show_severity = true # Show injury level numbers
orientation = "vertical" # "vertical" or "horizontal"
# Colors by severity
healthy_color = "#00FF00"
minor_color = "#FFFF00"
moderate_color = "#FF8800"
severe_color = "#FF0000"
critical_color = "#FF0000"
Properties
style
Display style for the injury doll:
style = "ascii" # ASCII art body (default)
style = "simple" # Text list only
style = "detailed" # Full visual with numbers
show_labels
Display body part names:
show_labels = true # Show "Head", "Chest", etc.
show_labels = false # Visual only
show_severity
Display injury severity numbers:
show_severity = true # Show severity level (1-4)
show_severity = false # Color-coded only
orientation
Layout orientation:
orientation = "vertical" # Tall layout (default)
orientation = "horizontal" # Wide layout
Body Parts
The injury doll tracks these body parts:
| Part | XML ID | Description |
|---|---|---|
| Head | head | Head injuries |
| Neck | neck | Neck injuries |
| Chest | chest | Chest/torso |
| Abdomen | abdomen | Stomach area |
| Back | back | Back injuries |
| Left Arm | leftArm | Left arm |
| Right Arm | rightArm | Right arm |
| Left Hand | leftHand | Left hand |
| Right Hand | rightHand | Right hand |
| Left Leg | leftLeg | Left leg |
| Right Leg | rightLeg | Right leg |
| Left Eye | leftEye | Left eye |
| Right Eye | rightEye | Right eye |
| Nerves | nsys | Nervous system |
Severity Levels
| Level | Name | Color (default) | Description |
|---|---|---|---|
| 0 | Healthy | Green | No injury |
| 1 | Minor | Yellow | Minor wound |
| 2 | Moderate | Orange | Moderate injury |
| 3 | Severe | Red | Severe wound |
| 4 | Critical | Bright Red | Critical injury |
Display Styles
ASCII Art Style
┌─ Injuries ───────────┐
│ ( o o ) │
│ ─┬─ │
│ ──┼── │
│ │ │
│ / \ │
│ / \ │
└──────────────────────┘
Body parts are colored by severity.
Simple Text Style
┌─ Injuries ───────────┐
│ Head: Minor │
│ Chest: Moderate │
│ Right Arm: Severe │
│ Left Leg: Minor │
└──────────────────────┘
Detailed Style
┌─ Injuries ───────────┐
│ HEAD [████░░] 2 │
│ CHEST [██████] 3 │
│ R.ARM [████████] 4│
│ L.LEG [██░░░░] 1 │
│ BACK [░░░░░░] 0 │
└──────────────────────┘
Examples
Standard Injury Doll
[[windows]]
name = "injuries"
type = "injury_doll"
row = 0
col = 100
width = 20
height = 12
style = "ascii"
show_labels = true
show_severity = true
border_style = "rounded"
Compact Text List
[[windows]]
name = "injury_list"
type = "injury_doll"
row = 0
col = 100
width = 25
height = 8
style = "simple"
show_severity = true
title = "Wounds"
Minimal Status
[[windows]]
name = "injury_mini"
type = "injury_doll"
row = 0
col = 100
width = 15
height = 5
style = "simple"
show_labels = false
show_severity = false
show_border = false
Wide Layout
[[windows]]
name = "injuries_wide"
type = "injury_doll"
row = 20
col = 0
width = 60
height = 5
style = "detailed"
orientation = "horizontal"
Severity Colors
Customize colors for each severity level:
[[windows]]
name = "injuries"
type = "injury_doll"
# Custom severity colors
[windows.colors]
healthy = "#00FF00" # Green
minor = "#FFFF00" # Yellow
moderate = "#FF8800" # Orange
severe = "#FF4444" # Red
critical = "#FF0000" # Bright red
bleeding = "#880000" # Dark red for blood
scarred = "#808080" # Gray for scars
Wound Types
Different wound states may display differently:
| State | Visual | Description |
|---|---|---|
| Fresh wound | Bright color | Recent injury |
| Bleeding | Pulsing/dark | Active bleeding |
| Scarred | Gray | Old injury |
| Healing | Fading | Being healed |
Animation
Injuries can animate to draw attention:
# Flash critical injuries
flash_critical = true
flash_rate = 500 # Milliseconds
# Pulse bleeding wounds
pulse_bleeding = true
Data Source
Injury data comes from XML elements:
<indicator id="IconBLEEDING" visible="y"/>
<body>
<part id="head" value="0"/>
<part id="chest" value="2"/>
<part id="rightArm" value="3"/>
</body>
The value attribute indicates severity (0-4).
Integration with Other Widgets
Combined with Health Bars
# Health bar
[[windows]]
name = "health"
type = "progress"
source = "health"
row = 0
col = 80
width = 20
height = 1
# Injury doll below
[[windows]]
name = "injuries"
type = "injury_doll"
row = 1
col = 80
width = 20
height = 10
Part of Status Panel
# Status indicators
[[windows]]
name = "status"
type = "indicator"
row = 0
col = 100
width = 20
height = 2
# Injuries below status
[[windows]]
name = "injuries"
type = "injury_doll"
row = 2
col = 100
width = 20
height = 10
Troubleshooting
Injuries not updating
- Verify receiving body data from game
- Check window is using correct type
- Ensure body part IDs match XML
Wrong colors
- Check severity color configuration
- Verify theme colors aren’t overriding
- Test with default colors first
Display too small
- Increase width and height
- Try “simple” style for compact spaces
- Adjust orientation for available space
See Also
- Status Indicators - Status condition display
- Progress Bars - Health/vitals display
- Active Effects - Buff/debuff display
Active Effects
The active effects widget displays current buffs, debuffs, spells, and other timed effects on your character.
Overview
Active effects widgets:
- Show currently active spells and abilities
- Display remaining duration
- Track buff and debuff status
- Update in real-time as effects expire
Configuration
[[windows]]
name = "effects"
type = "active_effects"
# Position and size
row = 0
col = 100
width = 30
height = 15
# Effect display options
show_duration = true # Show remaining time
show_icons = true # Show effect icons
sort_by = "duration" # "duration", "name", "type"
group_by_type = false # Group buffs/debuffs
# Filter options
show_buffs = true
show_debuffs = true
show_spells = true
show_abilities = true
# Colors
buff_color = "#00FF00"
debuff_color = "#FF0000"
spell_color = "#00FFFF"
expiring_color = "#FFFF00"
Properties
show_duration
Display remaining time for effects:
show_duration = true # Show "Spirit Shield (2:45)"
show_duration = false # Show "Spirit Shield" only
show_icons
Display icons for effects:
show_icons = true # Show icons/symbols
show_icons = false # Text only
sort_by
How to sort the effect list:
sort_by = "duration" # Shortest remaining first
sort_by = "name" # Alphabetical
sort_by = "type" # Buffs, then debuffs, etc.
group_by_type
Group effects by category:
group_by_type = true # Separate sections
group_by_type = false # Single list (default)
Filter Options
Control which effect types to display:
show_buffs = true # Beneficial effects
show_debuffs = true # Harmful effects
show_spells = true # Active spells
show_abilities = true # Active abilities
Display Format
Standard List
┌─ Active Effects ─────────────┐
│ ✦ Spirit Shield (5:23) │
│ ✦ Elemental Defense (4:15) │
│ ✦ Haste (2:30) │
│ ✧ Poison (1:45) │
│ ✦ Minor Sanctuary (0:58) │
└──────────────────────────────┘
- ✦ = Buff (beneficial)
- ✧ = Debuff (harmful)
Grouped Display
┌─ Active Effects ─────────────┐
│ BUFFS: │
│ Spirit Shield (5:23) │
│ Elemental Defense (4:15) │
│ Haste (2:30) │
│ │
│ DEBUFFS: │
│ Poison (1:45) │
└──────────────────────────────┘
Compact Display
┌─ Effects ────────────────────┐
│ Shield:5:23 Defense:4:15 │
│ Haste:2:30 Poison:1:45 │
└──────────────────────────────┘
Effect Types
| Type | Description | Default Color |
|---|---|---|
| Buff | Beneficial effect | Green |
| Debuff | Harmful effect | Red |
| Spell | Active spell | Cyan |
| Ability | Character ability | Blue |
| Item | Item effect | Purple |
Duration Display
Time Formats
| Remaining | Display |
|---|---|
| > 1 hour | 1:23:45 |
| > 1 minute | 5:23 |
| < 1 minute | 0:45 |
| Expiring soon | Flashing |
| Permanent | ∞ or – |
Expiring Warning
Effects close to expiring can be highlighted:
expiring_threshold = 30 # Seconds before expiring
expiring_color = "#FFFF00" # Yellow warning
expiring_flash = true # Flash when expiring
Examples
Full Effects Panel
[[windows]]
name = "effects"
type = "active_effects"
row = 0
col = 100
width = 30
height = 20
show_duration = true
show_icons = true
sort_by = "duration"
group_by_type = true
title = "Active Effects"
Buffs Only
[[windows]]
name = "buffs"
type = "active_effects"
row = 0
col = 100
width = 25
height = 10
show_buffs = true
show_debuffs = false
show_spells = true
buff_color = "#00FF00"
title = "Buffs"
Debuff Monitor
[[windows]]
name = "debuffs"
type = "active_effects"
row = 10
col = 100
width = 25
height = 8
show_buffs = false
show_debuffs = true
debuff_color = "#FF4444"
expiring_color = "#FFFF00"
title = "Debuffs"
Compact Status Bar
[[windows]]
name = "effect_bar"
type = "active_effects"
row = 0
col = 50
width = 50
height = 2
show_icons = true
show_duration = false
sort_by = "type"
show_border = false
Spell Timer
[[windows]]
name = "spells"
type = "active_effects"
row = 15
col = 100
width = 25
height = 12
show_buffs = false
show_debuffs = false
show_spells = true
sort_by = "duration"
title = "Active Spells"
Effect Colors
Customize colors by effect type:
[[windows]]
name = "effects"
type = "active_effects"
[windows.colors]
buff = "#00FF00" # Green
debuff = "#FF0000" # Red
spell = "#00FFFF" # Cyan
ability = "#0088FF" # Blue
item = "#FF00FF" # Magenta
expiring = "#FFFF00" # Yellow
permanent = "#FFFFFF" # White
Severity-Based Colors
For debuffs, color by severity:
[windows.debuff_colors]
minor = "#FFFF00" # Yellow
moderate = "#FF8800" # Orange
severe = "#FF0000" # Red
critical = "#FF0000" # Bright red + flash
Animation
Expiring Effects
# Flash effects about to expire
flash_expiring = true
flash_threshold = 30 # Seconds
flash_rate = 500 # Milliseconds
# Fade out expired effects
fade_on_expire = true
fade_duration = 1000 # Milliseconds
New Effects
# Highlight newly added effects
highlight_new = true
highlight_duration = 2000 # Milliseconds
highlight_color = "#FFFFFF"
Data Source
Effects are tracked from multiple XML sources:
<spell>Spirit Shield</spell>
<duration value="323"/>
<effect name="Poison" type="debuff" duration="105"/>
<component id="activeSpells">
Spirit Shield, Elemental Defense
</component>
Integration Examples
Combined with Spell List
# Active effects
[[windows]]
name = "active"
type = "active_effects"
row = 0
col = 100
width = 25
height = 10
# Known spells below
[[windows]]
name = "spells"
type = "spells"
row = 10
col = 100
width = 25
height = 15
Part of Combat Dashboard
# Health/vitals
[[windows]]
name = "vitals"
type = "progress"
row = 0
col = 80
width = 40
height = 4
# Active effects
[[windows]]
name = "effects"
type = "active_effects"
row = 4
col = 80
width = 20
height = 8
# Debuff alerts
[[windows]]
name = "debuffs"
type = "active_effects"
row = 4
col = 100
width = 20
height = 8
show_buffs = false
show_debuffs = true
Troubleshooting
Effects not showing
- Verify game is sending effect data
- Check filter settings (show_buffs, etc.)
- Ensure correct widget type
Duration not updating
- Effects may not send duration updates
- Check timer refresh rate
- Some effects show as permanent
Missing effect types
- Enable all show_* options to test
- Check if game sends that effect type
- Verify effect parsing in logs
See Also
- Spells - Known spell list
- Status Indicators - Status conditions
- Progress Bars - Vitals display
- Countdowns - Timer display
Room Window
The room window displays current room information including name, description, exits, objects, and other players.
Overview
Room windows:
- Show the current room name and description
- Display obvious exits
- List objects and creatures in the room
- Show other players present
- Update when you move
Configuration
[[windows]]
name = "room"
type = "room"
# Position and size
row = 0
col = 80
width = 40
height = 15
# Room-specific options
show_title = true # Show room name as title
show_description = true # Show room description
show_exits = true # Show obvious exits
show_objects = true # Show items/creatures
show_players = true # Show other players
# Visual options
title_color = "#FFFFFF"
description_color = "#CCCCCC"
exits_color = "#00FFFF"
objects_color = "#AAAAAA"
players_color = "#00FF00"
Properties
show_title
Display room name as widget title:
show_title = true # Room name in title bar
show_title = false # No title/generic title
show_description
Display the room description:
show_description = true # Full description
show_description = false # Hide description
show_exits
Display obvious exits:
show_exits = true # Show "Obvious exits: n, e, out"
show_exits = false # Hide exits
show_objects
Display items and creatures:
show_objects = true # Show objects in room
show_objects = false # Hide objects
show_players
Display other players:
show_players = true # Show player names
show_players = false # Hide players
Display Layout
┌─ Town Square Central ───────────────┐
│ This is the heart of the town, │
│ where merchants hawk their wares │
│ and travelers gather to share news. │
│ │
│ You also see a wooden bench, a │
│ town crier, and a young squire. │
│ │
│ Also here: Adventurer, Merchant │
│ │
│ Obvious paths: north, east, south, │
│ west, out │
└─────────────────────────────────────┘
Component Colors
Style each component separately:
[[windows]]
name = "room"
type = "room"
# Component colors
[windows.colors]
title = "#9BA2B2" # Room name
title_bg = "#395573" # Room name background
description = "#B0B0B0" # Description text
exits = "#00CCCC" # Exit directions
objects = "#888888" # Items/creatures
players = "#00FF00" # Other players
creatures = "#FF6600" # Hostile creatures
Examples
Standard Room Window
[[windows]]
name = "room"
type = "room"
row = 0
col = 80
width = 40
height = 15
show_title = true
show_description = true
show_exits = true
show_objects = true
show_players = true
border_style = "rounded"
Compact Room Info
[[windows]]
name = "room"
type = "room"
row = 0
col = 80
width = 40
height = 5
show_description = false # Hide description
show_objects = false # Hide objects
show_title = true
show_exits = true
show_players = true
Full Width
[[windows]]
name = "room"
type = "room"
row = 0
col = 0
width = "100%"
height = 8
show_border = false
transparent_background = true
Description Only
[[windows]]
name = "room_desc"
type = "room"
row = 0
col = 80
width = 40
height = 10
show_description = true
show_exits = false
show_objects = false
show_players = false
title = "Description"
Exits Only
[[windows]]
name = "exits"
type = "room"
row = 10
col = 80
width = 40
height = 2
show_description = false
show_objects = false
show_players = false
show_exits = true
show_border = false
Room Components
Room Name
Set by <streamWindow id='room' subtitle='Room Name'/>:
title_color = "#9BA2B2"
title_bg = "#395573" # Distinct background
Description
Set by <component id='room desc'>:
description_color = "#B0B0B0"
word_wrap = true # Wrap long descriptions
Exits
Set by <compass> and displayed as text:
exits_color = "#00CCCC"
exits_prefix = "Obvious paths: "
Objects/Creatures
Set by <component id='room objs'>:
objects_color = "#888888"
creature_highlight = true # Highlight creatures
creature_color = "#FF6600" # Creature color
Players
Set by <component id='room players'>:
players_color = "#00FF00"
players_prefix = "Also here: "
Interaction
With links = true:
| Input | Action |
|---|---|
| Click object | Look/interact |
| Click player | Look at player |
| Click exit | Move that direction |
| Right-click | Context menu |
Data Source
Room windows receive data from multiple XML elements:
<streamWindow id='room' subtitle='Town Square'/>
<component id='room desc'>This is the description...</component>
<component id='room objs'>You also see a bench...</component>
<component id='room players'>Also here: Alice, Bob</component>
<compass><dir value='n'/><dir value='e'/></compass>
Troubleshooting
Room not updating
- Check you’re receiving room data
- Verify window stream is correct
- Some areas suppress room info
Missing components
- Enable specific show_* options
- Check game is sending that component
- Verify component colors are visible
Text overflow
- Increase window height
- Enable word wrap
- Hide less important components
See Also
- Compass - Visual exit display
- Text Windows - General text display
- Highlights - Text styling
Inventory Widget
The inventory widget displays your character’s carried items and container contents.
Overview
Inventory widgets:
- Show items in containers (backpack, cloak, etc.)
- Display item counts and descriptions
- Support expandable container trees
- Enable quick item access
Configuration
[[windows]]
name = "inventory"
type = "inventory"
# Position and size
row = 0
col = 80
width = 40
height = 25
# Inventory-specific options
show_containers = true # Show container hierarchy
expand_containers = false # Auto-expand all containers
show_count = true # Show item counts
show_weight = true # Show weight (if available)
sort_by = "name" # "name", "type", "none"
# Interaction
clickable = true # Click to interact
double_click_action = "look" # "look", "get", "open"
Properties
show_containers
Display container hierarchy:
show_containers = true # Show nested structure
show_containers = false # Flat item list
expand_containers
Auto-expand containers:
expand_containers = false # Collapsed by default
expand_containers = true # All expanded
show_count
Display item counts:
show_count = true # "5 silver coins"
show_count = false # "silver coins"
show_weight
Display item weight (if available):
show_weight = true # Show weight info
show_weight = false # Hide weight
sort_by
Item sort order:
sort_by = "name" # Alphabetical
sort_by = "type" # By item type
sort_by = "none" # Game order
Display Format
Tree View
┌─ Inventory ────────────────────────┐
│ ▼ a leather backpack │
│ ├─ some silver coins (23) │
│ ├─ a healing potion │
│ ├─ a bronze lockpick │
│ └─ ▶ a small pouch │
│ │
│ ▼ a dark cloak │
│ ├─ a silver dagger │
│ └─ a vial of poison │
│ │
│ a sturdy shield │
│ a steel longsword │
└────────────────────────────────────┘
- ▼ = Expanded container
- ▶ = Collapsed container
Flat View
┌─ Inventory ────────────────────────┐
│ some silver coins (23) │
│ a healing potion │
│ a bronze lockpick │
│ a silver dagger │
│ a vial of poison │
│ a sturdy shield │
│ a steel longsword │
└────────────────────────────────────┘
Compact View
┌─ Items ───────────┐
│ coins(23) potion │
│ lockpick dagger │
│ poison shield │
│ longsword │
└───────────────────┘
Examples
Full Inventory Panel
[[windows]]
name = "inventory"
type = "inventory"
row = 0
col = 80
width = 40
height = 30
show_containers = true
expand_containers = false
show_count = true
clickable = true
title = "Inventory"
border_style = "rounded"
Compact Item List
[[windows]]
name = "items"
type = "inventory"
row = 0
col = 100
width = 25
height = 15
show_containers = false
show_count = true
sort_by = "name"
title = "Items"
Container Focus
[[windows]]
name = "backpack"
type = "inventory"
row = 10
col = 80
width = 30
height = 15
container = "backpack" # Focus on specific container
expand_containers = true
title = "Backpack"
Merchant View
[[windows]]
name = "merchant_inv"
type = "inventory"
row = 0
col = 60
width = 50
height = 35
show_containers = true
expand_containers = true
show_weight = true
sort_by = "type"
clickable = true
double_click_action = "look"
Item Interaction
Click Actions
| Action | Result |
|---|---|
| Single click | Select item |
| Double click | Configurable action |
| Right click | Context menu |
Context Menu Options
┌────────────────┐
│ Look │
│ Get │
│ Drop │
│ Put in... │
│ Give to... │
│ ────────────── │
│ Examine │
│ Appraise │
└────────────────┘
Keyboard Navigation
| Key | Action |
|---|---|
↑/↓ | Navigate items |
Enter | Default action |
Space | Expand/collapse |
l | Look at item |
g | Get item |
d | Drop item |
Item Colors
Color items by type or rarity:
[[windows]]
name = "inventory"
type = "inventory"
[windows.item_colors]
weapon = "#FF8800" # Orange
armor = "#888888" # Gray
potion = "#00FF00" # Green
scroll = "#FFFF00" # Yellow
gem = "#FF00FF" # Magenta
coin = "#FFD700" # Gold
container = "#00FFFF" # Cyan
default = "#FFFFFF" # White
Rarity Colors
[windows.rarity_colors]
common = "#FFFFFF" # White
uncommon = "#00FF00" # Green
rare = "#0088FF" # Blue
epic = "#FF00FF" # Purple
legendary = "#FFD700" # Gold
Container Options
Specific Container
Focus on a single container:
container = "backpack" # Only show backpack contents
container = "cloak" # Only show cloak contents
Container Depth
Limit nesting depth:
max_depth = 2 # Only show 2 levels deep
max_depth = 0 # Show all levels (default)
Auto-Refresh
auto_refresh = true # Update when inventory changes
refresh_rate = 1000 # Milliseconds (if polling)
Data Source
Inventory data comes from XML elements:
<inv id="stow">
<item>a leather backpack</item>
<item>a dark cloak</item>
</inv>
<container id="backpack">
<item>some silver coins</item>
<item>a healing potion</item>
</container>
Integration Examples
With Hands Display
# What you're holding
[[windows]]
name = "hands"
type = "hand"
row = 0
col = 80
width = 30
height = 3
# Full inventory below
[[windows]]
name = "inventory"
type = "inventory"
row = 3
col = 80
width = 30
height = 20
Merchant Layout
# Your inventory (left)
[[windows]]
name = "my_items"
type = "inventory"
row = 0
col = 0
width = 40
height = 30
title = "Your Items"
# Merchant inventory (right)
[[windows]]
name = "merchant"
type = "text"
stream = "merchant"
row = 0
col = 50
width = 40
height = 30
title = "Merchant"
Quick Access Bar
# Horizontal quick items
[[windows]]
name = "quick_items"
type = "inventory"
row = 0
col = 0
width = 80
height = 3
show_containers = false
sort_by = "type"
layout = "horizontal"
Weight Tracking
If weight tracking is enabled:
┌─ Inventory ────────────────────────┐
│ Carried: 45.2 lbs / 100 lbs │
│ ──────────────────────────────────│
│ ▼ leather backpack (12.5 lbs) │
│ ├─ silver coins (2.3 lbs) │
│ └─ healing potion (0.5 lbs) │
└────────────────────────────────────┘
show_weight = true
show_encumbrance = true
encumbrance_warning = 80 # Warn at 80% capacity
Troubleshooting
Inventory not updating
- Verify receiving inventory data
- Check window is correct type
- Try manual refresh command
Containers not expanding
- Check expand_containers setting
- Verify container data is being sent
- Click expand arrow manually
Missing items
- Check container filter settings
- Verify all containers are included
- Check item parsing in debug log
Performance with large inventory
- Set expand_containers = false
- Reduce max_depth
- Focus on specific containers
See Also
- Hands - Held items display
- Room Window - Room objects
- Text Windows - General text display
Spells Widget
The spells widget displays your character’s known spells, spell preparation status, and casting availability.
Overview
Spells widgets:
- List known spells and abilities
- Show spell preparation status
- Display mana costs and cooldowns
- Enable quick spell casting
Configuration
[[windows]]
name = "spells"
type = "spells"
# Position and size
row = 0
col = 100
width = 30
height = 20
# Spell display options
show_prepared = true # Show preparation status
show_cost = true # Show mana cost
show_circle = true # Show spell circle
group_by_circle = true # Group by spell circle
sort_by = "circle" # "circle", "name", "cost"
# Interaction
clickable = true # Click to cast
show_tooltip = true # Show spell info on hover
# Colors
prepared_color = "#00FF00"
unprepared_color = "#808080"
unavailable_color = "#FF0000"
Properties
show_prepared
Display spell preparation status:
show_prepared = true # Show ✓/✗ for prepared
show_prepared = false # List only
show_cost
Display mana cost:
show_cost = true # Show "(5 mana)"
show_cost = false # Name only
show_circle
Display spell circle number:
show_circle = true # Show circle/level
show_circle = false # Hide circle
group_by_circle
Group spells by circle:
group_by_circle = true # Sections by circle
group_by_circle = false # Single list
sort_by
Sort order for spells:
sort_by = "circle" # By spell circle
sort_by = "name" # Alphabetical
sort_by = "cost" # By mana cost
Display Format
Grouped by Circle
┌─ Spells ─────────────────────────┐
│ ─── Minor Spirit (100s) ─── │
│ ✓ Spirit Warding I (1) │
│ ✓ Spirit Defense II (2) │
│ ✗ Spirit Fog (6) │
│ │
│ ─── Major Spirit (200s) ─── │
│ ✓ Spirit Shield (3) │
│ ✗ Elemental Defense (5) │
└──────────────────────────────────┘
Simple List
┌─ Spells ─────────────────────────┐
│ ✓ Spirit Warding I (1) │
│ ✓ Spirit Defense II (2) │
│ ✓ Spirit Shield (3) │
│ ✗ Elemental Defense (5) │
│ ✗ Spirit Fog (6) │
└──────────────────────────────────┘
Compact Format
┌─ Spells ──────────────┐
│ 101✓ 102✓ 103✗ 104✓ │
│ 201✓ 202✗ 203✓ 204✗ │
└───────────────────────┘
Spell Status
| Symbol | Meaning | Color (default) |
|---|---|---|
| ✓ | Prepared | Green |
| ✗ | Not prepared | Gray |
| ◉ | Currently casting | Cyan |
| ⊘ | Unavailable | Red |
| ⏱ | On cooldown | Yellow |
Examples
Full Spell Panel
[[windows]]
name = "spells"
type = "spells"
row = 0
col = 100
width = 35
height = 25
show_prepared = true
show_cost = true
show_circle = true
group_by_circle = true
clickable = true
title = "Known Spells"
Compact Spell Bar
[[windows]]
name = "spell_bar"
type = "spells"
row = 0
col = 50
width = 40
height = 3
group_by_circle = false
show_cost = false
layout = "horizontal"
show_border = false
Prepared Spells Only
[[windows]]
name = "prepared"
type = "spells"
row = 10
col = 100
width = 25
height = 10
filter = "prepared" # Only show prepared
show_prepared = false # No status icon needed
title = "Ready Spells"
Quick Cast Panel
[[windows]]
name = "quick_cast"
type = "spells"
row = 0
col = 100
width = 20
height = 15
favorite_spells = [101, 103, 107, 201, 206]
clickable = true
double_click_action = "cast"
title = "Quick Cast"
Spell Interaction
Click Actions
| Action | Result |
|---|---|
| Single click | Select spell |
| Double click | Cast spell |
| Right click | Spell info menu |
| Shift+click | Prepare spell |
Context Menu
┌────────────────────┐
│ Cast │
│ Prepare │
│ ────────────────── │
│ Spell Info │
│ Show Incantation │
│ ────────────────── │
│ Add to Quick Cast │
│ Set Hotkey │
└────────────────────┘
Keyboard Shortcuts
| Key | Action |
|---|---|
↑/↓ | Navigate spells |
Enter | Cast selected |
p | Prepare selected |
i | Show info |
1-9 | Quick cast by position |
Spell Colors
Color spells by type or circle:
[[windows]]
name = "spells"
type = "spells"
[windows.spell_colors]
minor_spirit = "#00FFFF" # Cyan (100s)
major_spirit = "#0088FF" # Blue (200s)
cleric = "#FFFFFF" # White (300s)
minor_elemental = "#FF8800" # Orange (400s)
major_elemental = "#FF0000" # Red (500s)
ranger = "#00FF00" # Green (600s)
sorcerer = "#FF00FF" # Purple (700s)
wizard = "#FFFF00" # Yellow (900s)
Status Colors
[windows.status_colors]
prepared = "#00FF00" # Green
unprepared = "#808080" # Gray
casting = "#00FFFF" # Cyan
cooldown = "#FFFF00" # Yellow
unavailable = "#FF0000" # Red
insufficient_mana = "#FF8800" # Orange
Mana Cost Display
Show mana costs with current mana:
show_cost = true
show_current_mana = true # Show current/max
highlight_affordable = true # Dim unaffordable
┌─ Spells ─────────────────────────┐
│ Mana: 45/100 │
│ ──────────────────────────────── │
│ ✓ Spirit Shield (3) │
│ ✓ Elemental Defense (5) │
│ ✗ Major Sanctuary (50) │ ← Dimmed (can't afford)
└──────────────────────────────────┘
Cooldown Display
Show cooldown timers:
show_cooldowns = true
cooldown_format = "remaining" # "remaining", "ready_at", "bar"
┌─ Spells ─────────────────────────┐
│ ✓ Spirit Shield Ready │
│ ⏱ Mass Sanctuary (0:45) │
│ ⏱ Divine Intervention (2:30) │
└──────────────────────────────────┘
Data Source
Spell data comes from XML elements:
<spellList>
<spell id="101" name="Spirit Warding I" prepared="y"/>
<spell id="102" name="Spirit Barrier" prepared="n"/>
</spellList>
<spell>Spirit Shield</spell>
<duration value="300"/>
Integration Examples
With Active Effects
# Active spells/effects
[[windows]]
name = "active"
type = "active_effects"
row = 0
col = 100
width = 25
height = 10
title = "Active"
# Known spells below
[[windows]]
name = "spells"
type = "spells"
row = 10
col = 100
width = 25
height = 20
title = "Spells"
Combat Spell Panel
# Mana bar
[[windows]]
name = "mana"
type = "progress"
source = "mana"
row = 0
col = 80
width = 30
height = 1
# Offensive spells
[[windows]]
name = "attack_spells"
type = "spells"
row = 1
col = 80
width = 15
height = 10
filter_circles = [400, 500, 700]
title = "Attack"
# Defensive spells
[[windows]]
name = "defense_spells"
type = "spells"
row = 1
col = 95
width = 15
height = 10
filter_circles = [100, 200, 300]
title = "Defense"
Troubleshooting
Spells not showing
- Verify spell list data is being received
- Check filter settings
- Ensure widget type is correct
Preparation status wrong
- Check game is sending status updates
- Verify spell ID matching
- Manual refresh may help
Can’t click to cast
- Verify clickable = true
- Check double_click_action setting
- Ensure widget has focus
Missing spell circles
- Check filter_circles setting
- Verify all circles are enabled
- Check data parsing
See Also
- Active Effects - Active spell display
- Progress Bars - Mana display
- Hands - Spell hand display
- Countdowns - Cast time display
Dashboard Widget
The dashboard widget combines multiple data displays into a single composite panel, providing an at-a-glance overview of character status.
Overview
Dashboard widgets:
- Combine multiple metrics in one widget
- Provide compact status overview
- Reduce window count
- Customizable component selection
Configuration
[[windows]]
name = "dashboard"
type = "dashboard"
# Position and size
row = 0
col = 0
width = 40
height = 15
# Components to include
components = [
"vitals",
"experience",
"stance",
"encumbrance",
"wounds"
]
# Layout
layout = "vertical" # "vertical", "horizontal", "grid"
compact = false # Condensed display
# Visual options
show_labels = true
show_dividers = true
Properties
components
Which status elements to display:
# Full dashboard
components = [
"vitals", # Health, mana, stamina, spirit
"experience", # XP info
"stance", # Combat stance
"encumbrance", # Weight carried
"wounds", # Injury summary
"status", # Status indicators
"wealth", # Silver/gold
"position" # Standing/sitting/etc.
]
# Minimal dashboard
components = ["vitals", "stance"]
layout
Component arrangement:
layout = "vertical" # Stacked top to bottom
layout = "horizontal" # Side by side
layout = "grid" # 2-column grid
compact
Condensed display mode:
compact = false # Full labels and spacing
compact = true # Minimal spacing, abbreviations
show_labels
Display component labels:
show_labels = true # "Health: 100/100"
show_labels = false # "100/100"
show_dividers
Display lines between components:
show_dividers = true # Lines between sections
show_dividers = false # No dividers
Available Components
vitals
Health, mana, stamina, spirit bars:
Health: [████████████████░░░░] 80%
Mana: [████████████░░░░░░░░] 60%
Stamina: [██████████████████░░] 90%
Spirit: [████████████████████] 100%
experience
Experience and level info:
Level: 42
Exp: 12,345,678 / 15,000,000
Mind: Clear
stance
Combat stance display:
Stance: Offensive (100/80)
encumbrance
Weight and capacity:
Encumbrance: 45.2 / 100 lbs (45%)
wounds
Injury summary:
Wounds: Head(1) Chest(2) RArm(3)
status
Active status conditions:
Status: Stunned, Poisoned
wealth
Currency display:
Silver: 12,345
position
Character position:
Position: Standing
roundtime
Current roundtime:
RT: 3 seconds
target
Current target:
Target: a massive troll
Display Formats
Standard Vertical
┌─ Dashboard ──────────────────────┐
│ Health: [████████░░] 80/100 │
│ Mana: [██████░░░░] 60/100 │
│ Stamina: [█████████░] 90/100 │
│ ──────────────────────────────── │
│ Level 42 | Mind: Clear │
│ Exp: 12.3M / 15M │
│ ──────────────────────────────── │
│ Stance: Offensive (100/80) │
│ Position: Standing │
│ ──────────────────────────────── │
│ Wounds: None │
│ Status: Clear │
└──────────────────────────────────┘
Compact Horizontal
┌─ Status ─────────────────────────────────────────────┐
│ HP:80% MP:60% ST:90% | L42 | Off(100/80) | Standing │
└──────────────────────────────────────────────────────┘
Grid Layout
┌─ Dashboard ──────────────────────┐
│ Health: 80% │ Level: 42 │
│ Mana: 60% │ Mind: Clear │
│ Stamina: 90% │ Exp: 12.3M │
│ ─────────────┼────────────────── │
│ Stance: Off │ Position: Stand │
│ RT: 0 │ Silver: 12,345 │
└──────────────────────────────────┘
Examples
Full Status Dashboard
[[windows]]
name = "dashboard"
type = "dashboard"
row = 0
col = 80
width = 40
height = 20
components = [
"vitals",
"experience",
"stance",
"encumbrance",
"wounds",
"status",
"wealth",
"position"
]
layout = "vertical"
show_dividers = true
title = "Character Status"
Combat Dashboard
[[windows]]
name = "combat_dash"
type = "dashboard"
row = 0
col = 80
width = 35
height = 12
components = [
"vitals",
"stance",
"roundtime",
"target",
"wounds"
]
layout = "vertical"
compact = true
title = "Combat"
Minimal Status Bar
[[windows]]
name = "status_bar"
type = "dashboard"
row = 0
col = 0
width = 80
height = 2
components = ["vitals", "stance", "roundtime"]
layout = "horizontal"
compact = true
show_border = false
show_dividers = false
Experience Focus
[[windows]]
name = "exp_dash"
type = "dashboard"
row = 10
col = 100
width = 25
height = 8
components = ["experience", "wealth"]
layout = "vertical"
title = "Progress"
Hunting Dashboard
[[windows]]
name = "hunting"
type = "dashboard"
row = 0
col = 80
width = 40
height = 15
components = [
"vitals",
"stance",
"wounds",
"target",
"roundtime",
"encumbrance"
]
layout = "grid"
compact = false
title = "Hunting Status"
Component Colors
Customize colors for each component:
[[windows]]
name = "dashboard"
type = "dashboard"
[windows.colors]
# Vital bar colors
health = "#FF0000"
mana = "#0088FF"
stamina = "#00FF00"
spirit = "#FFFF00"
# Status colors
wounded = "#FF8800"
stunned = "#FF0000"
hidden = "#00FFFF"
# Text colors
labels = "#808080"
values = "#FFFFFF"
dividers = "#404040"
Threshold Colors
Change colors based on values:
[windows.health_thresholds]
high = "#00FF00" # > 75%
medium = "#FFFF00" # 25-75%
low = "#FF8800" # 10-25%
critical = "#FF0000" # < 10%
Data Sources
Dashboard pulls from multiple XML sources:
<dialogData id="health" text="80/100"/>
<dialogData id="mana" text="60/100"/>
<dialogData id="stamina" text="90/100"/>
<dialogData id="spirit" text="100/100"/>
<dialogData id="stance" text="offensive"/>
<indicator id="IconSTANDING" visible="y"/>
<exp>12345678</exp>
<nextExp>15000000</nextExp>
Building Custom Dashboards
Selective Components
Only include what you need:
# Minimalist - just vitals
components = ["vitals"]
# Combat-focused
components = ["vitals", "stance", "wounds", "roundtime"]
# Exploration-focused
components = ["vitals", "encumbrance", "position"]
# Progression-focused
components = ["experience", "wealth"]
Multiple Dashboards
Use different dashboards for different activities:
# Main status (always visible)
[[windows]]
name = "main_dash"
type = "dashboard"
row = 0
col = 80
width = 40
height = 8
components = ["vitals", "stance"]
# Combat details (show during hunting)
[[windows]]
name = "combat_dash"
type = "dashboard"
row = 8
col = 80
width = 40
height = 10
components = ["wounds", "target", "roundtime"]
visible = false # Toggle via keybind
Troubleshooting
Components not showing
- Verify component name is correct
- Check game is sending that data
- Ensure dashboard has sufficient size
Layout looks wrong
- Adjust width/height for layout type
- Try different layout option
- Enable/disable compact mode
Data not updating
- Verify XML data is being received
- Check component is correctly configured
- Test individual widgets for same data
Performance issues
- Reduce number of components
- Use compact mode
- Increase refresh interval
See Also
- Progress Bars - Individual vital displays
- Status Indicators - Status condition display
- Injury Doll - Wound display
- Performance - Performance metrics
Performance Monitor
The performance widget displays real-time performance metrics for debugging and optimization.
Overview
The performance widget shows:
- Frame rate (FPS)
- Frame timing statistics
- Network throughput
- Parser performance
- Memory usage
- Application uptime
Configuration
[[windows]]
name = "performance"
type = "performance"
# Position and size
row = 0
col = 0
width = 35
height = 15
# Which metrics to display
show_fps = true
show_frame_times = true
show_render_times = true
show_ui_times = false
show_wrap_times = false
show_net = true
show_parse = true
show_events = false
show_memory = true
show_lines = true
show_uptime = true
show_jitter = false
show_frame_spikes = false
show_event_lag = false
show_memory_delta = false
Metric Options
Frame Metrics
| Option | Description |
|---|---|
show_fps | Frames per second |
show_frame_times | Frame time min/avg/max |
show_jitter | Frame time variance |
show_frame_spikes | Count of slow frames (>33ms) |
Render Metrics
| Option | Description |
|---|---|
show_render_times | Total render duration |
show_ui_times | UI widget render time |
show_wrap_times | Text wrapping time |
Network Metrics
| Option | Description |
|---|---|
show_net | Bytes received/sent per second |
Parser Metrics
| Option | Description |
|---|---|
show_parse | Parse time and chunks/sec |
Event Metrics
| Option | Description |
|---|---|
show_events | Event processing time |
show_event_lag | Time since last event |
Memory Metrics
| Option | Description |
|---|---|
show_memory | Estimated memory usage |
show_lines | Total lines buffered |
show_memory_delta | Memory change rate |
General
| Option | Description |
|---|---|
show_uptime | Application uptime |
Display Example
┌─ Performance ──────────────────────┐
│ FPS: 60.0 │
│ Frame: 0.8/1.2/2.1 ms │
│ Render: 0.6 ms │
│ │
│ Net In: 1.2 KB/s │
│ Net Out: 0.1 KB/s │
│ │
│ Parse: 45 µs (12/s) │
│ │
│ Memory: ~2.4 MB │
│ Lines: 12,345 │
│ │
│ Uptime: 01:23:45 │
└────────────────────────────────────┘
Examples
Full Metrics
[[windows]]
name = "performance"
type = "performance"
row = 0
col = 0
width = 35
height = 18
show_fps = true
show_frame_times = true
show_render_times = true
show_ui_times = true
show_wrap_times = true
show_net = true
show_parse = true
show_events = true
show_memory = true
show_lines = true
show_uptime = true
show_jitter = true
show_frame_spikes = true
Minimal FPS
[[windows]]
name = "fps"
type = "performance"
row = 0
col = 0
width = 15
height = 1
show_fps = true
show_frame_times = false
show_render_times = false
show_net = false
show_parse = false
show_memory = false
show_lines = false
show_uptime = false
show_border = false
Network Focus
[[windows]]
name = "network"
type = "performance"
row = 0
col = 100
width = 20
height = 5
show_fps = false
show_net = true
show_parse = true
show_memory = false
title = "Network"
Memory Focus
[[windows]]
name = "memory"
type = "performance"
row = 5
col = 100
width = 20
height = 5
show_fps = false
show_net = false
show_memory = true
show_lines = true
show_memory_delta = true
title = "Memory"
Metric Details
FPS (Frames Per Second)
FPS: 60.0
Target is 60 FPS. Lower values indicate performance issues.
Frame Times
Frame: 0.8/1.2/2.1 ms
Shows min/avg/max frame time over recent samples. Lower is better.
Frame Jitter
Jitter: 0.3 ms
Standard deviation of frame times. Lower means smoother animation.
Frame Spikes
Spikes: 2
Count of frames taking >33ms (below 30 FPS). Should be 0 or very low.
Render Time
Render: 0.6 ms
Time spent rendering all widgets. Should be well under 16ms.
Network
Net In: 1.2 KB/s
Net Out: 0.1 KB/s
Network throughput. High values during combat/activity are normal.
Parse Time
Parse: 45 µs (12/s)
Average XML parse time and chunks processed per second.
Memory
Memory: ~2.4 MB
Lines: 12,345
Estimated memory usage and total lines in all buffers.
Uptime
Uptime: 01:23:45
Time since Two-Face started (HH:MM:SS).
Performance Overhead
The performance widget itself has minimal overhead:
- Only enabled metrics are collected
- Collection can be disabled entirely
# In config.toml
[performance]
collect_metrics = false # Disable all collection
Troubleshooting Performance
Low FPS
- Reduce buffer sizes in text windows
- Disable unused widgets
- Reduce highlight pattern count
- Use
fast_parse = truefor highlights
High Memory
- Reduce
buffer_sizeon windows - Close unused tabs
- Check for memory leaks (rising delta)
Network Issues
- Check connection stability
- Monitor for packet loss
- Consider bandwidth limitations
See Also
- Performance Optimization - Optimization guide
- Configuration - Performance settings
- Troubleshooting - Performance fixes
Customization Overview
Two-Face is highly customizable, allowing you to tailor every aspect of your gameplay experience.
Customization Areas
Two-Face can be customized in these areas:
| Area | Purpose | Configuration |
|---|---|---|
| Layouts | Window arrangement | layout.toml |
| Themes | Colors and styling | colors.toml |
| Highlights | Text pattern matching | highlights.toml |
| Keybinds | Keyboard shortcuts | keybinds.toml |
| Sounds | Audio alerts | Per-highlight |
| TTS | Text-to-speech | config.toml |
Quick Customization Commands
| Command | Opens |
|---|---|
.layout | Layout editor |
.highlights | Highlight browser |
.keybinds | Keybind browser |
.colors | Color palette browser |
.themes | Theme browser |
.window <name> | Window editor |
Customization Guides
This section covers:
- Creating Layouts - Design your window arrangement
- Creating Themes - Customize colors and styling
- Highlight Patterns - Advanced text matching
- Keybind Actions - All available actions
- Sound Alerts - Audio notifications
- TTS Setup - Text-to-speech configuration
Per-Character Customization
Each character can have unique settings:
~/.two-face/
├── config.toml # Global defaults
├── layout.toml # Default layout
├── colors.toml # Default colors
├── highlights.toml # Default highlights
├── keybinds.toml # Default keybinds
└── characters/
└── MyCharacter/
├── layout.toml # Character-specific layout
├── colors.toml # Character-specific colors
├── highlights.toml # Character-specific highlights
└── keybinds.toml # Character-specific keybinds
Configuration Loading Order
- Character-specific file (if exists)
- Global file (fallback)
- Embedded defaults (final fallback)
Example: Profession-Based Layouts
Create different layouts for different professions:
~/.two-face/characters/
├── Warrior/
│ └── layout.toml # Combat-focused layout
├── Empath/
│ └── layout.toml # Healing-focused layout
└── Wizard/
└── layout.toml # Spell-focused layout
Hot-Reloading
Many settings can be reloaded without restarting:
| Command | Reloads |
|---|---|
.reload colors | Color configuration |
.reload highlights | Highlight patterns |
.reload keybinds | Keyboard shortcuts |
.reload layout | Window layout |
.reload config | All configuration |
Backup Your Customizations
Before major changes:
# Backup all configuration
cp -r ~/.two-face ~/.two-face-backup
# Backup specific file
cp ~/.two-face/layout.toml ~/.two-face/layout.toml.bak
Sharing Customizations
Share your configurations with others:
- Export relevant
.tomlfiles - Share via GitHub Gist, Discord, etc.
- Others can copy to their
~/.two-face/directory
Popular Layout Packs
The Two-Face community shares layout packs:
- Hunting layouts
- Merchant layouts
- Roleplay layouts
- Accessibility layouts
Customization Tips
Start Simple
Begin with default settings and customize incrementally:
- Play with defaults for a few sessions
- Identify pain points
- Make one change at a time
- Test each change before moving on
Use the Editors
The built-in editors are often easier than editing files:
.layout # Visual layout editor
.highlights # Browse and edit highlights
.keybinds # Browse and edit keybinds
.window main # Edit main window properties
Keep Notes
Document your customizations:
# In any .toml file, add comments:
# Combat creature highlighting
# Red for dangerous, orange for normal
[creature_dangerous]
pattern = "(?i)\\b(dragon|lich|demon)\\b"
fg = "#FF0000"
Test in Safe Areas
Test new configurations in safe game areas before combat:
- Make changes
- Reload:
.reload highlights - Test with game output
- Adjust as needed
See Also
- Configuration Files - File reference
- Widgets Reference - Widget options
- Tutorials - Step-by-step guides
Creating Layouts
Layouts define the arrangement of widgets on your screen. This guide walks you through creating custom layouts.
Layout Basics
A layout is a collection of window definitions in layout.toml:
[[windows]]
name = "main"
type = "text"
row = 0
col = 0
width = 80
height = 40
streams = ["main", "death"]
[[windows]]
name = "health"
type = "progress"
source = "health"
row = 0
col = 80
width = 30
height = 1
Coordinate System
Two-Face uses a row/column grid:
(0,0)────────────────────────→ col
│
│ ┌─────────────┐
│ │ Widget │
│ │ row=5 │
│ │ col=10 │
│ └─────────────┘
│
↓ row
- row - Vertical position (0 = top)
- col - Horizontal position (0 = left)
- width - Widget width in columns
- height - Widget height in rows
Percentage-Based Sizing
Use percentages for responsive layouts:
[[windows]]
name = "main"
type = "text"
row = 0
col = 0
width = "60%" # 60% of screen width
height = "100%" # Full screen height
[[windows]]
name = "sidebar"
type = "text"
row = 0
col = "60%" # Start at 60% from left
width = "40%" # Remaining 40%
height = "100%"
Layout Planning
Step 1: Sketch Your Layout
Draw your desired layout on paper or in a text editor:
┌─────────────────────┬──────────────┐
│ │ Health │
│ ├──────────────┤
│ Main Window │ Mana │
│ ├──────────────┤
│ │ Compass │
│ ├──────────────┤
│ │ Room │
├─────────────────────┴──────────────┤
│ Command Input │
└────────────────────────────────────┘
Step 2: Calculate Dimensions
Determine sizes based on your terminal:
Terminal: 120 columns × 40 rows
Main window: 80 cols × 35 rows (0,0)
Sidebar: 40 cols × 35 rows (0,80)
Command: 120 cols × 5 rows (35,0)
Step 3: Define Windows
Create layout.toml:
# Main game window
[[windows]]
name = "main"
type = "text"
row = 0
col = 0
width = 80
height = 35
streams = ["main", "death"]
buffer_size = 2000
# Health bar
[[windows]]
name = "health"
type = "progress"
source = "health"
row = 0
col = 80
width = 40
height = 1
# Mana bar
[[windows]]
name = "mana"
type = "progress"
source = "mana"
row = 1
col = 80
width = 40
height = 1
# Compass
[[windows]]
name = "compass"
type = "compass"
row = 3
col = 80
width = 20
height = 5
# Room info
[[windows]]
name = "room"
type = "room"
row = 8
col = 80
width = 40
height = 15
# Command input
[[windows]]
name = "input"
type = "command_input"
row = 35
col = 0
width = 120
height = 5
Common Layout Patterns
Full-Width Main
[[windows]]
name = "main"
type = "text"
row = 0
col = 0
width = "100%"
height = "90%"
[[windows]]
name = "input"
type = "command_input"
row = "90%"
col = 0
width = "100%"
height = "10%"
Side Panel
[[windows]]
name = "main"
type = "text"
row = 0
col = 0
width = "70%"
height = "100%"
[[windows]]
name = "sidebar"
type = "tabbed_text"
row = 0
col = "70%"
width = "30%"
height = "100%"
tabs = ["speech", "thoughts", "combat"]
Three-Column
# Left column - Status
[[windows]]
name = "status"
type = "dashboard"
row = 0
col = 0
width = "20%"
height = "100%"
# Center column - Main
[[windows]]
name = "main"
type = "text"
row = 0
col = "20%"
width = "50%"
height = "100%"
# Right column - Info
[[windows]]
name = "info"
type = "tabbed_text"
row = 0
col = "70%"
width = "30%"
height = "100%"
Top/Bottom Split
# Main game (top 70%)
[[windows]]
name = "main"
type = "text"
row = 0
col = 0
width = "100%"
height = "70%"
# Combat log (bottom 30%)
[[windows]]
name = "combat"
type = "text"
row = "70%"
col = 0
width = "100%"
height = "30%"
streams = ["combat"]
Widget Types
Choose the right widget type for each purpose:
| Type | Purpose | Example |
|---|---|---|
text | General text display | Main window, logs |
tabbed_text | Multiple streams in tabs | Speech, thoughts |
command_input | Command entry | Input bar |
progress | Bars (health, mana) | Vitals |
countdown | Timers (RT, cast) | Roundtime |
compass | Navigation | Exits |
room | Room information | Room panel |
hand | Held items | Hands display |
indicator | Status icons | Conditions |
injury_doll | Body injuries | Wounds |
active_effects | Buffs/debuffs | Spells |
dashboard | Combined status | Status panel |
performance | Debug metrics | FPS, memory |
Window Properties
Common Properties
[[windows]]
name = "example" # Unique name (required)
type = "text" # Widget type (required)
row = 0 # Position
col = 0
width = 40
height = 20
# Visual
title = "My Window" # Custom title
show_title = true # Show title bar
show_border = true # Show border
border_style = "rounded" # plain, rounded, double
# Colors
bg_color = "#000000"
text_color = "#FFFFFF"
border_color = "#00FFFF"
# Behavior
visible = true # Initially visible
focusable = true # Can receive focus
Text Window Properties
[[windows]]
name = "main"
type = "text"
streams = ["main"] # Stream IDs to display
buffer_size = 1000 # Max lines to keep
wordwrap = true # Wrap long lines
timestamps = false # Show timestamps
Progress Bar Properties
[[windows]]
name = "health"
type = "progress"
source = "health" # health, mana, spirit, stamina
show_label = true # Show "Health"
show_numbers = true # Show "85/100"
show_percent = false # Show "85%"
bar_color = "#FF0000"
Using the Layout Editor
The built-in editor is often easier:
.layout
Features:
- Visual widget placement
- Drag to reposition
- Resize handles
- Property editing
- Live preview
Testing Your Layout
Quick Test
- Save
layout.toml - Run
.reload layout - Check appearance
- Adjust and repeat
Terminal Size Testing
Test at different sizes:
# Resize terminal and reload
resize -s 30 80 # Small
.reload layout
resize -s 40 120 # Medium
.reload layout
resize -s 50 160 # Large
.reload layout
Troubleshooting
Overlapping Windows
Windows are drawn in order. Later windows cover earlier ones:
# Background windows first
[[windows]]
name = "background"
# ...
# Foreground windows last
[[windows]]
name = "overlay"
# ...
Windows Cut Off
Check terminal size vs layout dimensions:
# Use percentages for safety
width = "50%" # Instead of: width = 80
# Or check minimum sizes
min_width = 20
min_height = 5
Responsive Issues
Test with percentages:
# Good: Adapts to terminal size
row = 0
col = "60%"
width = "40%"
height = "100%"
# Risky: Fixed size may not fit
row = 0
col = 100
width = 60
height = 50
Layout Examples
Minimalist
[[windows]]
name = "main"
type = "text"
row = 0
col = 0
width = "100%"
height = "95%"
show_border = false
[[windows]]
name = "input"
type = "command_input"
row = "95%"
col = 0
width = "100%"
height = "5%"
show_border = false
Information Dense
See the Hunting Setup Tutorial for a complex combat layout.
Roleplay Focused
See the Roleplay Setup Tutorial for a communication-focused layout.
See Also
- Layout Configuration - Full reference
- Widgets Reference - All widget types
- Your First Layout - Tutorial
Creating Themes
Themes control colors throughout Two-Face. This guide shows how to create custom color schemes.
Theme Basics
Themes are defined in colors.toml:
# UI Colors
[ui]
text_color = "#FFFFFF"
background_color = "#000000"
border_color = "#00FFFF"
focused_border_color = "#FFFF00"
# Text Presets
[presets.speech]
fg = "#53a684"
[presets.monsterbold]
fg = "#a29900"
Color Formats
Two-Face supports multiple color formats:
| Format | Example | Description |
|---|---|---|
| Hex RGB | #FF5500 | Standard hex |
| Named | crimson | From palette |
| Transparent | "-" | Clear background |
UI Colors
Configuration
[ui]
# Text
text_color = "#FFFFFF" # Default text
command_echo_color = "#FFFFFF" # Command echo
# Backgrounds
background_color = "#000000" # Widget background
textarea_background = "-" # Input background ("-" = transparent)
# Borders
border_color = "#00FFFF" # Unfocused borders
focused_border_color = "#FFFF00" # Focused border
# Selection
selection_bg_color = "#4a4a4a" # Text selection
Example: Dark Theme
[ui]
text_color = "#E0E0E0"
background_color = "#1A1A1A"
border_color = "#333333"
focused_border_color = "#007ACC"
selection_bg_color = "#264F78"
Example: Light Theme
[ui]
text_color = "#333333"
background_color = "#FFFFFF"
border_color = "#CCCCCC"
focused_border_color = "#0066CC"
selection_bg_color = "#ADD6FF"
Text Presets
Presets color semantic text types from the game:
Default Presets
[presets.speech]
fg = "#53a684" # Player dialogue
[presets.monsterbold]
fg = "#a29900" # Creature names
[presets.roomName]
fg = "#9BA2B2"
bg = "#395573" # Room titles
[presets.links]
fg = "#477ab3" # Clickable objects
[presets.whisper]
fg = "#60b4bf" # Whispers
[presets.thought]
fg = "#FF8080" # ESP/thoughts
Custom Preset
[presets.my_custom]
fg = "#FF6600"
bg = "#1A1A1A"
Prompt Colors
Color individual prompt characters:
[[prompt_colors]]
character = "R" # Roundtime
color = "#FF0000"
[[prompt_colors]]
character = "S" # Spell RT
color = "#FFFF00"
[[prompt_colors]]
character = "H" # Hidden
color = "#9370DB"
[[prompt_colors]]
character = ">" # Ready
color = "#A9A9A9"
Spell Colors
Color spell bars by spell number:
# Minor Spirit (500s)
[[spell_colors]]
spells = [503, 506, 507, 508, 509]
color = "#5c0000"
# Major Spirit (900s)
[[spell_colors]]
spells = [905, 911, 913]
color = "#9370db"
# Ranger (600s)
[[spell_colors]]
spells = [601, 602, 604, 605]
color = "#1c731c"
Color Palette
Define named colors for use in highlights:
[[color_palette]]
name = "danger"
color = "#FF0000"
category = "custom"
[[color_palette]]
name = "warning"
color = "#FFA500"
category = "custom"
[[color_palette]]
name = "success"
color = "#00FF00"
category = "custom"
Use in highlights:
[critical_hit]
pattern = "critical hit"
fg = "danger" # Resolved from palette
Theme Design Tips
Color Harmony
Use complementary colors:
- Analogous: Colors next to each other on wheel
- Complementary: Opposite colors
- Triadic: Three evenly spaced colors
Contrast
Ensure readable text:
- Light text on dark background
- Dark text on light background
- Minimum 4.5:1 contrast ratio
Consistency
Use a limited color palette:
- 2-3 accent colors
- Neutral backgrounds
- Semantic colors (red=danger, green=success)
Complete Theme Examples
Nord Theme
[ui]
text_color = "#D8DEE9"
background_color = "#2E3440"
border_color = "#3B4252"
focused_border_color = "#88C0D0"
selection_bg_color = "#4C566A"
[presets.speech]
fg = "#A3BE8C"
[presets.monsterbold]
fg = "#D08770"
[presets.roomName]
fg = "#ECEFF4"
bg = "#434C5E"
[presets.links]
fg = "#81A1C1"
[presets.whisper]
fg = "#B48EAD"
[[prompt_colors]]
character = "R"
color = "#BF616A"
[[prompt_colors]]
character = ">"
color = "#616E88"
Solarized Dark
[ui]
text_color = "#839496"
background_color = "#002B36"
border_color = "#073642"
focused_border_color = "#2AA198"
selection_bg_color = "#073642"
[presets.speech]
fg = "#859900"
[presets.monsterbold]
fg = "#CB4B16"
[presets.roomName]
fg = "#93A1A1"
bg = "#073642"
[presets.links]
fg = "#268BD2"
[[prompt_colors]]
character = "R"
color = "#DC322F"
[[prompt_colors]]
character = ">"
color = "#586E75"
High Contrast
[ui]
text_color = "#FFFFFF"
background_color = "#000000"
border_color = "#FFFFFF"
focused_border_color = "#FFFF00"
selection_bg_color = "#0000FF"
[presets.speech]
fg = "#00FF00"
[presets.monsterbold]
fg = "#FF0000"
[presets.roomName]
fg = "#FFFFFF"
bg = "#0000AA"
[presets.links]
fg = "#00FFFF"
[[prompt_colors]]
character = "R"
color = "#FF0000"
[[prompt_colors]]
character = ">"
color = "#FFFFFF"
Monokai
[ui]
text_color = "#F8F8F2"
background_color = "#272822"
border_color = "#49483E"
focused_border_color = "#F92672"
selection_bg_color = "#49483E"
[presets.speech]
fg = "#A6E22E"
[presets.monsterbold]
fg = "#FD971F"
[presets.roomName]
fg = "#F8F8F2"
bg = "#3E3D32"
[presets.links]
fg = "#66D9EF"
[[prompt_colors]]
character = "R"
color = "#F92672"
[[prompt_colors]]
character = ">"
color = "#75715E"
Applying Themes
Hot-Reload
.reload colors
Per-Character
Place theme in character folder:
~/.two-face/characters/MyChar/colors.toml
Creating Variants
Dark/Light Toggle
Create two theme files:
~/.two-face/themes/dark.toml
~/.two-face/themes/light.toml
Switch by copying:
cp ~/.two-face/themes/dark.toml ~/.two-face/colors.toml
Profession Variants
Create profession-specific themes:
~/.two-face/characters/Warrior/colors.toml # Combat colors
~/.two-face/characters/Empath/colors.toml # Healing colors
Sharing Themes
- Export your
colors.toml - Share via GitHub Gist, Discord, etc.
- Others copy to their
~/.two-face/directory - Run
.reload colors
See Also
- Colors Configuration - Full reference
- Theme System Architecture - How theming works
- Accessibility - High contrast themes
Highlight Patterns
Highlights let you colorize text based on patterns. This guide covers advanced pattern creation.
Pattern Basics
Highlights use regex (regular expressions):
[[highlights]]
name = "creatures"
pattern = "(?i)\\b(goblin|orc|troll)\\b"
fg = "#FF6600"
bold = true
Regex Syntax
Literal Text
# Exact match
pattern = "goblin"
# Multiple words
pattern = "goblin|orc|troll"
Character Classes
# Any digit
pattern = "\\d+" # Matches: 123, 45, 6789
# Any word character
pattern = "\\w+" # Matches: word, Word123
# Any whitespace
pattern = "\\s+" # Matches spaces, tabs
# Custom class
pattern = "[A-Z][a-z]+" # Matches: Hello, World
Quantifiers
# Zero or more
pattern = "a*" # Matches: "", a, aa, aaa
# One or more
pattern = "a+" # Matches: a, aa, aaa
# Zero or one
pattern = "colou?r" # Matches: color, colour
# Exactly n
pattern = "\\d{3}" # Matches: 123, 456
# n to m
pattern = "\\d{2,4}" # Matches: 12, 123, 1234
Anchors
# Start of line
pattern = "^You attack"
# End of line
pattern = "falls dead!$"
# Word boundary
pattern = "\\bgoblin\\b" # Won't match "goblins" or "hobgoblin"
Groups
# Non-capturing group
pattern = "(?:goblin|orc)"
# Capturing group (for future use)
pattern = "(\\w+) says,"
Modifiers
# Case insensitive
pattern = "(?i)goblin" # Matches: goblin, Goblin, GOBLIN
Common Patterns
Creature Names
[[highlights]]
name = "creatures"
pattern = "(?i)\\b(goblin|orc|troll|kobold|giant|wolf|bear)s?\\b"
fg = "#FF6600"
bold = true
Player Speech
[[highlights]]
name = "speech"
pattern = "^(\\w+) (says|asks|exclaims|whispers),"
fg = "#00FF00"
Your Character Name
[[highlights]]
name = "my_name"
pattern = "\\bYourCharacterName\\b"
fg = "#FFFF00"
bold = true
Damage Numbers
[[highlights]]
name = "damage"
pattern = "\\b\\d+\\s+points?\\s+of\\s+damage"
fg = "#FF0000"
bold = true
Currency
[[highlights]]
name = "silver"
pattern = "\\d+\\s+silvers?"
fg = "#C0C0C0"
[[highlights]]
name = "gold"
pattern = "\\d+\\s+gold"
fg = "#FFD700"
Room Exits
[[highlights]]
name = "exits"
pattern = "Obvious (paths|exits):"
fg = "#00FFFF"
ESP/Thoughts
[[highlights]]
name = "esp"
pattern = "^\\[.*?\\]"
fg = "#FF00FF"
stream = "thoughts"
Critical Hits
[[highlights]]
name = "critical"
pattern = "(?i)critical|devastating|massive"
fg = "#FF0000"
bg = "#400000"
bold = true
Healing
[[highlights]]
name = "healing"
pattern = "heal|restore|recover"
fg = "#00FF00"
Stream Filtering
Limit patterns to specific streams:
[[highlights]]
name = "combat"
pattern = "You (hit|miss)"
fg = "#FF0000"
stream = "main"
[[highlights]]
name = "esp_highlight"
pattern = "\\[ESP\\]"
fg = "#00FFFF"
stream = "thoughts"
Priority System
Higher priority wins when multiple patterns match:
[[highlights]]
name = "important"
pattern = "critical"
fg = "#FF0000"
priority = 100 # Checked first
[[highlights]]
name = "general"
pattern = ".*"
fg = "#808080"
priority = 0 # Checked last
Fast Parse Mode
For simple literal patterns, use fast_parse:
[[highlights]]
name = "names"
pattern = "Bob|Alice|Charlie"
fg = "#FFFF00"
fast_parse = true # Uses Aho-Corasick algorithm
When to use fast_parse:
- Pattern is literal strings only
- Pattern has many alternatives
- High-frequency matching needed
When NOT to use:
- Pattern uses regex features
- Pattern needs word boundaries
- Pattern uses anchors
Special Characters
Escape these with \\:
. * + ? ^ $ { } [ ] ( ) | \
# Match literal period
pattern = "Mr\\. Smith"
# Match literal asterisk
pattern = "\\*\\*emphasis\\*\\*"
# Match literal parentheses
pattern = "\\(optional\\)"
Sounds
Add sounds to highlights:
[[highlights]]
name = "whisper_alert"
pattern = "whispers to you"
fg = "#00FFFF"
sound = "~/.two-face/sounds/whisper.wav"
Squelch (Hide)
Hide matching text entirely:
[[highlights]]
name = "spam_filter"
pattern = "A gentle breeze|The sun|Birds chirp"
squelch = true
Redirect
Send matching text to different window:
[[highlights]]
name = "combat_redirect"
pattern = "You (attack|hit|miss|dodge)"
redirect = "combat"
redirect_mode = "copy" # or "move"
Testing Patterns
Test in Browser
.highlights
Navigate to your highlight and check the preview.
Test with Game Output
- Add highlight
- Run
.reload highlights - Trigger the pattern in-game
- Verify appearance
Online Regex Testers
Use regex101.com or similar to test patterns before adding.
Performance Tips
- Use fast_parse for simple patterns
- Use word boundaries
\bto limit matching - Be specific - avoid
.*when possible - Limit stream - use
stream = "main"when appropriate - Keep count low - fewer than 100 patterns recommended
Debugging
Pattern Not Matching
- Test pattern in regex101.com
- Check escaping (use
\\not\) - Check case sensitivity
- Verify stream setting
Pattern Too Broad
Add boundaries:
# Too broad
pattern = "hit" # Matches "white", "smith"
# Better
pattern = "\\bhit\\b" # Only matches "hit" as word
Escaping Issues
TOML strings need double backslashes:
# Wrong
pattern = "\d+"
# Correct
pattern = "\\d+"
Complete Example
# Combat Highlights
[[highlights]]
name = "damage_taken"
pattern = "(?i)(strikes|hits|bites|claws|slashes)\\s+you"
fg = "#FF4444"
bold = true
priority = 80
[[highlights]]
name = "damage_dealt"
pattern = "You\\s+(strike|hit|slash|stab)"
fg = "#44FF44"
priority = 80
[[highlights]]
name = "critical"
pattern = "(?i)critical|devastating"
fg = "#FF0000"
bg = "#400000"
bold = true
priority = 100
# Creatures
[[highlights]]
name = "creatures"
pattern = "(?i)\\b(goblin|orc|troll|kobold|wolf|bear|rat)s?\\b"
fg = "#FF8800"
bold = true
priority = 50
# Social
[[highlights]]
name = "my_name"
pattern = "\\bYourCharacter\\b"
fg = "#FFFF00"
bold = true
priority = 90
[[highlights]]
name = "friends"
pattern = "\\b(Alice|Bob|Charlie)\\b"
fg = "#00FF00"
fast_parse = true
priority = 60
# Currency
[[highlights]]
name = "silver"
pattern = "\\d+\\s+silvers?"
fg = "#C0C0C0"
bold = true
[[highlights]]
name = "treasure"
pattern = "(?i)(gold|gems?|jewel|treasure)"
fg = "#FFD700"
See Also
- Highlights Configuration - Full reference
- Theme System - How colors work
- Performance - Optimization
Keybind Actions
Two-Face supports extensive keyboard customization. This guide covers all available keybind actions.
Keybind Basics
Keybinds are defined in keybinds.toml:
[keybinds."ctrl+l"]
action = "layout_editor"
[keybinds."f1"]
action = "help"
[keybinds."ctrl+1"]
macro = "attack target"
Key Format
Modifiers
| Modifier | Format |
|---|---|
| Control | ctrl+ |
| Alt | alt+ |
| Shift | shift+ |
| Meta/Super | meta+ |
Special Keys
| Key | Format |
|---|---|
| F1-F12 | f1 to f12 |
| Enter | enter |
| Escape | escape |
| Tab | tab |
| Backspace | backspace |
| Delete | delete |
| Insert | insert |
| Home | home |
| End | end |
| Page Up | pageup |
| Page Down | pagedown |
| Arrows | up, down, left, right |
| Space | space |
Combination Examples
"ctrl+s" # Ctrl + S
"ctrl+shift+s" # Ctrl + Shift + S
"alt+f4" # Alt + F4
"f1" # F1 alone
"shift+tab" # Shift + Tab
Action Categories
Navigation Actions
| Action | Description |
|---|---|
scroll_up | Scroll window up |
scroll_down | Scroll window down |
page_up | Page up |
page_down | Page down |
scroll_top | Jump to top |
scroll_bottom | Jump to bottom |
focus_next | Focus next window |
focus_prev | Focus previous window |
focus_main | Focus main window |
focus_input | Focus command input |
Window Actions
| Action | Description |
|---|---|
layout_editor | Open layout editor |
window_editor | Open window editor |
toggle_border | Toggle window border |
toggle_title | Toggle window title |
maximize_window | Maximize current window |
restore_window | Restore window size |
close_window | Close current popup |
Editor Actions
| Action | Description |
|---|---|
highlight_browser | Open highlight browser |
keybind_browser | Open keybind browser |
color_browser | Open color browser |
theme_browser | Open theme browser |
settings_editor | Open settings |
Input Actions
| Action | Description |
|---|---|
history_prev | Previous command history |
history_next | Next command history |
clear_input | Clear command input |
submit_command | Submit current command |
cancel | Cancel current operation |
Text Actions
| Action | Description |
|---|---|
select_all | Select all text |
copy | Copy selection |
cut | Cut selection |
paste | Paste clipboard |
search | Open search |
search_next | Find next |
search_prev | Find previous |
Tab Actions
| Action | Description |
|---|---|
next_tab | Next tab |
prev_tab | Previous tab |
tab_1 to tab_9 | Jump to tab by number |
close_tab | Close current tab |
Application Actions
| Action | Description |
|---|---|
quit | Exit application |
help | Show help |
reload_config | Reload all config |
reload_colors | Reload colors |
reload_highlights | Reload highlights |
reload_keybinds | Reload keybinds |
reload_layout | Reload layout |
Game Actions
| Action | Description |
|---|---|
reconnect | Reconnect to server |
disconnect | Disconnect |
Macros
Send game commands:
[keybinds."ctrl+1"]
macro = "attack target"
[keybinds."ctrl+2"]
macro = "stance defensive"
[keybinds."ctrl+h"]
macro = "hide"
Multi-Command Macros
Separate with semicolons:
[keybinds."f5"]
macro = "stance offensive;attack target"
Macro with Variables
Use $input for prompted input:
[keybinds."ctrl+g"]
macro = "go $input"
# Prompts for input, then sends "go <input>"
Default Keybinds
Global
| Key | Action |
|---|---|
Escape | Cancel/close popup |
Ctrl+C | Copy |
Ctrl+V | Paste |
Ctrl+X | Cut |
Ctrl+A | Select all |
Navigation
| Key | Action |
|---|---|
Page Up | Scroll up |
Page Down | Scroll down |
Home | Scroll to top |
End | Scroll to bottom |
Tab | Focus next |
Shift+Tab | Focus prev |
Command Input
| Key | Action |
|---|---|
Enter | Submit command |
Up | History prev |
Down | History next |
Ctrl+L | Clear input |
Browsers
| Key | Action |
|---|---|
Up/k | Navigate up |
Down/j | Navigate down |
Enter | Select |
Delete | Delete item |
Escape | Close |
Context-Specific Keybinds
Some keybinds only work in specific contexts:
# Only in Normal mode
[keybinds."ctrl+l"]
action = "layout_editor"
mode = "normal"
# Only in Navigation mode
[keybinds."j"]
action = "scroll_down"
mode = "navigation"
Priority Layers
Keybinds are processed in order:
- Global keybinds - Always active
- Menu keybinds - In popup/editor
- User keybinds - Game mode
- Default input - Character insertion
Examples
Combat Setup
# Quick attacks
[keybinds."f1"]
macro = "attack target"
[keybinds."f2"]
macro = "stance offensive;attack target"
[keybinds."f3"]
macro = "stance defensive"
[keybinds."f4"]
macro = "hide"
# Movement
[keybinds."numpad8"]
macro = "go north"
[keybinds."numpad2"]
macro = "go south"
[keybinds."numpad4"]
macro = "go west"
[keybinds."numpad6"]
macro = "go east"
Spellcasting
[keybinds."ctrl+1"]
macro = "prep 101;cast"
[keybinds."ctrl+2"]
macro = "prep 103;cast"
[keybinds."ctrl+3"]
macro = "prep 107;cast target"
Navigation
[keybinds."ctrl+shift+up"]
action = "scroll_top"
[keybinds."ctrl+shift+down"]
action = "scroll_bottom"
[keybinds."alt+1"]
action = "focus_main"
[keybinds."alt+2"]
macro = ".focus combat"
Quick Commands
[keybinds."ctrl+l"]
macro = "look"
[keybinds."ctrl+i"]
macro = "inventory"
[keybinds."ctrl+e"]
macro = "experience"
[keybinds."ctrl+w"]
macro = "wealth"
Using the Keybind Browser
.keybinds
Features:
- Browse all keybinds
- Add new keybinds
- Edit existing keybinds
- Delete keybinds
- Filter by type
Conflict Resolution
If two keybinds use the same key:
- Higher priority wins
- Later definition overwrites earlier
- Context-specific beats general
Check for conflicts:
.keybinds
Look for duplicate key combos.
Troubleshooting
Keybind Not Working
- Check for conflicts
- Verify key format is correct
- Check context/mode setting
- Reload:
.reload keybinds
Key Not Detected
Some keys may not be captured:
- System shortcuts (Alt+Tab, etc.)
- Terminal shortcuts
- Media keys
Modifier Issues
Some terminals don’t report all modifiers:
- Try different modifier combos
- Use function keys (F1-F12) as alternatives
See Also
- Keybinds Configuration - Full reference
- Browser Editors - Keybind browser
- Command Input - Input widget
Sound Alerts
Two-Face can play sounds when specific text patterns match, providing audio alerts for important events.
Sound Basics
Add sounds to highlight patterns:
[[highlights]]
name = "whisper_alert"
pattern = "whispers to you"
fg = "#00FFFF"
sound = "~/.two-face/sounds/whisper.wav"
Supported Formats
| Format | Extension | Notes |
|---|---|---|
| WAV | .wav | Best compatibility |
| MP3 | .mp3 | Requires codec |
| OGG | .ogg | Good compression |
WAV is recommended for reliable playback.
Sound Configuration
Basic Sound
[[highlights]]
name = "my_alert"
pattern = "important event"
sound = "/path/to/sound.wav"
Volume Control
[[highlights]]
name = "quiet_alert"
pattern = "minor event"
sound = "/path/to/sound.wav"
volume = 0.5 # 0.0 to 1.0
Sound-Only (No Visual)
[[highlights]]
name = "audio_only"
pattern = "background event"
sound = "/path/to/sound.wav"
# No fg/bg colors - visual unchanged
Sound File Management
Directory Structure
~/.two-face/
└── sounds/
├── whisper.wav
├── attack.wav
├── death.wav
├── loot.wav
└── custom/
└── my_sounds.wav
Creating the Sounds Directory
mkdir -p ~/.two-face/sounds
Sound Sources
- System sounds - Copy from OS sound themes
- Online libraries - freesound.org, soundsnap.com
- Create your own - Record with Audacity
- Game packs - MUD sound packs
Common Alert Sounds
Combat Alerts
# Being attacked
[[highlights]]
name = "attack_received"
pattern = "(strikes|hits|bites|claws) you"
fg = "#FF4444"
sound = "~/.two-face/sounds/hit.wav"
# Near death
[[highlights]]
name = "low_health"
pattern = "You feel weak"
fg = "#FF0000"
bg = "#400000"
sound = "~/.two-face/sounds/warning.wav"
volume = 1.0
# Death
[[highlights]]
name = "death"
pattern = "You have died"
fg = "#FFFFFF"
bg = "#FF0000"
sound = "~/.two-face/sounds/death.wav"
Social Alerts
# Whispers
[[highlights]]
name = "whisper"
pattern = "whispers,"
fg = "#00FFFF"
sound = "~/.two-face/sounds/whisper.wav"
volume = 0.7
# Your name mentioned
[[highlights]]
name = "name_mention"
pattern = "\\bYourCharacter\\b"
fg = "#FFFF00"
sound = "~/.two-face/sounds/mention.wav"
# Group invite
[[highlights]]
name = "group_invite"
pattern = "invites you to join"
fg = "#00FF00"
sound = "~/.two-face/sounds/invite.wav"
Loot Alerts
# Treasure found
[[highlights]]
name = "treasure"
pattern = "(?i)(gold|gems|treasure|chest)"
fg = "#FFD700"
sound = "~/.two-face/sounds/loot.wav"
volume = 0.5
# Rare item
[[highlights]]
name = "rare_item"
pattern = "(?i)(legendary|artifact|ancient)"
fg = "#FF00FF"
sound = "~/.two-face/sounds/rare.wav"
Status Alerts
# Stunned
[[highlights]]
name = "stunned"
pattern = "You are stunned"
fg = "#FFFF00"
sound = "~/.two-face/sounds/stun.wav"
# Poisoned
[[highlights]]
name = "poisoned"
pattern = "(?i)poison|venom"
fg = "#00FF00"
sound = "~/.two-face/sounds/poison.wav"
Global Sound Settings
In config.toml
[sound]
enabled = true # Master switch
volume = 0.8 # Master volume (0.0-1.0)
concurrent_limit = 3 # Max simultaneous sounds
Disable All Sounds
[sound]
enabled = false
Sound Priority
When multiple patterns match:
[[highlights]]
name = "critical_alert"
pattern = "critical"
sound = "~/.two-face/sounds/critical.wav"
priority = 100 # Plays first
[[highlights]]
name = "minor_alert"
pattern = "hit"
sound = "~/.two-face/sounds/hit.wav"
priority = 50 # May be skipped if concurrent_limit reached
Rate Limiting
Prevent sound spam:
[[highlights]]
name = "combat_sound"
pattern = "You attack"
sound = "~/.two-face/sounds/attack.wav"
sound_cooldown = 500 # Milliseconds between plays
Platform Notes
Windows
- WAV files play natively
- No additional dependencies needed
Linux
Requires audio system:
# PulseAudio (most distros)
pactl list short sinks
# ALSA
aplay -l
macOS
- WAV files play natively
- Uses Core Audio
Troubleshooting
No Sound
- Check
[sound] enabled = true - Verify file path is correct
- Test file with system player
- Check volume settings
Delayed Sound
- Use WAV instead of MP3
- Reduce file size
- Check system audio latency
Wrong Sound
- Check pattern matching correctly
- Verify highlight priority
- Check for pattern conflicts
Creating Custom Sounds
Using Audacity
- Open Audacity
- Record or import audio
- Trim to desired length (0.5-2 seconds recommended)
- Export as WAV (16-bit PCM)
- Save to
~/.two-face/sounds/
Sound Guidelines
- Duration: 0.5-2 seconds
- Format: WAV 16-bit PCM
- Sample rate: 44100 Hz
- Channels: Mono or Stereo
- File size: Keep under 500KB
Sound Packs
Creating a Sound Pack
- Create themed sounds
- Package in zip/tar
- Include README with highlight configs
- Share with community
Installing a Sound Pack
- Extract to
~/.two-face/sounds/ - Add highlights from pack’s config
- Run
.reload highlights
See Also
- Highlights Configuration - Pattern configuration
- Highlight Patterns - Pattern syntax
- TTS Setup - Text-to-speech alternative
TTS Setup
Two-Face supports text-to-speech (TTS) for accessibility and hands-free notifications.
Overview
TTS features:
- Read game text aloud
- Announce specific events
- Screen reader compatibility
- Multiple voice options
Configuration
Basic Setup
In config.toml:
[tts]
enabled = true
voice = "default"
rate = 1.0 # Speaking rate (0.5-2.0)
volume = 1.0 # Volume (0.0-1.0)
Voice Selection
[tts]
voice = "default" # System default
# Or specific voice name from your system
voice = "Microsoft David" # Windows
voice = "Alex" # macOS
voice = "espeak" # Linux
Stream Configuration
Control which streams are spoken:
[tts]
enabled = true
streams = ["main", "speech", "thoughts"] # Streams to read
# Or exclude specific streams
exclude_streams = ["combat", "logons"]
Stream Options
| Stream | Description |
|---|---|
main | Main game output |
speech | Player dialogue |
thoughts | ESP/telepathy |
combat | Combat messages |
death | Death notices |
logons | Login/logout |
familiar | Familiar messages |
Pattern-Based TTS
Speak only when patterns match:
[[tts_patterns]]
name = "whisper_announce"
pattern = "whispers to you"
message = "You received a whisper"
[[tts_patterns]]
name = "death_announce"
pattern = "You have died"
message = "Warning: You have died"
[[tts_patterns]]
name = "stun_announce"
pattern = "You are stunned"
speak_match = true # Speak the matched text
Pattern Properties
[[tts_patterns]]
name = "my_pattern"
pattern = "regex pattern"
# What to speak:
message = "Custom message" # Speak this text
speak_match = true # Speak matched text
speak_line = true # Speak entire line
# Control:
priority = 100 # Higher = more important
rate = 1.5 # Override speaking rate
volume = 1.0 # Override volume
Platform Setup
Windows
Windows has built-in TTS (SAPI):
- TTS works out of the box
- Configure voices in Windows Settings → Time & Language → Speech
- Install additional voices from Microsoft Store
Available voices:
- Microsoft David (Male)
- Microsoft Zira (Female)
- Additional language packs
[tts]
enabled = true
voice = "Microsoft David"
macOS
macOS uses built-in speech synthesis:
- TTS works out of the box
- Configure in System Preferences → Accessibility → Spoken Content
- Download additional voices
Available voices:
- Alex (Male)
- Samantha (Female)
- Many international voices
[tts]
enabled = true
voice = "Alex"
Linux
Linux requires a TTS engine:
eSpeak
# Debian/Ubuntu
sudo apt install espeak
# Fedora
sudo dnf install espeak
# Arch
sudo pacman -S espeak
[tts]
enabled = true
engine = "espeak"
voice = "en"
Festival
# Debian/Ubuntu
sudo apt install festival
# Configure
sudo apt install festvox-kallpc16k
[tts]
enabled = true
engine = "festival"
Speech Dispatcher
# Debian/Ubuntu
sudo apt install speech-dispatcher
spd-say "test"
[tts]
enabled = true
engine = "speechd"
Rate and Pitch
Speaking Rate
[tts]
rate = 1.0 # Normal speed
rate = 0.5 # Half speed (slower)
rate = 2.0 # Double speed (faster)
Pitch (if supported)
[tts]
pitch = 1.0 # Normal pitch
pitch = 0.8 # Lower pitch
pitch = 1.2 # Higher pitch
Accessibility Features
Screen Reader Compatibility
Two-Face works with screen readers:
- NVDA (Windows)
- JAWS (Windows)
- VoiceOver (macOS)
- Orca (Linux)
[accessibility]
screen_reader_mode = true
announce_focus_changes = true
announce_new_content = true
High Contrast Mode
Combine with TTS:
[ui]
high_contrast = true
[tts]
enabled = true
announce_colors = false # Don't read color codes
TTS Commands
Toggle TTS
.tts on
.tts off
.tts toggle
Change Rate
.tts rate 1.5
Change Volume
.tts volume 0.8
Test TTS
.tts test "This is a test message"
List Voices
.tts voices
Common Patterns
Combat Awareness
[[tts_patterns]]
name = "attack"
pattern = "(strikes|hits|bites) you"
message = "Under attack"
priority = 90
[[tts_patterns]]
name = "stunned"
pattern = "You are stunned"
message = "Stunned"
priority = 100
[[tts_patterns]]
name = "roundtime"
pattern = "Roundtime: (\\d+)"
message = "Roundtime"
Social Notifications
[[tts_patterns]]
name = "whisper"
pattern = "(\\w+) whispers,"
speak_match = true
[[tts_patterns]]
name = "name_mention"
pattern = "\\bYourCharacter\\b"
message = "Someone mentioned you"
Navigation
[[tts_patterns]]
name = "room_name"
pattern = "^\\[.*?\\]$"
speak_line = true
[[tts_patterns]]
name = "exits"
pattern = "Obvious (paths|exits):"
speak_line = true
Interruption
Control how new speech interrupts current:
[tts]
interrupt_mode = "queue" # Wait for current to finish
interrupt_mode = "interrupt" # Stop current, start new
interrupt_mode = "priority" # Interrupt only if higher priority
Troubleshooting
No Sound
- Check
[tts] enabled = true - Verify system TTS works:
- Windows:
PowerShell: Add-Type -AssemblyName System.Speech; (New-Object System.Speech.Synthesis.SpeechSynthesizer).Speak("test") - macOS:
say "test" - Linux:
espeak "test"
- Windows:
- Check volume settings
Wrong Voice
- List available voices:
.tts voices - Set correct voice name exactly
- Voice names are case-sensitive on some systems
Too Fast/Slow
Adjust rate:
[tts]
rate = 0.8 # Slower
rate = 1.2 # Faster
Cuts Off
Increase buffer or reduce interrupt:
[tts]
interrupt_mode = "queue"
See Also
- Sound Alerts - Audio alerts
- Accessibility Tutorial - Full accessibility setup
- Configuration - Full config reference
Automation Overview
Two-Face provides automation features to streamline repetitive tasks and enhance gameplay efficiency.
Automation Features
| Feature | Description | Use Case |
|---|---|---|
| Cmdlists | Context menu commands | Right-click actions |
| Macros | Multi-command sequences | Complex actions |
| Triggers | Event-based responses | Automatic reactions |
| Scripting | Advanced automation | Custom logic |
Quick Start
Simple Macro
# In keybinds.toml
[keybinds."f1"]
macro = "attack target"
[keybinds."f2"]
macro = "stance defensive;hide"
Context Menu (Cmdlist)
# In cmdlist.toml
[[cmdlist]]
noun = "sword"
commands = ["look", "get", "drop", "put in backpack"]
Trigger
# In triggers.toml
[[triggers]]
pattern = "You are stunned"
command = ".say Stunned!"
Automation Philosophy
Enhancement, Not Replacement
Two-Face automation is designed to:
- Reduce tedium - Automate repetitive tasks
- Speed up common actions - Quick access to frequent commands
- Provide alerts - Notify of important events
Not Designed For
- Fully automated hunting
- AFK gameplay
- Botting
Automation Levels
Level 1: Keybind Macros
Simple command sequences bound to keys:
[keybinds."ctrl+1"]
macro = "prep 101;cast"
Best for: Frequent, simple commands
Level 2: Context Menus
Right-click commands for objects:
[[cmdlist]]
noun = "creature"
commands = ["attack", "look", "assess"]
Best for: Object-specific actions
Level 3: Triggers
Automatic responses to game events:
[[triggers]]
pattern = "roundtime"
command = ".notify"
Best for: Status monitoring, alerts
Level 4: Scripts (Future)
Complex conditional logic:
-- Future scripting API
on_event("stun", function()
if health() < 50 then
send("flee")
end
end)
Best for: Complex decision-making
Safety Guidelines
Avoid Detection Issues
- Don’t automate too quickly
- Add reasonable delays
- Keep human-like timing
- Don’t automate core gameplay
Respect Game Rules
Check game policies on automation:
- Some automation is allowed
- Unattended automation may not be
- When in doubt, don’t automate
Test Safely
Test automation in safe areas:
- Town squares
- Non-combat zones
- With backup plans
Automation Guides
This section covers:
- Cmdlists - Context menu system
- Macros - Multi-command keybinds
- Triggers - Event-based automation
- Scripting - Advanced automation (future)
Common Automation Tasks
Combat Efficiency
- Quick spell casting (macros)
- Target selection (cmdlist)
- Status alerts (triggers)
Inventory Management
- Quick get/drop commands
- Container management
- Sorting actions
Navigation
- Direction macros
- Room scanning
- Exit shortcuts
Social
- Quick emotes
- Response macros
- Greeting templates
See Also
- Keybind Actions - All keybind options
- Highlight Patterns - Pattern matching
- Sound Alerts - Audio notifications
Command Lists (Cmdlists)
Command lists provide context menu actions for clickable objects in the game.
Overview
When you right-click on a game object (creature, item, player), Two-Face shows a context menu with relevant commands.
┌─────────────────┐
│ a rusty sword │
├─────────────────┤
│ Look │
│ Get │
│ Drop │
│ Put in backpack │
│ Appraise │
└─────────────────┘
Configuration
Basic Cmdlist
In cmdlist.toml:
[[cmdlist]]
noun = "sword"
commands = ["look", "get", "drop", "appraise"]
Cmdlist Properties
[[cmdlist]]
noun = "creature" # Object noun to match
commands = [ # Available commands
"attack",
"look",
"assess"
]
priority = 100 # Higher = appears first
category = "combat" # Optional grouping
Noun Matching
Exact Match
[[cmdlist]]
noun = "sword"
commands = ["look", "get"]
Matches: “sword”, not “longsword”
Partial Match
[[cmdlist]]
noun = "sword"
match_mode = "contains"
commands = ["look", "get"]
Matches: “sword”, “longsword”, “shortsword”
Pattern Match
[[cmdlist]]
noun = "(?i).*sword.*"
match_mode = "regex"
commands = ["look", "get"]
Matches any noun containing “sword” (case-insensitive)
Command Syntax
Simple Command
commands = ["look", "get", "drop"]
# Results in: look {noun}, get {noun}, drop {noun}
Custom Command Format
commands = [
"look",
"get",
"put in backpack:put {noun} in my backpack",
"give to:give {noun} to {target}"
]
Format: label:command or just command
Variables
| Variable | Description |
|---|---|
{noun} | Object noun |
{exist} | Object ID |
{target} | Prompted target |
{input} | Prompted input |
Categories
Default Categories
# Creature commands
[[cmdlist]]
category = "creature"
noun = ".*"
match_mode = "regex"
commands = ["attack", "look", "assess"]
priority = 10
# Container commands
[[cmdlist]]
category = "container"
noun = "(?i)(backpack|bag|pouch|sack)"
match_mode = "regex"
commands = ["look in", "open", "close"]
priority = 20
Custom Categories
[[cmdlist]]
category = "alchemy"
noun = "(?i)(vial|potion|herb)"
match_mode = "regex"
commands = [
"look",
"get",
"drink:drink my {noun}",
"mix:mix my {noun}"
]
Common Cmdlists
Creatures
[[cmdlist]]
category = "combat"
noun = ".*" # Default for all creatures
match_mode = "regex"
commands = [
"attack",
"attack left",
"attack right",
"look",
"assess"
]
priority = 10
Weapons
[[cmdlist]]
category = "weapon"
noun = "(?i)(sword|dagger|axe|mace|staff)"
match_mode = "regex"
commands = [
"look",
"get",
"drop",
"wield",
"sheath",
"appraise"
]
Containers
[[cmdlist]]
category = "container"
noun = "(?i)(backpack|bag|pouch|cloak|sack)"
match_mode = "regex"
commands = [
"look in",
"open",
"close",
"get from:get {input} from my {noun}"
]
Players
[[cmdlist]]
category = "player"
noun = "^[A-Z][a-z]+$" # Capitalized names
match_mode = "regex"
commands = [
"look",
"smile",
"bow",
"whisper:whisper {noun} {input}",
"give:give {input} to {noun}"
]
Herbs/Alchemy
[[cmdlist]]
category = "herb"
noun = "(?i)(acantha|ambrominas|basal|cactacae)"
match_mode = "regex"
commands = [
"look",
"get",
"eat",
"put in:put my {noun} in my {input}"
]
Gems/Treasure
[[cmdlist]]
category = "treasure"
noun = "(?i)(gem|jewel|coin|gold|silver)"
match_mode = "regex"
commands = [
"look",
"get",
"appraise",
"sell"
]
Menu Separators
Add visual separators:
[[cmdlist]]
noun = "sword"
commands = [
"look",
"get",
"---", # Separator
"wield",
"sheath",
"---",
"appraise",
"sell"
]
Submenus
Create nested menus:
[[cmdlist]]
noun = "sword"
commands = [
"look",
"get",
"Combat>attack,attack left,attack right",
"Container>put in backpack:put {noun} in my backpack,put in sack:put {noun} in my sack"
]
Priority System
Higher priority cmdlists appear first:
# Specific override
[[cmdlist]]
noun = "magic sword"
commands = ["invoke", "look", "get"]
priority = 100 # High priority
# General fallback
[[cmdlist]]
noun = "(?i).*sword.*"
match_mode = "regex"
commands = ["look", "get"]
priority = 10 # Lower priority
Game Object Links
Two-Face extracts object data from game XML:
<a exist="12345" noun="sword">a rusty sword</a>
exist: Object ID for direct commandsnoun: Used for cmdlist matching- Text: Display name
Direct Commands
Use object ID for precise targeting:
[[cmdlist]]
noun = "sword"
commands = [
"look",
"_inspect {exist}", # Uses object ID
"get #{exist}", # Direct reference
]
Testing Cmdlists
- Save
cmdlist.toml - Run
.reload cmdlist - Right-click on matching object
- Verify menu appears correctly
Troubleshooting
Menu Not Appearing
- Check noun matches object exactly
- Try
match_mode = "contains" - Verify cmdlist.toml syntax
- Run
.reload cmdlist
Wrong Commands
- Check priority order
- Verify pattern matching
- Test with exact noun match first
Variables Not Replaced
- Check variable syntax
{noun},{exist} - Ensure object has required data
- Check for typos in variable names
Complete Example
# cmdlist.toml
# Combat creatures
[[cmdlist]]
category = "combat"
noun = ".*"
match_mode = "regex"
commands = [
"attack",
"attack left",
"attack right",
"---",
"look",
"assess"
]
priority = 5
# Weapons
[[cmdlist]]
category = "weapon"
noun = "(?i)(sword|dagger|axe|mace|staff|bow)"
match_mode = "regex"
commands = [
"look",
"get",
"---",
"wield",
"sheath",
"---",
"appraise"
]
priority = 20
# Containers
[[cmdlist]]
category = "container"
noun = "(?i)(backpack|bag|pouch|cloak|sack|chest)"
match_mode = "regex"
commands = [
"look in",
"open",
"close",
"---",
"get from:get {input} from my {noun}"
]
priority = 20
# Currency
[[cmdlist]]
category = "currency"
noun = "(?i)(silver|gold|coins?)"
match_mode = "regex"
commands = [
"look",
"get",
"count"
]
priority = 15
# Players
[[cmdlist]]
category = "player"
noun = "^[A-Z][a-z]+$"
match_mode = "regex"
commands = [
"look",
"---",
"smile",
"wave",
"bow",
"---",
"whisper:whisper {noun} {input}",
"give:give {input} to {noun}"
]
priority = 25
See Also
- Macros - Keybind automation
- Keybind Actions - Key commands
- Command Input - Command entry
Macros
Macros allow you to bind multiple commands to a single key press.
Overview
Macros send game commands when you press a key:
# In keybinds.toml
[keybinds."f1"]
macro = "attack target"
[keybinds."ctrl+1"]
macro = "prep 101;cast"
Basic Macros
Single Command
[keybinds."f1"]
macro = "attack target"
Press F1 → Sends “attack target” to game
Multiple Commands
Separate commands with semicolons:
[keybinds."f2"]
macro = "stance offensive;attack target"
Press F2 → Sends both commands in sequence
Command Sequences
Combat Sequence
[keybinds."f5"]
macro = "stance offensive;attack target;stance defensive"
Spell Casting
[keybinds."ctrl+1"]
macro = "prep 101;cast"
[keybinds."ctrl+2"]
macro = "prep 103;cast target"
[keybinds."ctrl+3"]
macro = "incant 107"
Movement
[keybinds."numpad8"]
macro = "go north"
[keybinds."numpad2"]
macro = "go south"
[keybinds."numpad4"]
macro = "go west"
[keybinds."numpad6"]
macro = "go east"
[keybinds."numpad5"]
macro = "out"
Delays
Add delays between commands (in milliseconds):
[keybinds."f5"]
macro = "prep 101;{500};cast"
# Wait 500ms between prep and cast
Delay Syntax
macro = "command1;{1000};command2"
# {1000} = 1 second delay
Example: Careful Spellcasting
[keybinds."f6"]
macro = "prep 901;{2000};cast target"
# Prep spell, wait 2 seconds for mana, cast
Variables
Input Prompt
[keybinds."ctrl+g"]
macro = "go $input"
Press Ctrl+G → Prompts for direction → Sends “go ”
Target Variable
[keybinds."f1"]
macro = "attack $target"
Uses current target if set, or prompts.
Last Target
[keybinds."f2"]
macro = "attack $lasttarget"
Attacks the last targeted creature.
Conditional Macros
With Input Check
[keybinds."f5"]
macro = "$input:prep $input;cast"
# Only executes if input provided
Multiple Inputs
[keybinds."f6"]
macro = "give $input1 to $input2"
# Prompts for two inputs
Common Macro Patterns
Quick Attacks
[keybinds."f1"]
macro = "attack target"
[keybinds."f2"]
macro = "attack left target"
[keybinds."f3"]
macro = "attack right target"
[keybinds."f4"]
macro = "feint target"
Defensive Moves
[keybinds."f5"]
macro = "stance defensive"
[keybinds."f6"]
macro = "hide"
[keybinds."f7"]
macro = "evade"
Stance Cycling
[keybinds."ctrl+up"]
macro = "stance offensive"
[keybinds."ctrl+down"]
macro = "stance defensive"
[keybinds."ctrl+left"]
macro = "stance neutral"
Looting
[keybinds."ctrl+l"]
macro = "search;loot"
[keybinds."ctrl+shift+l"]
macro = "search;loot;skin"
Information
[keybinds."i"]
macro = "inventory"
[keybinds."ctrl+i"]
macro = "inventory full"
[keybinds."ctrl+e"]
macro = "experience"
[keybinds."ctrl+w"]
macro = "wealth"
Healing
# Empath healing
[keybinds."ctrl+h"]
macro = "transfer $target"
# Herb usage
[keybinds."ctrl+1"]
macro = "get acantha from my herb pouch;eat my acantha"
[keybinds."ctrl+2"]
macro = "get basal from my herb pouch;eat my basal"
Social
[keybinds."ctrl+s"]
macro = "smile"
[keybinds."ctrl+b"]
macro = "bow"
[keybinds."ctrl+w"]
macro = "wave"
[keybinds."ctrl+g"]
macro = "greet $target"
Macro Organization
By Activity
# === COMBAT ===
[keybinds."f1"]
macro = "attack target"
[keybinds."f2"]
macro = "stance defensive"
# === SPELLS ===
[keybinds."ctrl+1"]
macro = "prep 101;cast"
# === MOVEMENT ===
[keybinds."numpad8"]
macro = "go north"
# === SOCIAL ===
[keybinds."ctrl+s"]
macro = "smile"
By Profession
Create character-specific keybinds:
~/.two-face/characters/Warrior/keybinds.toml
~/.two-face/characters/Wizard/keybinds.toml
Advanced Macros
Complex Combat Routine
[keybinds."f10"]
macro = "stance offensive;{100};attack target;{500};stance defensive"
Merchant Interaction
[keybinds."ctrl+m"]
macro = "order $input;buy"
Container Management
[keybinds."ctrl+p"]
macro = "put $input in my backpack"
[keybinds."ctrl+shift+p"]
macro = "get $input from my backpack"
Troubleshooting
Macro Not Executing
- Check key format is correct
- Verify keybinds.toml syntax
- Run
.reload keybinds - Check for key conflicts
Commands Out of Order
Add delays:
macro = "command1;{500};command2"
Input Prompt Not Appearing
- Check
$inputsyntax - Verify focus is on game
- Check for conflicting keybinds
Multiple Commands Failing
Some games have command limits:
- Add delays between commands
- Break into separate macros
- Check game’s command queue limit
Best Practices
- Keep macros simple - Complex macros are harder to debug
- Add delays when needed - Prevents command flooding
- Test in safe areas - Verify before combat use
- Document your macros - Add comments to keybinds.toml
- Use consistent keys - Group related macros logically
See Also
- Keybind Actions - All keybind options
- Keybinds Configuration - Full reference
- Cmdlists - Context menu commands
Triggers
Triggers automatically execute commands when specific text patterns appear in the game.
Overview
Triggers watch for patterns and respond:
# In triggers.toml
[[triggers]]
pattern = "You are stunned"
command = ".say Stunned!"
[[triggers]]
pattern = "falls dead"
command = "search;loot"
Basic Triggers
Simple Trigger
[[triggers]]
name = "stun_alert"
pattern = "You are stunned"
command = ".notify Stunned!"
Multiple Commands
[[triggers]]
name = "death_loot"
pattern = "falls dead"
command = "search;loot"
Trigger Properties
[[triggers]]
name = "my_trigger" # Unique name
pattern = "regex pattern" # Pattern to match
command = "game command" # Command to execute
# Optional properties
enabled = true # Enable/disable
priority = 100 # Higher = checked first
stream = "main" # Limit to specific stream
cooldown = 1000 # Minimum ms between triggers
category = "combat" # Grouping
Pattern Syntax
Triggers use regex patterns:
Literal Text
pattern = "You are stunned"
Case Insensitive
pattern = "(?i)you are stunned"
Word Boundaries
pattern = "\\bstunned\\b"
Capture Groups
pattern = "(\\w+) falls dead"
command = "search #{noun}"
Multiple Patterns
pattern = "(stun|web|prone)"
Command Options
Game Commands
command = "attack target"
Client Commands
command = ".notify Alert!" # Two-Face command
command = ".sound alert.wav" # Play sound
command = ".tts Danger!" # Text-to-speech
Multiple Commands
command = "search;loot;skin"
With Delay
command = "prep 101;{500};cast"
Captured Variables
Use captured groups in commands:
[[triggers]]
pattern = "(\\w+) whispers,"
command = ".notify Whisper from $1"
# $1 = captured name
Variable Reference
| Variable | Description |
|---|---|
$0 | Entire match |
$1 | First capture group |
$2 | Second capture group |
$n | Nth capture group |
Stream Filtering
Limit triggers to specific streams:
[[triggers]]
pattern = "ESP"
command = ".notify ESP received"
stream = "thoughts"
[[triggers]]
pattern = "attack"
command = ".beep"
stream = "combat"
Cooldowns
Prevent rapid-fire triggers:
[[triggers]]
pattern = "You are hit"
command = ".sound hit.wav"
cooldown = 500 # Only trigger once per 500ms
Common Triggers
Combat Alerts
# Stunned
[[triggers]]
name = "stun_alert"
pattern = "You are stunned"
command = ".notify Stunned!"
category = "combat"
# Webbed
[[triggers]]
name = "web_alert"
pattern = "webs stick to you"
command = ".notify Webbed!"
category = "combat"
# Low health
[[triggers]]
name = "low_health"
pattern = "You feel weak"
command = ".notify Low Health!;stance defensive"
category = "combat"
priority = 100
Social Alerts
# Whispers
[[triggers]]
name = "whisper"
pattern = "(\\w+) whispers,"
command = ".notify Whisper from $1"
category = "social"
# Name mentioned
[[triggers]]
name = "name_mention"
pattern = "\\bYourCharacter\\b"
command = ".sound mention.wav"
category = "social"
Loot Triggers
# Auto-search on kill
[[triggers]]
name = "auto_search"
pattern = "falls dead"
command = "search"
cooldown = 1000
category = "loot"
# Treasure alert
[[triggers]]
name = "treasure"
pattern = "(?i)treasure|gold|gems"
command = ".notify Treasure!"
category = "loot"
Status Triggers
# Roundtime tracker
[[triggers]]
name = "roundtime"
pattern = "Roundtime: (\\d+)"
command = ".countdown $1"
category = "status"
# Hidden status
[[triggers]]
name = "hidden"
pattern = "You melt into the shadows"
command = ".notify Hidden"
category = "status"
Priority System
Higher priority triggers are checked first:
[[triggers]]
name = "critical_danger"
pattern = "dragon"
command = "flee"
priority = 100 # Highest priority
[[triggers]]
name = "general_alert"
pattern = "attacks you"
command = ".beep"
priority = 10 # Lower priority
Conditional Triggers
With Capture Check
[[triggers]]
pattern = "(\\w+) says, \"(.+)\""
command = ".log $1: $2"
# Only triggers if both captures match
Negative Lookahead
pattern = "(?!You )\\w+ attacks"
# Matches "Goblin attacks" but not "You attack"
Enable/Disable
In Configuration
[[triggers]]
name = "my_trigger"
pattern = "something"
command = "respond"
enabled = false # Disabled by default
Via Commands
.trigger enable my_trigger
.trigger disable my_trigger
.trigger toggle my_trigger
Trigger Groups
.trigger enable combat # Enable all combat triggers
.trigger disable loot # Disable all loot triggers
Testing Triggers
- Add trigger to
triggers.toml - Run
.reload triggers - Look for matching text in game
- Verify trigger fires
Debug Mode
.trigger debug on
Shows when triggers match.
Safety Considerations
Avoid Automation Abuse
- Don’t automate core gameplay
- Keep human-like delays
- Don’t trigger in loops
- Use cooldowns
Test Thoroughly
- Test in safe areas
- Verify pattern accuracy
- Check for false positives
- Monitor for issues
Complete Example
# triggers.toml
# === COMBAT ALERTS ===
[[triggers]]
name = "stun_alert"
pattern = "(?i)you are stunned"
command = ".notify Stunned!;.sound stun.wav"
category = "combat"
priority = 100
cooldown = 1000
[[triggers]]
name = "web_alert"
pattern = "(?i)webs? (stick|entangle)"
command = ".notify Webbed!"
category = "combat"
priority = 90
[[triggers]]
name = "prone_alert"
pattern = "(?i)knock.*down|fall.*prone"
command = ".notify Prone!"
category = "combat"
priority = 90
# === HEALTH ALERTS ===
[[triggers]]
name = "low_health"
pattern = "(?i)feel (weak|faint|dizzy)"
command = ".notify Low Health!;.sound warning.wav"
category = "health"
priority = 100
[[triggers]]
name = "death"
pattern = "You have died"
command = ".notify DEAD!;.sound death.wav"
category = "health"
priority = 100
# === SOCIAL ===
[[triggers]]
name = "whisper"
pattern = "(\\w+) whispers,"
command = ".notify Whisper from $1;.sound whisper.wav"
category = "social"
stream = "main"
[[triggers]]
name = "name_mention"
pattern = "\\bYourCharacter\\b"
command = ".sound mention.wav"
category = "social"
cooldown = 5000
# === LOOT ===
[[triggers]]
name = "auto_search"
pattern = "falls dead"
command = "search"
category = "loot"
cooldown = 2000
enabled = false # Disabled by default
# === STATUS ===
[[triggers]]
name = "roundtime"
pattern = "Roundtime: (\\d+)"
command = ".rt $1"
category = "status"
See Also
- Highlight Patterns - Pattern syntax
- Sound Alerts - Audio notifications
- Macros - Keybind automation
Scripting (Future)
Note: Advanced scripting is a planned feature. This document describes the intended design.
Overview
Two-Face will support a scripting API for complex automation beyond simple triggers and macros.
Planned Features
Event-Based Scripts
-- React to game events
on_event("stun", function(duration)
notify("Stunned for " .. duration .. " seconds")
if health() < 50 then
send("flee")
end
end)
on_event("death", function()
notify("You have died!")
-- Auto-release logic
end)
State Access
-- Access game state
if health() < 30 then
send("hide")
end
if mana() > 50 and not has_spell("Spirit Shield") then
send("prep 107;cast")
end
Timers
-- Delayed actions
after(5000, function()
send("look")
end)
-- Repeating actions
every(60000, function()
if not hidden() then
send("perception")
end
end)
Variables
-- Persistent variables
set_var("kill_count", get_var("kill_count", 0) + 1)
if get_var("kill_count") >= 100 then
notify("100 kills reached!")
end
Language Options
Lua
Lightweight, embeddable, well-suited for game scripting:
function on_combat_start(enemy)
if enemy.name:match("dragon") then
send("flee")
else
send("attack " .. enemy.noun)
end
end
Rhai
Rust-native scripting language:
fn on_combat_start(enemy) {
if enemy.name.contains("dragon") {
send("flee");
} else {
send("attack " + enemy.noun);
}
}
Planned API
Core Functions
| Function | Description |
|---|---|
send(cmd) | Send command to game |
notify(msg) | Show notification |
log(msg) | Write to log |
sleep(ms) | Pause execution |
State Functions
| Function | Description |
|---|---|
health() | Current health |
max_health() | Maximum health |
mana() | Current mana |
stamina() | Current stamina |
spirit() | Current spirit |
Status Functions
| Function | Description |
|---|---|
stunned() | Is character stunned? |
hidden() | Is character hidden? |
prone() | Is character prone? |
roundtime() | Current roundtime |
Room Functions
| Function | Description |
|---|---|
room_name() | Current room name |
room_id() | Current room ID |
exits() | Available exits |
creatures() | Creatures in room |
Spell Functions
| Function | Description |
|---|---|
has_spell(name) | Check active spell |
spell_time(name) | Time remaining |
prepared_spell() | Currently prepared |
Example Scripts
Auto-Hide When Wounded
-- Auto-hide when health drops
on_event("health_change", function(current, max)
local percent = (current / max) * 100
if percent < 30 and not hidden() and not stunned() then
send("hide")
end
end)
Spell Manager
-- Keep defensive spells up
local defensive_spells = {
{spell = 107, name = "Spirit Shield"},
{spell = 103, name = "Spirit Defense"},
}
every(10000, function()
for _, s in ipairs(defensive_spells) do
if not has_spell(s.name) and mana() > 20 then
wait_for_rt()
send("incant " .. s.spell)
return -- One spell at a time
end
end
end)
Combat Assistant
-- Simple combat script
local target = nil
on_event("creature_enter", function(creature)
if target == nil then
target = creature
notify("Targeting: " .. creature.name)
end
end)
on_event("creature_death", function(creature)
if target and target.id == creature.id then
send("search")
target = nil
end
end)
on_key("f1", function()
if target then
wait_for_rt()
send("attack " .. target.noun)
else
notify("No target")
end
end)
Herb Monitor
-- Track herb usage
local herbs_used = {
acantha = 0,
basal = 0,
cactacae = 0,
}
on_pattern("eat.*acantha", function()
herbs_used.acantha = herbs_used.acantha + 1
if herbs_used.acantha % 10 == 0 then
notify("Used " .. herbs_used.acantha .. " acantha")
end
end)
Script Management
Loading Scripts
.script load my_script.lua
.script reload my_script
.script unload my_script
Listing Scripts
.script list
Script Directory
~/.two-face/scripts/
├── combat.lua
├── spells.lua
├── utils.lua
└── character/
└── my_char.lua
Safety Features
Sandboxing
Scripts will be sandboxed:
- No file system access
- No network access
- No shell commands
- Limited API surface
Rate Limiting
-- Commands are rate-limited
send("attack") -- Works
send("attack") -- Queued
send("attack") -- Queued
-- Actual sends spaced out
Kill Switch
.script stop # Stop all scripts
.script pause # Pause execution
.script resume # Resume execution
Current Alternatives
Until scripting is implemented, use:
- Triggers - Pattern-based automation
- Macros - Key-bound command sequences
- Cmdlists - Context menu commands
- External scripts - Lich scripts (via proxy)
Contributing
Interested in scripting implementation? Check:
- Development Guide
- Contributing Guide
- GitHub Issues for scripting discussion
See Also
- Triggers - Current pattern automation
- Macros - Keybind commands
- Architecture - System design
Tutorials
Step-by-step guides for common Two-Face setups and workflows.
Tutorial Philosophy
These tutorials guide you through complete workflows, explaining not just what to do but why each choice matters. Each tutorial builds a functional setup from scratch.
Available Tutorials
Your First Layout
Start here! Create a basic but functional layout:
- Understanding the coordinate system
- Placing essential widgets
- Testing and iteration
- Saving and loading
Time: 30 minutes | Difficulty: Beginner
Hunting Setup
Optimized layout for combat and hunting:
- Combat-focused widget arrangement
- Health/mana monitoring
- Quick action macros
- Creature targeting
Time: 45 minutes | Difficulty: Intermediate
Merchant Setup
Layout for trading and crafting:
- Inventory management
- Transaction logging
- Crafting timers
- Trade window optimization
Time: 30 minutes | Difficulty: Intermediate
Roleplay Setup
Immersive layout for roleplaying:
- Expanded text areas
- Chat organization
- Minimal HUD elements
- Atmosphere preservation
Time: 30 minutes | Difficulty: Beginner
Minimal Layout
Clean, distraction-free interface:
- Essential widgets only
- Maximum text space
- Keyboard-centric design
- Low resource usage
Time: 20 minutes | Difficulty: Beginner
Accessibility Setup
Screen reader and high contrast configurations:
- TTS integration
- High contrast themes
- Keyboard navigation
- Large text options
Time: 45 minutes | Difficulty: Intermediate
Tutorial Structure
Each tutorial follows the same pattern:
- Goal - What we’re building
- Prerequisites - What you need first
- Step-by-Step - Detailed instructions
- Configuration - Complete config files
- Testing - Verification steps
- Customization - Ways to adapt it
- Troubleshooting - Common issues
Before You Start
Prerequisites
- Two-Face installed and running
- Basic familiarity with TOML syntax
- A text editor
- Game account for testing
Backup First
Before modifying configurations:
# Backup your configs
cp -r ~/.two-face ~/.two-face.backup
Configuration Location
All config files live in ~/.two-face/:
~/.two-face/
├── config.toml # Main settings
├── layout.toml # Widget positions
├── colors.toml # Theme colors
├── highlights.toml # Text patterns
└── keybinds.toml # Key mappings
Learning Path
Complete Beginner
Combat Focus
Accessibility Needs
Trading/Crafting
Tutorial Tips
Take Your Time
Don’t rush through tutorials. Understanding each step helps you customize later.
Experiment
Tutorials provide starting points. Try variations to find what works for you.
Ask Questions
If something’s unclear:
- Check Troubleshooting
- Search existing issues
- Ask the community
Share Your Setups
Created something cool? Consider sharing it with the community!
See Also
- Cookbook - Quick recipes for specific tasks
- Configuration Reference - Complete config documentation
- Widget Reference - All widget types
Your First Layout
Create a functional Two-Face layout from scratch in 30 minutes.
Goal
By the end of this tutorial, you’ll have:
- A main text window for game output
- Health, mana, and stamina bars
- A compass for navigation
- A command input area
- Basic keybinds for common actions
Prerequisites
- Two-Face installed
- Connection to game (via Lich or direct)
- Text editor for config files
Step 1: Understanding the Grid
Two-Face uses a percentage-based grid where:
xandyare positions (0-100)widthandheightare sizes (0-100)- Origin (0, 0) is top-left corner
(0,0)─────────────────────────(100,0)
│ │
│ Your Terminal Window │
│ │
(0,100)───────────────────────(100,100)
Step 2: Plan Your Layout
Before coding, sketch your layout:
┌─────────────────────────────────────┐
│ Room Info (top bar) │
├──────────────────────────┬──────────┤
│ │ Compass │
│ ├──────────┤
│ Main Text │ Health │
│ │ Mana │
│ │ Stamina │
├──────────────────────────┴──────────┤
│ Command Input (bottom) │
└─────────────────────────────────────┘
Step 3: Create the Main Text Window
Open ~/.two-face/layout.toml and start fresh:
# Main game text window
[[widgets]]
type = "text"
name = "main"
title = "Game"
x = 0
y = 5
width = 75
height = 85
streams = ["main", "combat", "room"]
scrollback = 5000
What this does:
- Creates a text window named “main”
- Positions it from 0% left, 5% from top
- Takes 75% width, 85% height
- Shows main game output, combat, and room descriptions
- Keeps 5000 lines of history
Step 4: Add the Room Info Bar
Add a room info display at the top:
# Room information bar
[[widgets]]
type = "room"
name = "room_info"
x = 0
y = 0
width = 100
height = 5
show_exits = true
show_creatures = true
Step 5: Add the Compass
Position the compass in the right sidebar:
# Navigation compass
[[widgets]]
type = "compass"
name = "compass"
x = 76
y = 5
width = 24
height = 15
style = "unicode"
clickable = true
Style options:
"ascii"- Basic ASCII characters"unicode"- Unicode arrows (recommended)"minimal"- Just letters (N, S, E, W)
Step 6: Add Vital Bars
Add health, mana, and stamina bars below the compass:
# Health bar
[[widgets]]
type = "progress"
name = "health"
title = "Health"
x = 76
y = 21
width = 24
height = 3
data_source = "vitals.health"
color = "health"
show_text = true
# Mana bar
[[widgets]]
type = "progress"
name = "mana"
title = "Mana"
x = 76
y = 25
width = 24
height = 3
data_source = "vitals.mana"
color = "mana"
show_text = true
# Stamina bar
[[widgets]]
type = "progress"
name = "stamina"
title = "Stamina"
x = 76
y = 29
width = 24
height = 3
data_source = "vitals.stamina"
color = "stamina"
show_text = true
Step 7: Add Command Input
Add the command input at the bottom:
# Command input
[[widgets]]
type = "command_input"
name = "input"
x = 0
y = 91
width = 100
height = 9
history_size = 500
prompt = "> "
Step 8: Complete Layout File
Your complete layout.toml should look like:
# Two-Face Layout - Basic Setup
# Created following the "Your First Layout" tutorial
# Room information bar
[[widgets]]
type = "room"
name = "room_info"
x = 0
y = 0
width = 100
height = 5
show_exits = true
show_creatures = true
# Main game text window
[[widgets]]
type = "text"
name = "main"
title = "Game"
x = 0
y = 5
width = 75
height = 85
streams = ["main", "combat", "room"]
scrollback = 5000
# Navigation compass
[[widgets]]
type = "compass"
name = "compass"
x = 76
y = 5
width = 24
height = 15
style = "unicode"
clickable = true
# Health bar
[[widgets]]
type = "progress"
name = "health"
title = "Health"
x = 76
y = 21
width = 24
height = 3
data_source = "vitals.health"
color = "health"
show_text = true
# Mana bar
[[widgets]]
type = "progress"
name = "mana"
title = "Mana"
x = 76
y = 25
width = 24
height = 3
data_source = "vitals.mana"
color = "mana"
show_text = true
# Stamina bar
[[widgets]]
type = "progress"
name = "stamina"
title = "Stamina"
x = 76
y = 29
width = 24
height = 3
data_source = "vitals.stamina"
color = "stamina"
show_text = true
# Command input
[[widgets]]
type = "command_input"
name = "input"
x = 0
y = 91
width = 100
height = 9
history_size = 500
prompt = "> "
Step 9: Add Basic Keybinds
Create ~/.two-face/keybinds.toml:
# Basic navigation
[keybinds."numpad8"]
macro = "north"
[keybinds."numpad2"]
macro = "south"
[keybinds."numpad4"]
macro = "west"
[keybinds."numpad6"]
macro = "east"
[keybinds."numpad7"]
macro = "northwest"
[keybinds."numpad9"]
macro = "northeast"
[keybinds."numpad1"]
macro = "southwest"
[keybinds."numpad3"]
macro = "southeast"
[keybinds."numpad5"]
macro = "out"
# Quick actions
[keybinds."f1"]
macro = "look"
[keybinds."f2"]
macro = "inventory"
[keybinds."f3"]
macro = "experience"
# Widget navigation
[keybinds."tab"]
action = "next_widget"
[keybinds."shift+tab"]
action = "prev_widget"
[keybinds."page_up"]
action = "scroll_up"
[keybinds."page_down"]
action = "scroll_down"
# Escape to focus input
[keybinds."escape"]
action = "focus_input"
Step 10: Test Your Layout
-
Save both files
-
Reload configuration:
.reload -
Verify widgets appear:
- Main text window shows game output
- Compass displays available exits
- Vital bars show current values
- Command input accepts typing
-
Test keybinds:
- Press numpad keys for movement
- Press F1-F3 for quick commands
- Use Tab to cycle widgets
Testing Checklist
- Main window shows text
- Room info updates when moving
- Compass shows exits
- Health bar reflects actual health
- Mana bar works
- Stamina bar works
- Can type commands
- Command history works (up/down arrows)
- Keybinds execute commands
Customization Ideas
Adjust Sizes
If the sidebar feels too wide:
# Narrower sidebar (20% instead of 24%)
width = 80 # Main text
x = 81 # Sidebar widgets
width = 19 # Sidebar width
Add More Bars
Spirit bar for some professions:
[[widgets]]
type = "progress"
name = "spirit"
title = "Spirit"
x = 76
y = 33
width = 24
height = 3
data_source = "vitals.spirit"
color = "spirit"
show_text = true
Add Roundtime Display
[[widgets]]
type = "countdown"
name = "roundtime"
title = "RT"
x = 76
y = 37
width = 24
height = 3
data_source = "roundtime"
Add Status Indicators
[[widgets]]
type = "indicator"
name = "status"
x = 76
y = 41
width = 24
height = 8
indicators = ["hidden", "stunned", "webbed", "prone", "kneeling"]
Troubleshooting
Widgets Overlap
Check your coordinate math:
- Widget end = x + width (or y + height)
- Ensure widgets don’t exceed 100%
- Leave small gaps between widgets
Bars Not Updating
Verify data source names:
vitals.healthvitals.manavitals.staminavitals.spirit
Compass Not Showing Exits
The game must send exit data. Check that:
- You’re logged in
- You’ve moved to a room
- Brief mode isn’t hiding room info
Keybinds Not Working
- Check key syntax (
"numpad8"not"num8") - Verify no conflicts with system keys
- Reload config:
.reload keybinds
Next Steps
Congratulations! You’ve created your first layout.
Consider exploring:
- Hunting Setup - Combat optimization
- Creating Layouts - Advanced techniques
- Widget Reference - All widget types
See Also
Hunting Setup
Create an optimized layout for combat and hunting with real-time status monitoring.
Goal
Build a hunting-focused layout with:
- Combat text in a dedicated window
- Real-time vital monitoring with alerts
- Quick-access combat macros
- Target tracking
- Roundtime and casttime displays
- Injury awareness
Prerequisites
- Completed Your First Layout
- Understanding of basic keybinds
- Active hunting character
Layout Overview
┌────────────────────────────────────────────────────────────┐
│ Room: [name] Exits: N S E W [Compass]│
├────────────────────────────────────┬───────────────────────┤
│ │ ████████████ Health │
│ │ ████████░░░░ Mana │
│ Main Game Text │ ████████████ Stamina │
│ ├───────────────────────┤
│ │ RT: 3s Cast: -- │
│ ├───────────────────────┤
├────────────────────────────────────┤ [Hidden] [Prone] │
│ Combat Log │ [Stunned] [Webbed] │
│ (filtered combat text) ├───────────────────────┤
│ │ Head: ░░░░░ │
│ │ Chest: ██░░░ │
│ │ Arms: ░░░░░ │
├────────────────────────────────────┴───────────────────────┤
│ > [command input] │
└────────────────────────────────────────────────────────────┘
Step 1: Create the Layout
Create ~/.two-face/layout.toml:
# Hunting Layout - Combat Optimized
# Designed for active hunting with real-time monitoring
# ═══════════════════════════════════════════════════════════
# TOP BAR - Room and Navigation
# ═══════════════════════════════════════════════════════════
[[widgets]]
type = "room"
name = "room_info"
x = 0
y = 0
width = 85
height = 4
show_exits = true
show_creatures = true
creature_highlight = true
[[widgets]]
type = "compass"
name = "compass"
x = 86
y = 0
width = 14
height = 7
style = "unicode"
clickable = true
# ═══════════════════════════════════════════════════════════
# MAIN AREA - Game Text
# ═══════════════════════════════════════════════════════════
[[widgets]]
type = "text"
name = "main"
title = "Game"
x = 0
y = 4
width = 65
height = 50
streams = ["main", "room", "thoughts", "speech"]
scrollback = 5000
auto_scroll = true
# ═══════════════════════════════════════════════════════════
# COMBAT WINDOW - Filtered Combat Text
# ═══════════════════════════════════════════════════════════
[[widgets]]
type = "text"
name = "combat"
title = "Combat"
x = 0
y = 55
width = 65
height = 35
streams = ["combat"]
scrollback = 1000
auto_scroll = true
border_color = "red"
# ═══════════════════════════════════════════════════════════
# RIGHT SIDEBAR - Status Panel
# ═══════════════════════════════════════════════════════════
# Vital Bars
[[widgets]]
type = "progress"
name = "health"
title = "HP"
x = 66
y = 8
width = 34
height = 3
data_source = "vitals.health"
color = "health"
show_text = true
show_percentage = true
[[widgets]]
type = "progress"
name = "mana"
title = "MP"
x = 66
y = 12
width = 34
height = 3
data_source = "vitals.mana"
color = "mana"
show_text = true
show_percentage = true
[[widgets]]
type = "progress"
name = "stamina"
title = "ST"
x = 66
y = 16
width = 34
height = 3
data_source = "vitals.stamina"
color = "stamina"
show_text = true
show_percentage = true
[[widgets]]
type = "progress"
name = "spirit"
title = "SP"
x = 66
y = 20
width = 34
height = 3
data_source = "vitals.spirit"
color = "spirit"
show_text = true
show_percentage = true
# Timing Displays
[[widgets]]
type = "countdown"
name = "roundtime"
title = "RT"
x = 66
y = 24
width = 16
height = 3
data_source = "roundtime"
warning_threshold = 2
critical_threshold = 0
[[widgets]]
type = "countdown"
name = "casttime"
title = "Cast"
x = 83
y = 24
width = 17
height = 3
data_source = "casttime"
# Status Indicators
[[widgets]]
type = "indicator"
name = "status"
title = "Status"
x = 66
y = 28
width = 34
height = 10
indicators = [
"hidden",
"invisible",
"stunned",
"webbed",
"prone",
"kneeling",
"sitting",
"bleeding",
"poisoned"
]
columns = 2
# Injury Display
[[widgets]]
type = "injury_doll"
name = "injuries"
title = "Injuries"
x = 66
y = 39
width = 34
height = 20
style = "bars"
show_scars = false
# Active Effects
[[widgets]]
type = "active_effects"
name = "effects"
title = "Active"
x = 66
y = 60
width = 34
height = 30
show_duration = true
group_by_type = true
# ═══════════════════════════════════════════════════════════
# BOTTOM - Command Input
# ═══════════════════════════════════════════════════════════
[[widgets]]
type = "command_input"
name = "input"
x = 0
y = 91
width = 100
height = 9
history_size = 500
prompt = "› "
show_roundtime = true
Step 2: Combat Keybinds
Create combat-focused ~/.two-face/keybinds.toml:
# ═══════════════════════════════════════════════════════════
# COMBAT MACROS - Function Keys
# ═══════════════════════════════════════════════════════════
# Basic attacks
[keybinds."f1"]
macro = "attack target"
[keybinds."f2"]
macro = "attack left target"
[keybinds."f3"]
macro = "attack right target"
[keybinds."f4"]
macro = "feint target"
# Defensive actions
[keybinds."f5"]
macro = "stance defensive"
[keybinds."f6"]
macro = "hide"
[keybinds."f7"]
macro = "stance offensive"
[keybinds."f8"]
macro = "stance neutral"
# Combat utilities
[keybinds."f9"]
macro = "search"
[keybinds."f10"]
macro = "search;loot"
[keybinds."f11"]
macro = "skin"
[keybinds."f12"]
macro = "aim target"
# ═══════════════════════════════════════════════════════════
# SPELL MACROS - Ctrl+Number
# ═══════════════════════════════════════════════════════════
[keybinds."ctrl+1"]
macro = "incant 101"
[keybinds."ctrl+2"]
macro = "incant 102"
[keybinds."ctrl+3"]
macro = "incant 103"
[keybinds."ctrl+4"]
macro = "incant 104"
[keybinds."ctrl+5"]
macro = "incant 105"
[keybinds."ctrl+6"]
macro = "incant 106"
[keybinds."ctrl+7"]
macro = "incant 107"
[keybinds."ctrl+8"]
macro = "incant 108"
[keybinds."ctrl+9"]
macro = "incant 109"
[keybinds."ctrl+0"]
macro = "incant 110"
# ═══════════════════════════════════════════════════════════
# MOVEMENT - Numpad
# ═══════════════════════════════════════════════════════════
[keybinds."numpad8"]
macro = "north"
[keybinds."numpad2"]
macro = "south"
[keybinds."numpad4"]
macro = "west"
[keybinds."numpad6"]
macro = "east"
[keybinds."numpad7"]
macro = "northwest"
[keybinds."numpad9"]
macro = "northeast"
[keybinds."numpad1"]
macro = "southwest"
[keybinds."numpad3"]
macro = "southeast"
[keybinds."numpad5"]
macro = "out"
[keybinds."numpad_plus"]
macro = "go gate"
[keybinds."numpad_minus"]
macro = "go door"
# ═══════════════════════════════════════════════════════════
# EMERGENCY ACTIONS - Shift+Function
# ═══════════════════════════════════════════════════════════
[keybinds."shift+f1"]
macro = "flee"
[keybinds."shift+f2"]
macro = "stance defensive;hide"
[keybinds."shift+f3"]
macro = "get acantha from my pouch;eat my acantha"
[keybinds."shift+f4"]
macro = "get basal from my pouch;eat my basal"
[keybinds."shift+f5"]
macro = "get cactacae from my pouch;eat my cactacae"
# ═══════════════════════════════════════════════════════════
# TARGETING - Alt+Keys
# ═══════════════════════════════════════════════════════════
[keybinds."alt+1"]
macro = "target first"
[keybinds."alt+2"]
macro = "target second"
[keybinds."alt+3"]
macro = "target third"
[keybinds."alt+t"]
macro = "target $input"
[keybinds."alt+c"]
macro = "target clear"
# ═══════════════════════════════════════════════════════════
# WIDGET NAVIGATION
# ═══════════════════════════════════════════════════════════
[keybinds."tab"]
action = "next_widget"
[keybinds."shift+tab"]
action = "prev_widget"
[keybinds."page_up"]
action = "scroll_up"
[keybinds."page_down"]
action = "scroll_down"
[keybinds."home"]
action = "scroll_top"
[keybinds."end"]
action = "scroll_bottom"
[keybinds."escape"]
action = "focus_input"
Step 3: Combat Highlights
Add to ~/.two-face/highlights.toml:
# ═══════════════════════════════════════════════════════════
# COMBAT HIGHLIGHTING
# ═══════════════════════════════════════════════════════════
# Critical hits
[[highlights]]
pattern = "\\*\\* .+ \\*\\*"
fg = "bright_red"
bold = true
# Your attacks
[[highlights]]
pattern = "You (swing|thrust|slash|punch|kick)"
fg = "green"
# Enemy attacks
[[highlights]]
pattern = "(swings|thrusts|slashes|punches|kicks) at you"
fg = "red"
# Misses
[[highlights]]
pattern = "(miss|barely miss|narrowly miss)"
fg = "dark_gray"
italic = true
# Death messages
[[highlights]]
pattern = "falls dead"
fg = "bright_yellow"
bold = true
# ═══════════════════════════════════════════════════════════
# STATUS ALERTS
# ═══════════════════════════════════════════════════════════
# Stunned
[[highlights]]
pattern = "(?i)you are stunned"
fg = "black"
bg = "yellow"
bold = true
# Webbed
[[highlights]]
pattern = "(?i)webs? (stick|entangle)"
fg = "black"
bg = "magenta"
bold = true
# Prone
[[highlights]]
pattern = "(?i)(knock|fall).*?(down|prone)"
fg = "black"
bg = "cyan"
bold = true
# ═══════════════════════════════════════════════════════════
# DAMAGE NUMBERS
# ═══════════════════════════════════════════════════════════
# High damage
[[highlights]]
pattern = "\\b([5-9]\\d|[1-9]\\d{2,}) points? of damage"
fg = "bright_green"
bold = true
# Medium damage
[[highlights]]
pattern = "\\b([2-4]\\d) points? of damage"
fg = "green"
# Low damage
[[highlights]]
pattern = "\\b([0-1]\\d) points? of damage"
fg = "dark_green"
# ═══════════════════════════════════════════════════════════
# LOOT
# ═══════════════════════════════════════════════════════════
# Coins
[[highlights]]
pattern = "\\d+ (silver|gold|copper)"
fg = "bright_yellow"
# Gems
[[highlights]]
pattern = "(?i)(gem|jewel|diamond|ruby|emerald|sapphire)"
fg = "bright_cyan"
# Treasure
[[highlights]]
pattern = "(?i)treasure"
fg = "bright_yellow"
bold = true
Step 4: Combat Triggers
Add to ~/.two-face/triggers.toml:
# ═══════════════════════════════════════════════════════════
# COMBAT ALERTS
# ═══════════════════════════════════════════════════════════
# Stunned alert
[[triggers]]
name = "stun_alert"
pattern = "(?i)you are stunned"
command = ".notify STUNNED!"
category = "combat"
priority = 100
cooldown = 1000
# Webbed alert
[[triggers]]
name = "web_alert"
pattern = "(?i)webs? (stick|entangle)"
command = ".notify WEBBED!"
category = "combat"
priority = 100
cooldown = 1000
# Prone alert
[[triggers]]
name = "prone_alert"
pattern = "(?i)(knock|fall).*?(down|prone)"
command = ".notify PRONE!"
category = "combat"
priority = 100
cooldown = 1000
# Low health warning
[[triggers]]
name = "low_health"
pattern = "You feel (weak|faint|dizzy)"
command = ".notify LOW HEALTH!;stance defensive"
category = "combat"
priority = 100
# ═══════════════════════════════════════════════════════════
# HUNTING AUTOMATION (Optional - Use Carefully)
# ═══════════════════════════════════════════════════════════
# Auto-search on kill (disabled by default)
[[triggers]]
name = "auto_search"
pattern = "falls dead"
command = "search"
category = "loot"
cooldown = 2000
enabled = false
# Roundtime notification
[[triggers]]
name = "roundtime"
pattern = "Roundtime: (\\d+)"
command = ".rt $1"
category = "status"
Step 5: Color Theme for Combat
Add combat-focused colors to ~/.two-face/colors.toml:
[theme]
name = "Combat Focus"
# Vital colors - easy visibility
health = "#00ff00" # Bright green
health_low = "#ffff00" # Yellow warning
health_critical = "#ff0000" # Red danger
mana = "#0080ff" # Blue
stamina = "#ff8000" # Orange
spirit = "#ff00ff" # Magenta
# Combat window
combat_border = "#ff4444"
combat_bg = "#1a0000"
# Status indicators
hidden = "#00ff00"
stunned = "#ffff00"
webbed = "#ff00ff"
prone = "#00ffff"
# High contrast text
text = "#ffffff"
text_dim = "#808080"
Testing Your Setup
Combat Test Checklist
-
Vital Monitoring
- Health bar updates when damaged
- Mana bar updates when casting
- Low health triggers visual warning
-
Combat Window
- Combat text appears in combat window
- Non-combat text stays in main window
- Combat highlights are visible
-
Roundtime Display
- RT countdown shows during actions
- Cast timer shows during spellcasting
-
Status Indicators
- Hidden indicator shows when hiding
- Stun indicator triggers on stun
-
Keybinds
- F1 attacks target
- Numpad moves correctly
- Emergency flee works
Combat Workflow Test
- Find a creature
- Use F1 to attack
- Watch combat window for hit/miss
- Check RT countdown
- Use numpad to navigate away
- Verify flee (Shift+F1) works
Customization Tips
Profession-Specific Adjustments
Rogues: Prioritize hidden indicator, add ambush macros
[keybinds."f1"]
macro = "ambush target"
[keybinds."f2"]
macro = "hide;ambush target"
Wizards: Add spell tracking, larger mana display
[[widgets]]
type = "spells"
name = "known_spells"
x = 66
y = 60
width = 34
height = 30
Warriors: Add CM tracking, shield macros
[keybinds."f4"]
macro = "cman surge"
[keybinds."f5"]
macro = "cman feint"
Multi-Target Hunting
For areas with multiple creatures:
# Target cycling
[keybinds."alt+n"]
macro = "target next"
[keybinds."alt+p"]
macro = "target previous"
Troubleshooting
Combat Text Not Filtering
Verify stream configuration:
- Main window:
streams = ["main", "room", "thoughts", "speech"] - Combat window:
streams = ["combat"]
Roundtime Not Showing
Check data source is correct:
data_source = "roundtime"
Keybinds Not Responding During Combat
Ensure focus is on input widget:
[keybinds."escape"]
action = "focus_input"
Triggers Not Firing
- Check trigger syntax
- Verify
enabled = true(default) - Test pattern matches actual game text
See Also
Merchant Setup
Create a layout optimized for trading, crafting, and inventory management.
Goal
Build a merchant-focused layout with:
- Expanded inventory display
- Transaction logging
- Crafting timer tracking
- Multiple chat channels
- Appraisal quick-access
Prerequisites
- Completed Your First Layout
- Character with merchant activities
- Understanding of inventory commands
Layout Overview
┌────────────────────────────────────────────────────────────┐
│ Room: [Town Square] [Compass] │
├────────────────────┬───────────────────┬───────────────────┤
│ │ │ │
│ Main Game Text │ Inventory │ Transaction │
│ │ (tree view) │ Log │
│ │ │ │
│ │ │ │
├────────────────────┼───────────────────┴───────────────────┤
│ Trade Channel │ Crafting Timer │ Wealth Display │
│ │ [████████░░ 80%] │ 12,345 silver │
├────────────────────┴───────────────────────────────────────┤
│ > [command input] │
└────────────────────────────────────────────────────────────┘
Step 1: Create the Layout
Create ~/.two-face/layout.toml:
# Merchant Layout - Trading and Crafting Optimized
# Designed for inventory management and transactions
# ═══════════════════════════════════════════════════════════
# TOP BAR - Room and Navigation
# ═══════════════════════════════════════════════════════════
[[widgets]]
type = "room"
name = "room_info"
x = 0
y = 0
width = 85
height = 4
show_exits = true
show_creatures = false
show_players = true
[[widgets]]
type = "compass"
name = "compass"
x = 86
y = 0
width = 14
height = 6
style = "minimal"
clickable = true
# ═══════════════════════════════════════════════════════════
# LEFT COLUMN - Main Game Text
# ═══════════════════════════════════════════════════════════
[[widgets]]
type = "text"
name = "main"
title = "Game"
x = 0
y = 4
width = 35
height = 55
streams = ["main", "room"]
scrollback = 3000
auto_scroll = true
# ═══════════════════════════════════════════════════════════
# CENTER COLUMN - Inventory
# ═══════════════════════════════════════════════════════════
[[widgets]]
type = "inventory"
name = "inventory"
title = "Inventory"
x = 36
y = 4
width = 32
height = 55
show_containers = true
show_weight = true
expand_containers = true
sort_by = "name"
# ═══════════════════════════════════════════════════════════
# RIGHT COLUMN - Transaction Log
# ═══════════════════════════════════════════════════════════
[[widgets]]
type = "text"
name = "transactions"
title = "Transactions"
x = 69
y = 4
width = 31
height = 55
streams = ["commerce", "merchant"]
scrollback = 1000
auto_scroll = true
border_color = "yellow"
# ═══════════════════════════════════════════════════════════
# BOTTOM ROW - Trade Chat and Utilities
# ═══════════════════════════════════════════════════════════
# Trade channel
[[widgets]]
type = "text"
name = "trade"
title = "Trade Chat"
x = 0
y = 60
width = 50
height = 30
streams = ["thoughts", "speech"]
scrollback = 500
auto_scroll = true
# Crafting/Activity Timer
[[widgets]]
type = "countdown"
name = "crafting"
title = "Activity"
x = 51
y = 60
width = 24
height = 5
data_source = "roundtime"
show_bar = true
# Wealth Display (Custom text widget)
[[widgets]]
type = "text"
name = "wealth"
title = "Wealth"
x = 76
y = 60
width = 24
height = 10
streams = ["wealth"]
scrollback = 50
# Status indicators (minimal)
[[widgets]]
type = "indicator"
name = "status"
title = "Status"
x = 51
y = 66
width = 49
height = 8
indicators = ["kneeling", "sitting", "encumbered"]
columns = 3
# Vital bars (compact)
[[widgets]]
type = "progress"
name = "health"
title = "HP"
x = 51
y = 75
width = 24
height = 3
data_source = "vitals.health"
color = "health"
show_text = true
[[widgets]]
type = "progress"
name = "stamina"
title = "ST"
x = 76
y = 75
width = 24
height = 3
data_source = "vitals.stamina"
color = "stamina"
show_text = true
# Active effects (spells, buffs)
[[widgets]]
type = "active_effects"
name = "effects"
title = "Effects"
x = 51
y = 79
width = 49
height = 11
show_duration = true
compact = true
# ═══════════════════════════════════════════════════════════
# COMMAND INPUT
# ═══════════════════════════════════════════════════════════
[[widgets]]
type = "command_input"
name = "input"
x = 0
y = 91
width = 100
height = 9
history_size = 500
prompt = "$ "
Step 2: Merchant Keybinds
Create ~/.two-face/keybinds.toml:
# ═══════════════════════════════════════════════════════════
# INVENTORY MANAGEMENT - Function Keys
# ═══════════════════════════════════════════════════════════
[keybinds."f1"]
macro = "inventory"
[keybinds."f2"]
macro = "inventory full"
[keybinds."f3"]
macro = "look in my backpack"
[keybinds."f4"]
macro = "look in my cloak"
[keybinds."f5"]
macro = "wealth"
[keybinds."f6"]
macro = "experience"
# ═══════════════════════════════════════════════════════════
# TRADING ACTIONS - Ctrl Keys
# ═══════════════════════════════════════════════════════════
[keybinds."ctrl+a"]
macro = "appraise $input"
[keybinds."ctrl+g"]
macro = "get $input"
[keybinds."ctrl+d"]
macro = "drop $input"
[keybinds."ctrl+p"]
macro = "put $input in my backpack"
[keybinds."ctrl+s"]
macro = "sell $input"
[keybinds."ctrl+b"]
macro = "buy $input"
[keybinds."ctrl+o"]
macro = "order $input"
# ═══════════════════════════════════════════════════════════
# CONTAINER SHORTCUTS - Alt Keys
# ═══════════════════════════════════════════════════════════
[keybinds."alt+1"]
macro = "put $input in my backpack"
[keybinds."alt+2"]
macro = "put $input in my cloak"
[keybinds."alt+3"]
macro = "put $input in my sack"
[keybinds."alt+4"]
macro = "put $input in my pouch"
[keybinds."alt+g"]
macro = "get $input from my $input"
# ═══════════════════════════════════════════════════════════
# QUICK COMMERCE
# ═══════════════════════════════════════════════════════════
[keybinds."ctrl+shift+a"]
macro = "appraise all"
[keybinds."ctrl+shift+s"]
macro = "sell all"
# Give to player
[keybinds."ctrl+shift+g"]
macro = "give $input to $input"
# ═══════════════════════════════════════════════════════════
# MOVEMENT - Numpad
# ═══════════════════════════════════════════════════════════
[keybinds."numpad8"]
macro = "north"
[keybinds."numpad2"]
macro = "south"
[keybinds."numpad4"]
macro = "west"
[keybinds."numpad6"]
macro = "east"
[keybinds."numpad7"]
macro = "northwest"
[keybinds."numpad9"]
macro = "northeast"
[keybinds."numpad1"]
macro = "southwest"
[keybinds."numpad3"]
macro = "southeast"
[keybinds."numpad5"]
macro = "out"
[keybinds."numpad_plus"]
macro = "go counter"
[keybinds."numpad_minus"]
macro = "go door"
# ═══════════════════════════════════════════════════════════
# SOCIAL/TRADE CHAT
# ═══════════════════════════════════════════════════════════
[keybinds."ctrl+t"]
macro = "think $input"
[keybinds."ctrl+w"]
macro = "whisper $input"
# ═══════════════════════════════════════════════════════════
# WIDGET NAVIGATION
# ═══════════════════════════════════════════════════════════
[keybinds."tab"]
action = "next_widget"
[keybinds."shift+tab"]
action = "prev_widget"
[keybinds."page_up"]
action = "scroll_up"
[keybinds."page_down"]
action = "scroll_down"
[keybinds."escape"]
action = "focus_input"
# Quick focus specific widgets
[keybinds."alt+i"]
action = "focus_widget"
widget = "inventory"
[keybinds."alt+t"]
action = "focus_widget"
widget = "transactions"
[keybinds."alt+m"]
action = "focus_widget"
widget = "main"
Step 3: Commerce Highlights
Add to ~/.two-face/highlights.toml:
# ═══════════════════════════════════════════════════════════
# CURRENCY HIGHLIGHTING
# ═══════════════════════════════════════════════════════════
# Silver amounts
[[highlights]]
pattern = "\\d+,?\\d* silver"
fg = "bright_white"
bold = true
# Gold (if applicable)
[[highlights]]
pattern = "\\d+,?\\d* gold"
fg = "bright_yellow"
bold = true
# ═══════════════════════════════════════════════════════════
# TRANSACTION HIGHLIGHTING
# ═══════════════════════════════════════════════════════════
# Purchase
[[highlights]]
pattern = "(?i)you (buy|purchase|acquire)"
fg = "green"
# Sale
[[highlights]]
pattern = "(?i)you (sell|sold)"
fg = "cyan"
# Trade accepted
[[highlights]]
pattern = "(?i)trade (complete|accepted|confirmed)"
fg = "bright_green"
bold = true
# ═══════════════════════════════════════════════════════════
# ITEM HIGHLIGHTING
# ═══════════════════════════════════════════════════════════
# Gems
[[highlights]]
pattern = "(?i)(diamond|ruby|emerald|sapphire|pearl|opal|topaz|garnet)"
fg = "bright_cyan"
# Rare items
[[highlights]]
pattern = "(?i)(rare|unique|enchanted|magical)"
fg = "bright_magenta"
# Containers
[[highlights]]
pattern = "(?i)(backpack|cloak|sack|pouch|bag|chest|box)"
fg = "yellow"
# ═══════════════════════════════════════════════════════════
# MERCHANT NPCS
# ═══════════════════════════════════════════════════════════
[[highlights]]
pattern = "(?i)(merchant|shopkeeper|vendor|trader|clerk)"
fg = "bright_yellow"
# ═══════════════════════════════════════════════════════════
# PLAYER NAMES (Trade Context)
# ═══════════════════════════════════════════════════════════
[[highlights]]
pattern = "\\b[A-Z][a-z]+\\b(?= (offers|gives|trades|whispers))"
fg = "bright_white"
bold = true
Step 4: Transaction Triggers
Add to ~/.two-face/triggers.toml:
# ═══════════════════════════════════════════════════════════
# TRANSACTION ALERTS
# ═══════════════════════════════════════════════════════════
# Whisper received
[[triggers]]
name = "whisper_alert"
pattern = "(\\w+) whispers,"
command = ".notify Whisper from $1"
category = "social"
priority = 100
# Trade offer
[[triggers]]
name = "trade_offer"
pattern = "(\\w+) offers to trade"
command = ".notify Trade offer from $1"
category = "commerce"
priority = 90
# Payment received
[[triggers]]
name = "payment"
pattern = "(\\d+) silver"
command = ".log Payment: $1 silver"
category = "commerce"
stream = "commerce"
# ═══════════════════════════════════════════════════════════
# INVENTORY ALERTS
# ═══════════════════════════════════════════════════════════
# Encumbered
[[triggers]]
name = "encumbered"
pattern = "(?i)encumbered"
command = ".notify Encumbered!"
category = "status"
cooldown = 5000
# Container full
[[triggers]]
name = "container_full"
pattern = "(?i)won't fit|too full"
command = ".notify Container full!"
category = "inventory"
cooldown = 2000
# ═══════════════════════════════════════════════════════════
# CRAFTING TIMERS
# ═══════════════════════════════════════════════════════════
# Roundtime for crafting
[[triggers]]
name = "craft_rt"
pattern = "Roundtime: (\\d+)"
command = ".rt $1"
category = "crafting"
Step 5: Command Lists for Items
Add to ~/.two-face/cmdlist.toml:
# ═══════════════════════════════════════════════════════════
# GENERAL ITEMS
# ═══════════════════════════════════════════════════════════
[[cmdlist]]
category = "item"
noun = ".*"
match_mode = "regex"
commands = [
"look",
"get",
"drop",
"---",
"appraise",
"sell",
"---",
"put in>put {noun} in my backpack,put {noun} in my cloak,put {noun} in my sack"
]
priority = 10
# ═══════════════════════════════════════════════════════════
# CONTAINERS
# ═══════════════════════════════════════════════════════════
[[cmdlist]]
category = "container"
noun = "(?i)(backpack|cloak|sack|pouch|bag|chest|box)"
match_mode = "regex"
commands = [
"look in",
"open",
"close",
"---",
"get from:get {input} from my {noun}",
"inventory check"
]
priority = 50
# ═══════════════════════════════════════════════════════════
# CURRENCY
# ═══════════════════════════════════════════════════════════
[[cmdlist]]
category = "currency"
noun = "(?i)(silver|gold|coins?|copper)"
match_mode = "regex"
commands = [
"get",
"count",
"deposit"
]
priority = 40
# ═══════════════════════════════════════════════════════════
# GEMS AND VALUABLES
# ═══════════════════════════════════════════════════════════
[[cmdlist]]
category = "gems"
noun = "(?i)(gem|jewel|diamond|ruby|emerald|sapphire|pearl|stone)"
match_mode = "regex"
commands = [
"look",
"get",
"appraise",
"sell",
"---",
"put in pouch:put {noun} in my gem pouch"
]
priority = 45
# ═══════════════════════════════════════════════════════════
# PLAYERS (Trade Context)
# ═══════════════════════════════════════════════════════════
[[cmdlist]]
category = "player"
noun = "^[A-Z][a-z]+$"
match_mode = "regex"
commands = [
"look",
"---",
"whisper:whisper {noun} {input}",
"give:give {input} to {noun}",
"trade:trade {noun}",
"---",
"smile",
"bow"
]
priority = 60
Testing Your Setup
Merchant Workflow Test
-
Inventory Display
- Inventory widget shows items
- Container contents expand
- Weight displays correctly
-
Transaction Logging
- Commerce messages appear in transaction log
- Trade whispers show in trade chat
-
Quick Commands
- F1 shows inventory
- Ctrl+A prompts for appraise target
- Ctrl+S prompts for sell target
-
Context Menus
- Right-click item shows merchant options
- Put in container submenu works
- Player right-click shows trade options
Typical Trade Session
- Go to town square
- Check inventory (F1)
- Appraise items (Ctrl+A)
- Watch transaction log
- Test whisper alerts
- Try context menu trading
Customization Tips
For Crafters
Add crafting-specific bindings:
# Crafting macros
[keybinds."ctrl+c"]
macro = "craft $input"
[keybinds."ctrl+r"]
macro = "repair $input"
[keybinds."ctrl+e"]
macro = "enhance $input"
For Auctioneers
Track auction activity:
[[triggers]]
name = "auction_bid"
pattern = "(?i)bid.*?(\\d+) silver"
command = ".notify Bid: $1 silver"
category = "auction"
For Gatherers
Quick deposit macros:
[keybinds."ctrl+shift+d"]
macro = "deposit all"
[keybinds."ctrl+shift+w"]
macro = "withdraw $input silver"
Troubleshooting
Transaction Log Empty
Check stream configuration:
streams = ["commerce", "merchant"]
If no dedicated streams exist, try:
streams = ["main"]
Then filter with highlights.
Inventory Not Updating
Inventory requires game state updates:
- Type
inventoryto refresh - Check for parsing errors in logs
- Verify widget data source
Context Menu Missing
- Check cmdlist.toml syntax
- Verify pattern matches item nouns
- Reload:
.reload cmdlist
Keybind Prompts Not Appearing
Ensure $input syntax is correct:
macro = "sell $input" # Correct
macro = "sell {input}" # Wrong
See Also
Roleplay Setup
Create an immersive layout focused on storytelling and social interaction.
Goal
Build a roleplay-focused layout with:
- Maximum text visibility for story immersion
- Organized chat channels
- Minimal HUD elements
- Easy social command access
- Atmospheric preservation
Prerequisites
- Completed Your First Layout
- Interest in roleplay and social gameplay
- Basic emote familiarity
Design Philosophy
Roleplay layouts prioritize:
- Immersion - Large text areas, minimal UI chrome
- Readability - Clear text, comfortable spacing
- Organization - Separate IC (In-Character) and OOC (Out-Of-Character)
- Quick Social - Fast access to emotes and speech
Layout Overview
┌────────────────────────────────────────────────────────────┐
│ [Room Name] │
├────────────────────────────────────────────────────────────┤
│ │
│ │
│ Main Story Text │
│ (room descriptions, actions) │
│ │
│ │
│ │
├──────────────────────────────────┬─────────────────────────┤
│ Speech & Thoughts │ ESP/OOC Chat │
│ (IC communication) │ (group chat) │
├──────────────────────────────────┴─────────────────────────┤
│ > [command input] │
└────────────────────────────────────────────────────────────┘
Step 1: Create the Layout
Create ~/.two-face/layout.toml:
# Roleplay Layout - Immersive Storytelling
# Maximizes text space, organizes communication channels
# ═══════════════════════════════════════════════════════════
# TOP - Room Title Bar (Minimal)
# ═══════════════════════════════════════════════════════════
[[widgets]]
type = "room"
name = "room_title"
x = 0
y = 0
width = 100
height = 3
show_exits = false
show_creatures = false
show_players = false
title_only = true
# ═══════════════════════════════════════════════════════════
# MAIN AREA - Story Text (Largest Widget)
# ═══════════════════════════════════════════════════════════
[[widgets]]
type = "text"
name = "story"
title = ""
x = 0
y = 3
width = 100
height = 55
streams = ["main", "room"]
scrollback = 10000
auto_scroll = true
border = false
padding = 1
# ═══════════════════════════════════════════════════════════
# BOTTOM LEFT - In-Character Communication
# ═══════════════════════════════════════════════════════════
[[widgets]]
type = "text"
name = "speech"
title = "Speech"
x = 0
y = 59
width = 55
height = 31
streams = ["speech", "whisper"]
scrollback = 2000
auto_scroll = true
border_color = "blue"
# ═══════════════════════════════════════════════════════════
# BOTTOM RIGHT - Out-Of-Character / ESP
# ═══════════════════════════════════════════════════════════
[[widgets]]
type = "text"
name = "thoughts"
title = "ESP/OOC"
x = 56
y = 59
width = 44
height = 31
streams = ["thoughts", "group"]
scrollback = 1000
auto_scroll = true
border_color = "cyan"
# ═══════════════════════════════════════════════════════════
# COMMAND INPUT
# ═══════════════════════════════════════════════════════════
[[widgets]]
type = "command_input"
name = "input"
x = 0
y = 91
width = 100
height = 9
history_size = 500
prompt = "› "
Step 2: Immersive Theme
Create ~/.two-face/colors.toml:
[theme]
name = "Parchment"
# Background - soft, easy on eyes
background = "#1a1814" # Dark parchment
# Text colors - warm and readable
text = "#d4c5a9" # Cream text
text_dim = "#8b7355" # Muted brown
# UI elements - unobtrusive
border = "#3d3428" # Dark border
border_focused = "#5c4a32" # Focused border
# Speech colors - distinguishable but not harsh
speech = "#87ceeb" # Sky blue (say)
whisper = "#da70d6" # Orchid (whisper)
thoughts = "#98fb98" # Pale green (thoughts)
shout = "#ffa500" # Orange (shout)
# Room descriptions - slightly brighter
room_name = "#ffd700" # Gold
room_desc = "#d4c5a9" # Cream
# Player names
player = "#ffffff" # White
npc = "#daa520" # Goldenrod
# Emotes and actions
emote = "#b8860b" # Dark goldenrod
# Minimal status (if shown)
health = "#8fbc8f" # Dark sea green
mana = "#6495ed" # Cornflower blue
Step 3: Roleplay Keybinds
Create ~/.two-face/keybinds.toml:
# ═══════════════════════════════════════════════════════════
# SPEECH SHORTCUTS
# ═══════════════════════════════════════════════════════════
# Quick say (most common)
[keybinds."enter"]
action = "submit_input"
# Say with prompt
[keybinds."ctrl+s"]
macro = "say $input"
# Whisper
[keybinds."ctrl+w"]
macro = "whisper $input"
# Ask
[keybinds."ctrl+a"]
macro = "ask $input"
# Exclaim
[keybinds."ctrl+e"]
macro = "exclaim $input"
# ═══════════════════════════════════════════════════════════
# ESP/THOUGHTS
# ═══════════════════════════════════════════════════════════
[keybinds."ctrl+t"]
macro = "think $input"
[keybinds."ctrl+g"]
macro = "think [group] $input"
# ═══════════════════════════════════════════════════════════
# COMMON EMOTES - Function Keys
# ═══════════════════════════════════════════════════════════
[keybinds."f1"]
macro = "smile"
[keybinds."f2"]
macro = "nod"
[keybinds."f3"]
macro = "bow"
[keybinds."f4"]
macro = "wave"
[keybinds."f5"]
macro = "laugh"
[keybinds."f6"]
macro = "grin"
[keybinds."f7"]
macro = "sigh"
[keybinds."f8"]
macro = "shrug"
# Targeted emotes
[keybinds."shift+f1"]
macro = "smile $input"
[keybinds."shift+f2"]
macro = "nod $input"
[keybinds."shift+f3"]
macro = "bow $input"
[keybinds."shift+f4"]
macro = "wave $input"
# ═══════════════════════════════════════════════════════════
# ACTIONS AND POSES
# ═══════════════════════════════════════════════════════════
[keybinds."ctrl+1"]
macro = "act $input"
[keybinds."ctrl+2"]
macro = "pose $input"
[keybinds."ctrl+3"]
macro = "emote $input"
# ═══════════════════════════════════════════════════════════
# MOVEMENT (Unobtrusive)
# ═══════════════════════════════════════════════════════════
[keybinds."numpad8"]
macro = "go north"
[keybinds."numpad2"]
macro = "go south"
[keybinds."numpad4"]
macro = "go west"
[keybinds."numpad6"]
macro = "go east"
[keybinds."numpad7"]
macro = "go northwest"
[keybinds."numpad9"]
macro = "go northeast"
[keybinds."numpad1"]
macro = "go southwest"
[keybinds."numpad3"]
macro = "go southeast"
[keybinds."numpad5"]
macro = "go out"
# ═══════════════════════════════════════════════════════════
# LOOK AROUND
# ═══════════════════════════════════════════════════════════
[keybinds."ctrl+l"]
macro = "look"
[keybinds."ctrl+shift+l"]
macro = "look $input"
# ═══════════════════════════════════════════════════════════
# WINDOW NAVIGATION
# ═══════════════════════════════════════════════════════════
[keybinds."page_up"]
action = "scroll_up"
[keybinds."page_down"]
action = "scroll_down"
[keybinds."home"]
action = "scroll_top"
[keybinds."end"]
action = "scroll_bottom"
[keybinds."tab"]
action = "next_widget"
[keybinds."escape"]
action = "focus_input"
# Quick focus to specific windows
[keybinds."alt+1"]
action = "focus_widget"
widget = "story"
[keybinds."alt+2"]
action = "focus_widget"
widget = "speech"
[keybinds."alt+3"]
action = "focus_widget"
widget = "thoughts"
Step 4: Immersive Highlights
Add to ~/.two-face/highlights.toml:
# ═══════════════════════════════════════════════════════════
# SPEECH PATTERNS
# ═══════════════════════════════════════════════════════════
# Someone says
[[highlights]]
pattern = '(\\w+) says?, "'
fg = "#87ceeb"
# Someone whispers
[[highlights]]
pattern = "(\\w+) whispers,"
fg = "#da70d6"
# Someone asks
[[highlights]]
pattern = "(\\w+) asks?,"
fg = "#87ceeb"
# Someone exclaims
[[highlights]]
pattern = "(\\w+) exclaims?,"
fg = "#ffa500"
# Quoted speech
[[highlights]]
pattern = '"[^"]*"'
fg = "#ffffff"
bold = false
# ═══════════════════════════════════════════════════════════
# EMOTES AND ACTIONS
# ═══════════════════════════════════════════════════════════
# Emotes (verbs at start of line after name)
[[highlights]]
pattern = "^\\w+ (smiles|nods|bows|waves|laughs|grins|sighs|shrugs)"
fg = "#b8860b"
# ═══════════════════════════════════════════════════════════
# ROOM ELEMENTS
# ═══════════════════════════════════════════════════════════
# Room name (usually in brackets or emphasized)
[[highlights]]
pattern = "^\\[.*\\]$"
fg = "#ffd700"
bold = true
# Obvious exits
[[highlights]]
pattern = "Obvious (exits|paths):"
fg = "#8b7355"
# ═══════════════════════════════════════════════════════════
# PLAYER NAMES
# ═══════════════════════════════════════════════════════════
# Capital names (likely players)
[[highlights]]
pattern = "\\b[A-Z][a-z]{2,}\\b"
fg = "#ffffff"
# ═══════════════════════════════════════════════════════════
# OBJECTS AND ITEMS (Subtle)
# ═══════════════════════════════════════════════════════════
[[highlights]]
pattern = "(a|an|the|some) [a-z]+ [a-z]+"
fg = "#d4c5a9"
# ═══════════════════════════════════════════════════════════
# ESP/THOUGHTS
# ═══════════════════════════════════════════════════════════
[[highlights]]
pattern = "\\[.*?\\]:"
fg = "#98fb98"
# ═══════════════════════════════════════════════════════════
# TIME AND ATMOSPHERE
# ═══════════════════════════════════════════════════════════
[[highlights]]
pattern = "(?i)(dawn|morning|noon|afternoon|dusk|evening|night|midnight)"
fg = "#6495ed"
italic = true
[[highlights]]
pattern = "(?i)(rain|snow|fog|mist|wind|storm|clear|cloudy)"
fg = "#6495ed"
italic = true
Step 5: Social Command List
Add to ~/.two-face/cmdlist.toml:
# ═══════════════════════════════════════════════════════════
# PLAYERS - Social Context
# ═══════════════════════════════════════════════════════════
[[cmdlist]]
category = "player"
noun = "^[A-Z][a-z]+$"
match_mode = "regex"
commands = [
"look",
"---",
"Greet>smile {noun},bow {noun},wave {noun},nod {noun}",
"Say>say (to {noun}) {input},whisper {noun} {input},ask {noun} {input}",
"---",
"appraise"
]
priority = 50
# ═══════════════════════════════════════════════════════════
# ITEMS - RP Context
# ═══════════════════════════════════════════════════════════
[[cmdlist]]
category = "item"
noun = ".*"
match_mode = "regex"
commands = [
"look",
"get",
"touch",
"smell",
"taste",
"---",
"show:show my {noun} to {input}",
"give:give my {noun} to {input}"
]
priority = 10
# ═══════════════════════════════════════════════════════════
# FURNITURE AND SCENERY
# ═══════════════════════════════════════════════════════════
[[cmdlist]]
category = "furniture"
noun = "(?i)(chair|bench|stool|table|bed|couch|throne)"
match_mode = "regex"
commands = [
"look",
"sit on",
"stand",
"lean on"
]
priority = 30
Step 6: Optional - Minimal Status
If you want some status without breaking immersion:
# Add to layout.toml - minimalist status bar
[[widgets]]
type = "progress"
name = "health"
x = 0
y = 56
width = 50
height = 2
data_source = "vitals.health"
color = "health"
show_text = false
border = false
[[widgets]]
type = "progress"
name = "mana"
x = 50
y = 56
width = 50
height = 2
data_source = "vitals.mana"
color = "mana"
show_text = false
border = false
This adds thin, borderless bars between story and chat.
Testing Your Setup
Roleplay Flow Test
-
Text Visibility
- Room descriptions fill story window
- Text is comfortable to read
- Scrolling works smoothly
-
Communication
- Speech appears in speech window
- ESP/thoughts in separate window
- Colors distinguish speakers
-
Quick Actions
- F1-F8 emotes work
- Ctrl+S opens say prompt
- Movement numpad works
-
Immersion
- UI doesn’t distract
- Colors are comfortable
- Layout feels spacious
Social Interaction Test
- Go to a social area (town square)
- Use F1 to smile
- Use Ctrl+S to say something
- Check speech appears in correct window
- Verify ESP goes to thoughts window
Customization Ideas
Faction-Based Themes
Create themes matching your character’s background:
Elven Theme:
background = "#0a1510"
text = "#c0d890"
border = "#2a4530"
Dark Theme:
background = "#0d0d0d"
text = "#b0b0b0"
border = "#1a1a1a"
Event-Specific Layouts
For large events, expand thoughts window:
# Event layout - larger ESP
[[widgets]]
type = "text"
name = "thoughts"
x = 0
y = 60
width = 100
height = 30
streams = ["thoughts", "group"]
Character Macros
Pre-written character expressions:
[keybinds."ctrl+shift+1"]
macro = "emote adjusts their cloak with practiced ease"
[keybinds."ctrl+shift+2"]
macro = "emote glances around thoughtfully"
[keybinds."ctrl+shift+3"]
macro = "emote inclines their head in greeting"
Troubleshooting
Text Too Dense
Increase padding:
padding = 2
Or add line spacing if supported.
Can’t See Who’s Speaking
Adjust highlight patterns to catch speaker names before quotes.
ESP Flooding Main Window
Verify stream separation:
- Story:
["main", "room"] - Speech:
["speech", "whisper"] - Thoughts:
["thoughts", "group"]
Emotes Going to Wrong Window
Emotes typically go to “main” stream. If you want them separate, check parser configuration.
See Also
Minimal Layout
Create a clean, distraction-free interface with maximum text visibility.
Goal
Build a minimalist layout with:
- Maximum screen space for game text
- Essential information only
- Keyboard-centric design
- Low resource usage
- Quick load times
Prerequisites
- Basic Two-Face familiarity
- Preference for simple interfaces
- Keyboard navigation skills
Design Philosophy
The minimal layout follows these principles:
- Less is More - Only show what you actively need
- Text First - Game output takes priority
- Keyboard Driven - Reduce mouse dependency
- Performance - Fewer widgets = faster rendering
Layout Overview
┌────────────────────────────────────────────────────────────┐
│ │
│ │
│ │
│ Game Text │
│ (Full Screen) │
│ │
│ │
│ │
├────────────────────────────────────────────────────────────┤
│ HP: ████████░░ 82% MP: ████░░░░░░ 40% RT: 3 > [input] │
└────────────────────────────────────────────────────────────┘
Step 1: Create the Layout
Create ~/.two-face/layout.toml:
# Minimal Layout - Maximum Text, Minimum UI
# Clean, fast, keyboard-driven
# ═══════════════════════════════════════════════════════════
# MAIN TEXT - Full Width, Maximum Height
# ═══════════════════════════════════════════════════════════
[[widgets]]
type = "text"
name = "main"
title = ""
x = 0
y = 0
width = 100
height = 90
streams = ["main", "room", "combat", "thoughts", "speech"]
scrollback = 3000
auto_scroll = true
border = false
padding = 1
# ═══════════════════════════════════════════════════════════
# STATUS BAR - Compact Bottom Row
# ═══════════════════════════════════════════════════════════
# Health (compact)
[[widgets]]
type = "progress"
name = "health"
title = "HP"
x = 0
y = 91
width = 15
height = 3
data_source = "vitals.health"
color = "health"
show_percentage = true
border = false
# Mana (compact)
[[widgets]]
type = "progress"
name = "mana"
title = "MP"
x = 16
y = 91
width = 15
height = 3
data_source = "vitals.mana"
color = "mana"
show_percentage = true
border = false
# Roundtime (compact)
[[widgets]]
type = "countdown"
name = "roundtime"
title = "RT"
x = 32
y = 91
width = 8
height = 3
data_source = "roundtime"
border = false
# Command Input (rest of row)
[[widgets]]
type = "command_input"
name = "input"
x = 41
y = 91
width = 59
height = 9
history_size = 500
prompt = "> "
border = false
That’s it. Four widgets total.
Step 2: Minimal Theme
Create ~/.two-face/colors.toml:
[theme]
name = "Minimal"
# Clean, high-contrast colors
background = "#000000"
text = "#cccccc"
text_dim = "#666666"
# No visible borders
border = "#000000"
border_focused = "#333333"
# Vitals - visible but not distracting
health = "#00aa00"
health_low = "#aaaa00"
health_critical = "#aa0000"
mana = "#0066aa"
stamina = "#aa6600"
# Status bar background
status_bg = "#111111"
Step 3: Keyboard-Centric Keybinds
Create ~/.two-face/keybinds.toml:
# ═══════════════════════════════════════════════════════════
# CORE NAVIGATION - Always Available
# ═══════════════════════════════════════════════════════════
# Cardinal directions
[keybinds."numpad8"]
macro = "north"
[keybinds."numpad2"]
macro = "south"
[keybinds."numpad4"]
macro = "west"
[keybinds."numpad6"]
macro = "east"
# Diagonal
[keybinds."numpad7"]
macro = "northwest"
[keybinds."numpad9"]
macro = "northeast"
[keybinds."numpad1"]
macro = "southwest"
[keybinds."numpad3"]
macro = "southeast"
# Special movement
[keybinds."numpad5"]
macro = "out"
[keybinds."numpad_plus"]
macro = "go"
[keybinds."numpad_minus"]
macro = "climb"
# ═══════════════════════════════════════════════════════════
# ESSENTIAL COMMANDS - Function Keys
# ═══════════════════════════════════════════════════════════
[keybinds."f1"]
macro = "look"
[keybinds."f2"]
macro = "inventory"
[keybinds."f3"]
macro = "experience"
[keybinds."f4"]
macro = "health"
[keybinds."f5"]
macro = "stance defensive"
[keybinds."f6"]
macro = "stance offensive"
[keybinds."f7"]
macro = "hide"
[keybinds."f8"]
macro = "search"
# ═══════════════════════════════════════════════════════════
# QUICK ACTIONS - Ctrl Keys
# ═══════════════════════════════════════════════════════════
[keybinds."ctrl+a"]
macro = "attack target"
[keybinds."ctrl+s"]
macro = "search;loot"
[keybinds."ctrl+g"]
macro = "get $input"
[keybinds."ctrl+d"]
macro = "drop $input"
# ═══════════════════════════════════════════════════════════
# SCROLLING - Page Keys
# ═══════════════════════════════════════════════════════════
[keybinds."page_up"]
action = "scroll_up"
[keybinds."page_down"]
action = "scroll_down"
[keybinds."home"]
action = "scroll_top"
[keybinds."end"]
action = "scroll_bottom"
# Half-page scroll
[keybinds."ctrl+page_up"]
action = "scroll_half_up"
[keybinds."ctrl+page_down"]
action = "scroll_half_down"
# ═══════════════════════════════════════════════════════════
# FOCUS
# ═══════════════════════════════════════════════════════════
[keybinds."escape"]
action = "focus_input"
# Toggle between main and input (since only 2 widgets)
[keybinds."tab"]
action = "next_widget"
Step 4: Minimal Highlights
Create ~/.two-face/highlights.toml:
# Minimal highlighting - only essential patterns
# ═══════════════════════════════════════════════════════════
# CRITICAL ALERTS
# ═══════════════════════════════════════════════════════════
[[highlights]]
pattern = "(?i)you are stunned"
fg = "black"
bg = "yellow"
bold = true
[[highlights]]
pattern = "(?i)you have died"
fg = "black"
bg = "red"
bold = true
# ═══════════════════════════════════════════════════════════
# PLAYER INTERACTION
# ═══════════════════════════════════════════════════════════
# Whispers (important)
[[highlights]]
pattern = "(\\w+) whispers,"
fg = "magenta"
# Your name mentioned
[[highlights]]
pattern = "\\bYOURNAME\\b"
fg = "cyan"
bold = true
# ═══════════════════════════════════════════════════════════
# COMBAT (Subtle)
# ═══════════════════════════════════════════════════════════
# Hits
[[highlights]]
pattern = "\\*\\*.+\\*\\*"
fg = "red"
# Death
[[highlights]]
pattern = "falls dead"
fg = "yellow"
# ═══════════════════════════════════════════════════════════
# NAVIGATION AIDS
# ═══════════════════════════════════════════════════════════
# Room name
[[highlights]]
pattern = "^\\[.+\\]$"
fg = "white"
bold = true
# Exits
[[highlights]]
pattern = "Obvious (exits|paths):"
fg = "gray"
Step 5: No Triggers (Optional)
For truly minimal setup, skip triggers entirely. Or add only critical ones:
Create ~/.two-face/triggers.toml:
# Only life-saving triggers
[[triggers]]
name = "death_alert"
pattern = "You have died"
command = ".notify DEAD!"
priority = 100
[[triggers]]
name = "stun_alert"
pattern = "(?i)you are stunned"
command = ".notify Stunned"
priority = 100
cooldown = 2000
Alternative: Ultra-Minimal
For absolute minimalism, use just two widgets:
# Ultra-minimal - text and input only
[[widgets]]
type = "text"
name = "main"
x = 0
y = 0
width = 100
height = 92
streams = ["main", "room", "combat", "thoughts", "speech"]
scrollback = 2000
border = false
[[widgets]]
type = "command_input"
name = "input"
x = 0
y = 93
width = 100
height = 7
prompt = "> "
border = false
No status bars, no indicators. Pure text adventure.
Alternative: Vertical Split
If you want some separation without complexity:
# Two-column minimal
[[widgets]]
type = "text"
name = "main"
x = 0
y = 0
width = 70
height = 92
streams = ["main", "room", "combat"]
border = false
[[widgets]]
type = "text"
name = "chat"
x = 71
y = 0
width = 29
height = 92
streams = ["thoughts", "speech", "whisper"]
border_color = "gray"
[[widgets]]
type = "command_input"
name = "input"
x = 0
y = 93
width = 100
height = 7
prompt = "> "
border = false
Testing Your Setup
Performance Test
- Launch Two-Face
- Check startup time (should be fast)
- Scroll rapidly (should be smooth)
- Send many commands (no lag)
Functionality Test
- Text displays clearly
- Scrolling works (Page Up/Down)
- Commands execute
- Vital bars update
- RT countdown works
Keyboard-Only Test
Try a session without using the mouse:
- Navigate rooms with numpad
- Use F-keys for common commands
- Type commands directly
- Scroll with Page keys
Performance Tips
Reduce Scrollback
Less history = less memory:
scrollback = 1000 # Instead of 5000+
Disable Animations
If supported:
[performance]
animations = false
smooth_scroll = false
Simple Highlights
Complex regex patterns slow parsing:
# Fast (literal string)
pattern = "You are stunned"
# Slower (complex regex)
pattern = "(?i)you\\s+are\\s+(?:completely\\s+)?stunned"
Fewer Streams
Combining streams reduces processing:
# All in one (fastest)
streams = ["main"]
# Separated (more processing)
streams = ["main", "room", "combat", "thoughts"]
Customization
Adding Status on Demand
Create a keybind to show/hide status:
[keybinds."ctrl+h"]
action = "toggle_widget"
widget = "health"
Temporary Expansion
For complex situations, temporarily switch layouts:
.layout hunting # Switch to full layout
.layout minimal # Back to minimal
Color Adjustments
For different lighting conditions:
Day Mode (brighter):
background = "#1a1a1a"
text = "#e0e0e0"
Night Mode (darker):
background = "#000000"
text = "#909090"
Troubleshooting
Missing Information
If you need data not shown:
- Add minimal widget for that data
- Use keybind macros (F3 = experience)
- Create triggers for notifications
Accidental Scrollback
Enable auto-scroll:
auto_scroll = true
Or use End key to jump to bottom.
Can’t See Status
Check border settings - borders might be same color as background:
border = false # Hide borders entirely
Input Too Small
Increase input height:
height = 9 # More space for long commands
Philosophy
The minimal layout works best when you:
- Know the game well
- Prefer typing over clicking
- Want maximum text visibility
- Value performance over features
- Play on smaller screens or terminals
It doesn’t work well when you:
- Need constant visual status monitoring
- Rely heavily on context menus
- Are new to the game
- Want rich visual feedback
Choose the approach that matches your playstyle.
See Also
- Your First Layout - For more features
- Performance Optimization
- Creating Themes
Accessibility Setup
Configure Two-Face for screen readers, high contrast needs, and other accessibility requirements.
Goal
Create an accessible setup with:
- Screen reader integration (TTS)
- High contrast color themes
- Keyboard-only navigation
- Large text options
- Simplified layouts
Prerequisites
- Two-Face installed
- Screen reader software (optional)
- Understanding of your accessibility needs
Accessibility Features Overview
| Feature | Purpose | Configuration |
|---|---|---|
| TTS | Audio output | config.toml |
| High Contrast | Visual clarity | colors.toml |
| Large Text | Readability | Terminal settings |
| Keyboard Nav | Mouse-free use | keybinds.toml |
| Audio Alerts | Status awareness | triggers.toml |
Step 1: Enable Text-to-Speech
Configure TTS
Edit ~/.two-face/config.toml:
[tts]
enabled = true
voice = "default" # System default voice
rate = 1.0 # 0.5 (slow) to 2.0 (fast)
volume = 1.0 # 0.0 to 1.0
# What to speak
speak_room_descriptions = true
speak_combat = true
speak_speech = true
speak_whispers = true
speak_thoughts = false # Often too much
speak_system = true
# Filtering
skip_patterns = [
"^\\s*$", # Empty lines
"^Obvious exits:", # Repetitive
]
# Priority speaking (interrupts queue)
priority_patterns = [
"stunned",
"webbed",
"died",
"whispers,"
]
Platform-Specific TTS
Windows (SAPI):
[tts]
engine = "sapi"
voice = "Microsoft David" # or "Microsoft Zira"
macOS (say):
[tts]
engine = "say"
voice = "Alex" # or "Samantha"
Linux (espeak):
[tts]
engine = "espeak"
voice = "en-us"
# Install: sudo apt install espeak
Linux (speech-dispatcher):
[tts]
engine = "speechd"
voice = "default"
# Install: sudo apt install speech-dispatcher
Step 2: High Contrast Theme
Create ~/.two-face/colors.toml:
[theme]
name = "High Contrast"
# Maximum contrast backgrounds
background = "#000000" # Pure black
text = "#ffffff" # Pure white
text_dim = "#c0c0c0" # Light gray (still readable)
# Bold, distinct borders
border = "#ffffff"
border_focused = "#ffff00" # Yellow focus indicator
# Vitals - WCAG AAA contrast
health = "#00ff00" # Bright green
health_low = "#ffff00" # Yellow warning
health_critical = "#ff0000" # Red danger
mana = "#00ffff" # Cyan
stamina = "#ff8800" # Orange
spirit = "#ff00ff" # Magenta
# Status indicators - distinct colors
hidden = "#00ff00"
stunned = "#ffff00"
webbed = "#ff00ff"
prone = "#00ffff"
kneeling = "#ff8800"
# Speech - distinguishable
speech = "#00ffff" # Cyan
whisper = "#ff00ff" # Magenta
thoughts = "#00ff00" # Green
shout = "#ffff00" # Yellow
# Combat
attack_hit = "#00ff00"
attack_miss = "#ff0000"
damage_high = "#ff0000"
# Links and interactive elements
link = "#00ffff"
link_hover = "#ffff00"
Alternative: Yellow on Black
Some users prefer yellow text:
[theme]
name = "Yellow High Contrast"
background = "#000000"
text = "#ffff00"
text_dim = "#cccc00"
border = "#ffff00"
Alternative: White on Blue
Classic high-contrast scheme:
[theme]
name = "White on Blue"
background = "#000080"
text = "#ffffff"
border = "#ffffff"
Step 3: Accessible Layout
Create ~/.two-face/layout.toml:
# Accessible Layout - Clear, Logical Structure
# Optimized for screen readers and keyboard navigation
# ═══════════════════════════════════════════════════════════
# MAIN TEXT - Primary Focus
# ═══════════════════════════════════════════════════════════
[[widgets]]
type = "text"
name = "main"
title = "Game Output"
x = 0
y = 0
width = 100
height = 70
streams = ["main", "room", "combat", "speech"]
scrollback = 2000
auto_scroll = true
focus_order = 1 # First in tab order
# ═══════════════════════════════════════════════════════════
# STATUS - Spoken Summary
# ═══════════════════════════════════════════════════════════
[[widgets]]
type = "progress"
name = "health"
title = "Health"
x = 0
y = 71
width = 25
height = 5
data_source = "vitals.health"
color = "health"
show_text = true
show_percentage = true
announce_changes = true # TTS announces changes
focus_order = 2
[[widgets]]
type = "progress"
name = "mana"
title = "Mana"
x = 26
y = 71
width = 25
height = 5
data_source = "vitals.mana"
color = "mana"
show_text = true
show_percentage = true
announce_changes = true
focus_order = 3
[[widgets]]
type = "progress"
name = "stamina"
title = "Stamina"
x = 52
y = 71
width = 24
height = 5
data_source = "vitals.stamina"
color = "stamina"
show_text = true
show_percentage = true
focus_order = 4
[[widgets]]
type = "countdown"
name = "roundtime"
title = "Roundtime"
x = 77
y = 71
width = 23
height = 5
data_source = "roundtime"
announce_start = true # "Roundtime 5 seconds"
announce_end = true # "Ready"
focus_order = 5
# ═══════════════════════════════════════════════════════════
# STATUS INDICATORS - Audio Alerts
# ═══════════════════════════════════════════════════════════
[[widgets]]
type = "indicator"
name = "status"
title = "Status"
x = 0
y = 77
width = 100
height = 5
indicators = [
"hidden",
"stunned",
"webbed",
"prone",
"kneeling"
]
columns = 5
announce_changes = true # TTS announces status changes
focus_order = 6
# ═══════════════════════════════════════════════════════════
# COMMAND INPUT - Primary Interaction
# ═══════════════════════════════════════════════════════════
[[widgets]]
type = "command_input"
name = "input"
title = "Command"
x = 0
y = 83
width = 100
height = 17
history_size = 500
prompt = "Enter command: "
announce_echo = false # Don't repeat typed text
focus_order = 0 # Default focus
Step 4: Keyboard Navigation
Create ~/.two-face/keybinds.toml:
# ═══════════════════════════════════════════════════════════
# WIDGET NAVIGATION - Tab Order
# ═══════════════════════════════════════════════════════════
[keybinds."tab"]
action = "next_widget"
[keybinds."shift+tab"]
action = "prev_widget"
[keybinds."escape"]
action = "focus_input"
# Quick focus to specific widgets
[keybinds."alt+m"]
action = "focus_widget"
widget = "main"
[keybinds."alt+h"]
action = "focus_widget"
widget = "health"
[keybinds."alt+i"]
action = "focus_widget"
widget = "input"
# ═══════════════════════════════════════════════════════════
# SCROLLING - Standard Keys
# ═══════════════════════════════════════════════════════════
[keybinds."page_up"]
action = "scroll_up"
[keybinds."page_down"]
action = "scroll_down"
[keybinds."home"]
action = "scroll_top"
[keybinds."end"]
action = "scroll_bottom"
[keybinds."ctrl+home"]
action = "scroll_top"
[keybinds."ctrl+end"]
action = "scroll_bottom"
# Line-by-line scrolling
[keybinds."up"]
action = "scroll_line_up"
[keybinds."down"]
action = "scroll_line_down"
# ═══════════════════════════════════════════════════════════
# STATUS QUERIES - Audio Feedback
# ═══════════════════════════════════════════════════════════
[keybinds."f1"]
action = "speak_status" # TTS reads all vitals
[keybinds."f2"]
action = "speak_room" # TTS reads room description
[keybinds."f3"]
action = "speak_last" # Repeat last spoken text
[keybinds."f4"]
action = "stop_speaking" # Interrupt TTS
# ═══════════════════════════════════════════════════════════
# MOVEMENT - Consistent Keys
# ═══════════════════════════════════════════════════════════
[keybinds."numpad8"]
macro = "north"
[keybinds."numpad2"]
macro = "south"
[keybinds."numpad4"]
macro = "west"
[keybinds."numpad6"]
macro = "east"
[keybinds."numpad7"]
macro = "northwest"
[keybinds."numpad9"]
macro = "northeast"
[keybinds."numpad1"]
macro = "southwest"
[keybinds."numpad3"]
macro = "southeast"
[keybinds."numpad5"]
macro = "out"
# ═══════════════════════════════════════════════════════════
# COMMON ACTIONS
# ═══════════════════════════════════════════════════════════
[keybinds."f5"]
macro = "look"
[keybinds."f6"]
macro = "inventory"
[keybinds."f7"]
macro = "experience"
[keybinds."f8"]
macro = "health"
[keybinds."f9"]
macro = "attack target"
[keybinds."f10"]
macro = "hide"
[keybinds."f11"]
macro = "search"
[keybinds."f12"]
macro = "flee"
# ═══════════════════════════════════════════════════════════
# TTS CONTROL
# ═══════════════════════════════════════════════════════════
[keybinds."ctrl+space"]
action = "toggle_tts"
[keybinds."ctrl+up"]
action = "tts_rate_up"
[keybinds."ctrl+down"]
action = "tts_rate_down"
[keybinds."ctrl+shift+up"]
action = "tts_volume_up"
[keybinds."ctrl+shift+down"]
action = "tts_volume_down"
Step 5: Audio Alerts
Create ~/.two-face/triggers.toml:
# ═══════════════════════════════════════════════════════════
# CRITICAL STATUS - Spoken Alerts
# ═══════════════════════════════════════════════════════════
[[triggers]]
name = "stun_announce"
pattern = "(?i)you are stunned"
command = ".tts You are stunned!"
priority = 100
cooldown = 1000
[[triggers]]
name = "web_announce"
pattern = "(?i)webs? (stick|entangle)"
command = ".tts You are webbed!"
priority = 100
cooldown = 1000
[[triggers]]
name = "prone_announce"
pattern = "(?i)(knock|fall).*?(down|prone)"
command = ".tts You fell down!"
priority = 100
cooldown = 1000
[[triggers]]
name = "death_announce"
pattern = "You have died"
command = ".tts You have died!"
priority = 100
# ═══════════════════════════════════════════════════════════
# HEALTH WARNINGS
# ═══════════════════════════════════════════════════════════
[[triggers]]
name = "low_health"
pattern = "(?i)feel (weak|faint)"
command = ".tts Warning! Low health!"
priority = 100
cooldown = 5000
# ═══════════════════════════════════════════════════════════
# SOCIAL - Important Communications
# ═══════════════════════════════════════════════════════════
[[triggers]]
name = "whisper_announce"
pattern = "(\\w+) whispers,"
command = ".tts $1 whispers to you"
priority = 90
cooldown = 500
[[triggers]]
name = "name_mention"
pattern = "\\bYOURNAME\\b"
command = ".tts Your name was mentioned"
priority = 80
cooldown = 3000
# ═══════════════════════════════════════════════════════════
# ROUNDTIME
# ═══════════════════════════════════════════════════════════
[[triggers]]
name = "rt_long"
pattern = "Roundtime: ([5-9]|\\d{2,})"
command = ".tts Roundtime $1 seconds"
priority = 50
[[triggers]]
name = "rt_done"
pattern = "Roundtime: 0"
command = ".tts Ready"
priority = 50
enabled = false # Enable if you want ready alerts
Step 6: Large Text Configuration
Two-Face inherits font size from your terminal. Configure your terminal:
Windows Terminal
Settings → Profiles → Default → Appearance:
- Font size: 14-18 for comfortable reading
- Font face: Consolas, Cascadia Code, or other monospace
macOS Terminal
Terminal → Preferences → Profiles → Text:
- Font: Monaco or SF Mono
- Size: 14-18 pt
Linux Terminal (GNOME)
Preferences → Profiles → Default → Text:
- Custom font: DejaVu Sans Mono
- Size: 14-18
Recommended Fonts
High-readability monospace fonts:
| Font | Platform | Notes |
|---|---|---|
| JetBrains Mono | All | Designed for readability |
| Fira Code | All | Excellent legibility |
| Cascadia Code | Windows | Microsoft’s modern font |
| SF Mono | macOS | Apple’s system font |
| DejaVu Sans Mono | Linux | Comprehensive Unicode |
| OpenDyslexic Mono | All | Dyslexia-friendly |
Testing Your Setup
TTS Test
- Enable TTS in config
- Launch Two-Face
- Move to a new room
- Verify room description is spoken
- Test F1 (speak status) keybind
Navigation Test
- Use Tab to cycle through widgets
- Verify focus indicator is visible (yellow border)
- Use Page Up/Down in main window
- Use Escape to return to input
Contrast Test
- Check all text is readable
- Verify borders are visible
- Test in both light and dark room lighting
- Check vital bar colors are distinguishable
Checklist
- TTS speaks room descriptions
- TTS announces status changes
- Tab cycles through widgets
- Focus indicator clearly visible
- All text high contrast
- Vital bars distinguishable by color
- Keybinds work as expected
- F1 speaks current status
Customization
Adjust TTS Speed
For faster reading:
[tts]
rate = 1.5
For clearer pronunciation:
[tts]
rate = 0.8
Selective TTS
Only speak important things:
[tts]
speak_room_descriptions = true
speak_combat = false
speak_speech = true
speak_whispers = true
speak_thoughts = false
Different Voices
Some systems support multiple voices:
[tts]
combat_voice = "Microsoft David"
speech_voice = "Microsoft Zira"
Custom Alert Sounds
Combine TTS with audio:
[[triggers]]
name = "stun_alert"
pattern = "(?i)you are stunned"
command = ".sound stun.wav;.tts Stunned!"
Screen Reader Integration
Two-Face can work alongside dedicated screen readers:
NVDA (Windows)
- Two-Face TTS can coexist with NVDA
- Consider disabling Two-Face TTS if using NVDA
- NVDA will read terminal output automatically
VoiceOver (macOS)
- VoiceOver reads terminal content
- Use VoiceOver commands for navigation
- Two-Face keybinds may conflict - adjust as needed
Orca (Linux)
- Orca works with accessible terminals
- Ensure terminal has accessibility enabled
- May need to disable Two-Face TTS to avoid overlap
Troubleshooting
TTS Not Working
- Check TTS is enabled in config
- Verify system TTS works:
say "test"(macOS) or test in Windows settings - Check TTS engine setting matches installed software
- Review logs for TTS errors
Focus Not Visible
Increase border contrast:
border_focused = "#ffff00" # Bright yellow
Or add a focus background:
focused_bg = "#333300"
Colors Not Distinct
Test with colorblind simulation tools, then adjust:
# For red-green colorblindness
health = "#00ffff" # Cyan instead of green
danger = "#ffff00" # Yellow instead of red
Keybinds Conflicting
Screen readers use many keybinds. Check for conflicts and remap:
# If F1 conflicts with screen reader
[keybinds."ctrl+f1"]
action = "speak_status"
Resources
Color Contrast Tools
Screen Reader Documentation
Accessibility Standards
See Also
- TTS Setup - Detailed TTS configuration
- Creating Themes - Theme customization
- Keybind Actions - All available actions
Cookbook
Quick recipes for common Two-Face customizations and configurations.
Philosophy
The cookbook provides focused, copy-paste-ready solutions for specific tasks. Each recipe is self-contained and can be used independently.
Recipe Format
Each recipe includes:
- Goal - What you’ll achieve
- Configuration - Complete TOML to copy
- Explanation - How it works
- Variations - Alternative approaches
Available Recipes
Layout Recipes
- Split Main Window - Divide main text area into multiple panels
- Floating Compass - Overlay compass on main window
- Transparent Overlays - Semi-transparent widgets
Feedback Recipes
- Combat Alerts - Visual and audio combat notifications
- Custom Status Bar - Personalized status display
Organization Recipes
- Tabbed Channels - Organize communications in tabs
Quick Reference
Common Tasks
| Want to… | Recipe |
|---|---|
| Split main window | Split Main Window |
| Overlay compass | Floating Compass |
| Combat sounds | Combat Alerts |
| Organize chat | Tabbed Channels |
| Custom HUD | Custom Status Bar |
By Difficulty
Beginner:
- Combat Alerts
- Custom Status Bar
Intermediate:
- Split Main Window
- Tabbed Channels
- Floating Compass
Advanced:
- Transparent Overlays
Using Recipes
Copy and Modify
- Find the recipe for your goal
- Copy the configuration
- Paste into your config file
- Adjust values for your preferences
Combining Recipes
Recipes are designed to work together. To combine:
- Copy configurations from each recipe
- Adjust positions to prevent overlap
- Ensure no naming conflicts
Testing
After applying a recipe:
- Reload configuration (
.reloador restart) - Verify the feature works
- Adjust as needed
Contributing Recipes
Have a useful configuration? Consider contributing:
- Document it following the recipe format
- Test thoroughly
- Submit via GitHub
See Also
- Tutorials - Step-by-step guides
- Configuration - Config reference
- Customization - Detailed customization
Split Main Window
Divide the main text area into multiple panels for better organization.
Goal
Create separate text windows for different content types while maintaining a clean, organized layout.
Basic Split: Combat + Main
Two-column layout with combat on the left, main text on the right.
# Main game text (right side)
[[widgets]]
type = "text"
name = "main"
title = "Game"
x = 30
y = 0
width = 70
height = 85
streams = ["main", "room"]
scrollback = 5000
# Combat text (left side)
[[widgets]]
type = "text"
name = "combat"
title = "Combat"
x = 0
y = 0
width = 29
height = 85
streams = ["combat"]
scrollback = 2000
Horizontal Split: Main + Chat
Main text on top, communications below.
# Main game text (top)
[[widgets]]
type = "text"
name = "main"
title = "Game"
x = 0
y = 0
width = 100
height = 60
streams = ["main", "room", "combat"]
scrollback = 5000
# Chat window (bottom)
[[widgets]]
type = "text"
name = "chat"
title = "Chat"
x = 0
y = 61
width = 100
height = 24
streams = ["speech", "whisper", "thoughts"]
scrollback = 3000
Three-Panel Layout
Combat left, main center, chat right.
# Combat (left)
[[widgets]]
type = "text"
name = "combat"
title = "Combat"
x = 0
y = 0
width = 25
height = 85
streams = ["combat"]
# Main (center)
[[widgets]]
type = "text"
name = "main"
title = "Game"
x = 26
y = 0
width = 48
height = 85
streams = ["main", "room"]
# Chat (right)
[[widgets]]
type = "text"
name = "chat"
title = "Chat"
x = 75
y = 0
width = 25
height = 85
streams = ["speech", "whisper", "thoughts"]
Percentage-Based Split
Adapts to terminal size.
# Left panel (30% width)
[[widgets]]
type = "text"
name = "side"
x = "0%"
y = "0%"
width = "30%"
height = "80%"
streams = ["combat"]
# Right panel (70% width)
[[widgets]]
type = "text"
name = "main"
x = "30%"
y = "0%"
width = "70%"
height = "80%"
streams = ["main", "room"]
How It Works
Stream Assignment
Each widget shows only its assigned streams:
main- Primary game outputroom- Room descriptionscombat- Combat messagesspeech- Character speechwhisper- Private messagesthoughts- Mental communications
Position Calculation
Widgets are positioned using x, y coordinates:
- Fixed numbers = absolute cells
- Percentages = relative to terminal size
Gap Prevention
Leave 1 cell between widgets for borders:
# Widget 1 ends at x=29
width = 29
# Widget 2 starts at x=30
x = 30
Variations
Minimal Split
Just main and combat:
[[widgets]]
type = "text"
name = "main"
x = 0
width = "65%"
streams = ["main", "room", "speech", "whisper"]
[[widgets]]
type = "text"
name = "combat"
x = "66%"
width = "34%"
streams = ["combat"]
Stacked Chat
Multiple chat streams in separate panels:
[[widgets]]
type = "text"
name = "speech"
title = "Speech"
y = 0
height = "33%"
streams = ["speech"]
[[widgets]]
type = "text"
name = "whisper"
title = "Whispers"
y = "34%"
height = "33%"
streams = ["whisper"]
[[widgets]]
type = "text"
name = "thoughts"
title = "Thoughts"
y = "67%"
height = "33%"
streams = ["thoughts"]
Tips
- Consider Focus: Which panel needs most attention?
- Size by Importance: Larger panels for important content
- Test Scrollback: Reduce scrollback for secondary panels
- Add Borders: Visual separation helps readability
See Also
Floating Compass
Overlay a compass on top of the main text window for space efficiency.
Goal
Display the compass as a semi-transparent overlay, saving vertical space while keeping navigation visible.
Basic Floating Compass
# Main window (full width)
[[widgets]]
type = "text"
name = "main"
title = "Game"
x = 0
y = 0
width = 100
height = 85
streams = ["main", "room", "combat"]
z_index = 1
# Floating compass (top right corner)
[[widgets]]
type = "compass"
name = "compass"
x = 85
y = 1
width = 14
height = 7
z_index = 10
style = "compact"
border = false
clickable = true
Transparent Compass
With transparency support:
[[widgets]]
type = "compass"
name = "compass"
x = 85
y = 1
width = 14
height = 7
z_index = 10
style = "unicode"
transparent = true
opacity = 0.8
Corner Positions
Top Right (Default)
[[widgets]]
type = "compass"
name = "compass"
x = "86%"
y = 1
anchor = "top_right"
Top Left
[[widgets]]
type = "compass"
name = "compass"
x = 1
y = 1
anchor = "top_left"
Bottom Right
[[widgets]]
type = "compass"
name = "compass"
x = "86%"
y = "78%"
anchor = "bottom_right"
Compact Styles
Minimal Compass
[[widgets]]
type = "compass"
name = "compass"
style = "minimal"
width = 7
height = 3
# Shows: N E S W in a row
Icons Only
[[widgets]]
type = "compass"
name = "compass"
style = "icons"
width = 9
height = 5
# Shows directional arrows only
Full Rose
[[widgets]]
type = "compass"
name = "compass"
style = "rose"
width = 15
height = 9
# Shows full compass rose with all directions
Compass with Room Name
Combine compass with room title:
[[widgets]]
type = "compass"
name = "compass_with_room"
x = 75
y = 0
width = 25
height = 10
style = "unicode"
show_room_name = true
room_name_position = "top"
Toggle Visibility
Keybind to show/hide compass:
[keybinds."ctrl+c"]
action = "toggle_widget"
widget = "compass"
How It Works
Z-Index Layering
Higher z_index draws on top:
z_index = 1 # Main window (bottom)
z_index = 10 # Compass (top)
Transparency
When transparent = true:
- Background is not drawn
- Text underneath may show through
opacitycontrols visibility (0.0-1.0)
Click-Through
click_through = true # Clicks go to window below
click_through = false # Compass captures clicks
Variations
Mini Compass
Smallest possible:
[[widgets]]
type = "compass"
name = "mini_compass"
style = "dots"
width = 5
height = 3
# Shows • for available directions
Direction Indicators Only
Show only when exits available:
[[widgets]]
type = "compass"
name = "compass"
style = "indicators"
hide_when_empty = true
Centered Floating
[[widgets]]
type = "compass"
name = "compass"
x = "center"
y = 2
width = 15
height = 7
z_index = 10
Tips
- Test Overlap: Ensure compass doesn’t cover important text
- Use Borders Wisely: Borderless saves space
- Consider Clickability: Click navigation is convenient
- Adjust Opacity: Find balance between visible and unobtrusive
Troubleshooting
Compass Covers Text
- Reduce compass size
- Move to corner with less activity
- Add toggle keybind
Clicks Not Working
- Check
clickable = true - Verify
z_indexis highest - Ensure not blocked by
click_through
See Also
Transparent Overlays
Create semi-transparent widgets that layer over other content.
Goal
Display status information as overlays without completely blocking the view of the main game window.
Basic Transparent Widget
[[widgets]]
type = "progress"
name = "health_overlay"
title = ""
x = 1
y = 1
width = 20
height = 1
data_source = "vitals.health"
z_index = 100
transparent = true
opacity = 0.7
border = false
Overlay Status Bar
Minimal status overlay at top of screen:
# Transparent status strip
[[widgets]]
type = "dashboard"
name = "status_overlay"
x = 0
y = 0
width = 100
height = 1
z_index = 100
transparent = true
opacity = 0.8
border = false
components = ["health_mini", "mana_mini", "rt_mini"]
# Component definitions
[widgets.status_overlay.health_mini]
type = "progress"
width = 15
data_source = "vitals.health"
format = "HP:{value}"
[widgets.status_overlay.mana_mini]
type = "progress"
width = 15
data_source = "vitals.mana"
format = "MP:{value}"
[widgets.status_overlay.rt_mini]
type = "countdown"
width = 10
data_source = "roundtime"
format = "RT:{value}"
Corner Vitals Overlay
Health/mana in corner:
[[widgets]]
type = "text"
name = "main"
x = 0
y = 0
width = 100
height = 100
z_index = 1
# Health overlay (top left)
[[widgets]]
type = "progress"
name = "health"
x = 1
y = 1
width = 15
height = 1
data_source = "vitals.health"
z_index = 50
transparent = true
opacity = 0.75
border = false
show_text = true
format = "❤ {percent}%"
# Mana overlay (below health)
[[widgets]]
type = "progress"
name = "mana"
x = 1
y = 2
width = 15
height = 1
data_source = "vitals.mana"
z_index = 50
transparent = true
opacity = 0.75
border = false
show_text = true
format = "✦ {percent}%"
Overlay HUD Panel
Grouped status overlays:
[[widgets]]
type = "dashboard"
name = "hud"
x = "75%"
y = 1
width = "24%"
height = 15
z_index = 100
transparent = true
opacity = 0.85
border_style = "rounded"
border_opacity = 0.5
components = ["vitals", "timers", "status"]
[widgets.hud.vitals]
layout = "vertical"
items = ["health", "mana", "stamina", "spirit"]
[widgets.hud.timers]
layout = "horizontal"
items = ["roundtime", "casttime"]
[widgets.hud.status]
type = "indicator"
indicators = ["hidden", "stunned", "prone"]
Notification Overlay
Temporary overlay for alerts:
[[widgets]]
type = "text"
name = "notification"
x = "center"
y = 5
width = 40
height = 3
z_index = 200
transparent = true
opacity = 0.9
visible = false
auto_hide = 3000
border_style = "double"
align = "center"
Trigger configuration:
[[triggers]]
pattern = "You are stunned"
command = ".notify_show Stunned!"
Opacity Levels
Recommended opacity settings:
| Use Case | Opacity | Notes |
|---|---|---|
| Critical alerts | 0.95 | Nearly opaque |
| Status bars | 0.75-0.85 | Readable but see-through |
| Background info | 0.5-0.6 | Subtle overlay |
| Watermark | 0.2-0.3 | Barely visible |
How It Works
Transparency Rendering
When transparent = true:
- Background is not filled
- Only text/graphics are drawn
opacityaffects all rendering (0.0-1.0)
Z-Index Stacking
z_index = 1 # Base layer (main window)
z_index = 50 # Mid layer (status)
z_index = 100 # Top layer (HUD)
z_index = 200 # Alert layer (notifications)
Color Blending
With transparency, colors blend:
# Original: #ff0000 (red) at 0.5 opacity
# Over: #000000 (black) background
# Result: ~#800000 (dark red)
Advanced: Conditional Transparency
Change opacity based on state:
[[widgets]]
type = "progress"
name = "health"
transparent = true
opacity = 0.5
# Increase opacity when low
[widgets.health.dynamic_opacity]
condition = "value < 30"
opacity = 0.95
Tips
- Test Readability: Ensure text is readable over content
- Use High Contrast: Bright colors work better transparent
- Consider Activity: Busy areas need lower opacity
- Add Borders: Light borders improve definition
Troubleshooting
Text Hard to Read
- Increase opacity
- Add background blur (if supported)
- Use high contrast colors
- Add shadow/outline to text
Overlay Blocks Interaction
click_through = true # Pass clicks to window below
Flickering
- Reduce render rate for overlay
- Enable double buffering
- Use solid backgrounds for fast-updating widgets
Terminal Support
Transparency requires terminal support:
- Full support: iTerm2, Kitty, WezTerm
- Partial: Windows Terminal (with settings)
- None: Basic terminals, SSH
Check support:
echo $COLORTERM # Should include "truecolor"
See Also
Combat Alerts
Set up visual and audio notifications for combat events.
Goal
Never miss critical combat information with customized alerts for stuns, low health, deaths, and other important events.
Visual Alerts
Critical Highlight Pattern
# highlights.toml
# Stunned - high visibility
[[highlights]]
pattern = "(?i)you are stunned"
fg = "black"
bg = "yellow"
bold = true
flash = true
# Webbed
[[highlights]]
pattern = "(?i)webs? (stick|entangle|ensnare)"
fg = "black"
bg = "magenta"
bold = true
# Low health warning
[[highlights]]
pattern = "feel your life fading"
fg = "white"
bg = "red"
bold = true
flash = true
# Enemy death
[[highlights]]
pattern = "falls dead"
fg = "bright_yellow"
bold = true
# Critical hit received
[[highlights]]
pattern = "\\*\\* .+ \\*\\*"
fg = "bright_red"
bold = true
Audio Alerts
Sound Triggers
# triggers.toml
# Stun alert
[[triggers]]
name = "stun_sound"
pattern = "(?i)you are stunned"
sound = "alert_high.wav"
cooldown = 500
# Low health alert
[[triggers]]
name = "health_critical"
pattern = "feel your life fading|growing weak|death is near"
sound = "alarm.wav"
cooldown = 2000
# Enemy dies
[[triggers]]
name = "kill_sound"
pattern = "falls dead|crumples to the ground"
sound = "success.wav"
cooldown = 100
# Webbed
[[triggers]]
name = "web_sound"
pattern = "(?i)webs? (stick|entangle)"
sound = "warning.wav"
TTS Alerts
# triggers.toml
[[triggers]]
name = "stun_tts"
pattern = "(?i)you are stunned"
tts = "Stunned!"
tts_priority = "high"
[[triggers]]
name = "health_tts"
pattern = "feel your life fading"
tts = "Health critical!"
tts_priority = "urgent"
Status Widget Alerts
Flashing Indicators
# layout.toml
[[widgets]]
type = "indicator"
name = "combat_status"
x = 0
y = 85
width = 30
height = 3
indicators = ["stunned", "webbed", "prone", "bleeding"]
# Indicator styles
[widgets.combat_status.styles]
stunned = { fg = "black", bg = "yellow", flash = true }
webbed = { fg = "black", bg = "magenta", flash = true }
prone = { fg = "white", bg = "red" }
bleeding = { fg = "red", bg = "black", flash = true }
Health Bar Alerts
[[widgets]]
type = "progress"
name = "health"
data_source = "vitals.health"
# Color changes at thresholds
color_thresholds = [
{ value = 100, color = "health" },
{ value = 50, color = "health_low" },
{ value = 25, color = "health_critical", flash = true }
]
Combat Dashboard
Combined alert display:
[[widgets]]
type = "dashboard"
name = "combat_hud"
x = 75
y = 0
width = 25
height = 20
title = "Combat"
[widgets.combat_hud.components]
health = { type = "progress", data = "vitals.health" }
mana = { type = "progress", data = "vitals.mana" }
rt = { type = "countdown", data = "roundtime" }
status = { type = "indicator", items = ["stunned", "webbed", "prone"] }
target = { type = "text", data = "combat.target" }
Notification System
Popup Notifications
[[triggers]]
name = "death_notify"
pattern = "seems to have died|falls dead"
command = ".notify Enemy Killed!"
[[triggers]]
name = "stun_notify"
pattern = "(?i)you are stunned"
command = ".notify STUNNED!"
notification_style = "urgent"
Screen Flash
[[triggers]]
name = "critical_flash"
pattern = "feel your life fading"
action = "flash_screen"
flash_color = "red"
flash_duration = 200
Complete Combat Setup
highlights.toml
# Combat highlights
# Status effects
[[highlights]]
pattern = "(?i)you are stunned"
fg = "black"
bg = "yellow"
bold = true
[[highlights]]
pattern = "(?i)webs? (stick|entangle|ensnare)"
fg = "black"
bg = "magenta"
bold = true
[[highlights]]
pattern = "(?i)(knocked|fall) (down|prone|to the ground)"
fg = "black"
bg = "cyan"
bold = true
# Damage
[[highlights]]
pattern = "\\*\\* .+ \\*\\*"
fg = "bright_red"
bold = true
[[highlights]]
pattern = "\\d+ points? of damage"
fg = "red"
# Success
[[highlights]]
pattern = "falls dead|crumples"
fg = "bright_yellow"
bold = true
[[highlights]]
pattern = "(strike|hit|slash|stab).*(head|neck|chest)"
fg = "bright_green"
triggers.toml
# Combat triggers
[[triggers]]
name = "stun_alert"
pattern = "(?i)you are stunned"
sound = "stun.wav"
tts = "Stunned"
cooldown = 500
enabled = true
[[triggers]]
name = "web_alert"
pattern = "(?i)webs? (stick|entangle)"
sound = "web.wav"
cooldown = 500
[[triggers]]
name = "health_warning"
pattern = "feel your life fading|death is near"
sound = "alarm.wav"
tts = "Health critical"
tts_priority = "urgent"
cooldown = 3000
[[triggers]]
name = "kill_confirm"
pattern = "falls dead|crumples"
sound = "kill.wav"
cooldown = 100
Customizing Sounds
Sound File Locations
~/.two-face/sounds/
├── alert_high.wav
├── alarm.wav
├── success.wav
├── warning.wav
├── stun.wav
├── web.wav
└── kill.wav
Sound Settings
# config.toml
[sound]
enabled = true
volume = 0.7
alert_volume = 1.0 # Louder for alerts
Tips
- Prioritize Alerts: Not everything needs sound
- Use Cooldowns: Prevent alert spam
- Test Patterns: Verify regex matches correctly
- Balance Volume: Alerts should be noticeable but not jarring
See Also
Custom Status Bar
Create a personalized status display with exactly the information you need.
Goal
Build a compact status bar showing vitals, timers, and status in a format you design.
Basic Status Bar
Single-line status at bottom of screen:
[[widgets]]
type = "dashboard"
name = "statusbar"
x = 0
y = 98
width = 100
height = 2
border = false
layout = "horizontal"
components = ["health", "mana", "stamina", "spirit", "rt", "stance"]
spacing = 2
[widgets.statusbar.health]
type = "text"
format = "HP:{vitals.health}%"
color_condition = [
{ if = "value >= 70", color = "green" },
{ if = "value >= 30", color = "yellow" },
{ if = "value < 30", color = "red" }
]
[widgets.statusbar.mana]
type = "text"
format = "MP:{vitals.mana}%"
color = "blue"
[widgets.statusbar.stamina]
type = "text"
format = "ST:{vitals.stamina}%"
color = "orange"
[widgets.statusbar.spirit]
type = "text"
format = "SP:{vitals.spirit}%"
color = "magenta"
[widgets.statusbar.rt]
type = "text"
format = "RT:{roundtime}"
visible_when = "roundtime > 0"
color = "cyan"
[widgets.statusbar.stance]
type = "text"
format = "[{stance}]"
color = "white"
Mini Vitals Bar
Compact progress bars:
[[widgets]]
type = "dashboard"
name = "mini_vitals"
x = 0
y = 0
width = 60
height = 1
border = false
layout = "horizontal"
[widgets.mini_vitals.components]
hp = { type = "progress", width = 12, data = "vitals.health", format = "♥{value}" }
mp = { type = "progress", width = 12, data = "vitals.mana", format = "♦{value}" }
st = { type = "progress", width = 12, data = "vitals.stamina", format = "⚡{value}" }
sp = { type = "progress", width = 12, data = "vitals.spirit", format = "✧{value}" }
Icon Status Bar
Using Unicode symbols:
[[widgets]]
type = "dashboard"
name = "icon_status"
x = 0
y = 99
width = 100
height = 1
border = false
# Format: ❤95 ♦100 ⚡87 ✧100 ⏱3 🛡Off 👁Hid
components = [
{ icon = "❤", data = "vitals.health", color = "health" },
{ icon = "♦", data = "vitals.mana", color = "mana" },
{ icon = "⚡", data = "vitals.stamina", color = "stamina" },
{ icon = "✧", data = "vitals.spirit", color = "spirit" },
{ icon = "⏱", data = "roundtime", visible_when = "> 0" },
{ icon = "🛡", data = "stance", format = "{short}" },
{ icon = "👁", data = "hidden", format = "{status}" }
]
Two-Line Status
More detailed status bar:
[[widgets]]
type = "dashboard"
name = "status_two_line"
x = 0
y = 97
width = 100
height = 3
[widgets.status_two_line.line1]
layout = "horizontal"
items = [
"HP: {vitals.health}%",
"MP: {vitals.mana}%",
"ST: {vitals.stamina}%",
"SP: {vitals.spirit}%"
]
[widgets.status_two_line.line2]
layout = "horizontal"
items = [
"RT: {roundtime}s",
"CT: {casttime}s",
"Stance: {stance}",
"Room: {room.name}"
]
Status with Indicators
Include status flags:
[[widgets]]
type = "dashboard"
name = "full_status"
x = 0
y = 95
width = 100
height = 5
[widgets.full_status.sections]
vitals = { row = 1, content = "HP:{health}% MP:{mana}% ST:{stamina}% SP:{spirit}%" }
timers = { row = 2, content = "RT:{rt} CT:{ct}" }
flags = {
row = 3,
type = "indicators",
items = ["hidden", "invisible", "stunned", "webbed", "prone"],
style = "compact"
}
location = { row = 4, content = "[{room.name}]", align = "center" }
Minimal Status
Just the essentials:
[[widgets]]
type = "text"
name = "minimal_status"
x = 0
y = 99
width = 100
height = 1
border = false
content = "{health}❤ {mana}♦ {rt}⏱ {room.short_name}"
Contextual Status
Shows different info based on situation:
[[widgets]]
type = "dashboard"
name = "context_status"
[widgets.context_status.modes]
# Default mode
default = "HP:{health} MP:{mana} [{room.name}]"
# Combat mode (when in combat)
combat = "HP:{health} MP:{mana} RT:{rt} Target:{target}"
# Town mode (when in town)
town = "Silver:{silver} [{room.name}]"
[widgets.context_status.mode_triggers]
combat = "roundtime > 0 OR target != ''"
town = "room.type == 'town'"
Color Themes
Light Theme Status
[widgets.statusbar.theme]
background = "#e0e0e0"
text = "#333333"
health = "#00aa00"
health_low = "#aaaa00"
health_critical = "#aa0000"
mana = "#0000aa"
Dark Theme Status
[widgets.statusbar.theme]
background = "#1a1a1a"
text = "#c0c0c0"
health = "#00ff00"
health_low = "#ffff00"
health_critical = "#ff0000"
mana = "#0080ff"
Dynamic Updates
Flashing on Change
[widgets.statusbar.health]
flash_on_decrease = true
flash_duration = 500
flash_color = "red"
Smooth Transitions
[widgets.statusbar.health]
animate_changes = true
animation_duration = 200
Tips
- Keep It Compact: Status bar should be quick to read
- Prioritize Information: Most important data first/largest
- Use Color Wisely: Colors should convey meaning
- Test Readability: Ensure text is readable at a glance
Complete Example
# A complete status bar setup
[[widgets]]
type = "dashboard"
name = "main_status"
x = 0
y = 98
width = 100
height = 2
border = false
background = "#1a1a1a"
[widgets.main_status.row1]
items = [
{ text = "❤ ", color = "red" },
{ data = "vitals.health", width = 3, align = "right" },
{ text = "% " },
{ text = "♦ ", color = "blue" },
{ data = "vitals.mana", width = 3, align = "right" },
{ text = "% " },
{ text = "⚡ ", color = "orange" },
{ data = "vitals.stamina", width = 3, align = "right" },
{ text = "% " },
{ text = "│ ", color = "gray" },
{ text = "RT:", visible_when = "roundtime > 0" },
{ data = "roundtime", visible_when = "roundtime > 0" },
{ text = " │ ", color = "gray" },
{ indicators = ["hidden", "stunned", "prone"], style = "icons" }
]
See Also
Tabbed Channels
Organize different communication streams into tabs for cleaner chat management.
Goal
Separate game text, combat, and various chat channels into organized tabs you can quickly switch between.
Basic Tabbed Chat
[[widgets]]
type = "tabbed_text"
name = "channels"
x = 0
y = 60
width = 100
height = 25
[[widgets.channels.tabs]]
name = "All"
streams = ["speech", "whisper", "thoughts", "group"]
scrollback = 2000
[[widgets.channels.tabs]]
name = "Speech"
streams = ["speech"]
scrollback = 1000
[[widgets.channels.tabs]]
name = "Whispers"
streams = ["whisper"]
scrollback = 500
highlight_new = true
[[widgets.channels.tabs]]
name = "Thoughts"
streams = ["thoughts"]
scrollback = 500
Combat + Chat Tabs
Separate combat from communication:
[[widgets]]
type = "tabbed_text"
name = "main_tabs"
x = 0
y = 0
width = 75
height = 85
[[widgets.main_tabs.tabs]]
name = "Game"
streams = ["main", "room"]
scrollback = 5000
default = true
[[widgets.main_tabs.tabs]]
name = "Combat"
streams = ["combat"]
scrollback = 2000
notify_on_activity = true
[[widgets.main_tabs.tabs]]
name = "Chat"
streams = ["speech", "whisper", "thoughts"]
scrollback = 1000
notify_on_activity = true
[[widgets.main_tabs.tabs]]
name = "System"
streams = ["logons", "deaths", "experience"]
scrollback = 500
Notification Indicators
Show unread indicators on tabs:
[[widgets]]
type = "tabbed_text"
name = "chat_tabs"
[widgets.chat_tabs.settings]
show_unread_count = true
unread_indicator = "●"
unread_color = "yellow"
flash_on_highlight = true
[[widgets.chat_tabs.tabs]]
name = "Whisper"
streams = ["whisper"]
# Tab shows: "Whisper ●3" when 3 unread messages
notify_pattern = ".*" # All messages count
Keybind Navigation
Quick tab switching:
# keybinds.toml
[keybinds."alt+1"]
action = "switch_tab"
widget = "channels"
tab = 0
[keybinds."alt+2"]
action = "switch_tab"
widget = "channels"
tab = 1
[keybinds."alt+3"]
action = "switch_tab"
widget = "channels"
tab = 2
[keybinds."alt+4"]
action = "switch_tab"
widget = "channels"
tab = 3
# Cycle tabs
[keybinds."ctrl+tab"]
action = "next_tab"
widget = "channels"
[keybinds."ctrl+shift+tab"]
action = "prev_tab"
widget = "channels"
Filtered Tabs
Create tabs with filtered content:
[[widgets.channels.tabs]]
name = "Trade"
streams = ["speech"]
filter_pattern = "(?i)(sell|buy|trade|silver|coin)"
scrollback = 500
[[widgets.channels.tabs]]
name = "Group"
streams = ["speech", "thoughts"]
filter_pattern = "\\[Group\\]|\\[Party\\]"
scrollback = 500
Tab Styles
Minimal Tabs
[widgets.channels.style]
tab_position = "top"
tab_style = "minimal"
# Shows: [Game|Combat|Chat]
separator = "|"
active_indicator = "underline"
Full Tabs
[widgets.channels.style]
tab_position = "top"
tab_style = "full"
# Shows: ╔════════╗╔═══════╗
# ║ Game ║║Combat ║
active_bg = "blue"
inactive_bg = "gray"
Bottom Tabs
[widgets.channels.style]
tab_position = "bottom"
Per-Tab Settings
Different settings per tab:
[[widgets.channels.tabs]]
name = "Game"
streams = ["main", "room"]
scrollback = 5000
wrap = true
timestamps = false
[[widgets.channels.tabs]]
name = "Combat"
streams = ["combat"]
scrollback = 1000
wrap = false
timestamps = true
timestamp_format = "[%H:%M:%S]"
[[widgets.channels.tabs]]
name = "Log"
streams = ["experience", "logons", "deaths"]
scrollback = 500
timestamps = true
log_to_file = true
log_file = "~/.two-face/logs/events.log"
Searchable Tabs
Enable search within tabs:
[widgets.channels.settings]
searchable = true
search_keybind = "ctrl+f"
search_highlight = "yellow"
Auto-Focus Tab
Switch to tab on activity:
[[widgets.channels.tabs]]
name = "Whispers"
streams = ["whisper"]
auto_focus_on_activity = true
# Automatically switches to this tab when whisper received
Or only for specific patterns:
[[widgets.channels.tabs]]
name = "Important"
streams = ["whisper", "thoughts"]
auto_focus_pattern = "(?i)(urgent|emergency|help)"
Complete Setup
layout.toml
# Main game area with tabs
[[widgets]]
type = "tabbed_text"
name = "main_tabs"
x = 0
y = 0
width = 75
height = 70
[widgets.main_tabs.settings]
tab_position = "top"
show_unread_count = true
default_tab = "Game"
[[widgets.main_tabs.tabs]]
name = "Game"
streams = ["main", "room"]
scrollback = 5000
[[widgets.main_tabs.tabs]]
name = "Combat"
streams = ["combat"]
scrollback = 2000
# Chat area with tabs
[[widgets]]
type = "tabbed_text"
name = "chat_tabs"
x = 0
y = 71
width = 75
height = 14
[widgets.chat_tabs.settings]
tab_position = "top"
show_unread_count = true
[[widgets.chat_tabs.tabs]]
name = "All"
streams = ["speech", "whisper", "thoughts"]
scrollback = 1000
[[widgets.chat_tabs.tabs]]
name = "Local"
streams = ["speech"]
[[widgets.chat_tabs.tabs]]
name = "Private"
streams = ["whisper"]
notify_on_activity = true
[[widgets.chat_tabs.tabs]]
name = "ESP"
streams = ["thoughts"]
keybinds.toml
# Tab navigation
[keybinds."f1"]
action = "focus_widget"
widget = "main_tabs"
[keybinds."f2"]
action = "focus_widget"
widget = "chat_tabs"
[keybinds."ctrl+1"]
action = "switch_tab"
widget = "main_tabs"
tab = 0
[keybinds."ctrl+2"]
action = "switch_tab"
widget = "main_tabs"
tab = 1
Tips
- Don’t Over-Tab: Too many tabs defeats organization
- Use Notifications: Visual cues for important tabs
- Set Keybinds: Quick switching is essential
- Filter Wisely: Regex filters can miss messages
See Also
Hunting HUD
Create an optimized heads-up display for hunting with all critical information visible at a glance.
Goal
A compact layout showing health, mana, roundtime, target, buffs, and room info - everything a hunter needs without cluttering the main text area.
Layout
┌─────────────────────────────────────────────┬──────────────────┐
│ │ ▓▓▓▓▓▓▓▓░░ 326 │ Health
│ │ ▓▓▓▓▓▓▓▓▓▓ 481 │ Mana
│ │ ▓▓▓▓▓▓▓▓▓░ 223 │ Stamina
│ Main Game Text ├──────────────────┤
│ │ RT: ██░░░ 3s │
│ │ CT: ░░░░░ 0s │
│ ├──────────────────┤
│ │ N │
│ │ W + E │
│ │ S │
├─────────────────────────────────────────────┼──────────────────┤
│ Room: Angargreft, Pits of the Dead │ Target: valravn │
│ Exits: north, southeast │ [HIDDEN] [STANCE]│
├─────────────────────────────────────────────┴──────────────────┤
│ > _ │
└────────────────────────────────────────────────────────────────┘
Configuration
layout.toml
[layout]
name = "Hunting HUD"
columns = 120
rows = 40
# Main text window - takes most of the screen
[[widgets]]
type = "text"
name = "main"
stream = "main"
x = 0
y = 0
width = 85
height = 32
buffer_size = 3000
show_border = true
title = "Game"
# Vitals panel (right side, top)
[[widgets]]
type = "progress"
name = "health"
data_source = "vitals.health"
x = 85
y = 0
width = 35
height = 1
color = "health"
show_text = true
show_border = false
[[widgets]]
type = "progress"
name = "mana"
data_source = "vitals.mana"
x = 85
y = 1
width = 35
height = 1
color = "mana"
show_text = true
show_border = false
[[widgets]]
type = "progress"
name = "stamina"
data_source = "vitals.stamina"
x = 85
y = 2
width = 35
height = 1
color = "stamina"
show_text = true
show_border = false
[[widgets]]
type = "progress"
name = "spirit"
data_source = "vitals.spirit"
x = 85
y = 3
width = 35
height = 1
color = "spirit"
show_text = true
show_border = false
# Timers
[[widgets]]
type = "countdown"
name = "roundtime"
countdown_id = "roundtime"
x = 85
y = 5
width = 35
height = 1
color = "yellow"
label = "RT"
[[widgets]]
type = "countdown"
name = "casttime"
countdown_id = "casttime"
x = 85
y = 6
width = 35
height = 1
color = "cyan"
label = "CT"
# Compass
[[widgets]]
type = "compass"
name = "compass"
x = 85
y = 8
width = 35
height = 5
style = "graphical"
show_border = true
# Status indicators
[[widgets]]
type = "indicator"
name = "status"
x = 85
y = 14
width = 35
height = 2
indicators = ["hidden", "stunned", "webbed", "prone", "kneeling", "sitting"]
compact = true
# Active buffs (scrollable)
[[widgets]]
type = "effects"
name = "buffs"
category = "Buffs"
x = 85
y = 17
width = 35
height = 15
show_timers = true
show_border = true
title = "Buffs"
# Room info bar
[[widgets]]
type = "room"
name = "room_bar"
x = 0
y = 32
width = 85
height = 3
show_exits = true
compact = true
show_border = true
# Target display
[[widgets]]
type = "text"
name = "target"
stream = "target"
x = 85
y = 32
width = 35
height = 3
show_border = true
title = "Target"
# Command input
[[widgets]]
type = "input"
name = "command"
x = 0
y = 35
width = 120
height = 3
history_size = 200
prompt = "> "
show_border = true
highlights.toml (Hunting)
# Monster names (bold red)
[[highlights]]
pattern = "\\b(orc|troll|goblin|valravn|panther|drake|golem)\\b"
fg = "bright_red"
bold = true
# Your attacks hitting
[[highlights]]
pattern = "(You|Your).*(strike|hit|slash|stab|fire|cast)"
fg = "bright_green"
# Critical hits
[[highlights]]
pattern = "\\*\\* .+ \\*\\*"
fg = "bright_yellow"
bold = true
# Damage numbers
[[highlights]]
pattern = "\\d+ points? of damage"
fg = "red"
# Enemy death
[[highlights]]
pattern = "(falls dead|crumples|expires|dies)"
fg = "bright_yellow"
bold = true
# Loot
[[highlights]]
pattern = "(silver|coins|silvers|drops? a)"
fg = "bright_yellow"
# Stun warning
[[highlights]]
pattern = "(?i)you are stunned"
fg = "black"
bg = "yellow"
bold = true
flash = true
# Webbed
[[highlights]]
pattern = "(?i)webs? (stick|entangle)"
fg = "black"
bg = "magenta"
bold = true
keybinds.toml (Hunting)
# Quick stance changes
[[keybinds]]
key = "F1"
action = "send"
command = "stance offensive"
[[keybinds]]
key = "F2"
action = "send"
command = "stance defensive"
# Quick hide
[[keybinds]]
key = "F3"
action = "send"
command = "hide"
# Target next
[[keybinds]]
key = "F4"
action = "send"
command = "target next"
# Attack
[[keybinds]]
key = "F5"
action = "send"
command = "attack"
# Loot all
[[keybinds]]
key = "F6"
action = "send"
command = "loot"
# Quick look
[[keybinds]]
key = "F7"
action = "send"
command = "look"
# Health check
[[keybinds]]
key = "F8"
action = "send"
command = "health"
Triggers for Hunting
# triggers.toml
# Stun alert
[[triggers]]
name = "stun_alert"
pattern = "(?i)you are stunned"
sound = "stun.wav"
tts = "Stunned"
cooldown = 500
# Low health warning
[[triggers]]
name = "health_low"
pattern = "feel your life fading|death is near"
sound = "alarm.wav"
tts = "Health critical"
cooldown = 3000
# Enemy death confirmation
[[triggers]]
name = "kill_confirm"
pattern = "falls dead|crumples|expires"
sound = "kill.wav"
cooldown = 100
Tips
- Adjust widths based on your terminal size
- Add more indicators for your profession-specific statuses
- Customize highlights for creatures in your hunting area
- Use keybinds for your most common combat actions
See Also
Buff Tracker
Display active buffs, debuffs, and spell effects with countdown timers.
Goal
Track all active effects on your character with visual progress bars showing time remaining.
The Problem
GemStone IV sends buff data via the dialogData id='Buffs' XML elements. These include spell effects, sigils, and other timed buffs with progress bars and countdown timers.
Layout
┌─────────────────────────────────────────────┬────────────────────────┐
│ │ Active Effects │
│ ├────────────────────────┤
│ │ ▓▓▓▓▓▓▓░░░ 3:44 │
│ │ Fasthr's Reward │
│ │ │
│ Main Game Text │ ▓▓▓▓▓▓▓▓▓▓ 0:76 │
│ │ Celerity │
│ │ │
│ │ ▓▓▓▓░░░░░░ 1:57 │
│ │ Sigil of Defense │
│ │ │
│ │ ▓▓░░░░░░░░ 0:08 │
│ │ Sigil of Major Bane │
└─────────────────────────────────────────────┴────────────────────────┘
Configuration
layout.toml
# Active buffs panel
[[widgets]]
type = "effects"
name = "buffs"
category = "Buffs"
x = 85
y = 0
width = 35
height = 20
show_timers = true
show_border = true
title = "Active Effects"
sort_by = "time_remaining" # or "name", "value"
# Debuffs panel (if you want separate tracking)
[[widgets]]
type = "effects"
name = "debuffs"
category = "Debuffs"
x = 85
y = 20
width = 35
height = 10
show_timers = true
show_border = true
title = "Debuffs"
color_scheme = "danger"
# Cooldowns panel
[[widgets]]
type = "effects"
name = "cooldowns"
category = "Cooldowns"
x = 85
y = 30
width = 35
height = 8
show_timers = true
show_border = true
title = "Cooldowns"
Color Configuration
# colors.toml
# Buff bar colors by time remaining
buff_full = "#00FF00" # Green - plenty of time
buff_medium = "#FFFF00" # Yellow - getting low
buff_low = "#FF6600" # Orange - almost expired
buff_critical = "#FF0000" # Red - about to expire
# Category colors
buffs_color = "#00AAFF" # Blue for buffs
debuffs_color = "#FF0000" # Red for debuffs
cooldowns_color = "#FFAA00" # Orange for cooldowns
Alerts for Expiring Buffs
# triggers.toml
# Celerity about to expire
[[triggers]]
name = "celerity_warning"
pattern = "The heightened speed of Celerity fades"
tts = "Celerity expired"
sound = "buff_expire.wav"
# Spell shield warning
[[triggers]]
name = "shield_warning"
pattern = "Your (spell|elemental) shield dissipates"
tts = "Shield down"
sound = "warning.wav"
# Generic buff fade
[[triggers]]
name = "buff_fade"
pattern = "The (glow|aura|effect) of .+ fades"
sound = "buff_expire.wav"
cooldown = 1000
Advanced: Profession-Specific Buffs
Ranger Buffs
[[widgets]]
type = "effects"
name = "ranger_buffs"
category = "Buffs"
filter = ["Camouflage", "Nature's Touch", "Resist Nature", "Tangle Weed"]
x = 85
y = 0
width = 35
height = 10
Wizard Buffs
[[widgets]]
type = "effects"
name = "wizard_buffs"
category = "Buffs"
filter = ["Elemental Defense", "Elemental Targeting", "Haste", "Familiar"]
x = 85
y = 0
width = 35
height = 10
Cleric Buffs
[[widgets]]
type = "effects"
name = "cleric_buffs"
category = "Buffs"
filter = ["Spirit Shield", "Spirit Warding", "Benediction", "Prayer"]
x = 85
y = 0
width = 35
height = 10
Compact Mode
For smaller displays:
[[widgets]]
type = "effects"
name = "buffs_compact"
category = "Buffs"
x = 85
y = 0
width = 25
height = 15
show_timers = true
compact = true # Single line per buff
show_progress = false # Hide progress bars, just show time
abbreviate_names = true # "Sigil of Def" instead of full name
Tips
- Sort by time to see what’s expiring soon
- Use filters to show only profession-relevant buffs
- Add sound alerts for critical buff expirations
- Compact mode works well for limited screen space
XML Reference
The game sends buff data like this:
<dialogData id='Buffs' clear='t'></dialogData>
<dialogData id='Buffs'>
<progressBar id='115' value='89' text="Fasthr's Reward"
left='22%' top='0' width='76%' height='15' time='03:44:32'/>
<label id='l115' value='3:44 ' top='0' left='0' justify='2'/>
<!-- more buffs... -->
</dialogData>
Two-Face parses this into ActiveEffect elements with:
id- Spell/effect identifiertext- Display namevalue- Progress percentage (0-100)time- Time remaining string
See Also
Encumbrance Monitor
Track your carrying capacity with visual feedback and alerts.
Goal
Display your current encumbrance level with color-coded warnings as you pick up loot.
The Data
GemStone IV sends encumbrance via dialogData id='encum':
<dialogData id='encum'>
<progressBar id='encumlevel' value='10' text='Light' .../>
<label id='encumblurb' value='You feel confident that your load
is not affecting your actions very much.' .../>
</dialogData>
Layout Options
Option 1: Progress Bar
[[widgets]]
type = "progress"
name = "encumbrance"
data_source = "encumbrance"
x = 0
y = 35
width = 30
height = 1
show_text = true
color_thresholds = [
{ value = 100, color = "#00FF00" }, # Light - green
{ value = 75, color = "#66FF00" }, # Moderate - yellow-green
{ value = 50, color = "#FFFF00" }, # Significant - yellow
{ value = 25, color = "#FF6600" }, # Heavy - orange
{ value = 0, color = "#FF0000" } # Overloaded - red
]
Option 2: Text Display
[[widgets]]
type = "text"
name = "encum_display"
stream = "encumbrance"
x = 0
y = 35
width = 50
height = 2
show_border = true
title = "Load"
Option 3: In Status Bar
[[widgets]]
type = "dashboard"
name = "status_bar"
x = 0
y = 37
width = 120
height = 1
horizontal = true
[widgets.status_bar.components]
health = { type = "progress", data = "vitals.health", width = 20 }
mana = { type = "progress", data = "vitals.mana", width = 20 }
encum = { type = "progress", data = "encumbrance", width = 15 }
stance = { type = "text", data = "stance", width = 15 }
Highlights for Encumbrance Messages
# highlights.toml
# Light load - green
[[highlights]]
pattern = "Your current load is Light"
fg = "bright_green"
# Moderate - yellow
[[highlights]]
pattern = "Your current load is (Moderate|Somewhat)"
fg = "yellow"
# Heavy - orange
[[highlights]]
pattern = "Your current load is (Significant|Heavy)"
fg = "bright_yellow"
bold = true
# Overloaded - red
[[highlights]]
pattern = "Your current load is (Very Heavy|Overloaded|Extreme)"
fg = "bright_red"
bold = true
Alerts
# triggers.toml
# Getting heavy
[[triggers]]
name = "encum_heavy"
pattern = "Your load (is now|has become) (Heavy|Very Heavy)"
tts = "Load getting heavy"
sound = "warning.wav"
# Overloaded
[[triggers]]
name = "encum_overloaded"
pattern = "overloaded|encumbered"
tts = "Overloaded!"
sound = "alarm.wav"
cooldown = 5000
# Back to light
[[triggers]]
name = "encum_light"
pattern = "Your load (is now|has become) Light"
sound = "success.wav"
Encumbrance Levels
| Level | Value | Effect |
|---|---|---|
| Light | 0-20% | No penalty |
| Moderate | 20-40% | Minor RT increase |
| Significant | 40-60% | Noticeable RT increase |
| Heavy | 60-80% | Major penalties |
| Very Heavy | 80-100% | Severe penalties |
| Overloaded | 100%+ | Can’t move |
Merchant/Looting Setup
For heavy looting sessions:
# Dedicated encumbrance window
[[widgets]]
type = "text"
name = "loot_log"
stream = "loot"
x = 85
y = 20
width = 35
height = 10
show_border = true
title = "Loot"
# Encumbrance bar always visible
[[widgets]]
type = "progress"
name = "encum"
data_source = "encumbrance"
x = 85
y = 31
width = 35
height = 2
show_text = true
title = "Load"
Loot Highlights
# Valuable loot
[[highlights]]
pattern = "(drops? a|You also see).*(gem|jewel|gold|platinum|diamond)"
fg = "bright_yellow"
bold = true
# Silver/coins
[[highlights]]
pattern = "\\d+ (silver|silvers|coins)"
fg = "yellow"
# Boxes/containers
[[highlights]]
pattern = "(strongbox|chest|coffer|box|lockbox)"
fg = "cyan"
Tips
- Position strategically - Put encumbrance where you’ll notice it
- Use color coding - Visual feedback is faster than reading text
- Set up alerts - Don’t get stuck overloaded in a hunting ground
- Track loot separately - Dedicated loot window helps manage inventory
See Also
Stance Display
Show your current combat stance with quick-switch controls.
Goal
Display current stance (offensive/defensive) with visual indicator and easy keybind switching.
The Data
GemStone IV sends stance data via multiple XML elements:
<dialogData id='stance'>
<progressBar id='pbarStance' value='100' text='defensive (100%)' .../>
</dialogData>
<dialogData id='combat'>
<dropDownBox id='dDBStance' value="defensive"
cmd='_stance %dDBStance%'
content_text='offensive,advance,forward,neutral,guarded,defensive' .../>
</dialogData>
Layout Options
Option 1: Progress Bar
[[widgets]]
type = "progress"
name = "stance"
data_source = "stance"
x = 85
y = 5
width = 25
height = 1
show_text = true
color_dynamic = true # Color based on stance type
Option 2: Text Indicator
[[widgets]]
type = "indicator"
name = "stance_indicator"
x = 85
y = 5
width = 15
height = 1
indicators = ["offensive", "defensive", "neutral"]
style = "text"
Option 3: Combined with Vitals
[[widgets]]
type = "dashboard"
name = "combat_status"
x = 85
y = 0
width = 35
height = 8
title = "Combat"
[widgets.combat_status.components]
health = { type = "progress", data = "vitals.health", row = 0 }
mana = { type = "progress", data = "vitals.mana", row = 1 }
stance = { type = "progress", data = "stance", row = 2 }
rt = { type = "countdown", data = "roundtime", row = 3 }
Stance Colors
# colors.toml
# Stance-specific colors
stance_offensive = "#FF0000" # Red - aggressive
stance_advance = "#FF6600" # Orange
stance_forward = "#FFAA00" # Yellow-orange
stance_neutral = "#FFFF00" # Yellow
stance_guarded = "#00AAFF" # Light blue
stance_defensive = "#00FF00" # Green - safe
Quick Stance Keybinds
# keybinds.toml
# F1-F6 for stance ladder
[[keybinds]]
key = "F1"
action = "send"
command = "stance offensive"
[[keybinds]]
key = "F2"
action = "send"
command = "stance advance"
[[keybinds]]
key = "F3"
action = "send"
command = "stance forward"
[[keybinds]]
key = "F4"
action = "send"
command = "stance neutral"
[[keybinds]]
key = "F5"
action = "send"
command = "stance guarded"
[[keybinds]]
key = "F6"
action = "send"
command = "stance defensive"
# Quick toggles
[[keybinds]]
key = "Ctrl+O"
action = "send"
command = "stance offensive"
[[keybinds]]
key = "Ctrl+D"
action = "send"
command = "stance defensive"
Stance Highlights
# highlights.toml
# Stance change confirmations
[[highlights]]
pattern = "You are now in an? (offensive|advance) stance"
fg = "red"
bold = true
[[highlights]]
pattern = "You are now in a (forward|neutral) stance"
fg = "yellow"
[[highlights]]
pattern = "You are now in a (guarded|defensive) stance"
fg = "green"
bold = true
Stance Ladder Reference
| Stance | DS% | AS Penalty | Best For |
|---|---|---|---|
| Offensive | 0% | None | Max damage |
| Advance | 20% | -5 | Aggressive |
| Forward | 40% | -10 | Balanced attack |
| Neutral | 60% | -15 | Balanced |
| Guarded | 80% | -20 | Cautious |
| Defensive | 100% | -25 | Max defense |
Combat Triggers
# triggers.toml
# Auto-defensive on stun
[[triggers]]
name = "stun_defensive"
pattern = "(?i)you are stunned"
command = "stance defensive"
enabled = false # Enable if you want auto-stance
# Stance reminder when attacking
[[triggers]]
name = "offensive_reminder"
pattern = "Roundtime: \\d+ sec"
condition = "stance != offensive"
tts = "Not in offensive stance"
enabled = false
Tips
- Color code your stance for instant recognition
- Keybind frequently used stances - F1/F6 for offensive/defensive is common
- Consider auto-stance triggers for safety (controversial - some consider it automation)
- Position near RT - Stance and roundtime are often checked together
See Also
Target Window
Display your current combat target with quick targeting controls.
Goal
Show the currently targeted creature with health estimation and quick-switch capabilities.
The Data
GemStone IV sends target data via:
<dialogData id='combat'>
<dropDownBox id='dDBTarget' value="black valravn"
cmd="target %dDBTarget%"
content_text="none,black valravn,troll"
content_value="target help,#534103532,#534103533" .../>
</dialogData>
And targeting messages:
You are now targeting an eyeless black valravn.
Layout
[[widgets]]
type = "text"
name = "target"
stream = "target"
x = 85
y = 10
width = 35
height = 5
show_border = true
title = "Target"
Target Highlights
# highlights.toml
# Currently targeting
[[highlights]]
pattern = "You are now targeting"
fg = "bright_cyan"
bold = true
# Target cleared
[[highlights]]
pattern = "You are no longer targeting"
fg = "gray"
# Target status
[[highlights]]
pattern = "appears (dead|stunned|webbed|prone)"
fg = "bright_yellow"
bold = true
Targeting Keybinds
# keybinds.toml
# Target cycling
[[keybinds]]
key = "Tab"
action = "send"
command = "target next"
[[keybinds]]
key = "Shift+Tab"
action = "send"
command = "target previous"
# Clear target
[[keybinds]]
key = "Escape"
action = "send"
command = "target clear"
# Target random
[[keybinds]]
key = "Ctrl+T"
action = "send"
command = "target random"
# Attack current target
[[keybinds]]
key = "F5"
action = "send"
command = "attack"
# Target and attack
[[keybinds]]
key = "Ctrl+A"
action = "send"
command = "target random;attack"
Room Objects Integration
Combine with room objects display:
[[widgets]]
type = "room"
name = "room_with_targets"
x = 0
y = 30
width = 85
height = 5
show_objects = true
show_players = true
highlight_targets = true # Highlight targetable creatures
Target Status Parsing
Track target condition from combat messages:
# highlights.toml
# Target health estimation
[[highlights]]
pattern = "(appears to be |seems |is )(uninjured|in good shape)"
fg = "green"
[[highlights]]
pattern = "(has minor |slight |a few )(wounds|injuries|scratches)"
fg = "yellow"
[[highlights]]
pattern = "(has moderate |significant |serious )(wounds|injuries)"
fg = "orange"
[[highlights]]
pattern = "(has severe |critical |grievous |terrible )(wounds|injuries)"
fg = "red"
bold = true
[[highlights]]
pattern = "(appears dead|lies dead|crumples|falls dead)"
fg = "bright_green"
bold = true
Multiple Target Tracking
For hunting multiple creatures:
[[widgets]]
type = "text"
name = "creatures"
stream = "room_objects"
x = 85
y = 10
width = 35
height = 10
show_border = true
title = "Creatures"
filter = "monster" # Only show creatures, not items
Quick Target Macros
# Target specific creature types
[[keybinds]]
key = "Ctrl+1"
action = "send"
command = "target orc"
[[keybinds]]
key = "Ctrl+2"
action = "send"
command = "target troll"
[[keybinds]]
key = "Ctrl+3"
action = "send"
command = "target valravn"
# Target by condition
[[keybinds]]
key = "Ctrl+S"
action = "send"
command = "target stunned"
[[keybinds]]
key = "Ctrl+W"
action = "send"
command = "target webbed"
Combat Dashboard with Target
[[widgets]]
type = "dashboard"
name = "combat_hud"
x = 85
y = 0
width = 35
height = 20
title = "Combat"
[widgets.combat_hud.components]
target = { type = "text", data = "target", height = 3 }
health = { type = "progress", data = "vitals.health" }
rt = { type = "countdown", data = "roundtime" }
stance = { type = "progress", data = "stance" }
status = { type = "indicator", items = ["stunned", "hidden"] }
Tips
- Use Tab for cycling - Natural feel for target switching
- Color-code target status - Know at a glance if target is hurt
- Combine with room display - See all potential targets
- Quick macros - Target specific creature types you hunt often
See Also
Loot Window
Dedicated window for tracking loot drops and treasure.
Goal
Separate loot messages from main game text for easy tracking during hunting sessions.
Configuration
Basic Loot Window
[[widgets]]
type = "text"
name = "loot"
stream = "loot"
x = 85
y = 20
width = 35
height = 15
buffer_size = 500
show_border = true
title = "Loot"
Stream Filter Setup
Configure what goes to the loot stream:
# In config.toml
[streams.loot]
patterns = [
"drops? a",
"falls to the ground",
"\\d+ (silver|silvers|coins)",
"You also see",
"(gem|jewel|gold|platinum|diamond)",
"(strongbox|chest|coffer|lockbox)"
]
Loot Highlights
# highlights.toml
# Currency
[[highlights]]
pattern = "\\d+ (silver|silvers|coins)"
fg = "bright_yellow"
bold = true
# Valuable gems
[[highlights]]
pattern = "(diamond|emerald|ruby|sapphire|pearl)"
fg = "bright_cyan"
bold = true
# Boxes (lockpicking)
[[highlights]]
pattern = "(strongbox|chest|coffer|lockbox|box)"
fg = "cyan"
# Rare drops
[[highlights]]
pattern = "(rare|unusual|exceptional|perfect)"
fg = "bright_magenta"
bold = true
# Skins/trophies
[[highlights]]
pattern = "(skin|pelt|hide|claw|fang|horn)"
fg = "yellow"
Loot Alerts
# triggers.toml
# Big silver drop
[[triggers]]
name = "big_silver"
pattern = "(\\d{4,}) silvers?"
tts = "Big silver drop"
sound = "coins.wav"
# Box drop
[[triggers]]
name = "box_drop"
pattern = "drops? a.*(strongbox|chest|coffer)"
sound = "box.wav"
# Rare item
[[triggers]]
name = "rare_drop"
pattern = "drops? a.*(rare|unusual|exceptional)"
tts = "Rare item"
sound = "rare.wav"
Loot Summary Widget
Track session totals:
[[widgets]]
type = "dashboard"
name = "loot_summary"
x = 85
y = 35
width = 35
height = 3
title = "Session"
[widgets.loot_summary.components]
silver = { type = "counter", data = "session.silver", label = "Silver" }
boxes = { type = "counter", data = "session.boxes", label = "Boxes" }
kills = { type = "counter", data = "session.kills", label = "Kills" }
Multi-Tab Loot Tracking
[[widgets]]
type = "tabbed_text"
name = "treasure"
x = 85
y = 15
width = 35
height = 20
show_border = true
tabs = [
{ name = "Loot", stream = "loot" },
{ name = "Boxes", stream = "boxes" },
{ name = "Gems", stream = "gems" }
]
Keybinds for Looting
# keybinds.toml
# Quick loot
[[keybinds]]
key = "L"
action = "send"
command = "loot"
# Look for loot
[[keybinds]]
key = "Ctrl+L"
action = "send"
command = "look"
# Get all
[[keybinds]]
key = "G"
action = "send"
command = "get coins;get box"
# Appraise
[[keybinds]]
key = "Ctrl+A"
action = "send"
command = "appraise"
Tips
- Keep loot window small - Just needs to show recent drops
- Use sound alerts for valuable drops so you don’t miss them
- Color-code by value - Instantly spot the good stuff
- Position near encumbrance - Watch your load while looting
See Also
Inventory Browser
Navigate and display your inventory with container support.
Goal
Display inventory contents, open containers, and track worn items.
The Data
GemStone IV sends inventory via the inv stream:
<streamWindow id='inv' title='My Inventory' target='wear' ifClosed='' resident='true'/>
<clearStream id='inv' ifClosed=''/>
<pushStream id='inv'/>Your worn items are:
a patchwork dwarf skin backpack
an enruned urglaes band
<popStream/>
Container contents use <container> and <inv> tags:
<container id="535703780">
<inv id="123">a silver wand</inv>
<inv id="124">a gold ring</inv>
</container>
Basic Inventory Window
[[widgets]]
type = "text"
name = "inventory"
stream = "inv"
x = 85
y = 0
width = 35
height = 20
buffer_size = 200
show_border = true
title = "Inventory"
clickable = true # Enable clicking items
Tabbed Inventory View
[[widgets]]
type = "tabbed_text"
name = "inventory_tabs"
x = 85
y = 0
width = 35
height = 25
show_border = true
tabs = [
{ name = "Worn", stream = "inv" },
{ name = "Pack", stream = "container_pack" },
{ name = "Disk", stream = "container_disk" },
{ name = "Locker", stream = "container_locker" }
]
Inventory Highlights
# highlights.toml
# Containers
[[highlights]]
pattern = "(backpack|satchel|cloak|pouch|sack|bag)"
fg = "cyan"
# Weapons
[[highlights]]
pattern = "(sword|dagger|bow|staff|axe|mace|falchion)"
fg = "bright_red"
# Armor
[[highlights]]
pattern = "(armor|shield|helm|gauntlet|greave|vambrace)"
fg = "bright_blue"
# Magic items
[[highlights]]
pattern = "(wand|rod|amulet|ring|crystal|orb)"
fg = "bright_magenta"
# Valuable
[[highlights]]
pattern = "(gold|silver|platinum|mithril|vultite|ora)"
fg = "bright_yellow"
Inventory Commands
# keybinds.toml
# Check inventory
[[keybinds]]
key = "I"
action = "send"
command = "inventory"
# Check specific containers
[[keybinds]]
key = "Ctrl+I"
action = "send"
command = "look in my pack"
# Quick wear/remove
[[keybinds]]
key = "Ctrl+W"
action = "send"
command = "wear my"
[[keybinds]]
key = "Ctrl+R"
action = "send"
command = "remove my"
Container Browser Widget
[[widgets]]
type = "container"
name = "container_view"
x = 85
y = 0
width = 35
height = 25
show_border = true
title = "Container"
show_count = true # Show item count
show_capacity = true # Show if nearly full
nested = true # Support nested containers
Inventory Alerts
# triggers.toml
# Container full
[[triggers]]
name = "container_full"
pattern = "(can't fit|won't fit|is full|no room)"
tts = "Container full"
sound = "warning.wav"
# Item picked up
[[triggers]]
name = "item_got"
pattern = "You (pick up|get|grab)"
sound = "pickup.wav"
cooldown = 100
Hands Display Integration
[[widgets]]
type = "hands"
name = "hands"
x = 85
y = 26
width = 35
height = 4
show_spell = true
show_border = true
title = "Hands"
Full Inventory Layout
# Complete inventory management layout
[[widgets]]
type = "hands"
name = "hands"
x = 85
y = 0
width = 35
height = 3
show_spell = true
title = "Hands"
[[widgets]]
type = "text"
name = "worn"
stream = "inv"
x = 85
y = 3
width = 35
height = 15
title = "Worn Items"
[[widgets]]
type = "text"
name = "container"
stream = "container"
x = 85
y = 18
width = 35
height = 15
title = "Container"
[[widgets]]
type = "progress"
name = "encum"
data_source = "encumbrance"
x = 85
y = 33
width = 35
height = 2
title = "Load"
Tips
- Use tabs for different containers
- Enable clicking to interact with items
- Color-code item types for quick scanning
- Track encumbrance alongside inventory
- Set alerts for full containers
See Also
Architecture Overview
Two-Face is built on a rigorous three-layer architecture designed for maintainability, testability, and future GUI support.
Design Philosophy
Two-Face separates concerns into distinct layers:
- Data Layer - Pure data structures, no rendering logic
- Core Layer - Business logic, message processing
- Frontend Layer - Rendering, user interaction
This separation enables:
- Multiple frontends - TUI now, GUI later, same logic
- Testability - Core logic without rendering dependencies
- Maintainability - Clear boundaries and responsibilities
- Performance - Efficient data flow and change detection
High-Level Architecture
┌─────────────────────────────────────────────────────────────────┐
│ FRONTEND LAYER │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ TUI Frontend │ │ GUI Frontend │ │
│ │ (ratatui) │ │ (egui) │ │
│ │ │ │ [planned] │ │
│ └────────┬────────┘ └────────┬────────┘ │
│ │ │ │
│ │ Frontend Trait Interface │ │
│ └────────────────┬───────────────┘ │
├────────────────────────────┼────────────────────────────────────┤
│ CORE LAYER │
│ ┌─────────────────────────┴─────────────────────────┐ │
│ │ AppCore │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │MessageProc. │ │ Commands │ │ Keybinds │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ └───────────────────────┬───────────────────────────┘ │
├──────────────────────────┼──────────────────────────────────────┤
│ DATA LAYER │
│ ┌───────────────────────┴───────────────────────────┐ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ GameState │ │ UiState │ │ WindowState │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ └───────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│
┌─────────┴─────────┐
│ Network Layer │
│ (Tokio Async) │
└───────────────────┘
Module Organization
src/
├── main.rs # Entry point, CLI parsing
├── lib.rs # Library exports
│
├── core/ # Business logic
│ ├── mod.rs
│ ├── state.rs # GameState
│ ├── messages.rs # MessageProcessor
│ ├── input_router.rs # Input dispatch
│ ├── menu_actions.rs # Menu action handlers
│ └── app_core/
│ ├── mod.rs
│ ├── state.rs # AppCore
│ ├── commands.rs # Built-in commands
│ ├── keybinds.rs # Keybind dispatch
│ └── layout.rs # Layout management
│
├── data/ # Data structures
│ ├── mod.rs
│ ├── ui_state.rs # UiState, InputMode
│ ├── widget.rs # TextContent, ProgressData
│ └── window.rs # WindowState, WidgetType
│
├── frontend/ # Rendering
│ ├── mod.rs # Frontend trait
│ ├── events.rs # FrontendEvent
│ ├── common/ # Shared types
│ │ ├── color.rs
│ │ ├── input.rs
│ │ ├── rect.rs
│ │ └── text_input.rs
│ ├── tui/ # Terminal UI
│ │ ├── mod.rs
│ │ ├── runtime.rs # Event loop
│ │ ├── widget_manager.rs
│ │ ├── text_window.rs
│ │ ├── progress_bar.rs
│ │ └── ... (30+ widget files)
│ └── gui/ # Native GUI (planned)
│ └── mod.rs
│
├── config.rs # Configuration
├── config/
│ ├── highlights.rs
│ └── keybinds.rs
│
├── parser.rs # XML parser
├── cmdlist.rs # Context menu data
├── network.rs # Network connections
├── theme.rs # Theme system
├── sound.rs # Audio playback
├── tts/ # Text-to-speech
├── clipboard.rs # Clipboard access
├── selection.rs # Text selection
└── performance.rs # Performance tracking
Layer Rules
Import Restrictions
| Layer | Can Import From |
|---|---|
| Data | Nothing else |
| Core | Data only |
| Frontend | Data and Core |
#![allow(unused)]
fn main() {
// ✅ ALLOWED
// data/ imports nothing from core/ or frontend/
// core/ imports from data/
// frontend/ imports from data/ and core/
// ❌ FORBIDDEN
// data/ importing from core/ or frontend/
// core/ importing from frontend/
}
Responsibilities
| Layer | Responsibilities |
|---|---|
| Data | Define state structures, no behavior |
| Core | Process messages, handle commands, manage state |
| Frontend | Render widgets, capture input, convert events |
Key Components
AppCore
The central orchestrator containing all state and logic:
#![allow(unused)]
fn main() {
pub struct AppCore {
pub config: Config,
pub layout: Layout,
pub game_state: GameState,
pub ui_state: UiState,
pub message_processor: MessageProcessor,
pub parser: XmlParser,
}
}
Frontend Trait
Interface for UI implementations:
#![allow(unused)]
fn main() {
pub trait Frontend {
fn poll_events(&mut self) -> Result<Vec<FrontendEvent>>;
fn render(&mut self, app: &mut dyn Any) -> Result<()>;
fn cleanup(&mut self) -> Result<()>;
fn size(&self) -> (u16, u16);
}
}
Event Loop
The main loop coordinates all components:
- Poll user input (non-blocking)
- Process server messages
- Update state
- Render frame
- Sleep to maintain frame rate
Architecture Sections
This section covers:
- Core-Data-Frontend - Three-layer architecture in depth
- Message Flow - Data flow through the system
- Parser Protocol - XML protocol handling
- Widget Sync - Generation-based synchronization
- Theme System - Color resolution and theming
- Browser Editors - Popup configuration system
- Performance - Optimization strategies
Design Principles
- Separation of Concerns - Each layer has clear responsibilities
- Frontend Independence - Core logic works with any UI
- Event-Driven - Asynchronous network, synchronous rendering
- State Centralization - All state in Data layer
- Extensibility - New widgets/frontends plug in cleanly
Core-Data-Frontend Architecture
Two-Face strictly separates code into three layers. This design enables frontend independence and clean testing.
Layer Overview
┌─────────────────────────────────────┐
│ FRONTEND │
│ • Renders widgets │
│ • Captures user input │
│ • Converts events │
│ • NO business logic │
├─────────────────────────────────────┤
│ CORE │
│ • Processes messages │
│ • Handles commands │
│ • Dispatches keybinds │
│ • Routes data to state │
├─────────────────────────────────────┤
│ DATA │
│ • Pure data structures │
│ • No behavior │
│ • Owned by Core │
└─────────────────────────────────────┘
Data Layer
Location: src/data/
The data layer contains pure data structures with no logic or rendering.
UiState
#![allow(unused)]
fn main() {
pub struct UiState {
pub windows: HashMap<String, WindowState>,
pub focused_window: Option<String>,
pub input_mode: InputMode,
pub popup_menu: Option<PopupMenu>,
pub selection_state: Option<SelectionState>,
pub search_input: String,
}
}
GameState
#![allow(unused)]
fn main() {
pub struct GameState {
pub connected: bool,
pub character_name: Option<String>,
pub room_id: Option<String>,
pub room_name: Option<String>,
pub exits: Vec<String>,
pub vitals: Vitals,
pub left_hand: Option<String>,
pub right_hand: Option<String>,
pub spell: Option<String>,
pub status: StatusInfo,
}
}
WindowState
#![allow(unused)]
fn main() {
pub struct WindowState {
pub name: String,
pub widget_type: WidgetType,
pub streams: Vec<String>,
pub visible: bool,
pub rect: Rect,
// Content (varies by widget type)
pub text_content: Option<TextContent>,
pub progress_data: Option<ProgressData>,
pub countdown_data: Option<CountdownData>,
pub compass_data: Option<CompassData>,
}
}
TextContent
#![allow(unused)]
fn main() {
pub struct TextContent {
pub lines: VecDeque<StyledLine>,
pub scroll_offset: usize,
pub max_lines: usize,
pub title: String,
pub generation: u64, // Change counter
}
}
Input Modes
#![allow(unused)]
fn main() {
pub enum InputMode {
// Standard modes
Normal, // Command input active
Navigation, // Vi-style navigation
History, // Scrolling command history
Search, // Text search mode
Menu, // Context menu open
// Editor modes (priority windows)
HighlightBrowser,
HighlightForm,
KeybindBrowser,
KeybindForm,
ColorPaletteBrowser,
WindowEditor,
// ...
}
}
Core Layer
Location: src/core/
The core layer contains all business logic but no rendering code.
AppCore
The central orchestrator:
#![allow(unused)]
fn main() {
pub struct AppCore {
pub config: Config,
pub layout: Layout,
pub game_state: GameState,
pub ui_state: UiState,
pub message_processor: MessageProcessor,
pub parser: XmlParser,
pub performance_stats: PerformanceStats,
}
}
AppCore Methods
#![allow(unused)]
fn main() {
impl AppCore {
// Message processing
pub fn process_server_message(&mut self, msg: &str) { ... }
// User input
pub fn handle_command(&mut self, cmd: &str) { ... }
pub fn dispatch_keybind(&mut self, key: &KeyEvent) { ... }
// State management
pub fn get_window_state(&self, name: &str) -> Option<&WindowState> { ... }
pub fn get_window_state_mut(&mut self, name: &str) -> Option<&mut WindowState> { ... }
// Layout
pub fn apply_layout(&mut self, layout: Layout) { ... }
}
}
MessageProcessor
Routes parsed XML to appropriate state:
#![allow(unused)]
fn main() {
impl MessageProcessor {
pub fn process(
&mut self,
element: ParsedElement,
game_state: &mut GameState,
ui_state: &mut UiState,
) {
match element {
ParsedElement::Text { content, stream, .. } => {
// Route to appropriate window's text_content
}
ParsedElement::ProgressBar { id, value, max, .. } => {
// Update vitals or progress widget
}
ParsedElement::Compass { directions } => {
// Update game_state.exits
}
// ... handle all element types
}
}
}
}
Frontend Layer
Location: src/frontend/
The frontend layer renders UI and captures input. It has no business logic.
Frontend Trait
#![allow(unused)]
fn main() {
pub trait Frontend {
fn poll_events(&mut self) -> Result<Vec<FrontendEvent>>;
fn render(&mut self, app: &mut dyn Any) -> Result<()>;
fn cleanup(&mut self) -> Result<()>;
fn size(&self) -> (u16, u16);
}
}
FrontendEvent
#![allow(unused)]
fn main() {
pub enum FrontendEvent {
Key(KeyEvent),
Mouse(MouseEvent),
Resize(u16, u16),
Paste(String),
FocusGained,
FocusLost,
}
}
TUI Frontend
The terminal UI implementation:
#![allow(unused)]
fn main() {
pub struct TuiFrontend {
terminal: Terminal<CrosstermBackend<Stdout>>,
widget_manager: WidgetManager,
}
impl Frontend for TuiFrontend {
fn poll_events(&mut self) -> Result<Vec<FrontendEvent>> {
// Convert crossterm events to FrontendEvent
}
fn render(&mut self, app: &mut dyn Any) -> Result<()> {
// Sync data to widgets
// Render all visible widgets
}
}
}
Widget Manager
Caches and manages frontend widgets:
#![allow(unused)]
fn main() {
pub struct WidgetManager {
pub text_windows: HashMap<String, TextWindow>,
pub tabbed_text_windows: HashMap<String, TabbedTextWindow>,
pub command_inputs: HashMap<String, CommandInput>,
pub progress_bars: HashMap<String, ProgressBar>,
pub countdowns: HashMap<String, Countdown>,
pub compass_widgets: HashMap<String, Compass>,
// ... more widget types
// Generation tracking
pub last_synced_generation: HashMap<String, u64>,
}
}
Layer Communication
Data Flow Pattern
Server Message
↓
[Network Layer] ──────→ Raw XML string
↓
[Parser] ──────→ Vec<ParsedElement>
↓
[MessageProcessor] ───→ State updates
↓
[Data Layer] ──────→ GameState, UiState, WindowState
↓
[Sync Functions] ─────→ Widget updates
↓
[Frontend] ──────→ Rendered frame
User Input Flow
User Input
↓
[Frontend] ───────────→ FrontendEvent
↓
[Input Router] ───────→ Keybind match?
↓
[AppCore] ────────────→ Execute action
↓
Either:
• UI Action (state change)
• Game Command (send to server)
Strict Separation Benefits
1. Frontend Independence
Core and Data know nothing about rendering:
#![allow(unused)]
fn main() {
// In core/
// ❌ FORBIDDEN:
use ratatui::*;
use crossterm::*;
// ✅ ALLOWED:
use crate::data::*;
}
This enables adding a GUI frontend without modifying core logic.
2. Testability
Core logic can be tested without a terminal:
#![allow(unused)]
fn main() {
#[test]
fn test_message_processing() {
let mut app = AppCore::new(test_config());
// Simulate server message
app.process_server_message("<pushBold/>A goblin<popBold/>");
// Verify state without rendering
let main_window = app.ui_state.windows.get("main").unwrap();
assert!(main_window.text_content.as_ref().unwrap()
.lines.back().unwrap().segments[0].bold);
}
}
3. Clear Boundaries
Each layer has explicit responsibilities:
| Question | Answer |
|---|---|
| Where do I add a new data field? | Data layer |
| Where do I process server XML? | Core layer (parser/message processor) |
| Where do I render a new widget? | Frontend layer |
| Where do I handle a keybind? | Core layer (commands/keybinds) |
4. Maintainability
Changes are localized:
- Change rendering style → Frontend only
- Change data format → Data + consumers
- Change business logic → Core only
- Add new widget type → All layers, but well-defined boundaries
State Hierarchy
AppCore
├── config: Config # Configuration (immutable)
│ ├── connection: ConnectionConfig
│ ├── ui: UiConfig
│ ├── highlights: HashMap<String, HighlightPattern>
│ ├── keybinds: HashMap<String, KeyBindAction>
│ └── colors: ColorConfig
│
├── layout: Layout # Current window layout
│ └── windows: Vec<WindowDef>
│
├── game_state: GameState # Game session state
│ ├── connected: bool
│ ├── character_name: Option<String>
│ ├── room_id, room_name: Option<String>
│ ├── exits: Vec<String>
│ ├── vitals: Vitals
│ └── status: StatusInfo
│
├── ui_state: UiState # UI interaction state
│ ├── windows: HashMap<String, WindowState>
│ ├── focused_window: Option<String>
│ ├── input_mode: InputMode
│ └── popup_menu: Option<PopupMenu>
│
└── message_processor: MessageProcessor
├── current_stream: String
└── squelch_matcher: Option<AhoCorasick>
Adding a New Feature
Example: Add “Stance” Display
1. Data Layer - Add data structure:
#![allow(unused)]
fn main() {
// src/data/game_state.rs
pub struct GameState {
// ... existing fields
pub stance: Option<Stance>,
}
pub struct Stance {
pub name: String,
pub offensive: u8,
pub defensive: u8,
}
}
2. Core Layer - Handle the XML:
#![allow(unused)]
fn main() {
// src/core/messages.rs
ParsedElement::Stance { name, offensive, defensive } => {
game_state.stance = Some(Stance { name, offensive, defensive });
}
}
3. Frontend Layer - Render the widget:
#![allow(unused)]
fn main() {
// src/frontend/tui/stance_widget.rs
pub struct StanceWidget {
stance: Option<Stance>,
}
impl Widget for &StanceWidget {
fn render(self, area: Rect, buf: &mut Buffer) {
// Render stance display
}
}
}
4. Sync Function - Connect data to widget:
#![allow(unused)]
fn main() {
// src/frontend/tui/sync.rs
pub fn sync_stance_widgets(
game_state: &GameState,
widget_manager: &mut WidgetManager,
) {
if let Some(stance) = &game_state.stance {
let widget = widget_manager.stance_widgets
.entry("stance".to_string())
.or_insert_with(StanceWidget::new);
widget.update(stance);
}
}
}
See Also
- Message Flow - Detailed data flow
- Widget Sync - Synchronization patterns
- Performance - Optimization strategies
Message Flow
This document traces data flow through Two-Face from network to screen and from keyboard to game server.
Server → UI Flow
Complete Flow Diagram
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Network │───▶│ Parser │───▶│ Message │
│ (TCP/TLS) │ │ (XML) │ │ Processor │
└──────────────┘ └──────────────┘ └──────────────┘
│
┌──────────────────────────┼──────────────────────────┐
│ ▼ │
┌─────┴─────┐ ┌──────────────┐ ┌──────┴─────┐
│ GameState │ │ UiState │ │ Windows │
│ (vitals, │ │ (streams, │ │ (text │
│ room) │ │ focus) │ │ content) │
└───────────┘ └──────────────┘ └────────────┘
│
▼
┌──────────────┐
│ Frontend │
│ Render │
└──────────────┘
Step-by-Step Processing
1. Network Receives Data
#![allow(unused)]
fn main() {
ServerMessage::Text(raw_xml)
// Example: "<pushBold/>A goblin<popBold/> attacks you!"
}
The network task runs asynchronously, sending messages via channel.
2. Parser Extracts Elements
#![allow(unused)]
fn main() {
let elements: Vec<ParsedElement> = parser.parse(&raw_xml);
// Result:
// [
// Text { content: "A goblin", bold: true, span_type: Monsterbold, ... },
// Text { content: " attacks you!", bold: false, span_type: Normal, ... },
// ]
}
The parser maintains state (color stacks, current stream) across calls.
3. MessageProcessor Routes to State
#![allow(unused)]
fn main() {
for element in elements {
message_processor.process(element, &mut game_state, &mut ui_state);
}
// Processing logic:
match element {
ParsedElement::Text { content, stream, fg_color, bg_color, bold, span_type, link_data } => {
// Find window for this stream
if let Some(window) = ui_state.windows.get_mut(&stream) {
if let Some(text_content) = &mut window.text_content {
// Create styled line
let line = StyledLine::new(segments);
text_content.add_line(line); // Increments generation
}
}
}
ParsedElement::ProgressBar { id, value, max, text } => {
// Update vitals
match id.as_str() {
"health" => game_state.vitals.health = (value, max),
"mana" => game_state.vitals.mana = (value, max),
// ...
}
}
ParsedElement::Compass { directions } => {
game_state.exits = directions;
}
// ... handle 30+ element types
}
}
4. State Changes Detected
The sync system detects changes via generation counters:
#![allow(unused)]
fn main() {
// Each text_content tracks changes
pub struct TextContent {
pub lines: VecDeque<StyledLine>,
pub generation: u64, // Increments on every add_line()
}
}
5. Frontend Syncs and Renders
#![allow(unused)]
fn main() {
// In render loop:
sync_text_windows(&app.ui_state, &mut widget_manager);
sync_progress_bars(&app.game_state, &mut widget_manager);
sync_compass_widgets(&app.game_state, &mut widget_manager);
// ... more sync functions
// Render all widgets
terminal.draw(|frame| {
for (name, widget) in &widget_manager.text_windows {
frame.render_widget(&*widget, widget.rect());
}
// ... render other widget types
})?;
}
User Input → Game Flow
Complete Flow Diagram
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Frontend │───▶│ Keybind │───▶│ Core │
│ (events) │ │ Dispatch │ │ Commands │
└──────────────┘ └──────────────┘ └──────────────┘
│
┌────────────────────┴────────────────────┐
▼ ▼
┌──────────────┐ ┌──────────────┐
│ UI Action │ │ Game Command │
│ (open menu, │ │ (send to │
│ scroll) │ │ server) │
└──────────────┘ └──────────────┘
Keybind Processing Layers
Keybinds are processed in priority order:
#![allow(unused)]
fn main() {
// Layer 1: Global keybinds (always active)
if let Some(action) = config.global_keybinds.match_key(&key) {
return handle_global_action(action);
}
// Layer 2: Menu keybinds (in priority windows)
if has_priority_window(&ui_state.input_mode) {
if let Some(action) = config.menu_keybinds.match_key(&key, context) {
return handle_menu_action(action);
}
}
// Layer 3: User keybinds (game mode)
if let Some(action) = config.keybinds.get(&key_string) {
return handle_user_action(action);
}
// Layer 4: Default character input
command_input.insert_char(key.char);
}
Action Types
| Action Type | Example | Effect |
|---|---|---|
| UI Action | Open menu, scroll | Modifies UiState |
| Game Command | Send “look” | Sent to server |
| Macro | Multi-command sequence | Multiple server sends |
| Internal | Change layout | Config/layout change |
Event Loop
Main Loop Structure
#![allow(unused)]
fn main() {
pub fn run(config: Config) -> Result<()> {
// 1. Initialize
let runtime = tokio::runtime::Builder::new_multi_thread().build()?;
let mut app = AppCore::new(config)?;
let mut frontend = TuiFrontend::new()?;
// 2. Start network tasks
let (server_tx, server_rx) = mpsc::unbounded_channel();
let (command_tx, command_rx) = mpsc::unbounded_channel();
runtime.spawn(network_task(server_tx, command_rx));
// 3. Main event loop
loop {
// Poll for user input (non-blocking)
let events = frontend.poll_events()?;
for event in events {
match event {
FrontendEvent::Key(key) => {
if app.handle_key_event(key, &command_tx)? == ControlFlow::Break {
return Ok(());
}
}
FrontendEvent::Mouse(mouse) => {
app.handle_mouse_event(mouse)?;
}
FrontendEvent::Resize(w, h) => {
app.handle_resize(w, h)?;
}
}
}
// Process server messages (non-blocking drain)
while let Ok(msg) = server_rx.try_recv() {
match msg {
ServerMessage::Text(text) => {
app.process_server_message(&text)?;
}
ServerMessage::Connected => { ... }
ServerMessage::Disconnected => { ... }
}
}
// Render frame
frontend.render(&mut app)?;
// Sleep to maintain frame rate (~60 FPS)
std::thread::sleep(Duration::from_millis(16));
}
}
}
Async Network Integration
Network I/O runs in separate Tokio tasks:
#![allow(unused)]
fn main() {
async fn reader_task(
stream: TcpStream,
server_tx: UnboundedSender<ServerMessage>,
) {
let mut reader = BufReader::new(stream);
let mut line = String::new();
loop {
line.clear();
match reader.read_line(&mut line).await {
Ok(0) => {
server_tx.send(ServerMessage::Disconnected).ok();
break;
}
Ok(_) => {
server_tx.send(ServerMessage::Text(line.clone())).ok();
}
Err(_) => break,
}
}
}
async fn writer_task(
mut stream: TcpStream,
mut command_rx: UnboundedReceiver<String>,
) {
while let Some(cmd) = command_rx.recv().await {
if stream.write_all(cmd.as_bytes()).await.is_err() {
break;
}
}
}
}
Detailed Scenarios
Scenario: Monster Attack Message
Server sends: "<pushBold/>A massive troll<popBold/> swings at you!"
1. Network Task
└─▶ Receives bytes, sends ServerMessage::Text(...)
2. Main Loop
└─▶ Receives from channel, calls process_server_message()
3. Parser
└─▶ Parses XML tags:
• <pushBold/> → Push bold state
• "A massive troll" → Text with bold=true, span_type=Monsterbold
• <popBold/> → Pop bold state
• " swings at you!" → Text with bold=false
└─▶ Returns Vec<ParsedElement>
4. MessageProcessor
└─▶ For each Text element:
• Applies highlight patterns
• Routes to "main" stream
• Adds to main window's TextContent
• Increments generation counter
5. Sync Functions
└─▶ Detects generation change
└─▶ Copies new line to TextWindow widget
6. Frontend Render
└─▶ Renders TextWindow with new content
└─▶ Bold "A massive troll" in monsterbold color
Scenario: User Types Command
User types: "attack troll" and presses Enter
1. Frontend
└─▶ Captures key events: 'a', 't', 't', ..., Enter
└─▶ Sends FrontendEvent::Key for each
2. Input Router (for each key)
└─▶ Not a keybind → Insert into command input
3. On Enter
└─▶ Get command text: "attack troll"
└─▶ Check for client command (starts with '.')
└─▶ Not client command → Send to server
└─▶ command_tx.send("attack troll\n")
4. Writer Task
└─▶ Receives from channel
└─▶ Writes to TCP stream
5. Meanwhile
└─▶ Command echoed to main window
└─▶ Server response arrives via reader task
Scenario: Progress Bar Update
Server sends: <progressBar id='health' value='75' text='health 375/500'/>
1. Parser
└─▶ Creates ParsedElement::ProgressBar {
id: "health",
value: 375,
max: 500,
text: "health 375/500"
}
2. MessageProcessor
└─▶ Updates game_state.vitals.health = (375, 500)
3. Sync Functions
└─▶ sync_progress_bars() reads vitals
└─▶ Updates ProgressBar widget
4. Frontend
└─▶ Renders bar at 75% filled
Scenario: Room Change
Server sends multiple elements for room transition:
1. <clearStream id='room'/>
2. <nav rm='12345'/>
3. <streamWindow id='room' subtitle='Town Square'/>
4. <component id='room desc'>A bustling town square...</component>
5. <compass><dir value='n'/><dir value='e'/><dir value='out'/></compass>
Processing:
1. ClearStream
└─▶ Clears room window content
2. RoomId
└─▶ game_state.room_id = Some("12345")
3. StreamWindow
└─▶ game_state.room_name = Some("Town Square")
└─▶ Updates window title
4. Component
└─▶ Adds description to room window
5. Compass
└─▶ game_state.exits = ["n", "e", "out"]
└─▶ Compass widget updates
All synced and rendered in next frame.
Channel Communication
Channel Types
| Channel | Direction | Purpose |
|---|---|---|
server_tx | Network → Main | Server messages |
command_tx | Main → Network | Game commands |
Message Types
#![allow(unused)]
fn main() {
pub enum ServerMessage {
Text(String), // Server XML data
Connected, // Connection established
Disconnected, // Connection lost
Error(String), // Network error
}
}
Error Handling
Network Errors
#![allow(unused)]
fn main() {
// In reader task
match reader.read_line(&mut line).await {
Ok(0) => {
// EOF - server closed connection
server_tx.send(ServerMessage::Disconnected).ok();
}
Err(e) => {
// Read error
server_tx.send(ServerMessage::Error(e.to_string())).ok();
}
}
}
Parse Errors
Parse errors are logged but don’t crash:
#![allow(unused)]
fn main() {
// Invalid XML is skipped
if let Err(e) = parser.parse_chunk(&data) {
tracing::warn!("Parse error: {}", e);
// Continue processing
}
}
State Errors
Missing windows are handled gracefully:
#![allow(unused)]
fn main() {
// If window doesn't exist, message is silently dropped
if let Some(window) = ui_state.windows.get_mut(&stream) {
// Process
}
}
See Also
- Parser Protocol - XML parsing details
- Widget Sync - Generation-based sync
- Performance - Optimization strategies
Parser Protocol
Two-Face’s XML streaming parser converts GemStone IV/DragonRealms server data into strongly-typed Rust events.
Overview
The game server sends an XML-based protocol called “Wizard Front End” (WFE) or “Stormfront” protocol. The XmlParser maintains state across chunks and emits high-level ParsedElement values.
Parser Architecture
Server XML Stream
↓
┌──────────────────────┐
│ XmlParser │
│ ────────────────── │
│ • Color stacks │
│ • Stream tracking │
│ • Link metadata │
│ • Event matchers │
└──────────────────────┘
↓
Vec<ParsedElement>
↓
AppCore (routes to widgets)
ParsedElement Variants
The parser emits these typed events:
Text Content
#![allow(unused)]
fn main() {
ParsedElement::Text {
content: String, // Decoded text content
stream: String, // Target stream: "main", "speech", etc.
fg_color: Option<String>, // Foreground color "#RRGGBB"
bg_color: Option<String>, // Background color
bold: bool, // Bold styling
span_type: SpanType, // Semantic type
link_data: Option<LinkData>, // Clickable link metadata
}
}
SpanType Enum
| SpanType | Source | Purpose |
|---|---|---|
Normal | Plain text | Default text |
Link | <a> or <d> tags | Clickable game objects |
Monsterbold | <pushBold/> | Monster/creature names |
Spell | <spell> tags | Spell names |
Speech | <preset id="speech"> | Player dialogue |
Game State Updates
#![allow(unused)]
fn main() {
// Prompt (command input ready)
ParsedElement::Prompt {
time: String, // Unix timestamp
text: String, // Prompt character (e.g., ">")
}
// Roundtime countdown
ParsedElement::RoundTime { value: u32 } // Seconds
// Cast time countdown
ParsedElement::CastTime { value: u32 } // Seconds
// Hand contents
ParsedElement::LeftHand { item: String, link: Option<LinkData> }
ParsedElement::RightHand { item: String, link: Option<LinkData> }
ParsedElement::SpellHand { spell: String }
// Progress bars (health, mana, etc.)
ParsedElement::ProgressBar {
id: String, // "health", "mana", "spirit", "stamina"
value: u32, // Current value
max: u32, // Maximum value
text: String, // Display text like "mana 407/407"
}
// Compass directions
ParsedElement::Compass {
directions: Vec<String>, // ["n", "e", "out", "up"]
}
}
Stream Control
#![allow(unused)]
fn main() {
// Push new stream context
ParsedElement::StreamPush { id: String }
// Pop stream (return to main)
ParsedElement::StreamPop
// Clear stream contents
ParsedElement::ClearStream { id: String }
// Stream window metadata
ParsedElement::StreamWindow {
id: String,
subtitle: Option<String>, // Room name for "room" stream
}
}
Room Information
#![allow(unused)]
fn main() {
// Room ID for mapping
ParsedElement::RoomId { id: String }
// Room component (name, description, objects, players)
ParsedElement::Component {
id: String, // "room name", "room desc", "room objs", "room players"
value: String, // Component content
}
}
Combat/Status
#![allow(unused)]
fn main() {
// Injury display
ParsedElement::InjuryImage {
id: String, // Body part: "head", "leftArm", "chest"
name: String, // Level: "Injury1"-"Injury3", "Scar1"-"Scar3"
}
// Status indicators
ParsedElement::StatusIndicator {
id: String, // "poisoned", "diseased", "bleeding", "stunned", "hidden"
active: bool, // true = active, false = cleared
}
// Active effects (spells, buffs, debuffs)
ParsedElement::ActiveEffect {
category: String, // "ActiveSpells", "Buffs", "Debuffs", "Cooldowns"
id: String,
value: u32, // Progress percentage
text: String, // Effect name
time: String, // "HH:MM:SS"
}
ParsedElement::ClearActiveEffects { category: String }
}
Interactive Elements
#![allow(unused)]
fn main() {
// Menu response (from INV SEARCH, etc.)
ParsedElement::MenuResponse {
id: String, // Correlation ID
coords: Vec<(String, Option<String>)>, // (coord, optional noun) pairs
}
// Pattern-matched events (stun, webbed, etc.)
ParsedElement::Event {
event_type: String, // "stun", "webbed", "prone"
action: EventAction, // Set, Clear, or Increment
duration: u32, // Seconds
}
// External URL launch
ParsedElement::LaunchURL { url: String }
}
Link Data Structure
Clickable text carries metadata for game interaction:
#![allow(unused)]
fn main() {
pub struct LinkData {
pub exist_id: String, // Object ID or "_direct_"
pub noun: String, // Object noun or command
pub text: String, // Display text
pub coord: Option<String>, // Optional coord for direct commands
}
}
GemStone IV Links (<a> tags)
<a exist="12345" noun="sword">a rusty sword</a>
exist_id: “12345” (server object ID)noun: “sword” (for commands like “get sword”)- Clicking sends:
_INSPECT 12345or contextual command
DragonRealms Direct Commands (<d> tags)
<d cmd='get #8735861 in #8735860'>Some item</d>
exist_id: “direct” (marker for direct command)noun: “get #8735861 in #8735860” (full command)- Clicking sends the command directly
Stream System
The server uses streams to route content to different windows:
| Stream | Purpose | Widget Type |
|---|---|---|
main | Primary game output | Main text window |
speech | Player dialogue | Speech tab/window |
thoughts | ESP/telepathy | Thoughts tab/window |
inv | Inventory listings | Inventory window |
room | Room descriptions | Room info widget |
assess | Combat assessment | Assessment window |
experience | Skill/XP info | Experience window |
percWindow | Perception checks | Perception window |
death | Death messaging | Death/recovery window |
logons | Login/logout notices | Arrivals window |
familiar | Familiar messages | Familiar window |
group | Group information | Group window |
Stream Lifecycle
<!-- Server pushes to speech stream -->
<pushStream id='speech'/>
Soandso says, "Hello there!"
<popStream/>
<!-- Text returns to main stream -->
You nod to Soandso.
The parser tracks current_stream and emits StreamPush/StreamPop events.
Color Stack System
The parser maintains multiple stacks for nested styling:
#![allow(unused)]
fn main() {
pub struct XmlParser {
color_stack: Vec<ColorStyle>, // <color> tags
preset_stack: Vec<ColorStyle>, // <preset> tags
style_stack: Vec<ColorStyle>, // <style> tags
bold_stack: Vec<bool>, // <pushBold>/<popBold>
}
}
Priority (highest to lowest)
color_stack- Explicit<color fg="..." bg="...">tagspreset_stack- Named presets like<preset id="speech">style_stack- Style IDs like<style id="roomName">
XML Color Tags
<!-- Explicit color -->
<color fg='#FF0000' bg='#000000'>Red on black</color>
<!-- Named preset (from colors.toml) -->
<preset id='speech'>Someone says, "Hello"</preset>
<!-- Bold/monsterbold -->
<pushBold/>A goblin<popBold/> attacks you!
<!-- Style (from colors.toml) -->
<style id='roomName'>Town Square</style>
Event Pattern Matching
The parser checks text against configurable patterns for game events:
#![allow(unused)]
fn main() {
pub struct EventPattern {
pub pattern: String, // Regex pattern
pub event_type: String, // "stun", "webbed", "prone"
pub action: EventAction, // Set, Clear, Increment
pub duration: u32, // Fixed duration (seconds)
pub duration_capture: Option<usize>, // Capture group for duration
pub duration_multiplier: f32, // Convert rounds to seconds
pub enabled: bool,
}
}
Example: Stun Detection
[event_patterns.stun_start]
pattern = 'You are stunned!'
event_type = "stun"
action = "set"
duration = 5
[event_patterns.stun_with_rounds]
pattern = 'stunned for (\d+) rounds'
event_type = "stun"
action = "set"
duration_capture = 1
duration_multiplier = 5.0 # Rounds to seconds
[event_patterns.stun_end]
pattern = 'You are no longer stunned'
event_type = "stun"
action = "clear"
XML Entity Decoding
The parser decodes standard XML entities:
| Entity | Character |
|---|---|
< | < |
> | > |
& | & |
" | " |
' | ' |
Progress Bar Parsing
Progress bars contain both percentage and actual values:
<progressBar id='health' value='85' text='health 425/500' />
The parser extracts:
id: “health”value: 425 (parsed from text, not the percentage)max: 500 (parsed from text)text: “health 425/500”
Special Cases
mindState: Text like “clear as a bell” (no numeric extraction)encumlevel: Encumbrance percentagelblBPs: Blood Points (Betrayer profession)
Complete XML Tag Reference
Handled Tags
| Tag | Purpose | ParsedElement |
|---|---|---|
<prompt> | Command ready | Prompt |
<roundTime> | RT countdown | RoundTime |
<castTime> | Cast countdown | CastTime |
<left> | Left hand item | LeftHand |
<right> | Right hand item | RightHand |
<spell> | Prepared spell | SpellHand |
<compass> | Available exits | Compass |
<progressBar> | Stat bars | ProgressBar |
<indicator> | Status icons | StatusIndicator |
<dialogData> | Dialog updates | Various |
<component> | Room components | Component |
<pushStream> | Stream switch | StreamPush |
<popStream> | Stream return | StreamPop |
<clearStream> | Clear window | ClearStream |
<streamWindow> | Window metadata | StreamWindow |
<nav> | Room ID | RoomId |
<a> | Object link | Text with LinkData |
<d> | Direct command | Text with LinkData |
<menu> | Menu container | MenuResponse |
<mi> | Menu item | Part of MenuResponse |
<preset> | Named color | Color applied to Text |
<color> | Explicit color | Color applied to Text |
<style> | Style ID | Color applied to Text |
<pushBold> | Start bold | Bold applied to Text |
<popBold> | End bold | Bold removed |
<LaunchURL> | External URL | LaunchURL |
Ignored Tags
These tags are silently ignored:
<dropDownBox><skin><clearContainer><container><exposeContainer><inv>content (between open/close)
Usage Example
#![allow(unused)]
fn main() {
use two_face::parser::{XmlParser, ParsedElement};
// Create parser with presets and event patterns
let parser = XmlParser::with_presets(preset_list, event_patterns);
// Parse a line from the server
let elements = parser.parse_line("<pushBold/>A goblin<popBold/> attacks!");
// Route elements to appropriate widgets
for element in elements {
match element {
ParsedElement::Text { content, stream, bold, .. } => {
// Route to text widget for `stream`
}
ParsedElement::RoundTime { value } => {
// Update RT countdown widget
}
ParsedElement::Compass { directions } => {
// Update compass widget
}
// ... handle other variants
}
}
}
See Also
- Message Flow - How parsed elements flow through the system
- Theme System - How presets are resolved to colors
- Widget Sync - How parsed data reaches widgets
Widget Synchronization
Two-Face uses generation-based change detection to efficiently sync data to frontend widgets without full comparisons.
Overview
The sync system:
- Detects changes via generation counters (not content comparison)
- Syncs only changed data
- Handles incremental updates
- Supports full resyncs when needed
Generation-Based Change Detection
TextContent Generation
Every TextContent has a generation counter:
#![allow(unused)]
fn main() {
pub struct TextContent {
pub lines: VecDeque<StyledLine>,
pub scroll_offset: usize,
pub max_lines: usize,
pub title: String,
pub generation: u64, // Increments on every add_line()
}
impl TextContent {
pub fn add_line(&mut self, line: StyledLine) {
self.lines.push_back(line);
// Trim oldest lines when over limit
while self.lines.len() > self.max_lines {
self.lines.pop_front();
}
self.generation += 1; // Always increment
}
}
}
Sync Logic
#![allow(unused)]
fn main() {
// Get last synced generation for this window
let last_synced_gen = last_synced_generation.get(name).copied().unwrap_or(0);
let current_gen = text_content.generation;
// Only sync if generation changed
if current_gen > last_synced_gen {
let gen_delta = (current_gen - last_synced_gen) as usize;
// If delta > line count, need full resync (wrapped around or cleared)
let needs_full_resync = gen_delta > text_content.lines.len();
if needs_full_resync {
text_window.clear();
}
// Add only new lines
let lines_to_add = if needs_full_resync {
text_content.lines.len()
} else {
gen_delta.min(text_content.lines.len())
};
let skip_count = text_content.lines.len().saturating_sub(lines_to_add);
for line in text_content.lines.iter().skip(skip_count) {
text_window.add_line(line.clone());
}
// Update synced generation
last_synced_generation.insert(name.clone(), current_gen);
}
}
Benefits
- O(1) change detection - Compare numbers, not content
- Incremental updates - Only new lines synced
- Automatic full resync - Detects when buffer cleared
- Multi-window support - Each window tracked independently
Sync Architecture
WidgetManager
The WidgetManager caches all frontend widgets and tracks sync state:
#![allow(unused)]
fn main() {
pub struct WidgetManager {
// Widget caches
pub text_windows: HashMap<String, TextWindow>,
pub tabbed_text_windows: HashMap<String, TabbedTextWindow>,
pub command_inputs: HashMap<String, CommandInput>,
pub progress_bars: HashMap<String, ProgressBar>,
pub countdowns: HashMap<String, Countdown>,
pub compass_widgets: HashMap<String, Compass>,
pub hand_widgets: HashMap<String, Hand>,
pub indicator_widgets: HashMap<String, StatusIndicator>,
pub injury_doll_widgets: HashMap<String, InjuryDoll>,
pub active_effects_widgets: HashMap<String, ActiveEffects>,
pub room_windows: HashMap<String, RoomWindow>,
pub dashboard_widgets: HashMap<String, Dashboard>,
pub inventory_windows: HashMap<String, Inventory>,
pub spell_windows: HashMap<String, SpellList>,
pub performance_widgets: HashMap<String, PerformanceWidget>,
// Generation tracking for incremental sync
pub last_synced_generation: HashMap<String, u64>,
}
}
Sync Functions
Each widget type has a dedicated sync function:
| Function | Widget Type | Data Source |
|---|---|---|
sync_text_windows() | Text windows | WindowState.text_content |
sync_tabbed_text_windows() | Tabbed text | WindowState.text_content (per tab) |
sync_command_inputs() | Command input | UiState.command_input |
sync_progress_bars() | Progress bars | GameState.vitals |
sync_countdowns() | Countdown timers | GameState.roundtime |
sync_compass_widgets() | Compass | GameState.exits |
sync_hand_widgets() | Hand display | GameState.left_hand/right_hand |
sync_indicator_widgets() | Status indicators | GameState.status |
sync_injury_doll_widgets() | Injury display | GameState.injuries |
sync_active_effects() | Buffs/debuffs | GameState.active_effects |
sync_room_windows() | Room description | GameState.room_* |
sync_dashboard_widgets() | Dashboard | Multiple sources |
sync_inventory_windows() | Inventory | GameState.inventory |
sync_spells_windows() | Spells | GameState.spells |
sync_performance_widgets() | Performance | PerformanceStats |
Sync Pattern
Each sync function follows this pattern:
#![allow(unused)]
fn main() {
pub fn sync_text_windows(
ui_state: &UiState,
layout: &Layout,
widget_manager: &mut WidgetManager,
theme: &Theme,
) {
// 1. Find windows of this type in layout
for window_def in layout.windows.iter().filter(|w| w.widget_type == WidgetType::Text) {
let name = &window_def.name;
// 2. Get data from state
let window_state = match ui_state.windows.get(name) {
Some(ws) => ws,
None => continue,
};
let text_content = match &window_state.text_content {
Some(tc) => tc,
None => continue,
};
// 3. Ensure widget exists in cache
let text_window = widget_manager.text_windows
.entry(name.clone())
.or_insert_with(|| TextWindow::new(name.clone()));
// 4. Apply configuration
text_window.set_width(window_def.width);
text_window.set_colors(theme.resolve_window_colors(window_def));
// 5. Check generation for changes
let last_gen = widget_manager.last_synced_generation
.get(name).copied().unwrap_or(0);
let current_gen = text_content.generation;
if current_gen <= last_gen {
continue; // No changes
}
// 6. Sync content
let delta = (current_gen - last_gen) as usize;
let needs_full_resync = delta > text_content.lines.len();
if needs_full_resync {
text_window.clear();
}
let lines_to_add = if needs_full_resync {
text_content.lines.len()
} else {
delta.min(text_content.lines.len())
};
let skip = text_content.lines.len().saturating_sub(lines_to_add);
for line in text_content.lines.iter().skip(skip) {
text_window.add_line(line.clone());
}
// 7. Update generation
widget_manager.last_synced_generation.insert(name.clone(), current_gen);
}
}
}
Special Cases
Tabbed Text Windows
Tabbed windows track generation per tab:
#![allow(unused)]
fn main() {
pub struct TabbedTextSyncState {
pub tab_generations: HashMap<String, u64>, // Per-tab tracking
}
// In sync:
for tab in &tabbed_window.tabs {
let tab_gen = sync_state.tab_generations.get(&tab.name).copied().unwrap_or(0);
let current_gen = tab.text_content.generation;
if current_gen > tab_gen {
// Sync this tab
}
}
}
Progress Bars
Progress bars don’t use generations - they directly copy values:
#![allow(unused)]
fn main() {
pub fn sync_progress_bars(
game_state: &GameState,
widget_manager: &mut WidgetManager,
) {
// Direct sync - values are small, comparison is cheap
if let Some(widget) = widget_manager.progress_bars.get_mut("health") {
widget.set_value(game_state.vitals.health.0);
widget.set_max(game_state.vitals.health.1);
}
}
}
Countdowns
Countdowns track their own state and update per-frame:
#![allow(unused)]
fn main() {
pub fn sync_countdowns(
game_state: &GameState,
widget_manager: &mut WidgetManager,
) {
if let Some(widget) = widget_manager.countdowns.get_mut("roundtime") {
let remaining = game_state.roundtime_end
.map(|end| end.saturating_duration_since(Instant::now()))
.unwrap_or(Duration::ZERO);
widget.set_remaining(remaining);
}
}
}
Performance Optimizations
Width-Based Invalidation
Text windows invalidate wrap cache only when width changes:
#![allow(unused)]
fn main() {
impl TextWindow {
pub fn set_width(&mut self, width: u16) {
if self.current_width != width {
self.current_width = width;
self.invalidate_wrap_cache();
}
}
}
}
Lazy Line Wrapping
Lines are wrapped on-demand during render, not during sync:
#![allow(unused)]
fn main() {
fn render_visible_lines(&self, visible_height: usize) -> Vec<WrappedLine> {
// Only wrap lines that will actually be displayed
let visible_range = self.calculate_visible_range(visible_height);
self.lines
.iter()
.skip(visible_range.start)
.take(visible_range.len())
.flat_map(|line| self.wrap_line(line))
.collect()
}
}
Dirty Tracking
Widgets track whether they need re-rendering:
#![allow(unused)]
fn main() {
impl TextWindow {
pub fn is_dirty(&self) -> bool {
self.dirty
}
pub fn mark_clean(&mut self) {
self.dirty = false;
}
fn add_line(&mut self, line: StyledLine) {
// ...
self.dirty = true;
}
}
}
Sync Timing
When Sync Happens
Sync occurs every frame in the render loop:
#![allow(unused)]
fn main() {
// In TuiFrontend::render()
fn render(&mut self, app: &mut AppCore) -> Result<()> {
// Sync all widget types
sync_text_windows(&app.ui_state, &app.layout, &mut self.widget_manager, &app.theme);
sync_tabbed_text_windows(&app.ui_state, &app.layout, &mut self.widget_manager, &app.theme);
sync_progress_bars(&app.game_state, &mut self.widget_manager);
sync_countdowns(&app.game_state, &mut self.widget_manager);
sync_compass_widgets(&app.game_state, &mut self.widget_manager);
// ... more sync functions
// Then render
self.terminal.draw(|frame| {
// Render all widgets
})?;
Ok(())
}
}
Sync Frequency
- ~60 times per second - Once per frame
- Generation checks are O(1) - Very fast
- Only changed widgets sync content - Efficient
Debugging Sync Issues
Check Generation
#![allow(unused)]
fn main() {
// Debug: Print generation info
tracing::debug!(
"Window '{}': gen={}, last_synced={}, lines={}",
name,
text_content.generation,
last_synced_gen,
text_content.lines.len()
);
}
Check Widget Cache
#![allow(unused)]
fn main() {
// Debug: Check if widget exists
if widget_manager.text_windows.contains_key(name) {
tracing::debug!("Widget '{}' exists in cache", name);
} else {
tracing::warn!("Widget '{}' NOT in cache", name);
}
}
Common Issues
| Symptom | Possible Cause | Solution |
|---|---|---|
| Content not appearing | Widget not in layout | Add to layout.toml |
| Content appears late | High generation delta | Check buffer_size |
| Content duplicated | Generation not tracking | Check add_line increments |
| Old content showing | Full resync needed | Clear and resync |
See Also
- Message Flow - How data reaches sync
- Performance - Optimization details
- Core-Data-Frontend - Architecture overview
Theme System
Two-Face uses a layered color system for flexible theming and per-character customization.
Overview
The theme system provides:
- Presets - Named color schemes for game text (speech, monsterbold, etc.)
- Prompt Colors - Character-based prompt coloring (R, S, H, >)
- Spell Colors - Spell-specific bar colors in active effects
- UI Colors - Border, text, background colors for the interface
- Color Palette - Named colors for highlights and quick access
Configuration Files
~/.two-face/
├── colors.toml # Global colors
└── characters/
└── CharName/
└── colors.toml # Per-character overrides
Loading Order
- Load character-specific
colors.tomlif it exists - Fall back to global
~/.two-face/colors.toml - Fall back to embedded defaults
Presets
Presets define colors for semantic text types from the game server.
Default Presets
[presets.links]
fg = "#477ab3"
[presets.commands]
fg = "#477ab3"
[presets.speech]
fg = "#53a684"
[presets.roomName]
fg = "#9BA2B2"
bg = "#395573"
[presets.monsterbold]
fg = "#a29900"
[presets.familiar]
fg = "#767339"
[presets.thought]
fg = "#FF8080"
[presets.whisper]
fg = "#60b4bf"
Preset Structure
[presets.my_preset]
fg = "#RRGGBB" # Foreground color (hex)
bg = "#RRGGBB" # Background color (optional)
How Presets Are Applied
The parser applies presets when encountering specific XML tags:
| Preset | Triggered By | Example |
|---|---|---|
speech | <preset id="speech"> | Player dialogue |
monsterbold | <pushBold/> | Monster names |
roomName | <style id="roomName"> | Room titles |
links | <a> tags | Clickable objects |
commands | <d> tags | Direct commands |
whisper | <preset id="whisper"> | Whispers |
thought | <preset id="thought"> | ESP/thoughts |
familiar | <preset id="familiar"> | Familiar messages |
Prompt Colors
Prompt colors apply to individual characters in the game prompt.
Configuration
[[prompt_colors]]
character = "R"
color = "#ff0000"
[[prompt_colors]]
character = "S"
color = "#ffff00"
[[prompt_colors]]
character = "H"
color = "#9370db"
[[prompt_colors]]
character = ">"
color = "#a9a9a9"
Common Prompt Characters
| Character | Meaning | Suggested Color |
|---|---|---|
R | Roundtime | Red |
S | Spell roundtime | Yellow |
H | Hidden | Purple |
> | Ready | Gray |
K | Kneeling | Custom |
P | Prone | Custom |
UI Colors
UI colors control the application interface appearance.
Configuration
[ui]
command_echo_color = "#ffffff" # Echo of typed commands
border_color = "#00ffff" # Unfocused widget borders
focused_border_color = "#ffff00" # Focused widget borders
text_color = "#ffffff" # Default text color
background_color = "#000000" # Widget background
selection_bg_color = "#4a4a4a" # Text selection background
textarea_background = "-" # Input field background ("-" = transparent)
Transparent Background
Set textarea_background = "-" to use a transparent background for the command input area.
Spell Colors
Spell colors assign colors to specific spell numbers in the Active Spells widget.
Configuration
[[spell_colors]]
spells = [503, 506, 507, 508, 509] # Spell numbers
color = "#5c0000" # Bar color
bar_color = "#5c0000" # Same as color
text_color = "#909090" # Text on the bar
bg_color = "#000000" # Background behind bar
Spell Circle Examples
# Minor Spirit (500s) - Dark Red
[[spell_colors]]
spells = [503, 506, 507, 508, 509, 513, 515, 520, 525, 535, 540]
color = "#5c0000"
# Major Spirit (900s) - Purple
[[spell_colors]]
spells = [905, 911, 913, 918, 919, 920, 925, 930, 940]
color = "#9370db"
# Ranger (600s) - Green
[[spell_colors]]
spells = [601, 602, 604, 605, 606, 608, 612, 613, 617, 618, 620, 625, 640, 650]
color = "#1c731c"
# Sorcerer (700s) - Indigo
[[spell_colors]]
spells = [701, 703, 705, 708, 712, 713, 715, 720, 725, 730, 735, 740]
color = "#4b0082"
Color Palette
The color palette provides named colors for highlights and quick access.
Structure
[[color_palette]]
name = "red"
color = "#FF0000"
category = "red"
[[color_palette]]
name = "crimson"
color = "#DC143C"
category = "red"
[[color_palette]]
name = "forestgreen"
color = "#228B22"
category = "green"
Default Categories
- red - Red, crimson, darkred, firebrick, indianred
- orange - Orange, darkorange, coral, tomato
- yellow - Yellow, gold, khaki, lightyellow
- green - Green, lime, darkgreen, forestgreen, seagreen
- cyan - Cyan, aqua, darkcyan, teal, turquoise
- blue - Blue, darkblue, navy, royalblue, steelblue
- purple - Purple, darkviolet, darkorchid, indigo, violet
- magenta - Magenta, fuchsia, deeppink, hotpink, pink
- brown - Brown, saddlebrown, sienna, chocolate, tan
- gray - White, black, gray, darkgray, lightgray, silver
Using Palette Colors
In highlights, reference palette colors by name:
[death_blow]
pattern = "death blow"
fg = "crimson" # Resolved to #DC143C
bg = "darkred" # Resolved to #8B0000
Color Abstraction Layer
Two-Face provides a frontend-agnostic color system.
Color Struct
#![allow(unused)]
fn main() {
pub struct Color {
pub r: u8,
pub g: u8,
pub b: u8,
}
impl Color {
pub const fn rgb(r: u8, g: u8, b: u8) -> Self;
pub fn from_hex(hex: &str) -> Option<Self>;
pub fn to_hex(&self) -> String;
// Constants
pub const BLACK: Self = Self::rgb(0, 0, 0);
pub const WHITE: Self = Self::rgb(255, 255, 255);
pub const RED: Self = Self::rgb(255, 0, 0);
// ... more constants
}
}
NamedColor Enum
#![allow(unused)]
fn main() {
pub enum NamedColor {
// Standard ANSI colors
Black, Red, Green, Yellow, Blue, Magenta, Cyan,
Gray, DarkGray, LightRed, LightGreen, LightYellow,
LightBlue, LightMagenta, LightCyan, White,
// RGB color
Rgb(u8, u8, u8),
// ANSI 256-color palette (0-255)
Indexed(u8),
// Reset to default
Reset,
}
}
ANSI 256-Color Support
| Range | Description |
|---|---|
| 0-15 | Standard 16 ANSI colors |
| 16-231 | 6×6×6 color cube (216 colors) |
| 232-255 | Grayscale ramp (24 grays) |
Color Resolution
When resolving a color reference:
#![allow(unused)]
fn main() {
pub fn resolve_palette_color(&self, input: &str) -> String {
// If starts with #, use as-is
if input.starts_with('#') {
return input.to_string();
}
// Look up in palette (case-insensitive)
for entry in &self.color_palette {
if entry.name.eq_ignore_ascii_case(input) {
return entry.color.clone();
}
}
// Return original if not found
input.to_string()
}
}
Hot-Switching
Reload colors without restarting:
.reload colors
This:
- Reloads
colors.tomlfrom disk - Updates parser presets
- Refreshes all widgets
- Applies immediately to new text
Creating Custom Themes
Step 1: Copy Defaults
cp ~/.two-face/colors.toml ~/.two-face/colors-backup.toml
Step 2: Edit colors.toml
# Dark theme with blue accents
[presets.speech]
fg = "#6eb5ff"
[presets.monsterbold]
fg = "#ffd700"
[presets.links]
fg = "#87ceeb"
[ui]
border_color = "#2196f3"
focused_border_color = "#64b5f6"
text_color = "#e0e0e0"
background_color = "#121212"
Step 3: Reload
.reload colors
Theme Examples
High Contrast (Accessibility)
[ui]
text_color = "#ffffff"
background_color = "#000000"
focused_border_color = "#ffff00"
[presets.monsterbold]
fg = "#ff0000"
[presets.speech]
fg = "#00ff00"
Solarized Dark
[ui]
background_color = "#002b36"
text_color = "#839496"
border_color = "#073642"
focused_border_color = "#2aa198"
[presets.speech]
fg = "#859900"
[presets.monsterbold]
fg = "#cb4b16"
Nord Theme
[ui]
background_color = "#2e3440"
text_color = "#d8dee9"
border_color = "#3b4252"
focused_border_color = "#88c0d0"
[presets.speech]
fg = "#a3be8c"
[presets.monsterbold]
fg = "#d08770"
Color Format Reference
| Format | Example | Notes |
|---|---|---|
#RRGGBB | #FF5733 | Standard hex |
#rrggbb | #ff5733 | Lowercase also valid |
| Named | crimson | From color_palette |
| Empty | "" | Use default/transparent |
| Dash | "-" | Transparent (textarea only) |
Browser Commands
| Command | Description |
|---|---|
.colors | Browse color palette |
.spellcolors | Browse/edit spell colors |
.addspellcolor <spell> <color> | Add spell color |
.reload colors | Hot-reload colors.toml |
See Also
- Colors Configuration - Full colors.toml reference
- Highlights Configuration - Using colors in highlights
- Parser Protocol - How presets are applied
Browser & Editor System
Two-Face provides interactive popup interfaces for browsing and editing configuration without leaving the application.
Overview
The browser/editor system includes:
- Browsers - Read-only lists with selection (highlights, keybinds, colors)
- Editors - Forms for creating/modifying configuration
- Window Editor - Complex form for widget configuration
Architecture
┌─────────────────────────────────────────────────────────────────┐
│ User Interface │
├─────────────────────────────────────────────────────────────────┤
│ ┌──────────────┐ ┌──────────────┐ ┌───────────────────────┐ │
│ │ Browser │ │ Editor │ │ WindowEditor │ │
│ │ (List View) │ │ (Form View) │ │ (Complex Form) │ │
│ └──────────────┘ └──────────────┘ └───────────────────────┘ │
├─────────────────────────────────────────────────────────────────┤
│ Widget Traits │
│ ┌───────────┐ ┌───────────┐ ┌──────────┐ ┌────────────────┐ │
│ │ Navigable │ │ Selectable│ │ Saveable │ │ TextEditable │ │
│ └───────────┘ └───────────┘ └──────────┘ └────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
Widget Traits
All browsers and editors implement behavior traits.
Navigable
For list navigation:
#![allow(unused)]
fn main() {
pub trait Navigable {
fn navigate_up(&mut self); // Move selection up
fn navigate_down(&mut self); // Move selection down
fn page_up(&mut self); // Move up ~10 items
fn page_down(&mut self); // Move down ~10 items
fn home(&mut self) {} // Move to first (optional)
fn end(&mut self) {} // Move to last (optional)
}
}
Selectable
For selecting items:
#![allow(unused)]
fn main() {
pub trait Selectable {
fn get_selected(&self) -> Option<String>;
fn delete_selected(&mut self) -> Option<String>;
}
}
TextEditable
For text input fields:
#![allow(unused)]
fn main() {
pub trait TextEditable {
fn get_focused_field(&self) -> Option<&TextArea<'static>>;
fn get_focused_field_mut(&mut self) -> Option<&mut TextArea<'static>>;
fn select_all(&mut self); // Ctrl+A
fn copy_to_clipboard(&self); // Ctrl+C
fn cut_to_clipboard(&mut self); // Ctrl+X
fn paste_from_clipboard(&mut self); // Ctrl+V
}
}
FieldNavigable
For form field navigation:
#![allow(unused)]
fn main() {
pub trait FieldNavigable {
fn next_field(&mut self); // Tab
fn previous_field(&mut self); // Shift+Tab
fn field_count(&self) -> usize;
fn current_field(&self) -> usize;
}
}
Saveable
For forms that persist data:
#![allow(unused)]
fn main() {
pub trait Saveable {
type SaveResult;
fn try_save(&mut self) -> Option<Self::SaveResult>;
fn is_modified(&self) -> bool;
}
}
Toggleable / Cyclable
For boolean and enum fields:
#![allow(unused)]
fn main() {
pub trait Toggleable {
fn toggle_focused(&mut self) -> Option<bool>;
}
pub trait Cyclable {
fn cycle_forward(&mut self); // Space/Down
fn cycle_backward(&mut self); // Up
}
}
Browser Types
HighlightBrowser
Browse configured text highlights.
Command: .highlights
Features:
- Sorted by category, then name
- Color preview with sample text
- Category filtering
- Shows squelch status
- Shows sound indicator
- Shows redirect info
┌─ Highlights ────────────────────────────────────────┐
│ │
│ ═══ Combat ═══ │
│ > critical_hit "critical hit" [█████] │
│ damage_taken "strikes you" [█████] │
│ damage_dealt "You hit" [█████] │
│ │
│ ═══ Chat ═══ │
│ speech "^\\w+ says" [█████] │
│ whisper "whispers" [█████] ♪ │
│ │
│ [Enter] Edit [Delete] Remove [Escape] Close │
└─────────────────────────────────────────────────────┘
KeybindBrowser
Browse configured keybinds.
Command: .keybinds
Features:
- Grouped by type (Actions, Macros)
- Shows key combo and action
- Columnar layout
┌─ Keybinds ──────────────────────────────────────────┐
│ │
│ ═══ Actions ═══ │
│ > Ctrl+L layout_editor │
│ Ctrl+H highlight_browser │
│ F1 help │
│ │
│ ═══ Macros ═══ │
│ Ctrl+1 "attack target" │
│ Ctrl+2 "stance defensive" │
│ │
│ [Enter] Edit [Delete] Remove [Escape] Close │
└─────────────────────────────────────────────────────┘
ColorPaletteBrowser
Browse named color palette.
Command: .colors
Features:
- Grouped by color category
- Visual color swatches
- Hex code display
┌─ Color Palette ─────────────────────────────────────┐
│ │
│ ═══ Red ═══ │
│ > ██ red #FF0000 │
│ ██ crimson #DC143C │
│ ██ darkred #8B0000 │
│ │
│ ═══ Green ═══ │
│ ██ green #00FF00 │
│ ██ forestgreen #228B22 │
│ │
│ [Escape] Close │
└─────────────────────────────────────────────────────┘
SpellColorBrowser
Browse spell-specific colors.
Command: .spellcolors
Features:
- Spell number to color mapping
- Color preview bars
- Add/edit/delete support
ThemeBrowser
Browse available themes.
Command: .themes
Features:
- List saved theme profiles
- Preview theme colors
- Apply/save themes
Window Editor
The WindowEditor provides comprehensive widget configuration.
Command: .window <widget_name>
Field Categories
Geometry:
row,col- Positionrows,cols- Sizemin_rows,min_cols- Minimum sizemax_rows,max_cols- Maximum size
Appearance:
title- Window titleshow_title- Toggle title displaytitle_position- Title placementbg_color- Background colortext_color- Text colortransparent_bg- Use terminal background
Borders:
show_border- Enable bordersborder_style- Style (plain, rounded, double)border_color- Border colorborder_top/bottom/left/right- Individual sides
Content:
streams- Stream IDs for text widgetsbuffer_size- Max line countwordwrap- Enable word wrappingtimestamps- Show timestampscontent_align- Text alignment
Widget-Specific Fields
Tabbed Text:
tab_bar_position- Top or bottomtab_active_color- Active tab colortab_inactive_color- Inactive tab color
Compass:
compass_active_color- Available direction colorcompass_inactive_color- Unavailable direction color
Progress Bars:
progress_label- Bar labelprogress_color- Bar fill color
Dashboard:
dashboard_layout- Horizontal or verticaldashboard_spacing- Item spacing
Common Browser Features
Popup Dragging
All browsers support dragging by the title bar:
#![allow(unused)]
fn main() {
pub fn handle_mouse(
&mut self,
mouse_col: u16,
mouse_row: u16,
mouse_down: bool,
area: Rect,
) -> bool {
let on_title_bar = mouse_row == self.popup_y
&& mouse_col > self.popup_x
&& mouse_col < self.popup_x + popup_width - 1;
if mouse_down && on_title_bar && !self.is_dragging {
self.is_dragging = true;
self.drag_offset_x = mouse_col.saturating_sub(self.popup_x);
self.drag_offset_y = mouse_row.saturating_sub(self.popup_y);
return true;
}
// ...
}
}
Scroll with Category Headers
Browsers with categories handle scrolling with sticky headers for context.
Menu Keybinds
Common keybinds for browsers and editors:
| Key | Action | Context |
|---|---|---|
Up/k | Navigate up | All browsers |
Down/j | Navigate down | All browsers |
PageUp | Page up | All browsers |
PageDown | Page down | All browsers |
Enter | Select/confirm | Browsers → Editor |
Escape | Close/cancel | All |
Tab | Next field | Editors |
Shift+Tab | Previous field | Editors |
Space | Toggle/cycle | Toggleable/Cyclable |
Delete | Delete selected | Selectable |
Ctrl+S | Save | Saveable |
Form Validation
Editors validate input before saving:
#![allow(unused)]
fn main() {
impl Saveable for HighlightForm {
type SaveResult = HighlightPattern;
fn try_save(&mut self) -> Option<Self::SaveResult> {
// Validate required fields
let name = self.name_field.lines()[0].trim();
if name.is_empty() {
self.error_message = Some("Name is required".to_string());
return None;
}
// Validate pattern syntax
let pattern = self.pattern_field.lines()[0].trim();
if let Err(e) = regex::Regex::new(pattern) {
self.error_message = Some(format!("Invalid regex: {}", e));
return None;
}
// Build result
Some(HighlightPattern {
name: name.to_string(),
pattern: pattern.to_string(),
// ...
})
}
}
}
Integration Points
Commands
| Command | Browser/Editor | Function |
|---|---|---|
.highlights | HighlightBrowser | Browse highlights |
.highlight <name> | HighlightForm | Edit highlight |
.addhighlight | HighlightForm | Add highlight |
.keybinds | KeybindBrowser | Browse keybinds |
.keybind <key> | KeybindForm | Edit keybind |
.addkeybind | KeybindForm | Add keybind |
.colors | ColorPaletteBrowser | Browse palette |
.spellcolors | SpellColorBrowser | Browse spell colors |
.themes | ThemeBrowser | Browse themes |
.window <name> | WindowEditor | Edit widget |
.layout | LayoutEditor | Edit layout |
UI State
Browsers are tracked in MenuState:
#![allow(unused)]
fn main() {
pub enum MenuState {
None,
HighlightBrowser(HighlightBrowser),
HighlightForm(HighlightForm),
KeybindBrowser(KeybindBrowser),
KeybindForm(KeybindForm),
ColorPaletteBrowser(ColorPaletteBrowser),
SpellColorBrowser(SpellColorBrowser),
ThemeBrowser(ThemeBrowser),
WindowEditor(WindowEditor),
// ...
}
}
Creating a New Browser
1. Define Entry Structure
#![allow(unused)]
fn main() {
#[derive(Clone)]
pub struct MyEntry {
pub id: String,
pub name: String,
pub value: String,
}
}
2. Create Browser Struct
#![allow(unused)]
fn main() {
pub struct MyBrowser {
entries: Vec<MyEntry>,
selected_index: usize,
scroll_offset: usize,
// Popup dragging
pub popup_x: u16,
pub popup_y: u16,
pub is_dragging: bool,
pub drag_offset_x: u16,
pub drag_offset_y: u16,
}
}
3. Implement Traits
#![allow(unused)]
fn main() {
impl Navigable for MyBrowser {
fn navigate_up(&mut self) {
if self.selected_index > 0 {
self.selected_index -= 1;
self.adjust_scroll();
}
}
// ...
}
impl Selectable for MyBrowser {
fn get_selected(&self) -> Option<String> {
self.entries.get(self.selected_index).map(|e| e.id.clone())
}
// ...
}
}
4. Implement Widget Rendering
#![allow(unused)]
fn main() {
impl Widget for &MyBrowser {
fn render(self, area: Rect, buf: &mut Buffer) {
// Clear popup area
Clear.render(area, buf);
// Draw border and title
let block = Block::default()
.borders(Borders::ALL)
.title(" My Browser ");
block.render(area, buf);
// Render entries with selection highlighting
for (idx, entry) in self.visible_entries().enumerate() {
let style = if idx == self.selected_index {
Style::default().add_modifier(Modifier::REVERSED)
} else {
Style::default()
};
// ... render entry
}
}
}
}
5. Register Command
#![allow(unused)]
fn main() {
".mybrowser" => {
let browser = MyBrowser::new(&config.my_data);
app.ui_state.menu_state = MenuState::MyBrowser(browser);
}
}
See Also
- Commands - All browser commands
- Keybinds Configuration - Browser keybinds
- Highlights Configuration - Highlight editing
Performance Optimization
Two-Face is designed for smooth, low-latency gameplay with 60+ FPS rendering and sub-millisecond event processing.
Performance Goals
- 60+ FPS - Smooth terminal rendering
- Sub-millisecond event processing - Responsive input
- Efficient memory usage - Bounded buffers
- Low network overhead - Streaming parser
Performance Telemetry
PerformanceStats
The centralized telemetry collector:
#![allow(unused)]
fn main() {
pub struct PerformanceStats {
// Frame timing
frame_times: VecDeque<Duration>,
last_frame_time: Instant,
max_frame_samples: usize, // Default: 60
// Network stats
bytes_received: u64,
bytes_sent: u64,
bytes_received_last_second: u64,
bytes_sent_last_second: u64,
// Parser stats
parse_times: VecDeque<Duration>,
chunks_parsed: u64,
chunks_parsed_last_second: u64,
// Render timing
render_times: VecDeque<Duration>,
ui_render_times: VecDeque<Duration>,
text_wrap_times: VecDeque<Duration>,
// Event processing
event_process_times: VecDeque<Duration>,
events_processed: u64,
// Memory tracking
total_lines_buffered: usize,
active_window_count: usize,
// Application uptime
app_start_time: Instant,
}
}
Recording Methods
#![allow(unused)]
fn main() {
// Frame timing
stats.record_frame(); // Called each frame
stats.record_render_time(duration); // Total render
stats.record_ui_render_time(duration); // UI widgets
stats.record_text_wrap_time(duration); // Text wrapping
// Network
stats.record_bytes_received(bytes);
stats.record_bytes_sent(bytes);
// Parser
stats.record_parse(duration);
stats.record_elements_parsed(count);
// Events
stats.record_event_process_time(duration);
// Memory
stats.update_memory_stats(total_lines, window_count);
}
Available Metrics
| Metric | Method | Unit |
|---|---|---|
| FPS | fps() | frames/sec |
| Avg frame time | avg_frame_time_ms() | ms |
| Min frame time | min_frame_time_ms() | ms |
| Max frame time | max_frame_time_ms() | ms |
| Frame jitter | frame_jitter_ms() | ms (stddev) |
| Frame spikes | frame_spike_count() | count >33ms |
| Network in | bytes_received_per_sec() | bytes/sec |
| Network out | bytes_sent_per_sec() | bytes/sec |
| Parse time | avg_parse_time_us() | μs |
| Chunks/sec | chunks_per_sec() | count/sec |
| Render time | avg_render_time_ms() | ms |
| Lines buffered | total_lines_buffered() | count |
| Memory estimate | estimated_memory_mb() | MB |
| Uptime | uptime() | Duration |
Generation-Based Change Detection
Efficient Change Tracking
Widgets use generation counters instead of content comparison:
#![allow(unused)]
fn main() {
pub struct TextContent {
pub lines: VecDeque<StyledLine>,
pub generation: u64, // Increments on every add_line()
}
}
Sync Logic
#![allow(unused)]
fn main() {
let last_gen = last_synced_generation.get(name).unwrap_or(0);
let current_gen = text_content.generation;
// Only sync if generation changed - O(1) check
if current_gen > last_gen {
let delta = (current_gen - last_gen) as usize;
// Sync only new lines
}
}
Benefits
- O(1) change detection - Compare numbers, not content
- Incremental updates - Only new lines synced
- Automatic full resync - Detects when buffer cleared
Text Wrapping Optimization
Text wrapping is expensive. Two-Face optimizes it through:
Width-Based Invalidation
#![allow(unused)]
fn main() {
pub fn set_width(&mut self, width: u16) {
if self.current_width != width {
self.current_width = width;
self.invalidate_wrap_cache();
}
}
}
Lazy Wrapping
Lines wrapped on-demand during render:
#![allow(unused)]
fn main() {
fn render_visible_lines(&self, visible_height: usize) -> Vec<WrappedLine> {
// Only wrap lines that will be displayed
let visible_range = self.calculate_visible_range(visible_height);
self.lines
.iter()
.skip(visible_range.start)
.take(visible_range.len())
.flat_map(|line| self.wrap_line(line))
.collect()
}
}
Segment-Aware Wrapping
Preserves styling across word boundaries:
#![allow(unused)]
fn main() {
fn wrap_line(&self, line: &StyledLine) -> Vec<WrappedLine> {
for segment in &line.segments {
for word in segment.text.split_whitespace() {
if current_width + word_width > self.max_width {
// Emit current line, start new
}
// Add word with segment's style
}
}
}
}
Memory Management
Line Buffer Limits
Each text window has a maximum:
#![allow(unused)]
fn main() {
pub struct TextContent {
pub lines: VecDeque<StyledLine>,
pub max_lines: usize, // Default: 1000
}
impl TextContent {
pub fn add_line(&mut self, line: StyledLine) {
self.lines.push_back(line);
// Trim oldest lines
while self.lines.len() > self.max_lines {
self.lines.pop_front();
}
self.generation += 1;
}
}
}
VecDeque Efficiency
VecDeque provides O(1) operations at both ends:
#![allow(unused)]
fn main() {
lines.push_back(new_line); // O(1) add to end
lines.pop_front(); // O(1) remove from start
}
Memory Estimation
#![allow(unused)]
fn main() {
pub fn estimated_memory_mb(&self) -> f64 {
// ~200 bytes per line average
let line_bytes = self.total_lines_buffered * 200;
line_bytes as f64 / (1024.0 * 1024.0)
}
}
Per-Window Configuration
[[windows]]
name = "main"
buffer_size = 2000 # More history for main
[[windows]]
name = "thoughts"
buffer_size = 500 # Less for secondary
Render Optimization
Dirty Tracking
Widgets track whether they need re-rendering:
#![allow(unused)]
fn main() {
impl TextWindow {
pub fn is_dirty(&self) -> bool {
self.dirty
}
fn add_line(&mut self, line: StyledLine) {
self.dirty = true;
}
}
}
Double Buffering
Ratatui uses double-buffered rendering to minimize flicker.
Partial Rendering
Only changed widgets are re-rendered when possible.
Highlight Pattern Optimization
Regex Caching
Patterns compiled once at load:
#![allow(unused)]
fn main() {
pub fn compile_highlight_patterns(highlights: &mut HashMap<String, HighlightPattern>) {
for (name, pattern) in highlights.iter_mut() {
if !pattern.fast_parse {
pattern.compiled_regex = Some(regex::Regex::new(&pattern.pattern)?);
}
}
}
}
Fast Parse Mode
For simple literals, Aho-Corasick is faster:
[highlights.names]
pattern = "Bob|Alice|Charlie"
fg = "#ffff00"
fast_parse = true # Uses Aho-Corasick
Early Exit
Skip highlight if text already styled:
#![allow(unused)]
fn main() {
if segment.span_type != SpanType::Normal {
continue; // Monsterbold, Link, etc. take priority
}
}
Network Optimization
Buffered I/O
#![allow(unused)]
fn main() {
let reader = BufReader::new(stream);
}
Non-Blocking Polling
#![allow(unused)]
fn main() {
if crossterm::event::poll(Duration::from_millis(10))? {
// Handle event
}
}
Streaming Parser
Processes data as it arrives, not waiting for complete messages.
Performance Widget
Display real-time metrics:
[[windows]]
name = "performance"
type = "performance"
show_fps = true
show_frame_times = true
show_render_times = true
show_net = true
show_parse = true
show_memory = true
show_lines = true
show_uptime = true
Selective Collection
Enable only displayed metrics to reduce overhead:
#![allow(unused)]
fn main() {
stats.apply_enabled_from(&performance_widget_data);
}
Profiling
Built-in Metrics
Use the performance widget for real-time profiling.
Tracing
RUST_LOG=two_face=debug cargo run
Release Build
Always profile with release builds:
cargo build --release
Release optimizations:
- Full optimization (
opt-level = 3) - Link-time optimization (
lto = true) - Single codegen unit
Best Practices
For Users
- Reduce buffer sizes for secondary windows
- Use fast_parse for simple highlight patterns
- Disable unused performance metrics
- Keep highlight count reasonable (<100 patterns)
- Use compact layouts when possible
For Developers
- Use generation counters for change detection
- Avoid content comparison - use flags or counters
- Batch updates when possible
- Profile with release builds
- Use VecDeque for FIFO buffers
- Cache expensive computations
Performance Troubleshooting
Low FPS
- Reduce buffer sizes
- Disable unused widgets
- Reduce highlight patterns
- Use
fast_parse = true
High Memory
- Reduce
buffer_sizeon windows - Close unused tabs
- Check for memory leaks (rising delta)
High Latency
- Check network connection
- Reduce highlight patterns
- Simplify layout
Frame Spikes
- Check text wrapping (long lines)
- Reduce highlight complexity
- Profile with tracing
See Also
- Widget Sync - Generation-based sync details
- Configuration - Performance settings
- Troubleshooting - Performance fixes
Network Overview
Two-Face supports two connection modes for connecting to GemStone IV and DragonRealms servers.
Connection Modes
| Mode | Description | Use Case |
|---|---|---|
| Lich Proxy | Connect through Lich middleware | Standard setup, script support |
| Direct eAccess | Authenticate directly with Simutronics | Standalone, no Lich required |
Quick Comparison
| Feature | Lich Proxy | Direct eAccess |
|---|---|---|
| Lich scripts | ✓ Yes | ✗ No |
| Setup complexity | Medium | Simple |
| Dependencies | Lich, Ruby | OpenSSL |
| Authentication | Handled by Lich | Built-in |
| Latency | Slightly higher | Minimal |
Connection Flow
Lich Proxy Mode
┌─────────┐ ┌──────┐ ┌────────────┐ ┌──────────┐
│Two-Face │────▶│ Lich │────▶│ Game Server │────▶│ Response │
└─────────┘ └──────┘ └────────────┘ └──────────┘
▲ │ │
└──────────────┴────────────────────────────────┘
Two-Face connects to Lich’s local proxy port (typically 8000). Lich handles authentication and provides script execution capabilities.
Direct eAccess Mode
┌─────────┐ ┌─────────┐ ┌────────────┐
│Two-Face │────▶│ eAccess │────▶│ Game Server │
└─────────┘ └─────────┘ └────────────┘
│ │ │
└───────────────┴────────────────┘
Direct TLS connection
Two-Face authenticates directly with Simutronics’ eAccess servers, then connects to the game server.
Which Mode Should I Use?
Use Lich Proxy If:
- You use Lich scripts (recommended for most players)
- You want the Lich ecosystem (scripts, plugins)
- You’re already familiar with Lich
- You need advanced automation
Use Direct eAccess If:
- You want a standalone client
- You don’t use Lich scripts
- You want minimal latency
- You prefer fewer dependencies
- Lich isn’t available for your platform
Network Requirements
Ports
| Service | Port | Protocol |
|---|---|---|
| Lich proxy | 8000 (configurable) | TCP |
| eAccess | 7900/7910 | TLS |
| Game servers | Various | TCP |
Firewall Rules
If behind a firewall, ensure outbound connections to:
eaccess.play.net(port 7900/7910)- Game server IPs (assigned dynamically)
Security
Lich Mode
- Lich handles credential storage
- Connection to Lich is local (127.0.0.1)
- Game traffic may or may not be encrypted depending on Lich version
Direct Mode
- TLS encryption to eAccess
- Certificate pinning for authentication servers
- Credentials passed at runtime (not stored)
Configuration
Lich Mode
# config.toml
[connection]
mode = "lich"
host = "127.0.0.1"
port = 8000
Direct Mode
# config.toml
[connection]
mode = "direct"
game = "gs4" # or "dr" for DragonRealms
Credentials are provided via command line or environment variables (never stored in config).
Guides
This section covers:
- Lich Proxy - Setting up Lich connection
- Direct eAccess - Direct authentication
- TLS Certificates - Certificate management
- Troubleshooting - Connection problems
Quick Start
Lich Mode (Recommended)
- Install and configure Lich
- Start Lich and log into your character
- Launch Two-Face:
two-face --host 127.0.0.1 --port 8000
Direct Mode
- Launch Two-Face with credentials:
two-face --direct \ --account YOUR_ACCOUNT \ --password YOUR_PASSWORD \ --game prime \ --character CHARACTER_NAME
See Also
Lich Proxy Connection
Connect Two-Face through Lich for full script support and the Lich ecosystem.
Overview
Lich is a middleware application that sits between your client and the game server. It provides:
- Script execution (Ruby-based automation)
- Plugin support
- Community scripts repository
- Character management
Two-Face connects to Lich’s local proxy port, receiving game data and sending commands through Lich.
Prerequisites
- Lich installed and configured
- Ruby (required by Lich)
- Game account and subscription
Installing Lich
Download Lich
Visit the Lich Project website or community resources to download Lich.
Install Ruby
Lich requires Ruby. Install the appropriate version for your platform:
Windows:
- Download RubyInstaller from https://rubyinstaller.org/
- Install with DevKit option
macOS:
brew install ruby
Linux:
sudo apt install ruby ruby-dev
Configure Lich
- Run Lich setup wizard
- Enter your Simutronics credentials
- Select your character
- Configure proxy port (default: 8000)
Connecting Two-Face
Basic Connection
Once Lich is running and logged in:
two-face --host 127.0.0.1 --port 8000
Configuration File
Add to ~/.two-face/config.toml:
[connection]
mode = "lich"
host = "127.0.0.1"
port = 8000
auto_reconnect = true
reconnect_delay = 5
Command Line Options
two-face --host HOST --port PORT [OPTIONS]
Options:
--host HOST Lich proxy host (default: 127.0.0.1)
--port PORT Lich proxy port (default: 8000)
--no-reconnect Disable auto-reconnect
Connection Workflow
Step-by-Step
-
Start Lich
ruby lich.rb -
Log into character via Lich
- Use Lich’s login interface
- Select character
- Wait for game connection
-
Verify Lich is listening
- Lich should show proxy port status
- Default: listening on port 8000
-
Connect Two-Face
two-face --host 127.0.0.1 --port 8000 -
Verify connection
- Two-Face should display game output
- Commands typed in Two-Face should work in game
Diagram
┌──────────────────────────────────────────────────────────────┐
│ Your Computer │
│ │
│ ┌───────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Two-Face │◄───────▶│ Lich │◄───────▶│ Scripts │ │
│ └───────────┘ └──────────┘ └──────────┘ │
│ │ │ │
│ │ │ Port 8000 │
│ └─────────────────────┘ │
│ │ │
└─────────────────────────────┼───────────────────────────────┘
│
▼ Internet
┌──────────────────┐
│ Game Server │
└──────────────────┘
Multi-Character Setup
Different Ports per Character
If running multiple characters:
Lich configuration:
- Character 1: Port 8000
- Character 2: Port 8001
- Character 3: Port 8002
Two-Face instances:
# Terminal 1
two-face --port 8000
# Terminal 2
two-face --port 8001
# Terminal 3
two-face --port 8002
Profile-Based Configuration
Create character profiles in ~/.two-face/profiles/:
# ~/.two-face/profiles/warrior.toml
[connection]
port = 8000
# ~/.two-face/profiles/wizard.toml
[connection]
port = 8001
Launch with profile:
two-face --profile warrior
two-face --profile wizard
Lich Script Integration
Running Scripts
Lich scripts are controlled through Lich, not Two-Face. Common commands:
;script_name # Run a script
;kill script_name # Stop a script
;pause script_name # Pause a script
;unpause script_name # Resume a script
;list # List running scripts
These commands are sent to Lich, which executes them.
Script Output
Script output appears in Two-Face’s main text window alongside game output. You can filter script messages using stream filtering if Lich tags them appropriately.
Script Commands vs Game Commands
| Prefix | Destination | Example |
|---|---|---|
; | Lich (scripts) | ;go2 bank |
. | Two-Face (client) | .reload config |
| (none) | Game server | north |
Lich Settings
Proxy Configuration
In Lich’s configuration:
# Listen on all interfaces (for remote access)
proxy_host = "0.0.0.0"
proxy_port = 8000
# Listen on localhost only (more secure)
proxy_host = "127.0.0.1"
proxy_port = 8000
XML Mode
Ensure Lich is configured to pass through game XML:
xml_passthrough = true
Two-Face requires XML tags to properly parse game state.
Performance Considerations
Latency
Lich adds minimal latency (typically <10ms) due to local proxy processing.
Memory
Running Lich + Ruby + scripts uses additional memory. Typical usage:
- Lich base: ~50-100MB
- Per character: +20-50MB
- Scripts: Varies by script
CPU
Script execution uses CPU. Complex scripts may impact performance during heavy automation.
Troubleshooting
Connection Refused
Error: Connection refused to 127.0.0.1:8000
Solutions:
- Verify Lich is running
- Check Lich has logged into a character
- Confirm proxy port matches (check Lich status)
- Try a different port if 8000 is in use
Lich Not Receiving Commands
Solutions:
- Check Two-Face is connected (look for game output)
- Verify commands aren’t being intercepted
- Check for Lich script conflicts
XML Not Parsing
If widgets aren’t updating:
Solutions:
- Enable XML passthrough in Lich
- Check Lich version supports XML
- Verify game has XML mode enabled
Disconnects
If connection drops frequently:
Solutions:
- Check network stability
- Enable auto-reconnect:
[connection] auto_reconnect = true reconnect_delay = 5 - Check Lich logs for errors
Port Already in Use
Error: Port 8000 already in use
Solutions:
- Close other applications using the port
- Change Lich’s proxy port
- Find the process using the port:
# Linux/macOS lsof -i :8000 # Windows netstat -ano | findstr :8000
Advanced Configuration
Remote Lich Access
To connect from a different machine (advanced):
-
Configure Lich to listen on network:
proxy_host = "0.0.0.0" -
Connect from Two-Face:
two-face --host 192.168.1.100 --port 8000 -
Security warning: Only do this on trusted networks!
SSH Tunneling
For secure remote access:
# On remote machine
ssh -L 8000:localhost:8000 user@lich-server
# Then connect Two-Face locally
two-face --host 127.0.0.1 --port 8000
See Also
- Direct eAccess - Alternative without Lich
- Troubleshooting - More connection issues
- Configuration - Full config reference
Direct eAccess Connection
Connect directly to GemStone IV and DragonRealms without requiring Lich as middleware.
Overview
Direct eAccess mode allows Two-Face to authenticate directly with Simutronics’ servers, bypassing the need for Lich. This provides:
- Standalone operation
- Lower latency
- Fewer dependencies
- Simpler setup
Trade-off: No Lich script support in this mode.
Prerequisites
- OpenSSL library
- Game account and subscription
- Character already created
Installation Requirements
Windows
OpenSSL is installed via vcpkg during the build process:
- Ensure
VCPKG_ROOTenvironment variable is set - vcpkg handles OpenSSL installation automatically
If building from source:
vcpkg install openssl:x64-windows
macOS
OpenSSL is typically available via Homebrew:
brew install openssl
Linux
Install OpenSSL development package:
# Debian/Ubuntu
sudo apt install libssl-dev
# Fedora/RHEL
sudo dnf install openssl-devel
# Arch
sudo pacman -S openssl
Connecting
Command Line
two-face --direct \
--account YOUR_ACCOUNT \
--password YOUR_PASSWORD \
--game prime \
--character CHARACTER_NAME
Options
| Option | Description | Example |
|---|---|---|
--direct | Enable direct eAccess mode | Required flag |
--account | Simutronics account name | myaccount |
--password | Account password | mypassword |
--game | Game instance | prime, test, dr |
--character | Character name | Mycharacter |
Game Instance Values
| Value | Game |
|---|---|
prime | GemStone IV (Prime) |
test | GemStone IV (Test) |
plat | GemStone IV (Platinum) |
dr | DragonRealms |
drt | DragonRealms (Test) |
Environment Variables
For security, credentials can be passed via environment:
export TF_ACCOUNT="myaccount"
export TF_PASSWORD="mypassword"
export TF_GAME="prime"
export TF_CHARACTER="Mycharacter"
two-face --direct
Authentication Flow
How It Works
┌──────────────────────────────────────────────────────────────────┐
│ Direct eAccess Flow │
│ │
│ ┌─────────┐ TLS ┌─────────────┐ │
│ │Two-Face │◄──────────▶│ eAccess │ 1. TLS Handshake │
│ └─────────┘ │ 7900/7910 │ 2. Request hash key │
│ │ └─────────────┘ 3. Send credentials │
│ │ │ 4. Get character list │
│ │ │ 5. Request launch key │
│ │ ▼ │
│ │ ┌─────────────┐ │
│ └────────────────▶│ Game Server │ 6. Connect with key │
│ └─────────────┘ │
└──────────────────────────────────────────────────────────────────┘
Technical Details
- TLS Connection: Connects to
eaccess.play.net:7910using TLS - Challenge-Response:
- Requests 32-byte hash key from server
- Password is obfuscated:
((char - 32) ^ hashkey[i]) + 32
- Session Creation: Receives character list and subscription info
- Launch Ticket: Requests and receives game server connection key
- Game Connection: Connects to assigned game server with launch key
Security Implementation
- TLS Encryption: All eAccess communication is encrypted
- Certificate Pinning: Server certificate is verified (stored at
~/.two-face/simu.pem) - No SNI: Server Name Indication is disabled to match protocol requirements
- Single-Write TLS: Commands sent as single TLS records for compatibility
Certificate Management
First Connection
On first direct connection, Two-Face:
- Downloads the eAccess server certificate
- Stores it at
~/.two-face/simu.pem - Uses it for verification on subsequent connections
Certificate Location
~/.two-face/
└── simu.pem # eAccess TLS certificate
Refreshing Certificate
If authentication fails due to certificate issues:
# Delete stored certificate
rm ~/.two-face/simu.pem
# Reconnect to download fresh certificate
two-face --direct --account ... --password ... --game ... --character ...
Configuration
Config File (Partial)
You can specify some options in config, but credentials should not be stored:
# ~/.two-face/config.toml
[connection]
mode = "direct"
game = "prime"
# Do NOT store credentials in config file!
Recommended Approach
Use a wrapper script or shell alias:
# ~/.bashrc or ~/.zshrc
alias gs4='two-face --direct --game prime --account $GS4_ACCOUNT --password $GS4_PASSWORD --character'
# Usage
gs4 Mycharacter
Or a script with secure password handling:
#!/bin/bash
# gs4-connect.sh
read -sp "Password: " PASSWORD
echo
two-face --direct \
--account "$1" \
--password "$PASSWORD" \
--game prime \
--character "$2"
Multi-Character
Sequential Characters
Direct mode connects one character at a time. To switch:
- Exit Two-Face
- Reconnect with different
--characterparameter
Parallel Characters
Run multiple Two-Face instances in separate terminals:
# Terminal 1
two-face --direct --account myaccount --password mypass --game prime --character Warrior
# Terminal 2
two-face --direct --account myaccount --password mypass --game prime --character Wizard
Each instance maintains its own connection.
Performance
Latency Comparison
| Mode | Typical Latency |
|---|---|
| Direct | ~50-100ms (network only) |
| Lich | ~60-120ms (network + proxy) |
Direct mode eliminates the local proxy hop, providing marginally lower latency.
Resource Usage
- Memory: ~20-30MB base (no Lich overhead)
- CPU: Minimal (only TLS processing)
- Network: Standard game traffic
Troubleshooting
Authentication Failed
Error: Authentication failed
Solutions:
- Verify account credentials are correct
- Test login via official client or Lich
- Check account is active (not banned/suspended)
- Delete and re-download certificate:
rm ~/.two-face/simu.pem
Invalid Character
Error: Character not found
Solutions:
- Check character name spelling (case-sensitive)
- Verify character exists on specified game instance
- Check account has access to that game
Connection Timeout
Error: Connection to eaccess.play.net timed out
Solutions:
- Check internet connectivity
- Verify firewall allows port 7910 outbound
- Try again (may be temporary server issue)
TLS Handshake Failed
Error: TLS handshake failed
Solutions:
- Update OpenSSL to latest version
- Delete certificate and retry:
rm ~/.two-face/simu.pem - Check system time is correct (TLS requires accurate time)
OpenSSL Not Found
Error: OpenSSL library not found
Solutions:
- Install OpenSSL for your platform
- Set library path if non-standard location
- On Windows, verify vcpkg installation
Security Considerations
Credential Handling
- Never store passwords in config files
- Use environment variables or interactive prompts
- Clear shell history if credentials were typed
Network Security
- All eAccess traffic is TLS encrypted
- Game server traffic uses standard (unencrypted) protocol
- Certificate pinning prevents MITM attacks on authentication
Logging
Debug logs may contain sensitive information:
- Don’t share logs publicly without redaction
- Logs are stored in
~/.two-face/two-face.log
Comparison with Lich Mode
| Aspect | Direct eAccess | Lich Proxy |
|---|---|---|
| Scripts | ✗ None | ✓ Full Lich scripts |
| Setup | Simple | Medium |
| Dependencies | OpenSSL | Lich + Ruby |
| Latency | Lower | Slightly higher |
| Memory | Lower | Higher |
| Multi-char | Separate instances | Lich manages |
| Community | Limited | Large ecosystem |
When to Use Direct Mode
Good for:
- Players who don’t use scripts
- Minimal installations
- Performance-sensitive setups
- Learning/testing the game
Not recommended for:
- Heavy script users
- Automation needs
- Community script ecosystem access
See Also
- Lich Proxy - Alternative with script support
- TLS Certificates - Certificate details
- Troubleshooting - More connection issues
TLS Certificates
Understanding and managing TLS certificates for secure connections.
Overview
Two-Face uses TLS (Transport Layer Security) for encrypted communication with Simutronics’ eAccess authentication servers. This page covers certificate management for direct eAccess mode.
Certificate Architecture
Connection Security
┌─────────────────────────────────────────────────────────────┐
│ TLS Connection Flow │
│ │
│ Two-Face eAccess Server │
│ │ │ │
│ │──── ClientHello ───────────▶│ │
│ │ │ │
│ │◀─── ServerHello + Cert ─────│ │
│ │ │ │
│ │ Verify cert against │ │
│ │ stored simu.pem │ │
│ │ │ │
│ │──── Key Exchange ──────────▶│ │
│ │ │ │
│ │◀═══ Encrypted Channel ═════▶│ │
│ │
└─────────────────────────────────────────────────────────────┘
Why Certificate Pinning?
The eAccess server uses a self-signed certificate. Certificate pinning:
- First connection: Downloads and stores the certificate
- Subsequent connections: Verifies server presents the same certificate
- Protection: Prevents man-in-the-middle attacks
Certificate Storage
Location
~/.two-face/
└── simu.pem # eAccess server certificate
File Format
The certificate is stored in PEM format:
-----BEGIN CERTIFICATE-----
MIIDxTCCAq2gAwIBAgIJAK... (base64 encoded)
-----END CERTIFICATE-----
Permissions
The certificate file should have restricted permissions:
# Linux/macOS
chmod 600 ~/.two-face/simu.pem
# Windows
# File inherits user permissions from .two-face folder
Certificate Lifecycle
First Connection
On first direct eAccess connection:
- Two-Face connects to
eaccess.play.net:7910 - Server sends its certificate during TLS handshake
- Two-Face saves certificate to
~/.two-face/simu.pem - Connection continues with authentication
Subsequent Connections
On later connections:
- Two-Face loads stored certificate
- During TLS handshake, compares server’s certificate
- If match: connection proceeds
- If mismatch: connection fails (security protection)
Certificate Renewal
If Simutronics updates their certificate:
- Connection will fail (certificate mismatch)
- Delete the old certificate:
rm ~/.two-face/simu.pem - Reconnect to download new certificate
- Future connections use new certificate
Managing Certificates
Viewing Certificate
# View certificate details
openssl x509 -in ~/.two-face/simu.pem -text -noout
# View expiration date
openssl x509 -in ~/.two-face/simu.pem -enddate -noout
# View fingerprint
openssl x509 -in ~/.two-face/simu.pem -fingerprint -noout
Example Output
Certificate:
Data:
Version: 3 (0x2)
Serial Number: ...
Signature Algorithm: sha256WithRSAEncryption
Issuer: C=US, ST=..., O=Simutronics...
Validity
Not Before: Jan 1 00:00:00 2020 GMT
Not After : Dec 31 23:59:59 2030 GMT
Subject: CN=eaccess.play.net...
Deleting Certificate
To force certificate refresh:
# Linux/macOS
rm ~/.two-face/simu.pem
# Windows (PowerShell)
Remove-Item ~\.two-face\simu.pem
# Windows (Command Prompt)
del %USERPROFILE%\.two-face\simu.pem
Backing Up Certificate
If you want to preserve your certificate:
cp ~/.two-face/simu.pem ~/.two-face/simu.pem.backup
Troubleshooting
Certificate Verification Failed
Error: Certificate verification failed
Causes:
- Certificate changed on server
- Certificate file corrupted
- System time incorrect
Solutions:
- Delete and re-download:
rm ~/.two-face/simu.pem - Check system time is accurate
- Verify network isn’t intercepting traffic
Certificate Not Found
Error: Could not load certificate from ~/.two-face/simu.pem
Causes:
- First connection hasn’t occurred
- Certificate was deleted
- Permission issues
Solutions:
- Run direct connection to auto-download
- Check folder permissions
- Verify path is correct
TLS Handshake Error
Error: TLS handshake failed
Causes:
- OpenSSL version incompatibility
- Network proxy interference
- Server configuration changed
Solutions:
- Update OpenSSL
- Disable network proxies
- Delete certificate and retry
Self-Signed Certificate Warning
The eAccess certificate is self-signed, which is expected. Two-Face handles this through certificate pinning rather than chain validation.
Advanced Topics
Manual Certificate Download
If auto-download fails, manually retrieve:
# Connect and save certificate
openssl s_client -connect eaccess.play.net:7910 \
-servername "" \
</dev/null 2>/dev/null | \
openssl x509 -outform PEM > ~/.two-face/simu.pem
Note: The empty -servername "" disables SNI, which is required.
Certificate Validation
To manually verify a certificate:
# Compare fingerprints
openssl x509 -in ~/.two-face/simu.pem -fingerprint -sha256 -noout
Compare the fingerprint with known-good values from the community.
Multiple Certificates
If connecting to different game servers (test vs production):
~/.two-face/
├── simu.pem # Production eAccess
├── simu-test.pem # Test server (if different)
Configure via:
[connection]
certificate = "simu.pem" # or "simu-test.pem"
Security Best Practices
Do
- ✓ Keep certificate file permissions restrictive
- ✓ Verify certificate fingerprint periodically
- ✓ Delete and refresh if authentication fails unexpectedly
- ✓ Keep OpenSSL updated
Don’t
- ✗ Share your certificate file publicly
- ✗ Ignore certificate verification failures
- ✗ Use the same certificate across different machines (download fresh)
- ✗ Disable certificate verification
Technical Details
TLS Configuration
Two-Face’s TLS connection uses:
| Setting | Value | Reason |
|---|---|---|
| Protocol | TLS 1.2+ | Security |
| SNI | Disabled | Server requirement |
| Session caching | Disabled | Protocol compatibility |
| Cipher suites | System default | OpenSSL manages |
Single-Write Requirement
A critical implementation detail: commands must be sent as single TLS Application Data records. Two-Face ensures this by building complete messages in memory before writing to the TLS stream.
#![allow(unused)]
fn main() {
// Correct: Single write
let message = format!("{}\n", line);
stream.write_all(message.as_bytes())?;
// Incorrect: Multiple writes (would fail)
stream.write_all(line.as_bytes())?;
stream.write_all(b"\n")?;
}
See Also
- Direct eAccess - Direct connection setup
- Troubleshooting - Connection problems
- Network Overview - Connection modes
Network Troubleshooting
Solutions for common connection issues with Two-Face.
Quick Diagnostics
Check Connection Status
# Verify Two-Face is running
ps aux | grep two-face
# Check if port is in use (Lich mode)
netstat -an | grep 8000
# Test internet connectivity
ping eaccess.play.net
View Logs
# Tail the log file
tail -f ~/.two-face/two-face.log
# Search for errors
grep -i error ~/.two-face/two-face.log
Common Issues
Connection Refused
Symptom:
Error: Connection refused to 127.0.0.1:8000
Cause: Lich proxy isn’t running or listening on the expected port.
Solutions:
-
Verify Lich is running:
# Check for Lich process ps aux | grep -i lich -
Check Lich has connected to game:
- Lich must be logged into a character
- The proxy only starts after game connection
-
Verify port number:
- Check Lich’s configured proxy port
- Default is 8000, but may be different
-
Try a different port:
# If 8000 is blocked, configure Lich for 8001 two-face --port 8001
Connection Timeout
Symptom:
Error: Connection timed out
Cause: Network issue between Two-Face and the target.
Solutions:
-
Lich mode - Check Lich is responding:
telnet 127.0.0.1 8000 -
Direct mode - Check internet connectivity:
# Test eAccess server nc -zv eaccess.play.net 7910 # Or with OpenSSL openssl s_client -connect eaccess.play.net:7910 -
Firewall check:
- Ensure port 7910 is allowed outbound
- Check corporate/school firewall policies
-
Try again later:
- Simutronics servers may be temporarily unavailable
- Check official forums for server status
Authentication Failed
Symptom:
Error: Authentication failed
Cause: Invalid credentials or account issue.
Solutions:
-
Verify credentials:
- Double-check account name (case-insensitive)
- Verify password (case-sensitive)
- Test login via official client first
-
Account status:
- Ensure subscription is active
- Check account isn’t banned/suspended
- Verify no billing issues
-
Character name:
- Confirm character exists
- Check spelling (often case-sensitive)
- Verify character is on correct game instance
-
Certificate issue (direct mode):
# Reset certificate rm ~/.two-face/simu.pem # Retry connection
TLS Handshake Failed
Symptom:
Error: TLS handshake failed
Cause: SSL/TLS configuration issue.
Solutions:
-
Update OpenSSL:
# Check version openssl version # Update (varies by OS) # macOS: brew upgrade openssl # Ubuntu: sudo apt update && sudo apt upgrade openssl -
Reset certificate:
rm ~/.two-face/simu.pem -
Check system time:
- TLS requires accurate system time
- Sync with NTP server
-
Network proxy:
- Disable SSL inspection proxies
- Corporate proxies may intercept TLS
Character Not Found
Symptom:
Error: Character 'Mychar' not found on account
Cause: Character doesn’t exist or wrong game instance.
Solutions:
-
Check character name spelling:
- Some systems are case-sensitive
- No spaces or special characters
-
Verify game instance:
# GemStone IV Prime --game prime # GemStone IV Platinum --game plat # GemStone IV Test --game test # DragonRealms --game dr -
List characters:
- Login via official client to see character list
- Verify account has characters on that game
Disconnected Unexpectedly
Symptom: Connection drops during gameplay.
Cause: Network instability, server kick, or timeout.
Solutions:
-
Enable auto-reconnect:
[connection] auto_reconnect = true reconnect_delay = 5 -
Check network stability:
# Monitor packet loss ping -c 100 eaccess.play.net -
Idle timeout:
- Game may disconnect idle connections
- Configure keepalive if available
-
Server-side kick:
- Check for policy violations
- Too many connections from same IP
- Anti-automation detection
Port Already in Use
Symptom:
Error: Address already in use: 0.0.0.0:8000
Cause: Another application is using the port.
Solutions:
-
Find the process:
# Linux/macOS lsof -i :8000 # Windows netstat -ano | findstr :8000 -
Kill the process:
# Linux/macOS kill -9 <PID> # Windows taskkill /F /PID <PID> -
Use different port:
- Configure Lich for different port
- Connect Two-Face to new port
No Data Received
Symptom: Connected but no game output appears.
Cause: Stream configuration or parsing issue.
Solutions:
-
Check stream configuration:
# Ensure main window has appropriate streams [[widgets]] type = "text" streams = ["main", "room", "combat"] -
Verify XML passthrough (Lich mode):
- Lich must pass through game XML
- Check Lich configuration
-
Test with simple layout:
# Minimal test layout [[widgets]] type = "text" streams = [] # Empty = all streams x = 0 y = 0 width = 100 height = 100 -
Check logs for parsing errors:
grep -i parse ~/.two-face/two-face.log
SSL Certificate Error
Symptom:
Error: SSL certificate problem: self signed certificate
Cause: Certificate verification issue (expected for eAccess).
Solutions:
-
This is normal for first connection - certificate should be pinned automatically
-
If persistent, reset certificate:
rm ~/.two-face/simu.pem -
Check no MITM proxy is intercepting traffic
Platform-Specific Issues
Windows
OpenSSL not found:
# Ensure vcpkg installed OpenSSL
vcpkg list | findstr openssl
# Set VCPKG_ROOT
set VCPKG_ROOT=C:\path\to\vcpkg
Permission denied on config:
- Run as Administrator once to create directories
- Or manually create
%USERPROFILE%\.two-face\
macOS
OpenSSL version mismatch:
# Use Homebrew OpenSSL
export OPENSSL_DIR=$(brew --prefix openssl)
# Or install specific version
brew install openssl@3
Keychain access:
- macOS may prompt for keychain access
- Allow Two-Face to access certificates
Linux
Missing libraries:
# Debian/Ubuntu
sudo apt install libssl-dev ca-certificates
# Fedora
sudo dnf install openssl-devel ca-certificates
# Arch
sudo pacman -S openssl
SELinux blocking:
# Check for denials
ausearch -m AVC -ts recent
# Temporarily permissive (testing only)
sudo setenforce 0
Advanced Diagnostics
Network Capture
# Capture Lich traffic
tcpdump -i lo port 8000 -w lich_traffic.pcap
# Capture eAccess traffic
tcpdump -i eth0 host eaccess.play.net -w eaccess_traffic.pcap
Debug Logging
Enable verbose logging:
# config.toml
[logging]
level = "debug"
file = "~/.two-face/debug.log"
Test Mode
Some issues can be isolated with test connections:
# Test Lich connection
echo "look" | nc localhost 8000
# Test eAccess with OpenSSL
openssl s_client -connect eaccess.play.net:7910 -quiet
Getting Help
If issues persist:
- Check logs:
~/.two-face/two-face.log - Reproduce minimally: Simple config, default layout
- Search issues: Check GitHub issues for similar problems
- Report bug: Include:
- Two-Face version
- OS and version
- Connection mode
- Relevant log excerpts (redact credentials!)
- Steps to reproduce
See Also
- Lich Proxy - Lich connection details
- Direct eAccess - Direct connection details
- TLS Certificates - Certificate management
Development Guide
Resources for developers who want to build, modify, or contribute to Two-Face.
Overview
Two-Face is written in Rust, targeting terminal-based user interfaces. This section covers:
- Building from source
- Understanding the codebase
- Extending functionality
- Contributing to the project
Quick Start
Clone and Build
# Clone the repository
git clone https://github.com/your-repo/two-face.git
cd two-face
# Build in release mode
cargo build --release
# Run
./target/release/two-face --help
Prerequisites
- Rust 1.70+ (latest stable recommended)
- Cargo (comes with Rust)
- OpenSSL development libraries
- Platform-specific build tools
Documentation Structure
Building
Complete build instructions for all platforms:
- Development builds
- Release builds
- Cross-compilation
- Troubleshooting build issues
Project Structure
Tour of the codebase:
- Directory layout
- Module organization
- Key files and their purposes
- Architecture overview
Adding Widgets
Create new widget types:
- Widget trait implementation
- State management
- Rendering
- Testing widgets
Adding Browsers
Create new popup browser windows:
- Browser trait implementation
- Integration with widget system
- Event handling
Parser Extensions
Extend the XML parser:
- Adding new element types
- Custom parsing logic
- Stream handling
Testing
Test patterns and practices:
- Unit tests
- Integration tests
- Manual testing
- CI/CD
Contributing
How to contribute:
- Code style
- Pull request process
- Issue reporting
- Community guidelines
Development Workflow
Typical Development Cycle
1. Create feature branch
git checkout -b feature/my-feature
2. Make changes
- Write code
- Add tests
- Update documentation
3. Test locally
cargo test
cargo run -- [test args]
4. Format and lint
cargo fmt
cargo clippy
5. Submit PR
git push origin feature/my-feature
Development Build vs Release
| Aspect | Debug | Release |
|---|---|---|
| Command | cargo build | cargo build --release |
| Speed | Slow | Fast |
| Binary size | Large | Optimized |
| Debug symbols | Yes | No (by default) |
| Compile time | Fast | Slower |
Use debug builds for development, release for testing performance.
Architecture Overview
Three-Layer Design
┌───────────────────────────────────────────────┐
│ Frontend (TUI) │
│ Rendering, Input Handling, Themes │
├───────────────────────────────────────────────┤
│ Core │
│ State Management, Business Logic, Events │
├───────────────────────────────────────────────┤
│ Data │
│ Parsing, Models, Serialization │
└───────────────────────────────────────────────┘
Key Components
| Component | Purpose | Location |
|---|---|---|
| Parser | XML protocol handling | src/parser.rs |
| Widgets | UI components | src/data/widget.rs |
| State | Application state | src/core/ |
| TUI | Terminal rendering | src/frontend/tui/ |
| Config | Configuration loading | src/config.rs |
| Network | Connection handling | src/network.rs |
Getting Help
Documentation
- This guide (you’re reading it)
- Inline code documentation (
cargo doc --open) - Architecture docs in
docs/
Community
- GitHub Issues for bugs and features
- Discussions for questions
Code Questions
- Read existing similar code
- Check test files for usage examples
- Ask in GitHub Discussions
Tools
Recommended IDE
- VS Code with rust-analyzer
- IntelliJ IDEA with Rust plugin
- Vim/Neovim with rust.vim
Useful Commands
# Generate documentation
cargo doc --open
# Run specific test
cargo test test_name
# Check without building
cargo check
# Format code
cargo fmt
# Lint code
cargo clippy
# Watch for changes
cargo watch -x check
See Also
- Architecture Overview - System design
- Configuration - Config file format
- Widget Reference - Widget documentation
Building from Source
Complete instructions for building Two-Face on all supported platforms.
Prerequisites
Rust Toolchain
Install Rust via rustup:
# Install rustup (Linux/macOS)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Or on Windows, download from https://rustup.rs/
Verify installation:
rustc --version # Should be 1.70+
cargo --version
Platform-Specific Dependencies
Windows
Option 1: Visual Studio Build Tools
- Install Visual Studio Build Tools
- Select “Desktop development with C++”
Option 2: vcpkg for OpenSSL
# Clone vcpkg
git clone https://github.com/Microsoft/vcpkg.git
cd vcpkg
./bootstrap-vcpkg.bat
# Install OpenSSL
./vcpkg install openssl:x64-windows
# Set environment variable
set VCPKG_ROOT=C:\path\to\vcpkg
macOS
# Install Xcode Command Line Tools
xcode-select --install
# Install OpenSSL via Homebrew
brew install openssl
# Set OpenSSL path (add to ~/.zshrc or ~/.bashrc)
export OPENSSL_DIR=$(brew --prefix openssl)
Linux (Debian/Ubuntu)
sudo apt update
sudo apt install build-essential pkg-config libssl-dev
Linux (Fedora/RHEL)
sudo dnf install gcc openssl-devel pkg-config
Linux (Arch)
sudo pacman -S base-devel openssl pkg-config
Basic Build
Clone Repository
git clone https://github.com/your-repo/two-face.git
cd two-face
Debug Build
For development with debug symbols:
cargo build
Binary location: target/debug/two-face
Release Build
For optimized production build:
cargo build --release
Binary location: target/release/two-face
Run Directly
Without building separately:
# Debug mode
cargo run -- --help
# Release mode
cargo run --release -- --host 127.0.0.1 --port 8000
Build Profiles
Debug Profile (default)
# Cargo.toml [profile.dev]
opt-level = 0
debug = true
Fast compilation, slow execution, large binary.
Release Profile
# Cargo.toml [profile.release]
opt-level = 3
lto = true
codegen-units = 1
Slow compilation, fast execution, small binary.
Custom Profile
Create a custom profile in Cargo.toml:
[profile.release-with-debug]
inherits = "release"
debug = true
Build with: cargo build --profile release-with-debug
Feature Flags
Two-Face may have optional features:
# Build with specific features
cargo build --features "feature1,feature2"
# Build with all features
cargo build --all-features
# Build without default features
cargo build --no-default-features
Check Cargo.toml for available features.
Cross-Compilation
Setup
Install cross-compilation targets:
# List available targets
rustup target list
# Add a target
rustup target add x86_64-unknown-linux-gnu
rustup target add x86_64-pc-windows-gnu
rustup target add aarch64-apple-darwin
Build for Target
cargo build --release --target x86_64-unknown-linux-gnu
Using cross
For easier cross-compilation with Docker:
# Install cross
cargo install cross
# Build for Linux from any platform
cross build --release --target x86_64-unknown-linux-gnu
Build Optimization
Link-Time Optimization (LTO)
Already enabled in release profile:
[profile.release]
lto = true
Strip Symbols
Reduce binary size:
# After building
strip target/release/two-face
Or configure in Cargo.toml:
[profile.release]
strip = true
Size Optimization
For minimal binary size:
[profile.release]
opt-level = "z" # Optimize for size
lto = true
strip = true
panic = "abort"
codegen-units = 1
Dependency Management
Update Dependencies
# Check for outdated dependencies
cargo outdated
# Update all dependencies
cargo update
# Update specific dependency
cargo update -p package_name
Audit Dependencies
# Install cargo-audit
cargo install cargo-audit
# Check for security vulnerabilities
cargo audit
Build Troubleshooting
OpenSSL Not Found
Error: Could not find directory of OpenSSL installation
Windows Solution:
# Set vcpkg root
set VCPKG_ROOT=C:\path\to\vcpkg
# Or set OpenSSL dir directly
set OPENSSL_DIR=C:\path\to\openssl
macOS Solution:
export OPENSSL_DIR=$(brew --prefix openssl)
export OPENSSL_INCLUDE_DIR=$OPENSSL_DIR/include
export OPENSSL_LIB_DIR=$OPENSSL_DIR/lib
Linux Solution:
# Debian/Ubuntu
sudo apt install libssl-dev pkg-config
# Ensure pkg-config can find it
pkg-config --libs openssl
Linker Errors
Error: linking with cc failed
Solutions:
- Install build tools for your platform
- Check library paths are correct
- Ensure all dependencies are installed
Out of Memory
Error: out of memory during compilation
Solutions:
- Use fewer codegen units:
[profile.release] codegen-units = 1 - Reduce parallel jobs:
cargo build -j 2 - Close other applications
Slow Builds
Improve build times:
-
Use
sccache:cargo install sccache export RUSTC_WRAPPER=sccache -
Use
moldlinker (Linux):# Install mold sudo apt install mold # Configure in .cargo/config.toml [target.x86_64-unknown-linux-gnu] linker = "clang" rustflags = ["-C", "link-arg=-fuse-ld=mold"] -
Incremental compilation (default in debug)
CI/CD
GitHub Actions Example
name: Build
on: [push, pull_request]
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
steps:
- uses: actions/checkout@v3
- name: Install Rust
uses: dtolnay/rust-action@stable
- name: Build
run: cargo build --release
- name: Test
run: cargo test
Verification
Run Tests
cargo test
Run Application
# Show help
./target/release/two-face --help
# Test connection
./target/release/two-face --host 127.0.0.1 --port 8000
Check Binary
# File type
file target/release/two-face
# Dependencies (Linux)
ldd target/release/two-face
# Size
ls -lh target/release/two-face
See Also
- Project Structure - Codebase overview
- Testing - Test patterns
- Contributing - Contribution guide
Project Structure
A guided tour of the Two-Face codebase.
Directory Layout
two-face/
├── Cargo.toml # Project manifest
├── Cargo.lock # Dependency lock file
├── CLAUDE.md # Development notes
├── README.md # Project readme
│
├── src/
│ ├── main.rs # Entry point
│ ├── lib.rs # Library root (if applicable)
│ ├── config.rs # Configuration loading
│ ├── parser.rs # XML protocol parser
│ ├── network.rs # Network connections
│ │
│ ├── core/ # Core business logic
│ │ ├── mod.rs
│ │ ├── app_core/ # Application state
│ │ │ ├── mod.rs
│ │ │ ├── state.rs
│ │ │ └── layout.rs
│ │ └── menu_actions.rs
│ │
│ ├── data/ # Data models
│ │ ├── mod.rs
│ │ └── widget.rs # Widget data structures
│ │
│ └── frontend/ # User interface
│ └── tui/ # Terminal UI
│ ├── mod.rs
│ ├── input.rs
│ ├── input_handlers.rs
│ ├── command_input.rs
│ ├── text_window.rs
│ ├── tabbed_text_window.rs
│ ├── search.rs
│ ├── sync.rs
│ ├── menu_actions.rs
│ └── window_editor.rs
│
├── book/ # mdbook documentation
│ ├── book.toml
│ └── src/
│ └── *.md
│
├── tests/ # Integration tests
│ └── *.rs
│
└── docs/ # Additional documentation
Module Organization
Three-Layer Architecture
┌─────────────────────────────────────────────────────────────┐
│ src/frontend/tui/ │
│ │
│ Terminal rendering, input handling, visual themes │
│ Knows about: core, data │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ src/core/ │
│ │
│ Application state, business logic, event processing │
│ Knows about: data │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ src/data/ │
│ │
│ Data models, parsing, serialization │
│ Knows about: nothing (foundation layer) │
└─────────────────────────────────────────────────────────────┘
Import rule: Upper layers can import lower layers, not vice versa.
Key Files
Entry Point: src/main.rs
// Typical structure
fn main() {
// Parse command line arguments
let args = Args::parse();
// Load configuration
let config = Config::load(&args)?;
// Initialize application
let app = App::new(config)?;
// Run main loop
app.run()?;
}
Responsibilities:
- CLI argument parsing
- Configuration loading
- Application initialization
- Main event loop
Configuration: src/config.rs
#![allow(unused)]
fn main() {
// Configuration structures
pub struct Config {
pub connection: ConnectionConfig,
pub layout: LayoutConfig,
pub colors: ColorConfig,
pub keybinds: KeybindConfig,
// ...
}
impl Config {
pub fn load(path: &Path) -> Result<Self> { ... }
pub fn save(&self, path: &Path) -> Result<()> { ... }
}
}
Responsibilities:
- TOML parsing
- Default values
- Validation
- Serialization
Parser: src/parser.rs
#![allow(unused)]
fn main() {
// XML protocol parsing
pub enum ParsedElement {
Text(String),
RoomName(String),
RoomDesc(String),
Prompt(PromptData),
Vitals(VitalsData),
// Many more variants...
}
pub struct Parser {
// Parser state
}
impl Parser {
pub fn parse(&mut self, input: &str) -> Vec<ParsedElement> { ... }
}
}
Responsibilities:
- XML tag recognition
- State machine parsing
- Element extraction
- Stream identification
Network: src/network.rs
#![allow(unused)]
fn main() {
// Connection handling
pub enum ConnectionMode {
Lich { host: String, port: u16 },
Direct { account: String, ... },
}
pub struct Connection {
mode: ConnectionMode,
stream: TcpStream,
}
impl Connection {
pub fn connect(mode: ConnectionMode) -> Result<Self> { ... }
pub fn send(&mut self, data: &str) -> Result<()> { ... }
pub fn receive(&mut self) -> Result<String> { ... }
}
}
Responsibilities:
- Connection establishment
- TLS handling (direct mode)
- Data transmission
- Reconnection logic
Widget Data: src/data/widget.rs
#![allow(unused)]
fn main() {
// Widget type definitions
pub enum WidgetType {
Text,
TabbedText,
Progress,
Compass,
// ...
}
pub struct WidgetConfig {
pub widget_type: WidgetType,
pub name: String,
pub x: u16,
pub y: u16,
pub width: u16,
pub height: u16,
// Type-specific config...
}
}
Responsibilities:
- Widget type enumeration
- Configuration structures
- Layout data
- Serialization traits
Application State: src/core/app_core/state.rs
#![allow(unused)]
fn main() {
// Application state management
pub struct AppState {
pub vitals: Vitals,
pub room: RoomInfo,
pub inventory: Inventory,
pub indicators: Indicators,
// ...
generation: u64, // Change tracking
}
impl AppState {
pub fn update(&mut self, element: ParsedElement) {
// Update state based on parsed element
self.generation += 1;
}
}
}
Responsibilities:
- Game state tracking
- Update processing
- Change notification
- Generation counting
Layout: src/core/app_core/layout.rs
#![allow(unused)]
fn main() {
// Layout management
pub struct Layout {
widgets: Vec<Widget>,
focus_index: usize,
}
impl Layout {
pub fn load(config: &LayoutConfig) -> Result<Self> { ... }
pub fn render(&self, frame: &mut Frame) { ... }
pub fn handle_input(&mut self, key: KeyEvent) { ... }
}
}
Responsibilities:
- Widget arrangement
- Focus management
- Layout loading
- Coordinate calculation
TUI Module: src/frontend/tui/mod.rs
#![allow(unused)]
fn main() {
// Terminal UI main module
pub struct Tui {
terminal: Terminal<Backend>,
layout: Layout,
state: AppState,
}
impl Tui {
pub fn new() -> Result<Self> { ... }
pub fn run(&mut self) -> Result<()> { ... }
pub fn render(&mut self) -> Result<()> { ... }
pub fn handle_event(&mut self, event: Event) -> Result<()> { ... }
}
}
Responsibilities:
- Terminal setup/teardown
- Event loop
- Rendering coordination
- Input routing
Data Flow
Incoming Data
Network → Parser → State Update → Widget Sync → Render
│ │ │ │ │
│ │ │ │ └─ TUI draws frame
│ │ │ └─ Widgets check generation
│ │ └─ AppState updates, bumps generation
│ └─ Raw XML → ParsedElements
└─ TCP/TLS receives bytes
User Input
Terminal Event → TUI → Handler → Action → Effect
│ │ │ │ │
│ │ │ │ └─ State change or command send
│ │ │ └─ Keybind lookup / macro expansion
│ │ └─ Input type routing (key/mouse/resize)
│ └─ Event loop captures
└─ User presses key
Testing Structure
tests/
├── integration_test.rs # Full application tests
├── parser_tests.rs # Parser-specific tests
└── widget_tests.rs # Widget rendering tests
src/
├── parser.rs
│ └── #[cfg(test)] mod tests { ... } # Unit tests
└── config.rs
└── #[cfg(test)] mod tests { ... } # Unit tests
Configuration Files
User configuration (runtime):
~/.two-face/
├── config.toml
├── layout.toml
├── colors.toml
├── highlights.toml
├── keybinds.toml
├── triggers.toml
└── simu.pem
Documentation
book/ # mdbook documentation (this!)
├── book.toml
└── src/
├── SUMMARY.md
└── *.md
docs/ # Additional docs
└── *.md
Build Artifacts
target/
├── debug/ # Debug build
│ └── two-face
├── release/ # Release build
│ └── two-face
└── doc/ # Generated documentation
└── two_face/
See Also
- Architecture - System design
- Adding Widgets - Extend widget system
- Parser Extensions - Extend parser
Adding Widgets
Guide to creating new widget types for Two-Face.
Overview
Widgets are the visual building blocks of Two-Face. Each widget type:
- Displays specific data
- Has its own rendering logic
- Responds to state changes
- May handle user input
Widget Architecture
Widget Lifecycle
Configuration → Creation → State Sync → Render → Input (optional)
│ │ │ │ │
│ │ │ │ └─ Handle focus/keys
│ │ │ └─ Draw to terminal frame
│ │ └─ Check generation, update data
│ └─ Instantiate from config
└─ TOML defines type and properties
Key Traits
Widgets implement several traits:
#![allow(unused)]
fn main() {
// Core widget behavior
pub trait Widget {
fn render(&self, frame: &mut Frame, area: Rect);
fn name(&self) -> &str;
fn can_focus(&self) -> bool;
}
// State synchronization
pub trait Syncable {
fn sync(&mut self, state: &AppState) -> bool;
fn last_generation(&self) -> u64;
}
// Input handling (optional)
pub trait Interactive {
fn handle_key(&mut self, key: KeyEvent) -> bool;
fn handle_mouse(&mut self, mouse: MouseEvent) -> bool;
}
}
Step-by-Step: New Widget
Let’s create a “SpellTimer” widget that displays active spell durations.
Step 1: Define the Widget Type
Add to src/data/widget.rs:
#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum WidgetType {
Text,
Progress,
Compass,
// ... existing types ...
SpellTimer, // Add new type
}
}
Step 2: Create Widget Configuration
#![allow(unused)]
fn main() {
// In src/data/widget.rs or dedicated file
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SpellTimerConfig {
pub name: String,
pub x: u16,
pub y: u16,
pub width: u16,
pub height: u16,
// Widget-specific options
pub show_duration: bool,
pub group_by_circle: bool,
pub max_display: usize,
}
impl Default for SpellTimerConfig {
fn default() -> Self {
Self {
name: "spell_timer".into(),
x: 0,
y: 0,
width: 20,
height: 10,
show_duration: true,
group_by_circle: false,
max_display: 10,
}
}
}
}
Step 3: Create Widget Structure
Create src/frontend/tui/spell_timer.rs:
#![allow(unused)]
fn main() {
use crate::core::AppState;
use crate::data::SpellTimerConfig;
use ratatui::prelude::*;
use ratatui::widgets::*;
pub struct SpellTimerWidget {
config: SpellTimerConfig,
spells: Vec<ActiveSpell>,
last_generation: u64,
}
#[derive(Clone)]
struct ActiveSpell {
name: String,
duration: u32,
circle: u8,
}
impl SpellTimerWidget {
pub fn new(config: SpellTimerConfig) -> Self {
Self {
config,
spells: Vec::new(),
last_generation: 0,
}
}
}
}
Step 4: Implement Widget Trait
#![allow(unused)]
fn main() {
impl Widget for SpellTimerWidget {
fn render(&self, frame: &mut Frame, area: Rect) {
// Create border
let block = Block::default()
.title("Spells")
.borders(Borders::ALL);
let inner = block.inner(area);
frame.render_widget(block, area);
// Render spell list
let items: Vec<ListItem> = self.spells
.iter()
.take(self.config.max_display)
.map(|spell| {
let text = if self.config.show_duration {
format!("{}: {}s", spell.name, spell.duration)
} else {
spell.name.clone()
};
ListItem::new(text)
})
.collect();
let list = List::new(items);
frame.render_widget(list, inner);
}
fn name(&self) -> &str {
&self.config.name
}
fn can_focus(&self) -> bool {
false // This widget doesn't need focus
}
}
}
Step 5: Implement Syncable Trait
#![allow(unused)]
fn main() {
impl Syncable for SpellTimerWidget {
fn sync(&mut self, state: &AppState) -> bool {
// Check if state has changed
if state.generation() == self.last_generation {
return false; // No changes
}
// Update spell list from state
self.spells = state.active_spells()
.iter()
.map(|s| ActiveSpell {
name: s.name.clone(),
duration: s.remaining_seconds,
circle: s.circle,
})
.collect();
// Apply grouping if configured
if self.config.group_by_circle {
self.spells.sort_by_key(|s| s.circle);
}
self.last_generation = state.generation();
true // Widget needs redraw
}
fn last_generation(&self) -> u64 {
self.last_generation
}
}
}
Step 6: Register Widget Type
In the widget factory (typically src/frontend/tui/mod.rs or dedicated factory):
#![allow(unused)]
fn main() {
pub fn create_widget(config: &WidgetConfig) -> Box<dyn Widget> {
match config.widget_type {
WidgetType::Text => Box::new(TextWidget::new(config.into())),
WidgetType::Progress => Box::new(ProgressWidget::new(config.into())),
// ... existing types ...
WidgetType::SpellTimer => Box::new(SpellTimerWidget::new(config.into())),
}
}
}
Step 7: Add to Module
In src/frontend/tui/mod.rs:
#![allow(unused)]
fn main() {
mod spell_timer;
pub use spell_timer::SpellTimerWidget;
}
Step 8: Document Configuration
Update TOML documentation:
# Example spell timer configuration
[[widgets]]
type = "spell_timer"
name = "spells"
x = 80
y = 0
width = 20
height = 15
show_duration = true
group_by_circle = true
max_display = 10
Widget Best Practices
State Management
#![allow(unused)]
fn main() {
// GOOD: Check generation before updating
fn sync(&mut self, state: &AppState) -> bool {
if state.generation() == self.last_generation {
return false;
}
// ... update ...
self.last_generation = state.generation();
true
}
// BAD: Always update (wasteful)
fn sync(&mut self, state: &AppState) -> bool {
// ... update ...
true // Always redraws
}
}
Efficient Rendering
#![allow(unused)]
fn main() {
// GOOD: Cache computed values
struct MyWidget {
cached_lines: Vec<Line<'static>>,
// ...
}
fn sync(&mut self, state: &AppState) -> bool {
// Rebuild cache only when data changes
self.cached_lines = self.compute_lines(state);
true
}
fn render(&self, frame: &mut Frame, area: Rect) {
// Use cached data
let paragraph = Paragraph::new(self.cached_lines.clone());
frame.render_widget(paragraph, area);
}
}
Configuration Validation
#![allow(unused)]
fn main() {
impl SpellTimerConfig {
pub fn validate(&self) -> Result<(), String> {
if self.width == 0 || self.height == 0 {
return Err("Widget must have non-zero dimensions".into());
}
if self.max_display == 0 {
return Err("max_display must be at least 1".into());
}
Ok(())
}
}
}
Error Handling
#![allow(unused)]
fn main() {
fn render(&self, frame: &mut Frame, area: Rect) {
// Handle edge cases gracefully
if area.width < 3 || area.height < 3 {
// Area too small to render
return;
}
if self.spells.is_empty() {
// Show placeholder
let text = Paragraph::new("No active spells");
frame.render_widget(text, area);
return;
}
// Normal rendering...
}
}
Testing Widgets
Unit Tests
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_widget_creation() {
let config = SpellTimerConfig::default();
let widget = SpellTimerWidget::new(config);
assert_eq!(widget.name(), "spell_timer");
}
#[test]
fn test_sync_updates_generation() {
let mut widget = SpellTimerWidget::new(Default::default());
let mut state = AppState::new();
state.set_generation(1);
assert!(widget.sync(&state));
assert_eq!(widget.last_generation(), 1);
// Same generation shouldn't trigger update
assert!(!widget.sync(&state));
}
}
}
Visual Testing
Create a test layout that isolates your widget:
# test_layout.toml
[[widgets]]
type = "spell_timer"
name = "test"
x = 0
y = 0
width = 100
height = 100
See Also
- Widget Reference - Existing widgets
- Project Structure - Code organization
- Architecture - Sync system
Adding Browsers
Guide to creating new browser (popup window) types for Two-Face.
Overview
Browsers are popup windows that provide extended functionality:
- Configuration editors
- Search interfaces
- Help dialogs
- Selection menus
They overlay the main interface temporarily and capture input focus.
Browser Architecture
Browser Lifecycle
Trigger → Creation → Render → Input Loop → Result → Cleanup
│ │ │ │ │ │
│ │ │ │ │ └─ Restore main UI
│ │ │ │ └─ Return data or action
│ │ │ └─ Handle keys until dismiss
│ │ └─ Draw overlay on screen
│ └─ Instantiate browser with context
└─ Keybind or menu action
Key Components
#![allow(unused)]
fn main() {
// Browser trait defines popup behavior
pub trait Browser {
fn render(&self, frame: &mut Frame, area: Rect);
fn handle_input(&mut self, key: KeyEvent) -> BrowserResult;
fn title(&self) -> &str;
}
// Result of browser interaction
pub enum BrowserResult {
Continue, // Keep browser open
Close, // Close without action
Action(BrowserAction), // Close with action
}
pub enum BrowserAction {
SelectItem(String),
UpdateConfig(ConfigChange),
ExecuteCommand(String),
// ...
}
}
Step-by-Step: New Browser
Let’s create a “HighlightEditor” browser for editing highlight patterns.
Step 1: Define Browser Structure
Create src/frontend/tui/highlight_editor.rs:
#![allow(unused)]
fn main() {
use ratatui::prelude::*;
use ratatui::widgets::*;
pub struct HighlightEditor {
highlights: Vec<HighlightEntry>,
selected: usize,
editing: Option<EditField>,
scroll_offset: usize,
}
struct HighlightEntry {
pattern: String,
foreground: String,
background: Option<String>,
enabled: bool,
}
enum EditField {
Pattern,
Foreground,
Background,
}
impl HighlightEditor {
pub fn new(highlights: Vec<HighlightConfig>) -> Self {
let entries = highlights.into_iter()
.map(|h| HighlightEntry {
pattern: h.pattern,
foreground: h.fg,
background: h.bg,
enabled: h.enabled,
})
.collect();
Self {
highlights: entries,
selected: 0,
editing: None,
scroll_offset: 0,
}
}
}
}
Step 2: Implement Browser Trait
#![allow(unused)]
fn main() {
impl Browser for HighlightEditor {
fn title(&self) -> &str {
"Highlight Editor"
}
fn render(&self, frame: &mut Frame, area: Rect) {
// Calculate centered popup area
let popup_area = centered_rect(80, 80, area);
// Clear background
frame.render_widget(Clear, popup_area);
// Draw border
let block = Block::default()
.title(self.title())
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Cyan));
let inner = block.inner(popup_area);
frame.render_widget(block, popup_area);
// Split into list and detail areas
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Percentage(50),
Constraint::Percentage(50),
])
.split(inner);
// Render highlight list
self.render_list(frame, chunks[0]);
// Render selected highlight details
self.render_details(frame, chunks[1]);
// Render help bar at bottom
self.render_help(frame, popup_area);
}
fn handle_input(&mut self, key: KeyEvent) -> BrowserResult {
match key.code {
KeyCode::Esc => {
if self.editing.is_some() {
self.editing = None;
BrowserResult::Continue
} else {
BrowserResult::Close
}
}
KeyCode::Enter => {
if self.editing.is_some() {
self.commit_edit();
self.editing = None;
} else {
self.start_edit();
}
BrowserResult::Continue
}
KeyCode::Up => {
self.move_selection(-1);
BrowserResult::Continue
}
KeyCode::Down => {
self.move_selection(1);
BrowserResult::Continue
}
KeyCode::Char('s') if key.modifiers.contains(KeyModifiers::CONTROL) => {
BrowserResult::Action(BrowserAction::UpdateConfig(
self.build_config_change()
))
}
KeyCode::Char('n') if key.modifiers.contains(KeyModifiers::CONTROL) => {
self.add_new_highlight();
BrowserResult::Continue
}
KeyCode::Delete => {
self.delete_selected();
BrowserResult::Continue
}
_ => {
if self.editing.is_some() {
self.handle_edit_key(key);
}
BrowserResult::Continue
}
}
}
}
}
Step 3: Implement Helper Methods
#![allow(unused)]
fn main() {
impl HighlightEditor {
fn render_list(&self, frame: &mut Frame, area: Rect) {
let items: Vec<ListItem> = self.highlights
.iter()
.enumerate()
.skip(self.scroll_offset)
.map(|(i, h)| {
let style = if i == self.selected {
Style::default().bg(Color::DarkGray)
} else {
Style::default()
};
let enabled = if h.enabled { "✓" } else { " " };
let text = format!("[{}] {}", enabled, h.pattern);
ListItem::new(text).style(style)
})
.collect();
let list = List::new(items)
.block(Block::default().title("Patterns").borders(Borders::ALL));
frame.render_widget(list, area);
}
fn render_details(&self, frame: &mut Frame, area: Rect) {
if let Some(highlight) = self.highlights.get(self.selected) {
let details = vec![
Line::from(vec![
Span::raw("Pattern: "),
Span::styled(&highlight.pattern, Style::default().fg(Color::Yellow)),
]),
Line::from(vec![
Span::raw("FG: "),
Span::styled(&highlight.foreground, Style::default().fg(Color::Green)),
]),
Line::from(vec![
Span::raw("BG: "),
Span::raw(highlight.background.as_deref().unwrap_or("none")),
]),
];
let paragraph = Paragraph::new(details)
.block(Block::default().title("Details").borders(Borders::ALL));
frame.render_widget(paragraph, area);
}
}
fn render_help(&self, frame: &mut Frame, area: Rect) {
let help = " Enter: Edit | Ctrl+S: Save | Ctrl+N: New | Del: Delete | Esc: Close ";
let help_area = Rect {
x: area.x,
y: area.y + area.height - 1,
width: area.width,
height: 1,
};
let help_text = Paragraph::new(help)
.style(Style::default().bg(Color::DarkGray));
frame.render_widget(help_text, help_area);
}
fn move_selection(&mut self, delta: i32) {
let new_idx = (self.selected as i32 + delta)
.max(0)
.min(self.highlights.len() as i32 - 1) as usize;
self.selected = new_idx;
}
fn start_edit(&mut self) {
self.editing = Some(EditField::Pattern);
}
fn commit_edit(&mut self) {
// Apply edit changes
}
fn add_new_highlight(&mut self) {
self.highlights.push(HighlightEntry {
pattern: "new_pattern".into(),
foreground: "white".into(),
background: None,
enabled: true,
});
self.selected = self.highlights.len() - 1;
}
fn delete_selected(&mut self) {
if !self.highlights.is_empty() {
self.highlights.remove(self.selected);
if self.selected >= self.highlights.len() && self.selected > 0 {
self.selected -= 1;
}
}
}
fn build_config_change(&self) -> ConfigChange {
// Convert entries back to config format
ConfigChange::Highlights(
self.highlights.iter()
.map(|h| HighlightConfig {
pattern: h.pattern.clone(),
fg: h.foreground.clone(),
bg: h.background.clone(),
enabled: h.enabled,
})
.collect()
)
}
}
// Helper function for centered popups
fn centered_rect(percent_x: u16, percent_y: u16, area: Rect) -> Rect {
let popup_layout = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Percentage((100 - percent_y) / 2),
Constraint::Percentage(percent_y),
Constraint::Percentage((100 - percent_y) / 2),
])
.split(area);
Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Percentage((100 - percent_x) / 2),
Constraint::Percentage(percent_x),
Constraint::Percentage((100 - percent_x) / 2),
])
.split(popup_layout[1])[1]
}
}
Step 4: Register Browser
Add to browser registry:
#![allow(unused)]
fn main() {
pub enum BrowserType {
Search,
Help,
LayoutEditor,
HighlightEditor, // New browser
}
pub fn open_browser(browser_type: BrowserType, context: BrowserContext) -> Box<dyn Browser> {
match browser_type {
BrowserType::Search => Box::new(SearchBrowser::new(context)),
BrowserType::Help => Box::new(HelpBrowser::new()),
BrowserType::LayoutEditor => Box::new(LayoutEditor::new(context)),
BrowserType::HighlightEditor => Box::new(HighlightEditor::new(context.highlights)),
}
}
}
Step 5: Add Keybind
Allow users to open the browser:
#![allow(unused)]
fn main() {
// In keybind action handling
KeybindAction::OpenHighlightEditor => {
self.active_browser = Some(open_browser(
BrowserType::HighlightEditor,
self.build_browser_context()
));
}
}
Browser Best Practices
Input Handling
#![allow(unused)]
fn main() {
// Always allow Escape to close
fn handle_input(&mut self, key: KeyEvent) -> BrowserResult {
match key.code {
KeyCode::Esc => BrowserResult::Close,
// ... other handling
}
}
// Provide keyboard help
fn render_help(&self) {
// Always show available key commands
}
}
State Management
#![allow(unused)]
fn main() {
// Keep original data for cancel
struct MyBrowser {
original: Vec<Item>, // For reverting
modified: Vec<Item>, // Working copy
}
fn handle_input(&mut self, key: KeyEvent) -> BrowserResult {
match key.code {
KeyCode::Esc => {
// Discard changes
BrowserResult::Close
}
KeyCode::Char('s') if ctrl => {
// Save changes
BrowserResult::Action(self.build_action())
}
}
}
}
Visual Feedback
#![allow(unused)]
fn main() {
// Indicate edit mode
fn render(&self, frame: &mut Frame, area: Rect) {
let title = if self.editing.is_some() {
format!("{} [EDITING]", self.title())
} else {
self.title().to_string()
};
// ...
}
// Highlight modified fields
fn render_field(&self, value: &str, modified: bool) -> Span {
let style = if modified {
Style::default().fg(Color::Yellow) // Modified indicator
} else {
Style::default()
};
Span::styled(value, style)
}
}
Testing Browsers
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_browser_escape_closes() {
let mut browser = HighlightEditor::new(vec![]);
let result = browser.handle_input(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
assert!(matches!(result, BrowserResult::Close));
}
#[test]
fn test_selection_movement() {
let mut browser = HighlightEditor::new(vec![
HighlightConfig::default(),
HighlightConfig::default(),
]);
assert_eq!(browser.selected, 0);
browser.handle_input(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE));
assert_eq!(browser.selected, 1);
browser.handle_input(KeyEvent::new(KeyCode::Up, KeyModifiers::NONE));
assert_eq!(browser.selected, 0);
}
}
}
See Also
Parser Extensions
Guide to extending the XML protocol parser for new game elements.
Overview
The parser converts raw game XML into structured data that widgets can display. Extending the parser allows Two-Face to:
- Recognize new game elements
- Extract additional data
- Support new game features
- Handle protocol changes
Parser Architecture
Data Flow
Raw Input → Tokenizer → Parser State Machine → ParsedElements
│ │ │ │
│ │ │ └─ Vec<ParsedElement>
│ │ └─ Match patterns, build elements
│ └─ Split into tags/text
└─ Bytes from network
Key Types
#![allow(unused)]
fn main() {
// Parser output - each element represents parsed game data
pub enum ParsedElement {
Text(String),
RoomName(String),
RoomDesc(String),
Prompt(PromptData),
Vitals(VitalsData),
Indicator(IndicatorData),
Compass(CompassData),
Stream { id: String, content: String },
// ... many more
}
// Parser state tracks context
pub struct Parser {
state: ParserState,
buffer: String,
current_stream: Option<String>,
// ...
}
enum ParserState {
Normal,
InTag(String),
InStream(String),
// ...
}
}
Step-by-Step: New Element
Let’s add parsing for a hypothetical “spell_active” element.
Step 1: Analyze the Protocol
First, understand the XML format:
<!-- Example game XML -->
<spell_active id="107" name="Spirit Shield" duration="300" circle="1"/>
<spell_expire id="107"/>
Document what data we need:
- Spell ID
- Spell name
- Duration (seconds)
- Circle
Step 2: Define the ParsedElement Variant
Add to src/parser.rs:
#![allow(unused)]
fn main() {
pub enum ParsedElement {
// ... existing variants ...
/// Active spell notification
SpellActive {
id: u32,
name: String,
duration: u32,
circle: u8,
},
/// Spell expiration notification
SpellExpire {
id: u32,
},
}
}
Step 3: Create Data Structures
#![allow(unused)]
fn main() {
#[derive(Debug, Clone)]
pub struct SpellActiveData {
pub id: u32,
pub name: String,
pub duration: u32,
pub circle: u8,
}
impl SpellActiveData {
pub fn from_attributes(attrs: &HashMap<String, String>) -> Option<Self> {
Some(Self {
id: attrs.get("id")?.parse().ok()?,
name: attrs.get("name")?.clone(),
duration: attrs.get("duration")?.parse().ok()?,
circle: attrs.get("circle")?.parse().ok()?,
})
}
}
}
Step 4: Add Parsing Logic
In the parser’s tag handling:
#![allow(unused)]
fn main() {
impl Parser {
fn handle_tag(&mut self, tag: &str) -> Option<ParsedElement> {
let (name, attrs) = self.parse_tag(tag)?;
match name.as_str() {
// ... existing tags ...
"spell_active" => {
let data = SpellActiveData::from_attributes(&attrs)?;
Some(ParsedElement::SpellActive {
id: data.id,
name: data.name,
duration: data.duration,
circle: data.circle,
})
}
"spell_expire" => {
let id = attrs.get("id")?.parse().ok()?;
Some(ParsedElement::SpellExpire { id })
}
_ => None,
}
}
}
}
Step 5: Handle in Application State
In src/core/app_core/state.rs:
#![allow(unused)]
fn main() {
impl AppState {
pub fn process_element(&mut self, element: ParsedElement) {
match element {
// ... existing handlers ...
ParsedElement::SpellActive { id, name, duration, circle } => {
self.active_spells.insert(id, ActiveSpell {
id,
name,
remaining: duration,
circle,
});
self.generation += 1;
}
ParsedElement::SpellExpire { id } => {
self.active_spells.remove(&id);
self.generation += 1;
}
// ...
}
}
}
}
Step 6: Add Tests
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_spell_active() {
let mut parser = Parser::new();
let input = r#"<spell_active id="107" name="Spirit Shield" duration="300" circle="1"/>"#;
let elements = parser.parse(input);
assert_eq!(elements.len(), 1);
match &elements[0] {
ParsedElement::SpellActive { id, name, duration, circle } => {
assert_eq!(*id, 107);
assert_eq!(name, "Spirit Shield");
assert_eq!(*duration, 300);
assert_eq!(*circle, 1);
}
_ => panic!("Expected SpellActive"),
}
}
#[test]
fn test_parse_spell_expire() {
let mut parser = Parser::new();
let input = r#"<spell_expire id="107"/>"#;
let elements = parser.parse(input);
assert_eq!(elements.len(), 1);
match &elements[0] {
ParsedElement::SpellExpire { id } => {
assert_eq!(*id, 107);
}
_ => panic!("Expected SpellExpire"),
}
}
#[test]
fn test_missing_attributes() {
let mut parser = Parser::new();
// Missing required 'name' attribute
let input = r#"<spell_active id="107" duration="300"/>"#;
let elements = parser.parse(input);
// Should not produce SpellActive (missing name)
assert!(elements.iter().all(|e| !matches!(e, ParsedElement::SpellActive { .. })));
}
}
}
Common Patterns
Self-Closing Tags
#![allow(unused)]
fn main() {
// <tag attr="value"/>
fn parse_self_closing(&mut self, tag: &str) -> Option<ParsedElement> {
if !tag.ends_with('/') {
return None;
}
let clean = tag.trim_end_matches('/');
self.handle_tag(clean)
}
}
Paired Tags with Content
#![allow(unused)]
fn main() {
// <tag>content</tag>
fn handle_open_tag(&mut self, name: &str, attrs: &HashMap<String, String>) {
match name {
"room_desc" => {
self.state = ParserState::InRoomDesc;
self.buffer.clear();
}
// ...
}
}
fn handle_close_tag(&mut self, name: &str) -> Option<ParsedElement> {
match name {
"room_desc" => {
let desc = std::mem::take(&mut self.buffer);
self.state = ParserState::Normal;
Some(ParsedElement::RoomDesc(desc))
}
// ...
}
}
}
Streaming Content
#![allow(unused)]
fn main() {
// <pushStream id="combat"/>...<popStream/>
fn handle_push_stream(&mut self, id: &str) {
self.stream_stack.push(id.to_string());
self.current_stream = Some(id.to_string());
}
fn handle_pop_stream(&mut self) {
self.stream_stack.pop();
self.current_stream = self.stream_stack.last().cloned();
}
}
Attribute Extraction
#![allow(unused)]
fn main() {
fn parse_attributes(tag_content: &str) -> HashMap<String, String> {
let mut attrs = HashMap::new();
let re = regex::Regex::new(r#"(\w+)="([^"]*)""#).unwrap();
for cap in re.captures_iter(tag_content) {
attrs.insert(cap[1].to_string(), cap[2].to_string());
}
attrs
}
}
Best Practices
Graceful Degradation
#![allow(unused)]
fn main() {
// GOOD: Handle missing attributes
fn from_attributes(attrs: &HashMap<String, String>) -> Option<Self> {
Some(Self {
required: attrs.get("required")?.clone(),
optional: attrs.get("optional").cloned(), // Optional field
})
}
// BAD: Panic on missing data
fn from_attributes(attrs: &HashMap<String, String>) -> Self {
Self {
required: attrs["required"].clone(), // Panics if missing!
}
}
}
Preserve Unknown Elements
#![allow(unused)]
fn main() {
// Don't discard unrecognized tags
fn handle_unknown_tag(&mut self, tag: &str) -> ParsedElement {
ParsedElement::Unknown {
tag: tag.to_string(),
raw: self.current_raw.clone(),
}
}
}
Performance
#![allow(unused)]
fn main() {
// GOOD: Compile regex once
lazy_static! {
static ref ATTR_REGEX: Regex = Regex::new(r#"(\w+)="([^"]*)""#).unwrap();
}
// BAD: Compile regex every time
fn parse_attrs(s: &str) {
let re = Regex::new(r#"(\w+)="([^"]*)""#).unwrap(); // Slow!
}
}
Debugging Parser Issues
Logging
#![allow(unused)]
fn main() {
fn handle_tag(&mut self, tag: &str) -> Option<ParsedElement> {
log::trace!("Parsing tag: {}", tag);
let result = self.parse_tag_internal(tag);
if result.is_none() {
log::debug!("Unhandled tag: {}", tag);
}
result
}
}
Test with Real Data
Save actual game output for test fixtures:
#![allow(unused)]
fn main() {
#[test]
fn test_real_game_output() {
let input = include_str!("../test_fixtures/combat_sample.xml");
let mut parser = Parser::new();
let elements = parser.parse(input);
// Verify expected elements are present
assert!(elements.iter().any(|e| matches!(e, ParsedElement::Combat { .. })));
}
}
See Also
- Parser Protocol - Protocol details
- Message Flow - Data flow
- Adding Widgets - Use parsed data
Testing
Test patterns and practices for Two-Face development.
Current Test Status
Two-Face has comprehensive test coverage:
| Test Type | Count | Description |
|---|---|---|
| Unit Tests | 907 | In-module tests (#[cfg(test)]) |
| Parser Integration | 34 | XML parsing verification |
| UI Integration | 57 | End-to-end UI state tests |
| Doc Tests | 4 | Documentation examples |
| Ignored | 1 | Clipboard (requires system) |
| Total | 1,003 | All tests pass ✅ |
Testing Philosophy
Two-Face testing prioritizes:
- Parser accuracy - Correctly parse game protocol
- State consistency - Updates propagate correctly
- Widget rendering - Display matches data
- Configuration loading - Files parse without errors
- Real-world validation - Tests use actual game XML
- End-to-end flow - XML → Parser → MessageProcessor → UiState
Test Organization
two-face/
├── src/
│ ├── parser.rs
│ │ └── #[cfg(test)] mod tests { ... }
│ ├── config.rs
│ │ └── #[cfg(test)] mod tests { ... }
│ ├── selection.rs
│ │ └── #[cfg(test)] mod tests { ... }
│ ├── clipboard.rs
│ │ └── #[cfg(test)] mod tests { ... }
│ └── ...
│
├── tests/ # Integration tests
│ ├── parser_integration.rs # 34 parser-level tests
│ ├── ui_integration.rs # 57 end-to-end UI tests
│ └── fixtures/ # 45 real game XML fixtures
│ ├── session_start.xml
│ ├── vitals_indicators.xml
│ ├── room_navigation.xml
│ ├── combat_roundtime.xml
│ ├── buffs_progress.xml
│ ├── active_effects.xml
│ ├── injuries.xml
│ ├── text_routing.xml
│ └── ... (45 total fixtures)
│
└── Cargo.toml
Unit Tests
Basic Structure
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_function_name() {
// Arrange
let input = "test data";
// Act
let result = function_under_test(input);
// Assert
assert_eq!(result, expected_value);
}
}
}
Parser Tests
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_room_name() {
let mut parser = Parser::new();
let input = "<roomName>Town Square</roomName>";
let elements = parser.parse(input);
assert_eq!(elements.len(), 1);
assert!(matches!(
&elements[0],
ParsedElement::RoomName(name) if name == "Town Square"
));
}
#[test]
fn test_parse_vitals() {
let mut parser = Parser::new();
let input = r#"<progressBar id="health" value="75"/>"#;
let elements = parser.parse(input);
assert_eq!(elements.len(), 1);
if let ParsedElement::Vitals(data) = &elements[0] {
assert_eq!(data.health, Some(75));
} else {
panic!("Expected Vitals element");
}
}
#[test]
fn test_parse_malformed_input() {
let mut parser = Parser::new();
let input = "<unclosed tag without closing";
// Should not panic, may return empty or partial results
let elements = parser.parse(input);
// Parser should handle gracefully
}
#[test]
fn test_parse_empty_input() {
let mut parser = Parser::new();
let elements = parser.parse("");
assert!(elements.is_empty());
}
}
}
Configuration Tests
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = Config::default();
assert_eq!(config.connection.port, 8000);
assert!(config.connection.auto_reconnect);
}
#[test]
fn test_load_valid_config() {
let toml = r#"
[connection]
host = "127.0.0.1"
port = 8001
[[widgets]]
type = "text"
name = "main"
x = 0
y = 0
width = 100
height = 100
"#;
let config: Config = toml::from_str(toml).unwrap();
assert_eq!(config.connection.port, 8001);
assert_eq!(config.widgets.len(), 1);
}
#[test]
fn test_invalid_config() {
let toml = "invalid toml [[[";
let result: Result<Config, _> = toml::from_str(toml);
assert!(result.is_err());
}
#[test]
fn test_config_validation() {
let mut config = Config::default();
config.widgets.push(WidgetConfig {
width: 0, // Invalid
height: 0, // Invalid
..Default::default()
});
let result = config.validate();
assert!(result.is_err());
}
}
}
Widget Tests
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_widget_creation() {
let config = TextWidgetConfig {
name: "test".into(),
width: 80,
height: 24,
..Default::default()
};
let widget = TextWidget::new(config);
assert_eq!(widget.name(), "test");
assert!(widget.can_focus());
}
#[test]
fn test_widget_sync() {
let mut widget = TextWidget::new(Default::default());
let mut state = AppState::new();
// Initial sync
state.set_generation(1);
assert!(widget.sync(&state));
assert_eq!(widget.last_generation(), 1);
// No change
assert!(!widget.sync(&state));
// After state change
state.add_text("Hello");
state.set_generation(2);
assert!(widget.sync(&state));
assert_eq!(widget.last_generation(), 2);
}
#[test]
fn test_scrollback_limit() {
let config = TextWidgetConfig {
scrollback: 100,
..Default::default()
};
let mut widget = TextWidget::new(config);
// Add more lines than scrollback limit
for i in 0..200 {
widget.add_line(format!("Line {}", i));
}
assert!(widget.line_count() <= 100);
}
}
}
Integration Tests
Two-Face has 91 integration tests across two test files using 45 real game XML fixtures.
Parser Integration (parser_integration.rs - 34 tests)
Tests XML parsing at the parser level:
| Category | Tests | Description |
|---|---|---|
| Session Start | 7 | Mode, player ID, stream windows |
| Vitals & Indicators | 5 | Health, mana, hands, status |
| Room Navigation | 6 | Compass, exits, room components |
| Combat | 5 | Roundtime, casttime, combat flow |
| Edge Cases | 6 | Empty input, malformed XML, unicode |
| Performance | 2 | Parsing speed benchmarks |
| Parser State | 3 | Stream tracking, state reusability |
UI Integration (ui_integration.rs - 57 tests)
End-to-end tests: XML → Parser → MessageProcessor → UiState:
| Category | Tests | Description |
|---|---|---|
| Progress Bars | 12 | Vitals, stance, mindstate, custom IDs |
| Countdowns | 4 | Roundtime, casttime timers |
| Active Effects | 8 | Buffs, debuffs, cooldowns, spells |
| Indicators | 6 | Status indicators, dashboard icons |
| Room & Navigation | 5 | Room components, compass, subtitle |
| Text Routing | 8 | Stream routing, tabbed windows |
| Hands & Inventory | 6 | Left/right hands, spell hand, inventory |
| Entity Counts | 4 | Player count, target count |
| Edge Cases | 4 | Clear events, unknown streams |
Test Fixtures (45 files)
Located in tests/fixtures/:
| Category | Fixtures | Content |
|---|---|---|
| Session | session_start.xml, player_counts.xml | Login, player data |
| Vitals | vitals_indicators.xml, progress_*.xml | Health, mana, custom bars |
| Combat | combat_*.xml, roundtime_*.xml | Combat, RT/CT, targets |
| Effects | active_effects*.xml, buffs_*.xml | Buffs, debuffs, cooldowns |
| Indicators | indicators_*.xml, icon_*.xml | Status indicators |
| Room | room_*.xml, text_routing*.xml | Room data, stream routing |
| Injuries | injuries*.xml | Body part injuries |
| Hands | *_hand*.xml | Equipment in hands |
| Misc | spells_stream.xml, tabbed_*.xml | Spells, tabbed windows |
Example Integration Test
#![allow(unused)]
fn main() {
use two_face::parser::{ParsedElement, XmlParser};
/// Helper to parse XML and collect all elements
fn parse_xml(xml: &str) -> Vec<ParsedElement> {
let mut parser = XmlParser::new();
let mut all_elements = Vec::new();
for line in xml.lines() {
all_elements.extend(parser.parse_line(line));
}
all_elements
}
#[test]
fn test_extracts_compass_directions() {
let xml = include_str!("fixtures/room_navigation.xml");
let elements = parse_xml(xml);
let compass = elements.iter().find(|e| {
matches!(e, ParsedElement::Compass { .. })
});
assert!(compass.is_some(), "Should find compass element");
if let Some(ParsedElement::Compass { directions }) = compass {
assert!(directions.contains(&"n".to_string()));
assert!(directions.contains(&"s".to_string()));
}
}
}
Creating New Fixtures
Capture real game output for testing:
# Game logs are stored by Lich at:
# ~/.lich5/logs/GSIV-CharName/YYYY/MM/
# Copy relevant XML sections to test fixtures:
cp ~/.lich5/logs/GSIV-Nisugi/2025/10/session.xml tests/fixtures/
Using Fixtures
#![allow(unused)]
fn main() {
const COMBAT_FIXTURE: &str = include_str!("fixtures/combat_roundtime.xml");
#[test]
fn test_combat_parsing() {
let elements = parse_xml(COMBAT_FIXTURE);
// Find roundtime elements
let roundtime = elements.iter().find(|e| {
matches!(e, ParsedElement::RoundTime { .. })
});
assert!(roundtime.is_some());
}
}
Running Tests
All Tests
cargo test
Specific Test
cargo test test_parse_room_name
Specific Module
cargo test parser::tests
With Output
cargo test -- --nocapture
Verbose
cargo test -- --show-output
Test Patterns
Table-Driven Tests
#![allow(unused)]
fn main() {
#[test]
fn test_parse_indicators() {
let cases = vec![
("<indicator id='IconHIDDEN' visible='y'/>", "hidden", true),
("<indicator id='IconHIDDEN' visible='n'/>", "hidden", false),
("<indicator id='IconSTUNNED' visible='y'/>", "stunned", true),
("<indicator id='IconPRONE' visible='y'/>", "prone", true),
];
for (input, expected_name, expected_visible) in cases {
let mut parser = Parser::new();
let elements = parser.parse(input);
if let ParsedElement::Indicator { name, visible } = &elements[0] {
assert_eq!(name, expected_name, "Failed for input: {}", input);
assert_eq!(*visible, expected_visible, "Failed for input: {}", input);
} else {
panic!("Expected Indicator for input: {}", input);
}
}
}
}
Property-Based Testing
#![allow(unused)]
fn main() {
use proptest::prelude::*;
proptest! {
#[test]
fn test_scrollback_never_exceeds_limit(lines in 1..1000usize, limit in 1..500usize) {
let mut widget = TextWidget::new(TextWidgetConfig {
scrollback: limit,
..Default::default()
});
for i in 0..lines {
widget.add_line(format!("Line {}", i));
}
prop_assert!(widget.line_count() <= limit);
}
}
}
Snapshot Testing
#![allow(unused)]
fn main() {
#[test]
fn test_render_snapshot() {
let widget = ProgressWidget::new(Default::default());
let mut state = AppState::new();
state.vitals_mut().health = 75;
widget.sync(&state);
// Render to string buffer
let rendered = widget.render_to_string();
// Compare with stored snapshot
insta::assert_snapshot!(rendered);
}
}
Mocking
Mock Network
#![allow(unused)]
fn main() {
struct MockConnection {
responses: Vec<String>,
index: usize,
}
impl MockConnection {
fn new(responses: Vec<String>) -> Self {
Self { responses, index: 0 }
}
}
impl Connection for MockConnection {
fn receive(&mut self) -> Result<String> {
if self.index < self.responses.len() {
let response = self.responses[self.index].clone();
self.index += 1;
Ok(response)
} else {
Err(Error::EndOfStream)
}
}
}
#[test]
fn test_with_mock_connection() {
let mock = MockConnection::new(vec![
"<roomName>Test Room</roomName>".into(),
]);
let mut app = App::with_connection(mock);
app.tick();
assert_eq!(app.state().room_name(), Some("Test Room"));
}
}
Continuous Integration
GitHub Actions
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: dtolnay/rust-action@stable
- run: cargo test --all-features
Coverage
# Install tarpaulin
cargo install cargo-tarpaulin
# Run coverage
cargo tarpaulin --out Html
See Also
- Building - Build process
- Contributing - Contribution guide
- Project Structure - Code organization
Contributing Guide
How to contribute to Two-Face development.
Welcome!
Thank you for considering contributing to Two-Face! This document provides guidelines and information for contributors.
Ways to Contribute
Code Contributions
- Bug fixes
- New features
- Performance improvements
- Widget implementations
- Parser extensions
Non-Code Contributions
- Documentation improvements
- Bug reports
- Feature suggestions
- User support
- Testing and feedback
Getting Started
1. Fork and Clone
# Fork on GitHub, then clone your fork
git clone https://github.com/YOUR_USERNAME/two-face.git
cd two-face
2. Set Up Development Environment
# Install Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Install dependencies (varies by platform)
# See building.md for platform-specific instructions
# Build
cargo build
# Run tests
cargo test
3. Create a Branch
git checkout -b feature/my-feature
# or
git checkout -b fix/bug-description
Development Workflow
Making Changes
- Write code following the style guide
- Add tests for new functionality
- Update documentation if needed
- Run checks before committing
Pre-Commit Checks
# Format code
cargo fmt
# Run linter
cargo clippy -- -D warnings
# Run tests
cargo test
# Check documentation
cargo doc --no-deps
Commit Messages
Follow conventional commit format:
type(scope): description
[optional body]
[optional footer]
Types:
feat: New featurefix: Bug fixdocs: Documentationrefactor: Code restructuringtest: Adding testschore: Maintenance
Examples:
feat(parser): add spell duration parsing
fix(widget): correct scrollback overflow
docs: update installation instructions
refactor(tui): extract render helpers
Pull Request Process
Before Submitting
- Code follows style guide
- All tests pass
- No clippy warnings
- Documentation updated
- Commit messages are clear
PR Description Template
## Description
Brief description of changes
## Type of Change
- [ ] Bug fix
- [ ] New feature
- [ ] Breaking change
- [ ] Documentation update
## Testing
How was this tested?
## Checklist
- [ ] Tests added
- [ ] Documentation updated
- [ ] No breaking changes (or documented)
Review Process
- Submit PR against
mainbranch - CI checks run automatically
- Maintainers review code
- Address feedback
- Merge when approved
Code Style
Rust Style
Follow standard Rust conventions:
#![allow(unused)]
fn main() {
// Use rustfmt defaults
cargo fmt
// Naming
fn function_name() {} // snake_case for functions
struct StructName {} // PascalCase for types
const CONSTANT: u32 = 1; // SCREAMING_SNAKE_CASE for constants
let variable_name = 1; // snake_case for variables
}
Documentation
#![allow(unused)]
fn main() {
/// Brief description of the function.
///
/// More detailed explanation if needed.
///
/// # Arguments
///
/// * `param` - Description of parameter
///
/// # Returns
///
/// Description of return value
///
/// # Examples
///
/// ```
/// let result = my_function("input");
/// assert_eq!(result, expected);
/// ```
pub fn my_function(param: &str) -> Result<Output> {
// ...
}
}
Error Handling
#![allow(unused)]
fn main() {
// Use Result for recoverable errors
fn parse_config(path: &Path) -> Result<Config, ConfigError> {
let content = fs::read_to_string(path)?;
let config: Config = toml::from_str(&content)?;
Ok(config)
}
// Use Option for optional values
fn find_widget(&self, name: &str) -> Option<&Widget> {
self.widgets.iter().find(|w| w.name() == name)
}
}
Testing
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_descriptive_name() {
// Arrange
let input = setup_test_data();
// Act
let result = function_under_test(input);
// Assert
assert_eq!(result, expected);
}
}
}
Issue Guidelines
Bug Reports
Include:
- Two-Face version
- Operating system
- Steps to reproduce
- Expected behavior
- Actual behavior
- Relevant logs
Template:
**Version**: 0.1.0
**OS**: Windows 10 / macOS 14 / Ubuntu 22.04
**Steps to Reproduce**:
1. Do this
2. Then this
3. See error
**Expected**: What should happen
**Actual**: What actually happens
**Logs**:
relevant log output
Feature Requests
Include:
- Use case description
- Proposed solution
- Alternatives considered
Template:
**Use Case**:
Why do you need this feature?
**Proposed Solution**:
How should it work?
**Alternatives**:
What other approaches did you consider?
Community Guidelines
Code of Conduct
- Be respectful and inclusive
- Focus on constructive feedback
- Help others learn
- Credit others’ work
- Assume good intentions
Communication
- GitHub Issues for bugs and features
- GitHub Discussions for questions
- Pull Request comments for code review
Getting Help
- Check existing documentation
- Search closed issues
- Ask in GitHub Discussions
- Be specific in your questions
Release Process
Version Numbering
Follow Semantic Versioning (SemVer):
- MAJOR: Breaking changes
- MINOR: New features (backwards compatible)
- PATCH: Bug fixes
Changelog
Update CHANGELOG.md with:
- Version number and date
- Breaking changes (if any)
- New features
- Bug fixes
- Known issues
Recognition
Contributors are recognized in:
CONTRIBUTORS.md- Release notes
- Project README
Thank you for contributing to Two-Face!
See Also
- Building - Build instructions
- Project Structure - Code organization
- Testing - Test guidelines
Reference
Comprehensive reference documentation for Two-Face.
Quick Access
| Reference | Description |
|---|---|
| CLI Options | Command line flags |
| Config Schema | Complete configuration reference |
| Keybind Actions | All keybind actions A-Z |
| Parsed Elements | All ParsedElement variants |
| Stream IDs | All stream identifiers |
| Preset Colors | Named color presets |
| Environment Variables | Environment configuration |
| Default Files | Default configuration contents |
How to Use This Section
This reference section is designed for lookup, not learning. Use it when you:
- Need exact syntax for a configuration option
- Want to know all available values for a setting
- Need to look up a specific color name or stream ID
- Are troubleshooting a configuration issue
For learning and tutorials, see:
- Getting Started - Initial setup
- Tutorials - Step-by-step guides
- Configuration - Configuration concepts
Quick Reference Cards
Common CLI Flags
two-face --host HOST --port PORT # Lich mode
two-face --direct --account X ... # Direct mode
two-face --config PATH # Custom config
two-face --debug # Debug logging
Essential Config Keys
[connection]
mode = "lich" # or "direct"
host = "127.0.0.1"
port = 8000
[[widgets]]
type = "text" # Widget type
name = "main" # Unique name
x = 0 # Position (0-100)
y = 0
width = 100 # Size (0-100)
height = 100
Common Keybind Actions
[keybinds."key"]
action = "scroll_up" # Widget action
macro = "command" # Send command
Stream Quick Reference
| Stream | Content |
|---|---|
| main | Primary game output |
| room | Room descriptions |
| combat | Combat messages |
| speech | Player speech |
| thoughts | ESP/thoughts |
| whisper | Whispers |
Conventions
Value Types
| Type | Example | Description |
|---|---|---|
| string | "value" | Text in quotes |
| integer | 100 | Whole number |
| float | 1.5 | Decimal number |
| boolean | true | true or false |
| array | ["a", "b"] | List of values |
Color Values
Colors can be specified as:
- Preset name:
"red","bright_blue" - Hex code:
"#ff0000" - RGB: (in some contexts)
Path Values
Paths support:
- Absolute:
/home/user/.two-face/ - Home expansion:
~/.two-face/ - Relative (from config location)
See Also
CLI Options
Complete command line interface reference for Two-Face.
Usage
two-face [OPTIONS]
Connection Options
Lich Proxy Mode
| Option | Type | Default | Description |
|---|---|---|---|
--host | string | 127.0.0.1 | Lich proxy hostname |
--port | integer | 8000 | Lich proxy port |
two-face --host 127.0.0.1 --port 8000
Direct eAccess Mode
| Option | Type | Required | Description |
|---|---|---|---|
--direct | flag | Yes | Enable direct mode |
--account | string | Yes | Simutronics account |
--password | string | Yes | Account password |
--game | string | Yes | Game instance |
--character | string | Yes | Character name |
two-face --direct \
--account myaccount \
--password mypassword \
--game prime \
--character Mycharacter
Game Instance Values
| Value | Game |
|---|---|
prime | GemStone IV (Prime) |
test | GemStone IV (Test) |
plat | GemStone IV (Platinum) |
dr | DragonRealms (Prime) |
drt | DragonRealms (Test) |
Configuration Options
| Option | Type | Default | Description |
|---|---|---|---|
--config | path | ~/.two-face/config.toml | Main config file |
--layout | path | ~/.two-face/layout.toml | Layout config file |
--colors | path | ~/.two-face/colors.toml | Color theme file |
--keybinds | path | ~/.two-face/keybinds.toml | Keybinds file |
--profile | string | none | Load named profile |
# Custom config location
two-face --config /path/to/config.toml
# Load profile
two-face --profile warrior
Logging Options
| Option | Type | Default | Description |
|---|---|---|---|
--debug | flag | false | Enable debug logging |
--log-file | path | ~/.two-face/two-face.log | Log file location |
--log-level | string | info | Log verbosity |
Log Levels
| Level | Description |
|---|---|
error | Errors only |
warn | Warnings and errors |
info | General information (default) |
debug | Debug information |
trace | Verbose tracing |
# Debug logging
two-face --debug
# Custom log file
two-face --log-file /tmp/debug.log --log-level trace
Display Options
| Option | Type | Default | Description |
|---|---|---|---|
--no-color | flag | false | Disable colors |
--force-color | flag | false | Force color output |
# Disable colors (for logging/piping)
two-face --no-color
Miscellaneous Options
| Option | Type | Description |
|---|---|---|
--version, -V | flag | Print version |
--help, -h | flag | Print help |
--dump-config | flag | Print default config |
# Show version
two-face --version
# Show help
two-face --help
# Print default configuration
two-face --dump-config > ~/.two-face/config.toml
Option Precedence
Options are applied in this order (later overrides earlier):
- Default values
- Configuration files
- Environment variables
- Command line arguments
Examples
Basic Lich Connection
two-face --host 127.0.0.1 --port 8000
Direct Connection with Debug
two-face --direct \
--account myaccount \
--password mypassword \
--game prime \
--character Warrior \
--debug
Custom Configuration
two-face \
--config ~/games/two-face/config.toml \
--layout ~/games/two-face/hunting.toml
Profile-Based Launch
# Assumes ~/.two-face/profiles/wizard.toml exists
two-face --profile wizard
Scripted Launch (Lich)
#!/bin/bash
# launch_gs4.sh
# Start Lich in background
lich &
sleep 5
# Connect Two-Face
two-face --host 127.0.0.1 --port 8000
Environment-Based (Direct)
#!/bin/bash
# Credentials from environment
export TF_ACCOUNT="myaccount"
export TF_PASSWORD="mypassword"
export TF_GAME="prime"
two-face --direct --character "$1"
Short Options
| Short | Long | Description |
|---|---|---|
-h | --host | Host (Lich mode) |
-p | --port | Port (Lich mode) |
-c | --config | Config file |
-d | --debug | Debug mode |
-V | --version | Version |
# Short form
two-face -h 127.0.0.1 -p 8000 -d
Exit Codes
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | General error |
| 2 | Configuration error |
| 3 | Connection error |
| 4 | Authentication error |
See Also
Config Schema
Complete configuration file reference for Two-Face.
File Overview
| File | Purpose |
|---|---|
config.toml | Main configuration |
layout.toml | Widget layout |
colors.toml | Color theme |
keybinds.toml | Key bindings |
highlights.toml | Text patterns |
triggers.toml | Automation triggers |
config.toml
Connection Section
[connection]
mode = "lich" # "lich" or "direct"
host = "127.0.0.1" # Lich host (lich mode)
port = 8000 # Lich port (lich mode)
auto_reconnect = true # Auto-reconnect on disconnect
reconnect_delay = 5 # Seconds between reconnect attempts
game = "prime" # Game instance (direct mode)
| Key | Type | Default | Description |
|---|---|---|---|
mode | string | "lich" | Connection mode |
host | string | "127.0.0.1" | Proxy host |
port | integer | 8000 | Proxy port |
auto_reconnect | boolean | true | Enable auto-reconnect |
reconnect_delay | integer | 5 | Reconnect delay (seconds) |
game | string | "prime" | Game instance (direct) |
TTS Section
[tts]
enabled = false
engine = "default" # "sapi", "say", "espeak"
voice = "default"
rate = 1.0 # 0.5 to 2.0
volume = 1.0 # 0.0 to 1.0
speak_room_descriptions = true
speak_combat = false
speak_speech = true
speak_whispers = true
speak_thoughts = false
| Key | Type | Default | Description |
|---|---|---|---|
enabled | boolean | false | Enable TTS |
engine | string | "default" | TTS engine |
voice | string | "default" | Voice name |
rate | float | 1.0 | Speech rate |
volume | float | 1.0 | Volume level |
speak_* | boolean | varies | Speak stream types |
Logging Section
[logging]
level = "info" # "error", "warn", "info", "debug", "trace"
file = "~/.two-face/two-face.log"
max_size = 10485760 # 10MB
rotate = true
Performance Section
[performance]
render_rate = 60 # Target FPS
batch_updates = true
lazy_render = true
max_scrollback = 10000
layout.toml
Widget Definition
[[widgets]]
type = "text" # Widget type (required)
name = "main" # Unique name (required)
x = 0 # X position (0-100)
y = 0 # Y position (0-100)
width = 100 # Width (0-100)
height = 100 # Height (0-100)
# Optional common properties
title = "Main" # Widget title
border = true # Show border
border_color = "white" # Border color
focus_order = 1 # Tab order
visible = true # Initial visibility
Widget Types
text
[[widgets]]
type = "text"
streams = ["main", "room"]
scrollback = 5000
auto_scroll = true
show_timestamps = false
wrap = true
tabbed_text
[[widgets]]
type = "tabbed_text"
tabs = [
{ name = "All", streams = ["main", "room", "combat"] },
{ name = "Combat", streams = ["combat"] },
{ name = "Chat", streams = ["speech", "thoughts"] },
]
progress
[[widgets]]
type = "progress"
data_source = "vitals.health"
color = "health"
show_text = true
show_percentage = true
countdown
[[widgets]]
type = "countdown"
data_source = "roundtime"
warning_threshold = 2
critical_threshold = 0
compass
[[widgets]]
type = "compass"
style = "unicode" # "ascii", "unicode", "minimal"
clickable = true
indicator
[[widgets]]
type = "indicator"
indicators = ["hidden", "stunned", "prone"]
columns = 2
room
[[widgets]]
type = "room"
show_exits = true
show_creatures = true
show_players = false
command_input
[[widgets]]
type = "command_input"
prompt = "> "
history_size = 500
show_roundtime = true
colors.toml
Theme Definition
[theme]
name = "Custom Theme"
# Base colors
background = "#000000"
text = "#ffffff"
text_dim = "#808080"
# Borders
border = "#404040"
border_focused = "#ffffff"
# Vitals
health = "#00ff00"
health_low = "#ffff00"
health_critical = "#ff0000"
mana = "#0080ff"
stamina = "#ff8000"
spirit = "#ff00ff"
# Streams
main = "#ffffff"
combat = "#ff4444"
speech = "#00ffff"
thoughts = "#00ff00"
whisper = "#ff00ff"
room = "#ffff00"
# Indicators
hidden = "#00ff00"
stunned = "#ffff00"
prone = "#00ffff"
Color Values
Colors accept:
- Preset names:
"red","bright_blue" - Hex codes:
"#ff0000","#f00"
keybinds.toml
Keybind Definition
[keybinds."key_name"]
action = "action_name" # Widget action
# OR
[keybinds."key_name"]
macro = "game command" # Send command
Key Names
| Category | Examples |
|---|---|
| Letters | "a", "z", "A" (shift+a) |
| Numbers | "1", "0" |
| Function | "f1", "f12" |
| Navigation | "up", "down", "left", "right" |
| Editing | "enter", "tab", "backspace", "delete" |
| Special | "escape", "space", "home", "end" |
| Numpad | "numpad0", "numpad_plus" |
Modifiers
Combine with +:
"ctrl+a""shift+f1""alt+enter""ctrl+shift+s"
highlights.toml
Highlight Definition
[[highlights]]
pattern = "regex pattern"
fg = "color" # Foreground color
bg = "color" # Background color (optional)
bold = false
italic = false
underline = false
# Optional
enabled = true
priority = 100
fast_parse = false # Use literal matching
triggers.toml
Trigger Definition
[[triggers]]
name = "trigger_name"
pattern = "regex pattern"
command = "action"
# Optional
enabled = true
priority = 100
cooldown = 1000 # Milliseconds
category = "combat"
stream = "main" # Limit to stream
Type Reference
| Type | Format | Example |
|---|---|---|
| string | Quoted text | "value" |
| integer | Whole number | 100 |
| float | Decimal number | 1.5 |
| boolean | true/false | true |
| array | Brackets | ["a", "b"] |
| table | Section | [section] |
See Also
Keybind Actions
Complete reference of all available keybind actions.
Action Syntax
[keybinds."key"]
action = "action_name"
Navigation Actions
Widget Focus
| Action | Description |
|---|---|
next_widget | Focus next widget in tab order |
prev_widget | Focus previous widget |
focus_input | Focus command input |
focus_widget | Focus specific widget (requires widget param) |
[keybinds."tab"]
action = "next_widget"
[keybinds."shift+tab"]
action = "prev_widget"
[keybinds."escape"]
action = "focus_input"
[keybinds."alt+m"]
action = "focus_widget"
widget = "main"
Scrolling
| Action | Description |
|---|---|
scroll_up | Scroll up one page |
scroll_down | Scroll down one page |
scroll_half_up | Scroll up half page |
scroll_half_down | Scroll down half page |
scroll_line_up | Scroll up one line |
scroll_line_down | Scroll down one line |
scroll_top | Scroll to top |
scroll_bottom | Scroll to bottom |
[keybinds."page_up"]
action = "scroll_up"
[keybinds."page_down"]
action = "scroll_down"
[keybinds."home"]
action = "scroll_top"
[keybinds."end"]
action = "scroll_bottom"
Input Actions
Command Input
| Action | Description |
|---|---|
submit_input | Submit current input |
clear_input | Clear input line |
history_prev | Previous command in history |
history_next | Next command in history |
history_search | Search command history |
[keybinds."enter"]
action = "submit_input"
[keybinds."ctrl+u"]
action = "clear_input"
[keybinds."up"]
action = "history_prev"
Text Editing
| Action | Description |
|---|---|
cursor_left | Move cursor left |
cursor_right | Move cursor right |
cursor_home | Move to line start |
cursor_end | Move to line end |
delete_char | Delete character at cursor |
delete_word | Delete word at cursor |
backspace | Delete character before cursor |
Browser Actions
Open Browsers
| Action | Description |
|---|---|
open_search | Open text search |
open_help | Open help browser |
open_layout_editor | Open layout editor |
open_highlight_editor | Open highlight editor |
open_keybind_editor | Open keybind editor |
[keybinds."ctrl+f"]
action = "open_search"
[keybinds."f1"]
action = "open_help"
Search Actions
| Action | Description |
|---|---|
search_next | Find next match |
search_prev | Find previous match |
close_search | Close search |
Widget Actions
Tabbed Text
| Action | Description |
|---|---|
next_tab | Switch to next tab |
prev_tab | Switch to previous tab |
tab_1 through tab_9 | Switch to numbered tab |
close_tab | Close current tab |
[keybinds."ctrl+tab"]
action = "next_tab"
[keybinds."ctrl+shift+tab"]
action = "prev_tab"
[keybinds."ctrl+1"]
action = "tab_1"
Compass
| Action | Description |
|---|---|
compass_north | Go north |
compass_south | Go south |
compass_east | Go east |
compass_west | Go west |
compass_northeast | Go northeast |
compass_northwest | Go northwest |
compass_southeast | Go southeast |
compass_southwest | Go southwest |
compass_out | Go out |
compass_up | Go up |
compass_down | Go down |
Application Actions
General
| Action | Description |
|---|---|
quit | Exit application |
reload_config | Reload all configuration |
toggle_debug | Toggle debug overlay |
[keybinds."ctrl+q"]
action = "quit"
[keybinds."ctrl+r"]
action = "reload_config"
Layout
| Action | Description |
|---|---|
toggle_widget | Toggle widget visibility |
maximize_widget | Maximize focused widget |
restore_layout | Restore default layout |
[keybinds."ctrl+h"]
action = "toggle_widget"
widget = "health"
TTS Actions
| Action | Description |
|---|---|
toggle_tts | Enable/disable TTS |
speak_status | Speak current status |
speak_room | Speak room description |
speak_last | Repeat last spoken text |
stop_speaking | Stop current speech |
tts_rate_up | Increase speech rate |
tts_rate_down | Decrease speech rate |
tts_volume_up | Increase volume |
tts_volume_down | Decrease volume |
[keybinds."ctrl+space"]
action = "toggle_tts"
[keybinds."f1"]
action = "speak_status"
Macro Actions
Send game commands:
[keybinds."f1"]
macro = "attack target"
[keybinds."ctrl+1"]
macro = "prep 101;cast"
# With delay
[keybinds."f5"]
macro = "prep 901;{2000};cast"
# With input prompt
[keybinds."ctrl+g"]
macro = "go $input"
Macro Variables
| Variable | Description |
|---|---|
$input | Prompt for input |
$target | Current target |
$lasttarget | Last targeted creature |
{N} | Delay N milliseconds |
Action Parameters
Some actions require additional parameters:
[keybinds."alt+m"]
action = "focus_widget"
widget = "main" # Required: widget name
[keybinds."ctrl+h"]
action = "toggle_widget"
widget = "health" # Required: widget name
Complete Example
# keybinds.toml
# Navigation
[keybinds."tab"]
action = "next_widget"
[keybinds."shift+tab"]
action = "prev_widget"
[keybinds."escape"]
action = "focus_input"
# Scrolling
[keybinds."page_up"]
action = "scroll_up"
[keybinds."page_down"]
action = "scroll_down"
# Movement macros
[keybinds."numpad8"]
macro = "north"
[keybinds."numpad2"]
macro = "south"
# Combat macros
[keybinds."f1"]
macro = "attack target"
[keybinds."f2"]
macro = "stance defensive"
# Application
[keybinds."ctrl+q"]
action = "quit"
[keybinds."ctrl+f"]
action = "open_search"
See Also
Parsed Elements
Reference of all ParsedElement variants from the game protocol parser.
Overview
ParsedElements are the structured output of the XML parser. Each variant represents a specific type of game data.
Text Elements
Text
Plain text content.
#![allow(unused)]
fn main() {
ParsedElement::Text(String)
}
Source: Untagged text between XML elements.
StyledText
Text with formatting.
#![allow(unused)]
fn main() {
ParsedElement::StyledText {
text: String,
bold: bool,
preset: Option<String>,
}
}
Source: <pushBold/>, <popBold/>, <preset id="..."/>
Room Elements
RoomName
Current room name.
#![allow(unused)]
fn main() {
ParsedElement::RoomName(String)
}
Source: <roomName>...</roomName>
RoomDesc
Room description text.
#![allow(unused)]
fn main() {
ParsedElement::RoomDesc(String)
}
Source: <roomDesc>...</roomDesc>
RoomObjects
Objects in the room.
#![allow(unused)]
fn main() {
ParsedElement::RoomObjects(String)
}
Source: <roomObjs>...</roomObjs>
RoomPlayers
Players in the room.
#![allow(unused)]
fn main() {
ParsedElement::RoomPlayers(String)
}
Source: <roomPlayers>...</roomPlayers>
RoomExits
Available exits.
#![allow(unused)]
fn main() {
ParsedElement::RoomExits(String)
}
Source: <roomExits>...</roomExits>
Vitals Elements
Vitals
Character vital statistics.
#![allow(unused)]
fn main() {
ParsedElement::Vitals {
health: Option<u8>,
mana: Option<u8>,
stamina: Option<u8>,
spirit: Option<u8>,
}
}
Source: <progressBar id="..." value="..."/>
Progress bar IDs:
health,manapoints,stamina,spiritencumlevel,mindState,nextLvlPB
Timing Elements
Roundtime
Action roundtime.
#![allow(unused)]
fn main() {
ParsedElement::Roundtime(u32)
}
Source: <roundTime value="..."/> or Roundtime: N sec
CastTime
Spell casting time.
#![allow(unused)]
fn main() {
ParsedElement::CastTime(u32)
}
Source: <castTime value="..."/>
Status Elements
Indicator
Status indicator change.
#![allow(unused)]
fn main() {
ParsedElement::Indicator {
id: String,
visible: bool,
}
}
Source: <indicator id="..." visible="y|n"/>
Common indicator IDs:
IconHIDDEN,IconSTUNNED,IconWEBBEDIconPRONE,IconKNEELING,IconSITTINGIconBLEEDING,IconPOISONED,IconDISEASEDIconINVISIBLE,IconDEAD
Stance
Combat stance.
#![allow(unused)]
fn main() {
ParsedElement::Stance(String)
}
Source: <stance id="..."/>
Values: offensive, forward, neutral, guarded, defensive
Navigation Elements
Compass
Available directions.
#![allow(unused)]
fn main() {
ParsedElement::Compass {
directions: Vec<String>,
}
}
Source: <compass><dir value="..."/>...</compass>
Direction values:
n,s,e,w(cardinal)ne,nw,se,sw(diagonal)up,down,out
Hand Elements
LeftHand
Item in left hand.
#![allow(unused)]
fn main() {
ParsedElement::LeftHand {
noun: String,
name: String,
exist: Option<String>,
}
}
Source: <left exist="..." noun="...">...</left>
RightHand
Item in right hand.
#![allow(unused)]
fn main() {
ParsedElement::RightHand {
noun: String,
name: String,
exist: Option<String>,
}
}
Source: <right exist="..." noun="...">...</right>
Spell
Prepared spell.
#![allow(unused)]
fn main() {
ParsedElement::Spell(String)
}
Source: <spell>...</spell>
Stream Elements
PushStream
Start of named stream.
#![allow(unused)]
fn main() {
ParsedElement::PushStream(String)
}
Source: <pushStream id="..."/>
PopStream
End of current stream.
#![allow(unused)]
fn main() {
ParsedElement::PopStream
}
Source: <popStream/>
StreamContent
Content within a stream.
#![allow(unused)]
fn main() {
ParsedElement::StreamContent {
stream: String,
content: String,
}
}
Prompt Elements
Prompt
Game prompt.
#![allow(unused)]
fn main() {
ParsedElement::Prompt {
text: String,
time: Option<String>,
}
}
Source: <prompt time="...">...</prompt>
Link Elements
Link
Clickable object link.
#![allow(unused)]
fn main() {
ParsedElement::Link {
exist: String,
noun: String,
text: String,
}
}
Source: <a exist="..." noun="...">...</a>
CommandLink
Clickable command link.
#![allow(unused)]
fn main() {
ParsedElement::CommandLink {
cmd: String,
text: String,
}
}
Source: <d cmd="...">...</d>
Combat Elements
Combat
Combat message.
#![allow(unused)]
fn main() {
ParsedElement::Combat {
text: String,
}
}
Source: Text within combat stream.
Damage
Damage dealt or received.
#![allow(unused)]
fn main() {
ParsedElement::Damage {
amount: u32,
target: Option<String>,
}
}
Container Elements
Container
Container contents.
#![allow(unused)]
fn main() {
ParsedElement::Container {
id: String,
name: String,
contents: Vec<ContainerItem>,
}
}
ContainerItem
Item within container.
#![allow(unused)]
fn main() {
ContainerItem {
exist: String,
noun: String,
name: String,
}
}
Spell Elements
ActiveSpell
Currently active spell.
#![allow(unused)]
fn main() {
ParsedElement::ActiveSpell {
name: String,
duration: Option<u32>,
}
}
Source: <spell>...</spell> with duration
SpellExpire
Spell expired notification.
#![allow(unused)]
fn main() {
ParsedElement::SpellExpire {
name: String,
}
}
System Elements
Mode
Game mode change.
#![allow(unused)]
fn main() {
ParsedElement::Mode {
mode: String,
}
}
Source: <mode id="..."/>
Output
Output configuration.
#![allow(unused)]
fn main() {
ParsedElement::Output {
class: String,
}
}
Source: <output class="..."/>
ClearStream
Clear stream content.
#![allow(unused)]
fn main() {
ParsedElement::ClearStream(String)
}
Source: <clearStream id="..."/>
Unknown Element
Unrecognized XML.
#![allow(unused)]
fn main() {
ParsedElement::Unknown {
tag: String,
content: String,
}
}
Preserves unhandled elements for debugging.
Element Processing
Elements are processed in order:
#![allow(unused)]
fn main() {
for element in parser.parse(input) {
match element {
ParsedElement::RoomName(name) => {
state.room.name = name;
}
ParsedElement::Vitals { health, .. } => {
if let Some(h) = health {
state.vitals.health = h;
}
}
// ...
}
}
}
See Also
Stream IDs
Reference of all game stream identifiers.
Overview
Streams categorize game output. Each text window can subscribe to specific streams to filter content.
Core Streams
| Stream ID | Description | Content Type |
|---|---|---|
main | Primary game output | General text, actions, results |
room | Room descriptions | Room name, description, exits, objects |
combat | Combat messages | Attacks, damage, death |
speech | Spoken communication | Say, ask, exclaim |
whisper | Private messages | Whispers |
thoughts | ESP/mental communication | Think, group chat |
Communication Streams
| Stream ID | Description |
|---|---|
speech | Player speech (say, ask) |
whisper | Private whispers |
thoughts | ESP channel (think) |
shout | Shouted messages |
sing | Sung messages |
Game State Streams
| Stream ID | Description |
|---|---|
room | Room information |
inv | Inventory updates |
percWindow | Perception window |
familiar | Familiar view |
bounty | Bounty task info |
Combat Streams
| Stream ID | Description |
|---|---|
combat | Combat actions and results |
assess | Creature assessments |
death | Death notifications |
Specialty Streams
| Stream ID | Description |
|---|---|
logons | Player login/logout |
atmospherics | Weather and atmosphere |
loot | Loot messages |
group | Group/party messages |
System Streams
| Stream ID | Description |
|---|---|
raw | Unprocessed output |
debug | Debug information |
script | Script output (Lich) |
Using Streams
Widget Configuration
# Main window - everything
[[widgets]]
type = "text"
name = "main"
streams = ["main", "room", "combat"]
# Chat window - communication only
[[widgets]]
type = "text"
name = "chat"
streams = ["speech", "whisper", "thoughts"]
# Combat window - combat only
[[widgets]]
type = "text"
name = "combat"
streams = ["combat"]
Empty Streams (All)
# Empty array = receive all streams
[[widgets]]
type = "text"
streams = []
Tabbed Text
[[widgets]]
type = "tabbed_text"
tabs = [
{ name = "All", streams = [] },
{ name = "Game", streams = ["main", "room"] },
{ name = "Combat", streams = ["combat"] },
{ name = "Chat", streams = ["speech", "thoughts", "whisper"] },
]
Triggers
# Trigger on specific stream
[[triggers]]
pattern = "whispers,"
command = ".notify Whisper!"
stream = "whisper"
# Trigger on combat stream
[[triggers]]
pattern = "falls dead"
command = "search"
stream = "combat"
Stream Colors
Configure stream-specific colors in colors.toml:
[theme]
# Stream colors
main = "#ffffff"
room = "#ffff00"
combat = "#ff4444"
speech = "#00ffff"
whisper = "#ff00ff"
thoughts = "#00ff00"
Stream Priority
When streams overlap, content appears in all matching windows. Configure priority in layout:
[[widgets]]
type = "text"
name = "combat"
streams = ["combat"]
priority = 100 # Higher = checked first
Common Configurations
Single Window (All Content)
[[widgets]]
type = "text"
name = "main"
streams = [] # All streams
Two Windows (Game + Chat)
[[widgets]]
type = "text"
name = "game"
streams = ["main", "room", "combat"]
[[widgets]]
type = "text"
name = "chat"
streams = ["speech", "thoughts", "whisper"]
Three Windows (Specialized)
[[widgets]]
type = "text"
name = "story"
streams = ["main", "room"]
[[widgets]]
type = "text"
name = "combat"
streams = ["combat"]
[[widgets]]
type = "text"
name = "social"
streams = ["speech", "thoughts", "whisper"]
RP Layout
[[widgets]]
type = "text"
name = "story"
streams = ["main", "room"] # Immersive content
[[widgets]]
type = "text"
name = "ic"
streams = ["speech", "whisper"] # In-character
[[widgets]]
type = "text"
name = "ooc"
streams = ["thoughts"] # Out-of-character
Stream Detection
The parser assigns streams based on XML tags:
| XML Source | Stream |
|---|---|
<pushStream id="X"/> | Stream X |
<roomName> | room |
<roomDesc> | room |
| Combat XML | combat |
| Default text | main |
See Also
Preset Colors
Reference of all named color presets.
Basic Colors
| Name | Hex Code | Preview |
|---|---|---|
black | #000000 | ■ Black |
red | #800000 | ■ Red |
green | #008000 | ■ Green |
yellow | #808000 | ■ Yellow |
blue | #000080 | ■ Blue |
magenta | #800080 | ■ Magenta |
cyan | #008080 | ■ Cyan |
white | #c0c0c0 | ■ White |
Bright Colors
| Name | Hex Code | Preview |
|---|---|---|
bright_black / gray | #808080 | ■ Gray |
bright_red | #ff0000 | ■ Bright Red |
bright_green | #00ff00 | ■ Bright Green |
bright_yellow | #ffff00 | ■ Bright Yellow |
bright_blue | #0000ff | ■ Bright Blue |
bright_magenta | #ff00ff | ■ Bright Magenta |
bright_cyan | #00ffff | ■ Bright Cyan |
bright_white | #ffffff | ■ Bright White |
Alternative Names
| Alternative | Maps To |
|---|---|
grey | gray |
dark_gray | bright_black |
light_gray | white |
Semantic Colors
Vitals
| Name | Default | Purpose |
|---|---|---|
health | #00ff00 | Health bar |
health_low | #ffff00 | Low health warning |
health_critical | #ff0000 | Critical health |
mana | #0080ff | Mana bar |
stamina | #ff8000 | Stamina bar |
spirit | #ff00ff | Spirit bar |
Status Indicators
| Name | Default | Purpose |
|---|---|---|
hidden | #00ff00 | Hidden status |
invisible | #00ffff | Invisible status |
stunned | #ffff00 | Stunned status |
webbed | #ff00ff | Webbed status |
prone | #00ffff | Prone status |
kneeling | #ff8000 | Kneeling status |
sitting | #808080 | Sitting status |
dead | #ff0000 | Dead status |
Streams
| Name | Default | Purpose |
|---|---|---|
main | #ffffff | Main text |
room | #ffff00 | Room descriptions |
combat | #ff4444 | Combat messages |
speech | #00ffff | Player speech |
whisper | #ff00ff | Whispers |
thoughts | #00ff00 | ESP/thoughts |
UI Elements
| Name | Default | Purpose |
|---|---|---|
border | #404040 | Widget borders |
border_focused | #ffffff | Focused widget border |
background | #000000 | Background |
text | #ffffff | Default text |
text_dim | #808080 | Dimmed text |
Game Presets
Colors matching game highlight presets:
| Preset ID | Name | Color |
|---|---|---|
speech | Speech | #00ffff |
thought | Thoughts | #00ff00 |
whisper | Whispers | #ff00ff |
bold | Bold text | Inherits + bold |
roomName | Room name | #ffff00 |
roomDesc | Room description | #c0c0c0 |
Using Colors
In colors.toml
[theme]
# Use preset names
health = "bright_green"
mana = "bright_blue"
# Or hex codes
border = "#333333"
text = "#e0e0e0"
In highlights.toml
[[highlights]]
pattern = "something"
fg = "bright_red" # Preset name
bg = "#000080" # Hex code
In widgets
[[widgets]]
type = "progress"
color = "health" # Semantic color
border_color = "cyan" # Basic color
Hex Color Format
| Format | Example | Notes |
|---|---|---|
| 6-digit | #ff0000 | Full RGB |
| 3-digit | #f00 | Shorthand (expands to #ff0000) |
Color Selection Tips
High Contrast
For accessibility, use high-contrast combinations:
# Good contrast
text = "#ffffff"
background = "#000000"
# Also good
text = "#000000"
background = "#ffffff"
Colorblind-Friendly
Avoid relying solely on red/green distinction:
# Instead of red/green
health = "#00ff00" # Green
danger = "#ff0000" # Red
# Use additional cues
health = "#00ffff" # Cyan
danger = "#ff00ff" # Magenta
Thematic Palettes
Nord Theme:
background = "#2e3440"
text = "#eceff4"
red = "#bf616a"
green = "#a3be8c"
blue = "#81a1c1"
Solarized Dark:
background = "#002b36"
text = "#839496"
red = "#dc322f"
green = "#859900"
blue = "#268bd2"
Dracula:
background = "#282a36"
text = "#f8f8f2"
red = "#ff5555"
green = "#50fa7b"
blue = "#8be9fd"
Color Inheritance
Some colors inherit or derive from others:
# Base color
text = "#ffffff"
# Derived (if not specified)
text_dim = text * 0.5 # Dimmer version
See Also
Environment Variables
Reference of all environment variables used by Two-Face.
Connection Variables
Direct eAccess Mode
| Variable | Description | Required |
|---|---|---|
TF_ACCOUNT | Simutronics account name | Yes (if --account not provided) |
TF_PASSWORD | Account password | Yes (if --password not provided) |
TF_GAME | Game instance | Yes (if --game not provided) |
TF_CHARACTER | Character name | Yes (if --character not provided) |
# Set credentials via environment
export TF_ACCOUNT="myaccount"
export TF_PASSWORD="mypassword"
export TF_GAME="prime"
export TF_CHARACTER="Warrior"
# Launch without command-line credentials
two-face --direct
Lich Mode
| Variable | Description | Default |
|---|---|---|
TF_HOST | Lich proxy host | 127.0.0.1 |
TF_PORT | Lich proxy port | 8000 |
export TF_HOST="192.168.1.100"
export TF_PORT="8001"
two-face # Uses environment values
Configuration Variables
| Variable | Description | Default |
|---|---|---|
TF_CONFIG_DIR | Configuration directory | ~/.two-face |
TF_CONFIG | Main config file path | $TF_CONFIG_DIR/config.toml |
TF_LAYOUT | Layout config path | $TF_CONFIG_DIR/layout.toml |
TF_COLORS | Colors config path | $TF_CONFIG_DIR/colors.toml |
TF_KEYBINDS | Keybinds config path | $TF_CONFIG_DIR/keybinds.toml |
# Custom config directory
export TF_CONFIG_DIR="/home/user/games/two-face"
# Individual file overrides
export TF_LAYOUT="/home/user/layouts/hunting.toml"
Logging Variables
| Variable | Description | Default |
|---|---|---|
TF_LOG_LEVEL | Log verbosity | info |
TF_LOG_FILE | Log file path | $TF_CONFIG_DIR/two-face.log |
RUST_LOG | Rust logging (alternative) | - |
# Enable debug logging
export TF_LOG_LEVEL="debug"
# Custom log file
export TF_LOG_FILE="/tmp/two-face.log"
# Or use Rust's standard logging
export RUST_LOG="two_face=debug"
Log Levels
| Level | Description |
|---|---|
error | Errors only |
warn | Warnings and above |
info | General information |
debug | Debug information |
trace | Verbose tracing |
Display Variables
| Variable | Description | Default |
|---|---|---|
TF_NO_COLOR | Disable colors | false |
TF_FORCE_COLOR | Force colors | false |
TERM | Terminal type | System default |
COLORTERM | Color support level | System default |
# Disable colors (for logging/piping)
export TF_NO_COLOR=1
# Force colors (for terminals that don't advertise support)
export TF_FORCE_COLOR=1
Build Variables
Used during compilation:
| Variable | Description |
|---|---|
VCPKG_ROOT | vcpkg installation path (Windows) |
OPENSSL_DIR | OpenSSL installation path |
OPENSSL_LIB_DIR | OpenSSL library path |
OPENSSL_INCLUDE_DIR | OpenSSL headers path |
# Windows (vcpkg)
set VCPKG_ROOT=C:\tools\vcpkg
# macOS (Homebrew)
export OPENSSL_DIR=$(brew --prefix openssl)
# Linux (custom OpenSSL)
export OPENSSL_DIR=/opt/openssl
export OPENSSL_LIB_DIR=$OPENSSL_DIR/lib
export OPENSSL_INCLUDE_DIR=$OPENSSL_DIR/include
TTS Variables
| Variable | Description | Default |
|---|---|---|
TF_TTS_ENGINE | TTS engine override | System default |
TF_TTS_VOICE | Voice override | System default |
# Force specific TTS engine
export TF_TTS_ENGINE="espeak"
export TF_TTS_VOICE="en-us"
Precedence
Environment variables are applied in this order:
- Default values (lowest)
- Configuration files
- Environment variables
- Command-line arguments (highest)
Example:
# Config file says port = 8000
# Environment says TF_PORT=8001
# Command line says --port 8002
# Result: port 8002 is used
Security Notes
Credentials in Environment
Environment variables are visible to:
- The current process
- Child processes
- Process inspection tools
Recommendations:
- Don’t export credentials in shell profile files
- Use temporary environment for session
- Clear history after entering passwords
# Safer approach - prompt for password
read -sp "Password: " TF_PASSWORD
export TF_PASSWORD
Logging Redaction
Logs may contain environment values. The log system attempts to redact:
TF_PASSWORDTF_ACCOUNT(partially)
But review logs before sharing publicly.
Shell Integration
Bash/Zsh
# ~/.bashrc or ~/.zshrc
# Two-Face config
export TF_CONFIG_DIR="$HOME/.two-face"
export TF_LOG_LEVEL="info"
# Alias with common options
alias gs4='two-face --host 127.0.0.1 --port 8000'
Fish
# ~/.config/fish/config.fish
set -x TF_CONFIG_DIR "$HOME/.two-face"
set -x TF_LOG_LEVEL "info"
alias gs4 'two-face --host 127.0.0.1 --port 8000'
PowerShell
# $PROFILE
$env:TF_CONFIG_DIR = "$env:USERPROFILE\.two-face"
$env:TF_LOG_LEVEL = "info"
function gs4 { two-face --host 127.0.0.1 --port 8000 $args }
Windows Command Prompt
:: Set for current session
set TF_CONFIG_DIR=%USERPROFILE%\.two-face
:: Set permanently (requires admin)
setx TF_CONFIG_DIR "%USERPROFILE%\.two-face"
Debugging
Check current environment:
# Show all TF_ variables
env | grep ^TF_
# Show specific variable
echo $TF_CONFIG_DIR
See Also
Default Files
Default configuration file contents for reference.
config.toml
# Two-Face Configuration
# Default settings
[connection]
mode = "lich"
host = "127.0.0.1"
port = 8000
auto_reconnect = true
reconnect_delay = 5
[tts]
enabled = false
engine = "default"
voice = "default"
rate = 1.0
volume = 1.0
speak_room_descriptions = true
speak_combat = false
speak_speech = true
speak_whispers = true
speak_thoughts = false
[logging]
level = "info"
file = "~/.two-face/two-face.log"
[performance]
render_rate = 60
batch_updates = true
lazy_render = true
layout.toml
# Two-Face Layout
# Default widget arrangement
# Main game text window
[[widgets]]
type = "text"
name = "main"
title = "Game"
x = 0
y = 0
width = 75
height = 85
streams = ["main", "room", "combat", "speech", "thoughts"]
scrollback = 5000
auto_scroll = true
# Health bar
[[widgets]]
type = "progress"
name = "health"
title = "HP"
x = 76
y = 0
width = 24
height = 3
data_source = "vitals.health"
color = "health"
show_text = true
show_percentage = true
# Mana bar
[[widgets]]
type = "progress"
name = "mana"
title = "MP"
x = 76
y = 4
width = 24
height = 3
data_source = "vitals.mana"
color = "mana"
show_text = true
show_percentage = true
# Stamina bar
[[widgets]]
type = "progress"
name = "stamina"
title = "ST"
x = 76
y = 8
width = 24
height = 3
data_source = "vitals.stamina"
color = "stamina"
show_text = true
show_percentage = true
# Spirit bar
[[widgets]]
type = "progress"
name = "spirit"
title = "SP"
x = 76
y = 12
width = 24
height = 3
data_source = "vitals.spirit"
color = "spirit"
show_text = true
show_percentage = true
# Compass
[[widgets]]
type = "compass"
name = "compass"
x = 76
y = 16
width = 24
height = 10
style = "unicode"
clickable = true
# Roundtime
[[widgets]]
type = "countdown"
name = "roundtime"
title = "RT"
x = 76
y = 27
width = 24
height = 3
data_source = "roundtime"
# Status indicators
[[widgets]]
type = "indicator"
name = "status"
x = 76
y = 31
width = 24
height = 10
indicators = ["hidden", "stunned", "webbed", "prone", "kneeling"]
columns = 2
# Command input
[[widgets]]
type = "command_input"
name = "input"
x = 0
y = 86
width = 100
height = 14
history_size = 500
prompt = "> "
colors.toml
# Two-Face Colors
# Default theme
[theme]
name = "Default"
# Base colors
background = "#000000"
text = "#c0c0c0"
text_dim = "#808080"
# Borders
border = "#404040"
border_focused = "#ffffff"
# Vitals
health = "#00ff00"
health_low = "#ffff00"
health_critical = "#ff0000"
mana = "#0080ff"
stamina = "#ff8000"
spirit = "#ff00ff"
# Streams
main = "#ffffff"
room = "#ffff00"
combat = "#ff4444"
speech = "#00ffff"
whisper = "#ff00ff"
thoughts = "#00ff00"
# Status indicators
hidden = "#00ff00"
invisible = "#00ffff"
stunned = "#ffff00"
webbed = "#ff00ff"
prone = "#00ffff"
kneeling = "#ff8000"
sitting = "#808080"
dead = "#ff0000"
keybinds.toml
# Two-Face Keybinds
# Default key mappings
# Navigation - Numpad
[keybinds."numpad8"]
macro = "north"
[keybinds."numpad2"]
macro = "south"
[keybinds."numpad4"]
macro = "west"
[keybinds."numpad6"]
macro = "east"
[keybinds."numpad7"]
macro = "northwest"
[keybinds."numpad9"]
macro = "northeast"
[keybinds."numpad1"]
macro = "southwest"
[keybinds."numpad3"]
macro = "southeast"
[keybinds."numpad5"]
macro = "out"
# Widget navigation
[keybinds."tab"]
action = "next_widget"
[keybinds."shift+tab"]
action = "prev_widget"
[keybinds."escape"]
action = "focus_input"
# Scrolling
[keybinds."page_up"]
action = "scroll_up"
[keybinds."page_down"]
action = "scroll_down"
[keybinds."home"]
action = "scroll_top"
[keybinds."end"]
action = "scroll_bottom"
# Quick commands
[keybinds."f1"]
macro = "look"
[keybinds."f2"]
macro = "inventory"
[keybinds."f3"]
macro = "experience"
# Search
[keybinds."ctrl+f"]
action = "open_search"
# Quit
[keybinds."ctrl+q"]
action = "quit"
highlights.toml
# Two-Face Highlights
# Default text patterns
# Critical status
[[highlights]]
pattern = "(?i)you are stunned"
fg = "black"
bg = "yellow"
bold = true
[[highlights]]
pattern = "(?i)webs? (stick|entangle)"
fg = "black"
bg = "magenta"
bold = true
# Combat
[[highlights]]
pattern = "\\*\\* .+ \\*\\*"
fg = "bright_red"
bold = true
[[highlights]]
pattern = "falls dead"
fg = "bright_yellow"
bold = true
# Communication
[[highlights]]
pattern = "(\\w+) whispers,"
fg = "magenta"
[[highlights]]
pattern = "(\\w+) says?,"
fg = "cyan"
# Room elements
[[highlights]]
pattern = "^\\[.+\\]$"
fg = "bright_yellow"
bold = true
[[highlights]]
pattern = "Obvious (exits|paths):"
fg = "gray"
triggers.toml
# Two-Face Triggers
# Default automation (minimal)
# Whisper notification
[[triggers]]
name = "whisper_alert"
pattern = "(\\w+) whispers,"
command = ".notify Whisper from $1"
enabled = false
# Stun notification
[[triggers]]
name = "stun_alert"
pattern = "(?i)you are stunned"
command = ".notify Stunned!"
cooldown = 1000
enabled = false
cmdlist.toml
# Two-Face Command Lists
# Default context menus
# General items
[[cmdlist]]
noun = ".*"
match_mode = "regex"
commands = ["look", "get", "drop"]
priority = 1
# Creatures
[[cmdlist]]
category = "creature"
noun = ".*"
match_mode = "regex"
commands = ["attack", "look", "assess"]
priority = 10
# Containers
[[cmdlist]]
category = "container"
noun = "(?i)(backpack|bag|pouch|sack|cloak)"
match_mode = "regex"
commands = ["look in", "open", "close"]
priority = 20
# Players
[[cmdlist]]
category = "player"
noun = "^[A-Z][a-z]+$"
match_mode = "regex"
commands = ["look", "smile", "bow", "wave"]
priority = 30
Generating Defaults
To generate default configuration files:
# Dump defaults to stdout
two-face --dump-config
# Create default files
two-face --dump-config > ~/.two-face/config.toml
Restoring Defaults
To restore a file to defaults:
# Backup current config
cp ~/.two-face/config.toml ~/.two-face/config.toml.bak
# Generate fresh default
two-face --dump-config > ~/.two-face/config.toml
Or delete the file - Two-Face will use built-in defaults.
See Also
Migration Guides
Guides for transitioning to Two-Face from other clients.
Overview
If you’re coming from another GemStone IV or DragonRealms client, these guides help you translate your existing setup to Two-Face.
Migration Guides
| Previous Client | Guide |
|---|---|
| Profanity | From Profanity |
| Wizard Front End | From WFE |
| StormFront | From StormFront |
Common Migration Tasks
Configuration Translation
Each client has different configuration formats. Common concepts to translate:
| Concept | Profanity | WFE | Two-Face |
|---|---|---|---|
| Layout | Window positions | Window setup | layout.toml |
| Colors | Theme files | Color settings | colors.toml |
| Macros | Scripts/aliases | Macros | keybinds.toml |
| Triggers | Triggers | Triggers | triggers.toml |
| Highlights | Highlights | Highlights | highlights.toml |
Feature Mapping
| Feature | Common In | Two-Face Equivalent |
|---|---|---|
| Text windows | All clients | text widgets |
| Health bars | All clients | progress widgets |
| Compass | All clients | compass widget |
| Macros | All clients | Keybind macros |
| Triggers | All clients | triggers.toml |
| Scripts | Profanity/Lich | Via Lich proxy |
General Migration Steps
1. Install Two-Face
Follow the installation guide.
2. Connect to Game
Test connection before migrating configuration:
# Via Lich (existing setup)
two-face --host 127.0.0.1 --port 8000
3. Create Basic Layout
Start with default layout, then customize:
# Use built-in defaults first
two-face --dump-config > ~/.two-face/config.toml
4. Translate Configuration
Migrate settings from your previous client:
- Layout/window positions
- Colors/theme
- Macros/keybinds
- Triggers
- Highlights
5. Test and Refine
- Test each migrated feature
- Adjust as needed
- Take advantage of new features
What Carries Over
Lich Scripts
If you use Lich, your scripts continue to work. Two-Face connects through Lich just like other clients.
Game Settings
In-game settings (character options, game preferences) are server-side and unaffected.
Account Information
Your Simutronics account, characters, and subscriptions are unchanged.
What Changes
Configuration Files
Each client uses different file formats. Configuration must be recreated (not copied).
Keybinds
Keybind syntax differs. Translate your macros to Two-Face format.
Visual Layout
Screen arrangement uses different systems. Recreate your preferred layout.
Migration Tips
Start Fresh vs. Replicate
Option A: Start Fresh
- Use Two-Face defaults
- Learn new features
- Gradually customize
Option B: Replicate
- Match previous client exactly
- Familiar environment
- Then explore new features
Document Your Previous Setup
Before migrating, note:
- Window positions and sizes
- Important macros
- Color preferences
- Trigger patterns
Take Your Time
Migration doesn’t have to be immediate:
- Run Two-Face alongside previous client
- Migrate incrementally
- Test each feature
Version History
See Version History for:
- Release notes
- Breaking changes
- Upgrade instructions
Getting Help
If you encounter issues:
- Check Troubleshooting
- Search GitHub issues
- Ask the community
See Also
Migrating from Profanity
Guide for Profanity users transitioning to Two-Face.
Overview
Profanity and Two-Face are both terminal-based clients. Many concepts translate directly, though configuration syntax differs.
Key Similarities
| Feature | Profanity | Two-Face |
|---|---|---|
| Terminal-based | ✓ | ✓ |
| Lich support | ✓ | ✓ |
| Text windows | ✓ | ✓ |
| Macros | ✓ | ✓ |
| Triggers | ✓ | ✓ |
| Highlights | ✓ | ✓ |
Key Differences
| Aspect | Profanity | Two-Face |
|---|---|---|
| Config format | Custom format | TOML |
| Language | Ruby | Rust |
| Widget system | Fixed types | Flexible types |
| Layout | Relative | Percentage-based |
Configuration Translation
Window Layout
Profanity (in configuration):
window main 0 0 80 20
window health 81 0 20 3
Two-Face (layout.toml):
[[widgets]]
type = "text"
name = "main"
x = 0
y = 0
width = 80
height = 75
[[widgets]]
type = "progress"
name = "health"
x = 81
y = 0
width = 19
height = 5
data_source = "vitals.health"
Highlights
Profanity:
highlight "stunned" bold yellow
highlight /whispers,/ magenta
Two-Face (highlights.toml):
[[highlights]]
pattern = "stunned"
fg = "bright_yellow"
bold = true
[[highlights]]
pattern = "whispers,"
fg = "magenta"
Macros
Profanity:
macro F1 attack target
macro ctrl-1 "prep 101;cast"
Two-Face (keybinds.toml):
[keybinds."f1"]
macro = "attack target"
[keybinds."ctrl+1"]
macro = "prep 101;cast"
Triggers
Profanity:
trigger "You are stunned" echo "STUNNED!"
trigger /falls dead/ "search;loot"
Two-Face (triggers.toml):
[[triggers]]
pattern = "You are stunned"
command = ".notify STUNNED!"
[[triggers]]
pattern = "falls dead"
command = "search;loot"
Colors
Profanity:
color health green
color mana blue
color background black
Two-Face (colors.toml):
[theme]
health = "#00ff00"
mana = "#0080ff"
background = "#000000"
Lich Integration
Both clients connect through Lich the same way. Your Lich scripts continue to work:
# Same connection method
two-face --host 127.0.0.1 --port 8000
Feature Comparison
Text Windows
| Profanity | Two-Face |
|---|---|
main window | text widget with streams = ["main"] |
thoughts window | text widget with streams = ["thoughts"] |
| Multiple windows | Multiple text widgets |
Progress Bars
| Profanity | Two-Face |
|---|---|
| Built-in health bar | progress widget with data_source = "vitals.health" |
| Fixed appearance | Customizable colors and style |
Compass
| Profanity | Two-Face |
|---|---|
| ASCII compass | compass widget with style = "ascii" or "unicode" |
| Fixed location | Any position |
Step-by-Step Migration
1. Document Your Profanity Setup
List:
- Window positions
- Macros
- Triggers
- Highlights
- Color preferences
2. Create Two-Face Config Directory
mkdir -p ~/.two-face
3. Create Layout
Translate window positions to layout.toml:
# Approximate your Profanity layout
[[widgets]]
type = "text"
name = "main"
x = 0
y = 0
width = 80
height = 80
streams = ["main", "room"]
# Add other widgets...
4. Create Colors
Translate color preferences:
[theme]
background = "#000000"
text = "#c0c0c0"
health = "#00ff00"
# Add more...
5. Migrate Macros
# keybinds.toml
[keybinds."f1"]
macro = "attack target"
[keybinds."f2"]
macro = "hide"
# Add more...
6. Migrate Triggers
# triggers.toml
[[triggers]]
name = "stun_alert"
pattern = "You are stunned"
command = ".notify STUNNED!"
# Add more...
7. Migrate Highlights
# highlights.toml
[[highlights]]
pattern = "stunned"
fg = "bright_yellow"
bold = true
# Add more...
Common Adjustments
Layout Units
Profanity uses character columns/rows. Two-Face uses percentages (0-100).
Converting:
- Estimate percentage of screen
- Or:
(columns / terminal_width) * 100
Regex Patterns
Both support regex, but syntax may vary:
- Test patterns in Two-Face
- Adjust escaping as needed
Macro Syntax
| Profanity | Two-Face |
|---|---|
; separates commands | ; separates commands |
$input for prompt | $input for prompt |
| Delays | {ms} for delays |
What’s New in Two-Face
Features you might not have in Profanity:
- Tabbed windows: Multiple tabs in one widget
- Unicode compass: Modern arrow characters
- Percentage layout: Adapts to terminal size
- Active effects widget: Track spell durations
- Browser system: Popup editors
Troubleshooting
Macros Not Working
- Check key name format (
"f1"not"F1") - Verify TOML syntax (quotes around keys with special chars)
Colors Different
- Two-Face uses hex colors or preset names
- Verify color name mappings
Layout Looks Wrong
- Check percentage calculations
- Widgets shouldn’t overlap (x + width ≤ 100)
See Also
Migrating from Wizard Front End (WFE)
Guide for WFE users transitioning to Two-Face.
Overview
WFE (Wizard Front End) is a graphical Windows client. Two-Face is a terminal-based client, so the visual experience differs, but functionality translates.
Key Differences
| Aspect | WFE | Two-Face |
|---|---|---|
| Platform | Windows GUI | Terminal (cross-platform) |
| Interface | Graphical windows | Text-based widgets |
| Scripting | Built-in | Via Lich |
| Configuration | GUI settings | TOML files |
Feature Mapping
Windows to Widgets
| WFE Window | Two-Face Widget |
|---|---|
| Main Game Window | text widget |
| Inventory Window | inventory widget |
| Status Window | indicator widget |
| Compass | compass widget |
| Health/Mana Bars | progress widgets |
Configuration
| WFE | Two-Face |
|---|---|
| Settings dialogs | config.toml |
| Window layout | layout.toml |
| Color settings | colors.toml |
| Macros | keybinds.toml |
Macros Translation
WFE Macro
F1 = attack target
Ctrl+1 = prep 101;cast
Two-Face Keybind
[keybinds."f1"]
macro = "attack target"
[keybinds."ctrl+1"]
macro = "prep 101;cast"
Macro Variables
| WFE | Two-Face |
|---|---|
%0 (input) | $input |
%target | $target |
| Pause 500ms | {500} |
Highlights Translation
WFE Highlight
Text: "stunned" - Color: Yellow, Bold
Two-Face Highlight
[[highlights]]
pattern = "stunned"
fg = "bright_yellow"
bold = true
Triggers Translation
WFE Trigger
Pattern: "You are stunned"
Action: Play sound stun.wav
Two-Face Trigger
[[triggers]]
pattern = "You are stunned"
command = ".sound stun.wav"
Layout Translation
WFE uses absolute pixel positions. Two-Face uses percentages.
WFE Layout Concept
Main Window: 0,0 - 600x400
Side Panel: 605,0 - 200x400
Two-Face Equivalent
# Estimate percentages
[[widgets]]
type = "text"
name = "main"
x = 0
y = 0
width = 75
height = 90
[[widgets]]
type = "indicator"
name = "status"
x = 76
y = 0
width = 24
height = 20
Step-by-Step Migration
1. Document WFE Settings
Note:
- Window sizes and positions
- Macro assignments
- Highlight patterns
- Trigger patterns
- Color preferences
2. Install Two-Face
Follow installation guide.
3. Set Up Lich Connection
If you weren’t using Lich with WFE, you’ll need to:
- Install Lich
- Configure Lich with your credentials
- Connect Two-Face through Lich
two-face --host 127.0.0.1 --port 8000
Or use direct mode:
two-face --direct --account USER --password PASS --game prime --character CHAR
4. Create Basic Layout
# ~/.two-face/layout.toml
# Main window
[[widgets]]
type = "text"
name = "main"
x = 0
y = 0
width = 70
height = 85
streams = ["main", "room", "combat"]
# Health bar
[[widgets]]
type = "progress"
name = "health"
title = "HP"
x = 71
y = 0
width = 29
height = 4
data_source = "vitals.health"
color = "health"
# Mana bar
[[widgets]]
type = "progress"
name = "mana"
title = "MP"
x = 71
y = 5
width = 29
height = 4
data_source = "vitals.mana"
color = "mana"
# Compass
[[widgets]]
type = "compass"
name = "compass"
x = 71
y = 10
width = 29
height = 12
style = "unicode"
# Command input
[[widgets]]
type = "command_input"
name = "input"
x = 0
y = 86
width = 100
height = 14
5. Migrate Macros
# ~/.two-face/keybinds.toml
# Movement
[keybinds."numpad8"]
macro = "north"
[keybinds."numpad2"]
macro = "south"
# Combat
[keybinds."f1"]
macro = "attack target"
# Spells
[keybinds."ctrl+1"]
macro = "prep 101;cast"
6. Migrate Colors
# ~/.two-face/colors.toml
[theme]
background = "#000000"
text = "#ffffff"
health = "#00ff00"
mana = "#0000ff"
stamina = "#ffff00"
combat = "#ff0000"
speech = "#00ffff"
7. Migrate Triggers
# ~/.two-face/triggers.toml
[[triggers]]
name = "stun_alert"
pattern = "You are stunned"
command = ".notify STUNNED!"
cooldown = 1000
[[triggers]]
name = "death_search"
pattern = "falls dead"
command = "search"
enabled = false # Enable when ready
Visual Differences
GUI vs Terminal
WFE’s graphical interface provides:
- Clickable buttons
- Drag-and-drop
- Visual menus
Two-Face’s terminal interface provides:
- Keyboard-centric operation
- Works over SSH
- Lower resource usage
Adapting Workflow
| WFE Action | Two-Face Action |
|---|---|
| Click compass direction | Numpad navigation |
| Right-click context menu | .cmdlist system |
| Toolbar buttons | Function key macros |
| Settings dialog | Edit TOML files |
Scripting Differences
WFE Built-in Scripting
WFE has built-in scripting capabilities.
Two-Face + Lich
Two-Face relies on Lich for scripting:
# In game with Lich
;script_name # Run script
;kill script # Stop script
;list # List scripts
What You Gain
Moving to Two-Face:
- Cross-platform: Works on Windows, macOS, Linux
- SSH access: Play remotely via terminal
- Customizability: Deep configuration options
- Modern rendering: Unicode support
- Open source: Community improvements
What You Lose
- Visual GUI: No graphical interface
- Built-in scripting: Must use Lich
- Click-based operation: Keyboard-focused
Tips for GUI Users
Learn Terminal Basics
- Arrow keys navigate
- Tab cycles widgets
- Page Up/Down scrolls
- Type commands directly
Embrace Keyboard
Most efficient Two-Face users:
- Memorize key macros
- Use numpad for movement
- Minimize mouse usage
Gradual Transition
Consider running both clients initially while learning Two-Face.
See Also
Migrating from StormFront
Guide for StormFront users transitioning to Two-Face.
Overview
StormFront is Simutronics’ official graphical client. Two-Face is an alternative terminal-based client with different strengths.
Key Differences
| Aspect | StormFront | Two-Face |
|---|---|---|
| Type | Official GUI client | Third-party terminal |
| Platform | Windows/macOS | Cross-platform terminal |
| Scripting | Limited | Via Lich |
| Customization | Limited | Extensive |
| Price | Free | Free |
Feature Comparison
Windows
| StormFront | Two-Face |
|---|---|
| Game Window | text widget |
| Thoughts Window | text widget (thoughts stream) |
| Inventory Panel | inventory widget |
| Status Bar | indicator widget |
Visual Elements
| StormFront | Two-Face |
|---|---|
| Health/Mana bars | progress widgets |
| Compass rose | compass widget |
| Character portrait | Not supported |
| Spell icons | active_effects widget |
Why Switch?
Advantages of Two-Face
- Lich scripting: Full automation capability
- Deep customization: Complete control over appearance
- Terminal access: Play over SSH
- Cross-platform: Works everywhere
- Lightweight: Low resource usage
What You Keep
- Game experience
- Character data
- Account access
- In-game settings
What Changes
- Visual appearance
- Configuration method
- Scripting approach
Step-by-Step Migration
1. Choose Connection Method
Option A: Lich Proxy (Recommended)
Install Lich for scripting and automation:
- Install Lich and Ruby
- Configure Lich with your account
- Connect Two-Face through Lich
Option B: Direct Connection
Connect without Lich (no scripts):
two-face --direct \
--account YOUR_ACCOUNT \
--password YOUR_PASSWORD \
--game prime \
--character CHARACTER
2. Install Two-Face
Follow installation guide.
3. Create Configuration
Generate default configuration:
mkdir -p ~/.two-face
two-face --dump-config > ~/.two-face/config.toml
4. Create Layout
Approximate StormFront layout:
# ~/.two-face/layout.toml
# Room name at top
[[widgets]]
type = "room"
name = "room"
x = 0
y = 0
width = 100
height = 4
show_exits = true
# Main game text
[[widgets]]
type = "text"
name = "main"
x = 0
y = 4
width = 70
height = 55
streams = ["main", "room"]
# Thoughts/ESP window
[[widgets]]
type = "text"
name = "thoughts"
title = "Thoughts"
x = 0
y = 60
width = 70
height = 30
streams = ["thoughts"]
# Right sidebar - vitals
[[widgets]]
type = "progress"
name = "health"
title = "Health"
x = 71
y = 4
width = 29
height = 4
data_source = "vitals.health"
color = "health"
show_text = true
[[widgets]]
type = "progress"
name = "mana"
title = "Mana"
x = 71
y = 9
width = 29
height = 4
data_source = "vitals.mana"
color = "mana"
show_text = true
[[widgets]]
type = "progress"
name = "stamina"
title = "Stamina"
x = 71
y = 14
width = 29
height = 4
data_source = "vitals.stamina"
color = "stamina"
show_text = true
# Compass
[[widgets]]
type = "compass"
name = "compass"
x = 71
y = 19
width = 29
height = 12
style = "unicode"
clickable = true
# Status indicators
[[widgets]]
type = "indicator"
name = "status"
x = 71
y = 32
width = 29
height = 10
indicators = ["hidden", "stunned", "prone"]
# Active spells
[[widgets]]
type = "active_effects"
name = "spells"
title = "Spells"
x = 71
y = 43
width = 29
height = 20
show_duration = true
# Command input
[[widgets]]
type = "command_input"
name = "input"
x = 0
y = 91
width = 100
height = 9
prompt = "> "
5. Set Up Keybinds
StormFront uses basic keybinds. Expand with Two-Face:
# ~/.two-face/keybinds.toml
# Movement (numpad)
[keybinds."numpad8"]
macro = "north"
[keybinds."numpad2"]
macro = "south"
[keybinds."numpad4"]
macro = "west"
[keybinds."numpad6"]
macro = "east"
[keybinds."numpad7"]
macro = "northwest"
[keybinds."numpad9"]
macro = "northeast"
[keybinds."numpad1"]
macro = "southwest"
[keybinds."numpad3"]
macro = "southeast"
[keybinds."numpad5"]
macro = "out"
# Quick commands
[keybinds."f1"]
macro = "look"
[keybinds."f2"]
macro = "inventory"
[keybinds."f3"]
macro = "experience"
# Combat
[keybinds."f5"]
macro = "attack target"
[keybinds."f6"]
macro = "hide"
# Navigation
[keybinds."tab"]
action = "next_widget"
[keybinds."page_up"]
action = "scroll_up"
[keybinds."page_down"]
action = "scroll_down"
6. Configure Highlights
# ~/.two-face/highlights.toml
# Critical status
[[highlights]]
pattern = "(?i)you are stunned"
fg = "black"
bg = "yellow"
bold = true
[[highlights]]
pattern = "(?i)webs? (stick|entangle)"
fg = "black"
bg = "magenta"
bold = true
# Combat
[[highlights]]
pattern = "\\*\\* .+ \\*\\*"
fg = "bright_red"
bold = true
# Social
[[highlights]]
pattern = "(\\w+) whispers,"
fg = "magenta"
Visual Adaptation
StormFront Visual Elements
StormFront provides:
- Graphical status bars
- Clickable compass
- Character portrait
- Spell icons
Two-Face Equivalents
Two-Face provides text-based versions:
- ASCII/Unicode progress bars
- Clickable text compass
- Status text indicators
- Spell name lists
Missing Features
Some StormFront features don’t have equivalents:
- Character portrait
- Graphical inventory icons
- Animated status effects
Gaining Lich Scripts
StormFront users often don’t use Lich. Two-Face + Lich provides:
Popular Scripts
go2- Automatic navigationbigshot- Hunting automationlnet- Player networkrepository- Script management
Installing Scripts
# In-game with Lich
;repository download go2
;go2 bank
What You Gain
- Customizable interface: Complete layout control
- Powerful scripting: Full Lich ecosystem
- Terminal access: Play anywhere
- Lower resources: Faster, lighter
- Community support: Active development
Tips for StormFront Users
Terminal Navigation
- Arrow keys and numpad work intuitively
- Tab cycles between widgets
- Type commands directly in input
Learn Keyboard Shortcuts
Memorize key bindings for efficiency:
- F-keys for common commands
- Numpad for movement
- Ctrl combinations for macros
Explore Lich
Lich transforms gameplay:
- Start with
go2for navigation - Try utility scripts
- Gradually automate routine tasks
See Also
Version History
Release notes, breaking changes, and upgrade instructions.
Current Version
Version: 0.1.0 (Development)
Changelog Format
Each release includes:
- New Features: New functionality
- Improvements: Enhancements to existing features
- Bug Fixes: Resolved issues
- Breaking Changes: Changes requiring user action
- Known Issues: Outstanding problems
Version 0.1.0 (Planned)
Initial public release.
New Features
- Core application framework
- Lich proxy connection mode
- Direct eAccess authentication
- Widget system with 15+ widget types
- TOML-based configuration
- Keybind and macro system
- Trigger automation
- Text highlighting
- Color themes
- TTS support
Widget Types
text- Scrollable text windowstabbed_text- Tabbed text windowsprogress- Vital barscountdown- Roundtime/casttimecompass- Navigation compassindicator- Status indicatorsroom- Room informationcommand_input- Command entryinjury_doll- Injury displayactive_effects- Active spells/effectsinventory- Inventory viewspells- Known spellshand- Hand contentsdashboard- Composite statusperformance- Performance metrics
Supported Platforms
- Windows 10/11
- macOS 12+
- Linux (glibc 2.31+)
Known Issues
- Direct mode certificate handling edge cases
- Some Unicode rendering issues on older terminals
Upgrading
General Upgrade Process
-
Backup configuration:
cp -r ~/.two-face ~/.two-face.backup -
Download new version
-
Replace binary
-
Review release notes for breaking changes
-
Update configuration if needed
-
Test before deleting backup
Checking Version
two-face --version
Configuration Compatibility
Configuration files are versioned. When format changes:
- Two-Face will warn about outdated config
- Auto-migration may be attempted
- Manual migration instructions provided if needed
Breaking Changes Guide
Configuration Format Changes
If a configuration key is renamed or restructured:
Before:
[old_section]
old_key = "value"
After:
[new_section]
new_key = "value"
Migration: Update your config files manually or run:
two-face --migrate-config
Keybind Changes
If keybind syntax changes:
Before:
[keybinds.F1]
command = "look"
After:
[keybinds."f1"]
macro = "look"
Widget Changes
If widget types or properties change:
Before:
[[widgets]]
type = "old_type"
old_property = "value"
After:
[[widgets]]
type = "new_type"
new_property = "value"
Deprecation Policy
Deprecation Timeline
- Announcement: Feature marked deprecated in release notes
- Warning Period: Deprecated features log warnings
- Removal: Feature removed in future major version
Deprecation Notices
Deprecated features are logged:
[WARN] 'old_feature' is deprecated, use 'new_feature' instead
Development Versions
Pre-release Tags
alpha- Early testing, unstablebeta- Feature complete, testingrc- Release candidate, final testing
Example: 0.2.0-beta.1
Building from Source
For development versions:
git clone https://github.com/two-face/two-face
cd two-face
git checkout develop
cargo build --release
Reporting Issues
When Upgrading
If you encounter issues after upgrading:
- Check release notes for breaking changes
- Verify configuration is valid
- Try with default configuration
- Check Troubleshooting
- Report on GitHub if unresolved
Issue Template
**Version**: [e.g., 0.1.0]
**Previous Version**: [if upgrade issue]
**Platform**: [Windows/macOS/Linux]
**Description**:
[What happened]
**Expected**:
[What should happen]
**Steps**:
1. [Step 1]
2. [Step 2]
**Configuration** (if relevant):
```toml
[relevant config]
---
## Future Roadmap
### Planned Features
- Advanced scripting integration
- Plugin system
- More widget types
- Enhanced accessibility
- Performance improvements
### Community Requests
Track feature requests on GitHub Issues.
---
## Version Support
### Support Matrix
| Version | Status | Support |
|---------|--------|---------|
| 0.1.x | Current | Full support |
| Pre-release | Development | Limited |
### End of Life
When a version reaches EOL:
- Security patches may cease
- No new features
- Upgrade recommended
---
## See Also
- [Installation](../getting-started/installation.md)
- [Configuration](../configuration/README.md)
- [Troubleshooting](../troubleshooting/README.md)
Troubleshooting
Diagnosis and solutions for common Two-Face issues.
Quick Diagnosis
Issue Categories
| Symptom | Likely Cause | Go To |
|---|---|---|
| Won’t start | Missing dependency, bad config | Common Errors |
| Can’t connect | Network, firewall, credentials | Connection Issues |
| Slow/laggy | Performance settings, system load | Performance Issues |
| Colors wrong | Theme, terminal settings | Display Issues |
| Platform-specific | OS configuration | Platform Issues |
First Steps
Before diving into specific issues, try these general steps:
1. Check Logs
# View recent logs
tail -100 ~/.two-face/two-face.log
# Watch logs in real-time
tail -f ~/.two-face/two-face.log
2. Verify Configuration
# Validate config syntax
two-face --check-config
# Start with defaults (bypass config issues)
two-face --default-config
3. Update Two-Face
# Check current version
two-face --version
# Download latest release from GitHub
4. Check Dependencies
# Linux - verify libraries
ldd $(which two-face)
# macOS - check dylibs
otool -L $(which two-face)
Log Levels
Increase logging for diagnosis:
# config.toml
[logging]
level = "debug" # trace, debug, info, warn, error
file = "~/.two-face/two-face.log"
Log level details:
error- Only critical failureswarn- Warnings and errorsinfo- Normal operation (default)debug- Detailed operationtrace- Very verbose (large files)
Common Patterns
Configuration Issues
Symptom: Two-Face exits immediately or shows “config error”
Diagnosis:
# Check for TOML syntax errors
two-face --check-config 2>&1 | head -20
Common causes:
- Missing quotes around strings with special characters
- Unclosed brackets or braces
- Invalid key names
- Wrong data types
Connection Issues
Symptom: “Connection refused” or timeout
Diagnosis:
# Test Lich connection
nc -zv 127.0.0.1 8000
# Test direct connection
nc -zv eaccess.play.net 7910
Display Issues
Symptom: Missing colors, garbled text, wrong symbols
Diagnosis:
# Check terminal capabilities
echo $TERM
tput colors
# Test Unicode support
echo "╔═══╗ ← Should show box drawing"
Getting Help
Information to Collect
When reporting issues, include:
- Version:
two-face --version - Platform: Windows/macOS/Linux version
- Terminal: What terminal emulator
- Config: Relevant config sections (redact credentials!)
- Logs: Recent log entries
- Steps: How to reproduce
Report Template
**Version**: 0.1.0
**Platform**: Ubuntu 22.04
**Terminal**: Alacritty 0.12
**Description**:
What happened
**Expected**:
What should happen
**Steps to Reproduce**:
1. Start Two-Face with...
2. Do this...
3. See error
**Relevant Config**:
```toml
[connection]
mode = "lich"
Log Output:
[ERROR] relevant log lines
### Support Channels
1. **GitHub Issues**: Bug reports and feature requests
2. **GitHub Discussions**: Questions and help
3. **Documentation**: Check relevant sections first
## Troubleshooting Guides
### By Issue Type
- [Common Errors](./common-errors.md) - Error messages and solutions
- [Platform Issues](./platform-issues.md) - OS-specific problems
- [Performance Issues](./performance-issues.md) - Speed and responsiveness
- [Display Issues](./display-issues.md) - Visual problems
- [Connection Issues](./connection-issues.md) - Network problems
### By Symptom
| I see... | Check |
|----------|-------|
| "Config error" | [Common Errors](./common-errors.md#configuration-errors) |
| "Connection refused" | [Connection Issues](./connection-issues.md#connection-refused) |
| "Certificate error" | [Connection Issues](./connection-issues.md#certificate-issues) |
| Garbled text | [Display Issues](./display-issues.md#character-encoding) |
| Wrong colors | [Display Issues](./display-issues.md#color-problems) |
| Slow scrolling | [Performance Issues](./performance-issues.md#scroll-performance) |
| High CPU | [Performance Issues](./performance-issues.md#cpu-usage) |
| Crash on start | [Platform Issues](./platform-issues.md) |
## Quick Fixes
### Reset to Defaults
```bash
# Backup current config
mv ~/.two-face ~/.two-face.backup
# Start fresh - Two-Face creates defaults
two-face
Force Reconnect
In Two-Face:
.reconnect
Or restart:
# Kill existing process
pkill two-face
# Start fresh
two-face
Clear Cache
# Remove cached data
rm -rf ~/.two-face/cache/
# Remove certificate (re-downloads)
rm ~/.two-face/simu.pem
Diagnostic Mode
Start with extra diagnostics:
# Maximum verbosity
TF_LOG=trace two-face --debug
# Log to specific file
TF_LOG_FILE=/tmp/tf-debug.log two-face
See Also
- Installation - Setup instructions
- Configuration - Config reference
- Network - Connection setup
Common Errors
Error messages, their meanings, and solutions.
Configuration Errors
“Failed to parse config”
Message:
Error: Failed to parse config.toml: expected `=` at line 15
Cause: TOML syntax error
Solution:
- Check the indicated line number
- Look for:
- Missing
=in key-value pairs - Unclosed quotes
- Missing brackets for sections/arrays
- Missing
Example fix:
# Wrong
[connection]
mode lich # Missing =
# Correct
[connection]
mode = "lich"
“Unknown key”
Message:
Error: Unknown key 'colour' in config.toml at line 12
Cause: Misspelled or deprecated key name
Solution: Check the Configuration Reference for correct key names
Common misspellings:
colour→colorcolour_theme→color(in theme context)keybind→keybinds
“Invalid value type”
Message:
Error: Invalid type for 'port': expected integer, found string
Cause: Wrong data type for configuration value
Solution: Use correct types:
# Wrong
port = "8000" # String when integer expected
enabled = 1 # Integer when boolean expected
# Correct
port = 8000 # Integer
enabled = true # Boolean
“Missing required field”
Message:
Error: Missing required field 'type' in widget definition
Cause: Required configuration field not specified
Solution: Add the missing field
# Wrong - missing type
[[widgets]]
name = "main"
# Correct
[[widgets]]
type = "text"
name = "main"
Startup Errors
“Failed to initialize terminal”
Message:
Error: Failed to initialize terminal: not a tty
Cause: Running in non-interactive environment
Solution:
- Run in a proper terminal emulator
- Don’t pipe output: use
two-facenottwo-face | less - Ensure stdin is connected to a terminal
“Could not determine terminal size”
Message:
Error: Could not determine terminal size
Cause: Terminal doesn’t report dimensions
Solution:
- Try a different terminal emulator
- Set size manually:
stty rows 50 cols 120 two-face - Check
TERMenvironment variable:echo $TERM # Should be something like xterm-256color
“Failed to load font”
Message:
Error: Failed to load font: font not found
Cause: Terminal can’t render required characters
Solution:
- Install a font with Unicode support (Nerd Font, JetBrains Mono)
- Or disable Unicode in config:
[display] unicode = false
Connection Errors
“Connection refused”
Message:
Error: Connection refused (os error 111)
Cause:
- Lich not running (Lich mode)
- Wrong host/port
- Firewall blocking
Solution:
For Lich mode:
# Start Lich first
ruby lich.rb
# Then connect
two-face --host 127.0.0.1 --port 8000
For Direct mode:
# Check internet connectivity
ping eaccess.play.net
# Check port access
nc -zv eaccess.play.net 7910
“Connection timed out”
Message:
Error: Connection timed out after 30 seconds
Cause: Network issue preventing connection
Solution:
- Check internet connectivity
- Verify firewall settings
- Try increasing timeout:
[connection] timeout = 60
“Authentication failed”
Message:
Error: Authentication failed: invalid credentials
Cause: Wrong account or password (Direct mode)
Solution:
- Verify credentials work via Lich or web
- Check for typos
- Ensure account is active
- Try password without special characters
“Certificate verification failed”
Message:
Error: Certificate verification failed: certificate has expired
Cause: Cached certificate is outdated or corrupt
Solution:
# Remove cached certificate
rm ~/.two-face/simu.pem
# Reconnect - will download fresh certificate
two-face --direct ...
Runtime Errors
“Widget not found”
Message:
Error: Widget 'health' not found in layout
Cause: Configuration references non-existent widget
Solution: Ensure widget is defined in layout:
# Make sure this exists
[[widgets]]
type = "progress"
name = "health"
“Invalid regex pattern”
Message:
Error: Invalid regex in highlights.toml: unclosed group at position 5
Cause: Malformed regular expression
Solution:
- Check regex syntax
- Escape special characters properly
- Test pattern with regex tester
Common regex fixes:
# Wrong - unescaped special chars
pattern = "You (get|take" # Unclosed group
# Correct
pattern = "You (get|take)" # Closed group
# Wrong - unescaped brackets
pattern = "[Player]" # Character class
# Correct
pattern = "\\[Player\\]" # Literal brackets
“Stream buffer overflow”
Message:
Warning: Stream buffer overflow, dropping oldest entries
Cause: Too much data incoming, buffer full
Solution:
# Increase buffer size
[performance]
stream_buffer_size = 100000
# Or reduce scrollback
[[widgets]]
type = "text"
scrollback = 1000 # Reduce from default
“Render timeout”
Message:
Warning: Render timeout - frame dropped
Cause: UI can’t keep up with updates
Solution:
[performance]
render_rate = 30 # Reduce from 60
batch_updates = true # Combine updates
lazy_render = true # Skip unchanged
Widget Errors
“Widget overlap”
Message:
Warning: Widget 'status' overlaps with 'health'
Cause: Two widgets occupy same screen space
Solution: Adjust widget positions:
# Check x, y, width, height don't overlap
[[widgets]]
type = "progress"
name = "health"
x = 0
y = 0
width = 20
height = 3
[[widgets]]
type = "indicator"
name = "status"
x = 0
y = 3 # Start after health ends
width = 20
height = 5
“Widget outside bounds”
Message:
Error: Widget 'compass' extends beyond terminal (x=90, width=20, terminal=100)
Cause: Widget doesn’t fit in terminal
Solution:
- Use percentage-based positioning
- Make widget smaller
- Check terminal size
“Invalid data source”
Message:
Error: Invalid data_source 'vital.health' for widget 'hp_bar'
Cause: Data source path doesn’t exist
Solution: Use valid data sources:
# Wrong
data_source = "vital.health"
# Correct
data_source = "vitals.health"
Valid sources: vitals.health, vitals.mana, vitals.stamina, vitals.spirit, roundtime, casttime
Keybind Errors
“Invalid key name”
Message:
Error: Invalid key name 'Control+F' in keybinds.toml
Cause: Wrong key naming convention
Solution: Use correct format:
# Wrong
[keybinds."Control+F"]
# Correct
[keybinds."ctrl+f"]
Key naming rules:
- Modifiers:
ctrl,alt,shift(lowercase) - Separator:
+ - Keys: lowercase (
f1,enter,space)
“Duplicate keybind”
Message:
Warning: Duplicate keybind 'ctrl+f' - later definition wins
Cause: Same key defined multiple times
Solution: Remove duplicates or use different keys
“Unknown action”
Message:
Error: Unknown action 'focusInput' in keybind
Cause: Invalid action name
Solution: Check Keybind Actions for valid actions
# Wrong
action = "focusInput"
# Correct
action = "focus_input"
Recovery Steps
General Recovery
-
Start with defaults:
two-face --default-config -
Isolate the problem:
# Test each config file two-face --config ~/.two-face/config.toml --no-layout two-face --config ~/.two-face/config.toml --layout ~/.two-face/layout.toml -
Binary search configs:
- Comment out half the config
- If it works, problem is in commented half
- Repeat until found
Reset Everything
# Backup
mv ~/.two-face ~/.two-face.backup
# Fresh start
two-face
# Copy back configs one at a time
cp ~/.two-face.backup/config.toml ~/.two-face/
# Test
cp ~/.two-face.backup/layout.toml ~/.two-face/
# Test
# Continue until problem returns
See Also
Platform Issues
Platform-specific problems and solutions for Windows, macOS, and Linux.
Windows
Windows Terminal Issues
Colors Look Wrong
Symptom: Colors appear washed out or wrong in Windows Terminal
Solution:
- Enable true color in Windows Terminal settings
- Set color scheme to “One Half Dark” or similar
- Add to Two-Face config:
[display] color_mode = "truecolor"
Characters Display as Boxes
Symptom: Unicode characters show as □ or ?
Solution:
-
Install a font with full Unicode support:
- Cascadia Code
- JetBrains Mono
- Nerd Font variants
-
Set the font in Windows Terminal settings
-
Or disable Unicode in Two-Face:
[display] unicode = false
Slow Startup
Symptom: Two-Face takes several seconds to start
Solution:
- Check antivirus exclusions - add Two-Face executable
- Disable Windows Defender real-time scanning for config directory
- Run from SSD rather than HDD
PowerShell Issues
Keybinds Not Working
Symptom: Certain key combinations don’t register
Cause: PowerShell intercepts some keys
Solution:
- Use Windows Terminal instead of PowerShell directly
- Or disable PSReadLine:
Remove-Module PSReadLine
Copy/Paste Issues
Symptom: Can’t paste into Two-Face
Solution:
- Use
Ctrl+Shift+Vinstead ofCtrl+V - Or enable “Use Ctrl+Shift+C/V as Copy/Paste” in terminal settings
WSL Issues
Connection to Lich Fails
Symptom: Can’t connect to Lich running in Windows from WSL
Solution:
# In WSL, use Windows host IP
two-face --host $(cat /etc/resolv.conf | grep nameserver | awk '{print $2}') --port 8000
# Or use localhost forwarding
two-face --host localhost --port 8000
Performance Issues
Symptom: Slow or laggy in WSL
Solution:
- Use WSL2 (not WSL1)
- Store files in Linux filesystem, not
/mnt/c/ - Increase WSL memory allocation in
.wslconfig
OpenSSL on Windows
“Can’t find OpenSSL”
Symptom: Direct mode fails with OpenSSL errors
Solution:
-
Install via vcpkg:
vcpkg install openssl:x64-windows -
Set environment variable:
set VCPKG_ROOT=C:\path\to\vcpkg -
Rebuild Two-Face
macOS
Terminal.app Issues
Limited Colors
Symptom: Only 256 colors, not full truecolor
Solution:
- Use iTerm2 or Alacritty instead of Terminal.app
- Or configure for 256 colors:
[display] color_mode = "256"
Function Keys Don’t Work
Symptom: F1-F12 keys trigger macOS features instead
Solution:
- System Preferences → Keyboard → “Use F1, F2, etc. keys as standard function keys”
- Or use modifier:
[keybinds."fn+f1"] macro = "look"
iTerm2 Issues
Mouse Not Working
Symptom: Can’t click on widgets or compass
Solution:
- iTerm2 → Preferences → Profiles → Terminal
- Enable “Report mouse clicks”
- Enable “Report mouse wheel events”
Scrollback Conflict
Symptom: Page Up/Down scrolls iTerm instead of Two-Face
Solution:
- iTerm2 → Preferences → Keys → Key Bindings
- Remove or remap Page Up/Down
- Or use Two-Face scrollback keys:
[keybinds."shift+up"] action = "scroll_up"
Security Issues
“Cannot be opened because the developer cannot be verified”
Symptom: macOS Gatekeeper blocks Two-Face
Solution:
# Remove quarantine attribute
xattr -d com.apple.quarantine /path/to/two-face
# Or allow in System Preferences → Security & Privacy
“Permission denied” for Audio
Symptom: TTS features fail
Solution:
- System Preferences → Security & Privacy → Privacy → Microphone
- Add Terminal/iTerm2
- Also check Accessibility permissions
Apple Silicon (M1/M2)
Rosetta Performance
Symptom: Two-Face slow on Apple Silicon
Cause: Running x86 binary through Rosetta
Solution:
- Download ARM64/aarch64 build if available
- Or build from source:
rustup target add aarch64-apple-darwin cargo build --release --target aarch64-apple-darwin
Linux
Distribution-Specific Issues
Ubuntu/Debian
Missing Libraries:
# Install required libraries
sudo apt install libssl-dev pkg-config
# For audio (TTS)
sudo apt install libasound2-dev
Wayland Issues:
# If running Wayland, may need XWayland for some features
sudo apt install xwayland
Fedora/RHEL
Missing Libraries:
sudo dnf install openssl-devel pkg-config
# For audio
sudo dnf install alsa-lib-devel
Arch Linux
Missing Libraries:
sudo pacman -S openssl pkgconf
# For audio
sudo pacman -S alsa-lib
Terminal Emulator Issues
Alacritty
Font Rendering:
# ~/.config/alacritty/alacritty.yml
font:
normal:
family: "JetBrains Mono"
size: 11
Key Bindings Conflict:
- Check
~/.config/alacritty/alacritty.ymlfor conflicting bindings
Kitty
Graphics Protocol:
# ~/.config/kitty/kitty.conf
# Disable if causing issues
allow_remote_control no
Unicode:
symbol_map U+E000-U+F8FF Symbols Nerd Font
GNOME Terminal
True Color:
# Verify support
echo $COLORTERM # Should show "truecolor"
# If not, set in .bashrc
export COLORTERM=truecolor
X11 vs Wayland
Clipboard Issues
Symptom: Copy/paste doesn’t work
X11 Solution:
# Install xclip
sudo apt install xclip
Wayland Solution:
# Install wl-clipboard
sudo apt install wl-clipboard
# Set in config
[clipboard]
backend = "wayland" # or "x11"
Audio Issues (TTS)
PulseAudio
Symptom: TTS doesn’t play sound
Solution:
# Check PulseAudio is running
pulseaudio --check
# Restart if needed
pulseaudio --kill
pulseaudio --start
PipeWire
Symptom: Audio issues on modern systems
Solution:
# Install PipeWire PulseAudio compatibility
sudo apt install pipewire-pulse
# Restart
systemctl --user restart pipewire pipewire-pulse
Permission Issues
“Operation not permitted”
Symptom: Can’t write to config directory
Solution:
# Check ownership
ls -la ~/.two-face
# Fix if needed
sudo chown -R $USER:$USER ~/.two-face
chmod 755 ~/.two-face
chmod 644 ~/.two-face/*.toml
SELinux Blocking
Symptom: Works as root but not as user (Fedora/RHEL)
Solution:
# Check if SELinux is blocking
sudo ausearch -m avc -ts recent
# Create policy exception if needed
# (Consult SELinux documentation)
Cross-Platform Issues
Locale/Encoding
“Invalid UTF-8”
Symptom: Errors about encoding
Solution:
# Check locale
locale
# Set UTF-8 locale
export LC_ALL=en_US.UTF-8
export LANG=en_US.UTF-8
Time Zone Issues
Timestamps Wrong
Symptom: Log timestamps incorrect
Solution:
# Check timezone
date
# Set if needed
export TZ="America/New_York"
Or configure:
[logging]
use_local_time = true
Path Issues
“File not found” for Config
Symptom: Can’t find config files
Solution:
- Use explicit paths:
two-face --config /full/path/to/config.toml - Check
$HOMEis set correctly - Verify
~expansion works in your shell
See Also
- Installation - Setup by platform
- Common Errors - Error messages
- Display Issues - Visual problems
Performance Issues
Diagnosing and fixing slow, laggy, or resource-intensive behavior.
Symptoms
| Issue | Likely Cause | Quick Fix |
|---|---|---|
| Slow scrolling | Large scrollback | Reduce scrollback |
| Input delay | Render bottleneck | Lower render_rate |
| High CPU | Busy loop, bad pattern | Check highlights |
| High memory | Scrollback accumulation | Limit buffer sizes |
| Startup slow | Config parsing | Simplify config |
Performance Configuration
Optimal Settings
[performance]
render_rate = 60 # FPS target (30-120)
batch_updates = true # Combine rapid updates
lazy_render = true # Skip unchanged regions
stream_buffer_size = 50000
# Widget defaults
[defaults.text]
scrollback = 2000 # Lines to keep
auto_scroll = true
Low-Resource Settings
For older systems or constrained environments:
[performance]
render_rate = 30 # Lower FPS
batch_updates = true
lazy_render = true
stream_buffer_size = 20000
[defaults.text]
scrollback = 500 # Minimal scrollback
Scroll Performance
Slow Scrolling
Symptom: Page Up/Down is sluggish, mouse scroll lags
Causes:
- Too much scrollback content
- Complex text styling
- Pattern matching on scroll
Solutions:
-
Reduce scrollback:
[[widgets]] type = "text" scrollback = 1000 # Down from 5000+ -
Simplify highlights:
# Avoid overly complex patterns # Bad - matches everything pattern = ".*" # Better - specific pattern pattern = "\\*\\* .+ \\*\\*" -
Use fast patterns:
[[highlights]] pattern = "stunned" fast_parse = true # Use Aho-Corasick
Scroll Buffer Memory
Symptom: Memory usage grows over time
Solution:
[[widgets]]
type = "text"
scrollback = 2000
scrollback_limit_action = "trim" # Remove old content
Input Latency
Typing Delay
Symptom: Characters appear slowly after typing
Causes:
- Render blocking input
- Network latency
- Pattern processing
Solutions:
-
Priority input processing:
[input] priority = "high" buffer_size = 100 -
Reduce render rate during input:
[performance] input_render_rate = 30 # Lower when typing render_rate = 60 # Normal rate -
Check network:
# Test latency to game server ping -c 10 gs4.play.net
Command Delay
Symptom: Commands take long to send
Solution:
- Check Lich processing time
- Disable unnecessary Lich scripts
- Try direct mode for comparison
CPU Usage
High CPU at Idle
Symptom: CPU high even when nothing happening
Causes:
- Busy render loop
- Bad regex pattern
- Timer spinning
Diagnosis:
# Profile Two-Face
top -p $(pgrep two-face)
# Check per-thread
htop -p $(pgrep two-face)
Solutions:
-
Enable lazy rendering:
[performance] lazy_render = true idle_timeout = 100 # ms before sleep -
Check patterns:
# Avoid catastrophic backtracking # Bad pattern = "(a+)+" # Good pattern = "a+" -
Reduce timers:
[[widgets]] type = "countdown" update_rate = 100 # ms, not too fast
CPU Spikes During Activity
Symptom: CPU spikes during combat/busy scenes
Solutions:
-
Batch updates:
[performance] batch_updates = true batch_threshold = 10 # Combine N updates -
Limit pattern matching:
[[highlights]] pattern = "complex pattern" max_matches_per_line = 10 -
Use simpler widgets:
# Disable complex widgets during heavy activity [widgets.effects] auto_hide = true hide_threshold = 50 # Updates/second
Memory Usage
Memory Growth
Symptom: Memory usage increases continuously
Causes:
- Unlimited scrollback
- Stream buffer growth
- Memory leak (rare)
Solutions:
-
Limit all buffers:
[performance] max_total_memory = "500MB" [[widgets]] type = "text" scrollback = 2000 -
Enable periodic cleanup:
[performance] cleanup_interval = 300 # seconds -
Monitor memory:
watch -n 5 'ps -o rss,vsz -p $(pgrep two-face)'
Large Scrollback
Problem: Each text window can accumulate megabytes
Solution:
# Global limit
[defaults.text]
scrollback = 1000
# Per-widget override only where needed
[[widgets]]
type = "text"
name = "main"
scrollback = 3000 # Main window can be larger
Startup Performance
Slow Launch
Symptom: Takes several seconds to start
Causes:
- Config file parsing
- Font loading
- Network checks
Solutions:
-
Simplify config:
- Split large configs into smaller files
- Remove unused sections
-
Skip network check:
two-face --no-network-check -
Precompile patterns:
[highlights] precompile = true cache_file = "~/.two-face/pattern_cache"
Slow Reconnect
Symptom: Reconnection takes long
Solution:
[connection]
reconnect_delay = 1 # Start at 1 second
reconnect_max_delay = 30 # Cap at 30 seconds
reconnect_backoff = 1.5 # Exponential backoff
Network Performance
High Latency
Symptom: Actions feel delayed
Diagnosis:
# Check network latency
ping gs4.play.net
traceroute gs4.play.net
Solutions:
-
Enable Nagle’s algorithm (if many small packets):
[network] tcp_nodelay = false -
Or disable for lower latency:
[network] tcp_nodelay = true -
Buffer commands:
[input] send_delay = 50 # ms between commands
Bandwidth Issues
Symptom: Connection drops during heavy traffic
Solution:
[network]
receive_buffer = 65536
send_buffer = 32768
Profiling
Built-in Performance Widget
[[widgets]]
type = "performance"
name = "perf"
x = 0
y = 95
width = 100
height = 5
show = ["fps", "memory", "cpu", "network"]
Enable Timing Logs
[logging]
level = "debug"
[debug]
log_render_time = true
log_parse_time = true
log_pattern_time = true
External Profiling
# Linux - perf
perf record -g two-face
perf report
# macOS - Instruments
xcrun xctrace record --template "Time Profiler" --launch two-face
# Cross-platform - flamegraph
cargo flamegraph --bin two-face
Quick Reference
Performance Checklist
- Scrollback limited to reasonable size
- Lazy rendering enabled
- Batch updates enabled
- No catastrophic regex patterns
- Unused widgets disabled
- Network connection stable
- System resources adequate
Key Settings Summary
[performance]
render_rate = 60
batch_updates = true
lazy_render = true
stream_buffer_size = 50000
[defaults.text]
scrollback = 2000
[logging]
level = "warn" # Less logging = more performance
See Also
- Configuration - All settings
- Architecture Performance - Design details
- Common Errors - Error messages
Display Issues
Solving color, rendering, and visual problems.
Color Problems
Colors Don’t Appear
Symptom: Everything is monochrome
Causes:
- Terminal doesn’t support colors
TERMvariable incorrect- Color disabled in config
Solutions:
-
Check terminal support:
# Check TERM echo $TERM # Should be xterm-256color, screen-256color, etc. # Check color count tput colors # Should be 256 or higher -
Set correct TERM:
export TERM=xterm-256color -
Enable colors in config:
[display] colors = true color_mode = "truecolor" # or "256" or "16"
Wrong Colors
Symptom: Colors look different than expected
Causes:
- Terminal color scheme conflicts
- True color vs 256-color mismatch
- Theme file issues
Solutions:
-
Match color modes:
# For terminals with true color (24-bit) [display] color_mode = "truecolor" # For older terminals [display] color_mode = "256" -
Check terminal palette:
- Terminal color scheme affects named colors
- Try built-in “Dark” or “Light” preset
- Use hex colors for consistency:
[theme] background = "#1a1a1a" # Explicit hex
-
Test colors:
# Print color test for i in {0..255}; do printf "\e[48;5;${i}m %3d \e[0m" $i [ $((($i + 1) % 16)) -eq 0 ] && echo done
Washed Out Colors
Symptom: Colors look pale or desaturated
Causes:
- Terminal transparency
- Bold-as-bright setting
- Theme choices
Solutions:
-
Disable terminal transparency for accurate colors
-
Adjust terminal settings:
- Disable “Bold as bright” if colors seem too bright
- Enable “Bold as bright” if bold text is invisible
-
Increase color saturation in theme:
[theme] # Use more saturated colors health = "#00ff00" # Bright green mana = "#0080ff" # Bright blue
High Contrast / Low Visibility
Symptom: Some text hard to read
Solutions:
[theme]
# Increase contrast
background = "#000000"
text = "#ffffff"
text_dim = "#b0b0b0" # Brighter dim text
# Use accessible color combinations
# Background: #000000, Text: #ffffff (21:1 contrast)
# Background: #1a1a1a, Text: #e0e0e0 (13:1 contrast)
Character Rendering
Missing Characters (Boxes)
Symptom: Characters display as □, ?, or empty boxes
Cause: Font missing required glyphs
Solutions:
-
Install complete font:
- JetBrains Mono
- Fira Code
- Cascadia Code
- Nerd Font variants (include many symbols)
-
Set fallback font in terminal settings
-
Disable Unicode:
[display] unicode = false ascii_borders = true
Wrong Box Drawing Characters
Symptom: Borders look wrong or misaligned
Causes:
- Font substitution
- Line height issues
- Unicode normalization
Solutions:
-
Use monospace font designed for terminals
-
Adjust line spacing:
# Alacritty example font: offset: y: 0 # May need adjustment -
Use ASCII borders:
[display] border_style = "ascii" # Uses +--+ instead of ╔══╗
Emoji Display Issues
Symptom: Emoji not rendering or wrong width
Solutions:
-
Ensure emoji font installed:
- Noto Color Emoji (Linux)
- Apple Color Emoji (macOS)
- Segoe UI Emoji (Windows)
-
Configure terminal:
# Alacritty font: builtin_box_drawing: true -
Avoid emoji in critical areas:
[display] emoji_support = false # Use text instead
Layout Problems
Widget Overlap
Symptom: Widgets draw over each other
Diagnosis:
# Start with layout debug
two-face --debug-layout
Solutions:
-
Check coordinates:
[[widgets]] type = "text" name = "main" x = 0 y = 0 width = 70 height = 85 # Ends at y=85 [[widgets]] type = "progress" name = "health" x = 0 y = 86 # Start after main ends -
Use percentage positioning:
[[widgets]] type = "text" x = "0%" y = "0%" width = "70%" height = "85%"
Widgets Outside Screen
Symptom: Widgets cut off or invisible
Causes:
- Fixed positions larger than terminal
- Percentage math errors
Solutions:
-
Use percentages for flexible layouts:
[[widgets]] width = "25%" # Always fits -
Handle resize:
[layout] resize_behavior = "scale" # Rescale widgets -
Check terminal size:
echo "Columns: $(tput cols) Rows: $(tput lines)"
Wrong Z-Order
Symptom: Popup appears behind other widgets
Solution:
[[widgets]]
type = "text"
name = "popup"
z_index = 100 # Higher = on top
Border Issues
Borders Not Aligned
Symptom: Border corners don’t meet, gaps appear
Causes:
- Font character width inconsistency
- Terminal cell size
- Unicode width calculation
Solutions:
-
Use consistent font:
- Pure monospace font
- Same font for all text
-
ASCII fallback:
[display] border_style = "ascii" -
Adjust border characters:
[theme.borders] horizontal = "─" vertical = "│" top_left = "┌" top_right = "┐" bottom_left = "└" bottom_right = "┘"
Double-Width Character Issues
Symptom: CJK characters or emoji break alignment
Solution:
[display]
wide_character_support = true
wcwidth_version = "unicode_15"
Refresh Issues
Partial Redraws
Symptom: Parts of screen don’t update
Causes:
- Optimization too aggressive
- Terminal damage tracking
- SSH/tmux issues
Solutions:
-
Force full refresh:
# In Two-Face .refreshOr keybind:
[keybinds."ctrl+l"] action = "refresh_screen" -
Disable lazy rendering:
[performance] lazy_render = false -
tmux settings:
# In .tmux.conf set -g default-terminal "screen-256color" set -ag terminal-overrides ",xterm-256color:RGB"
Screen Flicker
Symptom: Screen flashes during updates
Solutions:
-
Enable double buffering:
[display] double_buffer = true -
Batch updates:
[performance] batch_updates = true -
Check terminal:
- Some terminals handle rapid updates poorly
- Try Alacritty, Kitty, or WezTerm
Ghost Characters
Symptom: Old characters remain after widget moves
Solution:
[display]
clear_on_move = true
full_redraw_on_resize = true
Terminal-Specific Issues
SSH Display Problems
Symptom: Display corrupted over SSH
Solutions:
-
Set TERM correctly:
ssh -t user@host "TERM=xterm-256color two-face" -
Enable compression:
ssh -C user@host -
Reduce color depth:
[display] color_mode = "256" # More compatible
tmux Display Problems
Symptom: Colors or characters wrong in tmux
Solutions:
-
Configure tmux:
# .tmux.conf set -g default-terminal "tmux-256color" set -as terminal-features ",xterm-256color:RGB" -
Start Two-Face correctly:
TERM=tmux-256color two-face
Screen Display Problems
Symptom: Issues in GNU Screen
Solutions:
# .screenrc
term screen-256color
defutf8 on
Accessibility
High Contrast Mode
[theme]
name = "high_contrast"
background = "#000000"
text = "#ffffff"
border = "#ffffff"
border_focused = "#ffff00"
health = "#00ff00"
health_low = "#ffff00"
health_critical = "#ff0000"
Large Text
Configure in terminal settings, not Two-Face:
- Increase font size to 14pt+
- Two-Face will adapt to available space
Screen Reader Hints
[accessibility]
screen_reader_hints = true
announce_focus_changes = true
Diagnostic Commands
Test Display Capabilities
# Color test
printf "\e[38;2;255;0;0mTrue Color Red\e[0m\n"
# Unicode test
echo "╔═══════╗"
echo "║ Test ║"
echo "╚═══════╝"
# Box drawing test
echo "┌─┬─┐"
echo "├─┼─┤"
echo "└─┴─┘"
Debug Mode
# Start with display debugging
two-face --debug-display
# Log display operations
TF_LOG_DISPLAY=1 two-face
See Also
Connection Issues
Solving network problems, authentication failures, and disconnections.
Connection Modes
Two-Face supports two connection modes:
| Mode | Use When | Port | Requires |
|---|---|---|---|
| Lich | Using Lich scripts | 8000 (default) | Lich running |
| Direct | Standalone | 7910 | Account credentials |
Quick Diagnosis
# Test Lich connection
nc -zv 127.0.0.1 8000
# Test direct connection
nc -zv eaccess.play.net 7910
# Check DNS
nslookup eaccess.play.net
# Full connectivity test
curl -v https://eaccess.play.net:7910 2>&1 | head -20
Lich Mode Issues
“Connection Refused”
Symptom:
Error: Connection refused to 127.0.0.1:8000
Causes:
- Lich not running
- Lich not listening on expected port
- Firewall blocking localhost
Solutions:
-
Start Lich first:
# Start Lich (method varies) ruby lich.rb # Or use your Lich launcher -
Check Lich is listening:
# Linux/macOS lsof -i :8000 # Windows netstat -an | findstr 8000 -
Verify port in Lich settings:
- Check Lich configuration for proxy port
- Match in Two-Face:
[connection] mode = "lich" port = 8000 # Match Lich's setting
Can’t Connect After Lich Starts
Symptom: Lich running but Two-Face can’t connect
Causes:
- Lich hasn’t initialized proxy yet
- Wrong interface binding
Solutions:
-
Wait for Lich startup:
[connection] connect_delay = 3 # seconds to wait -
Check Lich’s listen address:
# If Lich bound to specific interface [connection] host = "127.0.0.1" # Must match Lich
Disconnects When Lich Reloads
Symptom: Connection drops when Lich scripts reload
Solution:
[connection]
auto_reconnect = true
reconnect_delay = 2
Direct Mode Issues
Authentication Failed
Symptom:
Error: Authentication failed: invalid credentials
Causes:
- Wrong account name
- Wrong password
- Special characters in password
- Account locked/expired
Solutions:
-
Verify credentials:
- Test via web login first
- Check account status
-
Handle special characters:
# Quote password with special chars two-face --direct --account NAME --password 'P@ss!word' -
Use environment variables (more secure):
export TF_ACCOUNT="myaccount" export TF_PASSWORD="mypassword" two-face --direct -
Check for account issues:
- Verify subscription is active
- Check for account locks
“Certificate Verification Failed”
Symptom:
Error: Certificate verification failed
Causes:
- Corrupted cached certificate
- Man-in-the-middle (security concern!)
- Server certificate changed
Solutions:
-
Remove and re-download certificate:
rm ~/.two-face/simu.pem two-face --direct ... # Downloads fresh cert -
If error persists:
- Check if you’re behind a corporate proxy
- Verify you’re on a trusted network
- Certificate changes may indicate security issues
-
Manual certificate verification:
openssl s_client -connect eaccess.play.net:7910 -servername eaccess.play.net
“Connection Timed Out”
Symptom:
Error: Connection to eaccess.play.net:7910 timed out
Causes:
- Firewall blocking
- Network issues
- Server maintenance
Solutions:
-
Check basic connectivity:
ping eaccess.play.net traceroute eaccess.play.net -
Verify port access:
nc -zv eaccess.play.net 7910 -
Check firewall:
# Linux - check if port 7910 is blocked sudo iptables -L -n | grep 7910 # Windows - check firewall netsh advfirewall firewall show rule name=all | findstr 7910 -
Increase timeout:
[connection] timeout = 60 # seconds
“Character Not Found”
Symptom:
Error: Character 'Mychar' not found on account
Solutions:
-
Check character name spelling (case-sensitive)
-
Verify character exists on correct game:
two-face --direct --game prime ... # GemStone IV Prime two-face --direct --game plat ... # GemStone IV Platinum two-face --direct --game test ... # Test server -
List available characters:
two-face --direct --list-characters
Connection Drops
Random Disconnects
Symptom: Connection drops unexpectedly during play
Causes:
- Network instability
- Idle timeout
- Server-side disconnection
Solutions:
-
Enable auto-reconnect:
[connection] auto_reconnect = true reconnect_delay = 2 reconnect_attempts = 5 -
Send keepalives:
[connection] keepalive = true keepalive_interval = 30 # seconds -
Check for network issues:
# Monitor connection ping -i 5 gs4.play.net
Disconnects During High Activity
Symptom: Drops during combat or busy areas
Causes:
- Buffer overflow
- Processing can’t keep up
- Network congestion
Solutions:
-
Increase buffers:
[network] receive_buffer = 65536 send_buffer = 32768 -
Enable compression (if supported):
[network] compression = true
“Connection Reset by Peer”
Symptom:
Error: Connection reset by peer
Causes:
- Server closed connection
- Network equipment reset
- Rate limiting
Solutions:
-
Check for rapid reconnects (may be rate limited):
[connection] reconnect_delay = 5 # Don't reconnect too fast -
Review recent actions:
- Excessive commands?
- Script flooding?
Firewall Issues
Corporate/School Network
Symptom: Works at home, fails at work/school
Solutions:
-
Check if port 7910 allowed:
- Direct mode uses port 7910 (non-standard)
- May be blocked by corporate firewalls
-
Use Lich as intermediary:
- If Lich works (different protocol), use Lich mode
-
Request firewall exception:
- Port 7910 TCP outbound to eaccess.play.net
VPN Issues
Symptom: Connection fails when VPN active
Solutions:
-
Check VPN split tunneling:
- Gaming traffic might be routed through VPN
- Configure VPN to exclude game servers
-
DNS through VPN:
# Check DNS resolution nslookup eaccess.play.net -
Try without VPN to confirm VPN is the issue
Proxy Configuration
HTTP Proxy
[network]
http_proxy = "http://proxy.example.com:8080"
SOCKS Proxy
[network]
socks_proxy = "socks5://localhost:1080"
No Proxy for Game Servers
[network]
no_proxy = "eaccess.play.net,gs4.play.net"
SSL/TLS Issues
“SSL Handshake Failed”
Symptom:
Error: SSL handshake failed
Causes:
- TLS version mismatch
- Cipher suite incompatibility
- OpenSSL issues
Solutions:
-
Check OpenSSL version:
openssl version # Should be 1.1.x or higher -
Force TLS version (if needed):
[network] tls_min_version = "1.2" -
Rebuild with updated OpenSSL:
# Ensure vcpkg OpenSSL is current vcpkg update vcpkg upgrade openssl
“Certificate Chain” Errors
Symptom: Certificate chain validation errors
Solution:
# Reset certificate cache
rm ~/.two-face/simu.pem
rm ~/.two-face/cert_chain.pem
# Reconnect - will re-download
two-face --direct ...
Debugging Connections
Enable Network Logging
[logging]
level = "debug"
[debug]
log_network = true
log_network_data = false # true for packet dumps (verbose!)
View Network Debug Info
# Start with network debugging
TF_DEBUG_NETWORK=1 two-face
# Log to file for analysis
TF_LOG_FILE=/tmp/tf-network.log two-face --debug
Test Connection Step by Step
# 1. DNS resolution
nslookup eaccess.play.net
# 2. Basic connectivity
ping eaccess.play.net
# 3. Port availability
nc -zv eaccess.play.net 7910
# 4. TLS connection
openssl s_client -connect eaccess.play.net:7910
# 5. Full connection test
two-face --direct --test-connection
Quick Reference
Connection Checklist
- Correct mode (lich vs direct)
- Lich running (if lich mode)
- Credentials correct (if direct mode)
- Port accessible (8000 for lich, 7910 for direct)
- Firewall allows connection
- Certificate valid
- Network stable
Emergency Recovery
# Reset all connection state
rm ~/.two-face/simu.pem # Certificate cache
rm ~/.two-face/session.dat # Session cache
# Test with minimal config
two-face --default-config --direct --account NAME --password PASS
See Also
Appendices
Supplementary reference material and additional resources.
Contents
Glossary
Terms and definitions used throughout Two-Face documentation. Covers technical terms, abbreviations, and concepts from A to Z.
XML Protocol Reference
Complete reference for GemStone IV’s XML game protocol. Documents all tags, attributes, and data structures sent by the game server.
Color Codes
Comprehensive color reference including:
- Hex color format
- Named colors
- RGB values
- Two-Face presets
- 256-color palette
- Terminal compatibility
Unicode Symbols
Useful Unicode characters for layouts:
- Box drawing characters
- Progress bar elements
- Directional arrows
- Status indicators
- Decorative symbols
Acknowledgments
Recognition of:
- Projects that inspired Two-Face
- Open source libraries used
- Community contributors
- Standards and references
Purpose
These appendices provide:
- Quick Lookup - Find definitions and codes fast
- Deep Reference - Detailed technical specifications
- Design Resources - Characters and colors for customization
- Attribution - Credit to those who made Two-Face possible
Using the Appendices
For Users
- Glossary: When you encounter unfamiliar terms
- Color Codes: When creating custom themes
- Unicode Symbols: When designing layouts
For Developers
- XML Protocol: When extending the parser
- Glossary: For consistent terminology
- Acknowledgments: Understanding dependencies
See Also
- Reference Guide - Configuration reference
- Troubleshooting - Problem solving
- API Documentation - Programmatic interface
Glossary
Terms and definitions used throughout Two-Face documentation.
A
Aho-Corasick
A string-matching algorithm that can match multiple patterns simultaneously. Used by Two-Face for efficient highlight pattern matching when fast_parse = true.
ASCII
American Standard Code for Information Interchange. A character encoding standard. Two-Face can use ASCII-only mode for maximum terminal compatibility.
Auto-reconnect
Feature that automatically reconnects to the game server if the connection is lost.
Auto-scroll
Widget behavior that automatically scrolls to show the newest content as it arrives.
B
Browser
In Two-Face, a popup window that can be opened for detailed interaction with game data. Examples: highlight editor, spell list browser.
Buffer
Memory area for temporarily storing data. Two-Face uses buffers for incoming game data, scrollback history, and command history.
C
Casttime
Delay after casting a spell before you can act again. Displayed by countdown widgets.
Character Code
Unique identifier for a character in the game’s authentication system.
Command Input
Widget type for entering commands to send to the game.
Command List (cmdlist)
Configuration that maps context menu options to game objects based on noun matching.
Compass
Widget displaying available movement directions in the current room.
Config
Short for configuration. TOML files controlling Two-Face behavior.
Core Layer
The middle layer of Two-Face architecture, containing business logic and widget management.
D
Dashboard
Widget type that combines multiple status elements into a single display.
Data Layer
The foundation layer of Two-Face architecture, containing game state and parsing.
Data Source
Configuration path identifying where a widget gets its data (e.g., vitals.health).
Direct Mode
Connection method that authenticates directly with eAccess servers, bypassing Lich.
E
eAccess
Simutronics authentication server for GemStone IV and DragonRealms.
Effect
Active spell, buff, or debuff affecting your character. Tracked by active_effects widget.
F
Fast Parse
Optimization mode using Aho-Corasick algorithm for pattern matching instead of regex.
Focus
The currently selected/active widget. Input goes to the focused widget.
Frontend Layer
The presentation layer of Two-Face architecture, handling rendering and input.
G
Game State
Central data structure containing all parsed game information.
Generation
Version number that increments when data changes. Used for efficient change detection.
GemStone IV
Text-based MMORPG that Two-Face is designed for.
Glob
Pattern matching syntax using wildcards (* and ?).
Glyph
Visual representation of a character in a font.
H
Hash Key
In eAccess authentication, a 32-byte key used to obfuscate the password.
Hex Color
Color specified using hexadecimal notation (e.g., #FF0000 for red).
Highlight
Pattern-based text styling that colors matching text in game output.
Hook
In some contexts, a callback function. Not currently used in Two-Face.
I
Indicator
Widget showing boolean status (on/off states like hidden, stunned, prone).
Injury Doll
Widget displaying injuries to different body parts.
Input Widget
Widget that accepts text input from the user.
K
Keybind
Association between a key combination and an action or macro.
Keepalive
Network message sent periodically to prevent connection timeout.
L
Layout
Configuration defining widget positions, sizes, and arrangement.
Lazy Render
Optimization that only redraws changed portions of the screen.
Lich
Ruby-based proxy and scripting platform for GemStone IV.
Lich Mode
Connection method that connects through Lich proxy.
M
Macro
Command or sequence of commands triggered by a keybind.
Mana
Magical energy resource. Tracked by vitals.mana.
Monospace
Font where every character has the same width. Required for terminal UI.
MUD
Multi-User Dungeon. Text-based multiplayer games. GemStone IV is technically a MUD.
N
Nerd Font
Font family including many additional symbols and icons.
Node
Individual XML element in the game protocol.
O
OWASP
Open Web Application Security Project. Security standards referenced in development.
Overlay
Widget that draws on top of other widgets.
P
Palette
Set of colors available for use in themes.
ParsedElement
Internal representation of processed game data.
Parser
Component that interprets XML game protocol into structured data.
Pattern
Regular expression or literal string for matching text.
Percentage Positioning
Widget placement using percentages instead of fixed coordinates.
PEM
Privacy-Enhanced Mail. Certificate file format used for TLS.
Popup
See Browser.
Preset
Named color with associated hex value (e.g., “health” → “#00ff00”).
Progress Bar
Widget showing a value as a filled bar (health, mana, etc.).
Protocol
Rules for communication between client and server.
Proxy
Intermediary that relays connections (Lich acts as a proxy).
R
Reconnect
Re-establishing connection after disconnection.
Regex
Regular expression. Pattern matching syntax for text.
Render
Drawing the UI to the screen.
Render Rate
How many times per second the UI updates (FPS).
Room
In-game location. Room data includes name, description, exits.
Roundtime
Delay after an action before you can act again.
RT
Short for Roundtime.
S
Scrollback
History of text that can be scrolled back to view.
Session
Connection session with the game server.
SNI
Server Name Indication. TLS extension disabled for eAccess compatibility.
Spirit
Spiritual energy resource. Tracked by vitals.spirit.
SSL
Secure Sockets Layer. Legacy term, now TLS.
Stamina
Physical energy resource. Tracked by vitals.stamina.
Stream
Category of game output (main, room, combat, speech, etc.).
Sync
Process of updating widgets to match current game state.
T
Tab
In tabbed_text widget, a separate pane within the widget.
Tabbed Text
Widget type with multiple tabs, each showing different content.
Terminal
Text-based interface environment.
Theme
Collection of color definitions for consistent appearance.
Ticket
Authentication token for game server connection.
TLS
Transport Layer Security. Encryption for network connections.
TOML
Tom’s Obvious, Minimal Language. Configuration file format.
Trigger
Automation that executes commands based on game output patterns.
True Color
24-bit color (16.7 million colors). Also called “truecolor”.
TTS
Text-to-Speech. Audio output of game text.
U
Unicode
Character encoding standard supporting international characters and symbols.
Update
Change to game state that triggers widget refresh.
V
Vitals
Character health statistics (health, mana, stamina, spirit).
Viewport
Visible portion of scrollable content.
W
Widget
UI component displaying game information or accepting input.
Widget Manager
Core component managing widget lifecycle and updates.
X
XML
eXtensible Markup Language. Format of game protocol.
Z
Z-Index
Layer order for overlapping widgets. Higher values display on top.
See Also
- Architecture - System design
- Configuration - Settings reference
- Widgets - Widget types
XML Protocol Reference
Complete reference for GemStone IV’s XML game protocol.
Protocol Overview
GemStone IV sends game data as a mix of plain text and XML tags. The XML tags provide structured information about game state, while plain text contains narrative content.
Example Stream
<prompt time="1699900000">></prompt>
<pushStream id="room"/>
<compDef id='room desc'/>
[Town Square Central]
<popStream/>
This is the center of town.
<compass>
<dir value="n"/>
<dir value="e"/>
<dir value="s"/>
<dir value="w"/>
</compass>
Core Tags
<prompt>
Indicates command prompt, includes server timestamp.
<prompt time="1699900000">></prompt>
| Attribute | Description |
|---|---|
time | Unix timestamp from server |
<pushStream> / <popStream>
Redirects content to a named stream.
<pushStream id="combat"/>
You attack the troll!
<popStream/>
Common stream IDs:
main- Primary game outputroom- Room descriptionscombat- Combat messagesspeech- Character speechwhisper- Private messagesthoughts- Mental communicationsdeath- Death messagesexperience- Experience gainslogons- Login/logout noticesatmosphere- Ambient messages
<clearStream>
Clears a stream’s content.
<clearStream id="room"/>
<component>
Named data component for widgets.
<component id="room objs">a troll</component>
<component id="room players">Playername</component>
<component id="room exits">Obvious paths: north, south</component>
Common component IDs:
room objs- Objects in roomroom players- Players in roomroom exits- Exits descriptionroom desc- Room descriptionroom name- Room title
<compDef>
Component definition (marks component location).
<compDef id='room desc'/>
Vitals Tags
Health/Mana/Stamina/Spirit
<progressBar id="health" value="95"/>
<progressBar id="mana" value="100"/>
<progressBar id="stamina" value="87"/>
<progressBar id="spirit" value="100"/>
| Attribute | Description |
|---|---|
id | Vital type |
value | 0-100 percentage |
Experience
<progressBar id="encumbrance" value="50"/>
<progressBar id="mindState" value="5"/>
Encumbrance Levels
| Value | Description |
|---|---|
| 0 | None |
| 1-20 | Light |
| 21-40 | Moderate |
| 41-60 | Heavy |
| 61-80 | Very Heavy |
| 81-100 | Encumbered |
Timing Tags
<roundTime>
Sets roundtime value.
<roundTime value="1699900005"/>
| Attribute | Description |
|---|---|
value | Unix timestamp when RT ends |
<castTime>
Sets casttime value.
<castTime value="1699900003"/>
Navigation Tags
<compass>
Navigation directions available.
<compass>
<dir value="n"/>
<dir value="ne"/>
<dir value="e"/>
<dir value="se"/>
<dir value="s"/>
<dir value="sw"/>
<dir value="w"/>
<dir value="nw"/>
<dir value="up"/>
<dir value="down"/>
<dir value="out"/>
</compass>
Direction values:
n,ne,e,se,s,sw,w,nw- Cardinal/ordinalup,down,out- Vertical/exit
Status Tags
<indicator>
Boolean status indicator.
<indicator id="IconHIDDEN" visible="y"/>
<indicator id="IconSTUNNED" visible="n"/>
| Attribute | Description |
|---|---|
id | Indicator identifier |
visible | “y” or “n” |
Common indicators:
IconHIDDEN- Hidden statusIconSTUNNED- StunnedIconBLEEDING- BleedingIconPOISONED- PoisonedIconDISEASED- DiseasedIconKNEELING- KneelingIconPRONE- Lying downIconSITTING- SittingIconSTANDING- StandingIconJOINED- In a groupIconDEAD- Dead
<left> / <right>
Hand contents.
<left>a steel sword</left>
<right>a wooden shield</right>
Empty hands show as:
<left>Empty</left>
<spell>
Currently prepared spell.
<spell>Fire Spirit</spell>
Combat Tags
<a> (Anchor/Link)
Clickable links in game text.
<a exist="123456" noun="troll">troll</a>
| Attribute | Description |
|---|---|
exist | Object existence ID |
noun | Base noun for commands |
Target Information
<component id="target">a massive troll</component>
Spell/Effect Tags
Active Spells
Spell list updates:
<pushStream id="spells"/>
<clearStream id="spells"/>
<spell>Spirit Warding I (101)</spell> - 30 min remaining
<spell>Spirit Defense (103)</spell> - 25 min remaining
<popStream/>
Active Effects
<dialogData id="ActiveSpells">
<progressBar id="spell_101" value="90" text="Spirit Warding I"/>
</dialogData>
Inventory Tags
Container Contents
<inv id="1234">
<item>a steel sword</item>
<item>3 silver coins</item>
</inv>
Equipment
<pushStream id="inv"/>
Equipment:
armor: some full leather
weapon: a steel broadsword
<popStream/>
Injury Tags
Body Part Injuries
<component id="injury_head">0</component>
<component id="injury_neck">0</component>
<component id="injury_chest">1</component>
<component id="injury_back">0</component>
<component id="injury_leftArm">2</component>
<component id="injury_rightArm">0</component>
<component id="injury_leftHand">0</component>
<component id="injury_rightHand">0</component>
<component id="injury_abdomen">0</component>
<component id="injury_leftLeg">0</component>
<component id="injury_rightLeg">0</component>
<component id="injury_leftFoot">0</component>
<component id="injury_rightFoot">0</component>
<component id="injury_leftEye">0</component>
<component id="injury_rightEye">0</component>
<component id="injury_nsys">0</component>
Injury values:
0- No injury1- Minor injury2- Moderate injury3- Severe injury
Style Tags
<preset>
Applies named color preset.
<preset id="speech">Someone says, "Hello"</preset>
<preset id="whisper">Someone whispers, "Hello"</preset>
Common presets:
speech- Character speechwhisper- Whispersthought- Mental communicationroomName- Room titleroomDesc- Room descriptionbold- Bold text
<b> / <i> / <u>
Basic text formatting.
<b>Bold text</b>
<i>Italic text</i>
<u>Underlined text</u>
<d cmd="...>
Command suggestion (clickable).
<d cmd="look troll">look at the troll</d>
Control Tags
<mode>
Client mode setting.
<mode id="GAME"/>
<app>
Application character information.
<app char="Charactername" game="GS4" />
<output>
Output control.
<output class="mono"/>
Special Tags
<resource>
External resource reference.
<resource picture="12345"/>
<style>
Inline style application.
<style id="roomName"/>
<nav>
Navigation region.
<nav rm="12345"/>
Parsing Considerations
Entity Encoding
Special characters are XML-encoded:
>→><→<&→&"→"'→'
Nested Tags
Tags can nest:
<pushStream id="combat">
<preset id="thought">You sense <a exist="123" noun="creature">the creature</a></preset>
<popStream/>
Unclosed Tags
Some tags are self-closing or have implicit closure:
<prompt time="123">></prompt>
<dir value="n"/>
<progressBar id="health" value="95"/>
Implementation Notes
Stream Management
- Push/pop streams form a stack
- Multiple streams can be active
- Text goes to all active streams
- Clear affects only named stream
Component Updates
- Components update atomically
- Same ID replaces previous value
- Clear stream clears related components
Timing Precision
- Timestamps are Unix seconds
- Client should convert to local time
- Roundtime countdown based on server time
See Also
- Parser Protocol - Parsing implementation
- Parsed Elements - Internal representation
- Stream IDs - Stream reference
Color Codes Reference
Complete reference for colors in Two-Face.
Color Formats
Two-Face supports multiple color format specifications:
Hex Colors
6-digit or 3-digit hexadecimal:
color = "#ff0000" # Red (6-digit)
color = "#f00" # Red (3-digit shorthand)
color = "#FF0000" # Case insensitive
RGB Values
color = "rgb(255, 0, 0)" # Red
color = "rgb(0, 128, 255)" # Blue
Named Colors
Standard web colors:
color = "red"
color = "darkblue"
color = "forestgreen"
Preset Names
Two-Face specific presets:
color = "health" # Health bar color
color = "mana" # Mana bar color
color = "speech" # Speech text color
Standard Named Colors
Basic Colors
| Name | Hex | Preview |
|---|---|---|
black | #000000 | ████ |
white | #ffffff | ████ |
red | #ff0000 | ████ |
green | #00ff00 | ████ |
blue | #0000ff | ████ |
yellow | #ffff00 | ████ |
cyan | #00ffff | ████ |
magenta | #ff00ff | ████ |
Extended Colors
| Name | Hex | Preview |
|---|---|---|
orange | #ffa500 | ████ |
pink | #ffc0cb | ████ |
purple | #800080 | ████ |
brown | #a52a2a | ████ |
gray / grey | #808080 | ████ |
gold | #ffd700 | ████ |
silver | #c0c0c0 | ████ |
navy | #000080 | ████ |
teal | #008080 | ████ |
olive | #808000 | ████ |
maroon | #800000 | ████ |
lime | #00ff00 | ████ |
aqua | #00ffff | ████ |
fuchsia | #ff00ff | ████ |
Dark Variants
| Name | Hex |
|---|---|
darkred | #8b0000 |
darkgreen | #006400 |
darkblue | #00008b |
darkcyan | #008b8b |
darkmagenta | #8b008b |
darkyellow / darkgoldenrod | #b8860b |
darkgray / darkgrey | #a9a9a9 |
darkorange | #ff8c00 |
darkviolet | #9400d3 |
Light Variants
| Name | Hex |
|---|---|
lightred / lightcoral | #f08080 |
lightgreen | #90ee90 |
lightblue | #add8e6 |
lightcyan | #e0ffff |
lightpink | #ffb6c1 |
lightyellow | #ffffe0 |
lightgray / lightgrey | #d3d3d3 |
lightsalmon | #ffa07a |
lightseagreen | #20b2aa |
Bright Variants (Terminal)
| Name | Hex |
|---|---|
bright_black | #555555 |
bright_red | #ff5555 |
bright_green | #55ff55 |
bright_yellow | #ffff55 |
bright_blue | #5555ff |
bright_magenta | #ff55ff |
bright_cyan | #55ffff |
bright_white | #ffffff |
256-Color Palette
Standard Colors (0-15)
0 Black 8 Bright Black
1 Red 9 Bright Red
2 Green 10 Bright Green
3 Yellow 11 Bright Yellow
4 Blue 12 Bright Blue
5 Magenta 13 Bright Magenta
6 Cyan 14 Bright Cyan
7 White 15 Bright White
Color Cube (16-231)
6x6x6 color cube:
- R: 0, 95, 135, 175, 215, 255
- G: 0, 95, 135, 175, 215, 255
- B: 0, 95, 135, 175, 215, 255
Index = 16 + 36×r + 6×g + b (where r,g,b are 0-5)
Grayscale (232-255)
24 shades from dark to light:
| Range | Description |
|---|---|
| 232-235 | Near black |
| 236-243 | Dark gray |
| 244-251 | Light gray |
| 252-255 | Near white |
Two-Face Presets
Vitals Presets
| Preset | Default | Usage |
|---|---|---|
health | #00ff00 | Health bar (full) |
health_low | #ffff00 | Health bar (low) |
health_critical | #ff0000 | Health bar (critical) |
mana | #0080ff | Mana bar |
stamina | #ff8000 | Stamina bar |
spirit | #ff00ff | Spirit bar |
Stream Presets
| Preset | Default | Usage |
|---|---|---|
main | #ffffff | Main game text |
room | #ffff00 | Room descriptions |
combat | #ff4444 | Combat messages |
speech | #00ffff | Character speech |
whisper | #ff00ff | Private messages |
thoughts | #00ff00 | Mental communication |
UI Presets
| Preset | Default | Usage |
|---|---|---|
background | #000000 | Window background |
text | #c0c0c0 | Default text |
text_dim | #808080 | Dimmed text |
border | #404040 | Widget borders |
border_focused | #ffffff | Focused widget border |
Status Presets
| Preset | Default | Usage |
|---|---|---|
hidden | #00ff00 | Hidden indicator |
invisible | #00ffff | Invisible indicator |
stunned | #ffff00 | Stunned indicator |
webbed | #ff00ff | Webbed indicator |
prone | #00ffff | Prone indicator |
kneeling | #ff8000 | Kneeling indicator |
sitting | #808080 | Sitting indicator |
dead | #ff0000 | Dead indicator |
Color Modes
True Color (24-bit)
16.7 million colors:
[display]
color_mode = "truecolor"
256 Colors
256-color palette:
[display]
color_mode = "256"
Colors are mapped to nearest palette entry.
16 Colors
Basic terminal colors:
[display]
color_mode = "16"
No Color
Monochrome:
[display]
color_mode = "none"
Color Math
Brightness/Luminance
Calculate perceived brightness:
L = 0.299×R + 0.587×G + 0.114×B
Contrast Ratio
For accessibility:
Ratio = (L1 + 0.05) / (L2 + 0.05)
WCAG guidelines:
- 4.5:1 minimum for normal text
- 3:1 minimum for large text
- 7:1 enhanced contrast
Color Mixing
Linear interpolation:
mixed = color1 × (1-t) + color2 × t
Accessibility Colors
High Contrast Pairs
| Background | Foreground | Ratio |
|---|---|---|
#000000 | #ffffff | 21:1 |
#000000 | #ffff00 | 19.6:1 |
#000000 | #00ffff | 16.7:1 |
#000000 | #00ff00 | 15.3:1 |
#1a1a1a | #ffffff | 16.9:1 |
#1a1a1a | #e0e0e0 | 12.6:1 |
Color Blindness Friendly
Avoid relying solely on red/green distinction:
# Instead of red/green for health
[theme]
health = "#00ff00"
health_critical = "#ff00ff" # Magenta instead of red
# Or use brightness difference
health = "#80ff80" # Bright green
health_critical = "#800000" # Dark red (brightness contrast)
Terminal Compatibility
ANSI Escape Sequences
16 colors:
\e[30m - \e[37m # Foreground colors
\e[40m - \e[47m # Background colors
\e[90m - \e[97m # Bright foreground
\e[100m - \e[107m # Bright background
256 colors:
\e[38;5;Nm # Foreground (N = 0-255)
\e[48;5;Nm # Background
True color:
\e[38;2;R;G;Bm # Foreground (R,G,B = 0-255)
\e[48;2;R;G;Bm # Background
Terminal Detection
# Check color support
echo $TERM
echo $COLORTERM
tput colors
See Also
Unicode Symbols Reference
Useful Unicode characters for Two-Face layouts and customization.
Box Drawing Characters
Light Box Drawing
| Char | Code | Name |
|---|---|---|
| ─ | U+2500 | Light horizontal |
| │ | U+2502 | Light vertical |
| ┌ | U+250C | Light down and right |
| ┐ | U+2510 | Light down and left |
| └ | U+2514 | Light up and right |
| ┘ | U+2518 | Light up and left |
| ├ | U+251C | Light vertical and right |
| ┤ | U+2524 | Light vertical and left |
| ┬ | U+252C | Light down and horizontal |
| ┴ | U+2534 | Light up and horizontal |
| ┼ | U+253C | Light vertical and horizontal |
Heavy Box Drawing
| Char | Code | Name |
|---|---|---|
| ━ | U+2501 | Heavy horizontal |
| ┃ | U+2503 | Heavy vertical |
| ┏ | U+250F | Heavy down and right |
| ┓ | U+2513 | Heavy down and left |
| ┗ | U+2517 | Heavy up and right |
| ┛ | U+251B | Heavy up and left |
| ┣ | U+2523 | Heavy vertical and right |
| ┫ | U+252B | Heavy vertical and left |
| ┳ | U+2533 | Heavy down and horizontal |
| ┻ | U+253B | Heavy up and horizontal |
| ╋ | U+254B | Heavy vertical and horizontal |
Double Box Drawing
| Char | Code | Name |
|---|---|---|
| ═ | U+2550 | Double horizontal |
| ║ | U+2551 | Double vertical |
| ╔ | U+2554 | Double down and right |
| ╗ | U+2557 | Double down and left |
| ╚ | U+255A | Double up and right |
| ╝ | U+255D | Double up and left |
| ╠ | U+2560 | Double vertical and right |
| ╣ | U+2563 | Double vertical and left |
| ╦ | U+2566 | Double down and horizontal |
| ╩ | U+2569 | Double up and horizontal |
| ╬ | U+256C | Double vertical and horizontal |
Rounded Corners
| Char | Code | Name |
|---|---|---|
| ╭ | U+256D | Light arc down and right |
| ╮ | U+256E | Light arc down and left |
| ╯ | U+256F | Light arc up and left |
| ╰ | U+2570 | Light arc up and right |
Progress Bar Characters
Block Elements
| Char | Code | Name | Use |
|---|---|---|---|
| █ | U+2588 | Full block | 100% filled |
| ▉ | U+2589 | 7/8 block | ~87% |
| ▊ | U+258A | 3/4 block | 75% |
| ▋ | U+258B | 5/8 block | ~62% |
| ▌ | U+258C | 1/2 block | 50% |
| ▍ | U+258D | 3/8 block | ~37% |
| ▎ | U+258E | 1/4 block | 25% |
| ▏ | U+258F | 1/8 block | ~12% |
| ░ | U+2591 | Light shade | Empty |
| ▒ | U+2592 | Medium shade | Partial |
| ▓ | U+2593 | Dark shade | Mostly filled |
Vertical Bars
| Char | Code | Name |
|---|---|---|
| ▁ | U+2581 | Lower 1/8 |
| ▂ | U+2582 | Lower 1/4 |
| ▃ | U+2583 | Lower 3/8 |
| ▄ | U+2584 | Lower half |
| ▅ | U+2585 | Lower 5/8 |
| ▆ | U+2586 | Lower 3/4 |
| ▇ | U+2587 | Lower 7/8 |
Directional Symbols
Arrows
| Char | Code | Name | Direction |
|---|---|---|---|
| ↑ | U+2191 | Upwards arrow | North |
| ↗ | U+2197 | North east arrow | Northeast |
| → | U+2192 | Rightwards arrow | East |
| ↘ | U+2198 | South east arrow | Southeast |
| ↓ | U+2193 | Downwards arrow | South |
| ↙ | U+2199 | South west arrow | Southwest |
| ← | U+2190 | Leftwards arrow | West |
| ↖ | U+2196 | North west arrow | Northwest |
| ↕ | U+2195 | Up down arrow | Up/Down |
| ↔ | U+2194 | Left right arrow | In/Out |
Triangle Arrows
| Char | Code | Name |
|---|---|---|
| ▲ | U+25B2 | Black up-pointing triangle |
| ▶ | U+25B6 | Black right-pointing triangle |
| ▼ | U+25BC | Black down-pointing triangle |
| ◀ | U+25C0 | Black left-pointing triangle |
| △ | U+25B3 | White up-pointing triangle |
| ▷ | U+25B7 | White right-pointing triangle |
| ▽ | U+25BD | White down-pointing triangle |
| ◁ | U+25C1 | White left-pointing triangle |
Compass Rose
N
NW↑NE
W ←•→ E
SW↓SE
S
Using Unicode:
↑
↖ ↗
← ● →
↙ ↘
↓
Status Indicators
Circles
| Char | Code | Name | Use |
|---|---|---|---|
| ● | U+25CF | Black circle | Active/On |
| ○ | U+25CB | White circle | Inactive/Off |
| ◉ | U+25C9 | Fisheye | Selected |
| ◎ | U+25CE | Bullseye | Target |
| ◌ | U+25CC | Dotted circle | Empty |
| ⦿ | U+29BF | Circled bullet | Available |
Squares
| Char | Code | Name | Use |
|---|---|---|---|
| ■ | U+25A0 | Black square | Filled |
| □ | U+25A1 | White square | Empty |
| ▪ | U+25AA | Black small square | Bullet |
| ▫ | U+25AB | White small square | Placeholder |
Check Marks
| Char | Code | Name | Use |
|---|---|---|---|
| ✓ | U+2713 | Check mark | Success |
| ✗ | U+2717 | Ballot X | Failure |
| ✔ | U+2714 | Heavy check mark | Confirmed |
| ✘ | U+2718 | Heavy ballot X | Denied |
| ☑ | U+2611 | Checkbox checked | Option on |
| ☐ | U+2610 | Checkbox unchecked | Option off |
Stars and Decorations
Stars
| Char | Code | Name |
|---|---|---|
| ★ | U+2605 | Black star |
| ☆ | U+2606 | White star |
| ✦ | U+2726 | Black four pointed star |
| ✧ | U+2727 | White four pointed star |
| ⁂ | U+2042 | Asterism |
Bullets and Points
| Char | Code | Name |
|---|---|---|
| • | U+2022 | Bullet |
| ◦ | U+25E6 | White bullet |
| ‣ | U+2023 | Triangular bullet |
| ⁃ | U+2043 | Hyphen bullet |
| ⁌ | U+204C | Black leftwards bullet |
| ⁍ | U+204D | Black rightwards bullet |
Decorative Lines
| Char | Code | Name |
|---|---|---|
| ─ | U+2500 | Horizontal line |
| ═ | U+2550 | Double horizontal |
| ━ | U+2501 | Heavy horizontal |
| ┄ | U+2504 | Triple dash horizontal |
| ┅ | U+2505 | Heavy triple dash |
| ┈ | U+2508 | Quadruple dash |
| ⋯ | U+22EF | Midline horizontal ellipsis |
Game-Related Symbols
Combat
| Char | Code | Name | Use |
|---|---|---|---|
| ⚔ | U+2694 | Crossed swords | Combat |
| 🗡 | U+1F5E1 | Dagger | Weapon |
| ⛨ | U+26E8 | Shield | Defense |
| ☠ | U+2620 | Skull and crossbones | Death |
| ⚡ | U+26A1 | High voltage | Spell/Magic |
Status
| Char | Code | Name | Use |
|---|---|---|---|
| ♥ | U+2665 | Heart | Health |
| ♦ | U+2666 | Diamond | Mana |
| ⚗ | U+2697 | Alembic | Alchemy |
| ☀ | U+2600 | Sun | Day |
| ☾ | U+263E | Last quarter moon | Night |
Navigation
| Char | Code | Name | Use |
|---|---|---|---|
| 🏠 | U+1F3E0 | House | Home |
| 🚪 | U+1F6AA | Door | Exit |
| ⬆ | U+2B06 | Upward arrow | Up |
| ⬇ | U+2B07 | Downward arrow | Down |
| 🧭 | U+1F9ED | Compass | Navigation |
Typography
Quotation Marks
| Char | Code | Name |
|---|---|---|
| “ “ | U+201C/D | Curly double quotes |
| ’ ’ | U+2018/9 | Curly single quotes |
| „ | U+201E | Double low-9 quote |
| « » | U+00AB/BB | Guillemets |
Punctuation
| Char | Code | Name |
|---|---|---|
| … | U+2026 | Ellipsis |
| — | U+2014 | Em dash |
| – | U+2013 | En dash |
| · | U+00B7 | Middle dot |
| † | U+2020 | Dagger |
| ‡ | U+2021 | Double dagger |
Mathematical Symbols
Common
| Char | Code | Name |
|---|---|---|
| × | U+00D7 | Multiplication |
| ÷ | U+00F7 | Division |
| ± | U+00B1 | Plus-minus |
| ≈ | U+2248 | Almost equal |
| ≠ | U+2260 | Not equal |
| ≤ | U+2264 | Less or equal |
| ≥ | U+2265 | Greater or equal |
| ∞ | U+221E | Infinity |
Usage Examples
Progress Bar
[theme.progress]
filled = "█"
empty = "░"
partial = ["▏", "▎", "▍", "▌", "▋", "▊", "▉"]
Widget Borders
[theme.borders]
style = "rounded"
chars = "╭─╮│ │╰─╯"
Status Line
♥ 95/100 ♦ 100/100 ⚡ Ready
Compass Display
↑
↖ • ↗
← N →
↙ • ↘
↓
Font Requirements
To display all Unicode symbols:
Recommended Fonts
- JetBrains Mono - Excellent coverage
- Cascadia Code - Good coverage
- Fira Code - Good coverage
- Nerd Fonts - Best coverage (patched fonts)
Checking Font Coverage
# List available characters in font (Linux)
fc-match --format='%{family}\n' :charset=2500
# Check specific character
printf '\u2500' && echo " - Should show horizontal line"
See Also
- Display Issues - Character problems
- Creating Themes - Using symbols
- Platform Issues - Font setup
Acknowledgments
Recognition of projects, people, and resources that made Two-Face possible.
Inspiration
Profanity
Two-Face draws significant inspiration from Profanity, the pioneering terminal-based client for GemStone IV. Profanity demonstrated that a modern, efficient terminal interface was not only possible but could provide a superior gameplay experience.
Lich
Lich has been instrumental to the GemStone IV community, providing scripting capabilities and serving as an excellent proxy for frontend development. Two-Face’s Lich mode exists thanks to Lich’s well-designed proxy interface.
StormFront and Wizard
The official Simutronics clients (StormFront, Wizard Front End) established the standard for GemStone IV interfaces and defined the XML protocol that Two-Face parses.
Open Source Libraries
Two-Face is built on excellent open source foundations:
Core
- Rust - The programming language providing safety and performance
- Tokio - Asynchronous runtime for non-blocking I/O
- Ratatui - Terminal user interface library
Parsing
- quick-xml - Fast XML parser
- regex - Regular expression engine
- aho-corasick - Multi-pattern string matching
Configuration
- toml - TOML parser and serializer
- serde - Serialization framework
- dirs - Platform-specific directories
Network
Audio
Documentation
This documentation is built with:
- mdBook - The book-from-markdown tool
- GitHub Pages - Hosting
Community
GemStone IV Players
The GemStone IV community has been invaluable for:
- Feature suggestions
- Bug reports
- Beta testing
- Protocol documentation
- General encouragement
Contributors
Thank you to everyone who has contributed code, documentation, bug reports, or feedback. See the Contributors page for a complete list.
Simutronics
GemStone IV is developed and operated by Simutronics Corporation. Two-Face is an unofficial third-party client developed by fans.
Disclaimer: Two-Face is not affiliated with, endorsed by, or connected to Simutronics Corporation. GemStone IV and all related trademarks are property of Simutronics.
Standards and References
Two-Face implementation references:
- ANSI Escape Codes - Terminal control sequences
- VT100 - Terminal emulation reference
- Unicode - Character encoding standards
- WCAG - Web Content Accessibility Guidelines
Special Thanks
- The Rust community for excellent tooling and documentation
- Terminal emulator developers for maintaining standards
- Everyone who plays GemStone IV and keeps the community alive
- Open source maintainers everywhere
License
Two-Face is open source software. See the LICENSE file for details.
“Standing on the shoulders of giants.”
Contributing
Want to be acknowledged? Contribute to Two-Face! All contributions, from code to documentation to bug reports, are valued.
See Also
API Documentation
Programmatic interface documentation for Two-Face.
Rust API Documentation
Two-Face’s internal API is documented using Rust’s built-in documentation system.
Viewing Rustdoc
Generate and view the API documentation:
# Generate documentation
cargo doc --no-deps
# Open in browser
cargo doc --no-deps --open
# Include private items
cargo doc --no-deps --document-private-items
Online Documentation
When available, API documentation is hosted at:
- Stable: docs.rs/two-face (when published)
- Development: Generated from source
Module Overview
Two-Face is organized into three main layers:
Data Layer (src/data/)
Core data structures and game state.
#![allow(unused)]
fn main() {
// Key types
pub struct GameState { ... }
pub struct ParsedElement { ... }
pub enum StreamId { ... }
pub struct Vitals { ... }
}
Key modules:
data::game_state- Central state managementdata::parsed- Parser output typesdata::vitals- Health/mana/staminadata::room- Room information
Core Layer (src/core/)
Business logic and widget management.
#![allow(unused)]
fn main() {
// Key types
pub struct WidgetManager { ... }
pub struct AppState { ... }
pub trait Widget { ... }
}
Key modules:
core::widget_manager- Widget lifecyclecore::app_state- Application statecore::sync- State synchronizationcore::layout- Layout management
Frontend Layer (src/frontend/)
User interface and rendering.
#![allow(unused)]
fn main() {
// Key types
pub struct TuiApp { ... }
pub struct InputHandler { ... }
}
Key modules:
frontend::tui- Terminal UIfrontend::tui::input- Input handlingfrontend::tui::render- Rendering
Key Traits
Widget Trait
All widgets implement the core widget trait:
#![allow(unused)]
fn main() {
pub trait Widget {
/// Widget name/identifier
fn name(&self) -> &str;
/// Widget type
fn widget_type(&self) -> WidgetType;
/// Render the widget to a frame region
fn render(&self, frame: &mut Frame, area: Rect);
/// Handle input events
fn handle_input(&mut self, event: KeyEvent) -> Option<WidgetAction>;
/// Update from game state
fn update(&mut self, state: &GameState) -> bool;
}
}
Browser Trait
Popup windows implement:
#![allow(unused)]
fn main() {
pub trait Browser {
/// Browser title
fn title(&self) -> &str;
/// Render browser content
fn render(&self, frame: &mut Frame, area: Rect);
/// Handle input
fn handle_input(&mut self, event: KeyEvent) -> BrowserAction;
/// Initialize with game state
fn init(&mut self, state: &GameState);
}
}
Configuration API
Loading Configuration
#![allow(unused)]
fn main() {
use two_face::config::{Config, Layout, Theme};
// Load from default location
let config = Config::load()?;
// Load from specific path
let config = Config::from_file("path/to/config.toml")?;
// Access settings
let port = config.connection.port;
let render_rate = config.performance.render_rate;
}
Layout Configuration
#![allow(unused)]
fn main() {
use two_face::config::Layout;
let layout = Layout::load("layout.toml")?;
for widget_config in layout.widgets {
println!("Widget: {} at ({}, {})",
widget_config.name,
widget_config.x,
widget_config.y);
}
}
Parser API
Parsing Game Data
#![allow(unused)]
fn main() {
use two_face::parser::Parser;
let mut parser = Parser::new();
// Feed data from game
let elements = parser.parse(raw_data)?;
// Process parsed elements
for element in elements {
match element {
ParsedElement::Text(text) => { /* ... */ }
ParsedElement::Prompt(time) => { /* ... */ }
ParsedElement::Vitals(vitals) => { /* ... */ }
// ... other variants
}
}
}
ParsedElement Variants
#![allow(unused)]
fn main() {
pub enum ParsedElement {
Text(String),
Prompt(u64),
StreamPush(StreamId),
StreamPop,
StreamClear(StreamId),
Vitals(VitalUpdate),
Roundtime(u64),
Casttime(u64),
Compass(Vec<Direction>),
Indicator(String, bool),
Component(String, String),
// ... and more
}
}
Network API
Lich Connection
#![allow(unused)]
fn main() {
use two_face::network::LichConnection;
let conn = LichConnection::new("127.0.0.1", 8000)?;
conn.connect()?;
// Send command
conn.send("look")?;
// Receive data
let data = conn.receive()?;
}
Direct Connection
#![allow(unused)]
fn main() {
use two_face::network::DirectConnection;
let conn = DirectConnection::new(
"account",
"password",
"prime",
"character_name"
)?;
conn.authenticate()?;
conn.connect()?;
}
Event System
Input Events
#![allow(unused)]
fn main() {
use two_face::events::{Event, KeyEvent};
loop {
match event_receiver.recv()? {
Event::Key(key_event) => {
// Handle keyboard input
}
Event::Mouse(mouse_event) => {
// Handle mouse input
}
Event::Resize(w, h) => {
// Handle terminal resize
}
Event::GameData(data) => {
// Handle incoming game data
}
}
}
}
Widget Actions
#![allow(unused)]
fn main() {
pub enum WidgetAction {
None,
SendCommand(String),
OpenBrowser(BrowserType),
CloseBrowser,
FocusWidget(String),
Scroll(ScrollDirection),
Quit,
}
}
Building Extensions
Adding a Widget Type
#![allow(unused)]
fn main() {
use two_face::widgets::{Widget, WidgetType};
pub struct CustomWidget {
name: String,
// Custom fields
}
impl Widget for CustomWidget {
fn name(&self) -> &str {
&self.name
}
fn widget_type(&self) -> WidgetType {
WidgetType::Custom
}
fn render(&self, frame: &mut Frame, area: Rect) {
// Render implementation
}
fn handle_input(&mut self, event: KeyEvent) -> Option<WidgetAction> {
// Input handling
None
}
fn update(&mut self, state: &GameState) -> bool {
// Update from state, return true if changed
false
}
}
}
Adding a Parser Extension
#![allow(unused)]
fn main() {
use two_face::parser::{Parser, ParsedElement};
impl Parser {
pub fn handle_custom_tag(&mut self, tag: &str, attrs: &[Attribute])
-> Option<ParsedElement>
{
if tag == "customtag" {
// Parse custom tag
Some(ParsedElement::Custom(/* ... */))
} else {
None
}
}
}
}
Error Handling
Two-Face uses the anyhow crate for error handling:
#![allow(unused)]
fn main() {
use anyhow::{Result, Context};
fn load_config() -> Result<Config> {
let content = std::fs::read_to_string("config.toml")
.context("Failed to read config file")?;
let config: Config = toml::from_str(&content)
.context("Failed to parse config")?;
Ok(config)
}
}
Testing
Unit Tests
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parser_basic() {
let mut parser = Parser::new();
let result = parser.parse(b"<prompt>></prompt>").unwrap();
assert!(matches!(result[0], ParsedElement::Prompt(_)));
}
}
}
Integration Tests
#![allow(unused)]
fn main() {
// tests/integration_test.rs
use two_face::*;
#[test]
fn test_full_workflow() {
// Setup
let config = Config::default();
let mut app = App::new(config);
// Simulate game data
app.process_data(test_data());
// Verify state
assert!(app.game_state().vitals().health > 0);
}
}
Version Compatibility
API stability follows semantic versioning:
- 0.x.y: API may change between minor versions
- 1.x.y: Breaking changes only in major versions
See Also
- Project Structure - Code organization
- Adding Widgets - Widget development
- Parser Extensions - Parser development
- Contributing - Contribution guide