Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

VellumFE

Play GemStone IV in a client you can shape around your character — and be hunting in about five minutes.

Why you’d use it

You already know the game. What you want is a screen that tells you what you need mid-swing: vitals where your eye already goes, spells and effects visible without a SPELL check, thoughts and combat in their own windows instead of scrolling past you in one column.

VellumFE gives you that on one core with five ways to play — a terminal client, a native desktop window, your phone’s browser, an Android app, and an iPhone app. The same character, the same highlights, the same settings, whether you’re at your desk or on a couch.

And you configure all of it inside the client. Every feature ships an editor. Hand-editing a config file is a thing you may do, not a thing you have to do.

📷 screenshot pending
The Desktop GUI mid-hunt: main text left, vitals and roundtime along the bottom, active spells and the room's creatures docked right.

Get playing

Pick how you want to start. All three paths reach the same client.

  1. Download the release for your platform and extract it — see Installation.
  2. Double-click vellum-fe (vellum-fe.exe on Windows). Running it with no arguments opens the graphical launcher.
  3. Click ➕ New connection. Choose Lich if you launch through Lich (fill in host and port), or Direct to reach the game without Lich (fill in account, game world, and character).
  4. Tick Save password if you want it remembered — it goes into your OS credential store, never into a file.
  5. Click Save, then Launch.

Saved connections run the Desktop GUI unless you change Frontend under the connection’s Advanced fold.

Expected result: a native VellumFE window opens, connects, and your character’s room description scrolls into the main text window.

📷 screenshot pending
The launcher's connection list, with one Lich connection and one Direct connection, each showing its character and game world.

What you get

  • Windows you place. Drag any window where you want it, size it, and it stays there. Add windows for thoughts, deaths, ESP, or any game stream you care about — see Text Windows.
  • Highlights that do more than color. A rule can recolor a line, play a sound, rumble your controller, replace the text, send the line to a different window, or hide it outright. See Set up highlights and sound alerts.
  • Connect your way. Through Lich as a detachable client, or directly to play.net with no Lich running.
  • 36 built-in themes, ten of them accessibility variants — high-contrast (light and dark), deuteranopia, protanopia, tritanopia, monochrome, low-blue-light, photophobia, ADHD-focus, and reduced-motion.
  • Speech built in. Windows can read new lines aloud, so a thought or a death reaches you without a glance. See Speech.
  • Rebindable keys, hotbars of command buttons, and gamepad support.
  • Graphics on the desktop — skins, per-window frames, an injury doll, hand icons — with the terminal and phone rendering the same information as text.

Where your settings live

Everything lands under ~/.vellum-fe. Two layers matter:

  • global/ — settings shared by every character: highlights, keybinds, colors, hotbars, skins and images.
  • profiles/<character>/ — that one character’s overrides of any of the above, plus their layout, history, and log.

Point the whole thing somewhere else with --data-dir or the VELLUM_FE_DIR environment variable — handy for a portable install on a stick.

You change all of this in-app: type .settings for the settings editor, or .help for every command. The full list is in the Command Reference.

Where to go next

  • Getting Started — download, launcher, first connection.
  • How-To Guides — build a hunting layout, make vitals flash when you’re hurt, wire a combat hotbar.
  • Frontends — what each of the five can and can’t do.
  • Widgets — every window type, one page each.

Getting help

Config reference (TOML)

You do not need this to play — every file below has an in-app editor. It’s here for troubleshooting and for people who like to read their config.

Base directory: ~/.vellum-fe, overridden by --data-dir <DIR> or the VELLUM_FE_DIR environment variable.

PathHolds
launcher.tomlSaved launcher connections. Never contains passwords.
global/config.tomlShared settings: connection, UI, sound, speech, web server
global/highlights.tomlShared highlight, sound, squelch and redirect rules
global/keybinds.tomlShared key bindings
global/colors.tomlShared color palette, stream presets, spell colors
global/hotbars.tomlShared hotbar button definitions
global/controller.tomlGamepad binds, wheels, rumble, tuning (base layer)
global/macros.tomlMacro buttons for the phone/web client (a character’s own copy overrides it)
global/skins/One folder per skin, each with a skin.toml manifest
global/images/Shared art pool: icons, frames, dolls, compass, backgrounds, statusicons, hands
profiles/<character>/config.tomlThat character’s overrides
profiles/<character>/highlights.tomlThat character’s own highlight rules
profiles/<character>/keybinds.tomlThat character’s own key bindings
profiles/<character>/controller.tomlThat character’s gamepad overrides
profiles/<character>/layout.tomlAuto-saved layout for that character
profiles/<character>/history.txtCommand history
profiles/<character>/debug.logThat session’s log — the first place to look when something breaks
layouts/Named layouts from .savelayout, shared by all characters
highlights/Named highlight sets from .savehighlights
keybinds/Named keybind profiles
themes/Custom themes as <name>.toml, alongside the 36 built in

Files are written atomically, and the previous version is kept alongside as <name>.toml.bak — so a bad edit is one rename away from being undone.

Getting Started

Three short pages take you from a downloaded archive to a character standing in Wehnimer’s Landing.

What this section does for you

You have the game and you probably have Lich. What you don’t have yet is this client on your machine, pointed at your character. That’s the whole job of this section, and it’s three steps:

  1. Installation — get the right archive for your platform (or the Android APK), extract it, and know where the binary should live. Building from source is here too, if you’d rather.
  2. The Launcher — the double-click path. Save a connection per character, keep the password in your OS credential store, and start a session with one click. Launch several to play several characters at once.
  3. First Launch — the command-line path, choosing a frontend, and a tour of what’s on screen once you’re connected.

Read them in order the first time. After that, the Launcher page is the only one you’ll come back to.

Before you start

  • A way into the game. Either Lich running as a detachable client, or your play.net account details for a direct connection. VellumFE does both; you don’t have to pick now.
  • For the terminal frontend, a terminal with 256-color or true-color support. Windows Terminal, iTerm2, Kitty, Alacritty and modern GNOME Terminal all qualify.
  • For the desktop GUI, nothing extra — it’s a native window.
  • For Android, version 8.0 or newer. On Android 8 and 9, update Chrome first: it provides the app’s rendering engine there.

Get connected

If you’d rather not read three pages first, this is the short version.

  1. Extract the download and double-click vellum-fe. With no arguments it opens the graphical launcher.
  2. Click ➕ New connection, pick Lich or Direct, and fill in the connection details for your character.
  3. Click Save, then Launch.

Connections run the Desktop GUI by default; switch Frontend under Advanced to get a terminal session instead.

Expected result: a VellumFE window opens, connects, and your room description appears in the main text window.

📷 screenshot pending
The launcher's New connection form in Direct mode, showing the account, game world, character and Save password controls.

Once you’re in

Two commands are worth knowing on day one:

  • .help lists every dot-command, grouped by what it does.
  • .settings opens the settings editor. In the Desktop GUI you can also use the Settings button in the top toolbar, which stays open while you change things.

⚠️ .quit disconnects but leaves the window open — run it again to exit. .exit closes VellumFE outright. Turn off Keep Open On Quit in Settings ▸ UI if you’d rather .quit close immediately.

⚠️ Ctrl+C means different things in different frontends. In the terminal it copies your selected text. In the Desktop GUI it quits. To leave a terminal session, use .quit or .exit.

Then what

See also

  • Introduction — what VellumFE is and where its config lives
  • The Launcher — saved connections and keyring passwords in detail
  • First Launch — every command-line flag, explained

Installing VellumFE

Get a working vellum-fe on your machine, past your operating system’s defenses, in about two minutes.

What it’s for

You want to be in Icemule by tonight, not fighting a download. VellumFE ships as a single self-contained binary per platform — no installer, no runtime to install, no Ruby, no OpenSSL. Windows and macOS releases are code-signed, so your OS should let them run without an argument. This page gets the file onto your disk and proves it works; The Launcher gets you into the game.

📷 screenshot pending
The GitHub Releases page for a beta tag, with the platform assets and SHA256SUMS.txt listed under Assets.

Set it up

Every desktop release lives on the GitHub Releases page. This is a platform split rather than a frontend split — the same binary contains the terminal frontend, the desktop GUI, and the web server, and which one you get is decided at launch, not at download.

  1. Download vellum-fe-windows-x86_64.zip from the latest release.
  2. Right-click the zip and choose Extract All…. Put vellum-fe.exe somewhere permanent — C:\Tools\VellumFE\ works; your Downloads folder does not, because you will move it later and break your shortcuts.
  3. Double-click vellum-fe.exe.

The executable is signed through Azure Trusted Signing as part of the release build, which is what stops Defender from flagging low-prevalence Rust binaries as Trojan:Win32/Cloxer. If SmartScreen still shows a blue “Windows protected your PC” panel on a brand-new release, click More infoRun anyway — reputation builds up per-version over the first days after a release.

Expected result: the VellumFE Launcher window opens with the heading VellumFE and the line Choose a connection to launch. No black console window sits behind it.

Common setups

Verify what you downloaded

Every release publishes SHA256SUMS.txt alongside the assets. Compare before you run anything:

sha256sum -c SHA256SUMS.txt --ignore-missing

On Windows PowerShell, Get-FileHash vellum-fe-windows-x86_64.zip -Algorithm SHA256 prints a hash to match by eye against the line in SHA256SUMS.txt.

→ You see vellum-fe-windows-x86_64.zip: OK (or a matching hash), which means the archive is byte-for-byte the one CI built and signed.

Keep configs somewhere other than your home directory

VellumFE writes everything it owns — settings, layouts, highlights, skins, logs, saved connections — under ~/.vellum-fe/. It is created on first run. To put that elsewhere (a synced folder, a portable drive), point VELLUM_FE_DIR at the location before starting:

VELLUM_FE_DIR=/mnt/sync/vellum vellum-fe

The same directory can also be set per connection, in the Launcher’s Advanced fold under Data directory — handy when one character’s setup should live apart from the rest.

→ Start VellumFE and the folder you named fills with launcher.toml, global/, profiles/, and vellum-fe.log instead of your home directory.

Build it yourself

You need a recent stable Rust toolchain — CI builds on latest stable.

git clone https://github.com/Nisugi/VellumFE.git
cd VellumFE
cargo build --release

The binary lands at target/release/vellum-fe. Direct eAccess login uses your OS’s native TLS stack on Windows (SChannel) and macOS (Security.framework), so there is nothing extra to install; Linux compiles a bundled OpenSSL during the build, which needs Perl — present on effectively every distro.

./target/release/vellum-fe --version prints the version you just built.

Tips & gotchas

⚠️ A bare vellum-fe opens the Launcher; a vellum-fe with any flag does not. The no-arguments case is what double-clicking produces, and it routes to the graphical launcher. The moment you pass even one flag you are on the command-line path, and that path defaults to the terminal frontend, not the GUI — add --frontend gui if you wanted a window. This catches almost everyone once. See The Launcher.

⚠️ Extract the archive before running it. Windows will happily run an exe from inside a zip preview, then fail confusingly when the process tries to spawn a session copy of itself.

  • Nothing prints to your terminal. VellumFE logs to a file, because a terminal UI cannot share stdout with a log. Diagnostics live in ~/.vellum-fe/vellum-fe.log, including panics. Raise the level with RUST_LOG=debug.
  • Moving the binary is fine; moving it after saving connections is fine too. Connections are stored in ~/.vellum-fe/, not next to the exe. The Launcher spawns sessions using its own current path, so a moved binary keeps working as long as you launch the moved copy.
  • The apps keep their own connections; they don’t import your desktop ones. Your launcher.toml stays on the desktop, and the phone saves its logins on the device. The apps can pair with a running desktop session (CharactersScan QR to add), and a browser can act as a second screen without installing anything — two routes for two jobs, both in Put VellumFE on your phone.

See also

Config reference (TOML)

Installation itself writes no TOML — this is where the files it creates live. The base directory is ~/.vellum-fe/, or whatever VELLUM_FE_DIR names.

PathWhat it holds
launcher.tomlSaved connections for the Launcher. Never contains passwords.
ssh-launcher.tomlSSH target and per-character ports for the SSH Launcher. Never contains keys.
vellum-fe.logThe one log file, including panics and backtraces.
global/Shared across characters: highlights.toml, keybinds.toml, colors.toml, hotbars.toml, controller.toml, macros.toml, plus skins/, images/, data/.
profiles/<character>/Per-character config.toml, layout.toml, highlights.toml, keybinds.toml, hotbars.toml, controller.toml, history.txt, widget_state.toml, debug.log.
layouts/Named layouts from .savelayout: TUI as <name>.toml, GUI as <name>.json.
highlights/, keybinds/Named highlight and keybind sets.
themes/Custom themes as <name>.toml.

Environment variables

NameDefaultWhat it does
VELLUM_FE_DIR~/.vellum-feBase directory for every file above. Overridden by --data-dir and by a connection’s Data directory.
RUST_LOGinfoLog level filter for vellum-fe.log. debug for troubleshooting.

Every user-authored file is written atomically: VellumFE writes <name>.tmp, copies the current file to <name>.bak, then renames. If a save ever goes wrong, the previous version is one rename away.

The Launcher

Double-click, click Launch, you’re in — with your password in the OS credential store instead of your shell history.

What it’s for

You play more than one character, and you would rather not retype an eAccess account, a game world, and a port every time. The Launcher keeps each of those as a named connection you click once. Passwords go into Windows Credential Manager, the macOS Keychain, or the Linux secret service — never into a file, never onto a command line where every process on the machine can read it. Each Launch starts a separate session process, so running four characters at once is four clicks.

📷 screenshot pending
The VellumFE Launcher window: the heading VellumFE, the line Choose a connection to launch, and two saved connections, each with Launch, Edit and Delete buttons.

Set it up

The Launcher is a desktop window, so this is a frontend split: it belongs to the desktop GUI, and the terminal and mobile tabs say what you use instead.

  1. Start vellum-fe with no arguments — double-clicking does exactly this — or run vellum-fe --launcher if you want it from a terminal.
  2. Click ➕ New connection. The form is headed New connection.
  3. Fill in Name (this is also what --launch-profile takes), then choose Connection: Direct to reach the game through eAccess with no Lich, or Lich to attach to a detachable-client Lich you are already running.
    • Direct shows Account, Password, Save password, Game, and Character.
    • Lich shows Host, Port, and Character.
  4. Tick Save password if you want it remembered. Leave it unticked and the Launcher asks each time.
  5. Click Save.
  6. Back in the list, click Launch on the row you just made.

Expected result: the row appears with your name in bold and a summary beneath it — Nisugi @ GemStone IV for direct, or Nisugi via Lich @ 127.0.0.1:8000 for Lich. Clicking Launch opens a session window and the status line at the bottom reads Launched <name>.

📷 screenshot pending
The New connection form in Direct mode, showing Account, the masked Password field with its 👁 reveal button, Save password, the Game dropdown and Character.

Common setups

One account, several characters, all at once

Make a connection per character, all pointing at the same Account:

  1. ➕ New connection → Name Nisugi, Direct, Account MYACCT, Password once, Save password ticked, Game GemStone IV, Character Nisugi. Save.
  2. ➕ New connection → Name Alt, same Account MYACCT, leave Password blank, Save password still ticked, Character Alt. Save.
  3. Click Launch on both rows.

The saved password is keyed by account, not by connection, so the second one reuses the first’s stored password without you typing it again. Because each Launch spawns its own process, both characters run side by side with independent layouts.

→ Two session windows are open, each showing its own character, and the Launcher’s status line shows Launched Alt from the last click.

A connection that starts in the terminal instead

The Launcher defaults everything to the desktop GUI. To make one connection open the text interface:

  1. Edit the connection.
  2. Open the Advanced fold.
  3. Set Frontend to Terminal.
  4. While you are there, set Color mode to direct for true-color terminals, and tick PaletteSet up on startup if you use slot mode on a 256-color terminal. These two rows only appear when Frontend is Terminal.
  5. Save, then Launch.

→ A console window titled VellumFE opens running the terminal frontend, and it remembers its size and position the next time you launch that connection.

Serve this session to your phone

In the connection’s Advanced fold:

  1. Tick Web dashboardEnable on port and leave 8484.
  2. Set Bind address to 0.0.0.0 — the hint beside it reads 0.0.0.0 = allow LAN devices. Leaving it at 127.0.0.1 restricts the server to the machine it runs on.
  3. Save, Launch, then browse to http://<your-pc-ip>:8484/play on the phone.

→ The phone shows the live session, driven by the same core the desktop window is rendering.

Tips & gotchas

⚠️ The Launcher defaults to the GUI; a hand-typed command line defaults to the TUI. A new connection is created with Frontend: GUI, but vellum-fe invoked with flags uses --frontend tui unless you say otherwise. The same character launched two ways can land in two different interfaces. Set it explicitly under AdvancedFrontend, or pass --frontend gui.

⚠️ Deleting a connection can delete the saved password with it. The confirmation window Delete profile? says so: the keyring entry is removed unless another connection still uses the same account. Deleting your only MYACCT connection means the next one has to re-enter the password.

  • “Password was NOT stored” in red means the credential store refused the write — common on headless Linux, inside WSL, or under a bare window manager with no secret service running. The connection still saves and still launches; you are asked for the password each time. Install a secret service (GNOME Keyring, KWallet) to fix it.
  • A red line about launcher.toml on startup means the file could not be parsed. The Launcher deliberately shows the error rather than starting with an empty list, because saving from an empty list would overwrite your connections. Fix the file before saving anything.
  • Connection names cannot contain " or %. The Launcher rejects them at save time, because a terminal session’s name travels through a Windows cmd command line where neither can be passed safely.
  • Renaming a connection does not duplicate it. The edit form tracks the name you started with and replaces that entry.
  • The account name is not shown in the list. Each row’s summary is <character> @ <game> on purpose — the list is on screen constantly, and in screenshots. The account stays inside the edit form.
  • Nothing in the list? “No saved connections yet” with Create one to get started is the empty state, not an error.

The other launcher: cold-starting Lich over SSH

There is a second, separate feature with a confusingly similar name. The SSH Launcher does not manage connections — it starts a headless Lich on your home PC from wherever you are, over an existing WireGuard or Tailscale tunnel, then attaches to it. Use it when the machine that runs Lich is not the machine you are sitting at.

It lives inside a running session, not in the Launcher window:

  • Type .launcher (or bare .launch) in the command input to open the SSH Launcher panel.
  • Fill in Host (tunnel address), User, SSH port, Remote OS, optionally Attach host, and the Launch command template — where {character}, {game} and {port} are substituted per character.
  • Click Generate new key, then Copy public key and paste that one line into ~/.ssh/authorized_keys on the home PC. The private half goes to your OS secure store; the indicator changes to ✓ key stored. Expand Harden it (recommended) to see the restrict,command="…" prefix that limits a leaked key to launching the game and nothing else.
  • Add each character under Characters with its game token and its own detachable-client Port, then Save.
  • Run it with .launch <character>.

If a Lich is already listening on that port, the flow skips the SSH step entirely and attaches straight away. If not, it SSHes in, spawns Lich detached so it survives the SSH channel closing, then polls the port. The open port — not the spawn’s exit code — is what counts as success, so a message like “Launched Lich but 100.64.0.5:8001 never opened” means the command template or the character name is wrong, and the spawner’s own output is appended to tell you which. On a first connection the host key’s fingerprint is pinned; if a pinned key ever changes, the launch is refused outright rather than prompting.

.launch Nisugi reports progress in the session and ends attached to the freshly started Lich.

See also

Config reference (TOML)

~/.vellum-fe/launcher.toml — the Launcher’s connections

Written by the Launcher as [[profiles]] entries. No passwords are ever written here.

FieldTypeDefaultWhat it does
namestring(required)Display name and the key --launch-profile takes. Cannot contain " or %.
mode"direct" | "lich"(required)eAccess login, or attach to a running Lich.
accountstring""play.net account (direct only). Keys the saved password.
gamestring"prime"One of prime, platinum, shattered, test, dr, drplatinum, drfallen, drtest.
password_savedboolfalseTrue when a password for account is in the OS credential store.
characterstring""Character to log in as; also selects that character’s settings and layout.
frontend"gui" | "tui""gui"Note the default differs from the CLI’s --frontend tui.
hoststring"127.0.0.1"Lich host (lich mode).
portu168000Lich detachable-client port (lich mode).
custom_launchstringunsetFull Lich launch line; when present, connecting probes the port and SSH-launches Lich if it is down.
web_portu16unsetEnables the embedded web server on this port.
web_bindstringunset (= 127.0.0.1)0.0.0.0 lets other devices on your network connect.
nosoundboolfalseSkip audio device initialization entirely.
settings_profilestringunsetUse this settings folder instead of the character name, so several characters can share one setup.
data_dirstringunset (= ~/.vellum-fe)Per-connection override of the base directory.
color_mode"direct" | "slot"unsetTerminal color rendering (terminal frontend only).
setup_paletteboolfalseRun .setpalette at startup (pairs with slot).

Passwords are stored through the keyring crate under the service id vellum-fe, keyed by the lowercased account name. A just-typed password handed to a spawned session travels in the private VELLUM_FE_PASSWORD environment variable, which the session consumes and removes immediately — never on a command line.

~/.vellum-fe/ssh-launcher.toml — the SSH Launcher

Written by the SSH Launcher panel. No key material is ever written here.

FieldTypeDefaultWhat it does
ssh.hoststring""Tunnel address of the home PC.
ssh.userstring""SSH user on the home PC.
ssh.portu1622SSH port.
ssh.remote_os"windows" | "unix""windows"Chooses the detach mechanism for the spawned process.
ssh.lich_commandstring""Launch template. {character}, {game} and {port} are substituted; quoted paths are split correctly.
ssh.attach_hoststring""Where to attach after launch. Empty falls back to ssh.host.
ssh.key_savedboolfalseTrue when the ed25519 private key is in the OS secure store.
characters.<Name>.gamestring(required)Game token substituted into {game}gemstone for GS4.
characters.<Name>.portu16(required)That character’s detachable-client port. Give each character its own.

The private key is stored under the same vellum-fe keyring service with the account prefix ssh-launcher-key:, so it can never collide with a play.net password. Host keys are pinned on first use to ~/.vellum-fe/ssh-launcher-known-hosts, which is separate from your personal ~/.ssh/known_hosts — VellumFE never touches your own SSH state.

First Launch

Get your character on screen from a command line — through Lich, or straight to play.net — and know what you’re looking at when the room description lands.

What it’s for

You already know how you connect: Lich on a port, or account-and-password to the game. This page is the command-line route for both, so you can put VellumFE behind a Lich launcher entry, a shell alias, or a shortcut and stop thinking about it. The last section is a tour of the screen you land on — which window is which, where you type, how you scroll back to the thing that killed you.

If you’d rather click a saved connection than type flags, use the Launcher; it covers the same ground with stored passwords. Running vellum-fe with no arguments at all opens it.

⚠️ The same character, started two ways, lands in two different interfaces. A hand-typed command line defaults to the terminal UI; a saved Launcher connection defaults to the GUI. Pass --frontend gui (or -f gui) to make the command line match.

Set it up

  1. Start Lich as you always do, and note the port it prints.

  2. Run VellumFE with the GUI frontend pointed at that port:

    vellum-fe --frontend gui --port 8000 --character Rysk
    

    --port is Lich’s listening port; --host defaults to 127.0.0.1, so you only pass it when Lich runs on another machine.

  3. To skip Lich entirely, connect through play.net’s eAccess login instead:

    vellum-fe --frontend gui --direct --account myaccount --character Rysk --game prime
    

    Leave --password off. VellumFE prompts for it in the terminal (Password for account myaccount:) with the characters hidden, so the password never enters your shell history.

📷 screenshot pending
A freshly connected GUI session: the main game feed, the Room window, and the command input along the bottom, with the Windows / Settings / Zones / Editors toolbar hubs above.

Expected result: a desktop window opens, the game text starts scrolling in the main feed, and typing look in the input at the bottom returns the room description.

Common setups

Recipe 1 — Make Lich launch VellumFE for you

In Lich’s launcher, add VellumFE as a custom frontend and set the command to:

path\to\vellum-fe.exe --frontend gui --port %port% --key %key%

Lich substitutes %port% with the port it opened and %key% with the login key it received from the game; --key hands that key straight to the game server, so you are never asked for credentials. Drop --frontend gui from the line if you want the terminal UI.

Outcome: picking your character in Lich opens VellumFE already in the room you logged out in — no password prompt, no port to copy.

Recipe 2 — One set of settings, several characters

--character is your login name. --profile is your config name, and it picks which folder under ~/.vellum-fe/profiles/ holds your layout, highlights, keybinds, and hotbars. When you leave --profile off, it silently copies --character — which is why every new character starts with a blank-slate layout.

Point three characters at one shared config folder:

vellum-fe --port 8000 --character Rysk    --profile hunting
vellum-fe --port 8000 --character Nisugi  --profile hunting
vellum-fe --port 8000 --character Vellum  --profile hunting

Outcome: all three log in under their own names, and all three read and write ~/.vellum-fe/profiles/hunting/ — so a layout you save on one is there when you log in as the next.

Tips & gotchas

⚠️ In the terminal UI, Ctrl+C copies your selection — it does not quit. In the GUI, Ctrl+C quits. To leave either one, type .quit. Note that .quit disconnects but keeps the window open — run it again, or use .exit, to close the client outright.

⚠️ --profile is not --character. Omitting --profile makes your config folder follow your character name. If you rename the flag you pass, you land in a different folder and your layout looks lost. It is not lost — it is under the old profile name in ~/.vellum-fe/profiles/.

⚠️ The DragonRealms world names are spelled differently in two places. On the command line they are hyphenated: dr, dr-platinum, dr-fallen, dr-test. In config.toml and in saved Launcher connections they are not: dr, drplatinum, drfallen, drtest. A misspelled value in config.toml does not error — it falls back to GemStone IV Prime, and you find out at the character list.

The tour — what you’re looking at once you’re in. The default layout places six windows:

WindowWhat lands in it
mainThe game feed — room descriptions, combat, everything unrouted
RoomRoom name, description, and exits, kept current in place
thoughtsThe thoughts stream (ESP)
speechThe speech stream
societyThe society stream
command inputWhere you type, along the bottom

Everything else — vitals bars, a compass, an injury doll, hotbars — is a window you add yourself. In the GUI that’s the Windows button in the top toolbar, which opens a stay-open catalog with a checkbox per window. In the terminal, type .addwindow with no arguments to get a picker. See Widgets.

Where things are. Your command line is the bottom row of the screen; press Enter to send, Up and Down to walk back through history, and Ctrl+R to repeat your last command. Scrolling back is PageUp / PageDown a page at a time and Alt+PageUp / Alt+PageDown a line at a time in the terminal, or the mouse wheel and scrollbar in the GUI. Tab moves focus between windows in the terminal; in the GUI you click the window you want.

Getting help. Anything you type starting with a dot is a client command, not game input. Three worth knowing on day one:

  • .help — the full dot-command list, grouped by section
  • .menu — the main menu tree (the GUI also has toolbar hubs)
  • .settings — the in-app settings editor

Copying text. In the terminal, drag-select and the text is on your clipboard the moment you release. In the GUI, drag-select (double-click for a word, triple for a line) and press Ctrl+C. Copy is plain text in both, deliberately.

Direct mode and TLS. Direct connections use your operating system’s own TLS stack, so Windows and macOS need nothing extra. On Linux, see Installation.

See also

Config reference (TOML)

Everything on this page can live in config.toml instead of the command line. Per-profile config is ~/.vellum-fe/profiles/<profile>/config.toml; shared defaults are ~/.vellum-fe/global/config.toml.

[connection]

FieldTypeDefaultWhat it does
hoststring"127.0.0.1"Address to connect to (Lich’s host)
portinteger8000Port to connect to (Lich’s listening port)
characterstring(unset)Character name, used for Lich proxy selection and direct login
accountstring(unset)play.net account, direct connections only
passwordstring(unset)Stored in plain text. Leave it unset and answer the prompt instead
gamestring"prime"prime, platinum, shattered, test, dr, drplatinum, drfallen, drtest. An unrecognized value falls back to prime

CLI-vs-config precedence. A command-line switch always wins over the file. For each field the order is:

  • host / port--host / --port, then [connection]
  • account--account, then connection.account, then an error
  • password--password, then connection.password, then the hidden terminal prompt
  • character--character, then connection.character, then an error
  • game--game, then connection.game, then prime

A saved Launcher connection applied with --launch-profile <NAME> sits between the two: it fills the same fields these switches would, but any switch you type explicitly alongside it still wins. --launch-profile cannot be combined with --direct, --key, or --launcher.

Other switches this page uses

SwitchDefaultWhat it does
--frontend / -ftuitui, gui, or headless (no local UI; a browser at /play is the interface)
--key(unset)Lich’s %key% login key, passed to the game server
--profilefalls back to --characterWhich folder under profiles/ holds your config
--data-dir~/.vellum-feMoves the whole config tree; equivalent to the VELLUM_FE_DIR environment variable
--config(unset)Read one specific config.toml by path
--nosoundoffSkips audio device initialization entirely
--color-modefrom configdirect (true color), slot (256-color custom palette), indexed (256-color standard)
--setup-paletteoffPrograms the terminal palette at startup; pair with --color-mode slot
--web-port / --web-bindfrom [web]Turns on the embedded web server for phone/browser play
--launcheroffOpens the graphical Launcher — also what a no-argument run does

Subcommands (these run and exit instead of connecting):

  • vellum-fe validate-layout [FILE] — check a layout file, or your current one
  • vellum-fe migrate-layout --src <DIR> [--out <DIR>] [--dry-run] — convert old-format layouts
  • vellum-fe import-highlights <FILE.xml> [--out FILE] [--dry-run] — convert Wrayth/StormFront highlights to TOML

How-To Guides

Complete tasks, start to finish — each one ends with something working on your screen.

The rest of this manual is organized by thing: a page per widget, a page per frontend, a page per config file. That is the right shape when you know what you’re looking for and the wrong shape when you don’t. These guides are organized by goal instead.

Each one walks a single job end to end, and each one crosses features on the way. Building a hunting layout touches the window catalog, five shell zones, three widget types, and a saved layout file — four manual sections you’d otherwise have to find and sequence yourself. The guide sequences them for you.

They teach a path. The feature pages own the facts, so every step links back to the page behind it — when you want the full field list for a widget, follow the link rather than expecting a guide to repeat it.

The guides

Build a hunting layout Reach for this when your default screen is still the six windows it shipped with and you want the screen you’ll actually fight in — vitals, roundtime, targets, and a compass, saved so you can get it back.

Make your health bar shout when you’re hurt Reach for this when you’ve died to a health bar you didn’t look at, and you want your screen to demand attention instead of waiting for it. (Nothing blinks — the guide explains what actually happens instead.)

Set up highlights and sound alerts Reach for this when the line that matters — your bounty creature, a bleeder, someone saying your name — scrolls past in the same color as everything else.

Put VellumFE on your phone Reach for this when you want your character on a second screen beside the keyboard, or in your hand away from the desk.

Wire a hotbar for combat Reach for this when you’re retyping the same attack, stance, and spell-prep sequence every hunt and want them as buttons that gray out while they’re on cooldown.

Before any of them

Every guide assumes you are connected and looking at a session. If you are not there yet, start with Installation and First Launch, then come back.

Each guide is tabbed by frontend — Desktop GUI, Terminal (TUI), Mobile — and the tabs move together across the whole page, so picking your frontend once at the top carries through to the end. Where a frontend genuinely can’t do the task, its tab says so plainly and points at what it does instead.

See also

  • Frontends — all five, and how to choose between them
  • Widgets — the reference for everything a window can hold
  • Customization — layouts, highlights, keybinds, themes, skins
  • Command Reference — every dot-command in one table

Build a hunting layout

By the end you’ll have a combat screen — vitals, roundtime, targets, compass, hands — saved under a name you can load on any character, in either desktop frontend.

What you’ll build

VellumFE ships a deliberately plain screen. The default layout is six windows: main, command_input, thoughts, speech, room, and society. That is a reading screen. This guide turns it into a fighting screen.

You’ll finish with your story text still front and center, a vitals bar and a roundtime timer across the top where you can read them without leaving the text, and a right-hand column carrying targets, a compass, and your hands. Then you’ll save it as hunting, which makes it reproducible — on your next character, after a bad drag, or on the terminal.

📷 screenshot pending
The finished layout: story text filling the center, minivitals and RT in the Header zone, and Targets, Compass, Left Hand, and Right Hand stacked down the Right Bar.

Before you start

  • You’re connected and looking at a session. If not, see First Launch.
  • Two of these windows are GemStone IV only. minivitals and Reserve are gated to GS4 and won’t appear in the catalog on a DragonRealms character. Use the individual Health, Mana, Stamina, and Spirit bars there instead — they’re ungated, and step 2 tells you how.
  • Nothing here needs Lich, and nothing here edits a file by hand.

Steps

1. Turn on the zones you’re going to fill.

Click Zones in the top toolbar and switch on Header and Right Bar. The hub stays open, so you can toggle both and watch the canvas shrink around them. (Typed equivalent: .header on and .rightbar on.)

Expected result: a strip appears across the top of the window and a column down the right side. Both are empty, and your story window has resized to fit between them.

2. Add the windows.

Click Windows in the toolbar. The catalog opens and stays open — you’re going to add six windows without closing it once.

Two things to know before you start ticking. Rows are labeled by the window’s title, not its internal name, so you’re looking for RT, not “roundtime”, and Injuries, not “injury_doll”. And the categories start collapsed — expand a heading before you go hunting for a missing row.

Tick each of these, and set its zone on the same row using the control at the row’s right edge:

Tick this rowSet its zone to
minivitalsHeader
RTHeader
TargetsRight Bar
CompassRight Bar
Left HandRight Bar
Right HandRight Bar

On DragonRealms, there is no minivitals row. Tick Health, Mana, Stamina, and Spirit instead and send all four to Header — you’ll get four separate bars where GS4 gets one combined widget.

The zone control is a submenu rather than a drop-down list, so it opens a nested list beside the row. That’s deliberate: a true combo box counts as a click outside the toolbar menu and would close the whole catalog before you could pick anything.

📷 screenshot pending
The Windows catalog with categories expanded, minivitals and RT ticked, and a row's zone submenu open on Right Bar.

Expected result: six windows appear as you tick them — two in the header strip, four in the right column — and start showing live data immediately. The compass lights the exits of the room you’re standing in.

3. Arrange them.

Click outside the catalog to dismiss it. Now drag each window from anywhere on its body — no modifier, no need to find a title bar. Resize by dragging any edge or corner.

Edges snap: to the zone’s bounds, to the other windows in the same zone, and optionally to a grid, with a guide line drawn at each match. Put minivitals at the left of the header and RT at the right, then stack the right bar in the order you want to read it — Targets on top, Compass under it, the two hands at the bottom.

Hold Shift while you drop to suspend snapping entirely and place a window exactly under the pointer. That’s the escape hatch when a snap keeps pulling the compass flush against an edge you want it away from.

4. Pin the compass so it stops moving.

Right-click the compass ▸ ArrangeAdvanced ▸ tick Fixed size. Do the same for both hand windows.

Fixed size keeps a window’s exact width and height when the app window resizes or you zoom, so only its position adapts. A compass rose and a hands widget both look wrong stretched; the targets list wants to grow. Leave Targets unfixed.

5. Save it.

Type .savelayout hunting in the command input.

There is no button for this. Saving a layout is typed, in the GUI exactly as in the terminal — the catalog says so itself when you have no layouts yet.

Expected result: a confirmation in your story window. .layouts now lists hunting, and .loadlayout hunting rebuilds this screen from scratch at any point later.

Make it yours

If you fight in melee, trade the compass for the injury doll. A caster watches exits and mana; a melee character watches wounds. Untick Compass in the catalog and tick Injuries, sending it to Right Bar. The GUI draws it as a body diagram colored by wound and scar severity, and with a skin that ships doll art you can place the wound dots yourself through Calibrate doll… in the doll window’s right-click section. In the TUI, swap the .addwindow compass line for .addwindow injuries injury_doll 92 15 28 8.

You’ll see: a body diagram that colors a limb the moment you take a wound there, instead of you parsing the wound line out of scrolling text.

If you’re a spellcaster, add the effects trio to a Left Bar. Turn on the left zone (ZonesLeft Bar, or .leftbar on) and send Buffs, Debuffs, and Cooldowns there — three separate catalog rows, each an active-effects window carrying one category. Keeping them apart from the right column means your defensive spell timers never compete with your target list for the same glance.

You’ll see: three stacked lists counting down independently, so a lapsing 401 is visible before it drops rather than after.

If your terminal is small, put your side windows in a tabbed window instead of a column. Four windows down a 40-column right bar leaves nothing for text. Add one tabbedtext window and give it the streams you’d have split out, then switch tabs with a click. In the GUI, the tab list is Edit tabs… in the window’s own right-click section; in the TUI it’s the .editwindow form.

You’ll see: one window’s worth of screen doing four windows’ work, at the cost of only seeing one at a time.

When it doesn’t work

A window you ticked in the catalog doesn’t appear anywhere. Check its zone. If the zone is one you have switched off, the window is real and placed but the strip holding it isn’t drawn. Open Zones and turn that zone on, or move the window with right-click ▸ ArrangeMove toCenter.

You can’t find a row in the catalog. Two causes. The categories start collapsed — click a category heading to expand it before concluding the row is absent. And rows are sorted and labeled by title, so RT files under R, not under “roundtime” — search for what the window is called on screen, not what you’d type.

minivitals or Reserve is genuinely missing. Both are GemStone IV only and are filtered out of the catalog on a DragonRealms character. That’s gating working correctly, not a bug. Use the individual Health / Mana / Stamina / Spirit bars.

.addwindow compass prints a usage line and adds nothing. The command takes either no arguments (which opens the picker) or all six — name, type, x, y, width, and optionally height. A partial command falls through to the usage message. Type .addwindow alone and pick from the list instead.

A .addwindow window landed at the top-left corner in the wrong size. Numbers that fail to parse fall back silently rather than erroring — x and y to 0, width to 40, height to 10. Check for a stray character in the coordinates, delete it with .deletewindow <name>, and retype the line.

Dragging a TUI window’s middle selects text instead of moving it. That’s correct behavior: the TUI moves a window from its title bar only, unlike the GUI, which drags from anywhere. Grab the top row.

A TUI window won’t resize from its bottom edge. If it’s two rows tall or shorter, its bottom row is content, not a handle. Resize from the right column, or set the size in .editwindow.

A window snaps flush against an edge you want it away from. Hold Shift while dropping to suspend snapping for that gesture. If a window keeps drifting back to an edge on later resizes, it has a persistent anchor — right-click ▸ ArrangeRelease Anchors to forget it. The window stays exactly where it is and stops following. If snapping misbehaves in a way you can’t explain, .snapdebug writes a trace to ~/.vellum-fe/vellum-fe.log.

You resized the app window and everything is proportionally wrong. In the GUI, right-click the windows that shouldn’t stretch ▸ ArrangeAdvancedFixed size — a compass and a hands widget want it, a text window doesn’t. In the TUI, run .resize to refit the layout to the terminal’s new dimensions.

You deleted a window and want it back. In the GUI, Windows↩ Restore deleted… returns it with its position, streams, and widget type intact. That button only appears when something is waiting to be restored, so an empty stash means it’s genuinely not there. In the TUI, restore via .loadlayout hunting — which is the strongest argument for saving early and saving often.

⚠️ .deletewindow truly removes a window and .hidewindow only hides it — in both desktop frontends. There is no asymmetry between the GUI and the TUI to remember. Both refuse to remove your only main-feed window, so you cannot strand yourself without game text.

⚠️ In the Terminal (TUI), Ctrl+C copies your selection — it does not quit. In the Desktop GUI the same combination quits. Leave the TUI with .quit or .exit. This is the difference most likely to bite you while you’re moving between frontends mid-build.

See also

Make your health bar shout when you’re hurt

One glance — or one sound — tells you you’re at 30%, without reading a single number.

What you’ll build

A low-health alarm that works when your eyes are on the creature, not on your bars. You’ll wire three things that fire together at 25% health: a hotbar button that flips to red text on a dark red field, an indicator window that lights up, and a sound plus a controller buzz on the game’s own wound messaging. By the end, dropping below a quarter health changes what your screen looks like and what your chair feels like.

⚠️ Nothing in VellumFE blinks or animates a vitals bar. There is no flashing bar, no pulsing fill, no threshold color on the bar itself. The minivitals and progress windows expose Layout, Bar height, Bar text, Depleted color, and which bars show — and that is all. What this guide builds is louder and more reliable than a blink: a color change on a button, an icon that lights, a sound, and a controller rumble. Read the outcome as “your screen shouts,” not “your bar flashes.”

📷 screenshot pending
Health at 22%: the HP hotbar button has flipped to red on dark red, and the LOWHP indicator window is lit beside the mini-vitals bars.

Before you start

  • The visual half of this guide is condition-driven, and only the Desktop GUI can author conditions. The TUI hotbar editor covers a button’s label, command, hotkey, tooltip, category, and countdown — it shows states as a count and tells you to edit them elsewhere. The TUI indicator editor edits id, title, icon, and active/inactive colors, with no conditions. The TUI displays everything you author in the GUI perfectly; it just doesn’t build it. Each tab below says which parts it can do.
  • For the sound step: put an audio file in ~/.vellum-fe/global/sounds/. A bare name like lowhp works — the extension is tried for you.
  • For the rumble step: a connected controller, plus a rumble pattern defined in the Controller editor’s Rumble tab. Skip this step without one; the sound and colors work on their own.

Steps

1 — Put a vitals window on screen (if you don’t have one). Click the Windows button in the top toolbar and tick minivitals. Right-click it to set Bar text to Health: 99% so the number is there when you do want it.

2 — Add the alarm button’s bar. Type .addwindow vitals hotkeybar 0 0 20 3. The window name is the binding — a hotkeybar window shows the bar of the same name, so this window will display a bar called vitals.

3 — Build the bar and its low-health state. Open the Editors hub in the toolbar and click Hotbars (or type .hotbars). Add a bar named vitals, then click Add button. Set:

  • LabelHP
  • Commandhealth

Leave Global (all characters) ticked so every character gets the alarm.

4 — Add the state that changes the button’s appearance. Scroll to States — the note beside it reads (first matching state styles the button). Click Add state. A new state arrives preset to Roundtime active and dim; change it:

  • In the When row, leave the group set to all of.
  • On the condition row, open the kind combo and choose Vital.
  • Set the vital combo to health, the comparison to <, the number to 25, and the unit combo to %.
  • Under Style while active:, set the foreground to #ff4040 and the background to #400000.

Click Save bar.

5 — Add a second, earlier warning above it. Click Add state again, set it to Vital · health · < · 50 · %, and give it a yellow foreground (#ffd040). Now use the ^ button to move the 25% state above the 50% state. Order is everything: the first state whose condition matches wins, so if 50% sits first it will match at 22% too and you’ll never see red. Click Save bar.

6 — Light an indicator on the game’s own wound text. Type .addwindow LOWHP indicator 0 0 6 3. An indicator window’s name is its status id. Then open the Editors hub ▸ Highlights (or type .highlights) and click Add highlight. Fill in:

  • PatternYou are (badly hurt|gravely injured|at death's door)
  • Foreground#ff4040
  • Soundlowhp
  • Rumble — pick your pattern from the combo
  • Set statusLOWHP
  • Status duration20

Tick Global (all characters) and click Save. set_status is the trick that makes any game text drive a visual alert: the id lights every indicator and dashboard cell with that name, riding the same machinery the server’s own status icons use. Status duration clears it after 20 seconds so it doesn’t stick on after you heal.

Expected result: at full health the HP button sits in its normal color and the LOWHP indicator is dark. Drop under 50% and the button turns yellow; drop under 25% and it turns red on dark red. When the game calls you badly hurt, the sound plays, the controller buzzes, and the LOWHP indicator lights for 20 seconds.

Make it yours

Alarm on the wound, not the number. Health percent lags a big hit — you can be at 40% with a rank-3 chest wound and in more danger than someone at 20% clean. Add a state using the Injury condition instead: area chest, comparison >=, level 2. Levels 1-3 are wounds and 4-6 are scars, so >= 2 catches a serious wound and ignores old scars. Put it above your percentage states and it wins whenever it matches.

One alarm for “actually in trouble.” A single low number rarely means death; a low number while stunned does. Add a state, set its group combo to all of, and click + condition twice to build: Vital · health · < · 40 · % and Indicator · stunned · active. Only the combination lights it, so the alarm stays rare enough that you believe it.

Make it an absolute number. Percentages mislead at low levels, where 25% can be nine hit points. Switch the Vital row’s unit combo from % to abs and set the value to a real number of hit points you know is dangerous for your character.

When it doesn’t work

The button turned yellow but never red. Your 50% state is above your 25% state. The first matching state wins, and 22% satisfies “< 50%” perfectly well, so the yellow rule fires and the red one is never consulted. Use the ^ button on the red state’s card to move it above the yellow one, then Save bar. Order the widest condition last, always.

The button never changes at all. Check three things in order: the button has at least one entry under States (an empty state list means nothing can ever match); you clicked Save bar and the “unsaved changes” note is gone; and the hotkeybar window’s name matches the bar’s name exactly. A window named combat will not display a bar named vitals.

The indicator never lights. The indicator window’s name is its status id, and it must match the highlight’s Set status value. .addwindow LOWHP indicator … pairs with Set status LOWHP. Case doesn’t matter — ids are compared case-insensitively — but spelling does.

The indicator lights and never goes out. You left Status duration empty, which means “stay on until something clears it.” Either set a duration in seconds, or add a second highlight rule matching your recovery text with that id in Clear status.

The sound stays silent. The file must be in ~/.vellum-fe/global/sounds/. A bare name works and the extension is tried for you, so lowhp finds lowhp.wav — but a name with a typo finds nothing and fails quietly. Check the folder, and confirm sound is enabled in Settings.

Your states vanished after editing in the TUI. They didn’t. The TUI hotbar editor deliberately round-trips states untouched and displays them as a count, so a TUI save never destroys GUI-authored conditions. If they’re genuinely gone, check whether a per-character bar is shadowing your global one — a character bar with the same name replaces the global bar wholesale rather than merging with it. The editor’s bar list shows [G] for global and [C] for character so you can tell which one you’re editing.

The rumble combo is empty. Rumble patterns come from the Controller editor’s Rumble tab. Define one there first and it appears in the highlight form’s combo.

See also

Make the game shout when something matters

By the end you’ll have a bounty line that turns gold and chimes, a web that lights an on-screen indicator, and the ambient noise you’re tired of reading gone from the feed.

What you’ll build

Three rules, each one a different kind of “tell me”:

  1. Color + soundYou have completed your task prints in gold and plays a chime, so a bounty finishing lands even when you’re reading something else.
  2. A status light — being webbed switches on a custom status id, and an indicator window wired to that id lights up. This is the one worth learning: any line of game text can drive a visual, without the game ever sending a status update for it.
  3. Squelch — the ambient weather chatter never reaches your main window again.

You’ll prove all three with .testline, which feeds a fake game line through the live highlight pipeline, so nothing here depends on finding a mob first.

📷 screenshot pending
The main window with a gold bounty line, a lit WEBBED indicator beside the vitals, and no weather chatter in the feed.

Before you start

  • For the sound rule only: an audio file in ~/.vellum-fe/global/sounds/. VellumFE creates that folder and seeds it on first run. Drop a .wav or .mp3 in and it appears in the editor’s Sound dropdown. The examples below use chime.wav — substitute the name of a file you actually have.

  • Coming from Wrayth or StormFront? Convert your existing rules first instead of retyping them:

    vellum-fe import-highlights 70682.xml
    

    This runs and exits without connecting. It writes 70682-highlights.toml beside the source file; add --dry-run to see what it would produce, or --out <FILE> to choose the destination.

Steps

Rule 1 — a bounty that shows and chimes

  1. Click Editors in the top toolbar, then Highlights. The hub stays open, so you can keep working alongside it. (Or type .highlights in the command input.)
  2. Click Add highlight.
  3. Fill in the form:
    • Namebounty-done
    • PatternYou have completed your task
    • Foregroundgold
    • Sound — pick chime.wav from the dropdown
    • Tick Entire line so the whole line colors, not the matched words
  4. Click Save.
📷 screenshot pending
The highlight form with Name, Pattern, Foreground and Sound filled in for the bounty rule.

Rule 2 — webbed lights an indicator

  1. You need somewhere for the light to live. Type .addwindow webbed indicator 60 2 12 1 to place a one-row indicator window — an indicator window’s name is its status id, so this one answers to webbed.
  2. Back in the highlights editor, click Add highlight again:
    • Namewebbed-on
    • PatternYou are entangled in a web
    • Set statusWEBBED
    • Status duration — leave empty so it stays lit until cleared
  3. Save, then add the matching off-switch:
    • Namewebbed-off
    • PatternYou tear through the last of the webbing
    • Clear statusWEBBED
  4. Save.

Rule 3 — hide the weather

  1. Add highlight once more:
    • Namequiet-weather
    • PatternA gentle breeze blows through
    • Tick Squelch
  2. Save.

Prove all three without waiting for a mob

  1. Type each of these into the command input:

    .testline You have completed your task
    .testline You are entangled in a web
    .testline A gentle breeze blows through
    

Expected result: the first line prints in gold and the chime plays. The second lights your WEBBED indicator. The third produces nothing at all — the feed does not move, which is the squelch working.

Make it yours

Send lines somewhere else instead of hiding them. Squelch is absolute — the line is gone. If you’d rather keep it but move it out of the way, set Redirect to to another window’s name. The mode selector decides what happens to the original: redirect only removes it from the main feed, redirect copies leaves it in place and duplicates it. A rule with pattern asks you to and Redirect to thoughts puts merchant chatter in your thoughts window while your hunting feed stays clean.

Make a status clear itself. The webbed rule above stays lit until the matching Clear status rule fires — right if you have a reliable “you’re free” line, wrong if you don’t and it sticks on forever. Put 30 in Status duration instead and the status switches itself off after thirty seconds whether or not a clear rule ever matches. The two approaches combine: whichever happens first wins.

Rewrite noisy text rather than removing it. Replace rewrites the matched text in place and understands $1-style capture groups. A rule with pattern (\w+) just bit you! and Replace >>> $1 BIT YOU <<< keeps the information and makes it impossible to skim past. Set Window to scope the rewrite to one window while the colors still apply everywhere.

Buzz the controller. If you play with a gamepad, the Rumble dropdown fires a named rumble pattern on match — a low-health rule you feel rather than read. Patterns come from the Controller editor’s Rumble tab.

When it doesn’t work

The rule never fires. Confirm with .testline before blaming the rule: .testline injects your text verbatim into the live pipeline, so if the rule doesn’t fire there, the pattern is wrong. The most common cause is a regex metacharacter taken literally — (, ), [, ], ., ?, * and + all have meaning. To match a literal (2/3) you need \(2/3\). If your pattern is plain text with no regex in it at all, tick Fast parse — it uses literal matching, which is both faster and immune to this problem.

Colors don’t apply but the sound plays (or the reverse). The [highlights] section has four independent global switches, all on by default: sounds_enabled, replace_enabled, redirect_enabled and coloring_enabled. One being off disables that entire category across every rule while the others keep working. Check them in Settings before rewriting a rule that was fine all along.

No sound at all. Three things in order: the file must be in ~/.vellum-fe/global/sounds/; [sound] enabled must be true; and the master [sound] volume (default 0.7) multiplies against each rule’s Sound volume, so a low master value quietly mutes everything. On the phone, also check the sheet’s own Sound alerts toggle.

One sound machine-guns, or a repeated line only chimes once. Both are cooldown_ms (default 500). It’s a minimum gap between repeats of the same sound — a pattern that matches ten lines in a burst plays once, not ten times. The cooldown is tracked per sound, so two different rules with two different files never suppress each other. Raise it if a spammy pattern still chatters; lower it if you’re genuinely missing fast repeats.

The indicator doesn’t light. The id in Set status must match the indicator window’s id — matching ignores case, so webbed and WEBBED are the same id, but web is not. Confirm the window is actually on screen; a status firing at a hidden window looks identical to a rule that never matched.

A dashboard adds unknown ids for you. If your Set status id doesn’t exist yet, a dashboard widget appends it and lights it. An indicator window does not — it only flips an id it already has. So a dashboard is the faster way to see a new status working before you commit to placing a dedicated indicator for it.

Your rules vanished when you switched characters. Rules save per-profile by default. Tick Global (all characters) in the GUI form, or use the (global) list on the phone, for rules that should follow you everywhere.

See also

Play VellumFE on your phone

By the end you’ll have your character in your hand — either a real client you can log in from anywhere, or a second screen on the couch beside your PC.

What you’ll build

Two different things, and you probably want both eventually:

  • The app — a client that logs into GemStone IV on its own. Sit on the bus, log in, play. It can also attach to your desk session when you’d rather pick the game up where you left it.
  • The browser — a second screen. Nothing to install: point a phone, tablet or spare laptop at your PC on the same network and it mirrors the session you’re already playing, with your keyboard still in charge.

Neither one is the “proper” way. They do different jobs.

📷 screenshot pending
The same character in the native app (left) and in a tablet browser beside the desktop client (right).

Before you start

  • For the app: iOS is a beta — via TestFlight; Android is (in progress). See iOS App and Android App for how to get the current build.
  • For the browser, and for pairing the app with your desk session: the phone and the PC must be on the same network, and VellumFE’s web server must be running and bound so LAN devices can reach it. That’s Step 1 of the browser tab below.
  • Read this once before either route: pairing tokens keep strangers out, but the traffic itself is plain HTTP. Use this on a network you trust — your home Wi-Fi, not a café’s. To play from outside the house, put both devices on a Tailscale or WireGuard network and connect over that. Never forward this port to the open internet. .webinfo reprints this warning every time you run it, on purpose.

Steps

Serve the session, then pair a device with it.

  1. In the Launcher, open your connection’s Edit form and expand Advanced.
  2. Under Web dashboard, tick Enable on port and set a port — 8040 is a fine choice.
  3. Set Bind address to 0.0.0.0. The hint under the field reads 0.0.0.0 = allow LAN devices, which is exactly the difference: the default 127.0.0.1 serves this PC only, and a phone will never reach it.
  4. Save and launch the connection.
📷 screenshot pending
Launcher Advanced with Enable on port ticked and Bind address set to 0.0.0.0, showing the 0.0.0.0 = allow LAN devices hint.
  1. Once connected, type .webinfo in the command input. It prints two lines and then opens a pairing page in your browser:

    Web session URL (browser): http://192.168.1.21:8040/#token=…
    VellumFE app link: vellum://remote?host=192.168.1.21&port=8040&token=…&name=Rysk
    
  2. The pairing page shows two QR codes, and they are not interchangeable. The browser QR carries the http:// URL; the app QR carries the vellum:// deep link. Scan the browser one with your phone’s camera to open the browser client. Scan the app one from inside the app’s Scan QR to add screen to save the session as a character. Scanning the wrong code does nothing — no error, no result, which is a confusing thirty seconds if you don’t know to check.

📷 screenshot pending
The .webinfo pairing page with the browser QR and the app QR side by side, each under its own heading.

Expected result: the browser QR opens a working session on the phone within a couple of seconds; the app QR adds a named row to the app’s Characters list, showing 192.168.1.21:8040 with a live dot.

Make it yours

Set the phone up as a glance-only second screen. Leave your desktop client running and open the browser URL on a tablet propped beside the keyboard. You keep typing on the PC; the tablet shows the feed, the status drawer and your group’s condition without ever stealing focus. Nothing to install and nothing to uninstall when you’re done.

Author on the couch. The phone edits far more than people expect: macros, touch-wheel slices, highlight rules including redirects and squelch, colors, controller binds, and the entire desktop settings registry over the wire — written to the hosting machine’s config exactly as if you’d typed it there. See Make the game shout when something matters for the phone path through the highlight editor.

Know what stays at the desk. The phone cannot author layout or panel placement, window resizing, game keybinds, or macro hidden_when conditions. Its chrome is fixed by design, so windows are not yours to move there. Build the layout on the desktop; the phone renders what you built.

When it doesn’t work

.webinfo says the web server is disabled. The message is Web server is disabled. Enable [web] in config.toml or pass --web-port. The web server is off, not broken. Tick Enable on port in the Launcher’s Advanced section, or relaunch with --web-port 8040.

.webinfo says the web server is not running. A different message — Web server is not running (bind failed or still starting) — and a different cause. The server was asked to start but has no bound port. Usually another program already holds that port; pick a different one. If you launched a moment ago, give it a second and run .webinfo again.

The URL says 127.0.0.1 and the phone can’t load it. That address means “this PC only”, and no other device can ever reach it. VellumFE tells you so directly, printing Note: bind = "127.0.0.1" is this PC only. Set [web] bind = "0.0.0.0" so phones on your LAN can connect. Change Bind address to 0.0.0.0 and restart the session — the bind is fixed at startup.

The QR scans but nothing happens. You almost certainly scanned the wrong one of the two. The camera app handles the browser code (http://…); the app’s Scan QR to add screen handles the app code (vellum://remote?…). Crossed over, both fail silently.

The page loads but never connects. Phone and PC must be on the same network. Phones drop to mobile data when Wi-Fi looks unreliable, which silently ends the LAN connection. Check the phone is really on your Wi-Fi, and that the PC’s firewall isn’t blocking the port you chose.

You want to play from outside the house. Don’t forward the port. Put both devices on a Tailscale or WireGuard network and use the address that network gives your PC — everything else on this page works unchanged, and the traffic is encrypted by the tunnel.

See also

  • Browser Client — the browser client in full: its two modes, the settings sheet, and everything the phone can author
  • iOS App — TestFlight, the Characters picker, and backgrounding
  • Android App — installing the in-progress build, the Characters picker, and battery behavior
  • The Launcher — the Advanced section that turns the web server on, and the separate SSH Launcher for cold-starting Lich
  • Make the game shout when something matters — authoring highlight rules from the phone
  • Command Reference.webinfo

Wire a hotbar for combat

One-click attacks, stances, and spell prep — on buttons that dim, recolor, and count down as the fight changes.

What you’ll build

A row of combat buttons that isn’t just a row of buttons. Attack dims itself while roundtime is running and shows the seconds left. Hide goes green and reads “Hidden” once you’re in the shadows. Your 909 dims the moment you can’t afford it, and swaps to a different command while a cast is in flight. Every button also takes a hotkey, so the bar doubles as a set of bindings you can read.

📷 screenshot pending
The combat bar mid-fight: Attack dimmed with "3s" counting down, Hide green and reading "Hidden", 909 dimmed for lack of mana.

Before you start

  • Condition states are authored in the Desktop GUI. The TUI hotbar editor builds bars, buttons, hotkeys, and countdowns, but shows states as a count and leaves them untouched — it renders them faithfully, it just doesn’t edit them. The Mobile tab explains what the phone offers instead.
  • Know the bar-to-window rule before you start: a hotkeybar window displays the bar with the same name as the window. Name them to match or nothing appears.

Steps

1 — Place the bar’s window. Type .addwindow combat hotkeybar 0 0 40 3 in the command input. This creates a hotkeybar window named combat, which will display the bar named combat.

2 — Create the bar. Open the Editors hub in the toolbar and click Hotbars (or type .hotbars). Add a bar named combat and set Title: to Combat. Leave Global (all characters) ticked, or untick it to give this character its own bar. The bar list tags each bar [G] for global and [C] for character so you always know which you’re editing.

3 — Add an attack button that dims during roundtime. Click Add button and set Label to Attack, Command to attack, and Hotkey to f1 — or click Capture, which reads “Press a key…” while it waits, and press the key itself. If the key is already spoken for you’ll see “Key is already bound by … - it wins over this button.”; pick another.

Set Countdown overlay to Roundtime so the button shows the seconds left.

Now scroll to States, note the reminder beside it — (first matching state styles the button) — and click Add state. A new state arrives already set to Roundtime active with dim ticked, which is exactly this rule. Click Save bar.

4 — Add a Hide button that reports being hidden. Click Add button: Label Hide, Command hide, Hotkey alt+h, Tooltip Attempt to hide. Add a state, and on its condition row open the kind combo and choose Indicator; set the id to hidden and leave active ticked. Under Style while active:, set Label to Hidden and the foreground to #80ff80. A state’s style can replace the button’s text, so the button tells you your own state rather than offering an action you’re already in.

Add a second state below it for Roundtime active with dim ticked. Order matters: hidden sits above roundtime, so being hidden wins over being in roundtime. Click Save bar.

5 — Add a spell button that knows what you can afford. Click Add button: Label 909, Command incant 909. Set Countdown overlay to Casttime.

Add a state and choose Vital on its condition row: vital mana, comparison <, value 9, unit combo set to abs — absolute points, not percent, because a spell’s cost is a number of mana and not a fraction of your pool. Tick dim under Style while active:.

6 — Give that button a different command while casting. On a state’s card, fill in Command while active: — the field hinting (button command). Whatever you type there is sent instead of the button’s own command while that state matches. This is literal text; anything dynamic belongs inside the command, such as a ;eq … line that Lich evaluates. Click Save bar.

7 — Watch it before you commit. The Preview: row above the button list renders the bar live against your current game state, using the same resolution the real widget uses. Step into roundtime and watch the preview change. If the preview is wrong, the bar will be wrong.

Expected result: a Combat bar on screen. F1 attacks and the Attack button dims with a countdown while roundtime runs. Alt+H hides, and the button turns green reading Hidden once you are. The 909 button dims whenever you’re under 9 mana.

What the states look like on disk

You never need this file — the editor writes it — but three lines of it explain the ordering rule better than a paragraph can. Here is the Hide button from step 4:

[[bars.buttons.states]]                            # checked FIRST
[bars.buttons.states.when]
type = "indicator"
id = "hidden"
active = true
[bars.buttons.states.style]
label = "Hidden"
fg = "#80ff80"

[[bars.buttons.states]]                            # checked only if the above missed
[bars.buttons.states.when]
type = "rt_active"
[bars.buttons.states.style]
dim = true

States are checked top to bottom and the first match wins — the second block never runs while you’re hidden. Swap the two blocks and a hidden character in roundtime shows a dim “Hide” instead of a green “Hidden”. In the editor the ^ and v buttons on each state card are this same ordering; the file just makes it visible. Editors build one level of all of / any of nesting; deeper trees are file-authored, render as (nested group - edit in hotbars.toml), and still evaluate correctly.

Make it yours

A melee bar that reads your hands. Choose Hand holds on a condition row and set the hand to Right with item type weapon to light your attack button only when you’re actually armed. Pair it with a Hand empty state carrying a Command while active: of get my weapon, and one button both draws and swings depending on what you’re holding.

A caster bar that grays out what you can’t cast. Instead of writing a mana threshold per spell, use Spell affordable with the spell’s number. It reads the bundled spell table’s static costs against your current mana and fails closed — unknown numbers and formula-cost spells evaluate false — so a button grays out rather than lying to you.

A stance bar built from indicators. Give each stance button an Indicator state on standing, kneeling, sitting, or prone, styled bright, so the bar always shows your current posture as the one lit button. The same eleven ids — standing, kneeling, sitting, prone, stunned, bleeding, hidden, invisible, webbed, joined, dead — are available in every condition builder in the product.

When it doesn’t work

The window is there but empty. The window name and the bar name must match exactly — a hotkeybar window displays the bar of the same name, and there is no bar-picker in the window’s right-click menu (only Edit hotbars…, which opens this editor). Rename one to match the other.

The wrong state keeps winning. The first matching state wins, so a broad condition placed above a narrow one swallows it. Put “hidden” above “roundtime”, and “health < 25%” above “health < 50%”. Reorder with each card’s ^ and v, then Save bar.

The hotkey does nothing. Existing keybinds.toml bindings beat hotbar buttons. The editor warns you at the time — “Key is already bound by … - it wins over this button.” — and the button list marks the row (key conflict). Pick a free key, or rebind the keybind.

A duplicated button’s hotkey is gone. That’s deliberate: duplicating a key would guarantee a conflict, so Dup clears the copy’s hotkey. Give the copy its own.

The button shows text where you wanted art. Icons are GUI-only — the TUI always renders the label. In the GUI, check Face is set to Icon or Icon + label and that the bar’s Icon px is large enough to see; the default is 24, and barbar-style art reads best between 32 and 64.

Your global bar stopped responding to edits. A per-character bar with the same name replaces the global bar wholesale — it does not merge button by button. The bar list’s [G] / [C] badges tell you which copy exists; edit the [C] one, or delete it to fall back to global.

The state’s command override isn’t doing anything clever. Command while active: is literal text, not an expression. Put the logic in the command itself — a ;eq … line, which Lich intercepts and evaluates — rather than expecting the field to compute anything.

Nothing saved. Save bar is disabled until there are changes and shows “unsaved changes” beside it while there are. If it’s grayed out, your edit didn’t register; click into the field and change it again.

See also

Frontends

One character, one set of settings, five ways to sit down and play — pick the one that matches where you are right now.

What it’s for

You hunt at your desk, but you also want to check on a bounty from the couch, or keep a character parked over SSH on a box that has no desktop at all. VellumFE gives you five interfaces over one core, so the highlight that colors your critical hits, the keybind on your numpad, and the layout you spent an evening arranging follow you from the terminal to the desktop window to your phone. You configure a thing once. It shows up everywhere it can be shown.

The differences between the five are about what the surface can physically do — a terminal cannot draw a skin image, a phone cannot let you drag a window to a new corner — not about which one is the “real” client.

📷 screenshot pending
The same character in three frontends at once: the Desktop GUI hunting layout, the same layout in a terminal, and the phone's status drawer.

The five at a glance

FrontendHow you start itReach for it whenMaturity
Terminal (TUI)--frontend tui (the CLI default)You live in a terminal, play over SSH, or want the smallest possible footprintStable
Desktop GUI--frontend gui, or launch a saved connection (its default)You want mouse-first layout work, graphics, skins, and the stay-open toolbar hubsStable
Mobile WebEnable the web server (--web-port, or [web] in config.toml), then open the address in a browserYour PC is running the session and you want a second screen, or you want to play from a browser with no local UI at allStable
Android appSideloaded APKThe whole client on an Android phoneIn progress
iOS appTestFlightThe whole client on an iPhoneBeta — via TestFlight

Two of those rows carry a wrinkle worth stating plainly.

“Mobile Web” is two different products depending on how you start it. Run the TUI or GUI with the web server on and the browser is a sidecar — a second screen driving the session your PC already owns. Run --frontend headless and there is no local UI at all: the core plus the web server is the client, and the browser gets a login screen. Same code, different shape.

The Android and iOS apps are not a third thing. They are the same Rust core in a native shell, showing the web client’s UI in a WebView. What the shell adds is a saved Characters picker — the one native screen — so a phone can scan a pairing QR code and seal a saved server into the Keychain or Keystore. On a phone the login screen shows two tabs (play.net and Lich) plus that picker, reached through the person icon; a desktop browser shows three tabs, because it keeps the in-page Remote tab the phone replaces.

Set it up

Pick where you’re playing, then start the client that way. Every one of these connects the same character to the same game.

  1. Run vellum-fe with no arguments (or double-click it). The VellumFE Launcher window opens, headed VellumFE over “Choose a connection to launch”.
  2. Click ➕ New connection, fill in the New connection form, and click Save.
  3. Click Launch on the row you just made.

To start the GUI without going through the Launcher, run vellum-fe --frontend gui --port 8000 --character YourName.

Which frontend a saved connection uses is set per row under AdvancedFrontendGUI / Terminal. A saved connection defaults to GUI, but the --frontend command-line flag defaults to tui — the same character started two ways lands in two different interfaces. That surprise is worth knowing before you go looking for a bug.

📷 screenshot pending
A connection's Advanced section with the Frontend submenu open on GUI / Terminal.

Expected result: a native window opens with your layout, and the top toolbar shows the Windows, Settings, Zones, and Editors hubs.

Common setups

One character, desk and couch

Start the GUI from the Launcher and enable the web server on the same run. Play at the desk normally. When you get up, run .webinfo, scan the QR with your phone, and the phone joins the same session — the same roundtime, the same room, the same active spells. Nothing syncs, because nothing needed to: there is one session and two views of it.

You’ll see: your health bar moving on the phone at the same instant it moves on the monitor, and a command typed on either one landing in the game once.

A character parked on a headless box

On a server or a Raspberry Pi with no desktop, run vellum-fe --frontend headless --port 8000 --character YourName --web-port 8080 --web-bind 0.0.0.0. There is no local UI at all. Point any browser on your network at that box’s address and the browser is the entire client — including its own login screen, since there’s no desktop session to attach to.

You’ll see: the browser presenting a login overlay rather than immediately showing a game already in progress — that is how you tell headless from sidecar at a glance.

Tips & gotchas

⚠️ Ctrl+C means two different things. In the Terminal (TUI) it copies your selection and does not quit. In the Desktop GUI it quits. To leave the client from the terminal, use .quit or .exit. This is the single biggest trap when you move between the two.

⚠️ .quit may not close the window. By default it disconnects but leaves the client open — run it again, or use .exit, to close. Turn this off with ui.keep_open_on_quit in Settings.

  • The two “Launchers” are unrelated. The Launcher is the graphical connection list you get by running with no arguments. The SSH Launcher is an in-session panel (.launcher / .launch <character>) that cold-starts a headless Lich on another machine. They share a word and nothing else.
  • The terminal cannot render images, and that is deliberate. Skins, per-window background art, and the graphical injury doll, compass, and hand icons are GUI-only forever — a character cell has no pixels to put them in. The TUI renders the same information with glyphs and color. Nothing is missing from your character; only the artwork is.
  • Layouts are shared, appearance is not entirely. .savelayout and .loadlayout work in both desktop frontends against the same pool, so a layout you build in the GUI opens in the TUI. Skin frames inside it simply have nothing to draw on the terminal side.
  • Saving a layout is always typed. There is no GUI button for it — .savelayout <name> in either desktop frontend. The Windows catalog says so itself.

See also

Config reference (TOML)

Frontend selection is a command-line and per-connection concern; these are the settings that decide which face you get and how it reaches the network.

Command line

FlagTypeDefaultWhat it does
--frontendtui | gui | headlesstuiWhich interface to run. headless runs the core plus web server with no local UI.
--launcherflagoff (on when run with no arguments)Opens the graphical Launcher. GUI only.
--launch-profile <NAME>stringRuns a saved connection by name from launcher.toml. Conflicts with --direct, --key, --launcher.
--portu168000Lich proxy port. Overrides config.toml.
--hoststring127.0.0.1Lich proxy host. Overrides config.toml.
--characterstringCharacter to log in as (direct mode); also the fallback config-directory name.
--profilestringfalls back to --characterConfig-directory name, kept separate from the login name.
--directflagoffConnects to play.net without Lich. Enables --account, --password, --game.
--web-port <PORT>u16Enables the embedded web server on this port. Overrides [web].
--web-bind <ADDR>stringAddress the web server binds to. Overrides [web].
--data-dir <DIR>path~/.vellum-feConfig directory root. Also settable via VELLUM_FE_DIR.

Per-connection (launcher.toml, written by the Launcher)

FieldTypeDefaultWhat it does
frontend"gui" | "tui""gui"Which frontend this saved connection launches. Note the mismatch with the --frontend CLI default of tui.
save_passwordboolfalseStores the password in the OS credential store (service id vellum-fe, keyed by the lowercased account). Never written to a file.

Session behavior (config.toml)

FieldTypeDefaultWhat it does
ui.keep_open_on_quitbooltrueAfter .quit, disconnect but keep the window open. Run .quit again or .exit to close.

Terminal (TUI)

Play GemStone IV wherever a terminal reaches — over SSH, on a Raspberry Pi, in a tmux split next to your scripts — with the same layout, highlights, and keybinds as the desktop client.

What it’s for

You already have a terminal open. The TUI puts your whole character in it: the same windows, the same highlight rules, the same numpad movement macros as the Desktop GUI, drawn on a character grid instead of pixels. It is what a hand-typed vellum-fe command gives you by default, and it is the only frontend that survives a session over SSH on a machine with no desktop at all.

It is not a stripped-down mode. Mouse works — for links, scrolling, selection, dragging windows, and right-click menus. And the TUI keeps its own window editor, a full-screen form you open with .editwindow; the GUI’s separate editor was retired in favor of a right-click menu, so this form has no direct counterpart over there.

📷 screenshot pending
A hunting layout in a 24-bit-color terminal: main text left, vitals and roundtime across the top, thoughts and combat stacked at right.

Set it up

The GUI does not open the terminal frontend, but it does decide whether a saved connection opens in one. In the VellumFE Launcher, click Edit on a connection, open AdvancedFrontend, and choose Terminal. Save.

A saved connection defaults to GUI, while the --frontend command-line flag defaults to tui — so a character you always launched from a terminal will open in a window the first time you launch it from the Launcher instead.

📷 screenshot pending
The Launcher's AdvancedFrontend submenu with Terminal selected for a saved connection.

Expected result: clicking Launch on that row starts the character in a terminal rather than a native window.

Working with windows

Everything in this section is mouse-and-keyboard in the terminal itself.

Move a window by left-dragging its title bar — the top row, and only the top row. (The GUI lets you drag from anywhere; the TUI reserves the body for text selection.) A window locked in place can still be grabbed there, but will not move.

Resize a window by dragging its right column, bottom row, or the bottom-right corner cell. Two details save you a puzzled minute: the bottom row only acts as a handle when the window is taller than two rows, and the right column only when it is wider than one — so a single-row bar’s bottom row stays content, not a grab handle. A locked window resizes nowhere.

Right-click a title bar for Close Window, Edit Window…, and Open Menu. (Close Window is not offered on main or command_input.) Right-clicking the performance overlay opens its own metrics toggle menu instead.

Add, hide, delete, and restore windows by command:

GoalCommand
Add a window.addwindow for a picker, or .addwindow <name> <type> <x> <y> <w> [h]
Hide a window, keep it in the layout.hidewindow / .hidewin [name] (no name opens a picker)
Truly remove a window from the layout.deletewindow / .delwindow <name> — stashed so it can be restored
Bring a hidden window back.menuWindows submenu
Save / load a layout.savelayout [name] · .loadlayout [name] · .layouts to list
Refit the layout after resizing the terminal.resize

.deletewindow truly deletes in both the TUI and the GUI, and .hidewindow hides in both — there is no asymmetry to remember. Both refuse to remove your only main-feed window. There is no standalone .showwindow; restoring is the .menu route or .loadlayout.

The window editor

.editwindow (or .editwin) opens a full-screen form for the focused window, or .editwindow <name> for a specific one — the same form the title-bar right-click reaches via Edit Window…. It is keyboard-driven, and the bottom border always tells you the keys for wherever you are.

KeyDoes
Tab / Shift+TabNext / previous field
Ctrl+SSave and close
EscCancel, or back out of a sub-editor
Ctrl+POn the Streams field only — open a picker of every stream seen this session

The base footer reads [Ctrl+S: Save] [Esc: Cancel]. Step into a sub-editor and it changes to match: reorderable lists show [Ctrl+S: Save]─[Shift+↑/↓: Reorder]─[Esc: Cancel], and the stream picker shows [Enter: Add stream]─[Esc: Back].

That Ctrl+P picker is the one to remember. Subscribing a window to a stream normally means typing an id you have to already know; on the Streams field it lists what Lich has actually pushed this session, and Enter appends the highlighted id to the field. It does nothing on any other field, so if it seems dead, check which field has focus.

📷 screenshot pending
The TUI window editor on the Streams field with the Ctrl+P seen-streams picker open, footer reading [Enter: Add stream]─[Esc: Back].

Keys you get out of the box

These are the defaults in keybinds.toml, and every one of them is rebindable with .keybinds.

Text and windows

KeyAction
Tab / Shift+TabSwitch focused window (dot-command completion takes priority when one is offered)
PageUp / PageDownScroll the focused window one page (20 lines)
Alt+PageUp / Alt+PageDownScroll one line
Up / DownPrevious / next command in history
Ctrl+FStart search
Ctrl+PageUp / Ctrl+PageDownPrevious / next search match
EscClose a menu, picker, or editor; exit the current mode
F12Toggle the performance overlay

Numpad movement, pre-bound

num_1num_9 walk the compass the way the keypad is laid out (1=southwest, 2=south, 3=southeast, 4=west, 5=out, 6=east, 7=northwest, 8=north, 9=northeast). num_0 goes down and num_decimal goes up. num_plus sends look, num_minus sends info, num_multiply sends exp, and num_divide sends health.

Editing the command line — these are fixed terminal conventions rather than entries in keybinds.toml: Ctrl+A select all, Ctrl+C copy, Ctrl+X cut, Ctrl+V paste, Ctrl+Z undo, Ctrl+Shift+Z redo, Ctrl+E jump to end, Ctrl+U clear the line, Ctrl+W delete the word behind the cursor.

As you type, the newest history entry sharing your prefix appears as muted text from the cursor onward. Tab accepts it. When there is no suggestion and no dot-command completion pending, Tab falls back to switching windows. The suggestion uses the theme’s text_secondary color; set completion_color on the command-input window to change it.

Selecting, copying, and clicking

Drag across text to select it, and it copies to your clipboard the moment you release the mouse — no second keystroke. That is selection_auto_copy, on by default. Copy is plain text by design; styling is deliberately not carried to the clipboard.

Clicking game text does what the markup says: a URL opens your browser, a command link sends its command, a coordinate link runs its command list, and a plain noun asks the server for its context menu and shows what comes back. Ctrl+drag a link onto a hand, a container, an inventory row, or another link to send _drag, which is how you move an item without typing a target.

Common setups

A 256-color terminal that still looks right

Some terminals — an older xterm, a locked-down remote console — cap at 256 colors, and true color output turns muddy. Start with vellum-fe --port 8000 --character YourName --color-mode slot --setup-palette. Slot mode maps your configured colors into palette slots and --setup-palette programs them into the terminal at startup. If you change colors mid-session, run .setpalette to reprogram, and .resetpalette to hand the terminal’s own palette back before you exit.

You’ll see: your named highlight colors rendering as the exact hues you chose in .colors, instead of the nearest muddy approximation.

A layout that survives a terminal resize

Arrange your windows by dragging title bars and edges, then save with .savelayout hunting. When you later stretch the terminal or drop into a different-sized tmux pane, run .resize to refit the saved arrangement to the new dimensions rather than rebuilding it. .layouts lists what you have saved, and .loadlayout hunting brings it back — including on the Desktop GUI, which reads the same pool.

You’ll see: your combat window still occupying the right third of the screen after the pane changes size, rather than clipped off the edge or stranded in the middle.

Tips & gotchas

⚠️ In the TUI, Ctrl+C copies your selection — it does not quit. A raw-mode terminal owns its own keys, so nothing intercepts it. To leave, type .quit or .exit. In the Desktop GUI the same combination quits, because there the OS delivers it beneath the key layer. This is the one difference most likely to bite you when you switch frontends.

⚠️ .quit disconnects but keeps the client open by default. Run it again, or use .exit, to close. Change it with ui.keep_open_on_quit in Settings.

  • The terminal cannot render images, and that is by design, not a missing feature. Skins, per-window background art, and the graphical injury doll, compass, and hand icons are GUI-only forever — there are no pixels inside a character cell. The TUI shows the same underlying information with glyphs and color, so nothing about your character is hidden from you. If a layout you built in the GUI has skin frames, they load without complaint and simply have nothing to draw.
  • .setpalette needs slot mode. On direct (true color) it has nothing to program. Pair it with --color-mode slot.
  • Reset the palette before you leave a shared terminal. .resetpalette restores the terminal’s own colors; without it a multiplexer session can keep your palette after you disconnect.
  • Use a Nerd Font if you want the default countdown glyphs and compass rendering exactly as designed. Without one they fall back to plainer characters.
  • There is no Launcher in the terminal. It is an egui window, so it is GUI-only. Start a saved connection from a terminal with --launch-profile "<name>".
  • A one-row window has no bottom resize handle. That is deliberate — its bottom row is content. Resize it from the right column instead, or adjust it in .editwindow.
  • .menu has no default keybind. Type it, or use Open Menu from a title-bar right-click.

See also

Config reference (TOML)

Command-line flags that matter to the TUI

FlagTypeDefaultWhat it does
--frontendtui | gui | headlesstuiThe TUI is the default; pass it explicitly to override a saved connection.
--color-modedirect | slot | indexedfrom config.tomldirect = 24-bit RGB; slot = 256-color custom palette; indexed = standard 256-color, the safe fallback.
--setup-paletteflagoffRuns .setpalette at startup. Use with --color-mode slot.
--launch-profile <NAME>stringRuns a saved connection from launcher.toml in this frontend.
--port / --hostu16 / string8000 / 127.0.0.1Lich proxy target; overrides config.toml.
--directflagoffConnect to play.net without Lich; enables --account, --password, --game.
--nosoundflagoffSkips audio device initialization entirely.

Settings the TUI reads (config.toml)

FieldTypeDefaultWhat it does
ui.selection_auto_copybooltrueCopy a drag-selection to the clipboard on mouse-up. Turn off and selection stays visible without copying.
ui.keep_open_on_quitbooltrue.quit disconnects but leaves the client open; .quit again or .exit closes it.
ui.color_mode"direct" | "slot" | "indexed""direct"Terminal color rendering, same values as --color-mode.

Per-window field used above (layout.toml)

FieldTypeDefaultWhat it does
completion_colorcolor name or hextheme text_secondaryColor of the inline history suggestion in the command-input window.

TUI-only dot-commands

CommandWhat it does
.setpaletteLoad your palette colors into the terminal (slot color mode).
.resetpaletteRestore the terminal’s default palette.
.resizeRefit the layout to the current terminal size.
.transparentToggle transparent window backgrounds.

The in-app .help footer states the same split: commands marked (GUI) need the desktop GUI, and .setpalette / .resetpalette / .resize are TUI-only.

Desktop GUI

Build the layout you play in — drag windows anywhere, reskin them with real artwork, and change any setting from a right-click without leaving the game.

What it’s for

This is where you design your interface. Everything else — the terminal, the phone, the browser — plays the layout; the GUI is where you make it. You drag a window to the corner you want it in, right-click it to change its font, and watch the change land while combat text is still scrolling past.

It is also the only frontend with pixels. The compass is a drawn rose, the injury doll is a body diagram that colors by wound severity, and a skin can replace your window borders, backgrounds, and icons with real artwork. A character grid cannot do those things, so the GUI does them for everyone.

📷 screenshot pending
A GUI hunting layout with the toolbar's Windows, Settings, Zones, and Editors hubs across the top, vitals in the header, and a skinned frame on the story window.

Set it up

  1. Run vellum-fe with no arguments (or double-click it) to open the VellumFE Launcher, then click Launch on a saved connection. A saved connection opens in the GUI by default.
  2. To start the GUI directly from a shell instead, run vellum-fe --frontend gui --port 8000 --character YourName. Direct login works here too: vellum-fe --frontend gui --direct --account YOURACCOUNT --game prime --character YourName.
  3. Once the window is up, click Windows in the top toolbar and tick the windows you want. The catalog stays open while you work, so you can add several in one visit.

A saved connection defaults to GUI, but the --frontend command-line flag defaults to tui. The same character launched from the Launcher and from a typed command lands in two different interfaces. See First Launch for the full flag list.

📷 screenshot pending
The Windows hub open as a stay-open catalog, showing show/hide checkboxes, per-row zone dropdowns, and ➕ Custom window….

Expected result: a native window opens with your layout, and the top toolbar shows the Windows, Settings, Zones, and Editors hubs.

Working with windows

Adding, hiding, deleting

Adding a window is the Windows catalog, not a right-click. Click Windows in the toolbar and you get every window the client can have — the built-in catalog plus game dialogs, streams, and containers — sorted by title and grouped into collapsed categories. Each row is a checkbox (tick to show, untick to hide) and a zone dropdown. Setting the zone on a hidden window decides where it appears when you show it.

Two buttons sit at the top of the catalog:

  • ➕ Custom window… creates a blank widget of the type you pick and drops you into the new window’s right-click menu to configure it.
  • ↩ Restore deleted… brings back a window you deleted, with its position, streams, and widget type intact. It appears only when something is waiting to be restored.
GoalGestureTyped equivalent
Add / show a windowWindows ▸ tick the row.addwindow (picker), or the full .addwindow <name> <type> <x> <y> <w> [h]
Hide it, keep it in the layoutRight-click ▸ Hide, or untick in the catalog.hidewindow [name]
Remove it from the layoutRight-click ▸ WindowAdvancedDelete Window….deletewindow <name>
Bring a deleted one backWindows↩ Restore deleted…
Save the arrangement— (no button).savelayout <name>
Load one.menuLayouts.loadlayout <name> · .layouts

⚠️ .deletewindow truly deletes and .hidewindow hides — in both desktop frontends. There is no asymmetry to remember. Delete stashes the window so ↩ Restore deleted… can bring it back, and both commands refuse to remove your only main-feed window.

⚠️ There is no button to save a layout. .savelayout <name> is typed, in the GUI as much as the terminal. The catalog says so itself when no layouts exist yet.

Moving, resizing, snapping

Drag a window from anywhere — body or title bar, no modifier held. Interactive content still wins, so selecting text or clicking a link does not start a drag. Resize by dragging any edge or corner.

⚠️ The GUI drags from the whole window; the TUI drags from the title bar only. And Ctrl+drag is not window movement in either frontend — it is the object-drag gesture that sends _drag, for dropping an item onto a hand, a container, or another item.

While you drag or resize, edges snap to the zone’s bounds, to other windows in the same zone, and optionally to a grid. An engaged snap draws a guide line at the matched coordinate. Hold Shift to suspend snapping and place the window exactly where the pointer is — the guides disappear for as long as you hold it.

A snapped drop can promote to a persistent anchor, which makes the window keep following the edge it snapped to when the app window resizes. Right-click ▸ ArrangeRelease Anchors forgets them; the window stays exactly where it is and stops following. Dragging the window away from its snap does the same thing. The row is always visible, greyed when the window has no anchors, so you can tell at a glance which case you are in.

Tune the whole system under SettingsGUI: snap distance, which targets participate, grid pitch, and grid sizing. .snapdebug writes a trace of the snap engine to ~/.vellum-fe/vellum-fe.log when a snap is not doing what you expect.

Zones

The GUI arranges windows in five zones — Header, Footer, Left Bar, Center, and Right Bar. Toggle them from the toolbar’s Zones hub, which stays open so you can adjust several and watch each take effect, or type .header, .footer, .leftbar, .rightbar (each takes on, off, or toggle, defaulting to toggle).

Every zone is a free canvas: windows sit exactly where you drop them, may overlap, and remember their spot. Moving a window to a different zone has three routes:

  • Alt+drag the window. Hold Alt and drag, and a tinted overlay highlights the zone under your pointer; release to drop it there. Ordinary movement is suppressed while Alt is held, so the two gestures never fight.
  • Right-click ▸ Arrange ▸ Move to ▸ the zone you want.
  • The zone dropdown on that window’s row in the Windows catalog — the one that also works on a window that is currently hidden.

Overlapping and detaching

Where windows can overlap — Center, Header, and Footer — the right-click menu offers Send to Back, dropping the window behind anything it covers so a buried one becomes reachable. Clicking a window still raises it. The packed sidebars do not offer it, because nothing overlaps there.

Detach puts a window in its own OS window, restored across sessions. A detached window gets the same right-click menu with Reattach in place of Detach, and without Arrange, Group, or Send to Back — none of them mean anything inside an OS window. Drag its body to move it.

The right-click menu

Right-click any window body and you get every per-window setting there is. Changes apply live — there is no Save button. Text fields commit when you press Enter or click away. .editwindow <name> opens the same menu for a window by name.

⚠️ The GUI’s separate Window Editor no longer exists. If a guide tells you to open an editor window and press Save, it is describing the old client. Right-click is the one home for these settings now. (The TUI still has its own full-screen editor form.)

Three quick actions sit at the top — Hide, Detach (or Reattach), and Send to Back — then collapsible sections, each with its own Advanced fold so the menu opens short.

Window

Title, Title bar text (custom text for the bar, separate from the window’s name), Streams — the stream ids this window collects, with a + picker listing every stream seen this session so you do not have to know an id in advance — Buffer lines (scrollback size), Speak new lines (TTS), and Lock in place. A locked window can still be grabbed but will not move or resize; .lockwindows locks everything at once.

Under Advanced: Compact (condenses known content such as bounty text), Timestamps with a Timestamp at line start toggle, and Delete Window…. Delete is double-gated — the first click arms it, then the menu asks “Really delete this window from the layout?” with Delete and Cancel.

The widget section

Named for the widget itself, and present only when the widget has settings of its own:

  • Timer / Bar — the feed id it tracks, its label, color, whether it shows value/max or a bare value, and Stay visible at rest for timers that should not vanish at zero.
  • Effects — which category the window carries.
  • Room — section toggles for Description, Objects, Players, and Exits.
  • Targets — the filtered-appendage count and status position, plus Global target settings…, which jumps to SettingsTargets where the colors and excluded nouns live for every targets window at once.
  • ExperienceLevel, Mind state, Experience bar, Total exp, Ascension exp, and the two bar colors. EncumbranceLevel bar and Help text.
  • VitalsLayout (orientation), Bar height, Bar text format, Depleted color, and a checkbox per bar in display order.

Widgets with an editor of their own link to it from here: Edit tabs… (tab names, streams, and order), Edit hotbars…, Edit indicators…, Edit dashboard…, Hand icons… (status-driven icons for an empty hand, a held weapon, a prepared spell), Calibrate doll…, and Open Map Explorer.

📷 screenshot pending
A story window's right-click menu with Hide / Detach / Send to Back on top and the Window section expanded over the Streams field and its seen-stream picker.

Arrange, Appearance, Group

Arrange holds Move Window (a keyboard-friendly move that works even on a locked window), Move to ▸ each zone, and Release Anchors. Under Advanced: Fixed size, which keeps a window’s exact width and height when the app window resizes or zooms so only its position adapts — what you want for a compass or a hands widget.

Appearance splits into Text (font, text size, word wrap, content alignment), Title bar (show it, and its alignment), and Frame (border on/off and style, accent color, skin frame, background). Under Advanced sit the knobs you set once: title-bar height, which border sides draw, corner radius, and frame scale.

Group locks windows together so they move as a unit. It sets the group’s orientation (Stacked or Side by side), reorders members with ⬆ / ⬇, and ungroups one member or the whole group.

The toolbar hubs

Four stay-open menus sit across the top. None of them close when you click inside — only clicking outside dismisses them — so you can make several changes and watch each land.

HubWhat’s in it
WindowsThe full window catalog: show/hide checkbox and zone per row, ➕ Custom window…, ↩ Restore deleted…
SettingsAll Settings…, plus one row per section that opens the Settings window scrolled to that section
ZonesShow/hide and an overlay toggle for each of the five zones
EditorsThemes · Colors · Highlights · Keybinds · Menu Keybinds · Controller · Touch Wheel · Hotbars · Indicators · Streams & Custom Windows · Sorter · Room Images · UI Packs · Asset Manager (Jinx)

The distinction between the last two is worth knowing: Settings holds the knobs that live in config.toml, while Editors holds the authored content that owns its own files — your highlight rules, your keybinds, your hotbars.

Graphics and skins

The GUI draws what the terminal spells:

  • Compass rose — a vector rose with lit direction markers.
  • Injury doll — a body diagram colored by wound and scar severity. With a skin that ships doll art, wounds render as dots at positions you set by clicking, through Calibrate doll… in the doll window’s own menu section.
  • Status icons — vector pictograms for stance, hidden, stunned, and the rest of the indicator set.

A skin replaces all of it at once — nine-slice window borders, background art, icon sprites, a sprite compass and paperdoll. .skins lists what is installed, .setskin <name> activates one, .setskin none turns skinning off, and .makeskin <name> scaffolds a starter skin you can edit. Per-window, AppearanceFrame picks which frame from the active skin a given window wears. See Skins (GUI Graphics).

Common setups

A hunting layout you can rebuild in five minutes

  1. Click Zones and turn on Header and Right Bar.
  2. Click Windows, tick Vitals and set its zone dropdown to Header; tick Roundtime, Compass, and Targets and set them to Right Bar.
  3. Drag each one where you want it. Hold Shift while dropping the compass if the snap keeps pulling it flush against an edge you want it off of.
  4. Right-click the compass ▸ ArrangeAdvanced ▸ tick Fixed size, so it keeps its proportions when you resize the app window.
  5. Type .savelayout hunting.

You’ll see: the arrangement redraw exactly as you left it after .loadlayout hunting — including on the terminal, which reads the same layout pool.

A story window that reads the way you read

Right-click your main story window. Under Window, raise Buffer lines so a long hunt stays scrollable. Under Advanced, tick Timestamps and Timestamp at line start if you want to reconstruct a fight afterward. Under AppearanceText, pick a font and size that suit your monitor, and turn word wrap on. Under AppearanceFrame, set an accent color so the window you look at most is instantly identifiable.

You’ll see: each change take effect while game text is still arriving — timestamps appear on the next line, the font reflows what is already on screen, and nothing needs saving.

Tips & gotchas

⚠️ In the Desktop GUI, Ctrl+C quits. In the Terminal (TUI) the same combination copies your selection and does not quit at all. This is the single biggest trap when you move between the two. To copy in the GUI, select with the mouse (double-click for a word, triple-click for a line) and then press Ctrl+C.

⚠️ .quit disconnects but keeps the window open by default. Run it again, or use .exit, to close. Change it with ui.keep_open_on_quit in Settings.

  • There is no right-click “Add Window”. Adding is the Windows catalog or .addwindow. Right-click configures a window that already exists.
  • Copy is plain text, deliberately. Selections are anchored to the text itself, so they survive scrolling — drag past the window edge to auto-scroll and the copy picks up lines currently out of view — but colors and styling are not carried to the clipboard.
  • Hiding is the control for game dialogs. A hidden window stays hidden even when the game re-sends it. There is no separate blocklist to maintain.
  • Even the story window can be hidden, once another window or tab carries the main stream. Hiding the command input hands typing to the built-in bottom bar.
  • Terminal-only commands do nothing here. .setpalette, .resetpalette, .transparent, and .resize belong to the TUI; themes and skins handle appearance in the GUI.
  • Layouts are shared; the live arrangement is not. .savelayout / .loadlayout write to the pool both desktop frontends read, but the GUI keeps its own per-character live layout (positions, zoom, fonts) apart from the TUI’s layout.toml.
  • Ctrl+F opens in-window search with match highlighting. Ctrl+= / Ctrl+- / Ctrl+0 zoom the whole interface.
  • The two “Launchers” are unrelated. The Launcher is the graphical connection list. The SSH Launcher (.launcher / .launch <character>) is an in-session panel that cold-starts a headless Lich on another machine.

See also

Config reference (TOML)

The GUI writes its own state; you never need to hand-edit these to use the client. They are here for troubleshooting and for reading a layout someone shared with you.

Command line

FlagTypeDefaultWhat it does
--frontendtui | gui | headlesstuiPass gui explicitly; a saved connection defaults to gui instead.
--launcherflagon when run with no argumentsOpens the graphical Launcher. GUI only.
--launch-profile <NAME>stringRuns a saved connection by name from launcher.toml.
--web-port <PORT>u16Enables the embedded web server so a phone or browser can join this GUI session.
--data-dir <DIR>path~/.vellum-feConfig directory root. Also settable via VELLUM_FE_DIR.

Where GUI state lives

PathContains
~/.vellum-fe/gui/<profile>/<character>/layout_v1.jsonThe per-character live layout: window positions, zones, detached windows, tab groups, zoom, fonts.
~/.vellum-fe/layouts/<name>.jsonNamed layout checkpoints from .savelayout, shared with the TUI’s pool.
~/.vellum-fe/global/skins/Installed skins. The active skin is recorded in the GUI layout, not config.toml.

Session behavior (config.toml)

FieldTypeDefaultWhat it does
ui.keep_open_on_quitbooltrueAfter .quit, disconnect but keep the window open. .quit again or .exit closes it.

GUI-only dot-commands

CommandWhat it does
.header / .footer / .leftbar / .rightbar [on|off|toggle]Show or hide a shell zone. Default is toggle.
.skins · .setskin <name> · .makeskin <name>List, activate (none disables), or scaffold a skin.
.controllerOpen the controller bindings editor.
.webui [page|off]Render Lich WebUI pages as native docked panels. Needs a Lich proxy connection.
.snapdebugToggle the snap-engine trace in vellum-fe.log.
.editwindow [name]Open that window’s right-click menu; with no name, the Windows catalog.

The in-app .help footer states the same split: commands marked (GUI) need the desktop GUI, and .setpalette / .resetpalette / .resize are TUI-only.

Browser Client

Put a second screen beside your PC with nothing to install — your phone, tablet, or the spare laptop shows the same character you’re already playing.

What it’s for

You’re hunting at your desk and the good text is scrolling past the window you can’t see. Or you want to keep an eye on thoughts and your spell timers while your hands are busy elsewhere in the house. VellumFE ships a small web server inside the client: turn it on, open a browser on any device on your network, and you get a touch-first view of the same session — same character, same streams, same links to click.

Two shapes, both first-class:

  • A second screen for a running session (sidecar). Your TUI or GUI keeps running on the PC; the browser joins that same character. This is the shape this page leads with.
  • The whole client in the browser. Run VellumFE with no local UI at all and the browser is the only screen — Headless mode below.

If you want the client living on your phone rather than served from your PC, the Android and iOS apps do that job instead. Neither route is the lesser one — the app plays from anywhere, the browser sits beside your PC.

📷 screenshot pending
A phone browser showing the story pane, stream chips, and vitals strip beside a desktop GUI session running the same character.

Set it up

Serve the session from the connection you launch it with:

  1. In the VellumFE Launcher, click Edit on the connection (or ➕ New connection) and open the Advanced fold.
  2. Tick Enable on port next to Web dashboard and set the port — 8040 is a good first choice.
  3. Set Bind address to 0.0.0.0. The hint next to the field says 0.0.0.0 = allow LAN devices; the default 127.0.0.1 serves this PC only, so a phone will never reach it.
  4. Save, then Launch the connection.
  5. In the game input, type .webinfo. It prints Web session URL (browser): http://192.168.1.50:8040/#token=… and opens a pairing page with QR codes in your default browser.
  6. Scan the browser QR with the phone’s camera, or type that URL in.
📷 screenshot pending
The Launcher's Advanced fold with Web dashboard ticked on port 8040 and Bind address set to 0.0.0.0.

Expected result: the phone opens the game client already paired, showing your character’s live text. Your desktop window keeps playing exactly as before — this is a mirror, not a handover.

Common setups

A phone beside the keyboard while you hunt

  1. Launch your hunter in the desktop GUI with Web dashboard enabled on 8040 and Bind address 0.0.0.0.
  2. Run .webinfo and scan the browser QR.
  3. On the phone, tap the thoughts stream chip so the story pane filters to thoughts only.
  4. Swipe in from the right edge to open the status drawer and leave it on the Targets section.

→ You now watch thoughts and your target list on the phone while the desktop window stays on room text and combat. Tapping a creature in the drawer opens its attack/look/target menu and the command fires from your PC session.

Several characters, one dashboard

Launch two or three characters with web enabled. Unpinned instances treat the configured port as a base and walk upward to the next free one, so no two fight over 8040.

  1. Open http://192.168.1.50:8040/ — the bare root, not /play.
  2. The dashboard lists one card per running session, health-checked and auto-refreshing.
  3. Tap a card to play that character.

→ Switching characters is one tap, and a character you shut down drops off the list on its own. If you want one character to keep a fixed address you can bookmark straight to /play, set pinned = true in that character’s profile config — it then binds exactly that port or disables web for the session with a loud warning, never a silent neighbor port.

Playing from the browser

  • Read the game live, with streams as filter chips carrying unread badges. Long-press a chip to reorder them; the order follows your character across devices.
  • Send commands exactly as you would at the PC, dot-commands included. With a keyboard attached, Up/Down browse history; the ↻ button resends the last command and long-pressing it opens a history sheet. The newest matching history entry appears dim after the cursor — press Tab to accept it.
  • Tap links, nouns, and exits. A direct link fires immediately; a plain noun opens the server’s context menu in a bottom sheet. A mini compass floats over the text pane with live exits; hold it about half a second to lift and drag it somewhere else, and the spot is remembered per device.
  • Side drawers. Swipe from the left edge for the macro tray; from the right for the status drawerTargets, Players, Room, the injury doll, hands, and active effects with live countdowns.
  • Map. A map button appears in the top bar once map data exists (.mapdb download). Tap a room to walk there with native .go2, drag to pan, pinch to zoom. See Map and Travel.
  • Reconnect gracefully. The session resumes where you left off with a “missed output” marker for long gaps, and several devices can be connected at once.

Settings on the phone

The gear ⚙ opens the settings sheet. What you can author from here:

  • Appearance — theme presets, show/hide toggles for every piece of chrome, opacity sliders, and the Aa button for story text from 6 to 24 px. Theme, text size, and chip order roam with your character; chrome toggles and opacities stay per-device.
  • Client settings (saved on host) — the full desktop settings registry over the wire, editable at character or global scope, written to the hosting machine’s config exactly as if you had edited it there.
  • Highlight rules (this profile) / (global) — add and edit rules with color pickers, a sound dropdown, and a live preview.
  • Colors (this profile) / (global) — stream preset and prompt colors.
  • Streams (saved on host), Touch wheel (long-press ring), Controller (gamepad), Speech, and SSH launcher (cold-start Lich).
  • Advanced — raw TOML editors for highlights and colors with import/export, the practical way to move a desktop config onto the phone.

The phone’s highlight editor does redirects and squelch. Older documentation said it could not. The rule form has a Squelch checkbox and a redirect Off / redirect only / redirect + copy selector with a target field, so a phone-authored rule can send matching lines to another window or hide them outright.

What the phone genuinely cannot author: layout and panel placement, window resizing, game keybinds, and macro hidden_when conditions. Those stay on the desktop. Everything else on the list above is editable from the phone.

Headless mode

vellum-fe --frontend headless

This runs the core and the web server with no local UI at all. It prints the ready /play URL (token included) at startup and the browser does the rest. Give it credentials (--direct --account … --character …) to auto-connect, or give it nothing and it waits at the login screen.

The login screen is the same overlay the phone apps show. In a desktop browser it has three tabs:

  • play.net (default) — play.net account, password, character, a game selector covering every GemStone IV and DragonRealms world, and Remember this login. Submit is Connect.
  • Lichhost, port, label (optional), and Remember this connection (ticked by default). It attaches to a Lich session already running elsewhere; launch that Lich with --detachable-client. The tab also holds a custom launch command (optional) textarea: with one set, connecting probes the port first, SSHes to the host and runs the command if Lich is down, then attaches once the port opens. Configure the SSH side in ⚙ → SSH launcher (cold-start Lich).
  • Remote — points this browser at a different machine’s VellumFE web server using its host, port, and pairing token.

⚠️ A phone shows two tabs, not three. In the Android and iOS apps the in-page Remote tab is hidden on purpose and a native Characters picker replaces it — reached from the person icon on the login screen. The picker exists so the app can scan pairing QR codes with the camera and seal saved servers into the Keychain or Keystore. Same capability, better home.

Headless sessions look after themselves: drops reconnect with backoff and typing resets it; repeated drops with no input from you stop the reconnect loop so an abandoned session winds down instead of relogging all night; a hung login is retried by a watchdog; and quit returns to the login screen.

Tips & gotchas

⚠️ bind = "127.0.0.1" is this PC only. The most common failure is a phone that cannot reach the URL at all. .webinfo tells you when this is the cause, printing: bind = “127.0.0.1” is this PC only. Set [web] bind = “0.0.0.0” so phones on your LAN can connect.

⚠️ Security: keep the port on a network you trust. Pairing keeps strangers out, but the traffic is plain HTTP. For play away from home use Tailscale or WireGuard — never forward this port to the open internet. .webinfo repeats this line every time you run it.

  • .webinfo refuses when there’s nothing to pair. “Web server is disabled” means the [web] block is off and you never passed --web-port. “Web server is not running (bind failed or still starting)” means the port was taken or the server is a beat behind — run it again.
  • The pairing page shows two QR codes. The browser one carries an http://…/#token=… URL for any browser. The VellumFE app one carries a vellum://remote?… link only the Android or iOS app understands. Scanning the app QR into a plain browser does nothing.
  • A sidecar has no login screen. The desktop session owns the connection, so the browser gets no account fields and no login music toggle. That screen appears only in headless mode and in the phone apps.
  • After editing macros.toml on the PC, run .reloadmacros — connected phones update instantly rather than on next connect.
  • The wake button in the top bar keeps the phone’s screen on. Tap the title line to toggle between room name and character name.

See also

Config reference (TOML)

The [web] block in config.toml (or a character’s profile config). The Launcher’s Advanced fold and the phone’s Client settings (saved on host) sheet both write these fields, so hand-editing is a troubleshooting path, not the intended one.

[web]
enabled = true
port = 8040
bind = "0.0.0.0"
pinned = false
FieldTypeDefaultWhat it does
enabledboolfalseTurns the embedded web server on for this session. --web-port <n> enables it for one run without editing config.
portu168040The HTTP + WebSocket port. When pinned = false this is a base port: an instance that finds it taken walks upward to the next free one, so several characters launch without any per-character config.
bindstring"127.0.0.1"The bind address. The default serves this machine only. Set "0.0.0.0" deliberately to let phones and tablets on your LAN connect.
pinnedboolfalseBinds exactly port or disables web for the session with a loud warning — never a silent neighbor port. Set this in a character’s profile config when you want a stable /play bookmark for that character.

Routes served: / is the multi-session dashboard, /play is the game client, and /health is a token-free reachability check (it is what puts the live or offline dot next to a saved server in the phone apps’ Characters picker).

Android App

The whole client on your phone — log in, hunt, and script from a bus stop with no PC involved. (in progress)

What it’s for

You want to play GemStone IV without being at your desk. The Android app carries the same Rust core the desktop client runs, so the phone connects to play.net by itself: enter your account, pick a character, and you are in the game. Nothing has to be running at home.

When something is running at home, the app plays that too. It attaches to a Lich session on your PC so a scripted character keeps all its scripts, and it can pair with a VellumFE session already running on your desktop and become a live second screen for it. The app is a full client and a pairing client — not one or the other.

📷 screenshot pending
The Android app mid-hunt: story pane with stream chips, the vitals strip, the floating compass, and the macro rail.

Set it up

There is no Android app on the desktop — but the desktop is where you prepare the two things the phone will reach for.

  1. To let the phone mirror this session: open the connection’s Advanced fold in the VellumFE Launcher, tick Enable on port under Web dashboard, and set Bind address to 0.0.0.0. Launch, then type .webinfo and leave the pairing page open — the phone scans the VellumFE app QR from it.
  2. To let the phone attach to Lich: launch Lich with --detachable-client and note the host and port you gave it.

Expected result: a pairing page on screen with two QR codes, and a Lich host and port written down. The phone does the rest.

⚠️ The app shows two login tabs; a desktop browser shows three. The browser’s in-page Remote tab is hidden here on purpose, because the native Characters picker replaces it — the picker can use the camera to scan pairing QR codes and can seal saved servers into the Android Keystore, which a web form cannot. Nothing is lost; it moved.

Common setups

Take your desk session with you

  1. On the PC, launch your character with web enabled on 0.0.0.0 and run .webinfo.
  2. On the phone, open Characters⧉ Scan QR to add and scan the VellumFE app QR. The entry names itself after the character, because the link carries the character name.
  3. Tap the new row.

→ Your PC session keeps its own GUI window open and playing; the phone shows the same character live. Lich’s one-client limit does not bite here — the web layer mirrors the session rather than replacing its client.

Add a server by hand when there’s no QR to scan

  1. Characters+ Add manually. The dialog is titled Add character.
  2. Fill in Label (required, e.g. Rysk), Host (e.g. 192.168.1.21), Port (e.g. 8042), and Pairing token (optional). Tap Save.
  3. The row appears with a dot: green and live when the machine answers, grey and offline when it does not.

→ You can tell at a glance whether the PC is up before you tap, without waiting on a failed connection.

Playing

The screen is the browser client — story pane, stream chips, tappable exits and nouns, macro tray, status drawer with the injury doll and character sheet, sounds, and the highlight and color editors. Everything on that page applies here, including the fact that the phone’s highlight editor does handle redirects and squelch.

Battery & lifecycle

The session lives in a foreground service with a quiet status notification (“Playing — session live”, “Reconnecting…”) carrying a Stop button.

  • The wakelock is held only while a session is active — sitting at the login screen does not drain the battery.
  • Swiping the app away mid-session keeps playing. Swiping it away at the login screen stops the service.
  • Repeated drops with no input from you stop the reconnect loop, so a forgotten phone winds down instead of relogging all night.
  • On first launch it asks once for a battery-optimization exemption so Android does not throttle the connection mid-hunt. Decline it and you can grant it later in system battery settings.

Tips & gotchas

⚠️ On Android 8 and 9, update Chrome — not just Android System WebView. Those releases take the rendering engine from Chrome. If the client is blank or stuck on “connecting…”, update Chrome from the Play Store first. On Android 10 and newer, update Android System WebView.

  • Deleting a saved server is the on its row, which asks “Delete {name}?” with the note “Removes the saved server (and its pairing token).” Deleting the entry deletes the pairing — you re-scan to come back.
  • Saved credentials never sit in plaintext. A remembered play.net password is sealed by the Rust core with a device master key, and saved servers live in remote.bin sealed by an Android Keystore key. Neither is readable off the device, and neither uses SharedPreferences.
  • Scanning the wrong QR does nothing. The pairing page shows two: the browser one (http://…/#token=…) is for a browser, the VellumFE app one (vellum://remote?…) is for this app.
  • A vellum://lich?host=…&port=… link prefills the Lich tab and stops there. It never auto-connects — you always press Connect yourself.
  • The phone needs a private path to your PC. Home Wi-Fi, Tailscale, or a VPN. Never the open internet, for either the Lich port or the VellumFE web port.

See also

  • Browser client — the interface this app renders, and the [web] settings on the PC side.
  • iOS app — the same client on iPhone.
  • Installation — sideloading and updates.
  • The Launcher — where Web dashboard and Bind address live on the desktop.

iOS App

The whole client on your iPhone — log in and play with no PC involved. (beta — via TestFlight)

What it’s for

The iOS app carries the same Rust core the desktop client runs, so the phone talks to play.net by itself. Account, character, game world, Connect — you are in, with nothing running at home.

And when something is running at home, the app plays that as well: it attaches to a Lich session on your PC so a scripted character keeps its scripts, and it pairs with a VellumFE session already running on your desktop to become a live second screen for it. The app is a full client and a pairing client — not one or the other.

📷 screenshot pending
The iOS app in a hunt: story pane with stream chips, vitals strip, floating compass, and the macro rail.

Set it up

There is no iOS app on the desktop — but the desktop prepares what the iPhone will reach for.

  1. To let the iPhone mirror this session: in the VellumFE Launcher, open the connection’s Advanced fold, tick Enable on port under Web dashboard, and set Bind address to 0.0.0.0. Launch, then type .webinfo and leave the pairing page on screen — the iPhone scans the VellumFE app QR from it.
  2. To let the iPhone attach to Lich: launch Lich with --detachable-client and note the host and port.

Expected result: a pairing page showing two QR codes, plus a Lich host and port to hand to the phone.

⚠️ The app shows two login tabs; a desktop browser shows three. The browser’s in-page Remote tab is hidden here deliberately, because the native Characters picker replaces it — the picker can scan pairing QR codes with the camera and store saved servers in the iOS Keychain, which a web form cannot. The capability moved; it was not removed.

Common setups

Carry your desk session to the couch

  1. On the PC, launch the character with web enabled on 0.0.0.0 and run .webinfo.
  2. On the iPhone, open CharactersScan QR to add and scan the VellumFE app QR. The saved entry names itself after the character, because the link carries the character name.
  3. Tap the new row.

→ The PC session keeps its own window open and playing while the iPhone shows the same character live. Lich’s one-client limit does not apply — the web layer mirrors the session rather than replacing its client.

Add a server by hand when there’s no QR

  1. CharactersAdd manually. The sheet is titled Add character.
  2. Fill in Label (required, e.g. Rysk), Host (e.g. 192.168.1.21), Port (e.g. 8042), and Pairing token (optional). Tap Save.
  3. The row shows a dot: green and live when the machine answers, grey and offline when it does not.

→ You can see whether the PC is awake before tapping, instead of waiting out a failed connection.

Playing

The screen is the browser client — story pane, stream chips, tappable exits and nouns, macro tray, status drawer with the injury doll and character sheet, sounds, and the highlight and color editors. Everything on that page applies here, including the fact that the phone’s highlight editor does handle redirects and squelch.

Backgrounding

iOS has no equivalent of Android’s foreground service. When the app goes to the background iOS suspends it and the game connection goes stale.

  • On return, the session reconnects automatically and resumes where you left off. Expect reconnect-on-return rather than a keep-alive.
  • Repeated drops with no input from you stop the reconnect loop, so a forgotten phone winds down instead of relogging all night.
  • While you are on a paired desktop session, the app’s embedded core sits idle — there is no game connection on the phone to go stale, and the usual web reconnect picks the mirror back up when you come back.

Tips & gotchas

⚠️ Deleting a saved character deletes its pairing token with it. Swipe a row to delete, or tap Edit in the toolbar for the multi-select delete. To come back you re-scan the QR or re-enter the token.

  • Saved credentials never sit in plaintext. Saved servers live in the iOS Keychain under service dev.vellumfe.remote-server, accessible only after the first unlock and only on that device (never in a backup). A remembered play.net password is sealed by the Rust core with a device master key.
  • Scanning the wrong QR does nothing. The pairing page shows two: the browser one (http://…/#token=…) is for a browser; the VellumFE app one (vellum://remote?…) is for this app.
  • A vellum://lich?host=…&port=… link prefills the Lich tab and stops there. It never auto-connects — you always press Connect yourself.
  • The iPhone needs a private path to your PC. Home Wi-Fi, Tailscale, or a VPN. Never the open internet, for either the Lich port or the VellumFE web port.
  • The way back off a paired session is ⚙ → Leave this server (app login), which returns you to the two-tab login screen.

See also

  • Browser client — the interface this app renders, and the [web] settings on the PC side.
  • Android app — the same client on Android.
  • The Launcher — where Web dashboard and Bind address live on the desktop.

Widgets

Your layout is made of windows, and each window is wired to one thing the game tells you. This page is the map of what you can wire up.

What a widget is here

A window is a box on your screen. A widget is what fills it — and every widget type is bound to one specific feed coming off the wire. The compass window draws the exits the game sent with the room. The health bar moves because the game pushed a number named health. A targets window lists creatures because the room’s object line marked them bold.

That binding is the whole idea, and it has a practical consequence: you do not configure a widget to go find data — you place the window and the feed arrives. A bar with no feed id draws nothing at all, not an error. A window bound to a feed your game never sends stays empty forever.

So the question this section answers is not “how do I make a health bar work.” It is “which of these thirty-odd things do I want on screen, and where do I click to get it.”

📷 screenshot pending
The Windows catalog with every category collapsed, showing the groups — Status, Progress Bars, Countdowns, Active Effects, Entities, Hands, Text Windows, Character, Navigation, Hotbars, Containers, Dialogs, Other — in the order the client lists them.

The roster

These are the groups the in-app Windows catalog uses, in the order it shows them. Inside each group, rows are sorted alphabetically by the window’s title, which is not always the type string you would type — the Type to add column is the string, and it is the one that matters at the command line.

GS4 marks a widget offered only to GemStone IV characters; DR marks DragonRealms-only. Everything unmarked appears for both.

Status

Row readsType to addWhat it’s forPage
DashboarddashboardA grid of many status lights in one windowDashboard
Bleeding · Dead · Diseased · Hidden · Invisible · Joined · Kneeling · Poisoned · Prone · Sitting · Standing · Stunned · WebbedindicatorOne light per condition; the window’s name is the status idIndicators

The thirteen indicators sit in a nested Indicators fold inside this group, so the Dashboard row isn’t buried under them.

Progress Bars

Row readsType to addWhat it’s forPage
Health · Mana · Stamina · SpiritprogressOne bar per vitalProgress Bars
Concentration DRprogressThe DragonRealms concentration poolProgress Bars
StanceprogressYour current stance, as a barProgress Bars
minivitals GS4minivitalsAll your vitals as one compact stripMini Vitals

Custom bars are not a catalog row — see Route 2.

Countdowns

Row readsType to addWhat it’s forPage
RTcountdownRoundtime, counting downCountdowns
CastcountdownCast timeCountdowns
StuncountdownStun timeCountdowns

Active Effects

Row readsType to addWhat it’s forPage
Active Spellsactive_effectsSpells currently on you, with time leftActive Effects
Buffsactive_effectsBeneficial effectsActive Effects
Debuffsactive_effectsHarmful effectsActive Effects
Cooldownsactive_effectsAbilities you’re waiting onActive Effects

All four are the same widget with a different Category setting. One type string, four presets.

Entities

Row readsType to addWhat it’s forPage
TargetstargetsHostile creatures in the room; click to targetTargets
PlayersplayersWho else is here, with status tagsPlayers
ItemsitemsObjects on the groundItems

Hands

Row readsType to addWhat it’s forPage
Left Hand · Right Hand · SpellhandWhat you’re holding, and what you have preparedHands

Text Windows

Row readsType to addWhat it’s forPage
StorytextThe main game feedText Windows
Thoughts · Speech · Announcements · Loot · Death · Logons · Familiar · Ambients · Bounties · Society TaskstextOne window per stream, so it stops interrupting your story paneText Windows
ChattabbedtextSeveral streams as tabs in one windowTabbed Text

Character

Row readsType to addWhat it’s forPage
InventoryinventoryWhat you’re carryingInventory
Reserve GS4reserveWhat the game is holding aside for youReserve
SpellsspellsYour known-spells snapshot from loginSpells
Missing SpellsmissingspellsWatched spells that are not currently upMissing Spells
Injuriesinjury_doll (or injuries)A body diagram of your wounds and scarsInjury Display
EncumbranceencumHow weighed down you areEncumbrance
Experience GS4gs4_experienceLevel, mind state, progress to next levelExperience (GemStone IV)
Experience DRexperienceYour skill list and its field experienceExperience (DragonRealms)
Perceptions DRperceptionSorted perception entriesPerception

Both Experience widgets carry the title Experience and both live in Character, but they are different widgets on opposite game gates. You only ever see the one for your game.

Row readsType to addWhat it’s forPage
RoomroomRoom name, description, exits, and what’s in itRoom Window
CompasscompassThe exits, as a rose you can clickCompass
MapmapA live mini map; click a room to walk thereMap

Hotbars

Row readsType to addWhat it’s forPage
ActionshotkeybarButtons you design, that recolor on your stateHotbars
QuickbarquickbarButtons the game and your scripts push at youQuickbar

The Actions row is the hotbar widget — the title differs from the type string.

Containers

Row readsType to addWhat it’s forPage
(one row per bag you’ve opted in)containerThe contents of one specific containerContainers

Container rows are not fixed. A bag appears as a row once the game has shown you inside it this session, and ticking the row is what creates the window. There is no container row in a fresh catalog.

Dialogs

Row readsType to addWhat it’s forPage
Betrayer GS4betrayerBlood points and the Betrayer item listBetrayer

Not in the catalog at all

Six types exist and work, but no picker offers them. The only way to place one is to type the full .addwindow form.

Type to addWhat it’s forPage
command_input (or commandinput)The input line itself. Your layout already has one.Utility & Layout Widgets
spacerBlank filler that reserves space in a layoutUtility & Layout Widgets
containerOne bag’s contents — reachable by ticking a bag row, never by a fixed catalog entryContainers
performanceFrame timings and message throughputPerformance Monitor
webui (or lichui)A Lich WebUI page drawn as a native windowUtility & Layout Widgets
dialogpanel (or dialog_panel)A resident game dialog, addressed by its idUtility & Layout Widgets

How to add one

There are four routes, and they are not interchangeable — one of them isn’t something you do at all.

Route 1: the Windows catalog (GUI)

Click Windows in the top toolbar. The catalog stays open while you work. Every category is collapsed when it opens, so expand the group first, then tick the row. Ticking shows the window; unticking hides it. Each row also has a zone dropdown that places the window without your dragging it.

This is the route to teach a new player. It never asks for a type string, so none of the title-versus-type traps below can bite.

Route 2: the Custom window menu (GUI)

Six of the widgets are blanks rather than presets — a bar with no feed, a text window with no stream. Those are not catalog rows. They live under ➕ Custom window… at the top of the catalog, labelled Text, Tabbed text, Progress bar, Countdown, Entity list, and Active effects.

Creating one auto-opens its settings menu, because a custom bar or timer with no feed id renders as nothing until you give it one.

Route 3: .addwindow (works everywhere, including the GUI input)

.addwindow takes one argument or six or more — never two through five.

  • .addwindow with no arguments opens a picker, grouped by the same categories.
  • .addwindow <name> <type> <x> <y> <width> [height] creates a window directly.

⚠️ .addwindow players on its own adds nothing. Two-to-five arguments prints a usage line and stops. This surprises people constantly, because that middle form looks like the obvious one.

The <name> is a free label of your choosing — it does not have to be a catalog key, and the window is built from <type>, not from a preset. The exceptions where the name is load-bearing are called out below.

Route 4: the game does it for you

Some windows are not something you add. When the game first sends the feed, the client creates the window itself and binds it. GemStone IV’s Experience window works this way — the first time the game opens its experience dialog, the window appears. Re-sends never duplicate it.

The same discovery machinery is why a stream you’ve never seen before can turn up as an offered row later in the session.

The rules that apply to every widget

These hold for every type in the roster, so the individual pages don’t repeat them.

⚠️ An unrecognized type string does not error — it silently gives you a text window. Type .addwindow x activeeffects 0 0 30 10 and you get a blank text box, not a complaint. The type strings are exact: active_effects (never activeeffects), missingspells (never missing_spells), encum (never encumbrance). If a new window came up empty and boring, check your spelling before you check the feed.

⚠️ Catalog rows are labelled by the window’s TITLE, and several titles differ from the type string. The row that reads Encumbrance is type encum. The row that reads Actions is type hotkeybar. Story is text, RT is countdown, Injuries is injury_doll. Clicking the row is always safe; typing what the row says is not.

⚠️ Right-click is always the WINDOW menu. There is no per-row context menu anywhere — not on a target, not on an item, not on an inventory line. Left-click a row to act on that thing; right-click to configure the window holding it.

A few types accept two spellings. injury_doll and injuries are the same widget; so are command_input and commandinput, webui and lichui, dialogpanel and dialog_panel. Most types accept exactly one spelling, so don’t generalize from these four.

Not every widget has a settings section. The GUI’s right-click menu grows a widget-specific section only for types that have their own settings. Sixteen do: Countdown, Progress, Active Effects, Room, Targets, GS4 Experience, Encumbrance, MiniVitals, Tabbed Text, Hotkeybar, Indicator, Map, Injury Doll, Compass, Hand, Dashboard. Every other type — including Inventory, Reserve, Container, Players, Items, Spells, Missing Spells, Quickbar, Perception, Betrayer, Spacer, and the DragonRealms Experience widget — has no widget section, and that is not a bug. Their appearance and framing still live under Appearance; they have nothing else to configure.

A window’s name is usually a label, but twice it is a binding. An indicator window’s name is the status id it watches — there is no picker. A hotkeybar window shows the bar whose name matches the window’s name. For a countdown, the name works as a fallback feed id, which is why .addwindow roundtime countdown … works with nothing else set. A progress window has no such fallback — naming one health does nothing at all; it needs the Bar id set.

Feed ids are case-sensitive, and a widget bound to nothing draws nothing.

Game gating hides rows; it does not block the command. Both pickers filter by your game, but the six-argument .addwindow form does not check. You can force a GS4-only window onto a DragonRealms character and it will build, sit there, and stay empty. An unset game counts as GemStone IV, which is why GS4 rows show up when you connect through Lich without naming a game.

Placing, moving, and removing works the same for all of them. Drag to move, drag an edge to resize, .hidewindow to hide, .deletewindow to truly remove (deleted windows are stashed and restorable from ↩ Restore deleted…). None of that varies by widget type.

See also

Config reference (TOML)

Every window in your layout is one [[windows]] block in layout.toml. The catalog and .addwindow write these for you; hand-editing is for troubleshooting.

These keys are shared by every widget type. Type-specific keys are documented in each widget’s own page.

FieldTypeDefaultWhat it does
namestringUnique id. Load-bearing for indicator, hotkeybar, and countdown.
widget_typestring"text"Which widget fills the window. Unknown values fall back to text silently.
rowinteger0Top edge
colinteger0Left edge
rowsinteger10Height
colsinteger40Width
titlestringper-typeBorder title, and the label the GUI catalog sorts and displays by
show_borderbooleantrueDraw the frame
border_stylestring"single"single, double, rounded, thick, quadrant_inside, quadrant_outside
border_colorstringthemeHex color, or - to take the theme’s
lockedbooleanfalseRefuse move and resize
[[windows]]
name = "targets"
widget_type = "targets"
title = "Targets"
row = 0
col = 92
rows = 10
cols = 28
show_border = true

Text Windows

Give a stream its own window so the traffic you care about stops scrolling past in the middle of a fight.

What it’s for

GemStone sends far more than room descriptions down the wire. Thoughts, deaths, logons, ESP, bounty updates, society chatter, and your own combat all arrive as separate streams — and by default most of them stack into one column with everything else. The result is familiar: a death message you wanted to read gets three lines of swing-and-miss on top of it before your eye lands on it.

A text window is one scrollable pane bound to one or more of those streams. Point a window at death and death messages go there and only there. Point one at a Lich script’s own stream id and you have a purpose-built readout for that script. Your story text gets quieter, and the things you were squinting for get a place to sit still.

This is also how custom windows work. There is no separate widget for “a window for my script” — a custom window is a text window subscribed to whatever stream id the script pushes.

Set it up

1. Create the window.

Click Windows in the top toolbar, then ➕ Custom window…Text. The catalog stays open while you work.

Many streams already have a stock row in the catalog — Thoughts, Deaths, Logons — so scroll the list first and tick the row if it’s there. Categories start collapsed, so expand a heading before deciding a row is missing. Use ➕ Custom window… when you want a window for a stream that has no stock row, such as a Lich script’s own id.

2. Point it at a stream.

Right-click the new window and open the Window section. Type stream ids into the Streams field, comma-separated, and press Enter to commit.

You don’t have to know the ids by heart. Click the + beside the field for a menu of every stream that has actually arrived this session, and pick one to append it.

3. Set the buffer.

Buffer lines in the same section caps how much scrollback the window keeps. A busy combat window is fine at a few hundred; a thoughts window you want to read back through wants more.

📷 screenshot pending
The window right-click menu's Window section, with the Streams field, its + seen-stream picker open, and Buffer lines below.

Everything in this menu applies live — there is no Save button. Text fields commit when you press Enter or when focus leaves the field.

The rest lives one fold deeper. Window ▸ Advanced holds Compact, Timestamps, and Timestamp at line start. Font, text size, Word wrap, and content alignment are under Appearance ▸ Text.

Expected result: the window appears, and the next line the game sends on that stream lands in it instead of your main window.

Common setups

A quiet death log you can actually read back

Deaths matter and they’re easy to miss. Give them a window with timestamps so you can tell whether that was thirty seconds ago or twenty minutes.

In the GUI: Windows➕ Custom window…Text, then right-click it ▸ Window ▸ set Streams to death and Buffer lines to 500. Open Window ▸ Advanced and tick Timestamps, then tick Timestamp at line start so the time leads each line and the names stay aligned down the left.

In the TUI: .addwindow deaths text 80 0 40 8, then .editwindow deaths, put death in Streams, tick timestamps, Ctrl+S.

You’ll see: a short column of death notices, each stamped with its time, sitting still while your main window scrolls.

A bounty window that stops eating a third of your screen

Bounty text is verbose. Compact rewrites it into 1-4 lines, which turns a paragraph into a glanceable objective.

Make a text window subscribed to bounty, then right-click ▸ WindowAdvanced ▸ tick Compact.

You’ll see: your current task condensed to a few lines — and the change applies to the bounty already on screen, not only the next update.

⚠️ Compact only affects the bounty stream. It is not a general blank-line remover or a whitespace trimmer. Ticking it on a thoughts or combat window changes nothing at all, which looks like a broken checkbox but is the flag correctly doing nothing outside its one job.

Tips & gotchas

A window that subscribes to a stream always wins. Stream routing has a strict order: a subscribed window first, then a [streams.routes] entry, then the fallback window, then main as a last resort. So adding a stream to a window’s Streams field is all it takes — you never also have to write a route.

Routes only decide where an unsubscribed stream goes. Set one when you want a stream discarded, forced to main, or parked in a window that isn’t subscribed to it. To see the whole picture at once, open the Streams panel — EditorsStreams & Custom Windows, or type .streams. It lists every known stream with its effective destination and lets you change it in place.

⚠️ A route pointing at a window that doesn’t exist does not create one. Windows are never auto-created or auto-opened to satisfy a route; the stream quietly falls through to your fallback window instead. If a route seems ignored, check the window name still exists.

A hidden window still fills its buffer. Hiding is presentation only — text routed to a hidden window accumulates and is waiting when you show it again. That’s the difference between Hide and Delete.

⚠️ .deletewindow truly removes a window in both desktop frontends; .hidewindow only hides it. There is no GUI-versus-TUI asymmetry here. Delete stashes the window so Windows↩ Restore deleted… can bring it back with its streams intact, and both frontends refuse to remove your only main-feed window.

⚠️ In the Terminal (TUI), Ctrl+C copies your selection — it does not quit. In the Desktop GUI the same combination quits. Leave the TUI with .quit or .exit.

Word wrap is not purely cosmetic here. For a text window the Appearance ▸ Text ▸ Word wrap checkbox writes to the shared layout definition, so it travels with the window into the TUI and into layout.toml. Most other Appearance settings are GUI-only.

Per-window timestamp settings override the global one. ui.timestamp_position sets the default for everything; a window that has its own Timestamp at line start state ignores it. Change the global in Settings and a window you’ve already customized will not follow.

Scrolling: PageUp / PageDown move a 20-line page in the focused TUI window, Alt+PageUp / Alt+PageDown move a single line, and the mouse wheel scrolls whatever is under the cursor in both desktop frontends. A window auto-scrolls with new text unless you have scrolled back, so reading history doesn’t get yanked out from under you.

See also

Config reference (TOML)

A text window is widget_type = "text". Reference only — the Streams field and .editwindow write all of this for you.

[[windows]]
name = "deaths"
widget_type = "text"
streams = ["death"]
row = 0
col = 80
rows = 8
cols = 40
buffer_size = 500
show_timestamps = true
timestamp_position = "start"

Text widget fields

FieldTypeDefaultWhat it does
streamsarray of string[]Stream ids this window subscribes to. A subscribed window outranks every [streams.routes] entry.
buffer_sizeinteger10000Lines of scrollback kept in memory.
wordwrapbooltrueWrap long lines to the window width. Shared with the GUI’s Appearance ▸ Text ▸ Word wrap.
show_timestampsboolfalsePrefix or suffix each line with its arrival time.
timestamp_position"start" / "end"inherits ui.timestamp_position ("end")Where the timestamp sits. Set here, it overrides the global setting for this window only.
compactboolfalseCondenses verbose bounty text to 1-4 lines. No effect on any other stream.

Position and size (row, col, rows, cols) and the appearance fields are common to all windows — see layout.toml.

Stream routing (config.toml)

Routing decides where a stream goes when no window subscribes to it.

[streams]
fallback = "main"

[streams.routes]
bounty = "window:bounty"
ambients = "discard"
loot = "main"
FieldTypeDefaultWhat it does
fallbackstring"main"Window that receives any unsubscribed, unrouted stream.
routes.<stream>"discard" / "main" / "window:<name>"nonePer-stream destination for unsubscribed streams. Any other value is rejected when the config loads.
room_in_mainbooltrueDragonRealms: keeps <streamWindow id='room'> from switching the current stream.

A window: route naming a window that doesn’t exist falls back to fallback, then main. The older drop_unsubscribed list is migrated automatically to routes.<id> = "discard" on load.

Common stream ids

StreamContent
mainPrimary game output
speechPlayer dialogue
thoughtsESP / telepathy
deathDeath messages
familiarFamiliar messages
logonsLogin and logout notices
societySociety messages
bountyBounty information
lootLoot messages
announcementsAnnouncements
ambientsAmbient messages

streams is not limited to this list. Any id a Lich script pushes can feed a text window — that is exactly what a custom window is.

Tabbed Text

Stack several streams into one window and let the tab badges tell you which one moved.

What it’s for

Giving every stream its own window works right up until you run out of screen. Speech, thoughts, whispers, group chat, deaths, logons — six windows down a 40-column side bar leaves nothing for the game itself, and most of them sit empty most of the time anyway.

A tabbed text window puts them in one frame with a tab strip. You read one tab at a time, and the other tabs mark themselves unread when something lands. That’s the real payoff: you stop paying screen rent for streams that are quiet, without going blind to them. A whisper arrives while you’re watching combat, the Whispers tab lights up, and you click over when the swing is done.

Tabs you’d rather not be nagged about — a chatty group channel, a logon feed — can be set Quiet, so they still collect text but never mark themselves unread.

Set it up

1. Create the window.

Click Windows in the top toolbar, then ➕ Custom window…Tabbed text.

2. Open the Tab Editor.

Right-click the new window and choose Edit tabs… from the window’s own section. This opens the Tab Editor, a separate panel rather than an inline menu fold.

⚠️ The Tab Editor is Save-buffered — the rest of the right-click menu is not. Everywhere else in a window’s menu your change applies the instant you make it. Here, nothing takes effect until you click Save. Closing the panel without saving discards your edits.

3. Add your tabs.

Two routes, and they work together:

  • The Known streams checklist. Tick a stream to give it its own tab, named for the stream. Untick it to remove it from every tab. This is the fast way to build a window from scratch. The list covers every stream the client knows about — the well-known ones plus anything your sessions have ever declared — not only what has arrived today.
  • The grid. One row per tab: Name, Streams, Quiet, TS, reorder, Remove. Use it to rename a tab, put several streams on one tab, or set the order. The + beside a row’s Streams field lists streams seen this session and appends one to that tab.

Reorder with the and buttons on a row — the tab strip follows the grid’s order.

📷 screenshot pending
The Tab Editor: the grid of Name / Streams / Quiet / TS rows with reorder and Remove, the Known streams checklist below, and Add tab and Save.

4. Save.

Click Save. Two rules are enforced: a tabbed window needs at least one tab, and every tab needs a name — you’ll get a message rather than a silent failure if either is broken.

Expected result: the tab strip redraws with your tabs in order, and the next line on any subscribed stream lands on its tab, badging that tab unread if it isn’t the one you’re reading.

Common setups

A communication hub

One window carrying every conversational channel, with the noisy one muted.

Make a Tabbed text window and open Edit tabs…. Build four rows:

NameStreamsQuietTS
Speechspeech
Thoughtsthoughts
Whisperswhisper
Groupgroup

Timestamps on the three you’ll read back through; Quiet on Group so a busy party channel doesn’t badge the window every few seconds. Click Save.

You’ll see: one window where four used to go, with Whispers badging itself the moment someone whispers to you — and Group collecting quietly, unread mark never lighting.

An activity feed for a hunting screen

Combat, deaths, and arrivals in one 40-column pane so your side bar keeps room for targets and a compass.

Tabs: Combat on combat, Deaths on death, and Arrivals on logons with Quiet ticked — you want to be able to check who walked in without being pulled away from a fight every time somebody logs on.

In the TUI, that’s .addwindow activity tabbedtext 80 0 40 12, then .editwindow activity and A three times.

You’ll see: your fight in the front tab, a Deaths tab that badges when something dies near you, and an Arrivals tab that fills silently until you go looking.

Tips & gotchas

⚠️ The Tab Editor is Save-buffered; the rest of the window menu applies live. This is the one place in the GUI’s per-window settings where your changes are not already in effect. If tab edits “didn’t take,” you closed the panel without clicking Save.

Unticking a stream in Known streams removes it from every tab, not just the one you’re looking at. If that untick leaves a tab with no streams, the tab goes away. Tabs that were already empty — a row you just added and haven’t filled — are left alone. Remove a stream from one tab only by editing that row’s Streams field instead.

The two stream pickers in the Tab Editor are different lists. The Known streams checklist is the full universe the client knows about. The per-row + offers only streams seen this session. A stream you expect that hasn’t fired yet appears in the checklist but not in the + menu.

Quiet is per tab, and it’s about the unread mark, not the text. A Quiet tab still receives and buffers everything; it just never badges. That’s the setting you want for a channel you check on purpose rather than react to.

buffer_size on a tabbed window is per tab, not shared across the window, and it defaults to 10,000 lines.

⚠️ Word wrap on a tabbed window is GUI-only. For a plain text window, Appearance ▸ Text ▸ Word wrap writes to the shared layout definition and travels to the TUI. A tabbed window has no wordwrap field, so the setting is remembered per GUI tab and does not reach layout.toml or the terminal.

Three tab-switching actions exist, and none has a default key. Next Tab, Previous Tab, and Next Unread Tab live under the Tabs category in the keybinds editor. Bind Next Unread Tab if you like — it jumps straight to whatever just badged, which is the whole point of the badges.

A subscribed tab outranks stream routing. A tab that lists a stream receives it regardless of any [streams.routes] entry; routes only decide where unsubscribed streams go. Check effective destinations in the Streams panel — EditorsStreams & Custom Windows, or .streams.

⚠️ In the Terminal (TUI), Ctrl+C copies your selection — it does not quit. In the Desktop GUI the same combination quits. Leave the TUI with .quit or .exit.

See also

Config reference (TOML)

A tabbed window is widget_type = "tabbedtext", with one [[windows.tabs]] block per tab. Reference only — Edit tabs… and .editwindow write all of this.

[[windows]]
name = "comms"
widget_type = "tabbedtext"
row = 0
col = 80
rows = 20
cols = 40
buffer_size = 3000

[[windows.tabs]]
name = "Speech"
streams = ["speech"]
show_timestamps = true

[[windows.tabs]]
name = "Thoughts"
streams = ["thoughts"]
show_timestamps = true
timestamp_position = "start"

[[windows.tabs]]
name = "Group"
streams = ["group"]
ignore_activity = true

Window fields

FieldTypeDefaultWhat it does
tabsarray of tab tables[]The tab list, in display order.
buffer_sizeinteger10000Scrollback lines kept per tab.
tab_bar_positionstring"top"Where the tab strip sits.
tab_separatorboolfalseDraw a separator between tabs.
tab_active_colorstringthemeColor of the active tab label.
tab_inactive_colorstringthemeColor of inactive tab labels.
tab_unread_colorstringthemeColor of a tab with unread content.
tab_unread_prefixstringnoneMarker prefixed to an unread tab’s label.

The six presentation fields from tab_bar_position down have no editor UI today — they are TOML-only. The Tab Editor covers names, streams, Quiet, timestamps, and order.

Tab fields

FieldTypeDefaultWhat it does
namestringrequiredThe tab’s label. Saving with an unnamed tab is refused.
streamsarray of string[]Stream ids feeding this tab. Preferred form.
streamstringnoneLegacy single-stream field. When both are set, streams wins.
show_timestampsboolinherits ui.show_timestampsPer-tab timestamp override.
timestamp_position"start" / "end"inherits ui.timestamp_position ("end")Per-tab timestamp placement.
ignore_activityboolfalseThe Quiet checkbox — this tab never marks itself unread.

A tab with an empty streams array and no stream receives nothing. Saving through the GUI Tab Editor writes streams and clears the legacy stream field, so any tab you touch there is migrated to the plural form.

Progress Bars

Health, mana, stamina and spirit as bars you read in a glance — so you never type health with a troll swinging at you.

What it’s for

Mid-fight you need one number: am I still safe? Typing health costs you a line of scrollback and a beat of attention, and the answer is already three exchanges old by the time you read it. A progress bar keeps that answer on screen permanently, filling and draining as the game sends updates, so checking your health becomes a glance instead of a command.

Each bar is its own window, which is the point — you place health where your eye already goes, put spirit somewhere quieter, and skip the ones you don’t care about. Bars are not limited to vitals either: the same widget draws stance, encumbrance level, mind state, and any bar a Lich script invents, because a bar is really just “a number out of a maximum, drawn as a fill.”

If you’d rather have all four vitals stacked in one compact window, that is a different widget — see Mini Vitals.

Set it up

  1. Click Windows in the top toolbar. The catalog stays open while you work.
  2. Tick health, mana, stamina, and spirit. Each one appears as its own window, already bound to the right feed and colored.
  3. Set each row’s zone control to Header to line them up across the top, or leave them floating and drag them where you want.
  4. Right-click any bar to open its menu. The widget section holds Bar id, Label, Bar color, Show value/max, and Show value only. Changes apply live — there is no Save button, and text fields commit when you press Enter or click away.

For a bar the catalog doesn’t list — a Lich-pushed feed, or encumlevel — use ➕ Custom window… ▸ Progress bar. VellumFE drops you straight into the new window’s menu, because a bar with no Bar id draws nothing at all. Type the feed id there and it comes alive.

📷 screenshot pending
A health bar right-clicked, with the widget section open showing Bar id, Label, Bar color, and the Show value/max and Show value only checkboxes.

Expected result: four bars sit across the top of your layout, each filling to its current value and redrawing the moment the game sends a vitals update.

Common setups

A compact vitals strip that shows numbers, not words

You know which bar is which by position and color, so the word “Health” is wasted space — the numbers are what you actually want.

  1. Add health, mana, stamina and spirit from the Windows catalog and set each row’s zone to Header.
  2. Right-click each bar and tick Show value/max in the widget section.
  3. Drag each bar narrower until the four sit side by side across the header.

→ Your header reads 142/193 88/110 95/95 10/10 in dark red, dark blue, orange and gray — four numbers, one row, no labels.

A stance bar next to your roundtime

Stance is a bar like any other, and it belongs beside the timer that tells you when you can act.

  1. In the Windows catalog, tick stance. It arrives bound to the feed id pbarStance, titled Stance, in navy.
  2. Set its zone to match your roundtime countdown so the two sit together.
  3. Right-click it, clear the Label field, and the bar draws its percentage instead.

→ Dropping to guarded shows the stance bar visibly shorter, right beside your RT bar, so “can I act, and how exposed am I?” is one glance at one corner.

Tips & gotchas

⚠️ A progress window with an empty Bar id renders as nothing. It is not broken and it is not invisible-because-of-color — a bar with no feed has no value to draw. This is why creating one from ➕ Custom window… drops you into its menu immediately. If a bar you made is a blank rectangle, that empty field is almost always why.

⚠️ Bar ids are case-sensitive. mindState works; mindstate matches nothing and draws nothing. The same goes for encumlevel and pbarStance — copy the capitalization exactly.

⚠️ Unlike countdowns, a progress window’s name is not a fallback binding. Naming a window health does not feed it; only the Bar id field does. A countdown window will fall back to its name, which is why .addwindow roundtime countdown … appears to work with no id set and the same trick fails completely for bars.

Nothing on a bar changes color at a threshold. There is no low-health red, no flash, no pulse — a bar draws in one solid color at every value, by design. Alerting on low health is a job for a hotbar button’s condition state, an indicator, a sound, or controller rumble; see Make your health bar shout when you’re hurt, which builds exactly that.

Show value only wins over Show value/max. With both ticked you get the bare current number. The precedence is: value-only, then value/max, then your Label, then a percentage when the label is empty.

Feeds that report a percentage have no maximum. Stance arrives as 0–100 with no max, so Show value/max on a stance bar reads 100/0. Leave the numbers off for percent-style feeds and let the fill do the talking.

Encumbrance has a dedicated widget too. encumlevel works as a Bar id if a plain bar is what you want, but the encum window shows the level name with its own color bands — see Encumbrance.

Colors are per bar, and the defaults are deliberately dark. Health is #6e0202, mana #08086d, stamina #bd7b00, spirit #6e727c, stance #000080 — dark enough that the text drawn over the fill stays readable. If you brighten a bar, check the label is still legible against it.

See also

Config reference (TOML)

Written by the editors above. Hand-editing is for troubleshooting, not the normal path.

[[windows]]
name = "health"          # a label — NOT the feed binding
widget_type = "progress"
id = "health"            # the feed binding
row = 0
col = 0
rows = 3
cols = 20
color = "#6e0202"

Widget fields (ProgressWidgetData):

FieldTypeDefaultWhat it does
idstring(none)Progress feed identifier — the game’s progressBar id. Case-sensitive. With no id the window draws nothing.
labelstring(feed text)Overrides the label drawn over the fill. Empty label draws a percentage instead.
colorstringper-id defaultFill color, #rrggbb or a color name. One solid color at every value.
numbers_onlyboolfalseDraw current/max instead of the label.
current_onlyboolfalseDraw only the current value — no label, no max. Takes precedence over numbers_only.

Standard window keys — row, col, rows, cols, show_border, border_style, background_color, text_color, locked — apply as they do to any window.

Known feed ids. The client does not keep a closed list; it draws whatever progressBar ids arrive. The ones the GUI names are health, mana, stamina, spirit, encumlevel, and mindState, plus any custom id a Lich script pushes. The catalog additionally ships concentration (DragonRealms only) and stance, whose feed id is pbarStance.

Preset defaults: health #6e0202, mana #08086d, stamina #bd7b00, spirit #6e727c, concentration #00a0a0, stance #000080. Each preset is 20 columns wide and 3 rows tall.

Mini Vitals

All four vitals in one strip, the width of a header — for when four separate bar windows are three windows too many.

What it’s for

Four progress bars tell you everything you need, and cost you four windows to place, four borders to align, and four things to re-drag every time you rearrange a layout. Mini Vitals is the same information as one window: health, mana, stamina and spirit drawn side by side across a single row, sized and moved as one thing.

It is also the widget that knows your real numbers. Ordinary bars and the phone’s chrome work in percentages; this one carries 142/193 because the game sends the current value and the maximum together. Pick it when you want a compact strip and exact figures, and pick separate bars when you want each vital in a different corner of the screen.

Set it up

  1. Click Windows in the top toolbar and expand Progress Bars. The row reads minivitals in lowercase — this widget ships with no title, so the catalog falls back to its key. Tick it. (Typed equivalent: .addwindow minivitals minivitals 0 0 80 3.)
  2. Right-click the window and open the Vitals section. It holds Layout (One row or Stacked), Bar height, Bar text, Depleted color, and a Bars shown (in display order) list with a checkbox and ▲ ▼ buttons per bar.
  3. Set Bar text to taste. The choices show you their own output: Health: 191/193, Health: 99%, 191/193, 99%, or No text.
📷 screenshot pending
A Mini Vitals window right-clicked with the Vitals section open, showing Layout, Bar height, Bar text, Depleted color, and the Bars shown (in display order) list with its ▲ ▼ reorder buttons.

Expected result: one window carrying four filled bars that redraw the moment the game sends a vitals update, each reading its current value over its maximum.

Common setups

A Wrayth-style stats strip across the top of the screen

This is what the widget was shaped for: one row, no title, no wasted height.

  1. Add minivitals from the catalog and set its row’s zone to Header.
  2. Right-click it ▸ Vitals ▸ set Layout to One row and Bar text to 191/193 — position and color already tell you which bar is which, so the word is spent space.
  3. Right-click ▸ Appearance ▸ Frame and turn the border off, so the strip reads as part of the chrome rather than a box sitting on it.
  4. Drag the bottom edge until the bars are the height you want, or set Bar height directly.

You’ll see: a single unbroken row across the top of your layout reading four pairs of numbers in red, blue, orange and gray, each fill draining as you take the hit that caused it.

Tips & gotchas

⚠️ In the GUI these settings are shared by every Mini Vitals window; in the terminal they belong to the window. The GUI stores Layout, Bar height, Bar text, Depleted color and the bar list once, in your per-character GUI layout — change them on one window and every Mini Vitals window changes with it. The terminal stores its own set of fields per window in layout.toml. The two frontends do not share these settings at all, so a window you tuned in the GUI arrives in the terminal with terminal defaults, and the reverse.

⚠️ The GUI version draws four bars the terminal version cannot. The GUI’s bar list offers Mind, Encumbrance, Next Level and Blood alongside the four vitals, so one window can carry your health and your encumbrance together. The terminal accepts only health, mana, stamina, spirit and concentration, and silently ignores anything else in the list — a layout using the GUI’s extra bars renders in the terminal with those bars missing rather than with an error.

⚠️ Four bars maximum in the terminal, no cap in the GUI. The terminal’s [ Edit Bars & Colors ] editor refuses a fifth tick. The GUI lets you enable all eight, and they divide the width between them.

This widget and your ordinary bars read the same feed, so both are live at once. The game sends one health update and it lands in every place that wants it: each progress window bound to health, this widget, and the phone’s status row. Running a Mini Vitals strip and a separate health bar is not a conflict and costs you nothing.

The window ships with no title on purpose, which is why the catalog row reads minivitals in lowercase where its neighbours read proper names. Give it a title under Window if you want one, or leave the title bar off entirely.

Current Only beats Numbers Only in the terminal. With both ticked you get the bare current value. The order is: current-only, then numbers-only, then the game’s own text.

Nothing here changes color at a threshold. No low-health red, no flash, no pulse — each bar draws one solid color at every value. Alerting on low health is a hotbar button’s condition state, an indicator, a sound, or controller rumble; see Make your health bar shout when you’re hurt.

Depleted color is the unfilled part of each bar. Leave it empty and the unfilled portion follows the window background, which is usually what you want. Set it to pick out how much of each bar is gone at a glance.

This widget is GemStone IV only. Both pickers hide it from DragonRealms characters — it is absent from the GUI Windows catalog and from the bare-.addwindow picker. The full six-argument form does not check, so it will build the window on a DR character. If the game is not set at all, VellumFE assumes GemStone IV and the row appears, which is why it shows up when you connect through Lich without naming a game.

See also

Config reference (TOML)

Written by the catalog and the editors above. Hand-editing is for troubleshooting, not the normal path.

[[windows]]
name = "minivitals"
widget_type = "minivitals"
row = 0
col = 0
rows = 3
cols = 80
show_border = true
numbers_only = false
current_only = false
bar_order = ["health", "mana", "stamina", "spirit"]

widget_type = "minivitals". The type string is exactly minivitals; an unrecognized type does not error, it quietly creates a text window instead.

Widget fields (MiniVitalsWidgetData) — read by the terminal

FieldTypeDefaultWhat it does
numbers_onlybooleanfalseDraw current/max instead of the game’s text
current_onlybooleanfalseDraw only the current value. Takes precedence over numbers_only.
bar_orderarray of string["health","mana","stamina","spirit"]Which bars, in which order. Names outside health, mana, concentration, stamina, spirit are dropped.
health_colorstring#6e0202Fill color for the health bar
mana_colorstring#08086dFill color for the mana bar
stamina_colorstring#bd7b00Fill color for the stamina bar
spirit_colorstring#6e727cFill color for the spirit bar
concentration_colorstring(none)Fill color for the concentration bar
depleted_colorstring(none)Unfilled portion of each bar. Empty follows the window background.

The desktop GUI reads none of these. Its equivalent settings live in the per-character GUI layout file as one shared vitals config — orientation (horizontal / vertical), bar height (default 18.0, clamped 8–60), text format (label_value_max / label_percent / value_max / percent / none), depleted color, and the bar list (default health, mana, stamina, spirit; the full vocabulary adds mind, encumbrance, next_level, blood).

Catalog preset

Catalog rowCategoryTitleSizeGame
minivitalsProgress Bars(none — the row shows the key)3 rows x 80 cols (fixed at 3 rows; floor 40 cols)GemStone IV only

Rows are pinned at 3 by min_rows and max_rows — one content row plus two borders. The game gate hides the row from DragonRealms characters in both pickers; the six-argument .addwindow form is not gated. An unset game counts as GemStone IV.

Where the settings live

SettingDesktop GUITerminal (TUI)
Layout / orientationRight-click ▸ VitalsLayoutnot offered (always one row)
Bar heightRight-click ▸ VitalsBar heightwindow height
Bar textRight-click ▸ VitalsBar text.editwindowNumbers Only / Current Only
Which bars, and their orderRight-click ▸ VitalsBars shown.editwindow[ Edit Bars & Colors ]
Per-bar colornot offered.editwindow[ Edit Bars & Colors ] ▸ color column
Depleted colorRight-click ▸ VitalsDepleted color.editwindowDepleted

Standard window keys — row, col, rows, cols, show_border, border_style, title, locked — apply as they do to any window.

Countdowns

Roundtime as a shrinking bar in the corner of your eye — so you know when you can swing again without counting in your head.

What it’s for

Roundtime is the metronome of every fight, and the game gives it to you as a number buried in the prompt. Counting it yourself means either firing commands early and eating the “…but you are still recovering” line, or waiting too long and losing the exchange.

A countdown window turns that into something you don’t have to read: a number plus a row of blocks that empties as the seconds burn down. Peripheral vision handles it. When the blocks are gone, you go.

The same widget covers cast time, stun, and anything else with a deadline — a Lich script can push its own timer for a cooldown, a buff about to lapse, or a hunt window closing, and it draws in exactly the same way.

Set it up

  1. Click Windows in the top toolbar, expand Countdowns, and tick RT. It arrives bound to the roundtime feed, titled RT, drawn in red.
  2. Tick casttime as well if you cast — it arrives titled Cast in deep sky blue, so the two never get confused at a glance.
  3. Set both rows’ zone control to Header or Footer, or drag them wherever your eye naturally rests during combat.
  4. Right-click either timer. The widget section holds Timer id, Label, Fill color, and Stay visible at rest. Changes apply live, with no Save button.

For a script-driven timer, use ➕ Custom window… ▸ Countdown. VellumFE opens the new window’s menu immediately, because a countdown with no Timer id renders as nothing. Put the script’s id in that field and the timer starts working the next time the script fires.

📷 screenshot pending
A roundtime window right-clicked, with the widget section showing Timer id, Label, Fill color, and the Stay visible at rest checkbox.

Expected result: swing a weapon and the RT window fills with blocks, then empties one block per second until it clears.

Common setups

A combat corner that never moves

Timers that appear and disappear make your layout twitch, and a bar that jumps into existence is harder to notice than one that was always there.

  1. From the Windows catalog, tick roundtime and casttime, and set both zones to Footer.
  2. Right-click each and tick Stay visible at rest.
  3. Leave the default colors — red for RT, deep sky blue for CT.

→ Both windows sit in the footer permanently, reading RT: 0 and Cast: 0 with empty bars at rest. When you swing, the red one fills and drains without a single element shifting position.

A stun timer that works from the game’s own text

Stun has no timer feed of its own. VellumFE ships regex patterns that read the game’s stun messages and drive a countdown from them, which is already wired up before you touch anything.

  1. Tick stuntime in the Windows catalog. It arrives titled Stun in yellow, bound to the stuntime feed.
  2. Get stunned. The shipped [event_patterns] rules match You are stunned for 3 rounds, multiply the captured rounds by five to get seconds, and push that into the timer. A matching rule on You recover from being stunned. clears it early.

→ A three-round stun fills the Stun window to fifteen seconds and drains it, and recovering before it expires blanks the window immediately rather than letting it run down.

Tips & gotchas

⚠️ A countdown with no Timer id and a non-matching name renders as nothing. The window is there, the border draws, and the inside stays empty forever because no feed reaches it. This is the single most common “my timer is broken” report, and it is why creating one from ➕ Custom window… opens its config straight away.

⚠️ Timer ids are case-sensitive. roundtime works; Roundtime matches nothing.

A timer window matches on its Timer id or its window name. Both are checked, which makes .addwindow roundtime countdown … work with nothing else configured. It also means a window you name after a feed picks that feed up whether you meant it to or not — worth knowing before you name a custom timer casttime.

Timers hide at zero unless you tell them not to. Default behavior is to vanish when they expire, which keeps a busy layout quiet. Stay visible at rest keeps the label and a 0 on screen with an empty bar, so the window never appears or disappears. Pick whichever bothers you less; the setting is per window, so RT can stay put while a rarely-used timer hides.

The block count is bounded by window width, not by the timer. A sixty-second timer in a twenty-column window shows the number 60 and fills every available cell — the blocks measure “how much is left of what fits,” while the number is always exact. Widen the window if you want finer-grained visual resolution on long timers.

Four things can drive a timer. Native roundtime and casttime from the game; a [event_patterns] rule whose event_type matches (this is how stuntime is fed, and stun/rt/ct are aliased to stuntime/roundtime/casttime); or a script sending <vellumTimer id='...' value='epoch'/>, where value is the absolute epoch second the timer ends and 0 clears it.

Countdown color is split across two settings. Fill color in the widget section colors the number and the blocks. The window’s own background is under Appearance ▸ Frame. The shipped presets set their color as the window’s text color, which is why RT is red and CT is blue out of the box.

See also

  • Progress Bars — health, mana, and stamina, the other half of a combat corner
  • Mini Vitals — all four vitals compactly, to pair with your timers
  • Hotbars — buttons that gray out during roundtime, using the same Roundtime active and Casttime active conditions
  • Wire a hotbar for combat — timers and buttons working together
  • Highlight Patterns — the pattern system [event_patterns] sits beside
  • Creating Layouts — placing, zoning, and saving windows
Config reference (TOML)

Written by the editors above. Hand-editing is for troubleshooting, not the normal path.

[[windows]]
name = "roundtime"        # also acts as a fallback feed match
widget_type = "countdown"
id = "roundtime"          # the explicit feed binding
row = 0
col = 0
rows = 3
cols = 20
text_color = "#FF0000"
show_when_zero = true

Widget fields (CountdownWidgetData):

FieldTypeDefaultWhat it does
idstring(none)Countdown feed identifier (XML id). Case-sensitive. Falls back to the window name when unset.
labelstring(window title)Text drawn as the timer’s label.
iconcharThe glyph used for each remaining-second block.
colorstring(theme text)Color of the number and the blocks. Fill color in the editors.
countdown_background_colorstring(window bg)Background behind the timer. Accepts the old key background_color when reading.
show_when_zeroboolfalseKeep the timer visible at rest as label: 0 with an empty bar, instead of hiding at zero. Stay visible at rest in the GUI menu.

Feed ids: roundtime, casttime, stuntime, a custom [event_patterns] event_type, or an id a script pushes via <vellumTimer id='...' value='epoch'/>.

Preset defaults: roundtime is titled RT in #FF0000, casttime Cast in #00BFFF, stuntime Stun in #FFFF00. Each is 20 columns by 3 rows.

Driving a timer from game text. An [event_patterns] entry in config.toml reads the game’s own messages. Event types stun, rt and ct are aliased onto stuntime, roundtime and casttime; anything else becomes a feed id of the same name. These ship enabled:

[event_patterns.stun_rounds]
pattern = '^\s*You are stunned for ([0-9]+) rounds?'
event_type = "stun"
action = "set"
duration = 0                 # 0 = use the captured value
duration_capture = 1         # 1-based regex capture group
duration_multiplier = 5.0    # rounds → seconds
enabled = true

[event_patterns.stun_recovery]
pattern = 'You recover from being stunned\.'
event_type = "stun"
action = "clear"
duration = 0
duration_multiplier = 1.0
enabled = true

Driving a timer from a script. Send <vellumTimer id='dark-cataclyst' value='1764904999'/>, where value is the absolute epoch second the timer ends. A value of 0, or any time in the past, clears it. The tag never renders as text.

Standard window keys — row, col, rows, cols, show_border, border_style, text_color, locked — apply as they do to any window.

Compass

See every exit the room actually has, and click one to walk it — no guessing which directions the room accepts, no typing n into a wall.

What it’s for

The Obvious exits: line tells you where you can go, then scrolls away. So you guess, and the game tells you that you can’t go that way, and you guess again — which is a small tax you pay in every unfamiliar room and a real one when something is chasing you.

The compass shows the current room’s exits as a lit rose, updating the moment you arrive. Lit directions are real exits; the rest stay faint. Click a lit one and you move. It’s the fastest way to feel out an unmapped area, and it never scrolls off.

Set it up

  1. Click Windows in the top toolbar, expand Navigation, and tick Compass. Use the row’s zone control to send it somewhere it’ll live — the Right Bar is a good home. (Typed equivalent: .addwindow compass compass 92 15 28 7.)
  2. Right-click the compass to pick its art. The widget section has a Compass art drop-down listing Skin default, None, and any compass sets you’ve installed. None draws the built-in vector rose, which follows your theme colors.

Install more compass sets with .jinx list and .jinx install.

📷 screenshot pending
The compass window's right-click menu with the Compass art drop-down open on Skin default, None, and an installed set.

Expected result: a rose appears with your current room’s exits lit and the rest dimmed. Clicking a lit direction walks you that way and the rose redraws for the new room.

Common setups

A big rose in the corner of a hunting screen

Add the compass, send it to the Right Bar, then drag its corner out until it’s as large as you want. Right-click ▸ AppearanceFrame and turn the border off so the art sits on your background with nothing boxing it in.

You’ll see: a large, borderless rose you can hit without aiming, with the room’s real exits lit.

A thin strip that doesn’t cost you screen

Drag the compass window’s side edge inward until it’s narrow. The rose holds its size and the window just gets tighter around it, down to a 48-point floor — narrower than any other widget is allowed to go.

You’ll see: a compass small enough to tuck beside a vitals bar while staying clickable.

Tips & gotchas

⚠️ The compass window is free-form — it is not locked to a square. The rose paints as a centered square of whichever side is shorter. Drag one edge and the window grows while the art holds its size and the extra space pads evenly around it. Drag both and the art scales up with the window.

⚠️ Compass windows have a 48-point minimum width; every other widget stops at 120. If you’re wondering why the compass shrinks past where your other windows stop, that’s why.

“Skin default” and “None” are different answers. Skin default takes whatever rose your active skin ships, so changing skins changes your compass. None pins you to the built-in vector rose, which follows your theme’s link color for lit exits and stays put across skins.

Up, down, and out are in the rose, not beside it. Skinned sets have them drawn in; the vector rose puts them at the hub. Nothing lives in a side column, which is what lets the window shrink to 48 points and stay usable.

A dim direction is not a broken compass. The rose lights what the game reports for the current room. Unlit means the game didn’t list that exit — including hidden ones you haven’t found yet.

See also

Config reference (TOML)

widget_type = "compass". The compass has no widget-specific data block — its colors come from the window’s own fields, and its art from the GUI Compass art picker (stored with your GUI settings, not in the layout).

FieldTypeDefaultWhat it does
active_colorstringgreenTUI — color for available exits
inactive_colorstringdark grayTUI — color for unavailable directions
show_borderbooltrueDraw the window border
border_stylestringthemeBorder style, for example "rounded"

Sizing. The terminal draws into a small character grid; the built-in template is 9×5 to leave room for a border. In the GUI, size is free-form and the minimum width is 48 points.

[[windows]]
name = "compass"
widget_type = "compass"
title = "Compass"
row = 15
col = 92
rows = 5
cols = 9
show_border = true
border_style = "rounded"

Hands

Know what’s in each hand and what spell you have prepared, at a glance — instead of burning a round on inventory to find out you’re holding a shield you meant to drop.

What it’s for

You swap weapons, you get disarmed, you prepare a spell and then take a hit and lose track of whether it’s still there. The game will tell you, but only if you ask, and asking costs you the moment you needed the answer in.

The hand windows keep all three slots on screen: left, right, and the spell you have prepared. They’re one line each, so they cost almost nothing, and the item names stay clickable. In the desktop GUI they can go further — the icon itself can change based on what you’re holding, so “empty hand” and “axe in hand” look different from across the screen.

Set it up

  1. Click Windows in the top toolbar, expand Hands, and tick Left Hand, Right Hand, and Spell in the catalog. Set each row’s zone — stacking all three in the Right Bar keeps them together. (Typed equivalent: .addwindow left hand 92 22 28 1.)
  2. Right-click a hand window and choose Hand icons… in the widget section. That opens the editor for status-driven icons: pick a condition, pick the art it should show.
  3. For a frame around the window, right-click ▸ AppearanceFrame.
📷 screenshot pending
The Hand icons… editor with a state condition on the left and the hands image pool on the right.

Expected result: three one-line windows showing what you’re holding. Picking up a weapon updates the line immediately, and clicking the item name opens its verb menu.

Common setups

Three lines stacked under your vitals

Add all three hand windows and send them to the same zone, one under another, in the order left, right, spell. Right-click each ▸ AppearanceFrame and turn the border off so they read as one block instead of three boxes.

You’ll see: a three-line readout under your bars — both hands and your prepared spell, updating as you swap gear and cast.

An icon that changes when your hand is empty

Right-click your right-hand window ▸ Hand icons… and add two states, in this order:

  1. Hand empty (right) → pick an open-hand image.
  2. Hand holds → weapon (right) → pick a weapon image.

Save, then drop what you’re holding.

You’ll see: the icon flip to the open hand the moment the weapon leaves, and flip back when you pick it up — visible from across the screen without reading a word.

Tips & gotchas

⚠️ The first matching state wins. States are checked top to bottom, and a broad condition placed above a narrow one swallows it. Put Hand holds → a specific item above Hand holds → weapon, or the specific rule will never fire. If nothing matches, the window falls back to its static icon and colors.

⚠️ Which hand a window shows comes from its name, and the fallback is the spell hand. A name containing “left” is the left hand, one containing “right” is the right hand, and anything else — including a typo — becomes the spell hand. If a window named rigth is showing your prepared spell, that’s why.

Drag a hand window taller to get bigger art. In the GUI the icon fills the window’s height, so a one-line hand gets a small icon and a window dragged to two or four lines gets large art. The configured icon size acts as the floor, not the size.

Hand icon states use the same condition language as hotbar buttons and indicators. Learn it once and it transfers — Hand empty, Hand holds, Spell prepared, Roundtime active, Vital, Injury, and the rest, grouped with all of / any of. If you’ve built a combat hotbar, you already know this editor.

Empty reads differently in the spell slot. The two physical hands show Empty when you’re holding nothing; the spell window shows None when you have nothing prepared.

See also

Config reference (TOML)

widget_type = "hand". Set these through Hand icons… and the right-click menu (GUI) or .editwindow (TUI).

FieldTypeDefaultWhat it does
iconstringfrom templateStatic prefix, for example "L:", "R:", "S:"
icon_colorstringwindow colorIcon color
hand_text_colorstringwindow colorItem text color. Also accepts the old name text_color.
statesarrayemptyCondition-driven icon states, first match wins

Which hand: taken from the window nameleft → left hand, right → right hand, anything else → spell hand.

Each entry in states takes when (a condition), plus any of icon (GUI image), text (TUI prefix), and icon_color. A matched state replaces the static icon while its condition holds; no match falls through to the static settings above.

[[windows]]
name = "right"
widget_type = "hand"
icon = "R:"
row = 22
col = 92
rows = 1
cols = 28
show_border = false

  [[windows.states]]
  text = "(  )"
  [windows.states.when]
  type = "hand_empty"
  hand = "right"

  [[windows.states]]
  text = "R:"
  icon_color = "#c0392b"
  [windows.states.when]
  type = "hand_holds"
  hand = "right"
  item_type = "weapon"

Indicators

One glance tells you you’re stunned, bleeding, hidden, or webbed — without hunting for it in text that already scrolled past.

What it’s for

The game announces a status once, in a line that’s gone three messages later. Then you’re guessing. You cast into a stun you didn’t know you had, or you walk out of hiding you thought you’d lost, or you bleed out over four rooms because the message went by while you were reading something else.

An indicator is a small window that lights when a status is on and goes dark when it’s off. It doesn’t scroll, so the answer is always in the same spot on your screen — you learn where to look and stop reading for it. Put the three or four that actually change your decisions somewhere in your eyeline, and let the rest go.

The same machinery reaches further than the game’s own statuses. A highlight rule can light an indicator off any text you choose, which is how you build alerts the game never offered you.

Set it up

  1. Click Windows in the top toolbar, expand Status, then expand the Indicators fold inside it — the statuses sit two levels down so they don’t bury the dashboard row. Tick stunned, bleeding, webbed, hidden, or whichever you want. Set each row’s zone to send it somewhere it’ll live. (Typed equivalent: .addwindow stunned indicator 0 0 6 3.)
  2. To change the art, right-click the indicator and choose Edit indicators… in the widget section — or open the Editors hub ▸ Indicators, or type .indicators. All three open the same builder, a window titled Indicator Icons.
  3. Pick a status in the left list. On the right, set its Active icon (Y) and, if you want something visible while the status is off, its Inactive icon (N).
  4. Click Save all.
📷 screenshot pending
The Indicator Icons editor: the status list on the left, and Active icon (Y), Inactive icon (N), and Conditions (first match wins) for the selected status on the right.

Expected result: a small window that lights when the status turns on and goes dark when it turns off. Get stunned and the stunned window lights the moment the game says so.

Common setups

A row of afflictions above your input line

Add bleeding, stunned, webbed, and poisoned from the catalog and send all four to the same zone, side by side. Right-click each ▸ AppearanceFrame and turn the border off so they read as one strip instead of four boxes.

Because an inactive indicator draws nothing, the strip is empty when you’re fine. Take a wound that bleeds and exactly one icon appears.

You’ll see: blank space when you’re healthy, and a single lit icon the moment something is wrong — the emptiness is the signal.

An icon that changes with severity, not only on and off

Right-click your bleeding indicator ▸ Edit indicators…, select BLEEDING, and use Conditions (first match wins) to add two conditions, in this order:

  1. Injury — area chest, comparison >=, level 3 → pick your loudest image.
  2. Injury — area chest, comparison >=, level 1 → pick a quieter image.

Click Save all.

You’ll see: the quiet icon on a scratch and the loud one on a serious wound — severity you can read from across the room, off one window.

An alert the game never gave you

An indicator’s name is its status id, so create the window first and then point a highlight at it. Type .addwindow AMBUSH indicator 0 0 8 3, then open EditorsHighlights, add a rule matching the text you care about, and put AMBUSH in Set status with a Status duration of 15.

You’ll see: the AMBUSH window light for fifteen seconds whenever that text arrives, then clear itself. Make your health bar shout when you’re hurt builds exactly this alongside a sound and a controller buzz.

Tips & gotchas

⚠️ An indicator window’s NAME is its status id. There is no picker, anywhere. .addwindow stunned indicator … shows the stunned status because the window is called stunned. The Edit indicators… editor changes what each id looks like — it never changes which id a window shows. To point a window at a different status, make a new one with that name. Hotbar windows bind by name the same way.

⚠️ An indicator never adds a status it doesn’t already have — a dashboard does. If you set_status an id and nothing lights, an indicator window with that exact name probably doesn’t exist. A dashboard grows a new cell on its own the first time an unknown id fires; an indicator can’t, because the window is the id. This is the single most useful difference between the two widgets.

⚠️ Conditions are GUI-only; lighting is not. The GUI editor has Conditions (first match wins); the terminal editor has Id, Title, Icon, and the two colors. Both frontends display whatever you authored, and both light from the game and from set_status. Author conditions once in the GUI and your terminal renders the result.

An inactive indicator draws nothing at all — it doesn’t dim. Inactive art is opt-in, in both frontends. Set an Inactive icon (N) if you want something visible while the status is off; otherwise the window is empty, and inactive_color has nothing to color. An empty window is usually what you want: only real statuses take up visual space.

Ids are matched without regard to case, but spelling is exact. stunned, STUNNED, and Stunned are the same status. stuned is a different one that will never light.

The first matching condition wins. Conditions are checked top to bottom, so a broad one above a narrow one swallows it. Put Injury >= 3 above Injury >= 1, never the other way around. When nothing matches, the window falls back to its plain active or inactive icon.

The standard ids are: standing, kneeling, sitting, prone, stunned, bleeding, hidden, invisible, webbed, joined, and dead, plus the afflictions poisoned and diseased. Those same ids are the vocabulary of the Indicator condition in hotbars and hand icons.

.testline proves a rule without waiting for a mob. Type .testline followed by the text you’re matching and it runs through the live highlight pipeline, lighting the indicator if your rule works.

Grayscale is a global choice, with per-status overrides. The Indicator Icons editor’s Global icon art fold carries an Icon set picker and a Grayscale when inactive checkbox, which desaturates a configured inactive sprite rather than fading it. Install more icon sets with .jinx.

See also

Config reference (TOML)

widget_type = "indicator". Set these through Edit indicators… (GUI) or .indicators and .editwindow (TUI).

FieldTypeDefaultWhat it does
indicator_idstringthe window’s nameStatus to track. Matched case-insensitively.
iconstringnoneGlyph or text shown when active
active_colorstring#00ff00Color while the status is on
inactive_colorstring#555555Color while off — only visible if an inactive icon is set
default_statusstringnoneLegacy field, kept for old layouts
default_colorstringnoneLegacy field, kept for old layouts

Binding. A window created by name gets indicator_id set to its own name. Writing a different indicator_id into the file is honored, but nothing in either UI does that — treat the window name as the binding.

Icons and conditions live in the shared indicator templates (edited in Indicator Icons), not in the window. That’s why two windows with the same id always look alike, and why editing a template updates every window and dashboard cell using that id at once.

[[windows]]
name = "stunned"
widget_type = "indicator"
title = "Stunned"
indicator_id = "STUNNED"
row = 0
col = 0
rows = 2
cols = 1
active_color = "#00ff00"
show_border = false

Dashboard

Every status you care about in one compact grid — instead of a row of separate one-cell windows you have to place, size, and align by hand.

What it’s for

Once you’re tracking more than two or three statuses, individual indicator windows start costing more than they’re worth. Each is its own window to position, and eight of them means eight things to nudge back into line every time you rework your layout.

A dashboard is one window holding all of them. You list the statuses you want, pick horizontal, vertical, or a grid, and it lays them out and keeps its own height. Adding a tenth status is a line in an editor, not a new window.

It also does something no indicator can: it grows a cell on its own. Point a highlight rule at an id the dashboard has never heard of, and the first time that rule fires the cell appears. That makes it the right home for statuses you’re still experimenting with.

Set it up

  1. Click Windows in the top toolbar, expand Status, and tick dashboard. Set the row’s zone to place it. (Typed equivalent: .addwindow dashboard dashboard 0 0 10 3.)
  2. Right-click the window and choose Edit dashboard… in its widget section. A new dashboard ships empty, so this is where it gets its contents.
  3. Set LayoutHorizontal, Vertical, Flow (wrap), Grid 2x2, or Grid 3x3 — plus Spacing and the Hide inactive checkbox.
  4. Under Statuses, type an id or pick one from the Known… drop-down, then click Add. Use and to order them and to remove one.
  5. Click Save.
📷 screenshot pending
The Dashboard editor: Layout, Spacing, and Hide inactive at the top, the ordered Statuses list with its stack fields below, and the Save button showing "unsaved changes".

Expected result: one window showing your chosen statuses in the layout you picked, each cell lighting as its status turns on.

Common setups

One compact affliction strip

Add a dashboard, open Edit dashboard…, set Layout to Horizontal and Spacing to 1, and add BLEEDING, POISONED, DISEASED, and STUNNED. Leave Hide inactive ticked. Click Save, then right-click ▸ AppearanceFrame and turn the border off.

You’ll see: empty space while you’re healthy, and icons appearing one at a time as things go wrong — with the strip staying one row tall the whole time.

A posture and presence block beside your compass

Add a second dashboard, set Layout to Grid 2x2, and add STANDING, KNEELING, SITTING, and PRONE. Untick Hide inactive so all four cells always show and only the current one is lit. Click Save.

You’ll see: a two-by-two block where exactly one cell is bright — your posture readable at a glance, in a window that stays exactly two rows tall because the grid tells it so.

One square that shows three afflictions at once

In Edit dashboard…, give BLEEDING, POISONED, and DISEASED the same stack name, for example afflict. Click Save.

The three collapse into a single cell and their active icons paint over each other. It works because the artwork is authored to sit in different parts of the square — the way Wrayth did it — so blood, poison, and disease occupy the corners rather than covering one another.

You’ll see: one square instead of three, lighting up in layers as afflictions land, and a vertical dashboard that got shorter because three cells became one.

Tips & gotchas

⚠️ A dashboard auto-adds an unknown status; an indicator does not. The first time a set_status rule fires an id your dashboard has never listed, a cell appears for it. An indicator window can only flip an id it already has, because the window’s name is the id. This is the practical reason to reach for a dashboard when you’re inventing your own statuses.

One exception: the dashboard won’t auto-add an id that a combined indicator template already claims in one of its conditions. That id is spoken for, so it stays where it was authored.

⚠️ Two different editors, and it’s easy to open the wrong one. Edit dashboard… (in the dashboard window’s own right-click menu) chooses which statuses it lists, the layout, and the spacing. Edit indicators… — the Editors hub ▸ Indicators, or .indicators — chooses what each status looks like, for every dashboard and indicator at once. The dashboard editor has an Edit indicators… button that jumps straight there.

The dashboard editor is Save-buffered. Unlike the right-click menu around it, which applies live, this panel shows “unsaved changes” and does nothing until you click Save. Closing it first discards your edits.

Your dashboard’s height is decided by its rows, not by dragging. A Horizontal dashboard is one row tall. A Vertical one is as tall as its cell count. A Grid is as tall as cell count divided by columns, rounded up. Those all cap the window’s height so the frame hugs the grid instead of leaving a slab of empty space underneath — which is why a dashboard often won’t stretch as far as you drag it.

Flow is the exception, and that’s what it’s for. Flow (wrap) fills the width and wraps, so its row count depends on how wide the window is and can’t be known ahead of time. It’s the one layout left uncapped: it grows as its contents need. Reach for Flow when you have many statuses and want them to reflow as you resize; reach for Grid when you want a shape that holds still.

Stacked rows count as one cell for height, too. Three statuses sharing a stack name occupy one square, so a vertical dashboard of six statuses stacked in pairs is three rows tall, not six.

Hide inactive is on by default, and usually should stay on. A shipped dashboard starts with it ticked so the grid isn’t a wall of dim icons. Untick it for the posture case above, where seeing all the options makes the lit one meaningful.

⚠️ In the terminal, a grid drops anything past its cell count. grid:2x2 renders the first four statuses and silently ignores a fifth. Count your rows against your grid, or use Flow (wrap), which never truncates. The GUI doesn’t have this limit.

Anything the layout string doesn’t recognize becomes horizontal. A typo like verticle won’t error — it lays out horizontally. Use the editor’s drop-down and you can’t hit this.

Ids are matched without regard to case. BLEEDING and bleeding are one status. Icons and colors come from the shared templates, so a cell and an indicator window with the same id always look alike.

See also

Config reference (TOML)

widget_type = "dashboard". Set these through Edit dashboard… in the GUI.

FieldTypeDefaultWhat it does
dashboard_layoutstring"horizontal""horizontal", "vertical", "flow", or "grid:RxC". Unrecognized values fall back to horizontal.
dashboard_spacinginteger1Gap between cells. The editor allows 0–8.
dashboard_hide_inactivebooltrueHide cells whose status is off
dashboard_indicatorsarray of tablesemptyThe statuses to show, in display order

Each [[windows.dashboard_indicators]] entry:

FieldTypeDefaultWhat it does
idstringrequiredStatus id, matched case-insensitively
iconstringfrom templatePer-entry glyph. Kept for older terminal layouts; the shared templates are the normal source.
colorsarray of stringsfrom templateColors indexed by value — [off, on], or more for multi-level statuses
stackstringemptyLayer group. Entries sharing a name render into one cell with their icons painted over each other. Empty means its own cell.

Height. Rows come from the layout and the cell count, where stacked entries count once: vertical is one row per cell, grid:RxC is cells divided by C rounded up, horizontal is one row, and flow is uncapped because its wrapping depends on the window’s width.

[[windows]]
name = "status"
widget_type = "dashboard"
title = "Status"
row = 0
col = 0
rows = 1
cols = 15
dashboard_layout = "horizontal"
dashboard_spacing = 1
dashboard_hide_inactive = true
show_border = false

  [[windows.dashboard_indicators]]
  id = "STUNNED"

  [[windows.dashboard_indicators]]
  id = "BLEEDING"
  stack = "afflict"

  [[windows.dashboard_indicators]]
  id = "POISONED"
  stack = "afflict"

Room Window

Keep where you are on screen — name, description, what’s on the ground, who’s here, and which way out — so you never lose it to a wall of combat text.

What it’s for

The room scrolls past in your main window and then it’s gone. Three creatures later you can’t remember whether that was the clearing with the north exit, and you’re typing look again mid-roundtime to find out.

The room window pins that information in place. It rewrites itself when you move and holds still while you fight, so re-reading the room costs you a glance instead of a command. Every noun in it stays clickable — the boulder, the exits, the person who just walked in.

Set it up

A room window is in your layout already — it’s one of the six windows the default layout ships. If you closed it, click Windows in the top toolbar and tick Room in the catalog.

To choose what it shows, right-click the room window. The widget section holds Sections with four checkboxes: Description, Objects, Players, Exits. Untick one and it disappears from the window as you click — settings apply live, and there is no Save button.

📷 screenshot pending
The room window's right-click menu with Sections expanded, showing Description, Objects, Players, and Exits.

Expected result: the room window shows the name in bold, then the sections you left ticked, reflowing as one paragraph. Unticking Description drops the prose and leaves the name, the objects, and your exits.

Common setups

A compact “where am I” strip

You want the exits and who’s here, but the prose is long and you’ve read it. Right-click the room window, and under Sections untick Description. Then drag the window’s bottom edge up until it’s three or four lines tall, and park it above your main text.

You’ll see: the room name, the loose items, the other players, and Obvious exits: in a block short enough to read without moving your eyes off the fight.

A full room panel you can actually read

Keep all four sections ticked, then right-click ▸ AppearanceText and raise the text size. Under AppearanceFrame, turn the border off. Widen the window until the description stops wrapping mid-sentence.

You’ll see: a borderless block of room prose at a comfortable reading size, with every noun in it still clickable.

Tips & gotchas

⚠️ show_name is not the border title, and the GUI ignores it. In the terminal, turning it on adds the room name as a bold line inside the window — useful when you’ve hidden the border and lost the title with it. In the GUI the room name is always drawn as the first line of the content, so the setting has nothing left to do.

⚠️ Objects continue the description’s paragraph in the GUI, by design. “You also see…” runs on from the end of the description rather than starting its own line, matching the Wrayth layout. If you untick Description, the objects become the first line instead.

Clicking a noun does what the server said it should. A link that carries a command sends it. A link that is a plain noun asks the server for its verb menu and shows that menu where you clicked. A URL opens your browser. You get the same three behaviors in the room window as in your main text.

Empty sections take no space. A room with nobody in it doesn’t reserve a blank line for Players, so a window sized to a busy room will look under-filled in an empty one. Size it for the rooms you hunt in.

Room pictures are a VellumFE feature, not a game feed. GemStone declares a sprite slot on every room change and has never filled it. If you want art in this window, you map images to room ids yourself — see Inline Images.

See also

  • Compass — the exits from this window, as a clickable rose
  • Players — a dedicated who’s-here list, if the room window’s line isn’t enough
  • Text Windows — the main feed the room would otherwise scroll away in
  • Map — where those exits lead
  • Build a hunting layout — where the room window sits in a combat screen
Config reference (TOML)

widget_type = "room". Set these through the right-click menu (GUI) or .editwindow (TUI); this table is for reading a layout file or troubleshooting one.

FieldTypeDefaultWhat it does
show_descbooltrueShow the room description prose
show_objsbooltrueShow loose items and creatures (“You also see…”)
show_playersbooltrueShow other players (“Also here:”)
show_exitsbooltrueShow the Obvious exits: line
show_nameboolfalseTUI only — add the room name as a bold line inside the content. The GUI always renders the name as the first content line and does not read this field.
buffer_sizeinteger10000Lines retained for scrollback

show_name does not control the border title. That comes from the window’s own title, which the built-in template sets to "Room".

[[windows]]
name = "room"
widget_type = "room"
title = "Room"
row = 0
col = 0
rows = 10
cols = 80
show_desc = true
show_objs = true
show_players = true
show_exits = true

Map

Know where you are and where that road goes — and click a room across town to walk there, without keeping a wiki tab open beside the game.

What it’s for

GemStone is big, and the part of it you can see is one room of prose. Everything else lives in your head or in a browser tab: which way the bank is, whether this alley connects back to the square, how far you drifted while hunting.

The map draws the town around you from the map database — rooms as squares, exits as lines, your room ringed — and recenters as you walk. Go inside a building and it swaps to that building’s floor plan on its own. Click any room you can see and you walk there, using the client’s own pathing. No Lich required, which is why it works on the phone too.

Set it up

  1. Get map data first — the map cannot draw without it. Open Settings in the top toolbar, go to the Map section, and click Download map data. (Typed equivalent: .mapdb download.)
  2. Click Windows in the top toolbar, expand Navigation, and tick Map. Use the row’s zone control to place it. (Typed equivalent: .addwindow map map 0 0 30 12.)
  3. Right-click the map for its two controls: Custom map zoom — tick it to reveal a pixels-per-cell slider, untick to return to the default 16 — and Open Map Explorer.
📷 screenshot pending
A Map window showing the current room ringed with exit ticks, its right-click menu open on Custom map zoom and Open Map Explorer.

Expected result: the town draws around you with your room ringed, and it recenters with a short glide each time you move. Clicking another room starts a trip to it.

Common setups

A corner mini map you never think about again

Download the data, add the map window, and send it to a zone where it can stay — a corner of the Right Bar works. Leave zoom at its default. Then forget it: it follows you, swaps to a floor plan when you step inside a building, and swaps back when you leave.

You’ll see: a small live map that always shows the block you are standing on, with your room ringed and its exits ticked around the square.

Reading a whole town without leaving your chair

Right-click the map and choose Open Map Explorer. It opens as its own OS window, so you can park it on a second monitor. Turn Follow off, drag to pan and scroll to zoom, then click a room to select it and read its details in the collapsible Description, Environment, Forageables, Tags, and Exits sections. Walk here sends you to the selected room; double-clicking a room does the same in one gesture.

You’ll see: the full town laid out, with any room’s title, tags, and exits one click away — and a trip starting the moment you press Walk here.

Tips & gotchas

⚠️ The mini map does not pan or zoom by gesture. Dragging it moves the window, and the scroll wheel does nothing to it. Zoom is the Custom map zoom checkbox in its right-click menu. Drag-pan and scroll-zoom live in the Map Explorer, and pinch-zoom on the phone.

⚠️ The map is GUI and mobile only. The terminal prints a one-line hint instead of drawing. The travel commands behind it work identically in all three.

An empty map is telling you which thing is missing. Each message means something different: “Download map data in Settings > Map (or point at your Lich folder)” means no database; “Waiting for a mapped room…” means the database loaded but your room hasn’t matched yet; “Generating map…” means the layout is being computed and will appear shortly. Layouts are generated once per location and cached on disk, so only the first visit waits.

Nothing downloads on its own. Download map data and .mapdb download are explicit actions. .mapdb on its own reports the loaded database, its room count, and which release you have.

Downloaded releases carry GemStone data. DragonRealms sessions get their map from a Lich install instead — set Lich folder under Settings ▸ Map.

Unmapped shop interiors are normal, not broken. Map maintainers leave most shop interiors out on purpose, because they change constantly. Walk into one and the map holds the street outside: what is on screen stays mapped truth.

Cartography mode sketches those interiors instead. Turn on Cartography Mode in Settings ▸ Map and unmapped rooms draw as dashed, dimmed ghost rooms hanging off the room you entered from. Dashed means “what your client saw this session”; solid means “in the database”. Ghosts are never saved — they vanish when you close VellumFE, so they can never go stale. Ghost capture runs whether or not the mode is on; the setting controls only whether you see them.

Your map learns things it never writes down. Run forage sense or a ranger’s sense and the response is captured for the room you are in, showing up in the Map Explorer’s Environment and Forageables sections. Like Lich’s in-memory map edits, these are session-only — the map database on disk is never modified.

Player-shop warrens get their own map per town, listed in the location picker as, for example, Mist Harbor (Player Shops). Walking in switches the map over the same as entering any other location, which keeps the town map readable.

Explorer edits survive map updates. The Explorer’s Edit toggle lets you drag a group of rooms to tidy a layout (hold Alt for a single room). Edits save as per-room override diffs layered on top of any community-curated overrides shipped with the data. Reset overrides clears only your own layer.

If the map is stuck on the wrong room, run .room. It prints how your current room resolved, including its id, location, and edge count. On connections that never report a room id, the map falls back to matching title, description, and exits — and only trusts an unambiguous match, holding in place otherwise.

See also

  • Travel (.go2) — the commands behind every map click
  • Compass — the current room’s exits, for the step you are about to take
  • Room Window — the room’s own prose and exits as text
  • Travel & Day Passes — how the pathing engine routes, and what it costs to cross paid edges
Config reference (TOML)

Per-window (layout.toml)

widget_type = "map". This widget has exactly one field of its own.

FieldTypeDefaultWhat it does
zoomfloat16.0Pixels per grid cell. Clamped to 2.0–96.0 at paint time.

Everything else is a standard window field. The shipped template is 12 rows by 30 columns, with a floor of 5 rows by 10 columns.

[[windows]]
name = "map"
widget_type = "map"
title = "Map"
row = 0
col = 0
rows = 12
cols = 30
show_border = true
zoom = 16

Global (config.toml)

[map] — all four are editable in Settings ▸ Map, which also shows the downloaded version and offers Download map data and a delete action.

FieldTypeDefaultWhat it does
mapdb_pathstringunsetExplicit map database JSON file. Outranks everything.
mapdb_repostring"Nisugi/mapdb"GitHub owner/repo whose releases carry mapdb.json. Empty disables downloads.
lich_dirstringunsetLich install folder (the one containing data/). The newest data/<GAME>/map-<timestamp>.json for the connected game is used.
mapping_modeboolfalseCartography Mode — render unmapped rooms as ghost sketches.

Source priority: explicit file, then downloaded release, then Lich folder. The newest download plus one rollback are kept under ~/.vellum-fe/mapdb/. If a release also carries an overrides.json asset, it is applied underneath your own edits.

Dot-commands, available in every frontend including the phone: .mapdb (status), .mapdb download, .mapdb remove, .mapdb repo <owner/repo>, .go2 reload (force a fresh load), and .room (how the current room resolved).

[map]
lich_dir = "C:/Lich5"
mapdb_repo = "Nisugi/mapdb"
mapping_mode = false

Travel (.go2)

Type where you want to be and put your hands back on the keyboard — .go2 bank walks you there, waiting out roundtime and standing you up on the way.

What it’s for

Crossing town is a dozen movement commands you already know, typed in the right order, with a stand in the middle because something knocked you down. It is not hard. It is tedious, and you do it forty times a session.

.go2 does the walking. Name a destination — a room id, a tag like bank, a name you saved, or plain text to search for — and the client paths there over the map database and walks it, waiting out roundtime, standing you up, and pausing while you are stunned or webbed. Press Esc and it stops.

This is VellumFE’s own engine, not a wrapper. It runs without Lich, which is why it works on the phone. If you are on Lich, you can hand the hard cases back to ;go2.

Set it up

  1. Get map data first — travel paths over the same database the map draws. Open SettingsMap and click Download map data. (Typed equivalent: .mapdb download.)

  2. Type a destination in the command input:

    .go2 bank          the nearest room tagged "bank"
    .go2 8966          a map database room id
    .go2 u7150105      a game uid
    .go2 town square   text search over room titles, with a pick list if several match
    
  3. Open SettingsTravel to decide what the engine may spend and use. Native Map Clicks is on by default; the paid-travel switches — Use Portmasters, Use Urchin Guides, Use Day Passes, Get Silvers — are all off until you turn them on.

📷 screenshot pending
Settings with the Travel section open, showing Native Map Clicks on and the paid-travel switches off.

Expected result: a line like [go2] -> Town Square Central (8966): 12 rooms, ETA 0:47, then your character walks it, and [go2] arrived at room 8966 - travel time 0:44 on arrival.

Common setups

Save the places you actually go

Stand somewhere you return to constantly and save it:

.go2 save home
.go2 save shop 8966

The first saves the room you are standing in; the second saves an explicit id. Afterwards .go2 home walks you there from anywhere, and .go2 saved lists everything you have saved. .go2 back returns to wherever your last trip started.

You’ll see: [go2] saved 'home' -> room 8966 (travel there with .go2 home), and from then on .go2 home is a one-line trip from any town.

Find the nearest shop without knowing its name

Run .go2 targets anywhere. It prints tagged destinations reachable from where you stand, nearest first, with an ETA on each.

You’ll see: a list like .go2 furrier -> room 1247 (0:22) — copy the line, run it, and you are walking. This is the fastest way to orient in a town you don’t know.

Tips & gotchas

⚠️ .go2 targets and .go2 saved are different lists. targets is the map’s directory of tagged destinations near you (banks, shops, guilds). saved is your named rooms from .go2 save.

⚠️ Paid travel is opt-in, and off by default. Ferries, portmasters, urchin guides, and Chronomage day passes are all supported, but each has its own switch in Settings ▸ Travel. With them off, routes that need them are excluded from pathing rather than attempted — so a trip may report no route where Lich’s ;go2 would have paid its way through.

Destinations are tried in a fixed order, and the first match wins: back, then a room id, then a u-prefixed uid, then your saved names, then a map tag, then a text search of titles and descriptions. That is why saving a target named for a tag shadows the tag — the client refuses names that would shadow a room id or back for the same reason.

There are more destination words than the usage line shows. .go2 guild and .go2 guild shop route to your profession’s guild, which needs your profession known — run INFO once if it reports the profession is unknown. .go2 locker and .go2 public locker find the nearest locker you can use. .go2 goback is an alias for .go2 back.

Ambiguous text gives you a pick list, not a guess. .go2 town square with several matches prints the candidates with their ids so you can rerun with an exact one.

Failures name their cause. [go2] your current room hasn't resolved against the mapdb yet is the common one — run .room to see how your room matched. Others are direct: room {id} is not in the mapdb, you're already here..., and map database not loaded - configure it in Settings > Map.

A trip that goes wrong re-paths instead of stranding you. Ending up somewhere unexpected — fleeing, being teleported, walking by hand mid-trip — re-paths from where you actually are. A move that keeps failing gets that edge disabled for the session and the route recomputed. Dying aborts the trip.

One automation drives at a time. Starting a trip while something else is running (a .foreach batch, for instance) reports [go2] {that} is driving - .stop to cancel it first. .stop cancels whatever is running; .go2 stop cancels a trip specifically.

Map clicks can be handed to Lich instead. Turn Native Map Clicks off and clicking a room sends ;go2 <id> to Lich rather than walking natively. Separately, Lich ;go2 Fallback hands off to Lich only when native travel hits an edge it cannot cross. Both need a Lich connection — a direct connection has no Lich to hand off to.

Some regions scramble movement, so their map edges are meaningless to walk. These are declared as mazes and the engine uses a per-maze strategy instead. The shipped example is the Mist Harbor Ranger Guild jungle approach, where each character has a personal route revealed by an NPC — the walker asks automatically, captures the answer, and reuses it on every later trip.

See also

  • Map — the picture behind click-to-travel, and where map data comes from
  • Compass — one room’s exits, for the step you are taking by hand
  • Room Window — the room prose and its exits as text
  • Travel & Day Passes — how routes are planned, what paid edges cost, and how day passes are bought and spent
Config reference (TOML)

Global (config.toml)

[go2] — every field below is editable in Settings ▸ Travel in the GUI, .settings in the terminal, and the settings sheet on the phone. GUI labels are given so you can find each switch.

FieldTypeDefaultGUI labelWhat it does
savedtableemptySaved targets, name to room id. Written by .go2 save; listed by .go2 saved.
native_map_clicksbooltrueNative Map ClicksMap clicks travel natively. Off sends ;go2 <id> to Lich.
lich_fallbackboolfalseLich ;go2 FallbackWhen native travel can’t cross an edge, hand off to Lich’s ;go2. Lich connections only.
use_seekingboolfalseUse Voln SeekingRoute through Voln Symbol of Seeking edges. Only takes effect for a Voln Master.
use_portmastersboolfalseUse PortmastersRoute through portmaster ship travel. Costs silver.
get_silversboolfalseGet SilversLet travel withdraw from the bank to fund paid travel when short.
get_return_trip_silversboolfalseGet Return Trip SilversAlso withdraw enough to fund the return trip.
use_urchinsboolfalseUse Urchin GuidesRoute through urchin guides. Needs active access; off while mounted.
use_day_passboolfalseUse Day PassesRoute through Chronomage day-pass edges, using a pass in your day-pass sack.
buy_day_passstringemptyBuy Day PassWhen to buy a pass if none is held: on/yes, off/no, or a town-pair list like "sol,wl imt,wl". Needs Get Silvers.
day_pass_sackstringemptyDay Pass SackContainer holding your day passes, as a noun or name fragment.
weaponsackstringemptyWeapon SackContainer the hands-stow uses for a weapon your ready sheath doesn’t cover.
lootsackstringemptyLoot SackFallback container for anything not routed by ready, sheath, or weapon sack.
pathcodestableemptyPersonal maze routes, captured automatically from the maze NPC. Never hand-edited.
[go2]
native_map_clicks = true
use_portmasters = false
get_silvers = false
use_day_pass = false
day_pass_sack = ""

Command surface

CommandWhat it does
.go2Print the usage line
.go2 <id | uid | tag | saved name | text>Travel there
.go2 back / .go2 gobackReturn to where the last trip started
.go2 guild / .go2 guild shopYour profession’s guild. Needs profession known — run INFO.
.go2 locker / .go2 public lockerThe nearest locker you can use
.go2 stopCancel the active trip. Esc does the same on the desktop.
.go2 statusRooms done, rooms total, and ETA for the active trip
.go2 save <name> [id]Save a target. Without an id, saves the current room.
.go2 savedList your saved targets
.go2 targetsReachable tagged destinations, nearest first, with ETAs
.go2 reloadForce a fresh load of the map database
.stopCancel whatever automation is running, travel included
.portal [n]Walk the room’s non-compass exit (go door, climb stair). Several offer a pick menu.
.roomHow the current room resolved against the map database

Files

  • ~/.vellum-fe/travel_overrides.toml — teach the engine an edge it cannot cross. Copy travel_overrides.toml from the defaults; the file documents its own format. Overrides beat whatever the map database says about that edge.
  • ~/.vellum-fe/mazes.toml — add or replace maze definitions by name, on top of the shipped set.

Injury Display

Know which limb is about to fail you — and how badly — without typing health between every swing.

What it’s for

HEALTH tells you everything, once, in a block of text that scrolls away. Mid-fight you don’t want everything. You want to know whether it’s your right arm or your left leg, and whether it’s the scratch you took two rooms ago or the thing that’s about to cost you the fight.

The injury display is a body, drawn once and updated as the game reports damage. Each part carries its own severity, so a glance answers “where” and “how bad” at the same time. Wounds and scars are shown apart, which matters because a scar is permanent and a wound is the one you can still do something about.

It is also the widget that changes the most between frontends, and deliberately so. In the desktop GUI it can be real artwork — a painted body that swaps to a different pose when you go down, with hand-drawn wounds that stack in place. In the terminal it’s a compact figure of characters that colors by severity. Both read the same game data; only the drawing differs.

Set it up

  1. Click Windows in the top toolbar, expand Character, and tick Injuries. Set the row’s zone to place it. (Typed equivalent: .addwindow injuries injuries 0 0 10 8.)
  2. Right-click the window. Its widget section is named Injury doll.
  3. Use Doll image to pick the art: Skin default takes whatever body your active skin ships, None draws the built-in vector body, and the rest are dolls installed in your image pool. Install more with .jinx list and .jinx install.
  4. Tick Grayscale doll art if you want the body desaturated. The wound and scar dots keep their colors regardless.
📷 screenshot pending
The injuries window's right-click Injury doll section, with the Doll image drop-down open on Skin default, None, and installed pool dolls, above the Grayscale doll art checkbox and Calibrate doll….

Expected result: a body appears in the window. Take a wound and that part changes — a colored dot at the part’s position, or its hand-drawn wound art if the doll has any. Hover a wounded part and a tooltip names it and its severity, like left arm: severe injury.

Common setups

A painted body that lies down when you do

This is the feature that makes the widget more than a picture, and it needs a skin with doll art. In your active skin’s skin.toml, the [injury_doll] table is the default body. Add a [[injury_doll.variants]] entry with a name, a when condition, and its own complete skin table:

[[injury_doll.variants]]
name = "downed"
when = { type = "indicator", id = "prone", active = true }

[injury_doll.variants.skin]
base = "doll/downed.png"

Then right-click the injuries window ▸ Injury dollCalibrate doll…, switch the Doll set drop-down from Default to downed, and click each body part on the prone art to place its dot. Click Save to skin.

You’ll see: your doll standing while you’re upright, and the whole body swapping to the prone painting the instant you’re knocked down — with every wound dot landing in the right place on the new pose, because you calibrated that set separately.

Dots that read at a glance instead of a squint

Open Calibrate doll… on the default set. The bottom rows style the generated dots: a Wound color, a Scar color, a dot size slider as a percentage of the doll’s height, and an opacity slider. Flip Preview: between wounds and scars and drag the rank slider from 1 to 3 to see each severity live on the art before you commit.

Push dot size up until the dots are unmistakable at your normal window size, then click Save to skin.

You’ll see: wounds as solid circles with their rank numeral inside and scars as rings, big enough to read from your seat — and the change takes effect on the very next frame rather than waiting for a skin reload.

A hand that disappears with the arm above it

For dolls with hand-drawn art, a part can suppress itself. Give the part a hidden_when condition in the manifest:

[injury_doll.leftHand]
hidden_when = { type = "injury", area = "leftArm", cmp = ">=", level = 3 }
healthy = "doll/hand_ok.png"

You’ll see: the left hand stop drawing entirely once the left arm hits severity 3 — no orphaned hand floating below a missing arm. The hand’s own wound still appears in the hover tooltip, so you don’t lose the information, only the drawing.

Tips & gotchas

⚠️ Scar 1 is level 4, not level 1. The scale runs 0 through 6: 0 is healthy, 13 are wounds, and 46 are scars. So a condition of Injury >= 4 means “any scar at all”, and Injury >= 1 means “anything wrong”. The editor labels (Scar1), the TOML color keys (scar1_color), and the skin art keys (scar1) all use the short name while the underlying level is 4. Counting scars from 1 in a condition is the most common way to write a rule that never fires.

⚠️ A variant replaces the doll wholesale — it does not layer on top. A matched variant brings its own base image, its own anchors, its own part art, and its own dot styling. That is the point: a prone body repositions every limb, so nothing from the standing set would land correctly. It also means a variant inherits nothing — a hidden_when you wrote on the default set does not apply while a variant is active. Author each set completely.

⚠️ The calibrator’s Doll set drop-down discards unsaved work when you switch. Changing from Default to a variant reseeds every anchor and dot value from the manifest. Click Save to skin before you switch sets, every time.

The window may appear on its own. The injuries window is bound to the game’s own injuries dialog, and windows bound that way are created for you the first time the game opens their dialog — that is how the GemStone IV experience window arrives. If a doll turns up unbidden, this is why, and .hidewindow injuries puts it away. It won’t duplicate: if you already have one, the existing window takes the feed.

A pool doll and a skin doll are not the same thing. A doll picked from Doll image is a single flat image with a sidecar file beside it holding its anchors and dot styling — that’s all. Pool dolls carry no pose variants and no hidden parts, because those live in a skin’s manifest. Choose Skin default if you want variants. Calibrating a pool doll writes to its sidecar, so the calibration travels with the artwork and a shared doll can arrive already calibrated.

A part with hand-drawn art never gets a dot, and that’s a feature. Once any severity key is authored for a part, that part is fully hand-drawn: at a level with no art the base shows through. Skins that paint the worst case into the base rely on this — “no overlay” is a deliberate reveal, like the hole where a severed limb was. Parts with no art at all keep their generated dots, so one doll can mix both approaches freely.

The nervous system draws underneath everything else. It’s a full-body underlay rather than a badge, so limb wounds and dots paint over it instead of being covered by it.

The tooltip only appears when something is wrong. Hovering a healthy GUI doll says nothing at all — an “uninjured” tooltip read as a stray badge, so it was removed. If you get no tooltip, you have no wounds.

Grayscale is built on demand. Ticking Grayscale doll art generates desaturated copies of the art at that moment and drops them when you untick it. Skins can’t ship pre-made grayscale files; the client makes its own.

Clicking another player can open their doll in a popup. The game sends a separate per-player injuries dialog, and both desktop frontends draw it as a floating window titled with that player’s name — the terminal closes it with Esc, the GUI with its window close button. That popup always uses the default doll set and the default palette: variants and hidden parts read your state, so your own prone flag must never reshape someone else’s body.

Your severity colors don’t reach the phone. The phone ships the same seven default colors, so an unmodified setup matches everywhere — but per-window injury*_color overrides are not forwarded, so a recolored desktop doll and the phone’s doll will differ.

See also

Config reference (TOML)

widget_type = "injury_doll". Both injuries and injury_doll are accepted as the type string, and as the .addwindow type argument. Set the colors through .editwindow in the terminal; art and calibration are GUI-only.

Watch the type string. An unrecognized widget type does not error — it silently falls back to a plain text window. A typo like injurydoll or injury-doll gets you an empty text window, not a warning.

Window fields

FieldTypeDefaultWhat it does
injury_default_colorstring#333333Level 0 — an uninjured part
injury1_colorstring#aa5500Level 1 wound
injury2_colorstring#ff8800Level 2 wound
injury3_colorstring#ff0000Level 3 wound
scar1_colorstring#999999Level 4 — scar 1
scar2_colorstring#777777Level 5 — scar 2
scar3_colorstring#555555Level 6 — scar 3

A blank or whitespace value falls back to the default. The preset ships 8 rows by 10 columns with a minimum of 6 by 8.

Body parts

Fourteen parts, in the calibrator’s click-through order: head, leftEye, rightEye, neck, chest, abdomen, back, leftArm, rightArm, leftHand, rightHand, leftLeg, rightLeg, nsys. These same names are the vocabulary of the Injury condition in hotbars, indicators, and hand icons.

Skin doll art (skin.toml, GUI only)

KeyTypeDefaultWhat it does
[injury_doll] basestringnoneThe body image. Relative to the skin folder, or a pool path, or absolute.
[injury_doll.anchors]tablebuilt-inpart = [x, y] as fractions of the base image. Written by the calibrator.
[injury_doll.dots] wound_colorstring#e02020Solid wound dot color
[injury_doll.dots] scar_colorstring#b8b8b8Scar ring color
[injury_doll.dots] opacityfloat0.9Dot opacity, clamped 0–1
[injury_doll.dots] diameterfloat0.07Dot size as a fraction of the drawn doll height

Any other table under [injury_doll] is a body part. A part table takes hidden_when (a condition) plus per-severity image keys: healthy, injury1, injury2, injury3, scar1, scar2, scar3. Those key names are exact — an unrecognized one is logged and ignored.

[injury_doll]
base = "doll/body.png"

[injury_doll.dots]
wound_color = "#e02020"
scar_color = "#b8b8b8"
diameter = 0.07

[injury_doll.leftArm]
healthy = "doll/arm_ok.png"
injury1 = "doll/arm_i1.png"
injury3 = "doll/arm_severed.png"

[injury_doll.leftHand]
hidden_when = { type = "injury", area = "leftArm", cmp = ">=", level = 3 }

[[injury_doll.variants]]
name = "downed"
when = { type = "indicator", id = "prone", active = true }

[injury_doll.variants.skin]
base = "doll/downed.png"

A variant’s skin table has the same shape as [injury_doll] minus variants — variants do not nest, and attempting it is a parse error. The first variant whose when matches wins, in declaration order.

Window TOML

[[windows]]
name = "injuries"
widget_type = "injury_doll"
title = "Injuries"
row = 0
col = 0
rows = 8
cols = 10
show_border = true
border_style = "rounded"
content_align = "center"
injury1_color = "#aa5500"
injury2_color = "#ff8800"
injury3_color = "#ff0000"
scar1_color = "#999999"
scar2_color = "#777777"
scar3_color = "#555555"

Active Effects

Watch your buffs, debuffs, cooldowns and active spells sitting on screen with their remaining time — so a lapsing 401 is something you see coming, not something you find out about the hard way.

What it’s for

Every buff you’re running has a clock on it, and the game only tells you about that clock when you ask. Between asks you’re guessing. That’s fine right up until Spirit Warding drops in the middle of a swarm, or you burn a prep re-casting something that had four minutes left.

An active effects window puts the game’s own effect dialog on your screen and keeps it there. Each effect draws as one row: the name on the left, the time on the right, and a bar behind both that fills to show how much of the effect’s life is left. You get the same information the SPELL and SPELLUP checks give you, without spending a command on it mid-fight.

The same data feeds more than the window. The condition vocabulary that drives hotbar buttons, indicator icons, and hand icons reads these exact effect feeds — so a button that dims when a buff lapses and this window are two views of one truth. Wire both and the window tells you what, while the button tells you act now.

Set it up

Effects arrive in four separate dialogs, and one window shows exactly one of them. There is no combined view — three categories on screen means three windows.

1. Add the windows you want.

Click Windows in the top toolbar and expand Active Effects. Four rows sit there: buffs, debuffs, cooldowns, and active_spells. Tick each one you want. Every row arrives 10 rows by 30 columns, titled after its category.

Categories start collapsed in the catalog, so expand the heading before deciding a row is missing.

2. Change what a window shows.

Right-click the window. The widget section holds a Category combo listing ActiveSpells, Buffs, Debuffs, and Cooldowns. Pick one and the window switches feeds immediately. Settings apply live — there is no Save button.

📷 screenshot pending
A Buffs window right-clicked, its widget section open on the Category combo showing ActiveSpells, Buffs, Debuffs, and Cooldowns.

You can also build one from ➕ Custom window… ▸ Active effects, which creates the window with no category set. It renders empty until you pick one from that combo.

Expected result: cast a buff and a row appears in the Buffs window — the spell’s name, its duration on the right, and a filled bar behind the row.

Common setups

The three-window caster stack

One category per window means the layout does the sorting for you — you learn where to look rather than reading labels.

  1. From the Windows catalog, tick active_spells, buffs, and cooldowns.
  2. Set all three rows’ zone control to Right bar, so they stack in reading order.
  3. Right-click cooldowns and shrink it — cooldown lists are short, and the space is better spent on the spell list above it.
  4. Save with .savelayout hunting (typed — there is no GUI save button).

You’ll see: a right-hand column where your running spells sit on top, buffs beneath them, and cooldowns in a short block at the bottom. Nothing overlaps, and a spell dropping off the top list is a change in a place your eye already knows.

Colored spell families so you read shape, not text

Effect rows can carry per-spell colors, which turns the window into something you parse by color block instead of by reading every line.

  1. Type .spellcolors to open the Colors window on its Spell Colors tab.
  2. Click Add spell color range and enter spell numbers into Spell IDs, comma-separated — 101, 102, 103, 107, 120 for the Minor Spirit circle.
  3. Pick a bar color and a text color, then save.

VellumFE ships eleven of these already: Minor Spirit in Bondi blue, Wizard in dark orange, Bard in hot pink, Sorcerer in dark red, and so on.

You’ll see: your Active Spells window in bands of color, where a missing blue row reads as “my Minor Spirit set has a gap” from across the desk.

⚠️ Spell colors are matched by spell number, not name. The lookup takes the effect’s id as a number, so it only ever colors effects the game identifies numerically. The first matching entry wins, so if a number appears in two color entries, the one higher in the list is the one you get.

Tips & gotchas

⚠️ The displayed time does not tick down on the desktop. Both desktop frontends draw the duration string the game sent, and the game only re-sends an effect dialog when something changes. A row can read [02:14] for a while and then jump. The phone is the exception — it converts durations to local deadlines and counts down every second. Treat the desktop number as “last known,” and treat presence rather than the exact figure as the reliable signal.

⚠️ Changing a window’s Category wipes its contents. Old-category effects are meaningless under a new feed, so switching a window from Buffs to Debuffs clears every row immediately. The window looks broken for a moment. It repopulates the next time the game sends that dialog — cast something, or run a SPELL check, and the rows return.

The bar tracks the effect’s own percentage, not the clock. The fill comes from a value the game sends with each effect, so it moves when the game updates the dialog rather than draining smoothly. Read the bar as “roughly how much of this is left,” and the number as the figure.

Effects don’t disappear on their own. Nothing sweeps expired rows — an effect leaves the window only when the game sends a fresh dialog for that category. An effect showing [00:00] has almost certainly lapsed; the window is waiting for the game to say so.

Rows appear in arrival order and are never sorted. A new effect lands at the bottom and holds that spot for as long as it runs, so positions stay stable while you’re hunting. If you want expiry-ordered effects, the phone’s Effects sheet is the one surface that sorts.

The time format differs between the desktop frontends. The terminal brackets it and drops seconds past the hour — [12:34] under an hour, [03:06] for three hours six minutes. The GUI prints the game’s raw string unbracketed, 03:06:54. Same data, two presentations; an effect the terminal shows as [??:??] is one whose duration isn’t a clock value at all, such as an indefinite blessing.

A narrow window sacrifices the name before the time. Squeeze the terminal window far enough and the effect name truncates without an ellipsis; squeeze further and the name vanishes so the time survives. The GUI clips the name the same way but gives it back on hover, showing Spirit Warding I - 03:06:54 as a tooltip.

These feeds are the condition vocabulary. Effect active, Effect inactive, and Effect time remaining conditions each take a category, a name, and a match mode (Exact or Contains), and drive hotbar button states, indicator icons, and hand icons. Effect time remaining additionally compares against a threshold in seconds — which is how you build a button that lights up when a buff has under thirty seconds left. Those conditions compute remaining time live from an absolute expiry, so they are accurate even while the window’s displayed number is stale.

⚠️ Conditions can disagree with the window, and the condition is right. An effect past its expiry still draws in the window until the game clears it, but Effect active already reports it inactive. That’s not a bug in either place — the condition does the arithmetic the renderer doesn’t.

Only the GUI can author conditions. The terminal’s hotbar editor round-trips existing states untouched and shows "{N} state(s) defined - edit in the GUI editor or hotbars.toml". Mobile has no hotbars at all — the phone has macros, a separate system with no conditions.

See also

Config reference (TOML)

Written by the editors above. Hand-editing is for troubleshooting, not the normal path.

[[windows]]
name = "buffs"
widget_type = "active_effects"
category = "Buffs"
row = 0
col = 0
rows = 10
cols = 30
show_border = true
title = "Buffs"

Widget fields (ActiveEffectsWidgetData):

FieldTypeDefaultWhat it does
categorystring(required)Which effect dialog this window shows. Exactly one of Buffs, Debuffs, Cooldowns, ActiveSpells. Case- and spelling-exact; an empty or unrecognized value renders nothing.

widget_type must carry the underscore: active_effects. An unrecognized widget type falls back to a text window rather than failing to load.

Catalog presets. All four ship at 10 rows by 30 columns under the Active Effects heading:

Catalog rowcategoryTitle
buffsBuffsBuffs
debuffsDebuffsDebuffs
cooldownsCooldownsCooldowns
active_spellsActiveSpellsActive Spells

A fifth seed, active_effects_custom, is not a catalog row — it is the ➕ Custom window… ▸ Active effects creation flow, and it starts with an empty category.

Where the data comes from. The game pushes effects as dialog updates, one dialog per category — Active Spells (with a space on the wire), Buffs, Debuffs, Cooldowns. Each effect carries an id, a display name, a percentage, and a duration string. An effect missing any of those is dropped rather than half-drawn. A dialog marked as a clear empties that category outright; every other update adds or amends rows without removing any.

Spell colors (colors.toml). Matched on the effect’s id as a number, first entry wins:

[[spell_colors]]
spells = [101, 102, 103, 104, 105, 107, 112, 115, 117, 120, 140]
color = "#0086b3"        # bar color; `bar_color` is the current name
text_color = "#909090"
FieldTypeDefaultWhat it does
spellsarray of integer[]Spell numbers this entry colors. An explicit list, not a range.
bar_colorstring(falls back to color)Fill color for the effect’s bar
colorstringLegacy name for bar_color, still read
text_colorstring(theme text)Color of the effect name
bg_colorstringParsed and stored, but not applied to effect rows

A character-level spell_colors list replaces the global list wholesale when it is non-empty, rather than merging entry by entry.

Conditions reading these feeds (hotbars.toml, indicator and hand states):

[[bars.combat.buttons.states]]
when = { type = "effect_time", category = "Buffs", name = "Spirit Warding I", name_match = "exact", cmp = "<", seconds = 30 }
text_color = "#ff5555"
ConditionFieldsTrue when
effect_activecategory, name, name_matchThe effect is present and unexpired. Effects with no parseable duration count as active while present.
effect_inactivecategory, name, name_matchThe effect is absent or past its expiry
effect_timecategory, name, name_match, cmp, secondsRemaining seconds compare true. False when the effect is absent or has no parseable expiry.

category is one of buffs, debuffs, cooldowns, active_spells. name_match is exact (default) or contains. cmp is <, <=, >, or >=.

State ordering: the first matching state wins. A broad condition listed above a narrow one swallows it.

Standard window keys — row, col, rows, cols, show_border, border_style, title, locked — apply as they do to any window.

Targets

Every hostile creature in the room, with its status, in one list you can click to target — instead of re-reading the room description while something is hitting you.

What it’s for

Mid-fight the room description is the worst place to find a target. It scrolled away three messages ago, it lists things you can’t meaningfully hit next to the thing still swinging at you, and it buries “stunned” inside a sentence.

The targets window is the short version: hostile creatures only, one per line, each with its active statuses as short tags like [stu,prn]. Click one and you target it. Boss-tier and challenging creatures come in their own colors, so the dangerous one in a pile of six is obvious before you swing.

Set it up

  1. Click Windows in the top toolbar, expand Entities, and tick Targets. Use the row’s zone control to place it — the Right Bar keeps it beside the text without covering it. (Typed equivalent: .addwindow targets targets 92 3 28 12.)
  2. Right-click the window for its two per-window options: Show filtered appendage count and Status position.
📷 screenshot pending
The targets window's right-click menu showing Show filtered appendage count, the Status position drop-down, and the Global target settings… link.

Expected result: a list of the room’s hostile creatures. Clicking a name sends target #<id> and that row turns green with a > in front of it.

Common setups

A narrow combat strip beside the text

Add the window, send it to the Right Bar, and drag it down to about twelve rows. Right-click ▸ Status positionBefore the name, so every row starts with its tags and the status column lines up down the left edge.

You’ll see: [stu] a mud hog stacked over [stu,prn] a mud hog, scannable in one glance without reading the names.

Colors that pick the dangerous one out of a crowd

Right-click ▸ Global target settings… to jump to Settings ▸ Targets. Set Boss Color to something loud (#ff5555 ships as the default) and Challenging Color to a warmer #ffaa55. Save.

You’ll see: in a room of six creatures, the two the game flagged as boss-tier render red and the challenging one amber, while ordinary creatures stay in your normal text color.

Proof the filter is working

Right-click a targets window and tick Show filtered appendage count. Fight a sorcerer, or anything else that summons grasping limbs from the ground.

You’ll see: Appendages: 3 centered on the bottom border while the list itself stays clean — three unkillable limbs hidden, and you can tell they were hidden rather than missed.

Tips & gotchas

⚠️ A creature is only listed once the game has sent a <crtrStatus> snapshot marking it hostile. Until that arrives its hostility is unknown, and unknown creatures are excluded on purpose. A brand-new arrival can take a beat to appear, and a shopkeeper standing next to you never will. An empty list in a room full of prose is usually this, not a broken window.

⚠️ The targets click sends target in the GUI and TUI, but opens the verb menu on the phone. Same list, two different results from the same gesture.

⚠️ The TUI puts a live count in the title (Targets [03]); the GUI does not. The GUI window keeps whatever title you gave it.

Grasping appendages are filtered, and kraken tentacles are not. Spells like a sorcerer’s Grasp of the Dead (709) summon limbs that erupt from the ground and attack you. They’re targetable but can’t be damaged, so listing them is pure clutter — arms, claws, limbs, pincers, tentacles, and palpi are dropped from the list. The four kraken tentacle variants (amaranthine, ghostly, grizzled, ancient) are real creatures you can kill, so they stay. Nothing you configure changes that pair of rules.

The appendage count is a count of hostile appendages. A limb that never got a hostile snapshot isn’t counted, so the footer can read lower than the number actually flailing at you.

Dead creatures leave the list entirely. There is no dimmed corpse row here — that’s the Players window’s behavior. A creature that dies stops being listed.

A status without an abbreviation falls back to its first three characters. stunned is mapped to stu out of the box; an unmapped awake renders [awa]. Statuses of three characters or fewer pass through whole. Add your own pairs in Settings ▸ TargetsStatus abbreviations.

Several statuses show at once only when the game sends the structured feed. [stu,prn] comes from <crtrStatus>. When the client is falling back to reading the room text, one status is the most it can know.

truncation_mode = "noun" is not an always-on shortener. It swaps the full name for the bare noun only when a creature has a status and the name plus its tags would overflow the window. Widen the window and full names come back.

Global settings are global. Settings ▸ Targets changes colors, truncation, excluded nouns, and abbreviations for every targets window and the players window too. Only the two options in the right-click menu are per-window.

See also

  • Players — who else is here, and the [ded] styling for corpses
  • Items — the non-creature half of the same room feed
  • Room Window — the prose these lists are extracted from
  • Hotbars — buttons that light up on creature and vitals conditions
  • Build a hunting layout — placing targets in a combat screen
Config reference (TOML)

Per-window (layout.toml)

widget_type = "targets".

FieldTypeDefaultWhat it does
entity_idstring"targetcount"Entity feed id for the window
show_body_part_countboolfalseDraw Appendages: N on the bottom border. GUI label Show filtered appendage count; TUI label Show Appendages
status_positionstring(unset)"start" or "end"; overrides the global setting for this window only. Unset = follow the global

Global (config.toml, [target_list]) — edited in Settings ▸ Targets

FieldTypeDefaultWhat it does
status_positionstring"end""start" or "end" — which side of the name the tags sit on
truncation_modestring"noun""full" or "noun". "noun" falls back to the bare noun only when a status is present and the line would overflow
excluded_nounslist["arm", "coal"]Nouns never treated as targets (case-insensitive)
boss_colorstring"#ff5555"Boss-tier creatures (AscensionBoss or MiniBoss)
challenging_colorstring"#ffaa55"Creatures the game flags “challenging”
dead_colorstring"#888888"Dead players in the players window — see Players

[target_list.status_abbrev] maps a full status name to a short tag. Twelve pairs ship by default; unmapped statuses fall back to their first three characters.

[[windows]]
name = "targets"
widget_type = "targets"
title = "Targets"
row = 3
col = 92
rows = 12
cols = 28
show_border = true
show_body_part_count = true
status_position = "start"

[target_list]
status_position = "end"
truncation_mode = "noun"
excluded_nouns = ["arm", "coal"]
boss_color = "#ff5555"
challenging_color = "#ffaa55"
dead_color = "#888888"

[target_list.status_abbrev]
stunned = "stu"
frozen = "frz"
dead = "ded"
prone = "prn"

Players

See who else is in the room at a glance, and click a name to interact without typing it.

What it’s for

The room’s “Also here:” line is one long sentence that scrolls away, and it’s the line you most often want back — you’re deciding whether to loot in front of someone, looking for the person you’re grouping with, or checking whether the body on the floor is your friend.

The players window keeps that roster on screen and adds what the sentence buries: statuses as short tags, and corpses dimmed with a [ded] marker so you can tell “someone’s here” from “someone died here.” Click a name and the game’s own verb menu opens on that person.

Set it up

  1. Click Windows in the top toolbar, expand Entities, and tick Players. Set the row’s zone — the Right Bar under your targets window works well. (Typed equivalent: .addwindow players players 92 16 28 8.)
  2. There are no per-window options. Everything that shapes these rows — status side, abbreviations, and the dead color — lives in Settings ▸ Targets and is shared with the Targets window.
📷 screenshot pending
A Players window in the Right Bar listing three names, one dimmed with a [ded] tag.

Expected result: a list of everyone else in the room. Clicking a name opens that player’s verb menu at your cursor.

Common setups

A who’s-here strip under your targets

Add the players window, send it to the Right Bar, and drag it to about eight rows so it sits directly under Targets. Both read from the same room feed, so they update together.

You’ll see: hostiles above, people below, in one column you can check without moving your eyes off the fight.

Corpses that read as corpses

Right-click a targets window ▸ Global target settings… to reach Settings ▸ Targets. Set Dead Color to a dim gray (#888888 is the default) and confirm dead maps to ded under Status abbreviations. Save.

You’ll see: a fallen player renders as Regyy [ded] [prn] in gray, clearly separate from the living names above it, while still being clickable.

Tips & gotchas

⚠️ Right-clicking a player row opens the window menu, not a menu for that player. Right-click anywhere in a VellumFE window is always the window’s own menu. To act on a person, left-click the name — that’s what asks the server for their verb menu.

⚠️ The TUI puts a live count in the title (Players [02]); the GUI does not.

Two statuses can stack, from two different places. A status written before the name in the room text (a stunned Regyy) and one written after it (Regyy (prone), or the verbose “who is lying down”) are tracked separately, so a row can carry both: Regyy [stu] [prn].

When a player is dead, the [ded] tag always leads. A dead, prone player reads Regyy [ded] [prn] regardless of which other statuses apply.

Dead here means the room roster called them a body. The client reads the roster’s “the body of …” phrasing. It is not a health reading, and it does not follow players you can’t see.

Statuses without an abbreviation fall back to their first three characters. An unmapped awake renders [awa]; anything three characters or shorter passes through whole.

Status side is global, and this window has no override. Settings ▸ TargetsStatus Position moves the tags to the front or the back for players and targets together. Only the targets window can override it per-window.

See also

  • Targets — the hostile half of the same room feed, and the home of the shared [target_list] settings
  • Items — objects on the ground from that same feed
  • Room Window — the roster as prose, with its Players section toggle
  • Build a hunting layout — where this sits in a combat screen
Config reference (TOML)

Per-window (layout.toml)

widget_type = "players". This widget has exactly one field of its own.

FieldTypeDefaultWhat it does
entity_idstring"playercount"Entity feed id for the window

Everything else is a standard window field (title, show_border, border_color, row, col, rows, cols).

Global (config.toml, [target_list]) — edited in Settings ▸ Targets

The players window reads three of the shared target settings:

FieldTypeDefaultWhat it does
status_positionstring"end""start" or "end" — which side of the name the tags sit on
dead_colorstring"#888888"Text color for dead players (the [ded] rows)
status_abbrevtable12 pairsFull status name to short tag; unmapped statuses use their first three characters

The remaining [target_list] fields (truncation_mode, excluded_nouns, boss_color, challenging_color) affect the targets list only — see Targets.

[[windows]]
name = "players"
widget_type = "players"
title = "Also Here"
row = 16
col = 92
rows = 8
cols = 28
show_border = true

[target_list]
status_position = "end"
dead_color = "#888888"

[target_list.status_abbrev]
dead = "ded"
prone = "prn"
stunned = "stu"

Items

What’s actually on the ground, pulled out of the room prose and held still — so the box that just dropped doesn’t scroll away while you’re still fighting.

What it’s for

Loot arrives at the worst moment. The room description names it once, in the middle of a sentence that also names the creatures, and then combat messages push the whole thing off the top of your screen.

The items window keeps the room’s non-creature objects in a standing list: dropped loot, boxes, furniture, scenery. Click one and the game’s verb menu opens on it, so you can look, get, or open without typing a noun you’d have to scroll back to read. In the GUI you can also drag an item onto a container or your hand.

Set it up

  1. Click Windows in the top toolbar, expand Entities, and tick Items. Set the row’s zone — the Right Bar keeps it next to your other room lists. (Typed equivalent: .addwindow items items 92 25 28 10.)
  2. There are no per-window options for this widget; its border, colors, and title are the standard window settings under Appearance.
📷 screenshot pending
An Items window listing four objects on the ground, with the cursor over one row showing the pointing-hand cursor.

Expected result: a list of everything on the ground. Clicking one opens that object’s verb menu; an empty room reads No objects here.

Common setups

A loot shelf under your hunting column

Add the items window, send it to the Right Bar, and give it about ten rows below Targets and Players. All three read the same room feed and refresh together when you move or something drops.

You’ll see: one column that answers “what’s attacking me / who’s watching / what’s on the floor” without a single scroll.

Bagging loot without typing

With the items window and a container window both open in the GUI, drag an item row onto the container.

You’ll see: the item moves into that container, and it leaves the items list on the next room update — the same result as typing put <item> in <container>, with no noun to spell.

Tips & gotchas

⚠️ Right-clicking an item row opens the window menu, not a menu for that item. Left-click the name to ask the server for the object’s verbs.

⚠️ The TUI puts a live count in the title (Items [04]); the GUI does not. The GUI shows No objects here. in an empty room instead.

Creatures and items come from the same feed, split by how the game bolds them. The room’s object line marks creatures in bold and everything else plain. Bold entries become Targets; plain ones land here. That’s why a creature never appears in this window and a chest never appears in that one.

Dragging is a GUI-only gesture, and items are the only one of the three room lists that has it. Target and player rows respond to clicks only.

The list is in the game’s order, not sorted. Rows appear exactly as the room reports them, so a new drop shows up wherever the game puts it rather than at the bottom.

The window follows the room, not your inventory. Picking something up removes it here on the next room update. What you’re carrying belongs to the Inventory window.

See also

  • Targets — the bolded, creature half of the same room feed
  • Players — the room roster, with corpse styling
  • Inventory — what you’re carrying, rather than what’s on the floor
  • Containers — a window per bag, and the drop target for dragging loot
  • Room Window — the prose, with its Objects section toggle
Config reference (TOML)

Per-window (layout.toml)

widget_type = "items". This widget has exactly one field of its own.

FieldTypeDefaultWhat it does
entity_idstring"items"Entity feed id for the window

Everything else is a standard window field. The shipped template is 10 rows by 40 columns, with a floor of 4 rows by 20 columns.

There is no [items] config section — this window has no global settings. The [target_list] settings that shape Targets and Players do not apply to it.

[[windows]]
name = "loot"
widget_type = "items"
title = "Ground"
row = 25
col = 92
rows = 10
cols = 28
show_border = true

Inventory

Everything you’re carrying and wearing, standing still in its own window — so you can see what’s in your hands and on your back without typing inv and watching it scroll away.

What it’s for

INVENTORY answers a question you ask constantly and it answers it into the main window, where the next combat message buries it. Ten seconds later you’re typing it again.

The inventory window holds that answer permanently. It lists your worn and carried items, and it rewrites itself the moment the list changes — you pick something up, stow a gem, draw a weapon, and the window is already correct. Click any item and the game’s verb menu opens on it, so you can look, wear, or stow without spelling the noun.

Set it up

  1. Click Windows in the top toolbar, open the Character category, and tick Inventory. (Typed equivalent: .addwindow inventory inventory 92 3 40 20.)
  2. Use the row’s zone control to place it — the Right Bar keeps it out of the text.
  3. There is no widget section in this window’s right-click menu. Its look lives under Appearance, and Appearance ▸ Text ▸ Word wrap is the one setting that changes how the list reads.
📷 screenshot pending
An Inventory window in the right bar listing worn and carried items, with the pointer over one row showing the pointing-hand cursor.

Expected result: a standing list of what you’re carrying. Clicking an item opens that item’s verb menu; the list rewrites itself whenever your inventory changes.

Common setups

A carry column you can bag loot into

Add the inventory window, send it to the Right Bar, and give it about twenty rows. Open an Items window above it and a Container window for your pack below it. Now hold Ctrl and drag a row from the items window onto the container window.

You’ll see: the item lands in the bag, disappears from the ground list on the next room update, and appears inside the container window — the result of put <item> in <pack> with no noun typed. Dropping onto the inventory window’s body instead sends wear, so a cloak dragged there goes on rather than into a bag.

Watching a stow actually land

Keep the inventory window open and stow something from your hand.

You’ll see: the list rewrites in place the instant the game reports the change — no scroll, no re-typing inv, and the item you stowed is no longer listed loose.

Tips & gotchas

⚠️ This window is a snapshot, not a log — it has no scrollback. Every update clears it and refills it from the game’s current list. PageUp will not show you what you were carrying a minute ago, because that text was never kept. The Buffer size control the editors offer for this widget is inert; leave it alone.

⚠️ Dropping an item onto the inventory window’s empty body means wear, not “put in inventory”. Release over a row and the drop targets that specific item instead. This is the one drop target whose meaning isn’t “into the thing under the cursor.”

⚠️ In the GUI, Ctrl+C quits. In the TUI it copies your selection. Quit the TUI with .quit or .exit.

Dragging needs a modifier, and it’s Ctrl by default. Plain dragging selects text in both desktop frontends. Hold the drag modifier first — it’s ui.drag_modifier_key in config.toml and accepts ctrl, alt, or shift.

The window can be empty-looking rather than empty. Unlike Items, which prints No objects here., and Containers, which prints Empty., inventory renders nothing at all until the game has sent the list. Type inv once after login if a fresh window looks blank.

An unchanged list is not redrawn. The client compares each incoming snapshot against the last one and skips identical updates, so a window that doesn’t flicker on every inv is working correctly.

You can have more than one. Every inventory window receives the same feed, so a second one in another zone is a legitimate way to see a long list without a tall window.

Reserve is the same widget on a different feed. That window behaves identically — snapshot in, snapshot replaced.

.foreach and .sorter read your inventory without a window open. The item registry is fed straight from the feed whether or not you’ve placed this widget, so batch commands work either way. See Inventory Tools.

See also

  • Containers — a window per bag, and the drop target for stowing
  • Items — what’s on the ground rather than on you
  • Reserve — the same snapshot widget on the reserve feed (GemStone IV)
  • Inventory Tools (.foreach, .sorter) — batch commands over what you’re carrying
  • Hands — what’s in each hand, on its own
Config reference (TOML)

Per-window (layout.toml)

widget_type = "inventory". The .addwindow type string is inventory.

FieldTypeDefaultWhat it does
streamslist["inv"]Feed the window reads. inv is what the game sends
buffer_sizenumber0Inert for this widget. Saved and ignored — content is replaced wholesale each update
wordwrapbooltrueWrap long item names instead of clipping them
show_timestampsboolfalsePresent on the type but not offered in either editor for inventory windows

The shipped template is 20 rows by 40 columns with a floor of 4 rows. There is no [inventory] section in config.toml — this window has no global settings.

[[windows]]
name = "inventory"
widget_type = "inventory"
title = "Inventory"
row = 3
col = 92
rows = 20
cols = 40
show_border = true
streams = ["inv"]
wordwrap = true

Containers

A live window per bag — your pack, your bandolier, your gem pouch — each one showing what’s actually inside it right now, and each one a target you can drop loot onto.

What it’s for

LOOK IN MY PACK tells you what’s in the pack once, then scrolls away, and the next time you want to know you type it again. Meanwhile the answer changes every time you stow a gem.

A container window pins one bag open. It lists the contents, refreshes when the game re-sends them, and its items are clickable for the verb menu. Better, the whole window is a drop target: drag a gem from the ground onto your pouch and it goes in, no put and no noun.

The trade-off is deliberate — these windows are session-only. They come and go with your login and never touch your saved layout, which is what makes it reasonable to have one per bag without cluttering the layout you actually keep.

Set it up

Container windows are the one widget you don’t place from the catalog by type. You place them per bag, and a bag only becomes placeable once the client has seen inside it.

Look in the bag once first (look in my pack). That registers it — it does not open a window. The bag then appears as its own row under Containers in the Windows list, and ticking that row is what opens the window.

  1. Look in the container once: look in my pack.
  2. Click Windows in the top toolbar and open the Containers category. Your pack is a row there, labelled with its in-game name and marked (session).
  3. Tick the row. Untick it to close the window again.
📷 screenshot pending
The Windows catalog with the Containers category expanded, showing two bags as rows marked (session), one ticked.

Expected result: a window titled with the bag’s name opens in the middle of the screen at 40 by 15, listing its contents. An untracked bag reads No contents cached for “…”; a tracked but empty one reads Empty.

Common setups

A bagging strip for hunting

Look in your gem pouch and your pack. Tick both in the Windows list, then drag them into a column beside your Items window. Now hold Ctrl and drag a gem row from the items window onto the pouch window’s body.

You’ll see: the gem lands in the pouch and shows up inside the pouch window, while the items window drops it on the next room update — the same result as put gem in my pouch, with nothing typed. Dropping onto a row inside a container puts the item into that nested container instead.

Clearing the screen after a looting run

You’ve ticked four bags and the screen is busy. Type .hidecontainers.

You’ll see: every one of them closes at once and the client reports how many it closed. Your opt-ins are cleared with them, so those bags stay closed until you tick them again — they will not reappear the next time you look inside one.

Keeping one bag in your saved layout

If you always want your pack window in the same place, declare it in layout.toml with widget_type = "container" and container_title = "leather pack". That window is persistent — saved, restored, and placed wherever you put it.

You’ll see: the pack window comes back at your chosen position on every login, without ticking anything.

⚠️ Give it the same name the session window would get — the bag’s title, lowercased with spaces as underscores (leather_pack). Name it anything else and ticking the bag in the Windows list opens a second window on the same bag. Nothing merges them for you.

Tips & gotchas

⚠️ Container windows are session-only and are never saved. Close the client and every one of them is gone — they are not in your layout, and .savelayout does not capture them. This is by design; the persistent alternative is the layout-declared window in the recipe above.

⚠️ Looking in a bag does not open its window. It only makes the bag appear as a row in the Windows list. Ticking the row is the step that opens anything. Once ticked, though, the opt-in sticks for the session — look in that bag again after closing its window by any route other than unticking, and it re-opens on its own.

⚠️ .hidecontainers <name> matches on a substring and can close several windows at once. .hidecontainers pack closes every open container whose name contains “pack”. Matching is case-insensitive and spaces are treated as underscores, so .hidecontainers my pack finds my_pack correctly. Bare .hidecontainers closes them all.

⚠️ Bare .hidecontainers also closes open game dialog panels, even though it reports the count as “container window(s)”. If you have a combat or befriend panel open, it goes too.

⚠️ Only the TUI remembers where you dragged a container window. Move one in the terminal and its position is written to widget_state.toml, so it reopens there next session. The GUI does not persist session container geometry at all — every container window opens centered at 40 by 15 in the GUI, every time.

Contents come from the client’s item registry, not from a text buffer. The window shows what the client last learned about that bag, which is why an unlooked-in bag reads No contents cached for “…” rather than being empty. Look in it once and it fills.

Bag names are matched loosely. A window bound to pack finds “a battered leather pack” — matching tries the exact title, then a substring, then the title with articles stripped. Short distinctive words work well; over-specific ones can miss.

Only the TUI shows a count in the title. a leather pack [06], or (empty) when it’s bare. The GUI keeps whatever title the window has and prints Empty. in the body instead.

Word wrap is the one appearance control that matters here. It’s under Appearance ▸ Text in the GUI. Long item names clip rather than wrap when it’s off, which is usually what you want in a narrow bag window.

Right-clicking an item row opens the window menu, not a menu for that item. Left-click the item name to ask the server for its verbs.

.foreach needs the bag seen open, exactly like this window does. If a batch command reports it can’t find your container, look in it once and re-run. See Inventory Tools.

See also

Config reference (TOML)

Per-window (layout.toml)

widget_type = "container". Declaring one this way makes it persistent, unlike the session-only windows the Windows list creates.

FieldTypeDefaultWhat it does
container_titlestring"" (empty)The bag this window shows. Matched case-insensitively: exact title, then substring, then articles stripped. Empty = the window renders no contents

Everything else is a standard window field. Session-created container windows are always 40 by 15, centered.

[[windows]]
name = "leather_pack"
widget_type = "container"
container_title = "leather pack"
title = "Pack"
row = 20
col = 92
rows = 15
cols = 40
show_border = true

Position memory (widget_state.toml, TUI only)

Dragging a session container window in the TUI records its geometry under [containers], keyed by the window name. Written by the terminal frontend only.

[containers.leather_pack]
x = 40
y = 8
width = 44
height = 18

There is no [containers] section in config.toml — this widget has no global settings.

Reserve

A standing list of what the game is holding aside for you, so you can check it without spending a command and a screenful of text mid-hunt.

What it’s for

Some things the game sets aside for you rather than handing over — and the only way to see them is to ask, which costs you a command and scrolls your screen at the exact moment you’d rather it held still.

A reserve window parks that list on screen. The game sends the whole list at once, the window shows it, and it sits there until the game sends a different one. Nothing scrolls, nothing accumulates, and the names stay clickable — so you can act on an entry without typing a noun you’d have to scroll back to read.

It is the Inventory window’s twin in every mechanical respect. The only differences are which feed fills it and which game offers it.

Set it up

  1. Click Windows in the top toolbar, expand Character, and tick reserve. It arrives 20 rows by 40 columns, titled Reserve. (Typed equivalent: .addwindow reserve reserve 100 0 40 20.)
  2. There is no widget section in this window’s right-click menu — reserve has no settings of its own. Streams and Buffer lines live under Window, and Wrap text under Appearance ▸ Text. Leave Streams on reserve; that is the feed the game sends.
📷 screenshot pending
The Windows catalog with the Character group expanded, showing reserve beside inventory.

Expected result: a window titled Reserve holding the current list. Clicking an entry opens that item’s verb menu.

Common setups

A character column that answers “what do I have?” in one glance

Reserve is a single-purpose window and it earns its space by sitting next to its relatives rather than alone.

  1. Add inventory and reserve from the catalog’s Character group.
  2. Send both to the same zone — the Right Bar works well — and stack reserve under inventory.
  3. Give reserve the shorter of the two. It is usually the smaller list.

You’ll see: one column reading carried items on top and held-aside items below, both refreshing on their own when the game sends a new list, and every name in both clickable.

Tips & gotchas

⚠️ This window is GemStone IV only, and the two ways of adding it disagree about that. Both pickers hide it from DragonRealms characters — it is absent from the GUI Windows catalog and from the bare-.addwindow picker. The full six-argument .addwindow form does not check, so it will build the window on a DR character. You get an empty box: DragonRealms never sends this feed. If the game is not set at all, VellumFE assumes GemStone IV and the row appears — which is why it shows up when you connect through Lich without naming a game.

⚠️ There is no scrollback, and that is the design. Each list the game sends replaces the previous one wholesale rather than being added under it. Scrolling up shows you nothing earlier, because nothing earlier is kept. If you want history, the story pane already has it.

⚠️ Right-clicking a row opens the window menu, not a menu for that entry. Left-click the name to ask the server for that item’s verbs.

The window only redraws when the list actually changes. VellumFE compares each incoming list against the one it is already showing and leaves the window alone when they match. A window that looks frozen is usually correct — ask the game to re-send if you want to be sure.

Buffer lines does nothing useful here. The field is offered because this window shares its plumbing with text windows, but the contents are cleared on every update, so there is never a backlog for the buffer to cap. The shipped preset sets it to zero.

Keep the border on. Click detection on these rows assumes a bordered window, so turning the border off can shift where your clicks land by a row.

Leave the streams field alone unless you know why you’re changing it. It is editable in both frontends, but reserve is the feed the game sends. Point it elsewhere and the window fills with something that is not your reserve list.

Nothing about this window appears in a layout you share with a DragonRealms player. The window definition still loads for them, but it stays empty.

See also

  • Inventory — the same widget mechanics, fed by what you carry
  • Containers — a window per bag, for what’s inside them
  • Items — what’s on the ground, rather than what’s yours
  • Text Windows — how stream-fed windows work generally
  • Creating Layouts — placing and saving these windows
Config reference (TOML)

Written by the catalog and .addwindow. Hand-editing is for troubleshooting, not the normal path.

[[windows]]
name = "reserve"
widget_type = "reserve"
row = 0
col = 100
rows = 20
cols = 40
show_border = true
title = "Reserve"
streams = ["reserve"]
buffer_size = 0
wordwrap = true

Widget fields

widget_type = "reserve". The type string is exactly reserve; an unrecognized type does not error, it quietly creates a text window instead.

FieldTypeDefaultWhat it does
streamsarray of string["reserve"]Feeds the window reads. The game sends reserve.
buffer_sizeinteger0Scrollback cap. Inert here — content is replaced, not accumulated.
wordwrapbooleantrueWraps long lines instead of clipping them at the border
show_timestampsbooleanfalsePresent in the shared data shape; not offered in either editor for this widget.

These are the same four fields an inventory window carries — the two share one data shape.

Catalog preset

Catalog rowCategoryTitleSizeGame
reserveCharacterReserve20 rows x 40 cols (floor: 4 rows)GemStone IV only

The game gate hides the row from DragonRealms characters in both the GUI catalog and the .addwindow picker. An unset game counts as GemStone IV, so the row appears by default. The six-argument .addwindow form is not gated and will build the window regardless.

Where the settings live

SettingDesktop GUITerminal (TUI)
Streams, Buffer linesRight-click ▸ Window.editwindow reserve
Wrap textRight-click ▸ Appearance ▸ Text.editwindow reserve
Speak new lines (TTS)Right-click ▸ Windownot offered

There is no widget section in the GUI right-click menu for this type and no [reserve] config section — this window has no global settings. Standard window keys — row, col, rows, cols, show_border, border_style, title, locked — apply as they do to any window.

Spells

Your spell list parked on screen, so you can see what you know and what you’re running without spending a command on SPELL in the middle of something.

What it’s for

The game hands you your spell list once, at login, and then expects you to remember it. Checking costs a command and a screenful of text — which is a fine trade standing in town and a bad one three seconds into a fight.

A spells window keeps that list visible. It’s the game’s own spell output given a permanent home: styled the way the game styled it, with the same clickable links, sitting still while everything else scrolls.

Its natural partner is the Missing Spells window. This window shows what you have; that one shows what you’re missing — the spells you told VellumFE to watch that aren’t currently running. Together they answer “am I fully spelled up?” without a single typed command.

Set it up

1. Add the window.

Click Windows in the top toolbar, expand Character, and tick spells. It arrives 20 rows by 40 columns, titled Spells. Categories start collapsed, so expand the heading before deciding the row is missing.

2. Add its companion.

Tick missingspells in the same Character category. It arrives smaller — 8 rows by 28 columns, titled Missing Spells — because it’s usually empty and only needs room when something’s wrong.

3. Tell it what to watch.

The Missing Spells window is driven by a watch list, and it stays empty until you fill one. Spell up the way you normally would, then type:

.spellwatch add all

That snapshots everything currently running and watches it from then on.

📷 screenshot pending
A Spells window beside a Missing Spells window, the latter listing two watched spells in amber after a buff lapsed.

Expected result: the Spells window fills with your spell list, and Missing Spells reads All spells up in green until one of your watched spells drops off.

Common setups

“Am I still spelled up?” answered without asking

The spells window tells you what you have. The gap is what costs you, and a gap is much harder to see than a presence — you’re scanning for something that isn’t there.

  1. Spell up completely in town, the way you’d want to hunt.
  2. Type .spellwatch add all. VellumFE replies with something like Watching 9 active spells (9 new).
  3. Add a missingspells window somewhere your eye passes often — beside your vitals is good.
  4. Go hunt.

You’ll see: a window reading All spells up in green for as long as everything holds. When Spirit Warding lapses, that green line is replaced by an amber 101 Spirit Warding I — an item appearing where there was nothing, which is exactly the kind of change peripheral vision is good at catching.

Trimming the watch list to what you actually re-cast

add all is a blunt start and it watches things you don’t care about — a bard’s song you only run in town, or a one-off from another player.

  1. Type .spellwatch to list what you’re watching, with each entry marked active or MISSING.

  2. Drop the ones you won’t re-cast, several at once:

    .spellwatch rem [1605, 1617]
    
  3. Add anything you missed the same way: .spellwatch add [503, 513].

You’ll see: the list shrinks to the spells you genuinely maintain, so the Missing Spells window stops crying wolf about a buff you never intended to keep up.

Tips & gotchas

⚠️ The spells window is not a live “known spells” roster. The game sends this list once, at login, and the window shows that snapshot. It is replaced wholesale when the game sends a fresh one, and it keeps no history and doesn’t scroll back. If it looks stale or empty, run a SPELL check to make the game re-send.

⚠️ Clicking a spell does not cast or prepare it. A click on a plain spell link asks the server for that object’s menu and shows you the verbs it offers. It’s a two-step — click, then choose — not a one-click cast. Nothing fires from this window without you picking an entry.

Adding the window after login still works. VellumFE keeps the spell list it received and replays it into a spells window you create later in the session, so you don’t have to reconnect to populate a window you just added.

Missing Spells watches two categories, not four. It compares your watch list against Active Spells and Buffs only. Debuffs and cooldowns are deliberately excluded — a cooldown that isn’t running is normal, and reporting it as “missing” would make the window useless. So a watched entry that only ever appears under Cooldowns will read as missing forever.

The watch list has its own page. The .spellwatch command forms, the add all snapshot trap, where the list is stored, and the amber/green/gray display states all live on Missing Spells.

See also

Config reference (TOML)

Written by the catalog and .addwindow. Hand-editing is for troubleshooting, not the normal path.

[[windows]]
name = "spells"
widget_type = "spells"
row = 0
col = 80
rows = 20
cols = 40
show_border = true
title = "Spells"

[[windows]]
name = "missing"
widget_type = "missingspells"
row = 20
col = 80
rows = 8
cols = 28
show_border = true
title = "Missing Spells"

Widget fields. Neither widget has any of its own.

WidgetType stringDataWhere its content comes from
SpellsspellsSpellsWidgetData — no fieldsSubscribed to the game’s Spells stream, bound automatically at window creation. You do not set a stream.
Missing SpellsmissingspellsMissingSpellsWidgetData — no fieldsDerived: the per-character watch list minus everything currently in ActiveSpells and Buffs

The Missing Spells type string is missingspells — no underscore.

Catalog presets, both under the Character heading:

Catalog rowTitleSize
spellsSpells20 rows x 40 cols
missingspellsMissing Spells8 rows x 28 cols (floor: 3 x 14)

The watch list. .spellwatch builds and edits the list the Missing Spells window reads. Its full command surface, number forms, persistence path, and display colors are documented on Missing Spells.

Standard window keys — row, col, rows, cols, show_border, border_style, title, locked — apply as they do to any window.

Missing Spells

A window that stays blank until something you rely on drops off — then names it. The gap in your defenses, reported instead of noticed.

What it’s for

Seeing what you have is easy. Seeing what you’re missing is the hard part, because you’re scanning for something that isn’t there — and a lapsed buff looks exactly like a screen with nothing wrong.

This window inverts that. You tell VellumFE which spells you care about, and it shows you only the ones that aren’t running. Most of the time it reads All spells up in green and you ignore it. The moment Spirit Warding falls off mid-hunt, an amber line appears where there was nothing — a change in the corner of your eye, which is the one thing peripheral vision is genuinely good at.

The watch list is yours, per character, and it survives restarts. Building it is one command.

Set it up

1. Add the window.

Click Windows in the top toolbar, expand Character, and tick missingspells. It arrives 8 rows by 28 columns, titled Missing Spells — small on purpose, since it is empty most of the time.

2. Spell up the way you’d want to hunt.

The next step snapshots what’s running right now, so get yourself fully covered first.

3. Build the watch list.

.spellwatch add all

VellumFE replies with something like Watching 9 active spells (9 new).

📷 screenshot pending
A Missing Spells window listing 101 Spirit Warding I and 107 Spirit Warding II in amber after two wards lapsed, beside a green All spells up state for comparison.

Expected result: the window reads All spells up in green. Let a watched buff expire and that line is replaced by an amber 101 Spirit Warding I.

Common setups

The set-and-forget hunting watch

This is the whole feature in four steps, and it is worth doing once properly rather than tinkering with it later.

  1. Spell up completely in town, exactly the way you want to hunt.
  2. Type .spellwatch add all.
  3. Put a missingspells window somewhere your eye already passes — directly beside your vitals is the best spot, because that is where you look when something goes wrong anyway.
  4. Go hunt.

You’ll see: a small green All spells up for as long as everything holds. When a ward falls, that green line is replaced by amber 101 Spirit Warding I. Re-cast it and the window returns to green on its own — no command, no confirmation.

Trimming the list to what you actually re-cast

add all is a deliberately blunt start, and it will happily watch things you have no intention of maintaining: a bard’s song you only run in town, a one-off somebody else cast on you.

  1. Type .spellwatch to see the list, each entry marked active or MISSING.

  2. Drop several at once:

    .spellwatch rem [1605, 1617]
    
  3. Add anything the snapshot missed the same way: .spellwatch add [503, 513].

You’ll see: the list shrinks to the spells you genuinely maintain, and the window stops crying wolf about a buff you never intended to keep up. .spellwatch confirms each edit with a running total — Removed 2 spells from the watch list (7 left).

Watching one specific thing before a hard fight

You do not have to watch everything. Watching one spell makes this a single-purpose alarm.

  1. Clear the list: .spellwatch rem all.
  2. Watch the one that matters: .spellwatch add 414.

You’ll see: a window that is green until that one spell drops, then shows exactly one amber line. Nothing else can trigger it.

Tips & gotchas

⚠️ add all snapshots what is running at that instant. Run it while half-spelled and you have codified being half-spelled — the window will cheerfully report All spells up while you stand there missing three wards. Spell up fully first, then snapshot.

⚠️ The type string is missingspells, and a typo does not error. An unrecognized widget type silently falls back to a text window. If you typed missing_spells you now have a text window that will never show anything, not a broken missing-spells window.

It watches two effect categories, not four. Your watch list is compared against Active Spells and Buffs only. Debuffs and Cooldowns are deliberately excluded — a cooldown that isn’t running is normal, and reporting it as “missing” would make the window useless. The consequence worth knowing: a watched number that only ever appears under Cooldowns reads as missing forever.

Absence is the test, not expiry. Nothing here counts down. The game clears and re-sends a whole category whenever it changes, so a spell vanishing from that feed is what marks it missing. That makes the window accurate even though the times shown in your Active Effects windows sit frozen.

Numbers, in bulk, all-or-nothing. .spellwatch add 606, .spellwatch add [101,103,107], and .spellwatch add 101,103 all work — brackets are optional and spaces inside them are fine. A single unparseable number rejects the entire command rather than applying half of it, so a typo never leaves you with a partially-updated list. It prints the usage line and nothing changes.

⚠️ There is no .spellwatch clear. Clearing the list is .spellwatch rem all. remove works anywhere rem does.

Names come from a table first, the live feed second, and the number last. A watched spell shows as 101 Spirit Warding I when VellumFE knows the number, as whatever name the game last used for it this session when it doesn’t, and as a bare 64001 when it has never seen it named. A row that is all digits means the number is real but unrecognized — usually a script-pushed custom effect.

The rows ignore your spell colors. Missing-spells rows are always amber, whatever the spell color table says. Spell colors reach Active Effects windows; they do not reach this one.

Read the two empty states — they mean different things. All spells up in green means the watch list is satisfied. .spellwatch add <n> to watch in gray means you have not built a list yet: the window is working correctly and has nothing to do.

The list is per character and survives restarts. It rides that character’s session state, so each character keeps its own without you managing anything. Nothing needs saving.

The window is small by design. It ships 8 rows by 28 columns and floors at 3 by 14. If you watch twenty spells and twenty drop at once, the terminal’s title count still tells you the total even when only a few rows fit.

See also

  • Spells — the login-time spell list this window’s gaps are measured against
  • Active Effects — the live buff/debuff/cooldown feeds this window reads
  • Hotbars — a re-cast button that restyles itself when a buff lapses
  • Wire a hotbar for combat — turning a detected gap into one click
  • Color Palette.spellcolors and why they stop at this window
  • Creating Layouts — placing and saving the window
Config reference (TOML)

Written by the catalog and .addwindow. Hand-editing is for troubleshooting, not the normal path.

[[windows]]
name = "missingspells"
widget_type = "missingspells"
row = 20
col = 80
rows = 8
cols = 28
show_border = true
title = "Missing Spells"

Widget fields

widget_type = "missingspells"one word, no underscore. An unrecognized type does not error; it quietly creates a text window.

The widget carries no fields of its own. Its content is derived: the per-character watch list minus everything currently present in the ActiveSpells and Buffs effect feeds.

Catalog preset

Catalog rowCategoryTitleSizeGame
missingspellsCharacterMissing Spells8 rows x 28 cols (floor: 3 x 14)Both

Ungated — it appears in the GUI Windows catalog and the bare-.addwindow picker for both GemStone IV and DragonRealms characters.

.spellwatch

Usage: .spellwatch add|rem <number> | [n,n,...] | all.

FormWhat it does
.spellwatch / .spellwatch listLists the watch list, each entry marked active or MISSING, headed Watched spells (N missing):. With an empty list it prints the usage hint instead.
.spellwatch add <n>Adds one spell number
.spellwatch add [n,n,n]Adds several. Brackets optional (101,103 works); spaces inside them are fine
.spellwatch add allWatches everything currently in ActiveSpells and Buffs, deduped and sorted ascending. Replies Watching N active spells (M new)., or Nothing active in ActiveSpells/Buffs to add.
.spellwatch rem <n> / .spellwatch remove <n>Removes spells; same number forms
.spellwatch rem allClears the list, replying Cleared N watched spells. There is no clear subcommand.

Numbers parse as whole numbers up to 65535. Anything outside that range, any non-numeric element, and an empty or [] argument reject the entire command with the usage line. Entries keep add order, and adding a number already on the list is a no-op that still reports the total.

Persistence

The watch list lives in that character’s session cache:

~/.vellum-fe/profiles/<Character>/session_cache.toml

(or the same path under $VELLUM_FE_DIR when that is set), in the [character] table as watched_spells. It is written by autosave when it changes and restored at login. There is no reason to edit it by hand.

Display

StateTextColor
Watched spell not active<number> <name>, e.g. 101 Spirit Warding Iamber #d78700
Watch list satisfiedAll spells upgreen #5f875f
No watch list yet.spellwatch add <n> to watchgray #666666

Rows appear in watch-list order (the order you added them), not sorted. The spell color table does not apply. The terminal appends a count to the window titleMissing Spells (3) — and drops it when nothing is missing; the GUI never adds a count.

Where the settings live

SettingDesktop GUITerminal (TUI)
Widget settingsnone — no widget section in the right-click menunone — .editwindow shows base fields only
Title, border, colors, lockRight-click ▸ Window / Appearance.editwindow <name>

Standard window keys — row, col, rows, cols, show_border, border_style, title, locked — apply as they do to any window.

Experience (GemStone IV)

Your level, your mind state, and how close you are to the next one — parked on screen so you stop typing exp between every kill.

What it’s for

Mid-hunt you want two numbers: how saturated your mind is, and how much further you have to go. Asking for them costs a command and scrolls away the combat you were reading.

This window keeps both standing. The mind bar fills as your head fills, the experience bar creeps toward your next level, and your level sits above them. It updates itself when the game sends new numbers — you never ask.

This is the GemStone IV window. DragonRealms tracks experience per skill and has an entirely different widget for it: see Experience (DragonRealms). The two share a name in the catalog and nothing else.

Set it up

  1. Click Windows in the top toolbar, expand Character, and tick Experience. It arrives 5 rows by 30 columns, titled Experience, with its text centered. (Typed equivalent: .addwindow gs4_experience gs4_experience 0 0 30 5.)
  2. Right-click the window and open its Experience section to choose what it shows. Level, Mind state, and Experience bar are on; Total exp and Ascension exp are off. Mind bar color and Exp bar color take a #rrggbb value or a color name, and commit on Enter or when you click away.
📷 screenshot pending
The window's right-click menu with the Experience section open, showing the five field checkboxes above the two bar-color fields.

Expected result: a compact window reading your level, a Mind: bar, and a Next: bar. Ticking Total exp adds an Exp: 1,234,567 line beneath them.

Common setups

A one-line experience strip above your vitals

The window is at its best flattened into a status strip rather than kept as a box.

  1. Add Experience from the catalog’s Character group.
  2. Right-click it, open the Experience section, and untick Level. Your level rarely changes; the two bars are what you watch.
  3. Under Appearance ▸ Title bar, turn the title bar off, and under Appearance ▸ Frame keep only the left and right border sides.
  4. Drag it directly above your vitals window, or send it to the same zone.

You’ll see: two labelled bars, no chrome, sitting flush on your vitals stack — mind saturation and next-level progress readable without a glance away from combat.

Tips & gotchas

⚠️ This window is GemStone IV only, and the two ways of adding it disagree about that. Both pickers hide it from DragonRealms characters — it is absent from the GUI Windows catalog and from the bare-.addwindow picker. The full six-argument .addwindow form does not check, so it builds the window on a DR character, where it stays permanently empty. If the game is not set at all, VellumFE assumes GemStone IV and the row appears — which is why it shows up when you connect through Lich without naming a game.

⚠️ The two frontends label the bars differently. The GUI writes Mind: and Next: in front of the game’s text; the TUI paints the game’s text alone. Same numbers, different wording — don’t be thrown reading a screenshot from the other frontend.

The window can appear on its own. GemStone IV sends this data as a dialog, and VellumFE claims that dialog for this widget. If you have no experience window when the game first sends it, one is created and bound for you. It never duplicates: a second send finds the existing window and leaves it alone. Untick it in the catalog and it stays hidden even when the game re-sends.

Empty until the game speaks. Before the first update both frontends print No experience data yet. in dim text. Nothing is broken — the game sends this on login and on change, not on demand.

Two rows only appear when the numbers arrive. Total exp and Ascension exp stay blank until something has sent those figures, even with their boxes ticked. Ticking them on a session that never receives them changes nothing visible.

The height is capped, deliberately. The window floors at 3 rows and ceilings at 7 — one row per field plus borders. Ticking all five fields needs all 7; dragging taller does nothing, because there is nothing more to draw.

Leave Exp bar color empty on purpose if you’re capped. An unset exp color draws the filled portion in your theme background instead of a fill color, which reads correctly for a character with nowhere further to go. Setting a color overrides that.

A blank color field is not an error. Both color boxes accept #rrggbb or a color name; an unparseable value is ignored and the previous color stands.

See also

  • Experience (DragonRealms) — the other game’s experience widget, sharing the name and nothing else
  • Mini Vitals — health, mana, stamina and spirit in the same compact style
  • Progress Bars — standalone bars, including a mindState bar of your own
  • Encumbrance — the other single-purpose bar-plus-label window
Config reference (TOML)

Written by the catalog, .addwindow, and the window menu. Hand-editing is for troubleshooting, not the normal path.

[[windows]]
name = "gs4_experience"
widget_type = "gs4_experience"
title = "Experience"
row = 0
col = 0
rows = 5
cols = 30
min_rows = 3
max_rows = 7
min_cols = 20
show_border = true
align = "center"
show_level = true
show_mind_bar = true
show_exp_bar = true
show_total_exp = false
show_ascension_exp = false

Widget fields

widget_type = "gs4_experience". The type string is exactly gs4_experience; an unrecognized type does not error, it quietly creates a text window instead.

FieldTypeDefaultWhat it does
alignstring"center"left, center (or centre), or right. Anything else falls back to left. Read only when the window is first built — change it and reload the layout. Neither editor offers it.
show_levelbooleantrueThe level line
show_mind_barbooleantrueThe mind-state bar
show_exp_barbooleantrueThe next-level bar
show_total_expbooleanfalseAn Exp: 1,234,567 line, drawn only once that number arrives
show_ascension_expbooleanfalseAn Ascension: 1,234,567 line, same condition
mind_bar_colorstringunset (cyan)Mind bar fill. #rrggbb or a color name. Omitted from the file when unset.
exp_bar_colorstringunsetExp bar fill. Unset means the theme background — the correct look at max level. Omitted from the file when unset.

Standard window keys — row, col, rows, cols, show_border, border_style, border_sides, show_title, title, locked — apply as they do to any window. The TUI honors show_border, show_title, and border_sides on this widget specifically.

Catalog preset

Catalog rowCategoryTitleSizeGame
ExperienceCharacterExperience5 rows x 30 cols (floor 3, ceiling 7 rows; floor 20 cols)GemStone IV only

The gate hides the row from DragonRealms characters in both the GUI catalog and the .addwindow picker. An unset game counts as GemStone IV, so the row appears by default. The six-argument .addwindow form is not gated and builds the window regardless.

Where the settings live

SettingDesktop GUITerminal (TUI)
Level, Mind state, Experience bar, Total exp, Ascension expRight-click ▸ Experience.editwindow gs4_experience
Mind bar color, Exp bar colorRight-click ▸ Experience.editwindow gs4_experience (Mind / Exp)
Title, title bar, lockRight-click ▸ Window / Appearance ▸ Title bar.editwindow gs4_experience
Border, accent, backgroundRight-click ▸ Appearance ▸ Frame.editwindow gs4_experience

There is no [gs4_experience] config section — this window has no global settings. Word wrap and content alignment are not offered under Appearance ▸ Text for this widget; the layout is fixed rows, not flowing text.

Data the client stores but never shows

The feed carries more than the window draws: exact field experience and its cap, experience remaining until next level, and the Fash’lonae, Lumnis and RPA bonus flags. VellumFE parses and keeps all of them, and no frontend renders any of them today. Only total absorbed experience and ascension experience have display toggles.

Experience (DragonRealms)

Every skill the game is tracking for you, listed and standing still — so you can see what’s ripe to train without breaking rhythm to ask.

What it’s for

DragonRealms reports your training a skill at a time, and it reports a lot of skills. Asking for the whole picture buries the screen; asking about one skill tells you about one skill.

This window keeps the game’s own running tally on screen: one line per skill it is tracking, each showing the value the game last sent, in the order the game established at login. When a value changes the line changes, and nothing else moves. You read it in a glance and go back to what you were doing.

This is the DragonRealms window. GemStone IV tracks experience as a level, a mind state and a progress bar, and has an entirely different widget for it: see Experience (GemStone IV). The two share a name in the catalog and nothing else.

Set it up

  1. Click Windows in the top toolbar, expand Character, and tick Experience. It arrives 20 rows by 35 columns, titled Experience. (Typed equivalent: .addwindow experience experience 0 0 35 20.)
  2. There is no widget section in this window’s right-click menu — this widget has no settings of its own. Its border, title, colors and font are the standard window settings under Window and Appearance.
📷 screenshot pending
A DragonRealms Experience window listing tracked skills, one Name: value line each, in the game's login order.

Expected result: a scrollable list of Skill: value lines in a monospaced face. Before the game sends anything it reads No experience data yet.

Common setups

A tall skills column you can actually read

This widget is a list, and a list wants height far more than it wants width.

  1. Add Experience from the catalog’s Character group.
  2. Send it to the Right Bar and drag it to fill the column top to bottom. The list is long; every extra row is one more skill visible without scrolling.
  3. Keep it around 35 columns. Lines are Skill: value and rarely need more.
  4. In the GUI, drop the text size a step under Appearance ▸ Text to fit more lines.

You’ll see: one column showing the game’s whole tracked-skill tally at once, refreshing itself line by line as values change, with nothing else on screen moving.

Tips & gotchas

⚠️ This window is DragonRealms only, and unlike most game gates this one is real. Both pickers hide it from GemStone IV characters. Because VellumFE treats an unset game as GemStone IV, the row is hidden by default too — connecting through Lich without naming a game hides this window even on a DragonRealms character. Set the game, or use the full six-argument .addwindow form, which is not gated. Built on a GemStone IV character it stays permanently empty: that game never sends this feed.

⚠️ The terminal list does not scroll; the GUI list does. The GUI puts the lines in a scroll area, so a long list is fully reachable. The TUI draws them as a plain block and clips at the bottom border — lines past the window height are not shown and no key reaches them. In the terminal, size the window to the list rather than scrolling it.

Empty until the game speaks. Before the first update the GUI reads No experience data yet. and the TUI reads (No experience data). The game establishes which skills exist at login and sends values as they change.

The order is the game’s, not alphabetical. Skills appear in the sequence the game declared at login and hold that position for the session. A skill that changes does not jump to the top — the line updates in place, which is what makes the window readable at a glance.

A skill with no value yet is not listed. Only skills the game has actually sent a value for get a line. The list can therefore grow during a session as more skills report in.

The list is not cleared between characters in one run. Skill names accumulate for the life of the session, so if you have swapped characters without restarting, treat a surprising entry with suspicion and restart to get a clean list.

The window redraws only when a value actually changes. VellumFE compares each incoming value against the one it is showing and does nothing when they match. A window that looks frozen is usually correct.

Right-clicking a line opens the window menu. These lines are plain text, not game objects — there is nothing to click through to.

See also

  • Experience (GemStone IV) — the other game’s experience widget, sharing the name and nothing else
  • Perception — the other DragonRealms-only Character window
  • Progress Bars — including concentration, DragonRealms’ own bar
  • Encumbrance — a single-purpose Character window offered in both games
  • Text Windows — how scrolling, stream-fed windows work generally
Config reference (TOML)

Written by the catalog and .addwindow. Hand-editing is for troubleshooting, not the normal path.

[[windows]]
name = "experience"
widget_type = "experience"
title = "Experience"
row = 0
col = 0
rows = 20
cols = 35
min_rows = 5
min_cols = 20
show_border = true
align = "left"

Widget fields

widget_type = "experience". The type string is exactly experience; an unrecognized type does not error, it quietly creates a text window instead.

This widget has exactly one field of its own.

FieldTypeDefaultWhat it does
alignstring"left"left, center (or centre), or right. Anything else falls back to left. Read only when the window is first built — change it and reload the layout. Neither editor offers it, and the GUI list is always left-aligned regardless.

Everything else is a standard window field: row, col, rows, cols, show_border, border_style, title, locked. There is no [experience] config section — this window has no global settings, and no widget section in the GUI right-click menu.

The shipped layout_template.toml is wrong about this widget. Its commented example lists stream, buffer_size, show_rates and compact_mode. None of those exist. They are silently ignored if you copy them in. align is the only widget field.

Catalog preset

Catalog rowCategoryTitleSizeGame
ExperienceCharacterExperience20 rows x 35 cols (floor 5 rows, 20 cols)DragonRealms only

The gate hides the row from GemStone IV characters in both the GUI catalog and the .addwindow picker. An unset game counts as GemStone IV, so the row is hidden by default until the game is set to a DragonRealms instance. The six-argument .addwindow form is not gated and builds the window regardless.

Where the settings live

SettingDesktop GUITerminal (TUI)
Title, title bar, lockRight-click ▸ Window / Appearance ▸ Title bar.editwindow experience
Border, accent, backgroundRight-click ▸ Appearance ▸ Frame.editwindow experience
Font, text sizeRight-click ▸ Appearance ▸ Textnot offered

Word wrap and content alignment are not offered under Appearance ▸ Text for this widget — the lines are short and fixed-shape.

Encumbrance

Know you’re overloaded before the roundtime tells you — the level, the color, and the game’s own explanation of it, parked on screen.

What it’s for

Encumbrance is the stat you notice by its consequences. You loot a suit of plate, your swings get slower, and you spend a command and a screenful of text working out why. Meanwhile the game has been publishing your encumbrance level the whole time.

This window parks that reading on screen: a bar that fills as you load up, colored by how bad it has got, with the level name written across it and the game’s own description underneath. It changes the moment you pick something up, so “should I sell this before the next room?” becomes a glance instead of a check.

It is one of the few GemStone-flavored windows that works in both games — DragonRealms characters get it too.

Set it up

  1. Click Windows in the top toolbar, expand Character, and tick Encumbrance. It arrives 4 rows by 25 columns. (Typed equivalent: .addwindow encum encum 0 0 25 4 — note the type string is encum, not the word on the row.)
  2. Right-click the window and open the Encumbrance section. It carries two checkboxes: Level bar and Help text. Untick Help text for a bar on its own; untick Level bar for the description alone.
📷 screenshot pending
An Encumbrance window right-clicked with the Encumbrance section open, showing the Level bar and Help text checkboxes above the filled bar and its blurb.

Expected result: a bar reading Encumbrance: Light that lengthens and changes color as you load up, with the game’s description of that level on the line below.

Common setups

A carry-weight corner next to what you’re carrying

Encumbrance is only actionable next to the thing causing it, so put it where you already look when you’re deciding what to drop.

  1. Add Encumbrance from the catalog’s Character group and inventory from the same group.
  2. Send both to the same zone — the Right Bar works well — with encumbrance directly above the inventory list.
  3. Right-click the encumbrance window ▸ Encumbrance ▸ untick Help text. Once you know what “Moderate” means, the sentence explaining it is spent space, and the window shrinks to the bar alone.

You’ll see: a single colored bar above your carried list, shifting from green through orange as you loot, with the exact list of what did it directly underneath.

Tips & gotchas

⚠️ The dedicated widget and a plain bar bound to encumlevel are both real, and they are not the same thing. The game publishes one encumlevel reading and everything that wants it gets it, so you can run both at once. Choose by what you want on screen:

  • This widget — the level name on the fill, color bands that change as you load up, and the game’s description line. The most information, in a 4-row window.
  • A progress bar with Bar id encumlevel — one flat-colored fill, no bands, no description, in a window as small as you like. Right for a header strip where everything else is a bar too.
  • A bar inside Mini Vitals — desktop GUI only, where Encumbrance is one of the selectable bars, so it rides alongside health and mana in a single window.

⚠️ The color bands differ between frontends, and only the terminal lets you set them. The terminal uses four bands — 0–20, 21–50, 51–80, 81–100 — with Light, Moderate, Heavy and Critical colors you choose. The desktop GUI uses three fixed bands — 0–33, 34–66, 67 and up — in green, orange and red, and reads none of the four color fields. Setting them changes your terminal and leaves the GUI alone.

⚠️ Each frontend offers one toggle the other doesn’t. The GUI has Level bar and Help text; the terminal has Show Label (the description) and no way to hide the bar. A layout with show_bar = false renders bar-less in the GUI and with a bar in the terminal.

The two frontends word the bar differently. The GUI always prefixes it — Encumbrance: Light — while the terminal centers the bare level name on the fill. Content alignment is a terminal setting; the GUI ignores it here.

An empty window means the game hasn’t sent a reading yet. The terminal says so out loud with (No encumbrance data). It fills in on the next update.

This widget works in both games. Unlike its neighbours in the Character group, it carries no game gate, so it appears in the catalog and the .addwindow picker for DragonRealms characters as well as GemStone IV ones.

The window is capped at 4 rows and floors at 3, so growing it is a matter of width rather than height. With Help text off, 3 rows is the whole window.

See also

Config reference (TOML)

Written by the catalog and the editors above. Hand-editing is for troubleshooting, not the normal path.

[[windows]]
name = "encum"
widget_type = "encum"
title = "Encumbrance"
row = 0
col = 0
rows = 4
cols = 25
show_border = true
align = "left"
show_label = true
show_bar = true

widget_type = "encum". The type string is encum, not encumbrance — the catalog row and the window title read “Encumbrance”, but the type is the short form. An unrecognized type does not error, it quietly creates a text window instead.

Widget fields (EncumbranceWidgetData)

FieldTypeDefaultWhat it does
show_barbooleantrueDraw the level bar. GUI only — the terminal always draws it.
show_labelbooleantrueDraw the game’s description line under the bar
alignstring"left"Alignment of the description line: left, center, right. Terminal only.
color_lightstringgreenFill for 0–20%. Terminal only.
color_moderatestringyellowFill for 21–50%. Terminal only.
color_heavystringorange #ffa500Fill for 51–80%. Terminal only.
color_criticalstringredFill for 81–100%. Terminal only.

The desktop GUI’s bands are fixed at 0–33 green #55b86c, 34–66 orange #ff8800, and 67+ red #cd4d4d, and it prefixes the bar text with Encumbrance: .

Feed

The window is a view onto the game’s encum dialog: the encumlevel progress bar supplies the percentage and the level name, and the encumblurb label supplies the description. The same encumlevel reading is available as a Bar id for an ordinary progress window, and as the Encumbrance bar inside a desktop-GUI Mini Vitals window. All three can be on screen at once.

Catalog preset

Catalog rowCategoryTitleSizeGame
encumCharacterEncumbrance4 rows x 25 cols (floor 3 rows / 15 cols, ceiling 4 rows)Both games — no gate

Where the settings live

SettingDesktop GUITerminal (TUI)
Show the barRight-click ▸ EncumbranceLevel barnot offered
Show the descriptionRight-click ▸ EncumbranceHelp text.editwindow encumShow Label
Band colorsnot offered (fixed).editwindow encumLight / Moderate / Heavy / Critical
Content alignmentRight-click ▸ Appearance ▸ Text.editwindow encum

Standard window keys — row, col, rows, cols, show_border, border_style, title, locked — apply as they do to any window.

Perception

Your DragonRealms perception list parked on screen and sorted by urgency, so the spell about to lapse is at the top instead of buried in a wall of output.

What it’s for

Asking the game what you’re perceiving costs a command and a screenful of text, and the reply arrives in whatever order the game feels like. The thing you actually wanted — the one entry that’s fading — is somewhere in the middle of it.

A perception window parks that list somewhere permanent and sorts it, so the entries with the most life left sit at the top and anything fading falls to the bottom. It refreshes itself when the game sends a new list, and the entries stay clickable.

This is a DragonRealms window, and it is one of two — its counterpart is the DR Experience window.

Set it up

  1. Click Windows in the top toolbar, expand Character, and tick perception. It arrives 20 rows by 40 columns, titled Perceptions. (Typed equivalent: .addwindow perception perception 80 0 40 20.)
  2. There is nothing else to configure. This window has no widget section in its right-click menu — no feed to pick, no stream to set. Title, border, font and colors live under Window and Appearance as they do on any window.
  3. Click an entry to ask the server for that object’s verb menu.
📷 screenshot pending
A Perceptions window with percentage entries at the top, (OM) and (Cyclic) entries beneath, and a (Fading) entry last.

⚠️ The GUI does not apply this window’s three settings. Short spell names, text replacements and sort direction are authored in the terminal and honored there. The GUI always paints the list in the default order, unabbreviated and unreplaced. The same layout renders two ways across frontends.

Expected result: a window titled Perceptions listing what you’re perceiving, highest percentage first. Empty until the game sends a list, where it reads Nothing perceived.

Common setups

A DragonRealms character column

Perception earns its space beside its sibling rather than alone.

  1. Add perception and experience from the catalog’s Character group — the two DR-only windows.
  2. Send both to the same zone; the Right Bar works well.
  3. Give perception the taller of the two — the list runs longer than the skill list does.

You’ll see: one column reading what you’re perceiving on top and what you’re training below, each refreshing on its own when the game sends a new list.

Fitting a long list into a narrow window (terminal)

The default sort buries what’s fading at the bottom, which is backwards when a narrow window means you only see the top few rows.

  1. .editwindow perception.
  2. Set Use short spell names on.
  3. Cycle Sort direction to ascending.
  4. Ctrl+S.

You’ll see: abbreviated names fitting a much narrower window, with the entry closest to expiring now sitting in the top row where you’ll notice it. Widen it back out and turn short names off to read the full names again.

Tips & gotchas

⚠️ This window is DragonRealms only, and the two ways of adding it disagree about that. Both pickers hide it from GemStone IV characters — it is absent from the GUI Windows catalog and the bare-.addwindow picker. The six-argument .addwindow form is not gated and will build the window for a GS4 character; you get an empty box, because GemStone IV never sends this feed. When no game is set at all, VellumFE assumes GemStone IV, so a Lich connection made without naming a game will hide this row — set the game if the row is missing on a DR character.

⚠️ The window’s three settings are honored in the terminal and ignored in the GUI. Sort direction, short spell names and text replacements are read by the TUI renderer only. A layout you author in the terminal and open in the GUI will look different — same entries, default order, full names. Author for the frontend you play in.

Sorting is by kind first, then by number, and it is not alphabetical. Percentage entries rank above everything, ordered by their own percentage. Below them come ongoing-magic (OM) entries, then indefinite and cyclic ones, then anything the client doesn’t recognize, then countable (roisaen) entries by their count — and anything marked (Fading) always sorts last. Descending order puts that ranking top-to-bottom; ascending inverts the whole thing.

The window fills on a prompt, not the instant text arrives. Entries accumulate as the game sends them and are parsed, sorted and painted when the next prompt lands. A window that looks briefly stale during a long burst catches up on its own.

A fresh list replaces the old one wholesale. The game clears the window before sending a new set, so there is no scrollback and nothing accumulates. Scrolling up shows nothing earlier because nothing earlier is kept.

Highlight rules apply here. Perception rows run through the same highlight engine as your text windows, on the perception stream — so you can color a specific entry, or make one bold, with an ordinary highlight rule.

A text replacement that empties a row removes it. If a replacement rule reduces a row to nothing, the row is dropped rather than drawn blank. That is a legitimate way to hide entries you never want to see — and an easy way to lose one by accident with an over-broad rule.

Right-clicking a row opens the window menu, not a menu for that entry. Left-click the entry to ask the server for its verbs.

The stream and buffer settings you may see referenced are fixed. The window reads one feed with a fixed cap, and neither editor offers them, because changing them was never wired up.

See also

Config reference (TOML)

Written by the catalog and .addwindow. Hand-editing is for troubleshooting, not the normal path.

[[windows]]
name = "perception"
widget_type = "perception"
row = 0
col = 80
rows = 20
cols = 40
show_border = true
title = "Perceptions"
sort_direction = "descending"
use_short_spell_names = false
text_replacements = []

Widget fields

widget_type = "perception". The type string is exactly perception; an unrecognized type does not error, it quietly creates a text window instead.

FieldTypeDefaultWhat it does
sort_direction"descending" or "ascending""descending"Order by weight. Terminal only
use_short_spell_namesbooleanfalseSubstitute standard spell abbreviations. Terminal only
text_replacementsarray of { pattern, replace }[]Find/replace applied per row; an empty result drops the row. Terminal only
streamstring"percWindow"Inert. Fixed at the preset value; offered by no editor
buffer_sizeinteger100Inert. Fixed at the preset value; offered by no editor

The three working fields are read by the terminal renderer only. The GUI paints the list in the default order, unabbreviated and unreplaced.

Sort weights

Entry formWeightPosition under descending
(94%) and other percentages3000 + the percentageTop, ordered among themselves
(OM) — ongoing magic2000Below percentages
(Indefinite) / (Cyclic)1500Below OM
Anything unrecognized500Below indefinite
(82 roisaen) and other countsthe count itselfBelow unrecognized when the count is small
(Fading)0Last

ascending reverses the whole ordering.

Catalog preset

Catalog rowCategoryTitleSizeGame
perceptionCharacterPerceptions20 rows x 40 cols (floor: 5 x 20)DragonRealms only

The game gate hides the row from GemStone IV characters in both the GUI catalog and the .addwindow picker. An unset game counts as GemStone IV, so the row is hidden by default until the game is set. The six-argument .addwindow form is not gated and will build the window regardless.

Where the settings live

SettingDesktop GUITerminal (TUI)
Sort direction, short spell names, text replacements❌ no widget section in the right-click menu.editwindow perception
Title, border, lockRight-click ▸ Window.editwindow perception
Font, text size, colors, frameRight-click ▸ Appearance.editwindow perception

There is no [perception] config section — this window has no global settings. Standard window keys — row, col, rows, cols, show_border, border_style, title, locked — apply as they do to any window.

Empty states

FrontendText
Desktop GUINothing perceived.
Terminal (TUI)(blank)

Betrayer

Your blood pool and the items feeding it, on screen instead of behind a command — and the window grows and shrinks itself as that list changes.

What it’s for

The game publishes a Betrayer panel: a blood-point total out of 100, and a list of the items contributing to the pool, some of them flagged as active. It arrives as its own feed, which means it can sit permanently in your layout rather than being something you ask for.

This window draws that panel. A dark red bar carries the point total, the contributing items are listed beneath it, and the whole thing refreshes when the game sends an update. It is a small, single-purpose window for a small, single-purpose reading.

What the numbers mean in play is a GemStone IV mechanic, not a client feature. VellumFE reads what the game sends and draws it — the point total, the item names, and which items carry the game’s active marker. It does not interpret them.

Set it up

  1. Click Windows in the top toolbar, expand Dialogs, and tick Betrayer. It arrives 4 rows by 30 columns. (Typed equivalent: .addwindow betrayer betrayer 0 0 30 4.)
  2. There is no widget section in this window’s right-click menu — the GUI offers no Betrayer settings at all. Everything else on the menu applies as usual: Appearance ▸ Frame for the border, Arrange for placement, Window ▸ Lock in place to pin it.
📷 screenshot pending
A Betrayer window showing the dark red blood-point bar above its list of contributing items, with the Dialogs group of the Windows catalog open beside it.

Expected result: a window headed Betrayer with a red bar reading Blood Points: 100 and the contributing items listed under it.

Common setups

A blood pool that stays out of the way until it matters

The item list is what makes this window tall, and most of the time you want the number, not the list.

  1. Add Betrayer from the catalog’s Dialogs group and place it wherever you keep small readouts.
  2. In the terminal, .editwindow betrayer and untick Show Items. The window drops to its 3-row floor: border, bar, border.
  3. Right-click it in the GUI ▸ Appearance ▸ Frame and turn the border off, so what is left is a bare red strip.

You’ll see: a one-line red bar reading your point total, taking almost no space — and if you tick Show Items back on, the window grows on its own to fit the list again.

Tips & gotchas

⚠️ This window resizes itself, and it is the only widget that does. VellumFE recalculates its height from the item count on every update — one row for the bar, one per item, plus borders — clamped between 3 and 12 rows, and saves the new height into your layout. Dragging it to a height you like will not stick, because the next update recomputes it. Control the height with Show Items instead: off pins it at 3 rows.

⚠️ The active marker shows in the terminal and not in the desktop GUI. The game prefixes an active item with !. The terminal draws that ! in its own alert color; the GUI prints the line plainly, ! and all. The mark is visible in both — it is the coloring that differs.

An empty window means the game hasn’t sent the panel yet. The terminal says so out loud with (No blood pool data). It fills in when the panel next arrives, and it empties again when the game clears it.

The bar is the point total out of 100. It fills proportionally and carries the game’s own text — Blood Points: 100 — centered on the fill. When the game sends no text, the window writes that line itself from the number.

The desktop GUI can put this reading in a different window. A Mini Vitals window in the GUI offers Blood as one of its selectable bars, so the point total can ride alongside health and mana in one strip. That gets you the number without the item list — and without this window at all.

This widget is GemStone IV only. Both pickers hide it from DragonRealms characters — it is absent from the GUI Windows catalog and from the bare-.addwindow picker. The full six-argument form does not check, so it will build the window on a DR character. You get an empty box: DragonRealms never sends this feed. If the game is not set at all, VellumFE assumes GemStone IV and the row appears, which is why it shows up when you connect through Lich without naming a game.

See also

Config reference (TOML)

Written by the catalog and .editwindow. Hand-editing is for troubleshooting, not the normal path.

[[windows]]
name = "betrayer"
widget_type = "betrayer"
title = "Betrayer"
row = 0
col = 0
rows = 4
cols = 30
show_border = true
show_items = true
bar_color = "#8b0000"

widget_type = "betrayer". The type string is exactly betrayer; an unrecognized type does not error, it quietly creates a text window instead.

Widget fields (BetrayerWidgetData)

FieldTypeDefaultWhat it does
show_itemsbooleantrueList the contributing items under the bar. Also decides the window’s height.
bar_colorstring#8b0000Fill color of the point bar. Terminal only — the GUI draws #cd4d4d regardless.

rows is not yours to set for long: it is recomputed from the item count on every update and written back, clamped to the min_rows / max_rows below.

Feed

The window is a view onto the game’s BetrayerPanel dialog. The point total comes from its lblBPs label — read as Blood Points: N, treated as a percentage of 100 — and the item list from its lblitemN labels, kept in the order the game sends them with any leading ! intact. A clear from the game empties the window.

The point total is also selectable as the Blood bar inside a desktop-GUI Mini Vitals window.

Catalog preset

Catalog rowCategoryTitleSizeGame
betrayerDialogsBetrayer4 rows x 30 cols (floor 3 rows / 20 cols, ceiling 12 rows)GemStone IV only

The game gate hides the row from DragonRealms characters in both the GUI catalog and the .addwindow picker. An unset game counts as GemStone IV, so the row appears by default. The six-argument .addwindow form is not gated and will build the window regardless.

Where the settings live

SettingDesktop GUITerminal (TUI)
Show itemsnot offered.editwindow betrayerShow Items
Bar colornot offered (fixed).editwindow betrayerBar Color
Border, placement, lockRight-click ▸ Appearance / Arrange / Window.editwindow betrayer

There is no widget section in the GUI right-click menu for this type. The active-item color the terminal uses for the ! marker is a global setting ([ui] betrayer_active_color, default #ff4040), not a per-window one. Standard window keys — row, col, rows, cols, show_border, border_style, title, locked — apply as they do to any window.

Hotbars

Buttons you designed yourself — that dim during roundtime, turn green when you’re hidden, count down a cooldown, and answer to a hotkey.

What it’s for

You have a handful of commands you fire constantly, and a few you fire only at particular moments. Typing them is fine until the moment matters, and then you’re typing hide at a character who is already hidden, or incant 909 with six mana.

A hotbar is a row of buttons you author. Each sends a command on click or on a key. What makes it more than a row of shortcuts is that a button can watch your state and restyle itself — recolor, relabel, dim, swap its icon, swap the command it sends, or paint a countdown across its face. The button stops being a thing you aim at and becomes a thing you read.

One line on the difference: a quickbar shows the buttons the game and your scripts push at you; a hotbar shows the buttons you built, and only a hotbar reacts to your state. The full comparison table lives on the quickbar page.

Set it up

Two pieces have to exist: a bar (the buttons, in hotbars.toml) and a window to show it in. They are joined by name, and that is the single rule to remember.

⚠️ A hotkeybar window displays the bar with the same name as the window. There is no bar-picker anywhere in the interface. A window named combat shows the bar named combat; rename either and the pairing breaks. .addwindow combat hotkeybar 0 0 40 3 builds a window already bound to a bar called combat.

  1. Type .addwindow combat hotkeybar 0 0 40 3 in the command input. The name you choose is the bar it will look for. (The Windows toolbar catalog also carries an Actions row under Hotbars, but it creates a window named hotkeybar bound to a bar of that name — use .addwindow when you want to pick the name.)
  2. Open the Editors hub in the toolbar and click Hotbars, or type .hotbars. Add a bar named combat. Leave Global (all characters) ticked for a bar every character gets.
  3. Click Add button and fill in Label and Command. Add a Hotkey by typing it or clicking Capture and pressing the key.
  4. Under States, click Add state to make the button react to your situation. The kind combo carries thirteen leaves — Effect active, Roundtime active, Indicator, Vital, Injury, Hand holds among them.
  5. Watch the Preview: row above the button list. It renders the bar against your real game state using the same code the window uses. Click Save bar.

Right-click the window ▸ the Hotbar section ▸ Edit hotbars… reopens the same editor.

📷 screenshot pending
The hotbar editor: the bar list with [G] and [C] scope badges, the button form, and a button's States list showing (first matching state styles the button) above two ordered state cards.

Expected result: a bar of buttons on screen. Clicking one sends its command; step into roundtime and any button carrying a roundtime state dims and counts down.

Common setups

A button that tells you your own state

The payoff of a state is that a button can stop offering an action you’re already in.

  1. Open .hotbars, add a button with Label Hide and Command hide.
  2. Add a state, choose Indicator on its condition row, set the id to hidden and leave active ticked.
  3. Under Style while active:, set Label to Hidden and the foreground to #80ff80.
  4. Add a second state below it for Roundtime active with dim ticked. Save bar.

You’ll see: a plain Hide button in the open, dimmed while roundtime runs, and a green button reading Hidden the moment you’re in the shadows — with hidden winning over roundtime, because it sits above it.

A cooldown you can see from across the room

Give a button a Countdown overlay of Roundtime, Casttime, or an Effect by category and name. The remaining seconds paint on the button’s face and vanish when it reaches zero.

Pair it with a state on the same condition and set that state’s dim: the button dims and shows the number, so it reads as unavailable and tells you how long for.

You’ll see: an Attack button that dims and reads 3 at the top of a roundtime, ticking down to nothing as you become able to swing again.

Tips & gotchas

⚠️ The first matching state wins. States are checked top to bottom and the first hit styles the button; the rest never run. A broad condition above a narrow one swallows it — put Injury >= 3 above Injury >= 1, and health < 25% above health < 50%. Reorder with each card’s ^ and v. When nothing matches, the button falls back to its default style, then to the window’s and theme’s colors.

⚠️ Hotkeys belong to the BAR, not to the window. Every hotkey in every loaded bar registers when your config loads, whether or not any window is showing that bar. A bar you stopped displaying still owns its keys. These live only in the running keybind map — they are never written to keybinds.toml, and the keybind editor cannot see them.

⚠️ An existing keybind always beats a hotbar button. On a clash the button loses silently at runtime; the editor warns you at the time and marks the row (key conflict). Two buttons claiming one key behave the same way — the first bar in the file wins.

⚠️ A per-character bar REPLACES a global bar of the same name, wholesale. It is not a per-button merge: the character copy is the entire bar. The editor’s [G] and [C] badges tell you which copies exist. Delete the [C] one to fall back to global.

The window’s name is the binding, and there is no picker to change it. If a hotkeybar window renders empty, the overwhelmingly likely cause is that no bar carries the window’s name. This is the same rule indicator windows follow.

Icons are GUI-only; the terminal always draws the label. A button’s Face setting (Text / Icon / Icon + label), its icon art, grayscale, and border effects render in the desktop GUI only. Give every button a label worth reading and one bar serves both frontends.

Conditions are one vocabulary, learned once. The same thirteen leaves drive hotbar states, indicator icons, and hand icons. Indicator ids are standing, kneeling, sitting, prone, stunned, bleeding, hidden, invisible, webbed, joined, and dead. Injury levels run 1-3 for wounds and 4-6 for scars, with 0 healthy. Vital takes a % or an abs unit — use abs for spell costs, because a spell costs a number of mana and not a fraction of your pool.

Editors build one level of nesting. all of / any of groups nest once in the GUI builder. Deeper trees written by hand still evaluate correctly and render in the editor as (nested group - edit in hotbars.toml).

A state’s command override is literal text. Whatever you type in Command while active: is sent as-is instead of the button’s command while that state matches. Anything dynamic belongs inside the command — a ;eq … line that Lich intercepts and evaluates.

Conditions that can’t be answered fail closed. Spell affordable returns false for spell numbers it doesn’t know and for formula-cost spells, and hand item-type tests return false when the item classifier is unavailable. A button grays out rather than lying to you.

See also

Config reference (TOML)

Two files are involved. The window lives in your layout; the bar lives in hotbars.toml. Use .hotbars and the catalog rather than editing either by hand.

The window

[[windows]]
name = "combat"
widget_type = "hotkeybar"
row = 38
col = 0
rows = 3
cols = 60
bar = "combat"
orientation = "horizontal"
FieldTypeDefaultWhat it does
barstring"default"Bar name from hotbars.toml. A window created by .addwindow or the catalog sets this to the window’s own name, and neither window editor exposes the field — treat the window name as the binding
orientationstring"horizontal"horizontal flows buttons on one row; vertical puts one per row. Layout-file only — no editor in either frontend authors it

Preset: 3 rows x 60 cols, pinned at 3 rows, border on, title bar off, title Actions. Catalog category Hotbars, ungated — offered to GemStone IV and DragonRealms alike. Minimum size 20 cols x 1 row.

Widget section (GUI right-click): Hotbar — one entry, Edit hotbars…. TUI .editwindow: no widget fields at all. Everything about the buttons is in .hotbars.

The bar — hotbars.toml

Global: ~/.vellum-fe/global/hotbars.toml. Per character: ~/.vellum-fe/profiles/<Character>/hotbars.toml. A character bar with the same name replaces the global bar wholesale; character-only names append.

[[bars]]
name = "combat"
title = "Combat"
icon_size = 32

[[bars.buttons]]
id = "hide"
label = "Hide"
command = "hide"
hotkey = "alt+h"
tooltip = "Attempt to hide"
category = "Stealth"

[bars.buttons.countdown]
source = "effect"
category = "Cooldowns"
name = "Shadow Mastery"
name_match = "contains"

[[bars.buttons.states]]
[bars.buttons.states.when]
type = "indicator"
id = "hidden"
active = true
[bars.buttons.states.style]
label = "Hidden"
fg = "#80ff80"

[bars.buttons.default_style]
fg = "#d0d0d0"

Bar fields

FieldTypeDefaultWhat it does
namestringThe bar’s id. A window of this name displays this bar
titlestringnoneDisplay name in the editor
icon_sizeintegernoneIcon face edge in pixels, GUI only. Unset matches text-button height
buttonsarray[]The buttons, in display order

Button fields

FieldTypeDefaultWhat it does
idstringStable id, unique within the bar; editor bookkeeping
labelstringButton text
commandstringSent on click or hotkey
hotkeystringnonekeybinds.toml key syntax (alt+h, f5). Registers for every loaded bar; existing binds win
tooltipstringnoneGUI hover text
categorystringnoneEditor grouping only; no runtime effect
countdowntablenoneCountdown overlay source
statesarray[]Ordered condition rules; first match wins
default_styletablenoneAppearance when no state matches
icontablenoneBase icon, GUI only
icon_modestring"text"text, icon, or icon_and_label. GUI only

Countdown source (source is required)

sourceExtra fieldsShows
effectcategory, name, name_matchSeconds until that effect expires
roundtimeSeconds of roundtime left
casttimeSeconds of casttime left

category is one of Buffs, Debuffs, Cooldowns, ActiveSpells. name_match is exact (default) or contains; both are case-insensitive. An elapsed or absent source draws no overlay.

State fields

FieldTypeDefaultWhat it does
whentableThe condition. type names the leaf or all/any
styletableemptyAppearance while this state matches
countdowntablenoneReplaces the button’s countdown while active
commandstringnoneSent instead of the button’s command while active. Literal text

Style fieldslabel, fg, bg, dim (bool), icon. Unset fields fall through to the button’s default_style, then to widget and theme colors.

Condition leaveseffect_active, effect_inactive, effect_time, rt_active, ct_active, indicator, vital, injury, spell_affordable, hand_empty, hand_holds, spell_prepared, and time of day, plus the all and any groups. An unrecognized condition type is a parse error — unlike widget type strings, this one fails loudly.

Commands

CommandPurpose
.hotbars / .hotbarOpen the hotbar editor
.reload hotbarsRe-read hotbars.toml from disk

Quickbar

The game’s own row of one-click commands, given a permanent home — look, search, inventory, attack, and whatever else the game or a script hands you, without typing any of them.

What it’s for

The game ships a set of clickable command bars: a general one, a combat one, an information one. They exist whether or not you use them, and most of the time they go to waste because there is nowhere for them to live.

A quickbar window is that home. It shows one bar at a time as a row of buttons, and clicking one sends its command. Scripts push their own bars into the same place, which is how a Lich script gives you buttons without you configuring anything.

Reach for a quickbar when you want what the game and your scripts already offer. Reach for a hotbar when you want buttons you designed yourself — see the comparison below, because the two look identical and behave nothing alike.

Quickbar or hotbar?

QuickbarHotbar
Who writes the buttonsThe game and your scripts, pushed over the wireYou, in the .hotbars editor
Where they’re storedArrive live; cached per characterhotbars.toml, global or per character
Change with your state❌ never✅ recolor, relabel, dim on conditions
Countdown overlays
Hotkeys✅ per button
Several bars at once✅ one window switches between them✅ one window per bar
In-app editor❌ — custom bars are TOML-only.hotbars

In short: the quickbar surfaces what someone else made for you. The hotbar is where you build your own. Most players eventually run one of each.

Set it up

  1. Click Windows in the top toolbar, expand Hotbars, and tick quickbar. It arrives 3 rows by 120 columns with its title bar off, sized to sit as a strip across the top or bottom of your layout. (Typed equivalent: .addwindow quickbar quickbar 0 0 120 3.)
  2. Click any button to send its command. A button whose label ends in ...roleplay…, actions… — asks the server for a menu and shows you the verbs it offers instead of firing immediately.
  3. When more than one bar exists, a dropdown appears at the left of the row. Pick a bar from it to switch the window to that bar. With only one bar there is no dropdown, because there is nothing to switch to.
📷 screenshot pending
A quickbar window showing the game's main bar — look, search, inventory — with the bar-switching dropdown open at the left listing main, combat and information.

Expected result: a row of buttons across your layout. Clicking look sends look and the room description appears in your main window.

Common setups

A command strip along the bottom of your layout

The quickbar’s shape — wide and three rows tall, borders trimmed — is meant for one job.

  1. Add quickbar from the catalog’s Hotbars group.
  2. Drag it to the bottom of your layout, above your command input, and stretch it to the full width.
  3. Turn its title bar off (it ships off already) so it reads as a strip rather than a window.
  4. Use the switcher to park it on combat before you hunt.

You’ll see: one row of buttons under your text, with attack, ambush, aim, target and fire a click away — and a switcher that puts the general bar back when you return to town.

Your own bar, alongside the game’s

Custom quickbars are defined in config.toml, and they sit in the same switcher as the game’s. This is the one part of VellumFE with no in-app editor — there is no .quickbars command.

Add this to your config.toml:

[quickbars]
default = "quick-mine"

[[quickbars.custom]]
id = "quick-mine"
title = "Mine"
entries = [
  { type = "link", label = "loot", command = "loot" },
  { type = "sep" },
  { type = "link", label = "skin", command = "skin" },
  { type = "link", label = "search", command = "search" },
]

Restart, and the switcher gains a Mine entry — selected on arrival, because default names it.

You’ll see: your own three buttons in the same strip as the game’s, switchable to and from the combat bar like any other.

⚠️ The id must be exactly quick or start with quick-. Any other id is skipped silently at load with only a warning in the log — no error, no button, no explanation on screen. id = "mine" gives you nothing; id = "quick-mine" works.

Tips & gotchas

⚠️ A quickbar is not a hotbar and cannot be made into one. Its buttons never change color, never dim during roundtime, never show a countdown, and never take a hotkey. If you want a button that reacts to your state, that is a hotbar, and it is a different widget.

⚠️ The bar switcher looks different in each frontend, and one of them hides. The terminal always shows a >> marker at the left of the row and opens a popup list. The GUI shows a dropdown — but only when two or more bars exist. On a session with a single bar, the GUI quickbar looks like it has no switcher, because it doesn’t need one.

Two kinds of button, and the difference is what happens on click. A plain button sends its command straight to the game. A button ending in ... is a menu button: clicking asks the server what verbs apply and shows you a menu to pick from. Nothing fires until you choose.

Empty is a real state and it tells you something. A quickbar window with no bars reads No quickbars configured. in the GUI. Since the game sends its bars once, at login, this normally means you attached to an already-running session and missed the burst — see the next point.

Bars are cached per character, so attaching to a running Lich still gets you buttons. VellumFE writes the bars it has seen into that character’s session cache and restores them on the next start. Where the cache is cold and the login burst was missed, it seeds the game’s three standard bars — main, combat and information — so the window is never blank on a mid-session attach.

Your active bar is remembered too. The bar you last switched to is saved alongside the bars themselves, so the window comes back on the one you were using rather than resetting to the first.

Custom bars merge with the game’s; they don’t replace them. Defining quick-mine adds a switcher entry. Defining quick replaces the game’s main bar with yours, because that id is already taken.

Entries missing required pieces are dropped one at a time. A link with no label or no command, or a menulink missing exist or noun, is skipped while the rest of the bar builds normally. A bar that came out short is usually a half-written entry, not a broken file.

Skip selection_fg and selection_bg if you see them in an old layout. Selection colors come from your theme, not from the window. Those keys do nothing.

See also

Config reference (TOML)

The window is written by the catalog and .addwindow. Custom bars are the exception in this manual: they have no in-app editor, so config.toml is the only route.

The window

[[windows]]
name = "quickbar"
widget_type = "quickbar"
row = 0
col = 0
rows = 3
cols = 120
show_border = true
show_title = false
title = "Quickbar"

widget_type = "quickbar". The type string is exactly quickbar; an unrecognized type does not error, it quietly creates a text window instead.

Widget fields: none. The content is entirely the bars the game, your scripts, and [quickbars] supply.

Catalog rowCategoryTitleSizeGame
quickbarHotbarsQuickbar3 rows x 120 cols, pinned to 3 rows, title bar offBoth

Ungated — offered to GemStone IV and DragonRealms characters alike.

Custom bars — [quickbars] in config.toml

[quickbars]
default = "quick-mine"       # which bar is active on start; must be a defined id

[[quickbars.custom]]
id = "quick-mine"            # required: exactly "quick", or starts with "quick-"
title = "Mine"               # optional; falls back to the id in the switcher
entries = [
  { type = "link", label = "loot", command = "loot", echo = "loot" },
  { type = "menulink", label = "roleplay...", exist = "qlinkrp", noun = "" },
  { type = "sep" },
]
FieldTypeDefaultWhat it does
defaultstringnoneBar active on start. Ignored with a log warning when the id is unknown or malformed
customarray of tables[]Bar definitions
custom.idstringMust be quick or start with quick-. Any other value skips the whole bar with a log warning
custom.titlestringthe idSwitcher label. Blank or whitespace falls back to the id
custom.entriesarray[]Buttons, in display order

Entry types (type is required):

typeRequiredOptionalBehavior
linklabel, commandechoSends command to the game on click
menulinklabel, exist, nounRequests the server’s verb menu for that object
sep / separatorVisual divider between buttons

An entry missing a required field is skipped; the rest of the bar still builds.

Where bars come from at runtime

SourceNotes
The gameSent once, at login, as quick… dialogs
ScriptsThe same tags; a clear flag replaces the bar’s contents, otherwise entries append
[quickbars.custom]Applied at startup
Seeded fallbackThree standard bars seeded when the cache is cold on a mid-session attach
SwitchingThe game can change your active bar on its own

Only ids that are quick or begin with quick- are treated as quickbars. Anything else on the same wire tag is a dialog panel, not a quickbar button.

Persistence

Bars, their order, and the active id are cached per character in:

~/.vellum-fe/profiles/<Character>/session_cache.toml

That is what lets a mid-session attach to a running Lich still show buttons.

Where the settings live

SettingDesktop GUITerminal (TUI)
Widget settingsnone — no widget section in the right-click menunone — .editwindow shows base fields only
Custom bar definitionsconfig.toml only — no in-app editorconfig.toml only
Keyboard focus for arrow keysn/a — the GUI drives the bar by mouseremove "quickbar" from [ui.focus] exclude
Selection colorsfrom the active themefrom the active theme

Standard window keys — row, col, rows, cols, show_border, border_style, border_sides, title, show_title, locked — apply as they do to any window. Selection colors are theme-owned; a selection_fg or selection_bg key in a layout file is read by nothing.

Utility & Layout Widgets

The five windows that hold your layout together rather than reporting on the game — and the four of them you can only add by typing the long form.

What it’s for

Most windows in this manual show you something about your character. These five do a job instead: they pad a layout, host your typing, report on the client itself, or hand a panel to something outside the game.

You reach for them rarely and deliberately. The catch is that four of the five are close to invisible in the interface, which is what the rest of this page is mostly about.

⚠️ Only spacer is in the catalog. The other four exist, but nothing offers them. command_input, performance, webui, and dialogpanel are all valid window types, and none of them appears in the GUI Windows catalog or in the picker you get from a bare .addwindow. The full six-argument .addwindow form is the only way to type one into existence — and for two of them, typing it is the wrong move anyway (see below).

A window already in your layout is a different matter: command_input and spacer both show up in the hide and edit pickers once they exist. It is only adding that is closed.

⚠️ A misspelled widget type does not error — it silently gives you a text window. There is no “unknown type” message. Three of these five accept two spellings, and the near-misses are exactly the ones you’d guess wrong:

TypeAlso acceptedGuessing wrong gives you
command_inputcommandinputa text window
performancea text window
webuilichuia text window
dialogpaneldialog_panela text window
spacera text window

If you typed .addwindow and got an empty bordered box, check your spelling first.

Set it up

This tabbed block covers the shared gesture — adding a window that isn’t in the catalog. Each widget’s own section below adds what’s specific to it.

  1. For spacer, click Windows in the top toolbar, expand Other, and tick the spacer row. Repeat for as many as you need — each gets its own generated name.
  2. For the other four, type the full form in the command input, for example .addwindow gaps spacer 10 4 6 2 or .addwindow perf performance 0 0 40 10. The catalog will not offer them, and there is no right-click Add anywhere in the GUI.
  3. Position it by dragging, and set borders and background through right-click ▸ Appearance like any other window.
📷 screenshot pending
A performance window added by typed command, showing frame, render, and network timings beside a text window.

Expected result: the window appears at the coordinates you gave. A spacer shows as blank space; a performance window immediately starts printing timings.

Common setups

spacer — layout padding

The only widget here that is in the catalog, and the only one most players will use.

A spacer draws nothing. It occupies grid space so the windows around it land where you want them, which is how you get a gap between two bars or push a strip to one side without giving those windows sizes that fight you on the next resize.

It never draws a border and never draws a title — the preset turns both off, deliberately. It respects your theme background rather than punching a hole in the layout.

Its minimum size is 1 by 1, the smallest of any widget. That matters: a thin alignment spacer survives a resize. Everything else in the layout floors at 5 cols by 3 rows, so a narrow strip of any other type gets clamped up and shoves the layout along after it.

Spacers are dynamically named — spacer_1, spacer_2 — and they show up in the hide and edit pickers under Other. .editwindow on a spacer offers no widget fields, only the standard geometry and border ones.

You’ll see: blank space that holds its place, letting the windows on either side keep the sizes you gave them.

command_input — your typing line as a real window

Every frontend always has somewhere to type. So why would you add one?

Because the command input is a normal dockable window, and the always-there version is a fallback. In the desktop GUI the input is a placeable window like any other — you can move it, resize it, style it, and park it in a shell zone. The GUI watches whether that window actually rendered this frame; if it didn’t — because the definition is missing, the window is hidden, or its zone is collapsed — it paints a fixed panel at the bottom of the screen instead, so the input can never be lost. That fallback panel is not movable and has no grip.

So: the input you see by default is either your window or the safety net, and adding or styling a command_input window is how you take charge of which.

The default layout already ships a command_input window, so in practice you edit that one rather than adding a second.

Settings. This is the widest widget form on this page. TUI .editwindow exposes Prompt icon, Prompt icon color, Text color, Cursor color, Cursor background, and Completion color — the color of the greyed history suggestion. The GUI has no widget section for it, so those six fields are terminal-authored; the GUI reads them from the layout.

You’ll see: a prompt character in your chosen color ahead of the cursor, and a dimmed history suggestion completing as you type.

performance — the client’s own vitals

A window of frame timings, render and UI times, text-wrap cost, network and parse throughput, event counts, CPU, and memory. It reports on VellumFE, not on your character.

Reach for it when the client feels slow and you want to know which part is slow — or when you’re reporting a problem and want numbers to attach.

Metrics collection runs at all times, so the window is accurate the moment you open it; there’s nothing to enable first. Every section has its own toggle, and all ten default to on.

TUI .editwindow gives you an Edit metrics entry for those toggles. The GUI has no widget section, so which metrics show is terminal-authored.

The Performance Monitor reference documents what every number means — read it there rather than guessing from the labels.

You’ll see: live timings that move as you play, with the slow section standing out from the rest.

webui — a Lich script’s own panel

A WebUI window hosts a page published by a Lich script, so a script can give you real controls inside your layout instead of printing text at you.

⚠️ Do not add this one with .addwindow. Type .webui <script/page> instead — for example .webui creaturebar/main. That builds the window and binds it to the page, sizing it from the page’s own hint. .webui with no argument opens a picker of registered pages, and .webui off closes the panel.

A window you add by hand with .addwindow … webui … has an empty page binding and shows nothing, with no way to fill it in: neither frontend’s editor exposes the page field.

Windows made this way are named webui:<page>, which is why you may see that prefix in your window lists.

You’ll see: a script’s own interface — buttons, lists, whatever it published — sitting in your layout as a native window.

dialogpanel — a resident game dialog, kept on screen

The game sends some interfaces as dialogs rather than text: the combat panel and similar resident controls. A dialog panel window renders one of them by id, from the accumulated store of dialog data the client keeps.

⚠️ These arrive on their own — you don’t add them. When the game opens a resident dialog, the client records it and builds the panel, placing it from the dialog’s own declared position and size hints. It then appears in your Windows list, where you tick it to show and untick it to hide.

A dialogpanel you add by hand with .addwindow has an empty dialog id and renders nothing, and no editor in either frontend exposes that id — the game defines these controls.

Game-created panels are session windows. They don’t persist to your layout the way a text window does, and a panel that was placed by the game reopens where the game’s hints put it, unless you’ve moved it and it saved a position for that id.

Effect dialogs are a separate story with their own widget — see Active Effects, which is fed by four named dialogs and is what you actually want for buffs, debuffs, cooldowns, and active spells.

You’ll see: the game’s own panel as a window you can place, rather than as text scrolling past in your story pane.

Tips & gotchas

⚠️ “Not in the catalog” is not the same as “not supported”. All five render fully and reload from your layout file correctly. What four of them lack is a way to add them by pointing — and for webui and dialogpanel, that’s because a different route creates them properly bound.

⚠️ Hiding the command input means two different things. In the desktop GUI the window goes away and the fixed fallback bar takes over. In the terminal the flag is saved but the input stays on screen, with a message explaining why: “Command input hidden in the layout (GUI shows its fallback bar); the TUI keeps it visible.” One layout, two behaviors, on purpose.

⚠️ Bare .hidecontainers closes dialog panels too. It reports the count as “container window(s)”, which is misleading — it closes every session window, panels included. Name a container explicitly if you only meant to close a bag.

Use a spacer rather than oversizing a neighbor. A spacer’s 1 by 1 floor is what lets it stay thin. Padding a layout by making the window next to it wider works until the next resize redistributes the space and moves everything after it.

The editors are lopsided here, and the terminal usually wins. command_input and performance both have real terminal forms and no GUI widget section at all. If a setting on this page seems to have no home in the desktop GUI, that is why — author it once in the terminal and the GUI reads it from the layout.

See also

  • Performance Monitor — what every metric in the performance window means
  • Active Effects — the dialog-fed widget you probably want instead of a raw dialog panel
  • Containers — the other session-only windows .hidecontainers reaches
  • Creating Layouts — placing, saving, and restoring all of these
  • Widgets — the full roster, and the three routes for adding any of them
Config reference (TOML)

All five accept the standard window keys — row, col, rows, cols, show_border, border_style, border_sides, title, show_title, locked — as any window does. Only spacer can be created from the catalog.

Type strings and aliases

Widgetwidget_typeAliasIn the catalog?Minimum size
Spacerspacer✅ yes1 col x 1 row
Command inputcommand_inputcommandinput❌ no5 x 3 (default floor)
Performanceperformance❌ no20 cols x 4 rows
WebUI panelwebuilichui❌ no5 x 3 (default floor)
Dialog paneldialogpaneldialog_panel❌ no14 cols x 4 rows

An unrecognized widget_type falls back to text with no error.

spacer

[[windows]]
name = "spacer_1"
widget_type = "spacer"
row = 10
col = 4
rows = 2
cols = 6
show_border = false
show_title = false

No widget fields. Borders and titles are off by design.

command_input

[[windows]]
name = "command_input"
widget_type = "command_input"
row = 44
col = 0
rows = 1
cols = 120
locked = true
prompt_icon = ">"
FieldTypeDefaultWhat it does
prompt_iconstringnoneCharacter or short string before the cursor
prompt_icon_colorstringnoneIts color
input_text_colorstringnoneTyped text color. Reads a legacy text_color key from old files
cursor_colorstringnoneCursor foreground
cursor_background_colorstringnoneCursor background
completion_colorstringnoneThe greyed inline history suggestion

Preset pins the window to exactly 1 row and ships it locked = true.

performance

[[windows]]
name = "performance"
widget_type = "performance"
rows = 10
cols = 40
show_fps = true
show_memory = true
FieldTypeDefaultWhat it does
enabledbooltrueDraw the window’s contents
show_fpsbooltrueFrame rate
show_render_timesbooltrueRender timings
show_ui_timesbooltrueUI build timings
show_wrap_timesbooltrueText-wrap cost
show_netbooltrueNetwork throughput
show_parsebooltrueParser throughput
show_eventsbooltrueEvent counts
show_cpubooltrueCPU usage
show_memorybooltrueMemory usage

webui

[[windows]]
name = "webui:creaturebar/main"
widget_type = "webui"
page = "creaturebar/main"
FieldTypeDefaultWhat it does
pagestring""The script/page id this window hosts. Written by .webui; no editor exposes it

dialogpanel

[[windows]]
name = "combat"
widget_type = "dialogpanel"
dialog_id = "combat"
FieldTypeDefaultWhat it does
dialog_idstring""Which dialog’s controls to render. Set by the client when the game opens the dialog; no editor exposes it

Where the settings live

WidgetGUI widget sectionTUI .editwindow
spacernoneno widget fields
command_inputnonesix color and prompt fields
performancenoneEdit metrics toggles
webuinonenone — page set by .webui
dialogpanelnonenone — the game defines the controls

Customization

Create your perfect interface with custom layouts, highlights, keybinds, sounds, and themes.

Topics

Quick Start

Custom Layout

  1. .menu → Windows → Add Window (or .addwindow)
  2. Ctrl+drag to move windows, drag borders to resize
  3. .savelayout myname to save

Add a Highlight

.addhighlight in-app, or edit ~/.vellum-fe/global/highlights.toml:

[death]
pattern = "appears dead"
fg = "#00ff00"
bold = true
sound = "kill.wav"

Add a Keybind

.addkeybind in-app, or edit ~/.vellum-fe/global/keybinds.toml:

[user]
f2 = { macro_text = "stance offensive\r" }
f3 = { macro_text = "stance defensive\r" }

Switch Theme

.themes

Apply file edits without restarting: .reload

Creating Layouts

Design custom window arrangements for different playstyles.

Editing a Window

  • GUI: right-click the window — every setting lives in its menu and applies live (see The Window Menu). The Streams field has a + picker of stream ids seen this session.
  • TUI: .menu → Windows → Edit Window → [window name], then save (applied immediately). On a text window’s Streams field, Ctrl+P opens the seen-streams picker.

Or via command: .editwindow <name> — the easy way to wire a window to a Lich script’s custom stream (see Custom Streams).

Adding Windows

Via Menu

.menu → Windows → Add Window → [Category] → [Widget]

Via Command

.addwindow                                  # opens a picker
.addwindow loot text 100 0 30 10            # name type x y width [height]

Via Config

Edit your layout file directly:

[[windows]]
name = "mywindow"
widget_type = "text"
streams = ["main"]
row = 0
col = 0
rows = 20
cols = 60

Positioning

Grid Coordinates

row = 0       # Top edge (0 = top)
col = 0       # Left edge (0 = left)
rows = 20     # Height
cols = 60     # Width

Overlapping

Windows can overlap. Later windows in the file render on top.

Saving and Switching

.savelayout hunting     # save to ~/.vellum-fe/layouts/hunting.toml
.loadlayout hunting
.layouts                # list saved layouts
.resize                 # refit layout to the current terminal size

Window positions also auto-save per character: any layout change (moving or resizing a window, editing its streams, adding or hiding windows) is written to the per-character auto-save a few seconds later, and .savelayout / .loadlayout update it immediately — so your layout comes back on next launch even if the session ends without a clean .quit (closed terminal, crash). On startup the client loads the per-character auto-save first, falling back to your saved default layout (.savelayout with no name) if no auto-save exists. Use .resize to refit the current layout to the terminal, or .loadlayout <name> to switch layouts.

The same three commands work in the Desktop GUI on its own layout format: named checkpoints of the GUI arrangement, saved to the same shared ~/.vellum-fe/layouts/ folder (as <name>.json next to the TUI’s <name>.toml), so any character can load a layout any character saved — exactly like the TUI. The two formats don’t cross-load.

Example Layouts

Hunting Layout

terminal_width = 160
terminal_height = 50

# Main game text
[[windows]]
name = "main"
widget_type = "text"
streams = ["main"]
row = 0
col = 0
rows = 40
cols = 100

# Targets on right
[[windows]]
name = "targets"
widget_type = "targets"
row = 0
col = 100
rows = 15
cols = 30

# Items below targets
[[windows]]
name = "items"
widget_type = "items"
row = 15
col = 100
rows = 10
cols = 30

# Vitals
[[windows]]
name = "health"
widget_type = "progress"
id = "health"
row = 25
col = 100
rows = 1
cols = 30

# Roundtime
[[windows]]
name = "rt"
widget_type = "countdown"
id = "roundtime"
row = 26
col = 100
rows = 1
cols = 30

# Command input
[[windows]]
name = "command_input"
widget_type = "command_input"
row = 47
col = 0
rows = 3
cols = 160

Minimal Layout

terminal_width = 80
terminal_height = 24

[[windows]]
name = "main"
widget_type = "text"
streams = ["main"]
row = 0
col = 0
rows = 21
cols = 80

[[windows]]
name = "command_input"
widget_type = "command_input"
row = 21
col = 0
rows = 3
cols = 80

Per-Character Layouts

When you launch with --character NAME, the current layout auto-saves to:

~/.vellum-fe/profiles/CharName/layout.toml

Validate a layout file from the command line:

vellum-fe validate-layout hunting.toml

Highlight Patterns

Recipes for common highlighting tasks. Full field reference: highlights.toml. The in-app editor (.addhighlight) walks you through the same fields.

Color Important Text

[creature_dead]
pattern = "appears dead"
fg = "#00ff00"
bold = true
category = "Combat"

[stunned]
pattern = "You are stunned"
fg = "#ff4500"
bold = true
sound = "alert.wav"
rumble = "long"           # buzz the controller too (optional)
category = "Warnings"

Highlight Names (Friends and Enemies)

Use fast_parse for lists of literal words — it’s much faster than regex:

[friends]
pattern = "Mandrill|Monolis|Chiora"
fg = "#ff00ff"
bold = true
fast_parse = true
category = "Players"

[enemies]
pattern = "Sihtric|Ehria"
fg = "#ffffff"
bg = "#8b0000"
bold = true
fast_parse = true
category = "Players"

Hide Spam (Squelch)

[ambient_spam]
pattern = "A cool breeze|The wind blows|A leaf falls"
fast_parse = true
squelch = true
category = "Squelch"

Route Lines to Another Window

[loot_lines]
pattern = "^You gather"
redirect_to = "loot"
redirect_mode = "redirect_copy"    # "redirect_only" (the default) to move instead of copy

Rewrite Text

Capture groups from the pattern are available as $1, $2:

[shorten_deaths]
pattern = "The death cry of (\\w+)"
replace = "† $1"
fg = "#ff0000"

Limit to One Stream or Window

[thought_names]
pattern = "^\\[(\\w+)\\]"
fg = "#9370db"
stream = "thoughts"       # only applies to the thoughts stream

For replacement patterns, window = "..." limits the replacement to one window by name (colors still apply everywhere). Both filter fields are editable in the TUI highlight form (.edithighlight).

Custom Statuses

A rule can drive a status icon of your own invention. Set these in the GUI highlight editor:

  • Set status — a status id to activate when the rule matches (e.g. POISONED).
  • Status duration — seconds until it clears itself; leave empty to keep it on until a clearing rule matches.
  • Clear status — a status id to deactivate on match.

Any indicator or dashboard entry whose id matches lights up, exactly like the game’s built-in statuses — so it gets skin/pool icon art, the per-indicator icon picker, grayscale-when-inactive, and TUI glyphs for free. Typical use: regex a spell’s wear-off message into a status so you can see the buff drop:

[silver_lace_down]
pattern = "The silvery luminescence fades"
set_status = "SILVERLACE_DOWN"
status_duration = 30.0

Then add an indicator window with id SILVERLACE_DOWN (Indicator Templates editor) and pick an icon for it.

Test Your Patterns

Don’t wait for the game — inject a line:

.testline The death cry of Grimswarm echoes!

Tips

  • Patterns are regexes: escape literal . ( [, use (?i) for case-insensitive, anchor with ^ where you can.
  • Use category — the .highlights browser groups by it.
  • Save variants per activity: .savehighlights hunting, .loadhighlights hunting.

Keybind Actions

Recipes for common keybind tasks. Full format and action reference: keybinds.toml. The in-app editor (.keybinds / .addkeybind) edits the same file.

Your custom binds go in the [user] section, as "key" = value.

Send a Command with One Key

Use macro_text; end with \r to press Enter:

[user]
f5 = { macro_text = "stance defensive\r" }
f6 = { macro_text = "look in my backpack\r" }
"ctrl+g" = { macro_text = "group\r" }

Omit the \r to type text into the input line without sending — useful for prefixes you finish by hand:

"ctrl+w" = { macro_text = "whisper Rolfard " }

Numpad Movement

Ships by default; adjust to taste:

num_8 = { macro_text = "n\r" }
num_2 = { macro_text = "s\r" }
num_5 = { macro_text = "out\r" }
num_decimal = { macro_text = "up\r" }
num_plus = { macro_text = "look\r" }

Numpad keys take modifiers, so you can layer more binds onto the same keys:

"ctrl+num_8" = { macro_text = "north\r" }
"alt+num_plus" = { macro_text = "look in\r" }
"ctrl+alt+num_divide" = "health"

Rebind a Client Action

[user]
"ctrl+r" = "send_last_command"
tab = "switch_current_window"
page_up = "scroll_current_window_up_page"

Change the Quit / Search Keys

Application-level keys live in [app]:

[app]
quit = "ctrl+q"          # default is ctrl+c
start_search = "ctrl+f"

Swap Keybind Sets

.savekeybinds hunting
.loadkeybinds hunting
.keybindprofiles

When a Key Doesn’t Work

Terminals differ in what they send. Run with RUST_LOG=debug, press the key, and look for KEY EVENT lines in the log — then bind exactly that name. Classic case: terminals that send delete for backspace.

Controller & Gamepad

Not written yet. This page is queued for the manual rebuild; it will cover gamepad binds, wheels, and interact mode.

Sound Alerts

Audio notifications for game events.

Setup

  1. Put sound files in ~/.vellum-fe/global/sounds/
  2. Reference them in highlights

Using Sounds

In highlights.toml:

[death_alert]
pattern = "appears dead"
fg = "#00ff00"
sound = "kill.wav"
sound_volume = 0.8

Sound Properties

PropertyTypeDefaultDescription
soundstring-Filename in sounds directory
sound_volumefloat1.0Volume (0.0 to 1.0)

Global Settings

In config.toml:

[sound]
enabled = true
volume = 0.7              # Master volume
cooldown_ms = 500         # Min time between sounds
startup_music = true      # Play music on launch

startup_music plays the classic login theme once the game connection comes up (not while you’re still on the login screen — no connection, no music). On the mobile apps a Stop / Don’t play again bar appears while it plays, and it can be turned back on under ⚙ Settings → Login music.

Disabling Sounds

All Sounds

[sound]
enabled = false

Highlight Sounds Only

[highlights]
sounds_enabled = false

Example Sounds Setup

Directory structure:

~/.vellum-fe/global/sounds/
├── kill.wav
├── alert.wav
├── whisper.wav
└── danger.wav

Highlights:

[kill]
pattern = "appears dead"
sound = "kill.wav"

[stunned]
pattern = "You are stunned"
sound = "alert.wav"
sound_volume = 1.0

[whisper]
pattern = "whispers to you"
sound = "whisper.wav"
sound_volume = 0.5

[bleeding]
pattern = "Blood runs down"
sound = "danger.wav"

Supported Formats

  • WAV (recommended)
  • MP3
  • OGG
  • FLAC

Troubleshooting

No sound playing?

  1. Check [sound] enabled = true in config.toml
  2. Check [highlights] sounds_enabled = true
  3. Verify file exists in sounds directory
  4. Check file format is supported
  5. Try increasing volume

Emoji

VellumFE renders :shortcode:-style emoji in incoming game text, and lets you add your own custom (Discord-server-style) emoji.

Standard emoji

Any standard gemoji shortcode in game text is rendered as its emoji:

Someone thinks, "heading to town :grin:"
  • In the GUI, emoji are drawn in full color (Twemoji artwork). Toggle color vs. monochrome with the ui.color_emoji setting.
  • On the phone/web client, emoji use the system emoji font.
  • In the TUI, emoji render as whatever Unicode glyph your terminal font provides.

Shortcode expansion is controlled by the ui.emoji_shortcodes setting (default on).

Custom emoji

Drop image files into ~/.vellum-fe/emoji/ and reference them as :name:, just like Discord server emoji.

  1. Create the directory ~/.vellum-fe/emoji/ if it does not exist.
  2. Add an image named after the shortcode you want, e.g. ~/.vellum-fe/emoji/vibecat.png for :vibecat:.
  3. Restart, or run .reload, to pick up new files.

Supported formats:

FormatExtensionAnimated
PNG.pngif it is an animated PNG (APNG)
APNG.apngyes
GIF.gifyes
WebP.webpyes, if the file is animated

Animated GIF, WebP, and APNG all animate in the GUI and on the phone. Discord serves its animated custom emoji as animated WebP — save one as ~/.vellum-fe/emoji/<name>.webp and it animates. (To grab one: right-click the emoji in Discord → Copy Link, open the …​.webp URL in a browser, and save it.)

Naming rules: the filename (without extension) is the shortcode, and may contain letters, digits, _, +, and - only. Names are case-insensitive (VibeCat.png matches :vibecat: and :VibeCat:). If a custom emoji shares a name with a standard one, your custom emoji wins.

Sizing custom emoji (GUI)

Two settings tune how custom emoji look in the GUI (Client settings → UI):

SettingDefaultEffect
Custom Emoji Size (ui.custom_emoji_size)1.0Image height as a multiple of the text line height. Above 1.0 the emoji stand out above the line.
Custom Emoji Spacing (ui.custom_emoji_spacing)0.2Breathing room around each emoji, as a fraction of the line height added to its width.

How each frontend renders custom emoji

  • GUI — draws the image inline, animating APNG/GIF frames.
  • Phone/web — draws the image inline via a private, token-gated endpoint; GIF/APNG animate natively in the browser.
  • TUI — cannot render images, so it shows the literal :name: text as a readable fallback.

Emoji are never sent to the game

GemStone IV is a roleplaying game, and sending emoji (or the shortcodes that render as emoji) as speech, thoughts, whispers, or actions can get you warned or banned. Emoji are a display convenience on your side only.

VellumFE strips emoji from every command before it reaches the game — whether you type it, a hotbar sends it, or a Lich script sends it. This includes actual emoji characters and any :shortcode: that resolves to a known emoji (standard or custom). Ordinary text, unknown shortcodes, Lich script syntax, and things like time strings (12:30:45) are left untouched. This protection is always on.

So a Lich script can safely send :vibecat: for VellumFE to display, and the game will never see it.

Inline Images

VellumFE can float real images into text, with the text wrapping around them. Images are display-only and driven by Lich scripts through XML tags the game itself never sends.

Installing art

Drop image files into:

~/.vellum-fe/global/images/inline/

The filename (without extension) is the name you reference. sunset.png becomes src='sunset'.

FormatExtensionAnimated
PNG.pngif it is an animated PNG (APNG)
APNG.apngyes
GIF.gifyes
WebP.webpyes, if the file is animated
JPEG.jpg / .jpegno

Names may contain letters, digits, _, +, and - only, and are case-insensitive. Restart or run .reload to pick up new files.

Sizing tip: the art is drawn a few text rows tall, so a 512×512 export looks identical to a 4000×4000 one and loads far faster.

The tag

<vellumImg src='sunset' rows='4' align='left'/>
AttributeDefaultMeaning
src(required)Image name. Never a path.
rows1Height in text rows. Width follows the art’s aspect ratio.
alignleftWhich side the image floats on: left or right.

rows is a request, not a guarantee, and the two window kinds size the picture differently — on purpose:

  • Text windows (story, thoughts, custom, tabs): the picture follows the height of the text that wraps it. One short line beside it gives a small picture; a paragraph grows it toward rows. A standalone image with no text after it renders at exactly rows. Press-and-hold always shows the art at full size, so a small float loses nothing.
  • The room window: the picture renders at rows, shrinking only with the window itself (its height, and a cap of about half its width).

In both, aspect ratio is always preserved, the ceiling is 8 rows, and a window too narrow to wrap beside at all puts the image on its own rows with the text below.

Putting an image in the room window

GemStone declares a sprite component on every room change and never puts anything in it — the official client shows no room-window images at all. VellumFE uses that unused slot for room art:

_respond "<compDef id='sprite'><vellumImg src='sunset' rows='4' align='left'/></compDef>"

<component id='sprite'>…</component> works too.

Art plus the room description

To float art beside real room prose, put the image at the front of the description component:

_respond <<~'XML'.strip
  <clearStream id='room'/><pushStream id='room'/><compDef id='room desc'><vellumImg src='sunset' rows='4' align='left'/>Blue and red arrows painted upon the smooth planks of the dock point to the northeast, while a solid green arrow points to the southeast.  A pair of sturdy steps leads down to a finger pier with five docking slips.</compDef><compDef id='room exits'>Obvious paths: <d>northeast</d>, <d>southeast</d>, <d>west</d></compDef><popStream id='room'/>
XML

The description wraps beside the image and rejoins the full width once it clears the image’s bottom edge.

Ruby quoting: room XML is full of both ' and ", and room names contain apostrophes (“Kraken’s Fall”). A single-quoted heredoc (<<~'XML') passes everything through literally with no escaping and no interpolation — far less painful than a quoted string.

Art clears when you move

The game re-declares sprite (empty) on every room change, so script art disappears when you walk. That is usually what you want — set it from a script that reacts to the room you are in.

Room art without a script

Instead of sending a tag every time, you can map art to rooms once and have it appear whenever you walk in.

Stand in the room and run:

.roomimages set sunset

That maps the room you are standing in — you never type a room number. Then turn the feature on:

.roomimages on
CommandWhat it does
.roomimagesState, how many rooms are mapped, and what this room shows
.roomimages on / offMaster switch
.roomimages set <image>Map the current room (moves it if already mapped elsewhere)
.roomimages clearUnmap the current room
.roomimages listEvery image and the rooms it covers
.roomimages editOpen the editor

The editor

.roomimages edit, or Editors → Room Images in the GUI. One card per image, since a single picture usually covers many rooms:

[thumb]  krakens_fall_pier          Rows [4]  Align [Left]
         7118245  Kraken's Fall, Third Pier          ✕
         7118250  Kraken's Fall, Second Pier         ✕
         [+ Add current room]

Rows and Align are per image, so every room sharing a picture is framed the same way. + Add current room is the quickest way to build a mapping: walk around and click it in each room you want covered.

Where the mappings live

~/.vellum-fe/global/room_images.toml, with an optional per-character file at ~/.vellum-fe/profiles/<character>/room_images.toml:

[[image]]
name = "krakens_fall_pier"
rooms = [7118245, 7118250, 7118251]
rows = 6
align = "left"

Rooms are keyed by the game’s own room id. A room can belong to only one image — mapping it somewhere else moves it rather than leaving a duplicate.

Different art by time of day

Add variants to show a night version of the same view:

[[image]]
name = "krakens_fall_pier"
rooms = [7118245, 7118250]

  [[image.variant]]
  name = "krakens_fall_pier_night"
  when = { type = "time_of_day", phase = "night" }

The first variant whose condition matches wins; none matching falls back to the entry’s own name.

In the editor, each image card has an + Add variant button and a row per variant — pick the phase and the art from dropdowns. Conditions more complex than time of day are shown read-only there and edited in the file.

Elanthian time runs on US Eastern, so VellumFE computes the phase from your system clock — no time command, no waiting for the game to tell it, and it is correct the moment you log in. The four phases are dawn, day, dusk, and night.

Sunrise and sunset in Elanthia follow Central more closely than Eastern, so the phase boundaries are approximate by design rather than a claim of exactness.

Variants use the same condition system as hotbar button states and injury doll art, so they are not limited to time — a variant can key off effects, injuries, vitals, or any combination. Time of day is also now available to hotbars and the doll for the same reason.

Script art still wins: if a script sends a sprite for the room you are in, that is what shows.

GemStone’s own room pictures

The game sends <resource picture='N'/> on every room change — the official client uses it to show a picture beside the room name. The wire carries only the number, so VellumFE fetches the art from play.net and caches it.

This is off by default. It sends requests to play.net, so it is an explicit opt-in: run .settings and turn on Game Room Pictures under Appearance.

Once on, walking into a room with a picture downloads it once and caches it under global/images/gs-art/. The image appears on the next room change after it arrives — fetching happens in the background so the game feed is never held up.

Your own art still wins: install images/inline/32.png and rooms with picture='32' use yours instead of the download. Most rooms send picture='0', meaning no picture at all, and ids with no art are remembered so they are not requested again.

Putting an image in the story window

Send the tag on its own:

_respond "<vellumImg src='sunset' rows='4' align='left'/>"

The image floats and the following lines wrap beside it, rejoining the full width once they clear its bottom edge — the same behaviour as the room window. This works in every text window: story, thoughts, combat, custom windows, and each tab of a tabbed window.

Press and hold to enlarge

Press and hold an image to see it at full size; release to shrink it back. Works with mouse and touch, in both the desktop GUI and the phone client.

Where images work

FrontendSupport
GUI — room windowFull: floats, wrapping, hold-to-enlarge
GUI — text windowsFull: floats, wrapping, hold-to-enlarge
Phone / webFull: floats via CSS, hold-to-enlarge
TUI[img:name] text fallback (terminals cannot draw images)

An image name with no matching file also shows the [img:name] fallback, so a missing file is visible rather than blank.

Images are never sent to the game

These tags are display instructions your client interprets; they travel from Lich to VellumFE only and never reach the game server. src is a name, not a path — it is checked against a restricted alphabet and then resolved through the client’s own image list, so a script can name art you installed but can never read another file on your disk.

See also: Emoji for :shortcode:-sized inline pictures.

Themes

Themes control the client’s UI colors — windows, menus, editors, buttons — as one switchable unit. They work in both the TUI and GUI.

Switching Themes

.themes            # browse and preview all themes
.settheme nord     # switch directly by name

The active theme is saved to your config (active_theme) and restored on launch.

Built-in Themes

35+ presets, including:

  • Classics: dark (default), light, nord, dracula, solarized-dark, solarized-light, monokai, gruvbox-dark, catppuccin, night-owl
  • Flavors: cyberpunk, synthwave, retro-terminal, ocean-depths, forest-canopy, sunset-boulevard, arctic-night, sepia-parchment, cherry-blossom, slate-professional, and more
  • Accessibility: high-contrast-dark, high-contrast-light, deuteranopia, protanopia, tritanopia, monochrome, low-blue-light, photophobia, adhd-focus, reduced-motion

Custom Themes

.edittheme         # edit the current theme in-app

Saving writes a TOML file to ~/.vellum-fe/themes/<name>.toml and makes it active. A custom theme with the same name as a built-in overrides it.

Theme files are flat tables of color fields — hex values or palette color names:

name = "my-theme"
description = "My tweaked dark theme"
window_background = "#1a1b26"
text_primary = "#c0caf5"
link_color = "Link"        # palette name from colors.toml
# ... every color field must be present — a file with missing
# fields is skipped when themes are loaded

Hand-written theme files must define all color fields; there is no per-field fallback. The easiest (and recommended) way to author one is .edittheme on a built-in you like — it writes a complete file — then save under a new name.

Themes vs. colors.toml

Two separate systems:

Themescolors.toml
ControlsWidget/UI colors (windows, menus, editors)Game text colors (speech, links, monsterbold), spell colors, terminal palette
Switch.settheme, instantEdited in place (.uicolors, .spellcolors, .colors)

Switching a theme does not change your game-text colors, and vice versa.

For image-based decoration in the GUI (background art, icon sprites), see Skins — themes own colors, skins own graphics.

Skins (GUI Graphics)

Skins layer your own images on top of the GUI: window backgrounds, nine-slice window borders, status icon sprites, a sprite compass, and a sprite injury paperdoll. Themes own colors and fonts; skins own graphics.

Skins apply to the Desktop GUI only — the terminal has no image pipeline. Without a skin, the GUI uses its built-in vector graphics and theme colors, and anything a skin doesn’t cover (or fails to load) falls back to that.

Using Skins

.skins            # list installed skins
.setskin parchment
.setskin none     # back to plain theme rendering

The active skin is remembered in your GUI layout, so layout checkpoints carry it (see below). .skin is an alias. The GUI settings editor (.settings) has a Skin section with the same picker, an “Open skins folder” button, and a “Create” button.

Making a Skin

No art skills needed — Generating Skin Art with AI is a working prompt kit for producing every image below with an image model, including the keying script that turns black backgrounds into real transparency.

The quickest start:

.makeskin myskin

This creates ~/.vellum-fe/global/skins/myskin/skin.toml with every section present but commented out — uncomment a line, point it at a PNG, done. It never overwrites an existing skin.

While a skin is active, edits to its skin.toml hot-reload within a second. Edited images don’t touch the manifest, so after swapping an image file run .reloadskin to force a full reload.

A skin is a folder under ~/.vellum-fe/global/skins/<name>/ containing a skin.toml manifest plus image files (PNG, JPEG, WebP, or BMP):

~/.vellum-fe/global/skins/parchment/
├── skin.toml
└── bg/
    ├── paper.png
    └── vellum.png

Art that several skins share belongs in the image pool at ~/.vellum-fe/global/images/ (subfolders: icons/, frames/, dolls/, compass/, backgrounds/). Relative manifest paths look in the skin folder first, then the pool, so image = "backgrounds/paper.png" works from any skin without copying the file. The applied skin is remembered in the GUI layout — .savelayout checkpoints carry their skin, and .loadlayout brings it back (a checkpoint saved without a skin keeps the current one).

[meta]
name = "Parchment"
description = "Warm paper backgrounds for text windows"

# Applies to every window without its own [window.<name>] entry.
[window.default.background]
image = "bg/paper.png"   # relative to the skin folder (absolute paths allowed)
fit = "cover"            # stretch | cover | contain | tile | center
opacity = 0.85           # 0.0-1.0
tint = "#c0a878"         # optional multiply tint
scrim = 0.3              # 0.0-1.0 theme-colored overlay for text readability

# Windows are matched by their layout window name ("main", "thoughts", ...).
[window.main.background]
image = "bg/vellum.png"
scrim = 0.5

Tip: scrim paints a theme-colored wash over the image so text stays readable — start around 0.3 and adjust.

Window Borders (Nine-Slice)

[window.main.border]
image = "borders/frame.png"
slice = [12, 12, 12, 12]   # insets in source pixels: top, right, bottom, left
scale = 1.0                # source pixels -> screen points

Named Frames (Per-Window Picker)

[window.<name>.border] bakes the frame choice into the skin. To let players mix frames themselves, name them under [frames.*]:

[frames.ornate]
image = "borders/ornate.png"
slice = [16, 16, 16, 16]
scale = 0.75

[frames.plain]
image = "borders/plain.png"
slice = [6, 6, 6, 6]

Every named frame shows up in each window’s right-click menu under Appearance → Frame → Skin frame, alongside Skin default (follow the skin’s own [window.*] mapping) and None (no frame on this window). The choice is saved per window in the GUI layout, so it survives restarts and travels with .savelayout checkpoints. If a layout names a frame the active skin doesn’t define, the window falls back to the skin’s default mapping.

The name none is reserved — a [frames.none] entry is ignored.

No Skin Required: the Image Pool

Everything above describes skins, but none of it requires one. Art installed into the shared pool (~/.vellum-fe/global/images/<category>/, usually via .jinx install) is selectable straight from the GUI:

  • Injury doll — right-click the injuries window → Appearance → Doll image lists every doll in the pool; Calibrate doll… places the wound anchors, saved into the image’s sidecar toml so the calibration travels with the art (Jinx dolls can ship pre-calibrated). A Grayscale doll art checkbox desaturates the art (dots keep their colors); the grayscale copy is built only while it’s checked.
  • Frames — the per-window Skin frame picker lists pool frames (those with a slice/scale sidecar) alongside the skin’s [frames.*].
  • Backgrounds — every window’s Appearance menu has a Background picker over backgrounds/ pool images.
  • Compass — the compass window’s Appearance menu picks a pool set (compass/<set>_<role>.png, roles rose, nnw, up, down, out).
  • Status icons — the Indicator Templates editor picks a pool set (statusicons/<set>_<glyph>.png, glyph = indicator id) plus per-indicator overrides: any pool image or a hotbar sheet cell. A Grayscale when inactive checkbox shows the desaturated icon for inactive statuses instead of fading it.

All of these choices live in your GUI layout (checkpoints and .uiexport carry them). When an arrangement is worth sharing, .saveskin <name> compiles it into global/skins/<name>/skin.toml referencing the pool art — a publishable skin, without hand-writing any TOML.

Status Icons

Replace the built-in vector pictograms in the dashboard and indicator widgets, keyed by indicator id (case-insensitive):

[icons]
kneeling = "icons/kneeling.png"
stunned = "icons/stunned.png"
hidden = "icons/hidden.png"

Sprite Compass

A rose image plus one overlay per direction, drawn only while that exit exists. Author every overlay at the same canvas size as the rose, so positioning lives in the art:

[compass]
rose = "compass/rose.png"
n = "compass/n.png"
ne = "compass/ne.png"
# ... e, se, s, sw, w, nw — plus optional up / down / out overlays

Sprite Injury Paperdoll

A base body image; wounds and scars render as generated dots on top of it — a solid circle for wounds, a ring for scars, with the severity rank (1–3) inside. Parts use the protocol names (head, neck, chest, abdomen, back, leftArm, rightArm, leftHand, rightHand, leftLeg, rightLeg, leftEye, rightEye, nsys).

[injury_doll]
base = "doll/body.png"

Calibrating dot positions

Where each part’s dot lands is set by clicking, not by hand-editing: Settings > Appearance > Skin > Calibrate injury doll. The calibrator shows your doll art with every part’s dot live; click to place the highlighted part, adjust dot size, opacity, and the wound/scar colors with the controls below, then Save to skin. It writes the [injury_doll.anchors] and [injury_doll.dots] tables into the skin’s skin.toml (everything else in the file, comments included, is left alone), so calibration travels with the skin when you share the folder.

Coordinates are stored as fractions of the base image, so any image size works and a uniform resize never needs recalibrating. Parts you don’t calibrate use sensible built-in defaults. back and nsys (and usually the eyes) have no natural spot on a front-view silhouette — the convention is eyes at the top corners, back bottom-left, nerves bottom-right, but any click position works; the base art can mark those spots (letters, icons) or leave them as empty margin.

# Written by the calibrator — shown here for reference.
[injury_doll.anchors]
head = [0.5, 0.09]
chest = [0.5, 0.3]

[injury_doll.dots]
wound_color = "#e02020"
scar_color = "#b8b8b8"
opacity = 0.9
diameter = 0.07     # fraction of the drawn doll height

Hand-drawn overlays (optional)

A part can instead ship full-canvas overlays per state, authored on the same canvas as the base so they stack in place — useful for effects a dot can’t express, like nervous-system damage drawn across the whole body. The state keys are healthy (uninjured), injury1injury3 (wounds), and scar1scar3 (scars). The renderer draws the base, then exactly one overlay per part — the one matching that part’s current state.

A part with any overlay art is treated as fully hand-drawn: at a state with no art, nothing is drawn over the base — never a generated dot. Parts with no overlay art keep their dots, so one doll can mix hand-drawn limbs with dot-only back / nsys / eyes.

[injury_doll.nsys]
injury1 = "doll/nerves_i1.png"
injury2 = "doll/nerves_i2.png"
injury3 = "doll/nerves_i3.png"

Because a missing state deliberately reveals the base, two authoring schemes both work:

  • Worst-case base: the base is the fully-wounded body, with transparent holes where limbs can be severed. Each part’s overlays paint it back toward health (healthy, injury1, injury2, scars); the severe state ships no overlay so the base shows through. Omitting a state reveals the hole — a severed limb.
  • Empty base: the base is empty (or just a backdrop) and every state, including the severe one, is its own transparent overlay.
[injury_doll.leftArm]
healthy = "doll/arm_ok.png"
injury1 = "doll/arm_i1.png"
injury2 = "doll/arm_i2.png"
# injury3 omitted -> the base shows at rank 3 (e.g. a severed arm)
scar1 = "doll/arm_s1.png"
scar2 = "doll/arm_s2.png"
scar3 = "doll/arm_s3.png"

Suppressing a part conditionally

A part can be skipped entirely — no overlay, no dot, at any severity — while a condition holds, using the same condition vocabulary as variants. This encodes anatomical dependencies in the skin: a severed arm’s base hole would otherwise leave a healthy hand floating next to the stump.

[injury_doll.leftHand]
hidden_when = { type = "injury", area = "leftArm", cmp = ">=", level = 3 }
healthy = "doll/leftHand_ok.png"
injury1 = "doll/leftHand_i1.png"

While leftArm is at rank 3, leftHand draws nothing; its wound (if any) still lists in the hover tooltip. Each doll set — the default and every variant — carries its own hidden_when rules.

Doll variants (optional)

Named alternate dolls selected by game state — for example a body on the ground when the character is prone or has lost both legs. Each variant is a complete replacement doll: its own base, anchors, dot styling, and overlays. That’s deliberate — a prone body repositions every limb, so inheriting standing anchors would misplace every wound.

Variants are checked in the order they appear; the first whose when condition matches wins, and none matching falls back to the default [injury_doll] set. Conditions use the same vocabulary as hotbar button states: indicator (prone, kneeling, sitting, dead, …), injury (area / cmp / level), and all / any nesting.

[injury_doll]
base = "doll/standing.png"        # default when no variant matches

[[injury_doll.variants]]
name = "downed"
[injury_doll.variants.when]
type = "any"
conditions = [
  { type = "indicator", id = "prone", active = true },
  { type = "all", conditions = [
      { type = "injury", area = "leftLeg",  cmp = ">=", level = 3 },
      { type = "injury", area = "rightLeg", cmp = ">=", level = 3 } ] },
]
[injury_doll.variants.skin]
base = "doll/downed.png"
[injury_doll.variants.skin.anchors]
head = [0.2, 0.7]
[injury_doll.variants.skin.leftArm]
healthy = "doll/downed_arm_ok.png"

Variants apply to your own doll only — another player’s injuries popup always shows the default set, since the conditions read your character’s state. Doll images picked from the pool (right-click → Appearance → Doll image) carry no overlays or variants; these features live in a skin’s [injury_doll] section.

Each variant calibrates separately: the calibrator’s Doll set picker switches between the default doll and each variant (a prone body puts every anchor somewhere new), and Save writes into that variant’s own tables inside [[injury_doll.variants]]. The phone client follows along automatically — the host resolves which variant and which suppressed parts are active and pushes them to the phone, which just switches art.

Notes

  • Absolute image paths are allowed on purpose, so a skin can point at art from another install (e.g. your local Wrayth graphics) without copying.
  • Every piece is optional — a skin can be nothing but one background.

Sharing UI Packs

Not written yet. This page is queued for the manual rebuild; it will cover exporting and importing .vellumpack files.

Generating Skin Art with AI

Every image slot a skin can fill — window backgrounds, nine-slice frames, status icons, hotbar icon sheets, the compass, the injury doll — can be produced with an image model like Gemini in an afternoon, without drawing anything. This chapter is a working prompt kit: the technique, per-asset prompts, and the post-processing that turns raw generations into game-ready PNGs.

The prompts below produce a dark-fantasy hand-painted look; swap the style language for your own. The technique — style anchor, black keying, batching, image conditioning — applies to any style.

The three rules

  1. Anchor style with an image, not adjectives. Generate one image you love, then attach it to every later request (“in the exact style of the attached reference”). Consistency comes from image conditioning; repeating adjectives drifts.
  2. Never ask for a transparent background. Image models cannot emit an alpha channel — they ignore you or paint a fake checkerboard. Ask for a solid pure black background and key it out afterward (see Post-processing).
  3. Reject gloss immediately. Shine creeps back with each generation unless the NO-gloss lines stay in the prompt (for matte styles).

Master style block

Prepend something like this to every prompt, with your style anchor attached:

Art direction: dark-fantasy medieval game asset in the exact style of the attached reference image — hand-painted illustration with clean dark ink outlines, muted desaturated earth tones (olive greens, worn browns, iron greys, dull brass), weathered matte surfaces. Absolutely NO gloss, NO shiny highlights, NO glow, NO bloom, NO lens flare, NO neon or saturated colors. Lighting is flat and diffuse. Solid PURE BLACK background — no scenery, no ground shadow, no vignette, no gradient. No text, no watermark, no border.

Models approximate canvas sizes; aspect ratio is what they honor. The skin loader scales everything and doll anchors are fractional, so exact pixel dimensions never matter — internal consistency is everything.

Compass (12 images, one shared canvas)

The widget stacks per-direction overlays on the rose, so registration matters more than beauty. Generate two “master” states; cut the 11 overlays from the all-lit master in an editor (models cannot guarantee pixel registration across separate generations — don’t fight it).

Prompt A — the rose (base, everything unlit):

A square compass rose for a fantasy game UI, viewed flat from directly above. An aged dark-iron ring with a worn brass center hub. Eight directional pointers (N, NE, E, SE, S, SW, W, NW) as tarnished arrowheads radiating from the hub, plus a small UP chevron at the top inside the ring and a small DOWN chevron at the bottom inside the ring. ALL pointers, both chevrons, and the hub are in a dormant, unlit state: dark gunmetal, barely distinguishable from the ring. Square canvas, composition perfectly centered and symmetrical.

Prompt B — all-lit master (same composition):

The IDENTICAL compass rose, same canvas, same composition, same camera — but every pointer, both chevrons, and the center hub are now in an active state: dull ember-copper with a faint warm edge, still matte, like heated iron cooling — NOT glowing, NOT shiny.

Slice each lit pointer out of B onto a transparent canvas → n.png, ne.pngnw.png, up.png, down.png, out.png (the hub). Rose from A → rose.png. If a generation drifts, regenerate B by attaching A and asking for “the same image with all pointers lit.”

Manifest section: [compass] — see Skins. Pool folder: ~/.vellum-fe/global/images/compass/.

Status icons (16 glyphs, one sprite sheet)

One sheet keeps the style uniform; slice into individual PNGs after.

A 4x4 grid sprite sheet of sixteen square fantasy game status icons, uniform in style, stroke weight, and visual density, each a single bold pictogram readable at 20 pixels. Flat matte colors from the reference palette, one restrained accent color where meaning demands it. Cells in row-major order:

  1. an open left hand, palm out
  2. an open right hand, palm out
  3. a hand with a subtle rune circle above the palm (spellcasting hand)
  4. a standing figure
  5. a kneeling figure
  6. a sitting cross-legged figure
  7. a figure lying prone
  8. a skull (dead)
  9. stars circling a tilted head (stunned)
  10. a falling blood drop (bleeding), dull crimson accent
  11. a hooded figure half-dissolved into shadow (hidden)
  12. a dotted outline of a figure (invisible)
  13. a figure wrapped in spiderweb strands (webbed)
  14. a serpent coiled around a drop (poisoned), dull green accent
  15. a gaunt face with sunken cheeks (diseased), pale ochre accent
  16. two linked shackle rings (group-joined)

Slice to lefthand.png, righthand.png, spellhand.png, standing.png, kneeling.png, sitting.png, prone.png, dead.png, stunned.png, bleeding.png, hidden.png, invisible.png, webbed.png, poisoned.png, diseased.png, joined.png — these names match the [icons] manifest keys.

Injury dolls (one base image per race/class)

The app draws wounds itself as dots at calibrated anchor points, so the art must expose every anchored body part. The requirements below are non-negotiable — bake them into the prompt. Swap the {RACE} / {CLASS} slots (GemStone IV races: Human, Giantman, Dwarf, Halfling, Elf, Dark Elf, Half-Elf, Sylvankind, Forest Gnome, Burghal Gnome, Half-Krolvin, Erithian, Aelotoi).

A full-body character portrait of a {RACE} {CLASS} from a fantasy world, in the exact painted style, palette, framing, and proportional scale of the attached reference image. Requirements, all mandatory:

  • standing straight, facing the viewer directly, perfectly front-on
  • arms relaxed and held slightly away from the torso so upper arms, forearms, and both open hands are fully visible and do not overlap the body
  • legs slightly apart, both feet visible
  • head bare or with headwear that leaves the face open: BOTH eyes must be clearly visible, open, and unobstructed
  • neck visible between head and collar
  • the full figure fits inside the frame with a small margin — nothing cropped
  • portrait orientation, roughly 3:4
  • muted, weathered, matte clothing appropriate to a {CLASS}; no shiny armor, no glowing effects, no magic auras
  • solid pure black background, no ground shadow

After dropping one into a skin, run Settings → Skin → “Calibrate injury doll” and click through the parts — anchors are stored as fractions of the image, so exact pixel size never matters. Generate dolls on demand for characters that exist; the calibrator makes each new doll a two-minute job. The greyscale _grey.png variant from post-processing makes colored wound dots pop.

Window frames (nine-slice borders)

The renderer slices the image into nine regions from the manifest’s slice insets: corners draw at fixed size, the four straight runs stretch along one axis, and the center is never drawn. That dictates the art:

  • The frame must run flush to all four canvas edges, with a roughly uniform band thickness (target ~1/8 of the canvas width).
  • Corner ornament is free — it draws unstretched. The straight runs between corners must be continuous, uniform material (riveted iron, wood grain along the run, plain rope) with no medallions or creatures mid-run: anything distinct there gets stretched.
  • Corner caps thicker than the runs are fine — real generations come back that way and it looks good. Set slice to the cap size; the black between the runs’ inner edge and the cap depth becomes transparent in post and simply never draws.
  • Inspect the inner corners at full zoom before accepting — models like to drop small “glint” artifacts there, and anything inside the corner squares gets drawn over window content. A clean corner can be mirrored over a blemished one in an editor (the bands line up by construction).

Prompt:

A square ornamental window frame for a fantasy game UI, viewed flat. An aged dark-iron band with worn brass corner caps, riveted along its length. The frame runs flush to all four edges of the canvas with a uniform thickness on all sides, about one eighth of the canvas wide. Corner ornamentation stays within the corner squares; the four straight runs between corners are plain, continuous, evenly textured metal with no emblems or breaks. The inner edge of the frame is a crisp straight line. The center of the frame is empty pure black.

Frames need the keying pass with a twist: the center is a sealed pocket the edge flood can’t reach, so seed it explicitly and use a low threshold so near-black seams in the art survive:

python dekey.py frame.png --seed 1024,1024 --threshold 12 --no-grey

Worked example (a 2048px generation with ~307px caps and ~240px runs):

[window.default.border]
image = "frames/frame_iron.png"
slice = [310.0, 310.0, 310.0, 310.0]  # measure the CAPS in source pixels
scale = 0.045                         # 2048px source -> ~14pt caps on screen

Measure the caps once in an editor, set slice to that, then tune scale by eye. Pool folder: ~/.vellum-fe/global/images/frames/.

Window background textures

Backgrounds sit behind text, so restraint is the whole game: low contrast, no focal points, no vignette (windows crop the image at arbitrary aspect ratios with fit = "cover", so nothing about the composition may matter). The manifest’s scrim paints a theme-colored wash on top for readability — author the texture legible-ish and let scrim do the rest.

A flat, even, borderless surface texture for a game UI background: {aged parchment with faint fiber flecks | worn dark leather with fine grain | rough hewn dark stone}. Perfectly uniform lighting across the whole canvas — no vignette, no hotspot, no directional shadow, no focal detail anywhere. Very low contrast, subtle texture only. Landscape orientation.

Don’t ask for “seamless/tileable” — image models can’t actually do it and the seam will show. Use fit = "cover" and generate generously sized. Backgrounds are opaque by design — no keying pass.

[window.default.background]
image = "backgrounds/leather.png"
fit = "cover"
opacity = 1.0
scrim = 0.35

Hotbar icon sheets

Hotbar buttons pull from [sheets] sprite sheets: square cells, no padding, indexed 1-based left→right then top→bottom (64px cells by default). Models keep style coherent across about 16 icons per generation, so build sheets from 4x4 grids — reuse the status-icon prompt frame with new cell contents (ability and spell pictograms).

After acceptance, resample the image so cells land exactly on the cell size (a 4x4 sheet → 256x256 for 64px cells) — the loader tiles by pixel arithmetic, and drifted cell boundaries bleed neighbors into buttons. Keep a note of which cell is which when you slice; the index is the only key.

[sheets.combat]
path = "icons/combat.png"
cell = 64

Post-processing (dekey.py)

Every keyed image (compass, icons, dolls, frames — not backgrounds) goes through the same script to turn the black background into a real alpha channel. It flood-fills from the image edges, so black ink lines and dark clothing inside a figure survive; only background connected to the border becomes transparent. It also emits a greyscale twin derived from the same pixels (never generate a greyscale variant separately — it will not match).

python dekey.py TheImage.png                  # -> _alpha.png + _grey.png
python dekey.py TheImage.png --threshold 40   # if a dark halo remains
python dekey.py frame.png --seed 1024,1024 --threshold 12 --no-grey

Save this as dekey.py (needs Python with Pillow: pip install Pillow):

#!/usr/bin/env python3
"""Give AI-generated skin art a REAL alpha channel."""

import argparse
import sys
from collections import deque
from pathlib import Path

from PIL import Image, ImageOps


def dekey(img, threshold, seeds=()):
    img = img.convert("RGBA")
    w, h = img.size
    px = img.load()

    def is_bg(x, y):
        r, g, b, _ = px[x, y]
        return r <= threshold and g <= threshold and b <= threshold

    seen = bytearray(w * h)
    queue = deque()
    for x in range(w):
        for y in (0, h - 1):
            if is_bg(x, y) and not seen[y * w + x]:
                seen[y * w + x] = 1
                queue.append((x, y))
    for y in range(h):
        for x in (0, w - 1):
            if is_bg(x, y) and not seen[y * w + x]:
                seen[y * w + x] = 1
                queue.append((x, y))
    for x, y in seeds:
        if not (0 <= x < w and 0 <= y < h):
            raise SystemExit(f"seed {x},{y} outside {w}x{h} image")
        if not is_bg(x, y):
            raise SystemExit(f"seed {x},{y} is not background")
        if not seen[y * w + x]:
            seen[y * w + x] = 1
            queue.append((x, y))

    while queue:
        x, y = queue.popleft()
        px[x, y] = (0, 0, 0, 0)
        for nx, ny in ((x - 1, y), (x + 1, y), (x, y - 1), (x, y + 1)):
            if 0 <= nx < w and 0 <= ny < h and not seen[ny * w + nx] and is_bg(nx, ny):
                seen[ny * w + nx] = 1
                queue.append((nx, ny))
    return img


def main():
    ap = argparse.ArgumentParser(description=__doc__)
    ap.add_argument("input", type=Path)
    ap.add_argument("--threshold", type=int, default=30,
                    help="max RGB channel value treated as background")
    ap.add_argument("--no-grey", action="store_true", help="skip greyscale variant")
    ap.add_argument("--seed", action="append", default=[], metavar="X,Y",
                    help="extra interior flood seed (repeatable); needed for "
                         "closed frames whose center the edge flood cannot reach")
    args = ap.parse_args()

    seeds = []
    for spec in args.seed:
        try:
            x, y = (int(part) for part in spec.split(","))
        except ValueError:
            ap.error(f"--seed wants X,Y integers, got {spec!r}")
        seeds.append((x, y))

    img = dekey(Image.open(args.input), args.threshold, seeds)
    out_alpha = args.input.with_name(args.input.stem + "_alpha.png")
    img.save(out_alpha)
    print(f"wrote {out_alpha}")

    if not args.no_grey:
        grey = ImageOps.grayscale(img).convert("RGBA")
        grey.putalpha(img.getchannel("A"))
        out_grey = args.input.with_name(args.input.stem + "_grey.png")
        grey.save(out_grey)
        print(f"wrote {out_grey}")
    return 0


if __name__ == "__main__":
    sys.exit(main())

Workflow notes

  • Always attach the previous batch’s accepted output as a reference when generating siblings.
  • If a result comes back with scenery or a non-black background, ask to “re-render the same image on a solid pure black background” rather than regenerating fresh.
  • Not every widget is skinnable: progress bars, countdown timers, the command input, title bars, and scroll bars have no manifest slots today. Check Skins for the current surface before generating art for something.

Submitting Art to the Community Repo

Made a nice injury doll, window frame, or icon sheet? The community asset repository accepts submissions from anyone with a GitHub account — no git knowledge required. You fill out a short form, attach your image, and a robot does the rest: it cleans the image up, checks the name isn’t taken, and stages it for a quick human review. Once approved, it’s live for every VellumFE user.

The whole thing runs on GitHub’s issue system:

Submit art here → github.com/Nisugi/vellum-assets/issues/new/choose

What can be submitted

Each kind of art has its own form, which asks only for what that kind needs:

FormWhat it isOne submission is…
Injury dolla full-body character image for the injury displayone image
Hotbar icon sheeta grid of square ability icons for hotbar buttonsone sheet image
Window framean ornamental nine-slice border drawn around windowsone image
Window backgrounda flat texture that sits behind window textone image
Compass arta compass rose and/or its lit direction overlaysan image or a zip
Status iconsthe glyph set for stance and conditionsan image or a zip
Hand iconsleft, right, and spell hand icons for the hands widgetan image or a zip

Full skins (complete graphics themes) and layouts aren’t submitted through these forms — they’re folders rather than single images, and go in by pull request instead; see the contributor notes in the asset repository.

The black-background trick

Game art needs transparency — a doll or a frame has to sit on top of whatever is behind it. But AI image generators cannot produce transparency; ask for it and you’ll get a fake checkerboard. So the pipeline uses a convention borrowed from film: paint your art on a solid pure-black background, and the submission robot turns the black into real transparency for you.

It’s smarter than “delete everything black.” The robot flood-fills inward from the edges of the canvas, so only background connected to the border is removed — black ink outlines, dark hair, and iron armor inside your artwork are untouched. (For window frames it also clears the enclosed center pocket, and treads gently around near-black metalwork.)

Two consequences worth knowing:

  • If your image already has real transparency — you cleaned it up yourself in an editor — the robot detects that and leaves it alone. (Detection requires meaningful transparency; a few stray transparent pixels from an export won’t disable keying. Filling in the threshold field always forces a re-key either way.)
  • Don’t submit greyscale variants of anything. VellumFE desaturates art at runtime when it needs to (the injury doll base, dormant states), so only the color version is ever stored.

Window backgrounds are the one exception to all of this: they must be fully opaque, and transparency in one gets a submission rejected.

If the cleanup comes out wrong

The pull request the robot opens shows your processed images — look at them. Two failure modes, one knob:

  • a dark halo clinging around your art means the cutoff was too strict — edit your issue and set Background threshold (advanced) to something higher, like 40;
  • dark details getting erased where they touch the background means it was too loose — set it lower, like 15.

Every edit to the issue re-runs the robot and updates the pull request in place, so you can tune until it looks right. Leave the field blank to use the defaults, which are right for most art.

For art direction — style, palette, and ready-to-use generation prompts that produce correctly black-keyed images — see Generating Skin Art with AI.

Names

Every asset needs a name: lowercase letters, numbers, _ and - only (the form explains this too). Names are first come, first served — if elf_wizard is taken in the dolls category, the robot will tell you and you just pick another.

For the two zip categories, files inside your zip must be named for their role so the client knows what each image is:

  • Compass: rose (the base), n ne e se s sw w nw (lit pointers), up down (chevrons), out (the hub)
  • Status icons: standing kneeling sitting prone dead stunned bleeding hidden invisible webbed poisoned diseased joined
  • Hand icons: lefthand righthand spellhand

Name each file either <role>.png or <anything>_<role>.png — both work. A partial set is fine (just a rose, say). Each set is published as its own folder: files named for a bare role land in the set named on the form, so emberiron + rose.png becomes emberiron/rose.png; files with their own prefix land in a set of that name, so one zip can carry ember_spellhand.png and frost_spellhand.png into two separate sets. You don’t need a zip at all for one image — attach it directly and pick its role in the form’s dropdown. Adding a missing piece to an existing set later is fine too.

What happens after you click Submit

  1. Within a minute or two, the robot processes your submission and comments on your issue with the result.
  2. Accepted — it opens a pull request with your finished, cleaned-up files. A maintainer eyeballs the art (that’s the whole review) and merges. The site rebuilds itself, and your asset is live in the gallery and in everyone’s .jinx list, credited to you.
  3. Rejected — the comment says exactly what’s wrong: name taken, file the wrong shape, no attachment found, a zip filename that doesn’t match a role. Edit your issue to fix it — don’t open a new one — and the robot automatically re-runs on the edit.

Nothing is ever published automatically; a person approves every submission before it goes live.

Per-category fine print

  • Injury dolls — front-facing, standing straight, arms slightly away from the torso, both hands, both feet, and both eyes visible, full figure in frame, roughly 3:4 portrait. The prompt kit bakes all of this in.
  • Hotbar icon sheets — square cells, no padding between them, and the form asks for your cell size in pixels. Cell boundaries must land exactly on multiples of that size, or buttons will bleed into their neighbors. Sheets are used as-is — no background removal.
  • Window frames — uniform band thickness, ornament confined to the corners, continuous plain material along the straight runs (they get stretched), pure-black center. The renderer slices the frame at the corner cap size — the smallest square, anchored at a canvas corner, that contains the corner ornament. The robot measures this automatically from your image; the number it found is shown in the pull request. If it looks wrong, measure the caps yourself in an editor and put the pixel value in the form’s cap-size field to override.
  • Compass overlays — the lit pointers are stacked on top of the rose at runtime, so they must be registered pixel-perfect against it. Generate them from one master image; don’t generate each separately.
  • Status icons — one bold pictogram per glyph, readable at 20 pixels.
  • Backgrounds — perfectly even lighting, no vignette, no focal point anywhere, very low contrast. Windows crop backgrounds at arbitrary shapes, so no part of the composition may matter.

Features

Not written yet. This page is queued for the manual rebuild; the pages below cover the parts of VellumFE that are not windows.

Speech (Text-to-Speech)

Not written yet. This page is queued for the manual rebuild; it will cover TTS setup, per-window speech, and queue navigation.

Stream Routing

Not written yet. This page is queued for the manual rebuild; it will cover sending game streams to the windows you choose.

Travel & Day Passes

Not written yet. This page is queued for the manual rebuild; it will cover the native travel engine in depth.

Inventory Tools (.foreach, .sorter)

VellumFE ships native versions of two of the most-used Lich inventory scripts, so they work without Lich (direct connections included). If you are running through Lich, the classic ;foreach and ;sorter still work as always — these are the .-prefixed native equivalents, and you can use whichever you prefer.

Both rely on the same item database Lich uses (gameobj-data.xml); see the data pack below for how VellumFE keeps it fresh.

.foreach — run commands over matching items

.foreach finds items in your containers and runs a series of commands for each one.

.foreach [OPTIONS] [ATTR=]VALUE in <CONTAINER>[,<CONTAINER>...][; command; command; ...]

Some examples:

.foreach gem in backpack; get item; sell item
.foreach box in locker; move to backpack
.foreach unique sorted scroll in cloak; read item
.foreach first 5 sellable=gemshop in pouch; get item; appraise item
.foreach name=*quartz orb in inv-sack; get item; put item in locker
.foreach in bandolier

The last form — no commands — is a dry run: it lists the matching items with their types and ids, so you can check a filter before acting on it. Running a dry run first is a good habit.

Filters (what to match)

AttributeMatchesExample
type (default)item category from gameobj-data.xml.foreach gem in bag
sellableshop the item sells at.foreach sellable=furrier in sack
nounthe item’s noun.foreach noun=box in locker
name / fullnamethe full item name.foreach name=*ruby* in pouch
quickshorthand for name=*VALUE*.foreach q=elan*pack in cloak

Shorthands t, s, n, m, f, q work too. Values support * wildcards, comma-separated lists (match any), and /regex/ for a regular-expression match against the name. type=none matches items with no known type.

Options (which matches, in what order)

OptionEffect
uniqueskip repeats of the same item name
first Nact on only the first N matches
after Nskip the first N matches
sortedorder alphabetically (by last word, articles ignored)
reversedreverse the order

Targets (where to look)

A target is either a container name or a pseudo-target keyword. A comma-separated list searches several. Suffix any target with ? to skip it if it isn’t found instead of stopping with an error:

.foreach box in inv-sack, disk?; move to locker
.foreach gem in worn; get item; put item in my gem pouch
.foreach herb in floor; get item

Container names — a container you have looked in this session, addressed by title (backpack, red sack, my bandolier).

Containers must have been opened and looked in. Like ;foreach, this is blind to closed containers — if a container isn’t matching, look in it once so VellumFE has seen its contents.

Pseudo-targets:

KeywordMatches
inv / inventoryeverything you carry — worn items, both hands, and items at your feet
wornworn items only
feetitems at your feet (“Placed alongside you”)
floor / ground / roomloot on the ground in the room

Not yet supported (planned): desc (room-description scenery) and locker auto-open.

Marked / registered filters

Filter by whether items are marked or registered:

.foreach marked gem in my gem pouch; get item; sell item
.foreach unregistered in backpack

Options: marked, unmarked, registered, unregistered. This status isn’t in the passive game feed — VellumFE fetches it with an INVENTORY FULL scan (its output is hidden). The first time you use one of these on items whose status isn’t yet known, .foreach runs the scan and asks you to re-run the command in a moment; the second run has the data.

Commands

Each command runs once per matched item. Two words are substituted:

  • item#<exist id> (the item’s unique id — works on direct connections, since these are ordinary game commands)
  • container#<id> of the container the item was found in

So put item in container becomes put #12345 in #77. Because ids are unambiguous, five identical gems are each handled individually with no noun-collision guesswork.

Implicit get: if the first command is drop, place, sell, appraise, trash, register, mark, or unmark, a get item is run first automatically — so .foreach gem in bag; sell item gets and sells each gem.

A few special commands are handled by VellumFE rather than sent to the game:

CommandEffect
waitrtwait until roundtime clears before continuing
sleep <seconds>pause (e.g. sleep 1.5)
echo <text>print a note to your main window
move to <container>shortcut for dragging the item into that container

Everything else is sent to the game verbatim, once per item — including ;yourscript item when you’re behind Lich.

Pacing and stopping

Commands are paced: one goes out at a time, only while roundtime is clear, with a short gap between. A run announces each item as it starts.

Stop a run at any time with .stop (or Esc). Dying stops it automatically. Only one automation task drives the game at once — if a .go2 trip is in progress, .foreach will tell you to .stop it first, and vice-versa.

.sorter — categorized container contents

.sorter reformats the output of looking in a container so items are grouped by type and duplicates are counted, instead of one long run-on sentence.

.sorter on      # enable
.sorter off     # disable
.sorter         # toggle

With it on, look in my backpack turns this:

In the backpack you see a blue sapphire, a quartz crystal, a blue
sapphire and a copper lockpick.

into this:

In the backpack:
  gem (3): quartz crystal, blue sapphire (2)
  other (1): copper lockpick

Item names stay clickable, categories come from the same gameobj-data.xml as .foreach, and the setting is remembered between sessions (it also appears in Settings → UI → Sort Container Looks).

The data pack (.data)

.foreach and .sorter classify items using gameobj-data.xml, the same file Lich maintains through ;repo. VellumFE resolves it from the first available of three sources:

  1. Your Lich data folder, if the Lich directory is configured (Settings → Map → Lich Directory) — the freshest copy, since your ;repo keeps it current.
  2. A local copy in ~/.vellum-fe/global/data/.
  3. A bundled snapshot, refreshed with each VellumFE release.

Check what’s in use and how old it is:

.data status    # show the source and age of each data asset
.data reload     # re-resolve now (e.g. after ;repo download in Lich)

In the GUI, the same information and a Reload button live under Settings → Data. Setting your Lich folder (Settings → Map → Lich Directory) lets VellumFE use your ;repo-maintained copy, which is always the freshest.

Asset Manager (.jinx)

VellumFE can download and keep current the files that shape your game and your interface — game data (item and spell databases, the map), skins, icon sets, and shareable layouts / UI packs — without a Lich install. Direct connections and phones get the same fresh files everyone else does.

It speaks the same federated-repository protocol as Lich’s jinx package manager, so it reads the existing community repositories for game data and VellumFE’s own repositories for skins, icons, and layouts. If you do run through Lich, ;jinx still works exactly as before — this is the native, .-prefixed equivalent, and you can use whichever you prefer.

Nothing downloads on its own. Every install and update is something you ask for (or opt into with .jinx auto-update). VellumFE never reaches out to the network unless you tell it to.

Quick start

.jinx list                       see everything available
.jinx search parchment           find assets by name
.jinx info parchment             read details before installing
.jinx install parchment          install it
.jinx update parchment           pull the latest version
.jinx auto-update --dry-run       see what has updates (without applying)
.jinx auto-update                update everything you've installed

What you can install

KindExamplesWhere it lands
Game datagameobj-data.xml, effect-list.xml, spell-list.xml, mapdb.jsonshared data store; loaded live
Skinsfull GUI graphics themes (frames, colors, icons)global/skins/<name>/
Icon setsshared icon sheets for hotbars / status / handsglobal/images/icons/
Layouts / UI packscomplete window arrangements shared as .vellumpackyour layouts folder

Game-data files are the same ones Lich maintains, so if you have a Lich install keeping them fresh, VellumFE already prefers that copy — see the data pack. .jinx is how you keep them current without Lich.

VellumFE deliberately installs only data and interface assets — it never downloads or runs scripts. Executable code stays Lich’s domain.

Commands

Finding and inspecting

.jinx list [--repo=<name>]        list all assets (optionally one repo)
.jinx search <pattern>            search names across every repo
.jinx info <name>                 type, age, author, description

info shows the author and description for VellumFE assets (skins, layouts) so you know what a thing is before you install it.

Installing and updating

.jinx install <name> [--repo=<name>] [--force]
.jinx update  <name> [--force]
  • The kind (data / skin / icon / layout) is detected automatically from the repository — you just name the asset.
  • If the same name exists in two repositories, add --repo=<name> to pick one. .jinx will tell you when it needs that.
  • --force overwrites even when the local copy looks changed.

When you install a skin, VellumFE asks whether to switch to it — it never restyles your interface without your say-so.

Keeping everything current

.jinx auto-update [--dry-run]

Checks every asset you’ve installed against its repository and updates the ones that changed. --dry-run reports what would update and changes nothing — a good first step.

Managing repositories

VellumFE ships with the standard repositories already configured. To add a community repo (for example, a friend’s skin collection):

.jinx repo list                          show configured repositories
.jinx repo add <name> <https-url>        add one (HTTPS only)
.jinx repo change <name> <https-url>     point a repo at a new URL
.jinx repo rm <name>                     remove one

Only https:// URLs are accepted.

How updates know what changed

Each repository publishes a small manifest listing every asset and a checksum of its current contents. VellumFE compares that checksum to what you have installed:

  • clean — your copy matches the repository.
  • modified — your copy differs (you edited it, or it’s out of date).
  • update available — the repository has a newer version.

For multi-file assets like skins, the whole bundle is checksummed as a unit, so any change to any file shows up as a single “update available.” Before an update overwrites anything, VellumFE backs up what it replaces, so trying a new version is always safe to undo.

Sharing your own skins, icons, and layouts

VellumFE’s community assets live in a single public repository, and contributions are welcome. The short version:

  1. Build your asset. A layout or UI pack is just a .vellumpack from .uiexport. A skin is its skin folder.
  2. Add a small meta.toml next to it — title, author, description, version, and a preview.png.
  3. Open a pull request to the community assets repository.

A validation check confirms the structure (valid meta.toml, a preview image, sensible size). Once merged, an automated job rebuilds the manifest, and your asset is live in everyone’s .jinx list — no server to run, no manual step.

See the contributor guide below for the exact meta.toml fields and folder layout.

Contributing details

A contributor writes exactly one file by hand — everything else (checksums, timestamps, the manifest entry) is generated automatically.

meta.toml, placed beside your asset:

title       = "Parchment"                        # shown in the gallery
author      = "YourName"
description = "Warm aged-paper theme with rune icons."
version     = "1.2.0"                             # bump when you change it
tags        = ["warm", "fantasy", "high-contrast"]
preview     = "preview.png"                       # ships inside the asset

Folder layout in the community repository:

skins/
  parchment/
    meta.toml
    preview.png
    skin.toml
    ...skin files...
icons/
  <your-icon-set>/meta.toml, ...
layouts/
  <your-layout>.vellumpack + meta.toml

Requirements the validator checks:

  • a valid meta.toml with title, author, description, version
  • a real preview.png (for skins and layouts), reasonably sized
  • the asset stays within its folder and under the size cap
  • for skins, a skin.toml that parses

When your PR merges, the manifest rebuilds automatically and the asset appears in .jinx list for everyone.

Troubleshooting

  • “more than one repo has asset(…)” — add --repo=<name>.
  • “… already exists” / looks modified — you (or an update) changed the local copy; re-run with --force to overwrite. Your old copy is backed up first for multi-file assets.
  • A manifest fails to load — the repository may be temporarily down; .jinx list skips it and reports the error. Try again later, or check .jinx repo list for the URL.
  • Nothing seems to update — run .jinx auto-update --dry-run to see the comparison; “clean” everywhere means you’re current.

Lich WebUI Panels

Not written yet. This page is queued for the manual rebuild; it will cover rendering Lich script pages as native panels.

Profiles & Setting Scope

Not written yet. This page is queued for the manual rebuild; it will cover global-versus-character settings and how they merge.

In-App Editors

Not written yet. This page is queued for the manual rebuild; the pages below cover every configuration panel in the client.

Settings & Profiles

Not written yet. This page is queued for the manual rebuild; it will cover the Settings window and per-character scope.

Appearance Editors

Not written yet. This page is queued for the manual rebuild; it will cover the colors, themes, skins, and icon editors.

Input Editors

Not written yet. This page is queued for the manual rebuild; it will cover keybinds, menu keybinds, controller, and touch-wheel editors.

Window & Layout Editors

Not written yet. This page is queued for the manual rebuild; it will cover the Windows catalog, tab editor, dashboard, and streams panel.

Command Reference

Anything you type starting with . is handled by VellumFE instead of being sent to the game. Command names are case-insensitive; Tab completes them. .help prints an abbreviated version of this list in-game. Unknown commands print a hint.

Everything else you type goes to the game unchanged. (Typing the game command quit also saves your settings on the way out.)

General

CommandAliasesDescription
.help.h, .?List all commands
.version.verShow VellumFE version
.quit.qExit VellumFE (saves settings)
.menuOpen the main menu
.settingsOpen the settings editor
.reload [what]Reload config from disk: highlights, keybinds, hotbars, settings, colors, layout, or everything
.roomShow how the current room resolved against the map database (stream ids, mapdb room/location, routable exits, tags) — for debugging the map and pathing
.mapdb [download|remove|repo <r>]Manage downloaded map data from any frontend (no args = status). On phones this is the way to fetch the map — there’s no Settings > Map panel there
.go2 <target>Native map travel: room id, uid (u7150105), tag (bank), saved name, or text search — see the Travel chapter
.go2 stop / .go2 statusCancel / show the active trip
.go2 save <name> [id]Save a travel target (.go2 targets lists them, .go2 back returns to the trip start)
.portal [n|word]Walk the room’s non-compass exit (go door, climb stair, …) from the map data (room objects as fallback). One candidate walks it; several open a picker menu (keyboard/pad navigable) — or pick by number or word. Controller d-pad left by default
.foreach ... in <bag>; cmd; cmdRun commands over matching container items (native ;foreach). No commands = dry-run list. See Inventory Tools
.sorter [on|off]Categorize “look in container” output by item type (native ;sorter). No arg toggles
.stopStop whatever automation is driving — a .go2 trip or a .foreach run
.data [status|reload]Inspect / refresh the shared item database used by .foreach and .sorter (Lich folder → local → bundled)

Windows & Layout

CommandAliasesDescription
.windowsList all windows
.addwindow [name type x y w [h]]Add a window (no args opens a picker)
.deletewindow <name>.delwindowHide a window (kept in the layout; in the GUI, Delete Window in the right-click menu’s Window ▸ Advanced removes it for real)
.editwindow [name].editwinEdit a window — TUI: opens its window editor; GUI: opens its right-click menu (no name: TUI picker / GUI Windows catalog)
.hidewindow [name].hidewinHide a window
.rename <window> <new title>Rename a window’s title
.border <window> <style> [color]Set border sides: all, none, top, bottom, left, right
.lockwindows.lockall, .unlockwindows, .unlockallToggle move/resize lock on all windows
.performance [dump].perfToggle the performance monitor; dump writes a diagnostic report file
.savelayout [name]Save the current layout under a name in the shared ~/.vellum-fe/layouts/ folder, loadable by any character (each frontend has its own format: TUI .toml grids, GUI .json checkpoints)
.loadlayout <name>Load a saved layout; in the GUI it applies live to the running session
.layoutsList saved layouts for this frontend
.resizeRefit layout to the current terminal size (TUI)
.nexttab / .prevtabSwitch tabs in a tabbed window
.gonew.nextunreadJump to the next tab with unread messages
.streamsOpen the stream routing editor: every known stream and where it goes (a window, main, or discard)

Sharing Your UI

CommandAliasesDescription
.packs.packeditorOpen the pack editor panel (TUI and GUI): pick parts with checkboxes, export to a folder of your choice, and import from ~/.vellum-fe/imports/ (a dropdown lists any .vellumpack you drop there) or a pasted path, with a preview and per-part install selection.
.uiexport <name> [parts...]Bundle the files that make your UI into ~/.vellum-fe/exports/<name>.vellumpack — a single shareable file. Parts: layout (TUI grid + the GUI’s live arrangement when exported from the GUI), highlights, keybinds, controller, hotbars, colors, macros, skin (the active skin’s whole folder), theme (the active theme, including its file for custom themes), sounds (the shared sound folder), quickbars, and settings (general settings: UI options, sound/TTS prefs, stream routes — never connection or account data). Default: all. Connection settings and passwords are never included.
.uiimport <name|file>Preview a pack: its parts, skin, theme, and file count. Names resolve from ~/.vellum-fe/imports/ and exports/.
.uiimport <name|file> apply [parts...]Install a pack (optionally only the listed parts): replaced files are backed up to ~/.vellum-fe/backups/, everything hot-reloads, and layouts land as named checkpoints (.loadlayout <packname>). Skins and themes activate; quickbars and settings merge into your config without touching connection data. Unknown or unsafe entries in a pack are skipped, never written.

Post packs in the community Discord — favorites can become shipped default layouts.

Highlights

CommandAliasesDescription
.highlights.hlBrowse highlights
.addhighlight.addhlCreate a highlight
.edithighlight [name].edithlEdit a highlight
.testline <text>Inject a fake game line to test patterns
.savehighlights [name].savehlSave highlights as a named profile
.loadhighlights [name].loadhlLoad a highlight profile
.highlightprofiles.hlprofilesList highlight profiles

Keybinds

CommandAliasesDescription
.keybinds.kbBrowse keybinds (press f to cycle the scope filter: all / global / character)
.controllerEdit gamepad button bindings (GUI; see Controllers)
.addkeybind.addkeyCreate a keybind
.savekeybinds [name].savekbSave keybinds as a named profile
.loadkeybinds <name>.loadkbLoad a keybind profile
.keybindprofiles.kbprofilesList keybind profiles

Hotbars

CommandAliasesDescription
.hotbars.hotbarOpen the hotbar editor (bars of command buttons; see Hotbars)

Colors & Themes

CommandAliasesDescription
.themesBrowse and apply themes
.settheme <name>.themeSwitch theme by name
.editthemeEdit the current theme
.skinsList installed GUI skins
.setskin <name>.skinApply a skin (.setskin none disables; GUI only)
.makeskin <name>Create a starter skin to edit
.reloadskinForce-reload the active skin (after editing images)
.colors.colorpaletteBrowse the color palette
.addcolor.createcolorAdd a palette color
.uicolorsEdit UI element colors
.spellcolorsEdit spell-circle colors
.addspellcolor.newspellcolorAdd a spell color entry
.setpaletteLoad palette into terminal slots (TUI, 256-color mode)
.resetpaletteReset the terminal palette (TUI)
.harmony [scheme]Generate the game-text preset colors from the active theme (.harmony schemes lists the schemes; the GUI Colors editor’s Generate tab adds seed swatches, pins, and a live preview)
.harmony skin <name>Write a matching skin — panel backgrounds and nine-slice frames rendered from the same harmony — to global/skins/<name>/; activate with .setskin <name> (GUI)

Text-to-Speech

.tts with no subcommand shows status. Settings changes save immediately. The GUI has the same controls in Settings > Speech; per-window speech is the “speak new lines” checkbox in the window’s right-click menu (tts_speak in layout.toml).

CommandDescription
.tts on / .tts offEnable / disable text-to-speech
.tts muteToggle mute without turning TTS off
.tts rate <0.5-3.0>Speech rate (1.0 = normal)
.tts volume <0.0-1.0>Speech volume
.tts voice <name|default>Pick a voice by name, or return to the engine default
.tts voicesList available voices
.tts testSpeak a sample line
.tts clearClear the pending speech queue
.tts statusShow enabled/muted state, rate, volume, voice, and queue depth

Misc

CommandAliasesDescription
.transparentToggle transparent window backgrounds (TUI)
.hidecontainers [title]Close container windows (all, or one by title)
.reloadmacrosReload macros.toml and push to connected phones
.webinfoShow the phone pairing URL / app link and open their QR codes
.webui [page|off]Lich WebUI panels (GUI, Lich 5.18+): no args picks from Lich’s registered pages, a name opens that page, off disconnects

Configuration

VellumFE uses TOML files for configuration, stored in ~/.vellum-fe/ (override the location with the VELLUM_FE_DIR environment variable or --data-dir).

Configuration Files

FilePurpose
config.tomlGeneral settings (connection, UI, sound, TTS, web server)
layout.tomlWindow positions, sizes, and properties (TUI)
keybinds.tomlKeyboard shortcuts
controller.tomlGamepad binds, wheels, rumble, tuning (global + per-character)
highlights.tomlText highlighting, sounds, squelch rules
colors.tomlColor palette, stream presets, spell colors
hotbars.tomlHotkey bars (command buttons)
macros.tomlMacro buttons for the mobile web frontend

Directory Layout

~/.vellum-fe/
├── launcher.toml         # Launcher profiles (passwords live in the OS keyring)
├── global/               # Shared settings for all characters
│   ├── config.toml
│   ├── keybinds.toml
│   ├── controller.toml    # Gamepad binds/wheels/rumble/tuning (per-char override in profiles/)
│   ├── highlights.toml
│   ├── colors.toml
│   ├── hotbars.toml
│   ├── macros.toml
│   ├── sounds/           # Sound files for highlight alerts
│   ├── skins/            # GUI skins (one folder per skin: skin.toml + images)
│   └── images/           # Shared image pool (icons/, frames/, dolls/, compass/, backgrounds/)
├── layouts/              # Saved layouts (.savelayout / .loadlayout)
├── profiles/
│   └── CharName/         # Per-character overrides + auto-saved layout.toml
├── themes/               # Custom themes (.edittheme saves here)
└── vellum-fe.log

Files in profiles/<name>/ override the matching global file for that character.

Editing Configuration

Most things can be edited in-app without touching files:

CommandOpens
.settingsSettings editor — every registered setting, on both frontends
.highlightsHighlights browser
.keybindsKeybinds browser
.hotbarsHotbar editor
.streamsStream routing editor
.colorsColor palette browser
.uicolors / .spellcolorsUI element / spell-circle colors
.themesTheme browser
.ttsText-to-speech controls (GUI: Settings > Speech)

If you edit files directly, apply changes without restarting:

.reload              # reload everything
.reload highlights   # or just one: highlights, keybinds, settings, colors, layout
.reloadmacros        # macros.toml (also pushes to connected phones)

How Saves Work

You shouldn’t need to care, but for the curious:

  • Sparse saves — user files only contain what you’ve changed from the shipped defaults, with your comments preserved. Settings you never touched pick up new defaults automatically on upgrade.
  • Atomic writes with backups — every save writes a temp file and swaps it in, keeping a .bak of the previous version. A crash mid-save can’t corrupt your config.
  • Additive default refresh — new shipped highlights/keybinds/hotbars appear after an upgrade, but ones you deleted stay deleted (a .defaults-seen.toml sidecar remembers what you’ve already been given).

Resetting to Defaults

Delete a configuration file and it is recreated with defaults on next launch:

rm ~/.vellum-fe/global/keybinds.toml

Or delete the entire directory for a full reset. (Individual shipped entries you deleted from collection files like highlights stay deleted across upgrades — deleting the whole file is the reset switch.)

config.toml

General client settings: connection, UI behavior, sound, TTS, and the web server. Most values can also be changed in-app via .settings. Apply file edits with .reload settings.

Connection

[connection]
host = "127.0.0.1"
port = 8000
character = "YourName"

# For direct connection (optional - can use CLI instead)
account = "your_account"
password = "your_password"  # Stored in plain text!
game = "prime"              # GS4: prime, platinum, shattered, test
                            # DR: dr, drplatinum, drfallen, drtest

Tip: For security, omit password — VellumFE prompts for it securely at startup in direct mode. CLI arguments override these values.

User Interface

[ui]
buffer_size = 10000             # Default lines kept per window
border_style = "single"         # single, double, rounded, thick, none
countdown_icon = "█"            # Glyph for RT/CT timer blocks
color_mode = "direct"           # direct, slot, indexed (see below)

# Text selection
selection_enabled = true
selection_respect_window_boundaries = true
selection_auto_copy = true      # Copy on mouse-up

# Commands
command_echo = true             # Show sent commands in main window
min_command_length = 3          # Min length to save in history

# Drag modifier for moving windows
drag_modifier_key = "ctrl"      # ctrl, alt, or shift

Every game window (dialogs, panels, streams, containers) is hidden by default and never auto-opens until you show it in the Windows list (Windows → Show/Hide windows). There is no dialog blocklist — hidden windows simply stay hidden even when the game re-sends them.

Color Modes

ModeDescription
direct24-bit true color. Use with modern terminals (kitty, alacritty, Windows Terminal)
slot256-color with custom palette via .setpalette. For terminals supporting OSC 4
indexed256-color with standard palette (closest match). Safe fallback

Focus Navigation

Control which windows are focusable with Tab:

[ui.focus]
types = ["text", "tabbedtext"]  # Widget types that can receive focus
exclude = ["bounty", "society"] # Specific windows to skip
order = []                      # Custom focus order (empty = layout order)

Target List

Configure the targets widget display:

[target_list]
status_position = "end"         # "end" or "start"
truncation_mode = "noun"        # "full" or "noun"
excluded_nouns = ["arm", "coal"]
boss_color = "#ff5555"          # AscensionBoss / MiniBoss creatures
challenging_color = "#ffaa55"   # "challenging" creatures

[target_list.status_abbrev]
stunned = "stu"
frozen = "frz"
dead = "ded"

The boss and challenging colors apply when the game’s structured creature status feed classifies a creature; see Targets.

Highlights

Global toggles for the highlight system:

[highlights]
sounds_enabled = true           # Play sounds on match
replace_enabled = true          # Apply text replacements
redirect_enabled = true         # Route lines to other windows
coloring_enabled = true         # Apply color highlighting

Map

Where the map widget finds its data (GUI):

[map]
lich_dir = "C:/Lich5"              # Lich install (folder containing data/)
# mapdb_path = "C:/maps/map.json"    # explicit file; overrides everything
mapdb_repo = "Nisugi/mapdb"        # GitHub repo for Settings > Map downloads
mapping_mode = false               # Cartography mode: sketch unmapped rooms

All of these are editable in Settings → Map in the GUI. Downloaded map data outranks the Lich folder; an empty mapdb_repo disables downloads. mapping_mode gates ghost-room sketches — see ghost rooms.

Travel (.go2)

Managed by the client — you normally never edit this section:

[go2]
native_map_clicks = true   # Map clicks travel natively; false sends ;go2 to Lich

[go2.saved]                # .go2 save <name> writes these
bank = 3517

[go2.pathcodes]            # Maze routes, captured automatically in-game

native_map_clicks is also in Settings → Travel in the GUI. See the Travel chapter for the .go2 command family.

Sound

[sound]
enabled = true
volume = 0.7                    # 0.0 to 1.0
cooldown_ms = 500               # Min time between repeated sounds
startup_music = true
startup_music_delay_ms = 0      # Delay before the login theme starts

Text-to-Speech

Prefer the editors: Settings > Speech in the GUI, or the .tts commands on any frontend.

[tts]
enabled = false
rate = 1.0                      # 0.5 (slow) to 3.0 (engine max)
volume = 1.0
speak_thoughts = true
speak_speech = true
speak_main = false              # Usually too noisy
# voice = "Microsoft Zira"      # Voice by name (.tts voices lists them)

# Lines matching these regexes are shown but never spoken
# gags = ["^You feel fully rested"]

# Pronunciation fixes applied before speaking
# substitutions = [{ pattern = "Wehnimer's", replacement = "Wenimers" }]

The three speak_* toggles are classic shortcuts for main/thoughts/speech. Any other window can opt in per-window: the “speak new lines” checkbox in its right-click menu (TUI: window editor; tts_speak in layout.toml).

TTS queue navigation keys are bound in keybinds.toml (defaults: Ctrl+Alt+arrows, F7F11).

Web Server (Mobile Frontend)

Embedded HTTP + WebSocket server that lets a phone browser join the session. Off by default; see Mobile Web.

[web]
enabled = false
port = 8040           # base port; instances walk upward unless pinned
bind = "127.0.0.1"    # set "0.0.0.0" to allow phones on your LAN
pinned = false        # true = bind exactly this port or fail loudly

Phones must pair once via a token — run .webinfo in-game for the URL and QR code.

Security: pairing keeps strangers out, but keep the port on a trusted LAN; for off-LAN play use Tailscale/WireGuard. Never expose it to the open internet.

Quickbars

Define custom quickbar windows that send commands:

[quickbars]
default = "quick-custom"

[[quickbars.custom]]
id = "quick-custom"     # must be "quick" or start with "quick-"
title = "Custom"
entries = [
  { type = "link", label = "look", command = "look" },
  { type = "sep" },
  { type = "link", label = "inventory", command = "inventory" }
]

Stream Routing

Controls where a stream’s text goes when no window subscribes to it. Prefer the editor: .streams (or the GUI’s Streams panel) lists every known stream and lets you set its route.

[streams]
fallback = "main"               # Streams with no route entry go here
room_in_main = true             # Show room text in main (DR only)

[streams.routes]
# Per-stream policy: "discard", "main", or "window:<name>"
speech = "discard"              # Already echoed in main; drop the duplicate
targetlist = "discard"          # Lich script noise without a widget
logons = "window:arrivals"      # Send to a window (received even while hidden)

Windows that subscribe to a stream always win; routes only decide what happens to orphaned streams. A window:<name> route never creates or opens the window — if it doesn’t exist, the stream uses fallback.

Legacy: older configs used drop_unsubscribed = [...]. It still loads — each entry is migrated to routes.<stream> = "discard" — but routes is the current mechanism.

Logging

Capture raw XML for debugging (written to profiles/<character>/logs/):

[logging]
enabled = false
# dir = "logs"
# timestamps = true
# max_lines_per_file = 30000

Event Patterns

Regex patterns that drive countdown timers. The defaults cover standard stun messages; add your own:

[event_patterns.stun_rounds]
pattern = '^\s*You are stunned for ([0-9]+) rounds?'
event_type = "stun"             # which countdown this feeds (see below)
action = "set"                  # set or clear
duration_capture = 1            # capture group holding the duration
duration_multiplier = 5.0       # rounds -> seconds
enabled = true

The event_type names the countdown the pattern feeds: stun drives the stun window, rt/ct drive roundtime/casttime, and any other string feeds the countdown window whose id matches it (case-sensitive). To build a timer for a spell that lasts multiple rounds: add a custom countdown window (Windows > Add Window > Countdowns > Custom) and give it an id like tremors, then add a pair of patterns:

[event_patterns.tremors_start]
pattern = 'The ground begins to shake violently for ([0-9]+) rounds'
event_type = "tremors"
action = "set"
duration_capture = 1
duration_multiplier = 5.0       # rounds -> seconds
enabled = true

[event_patterns.tremors_end]
pattern = 'The ground grows still\.'
event_type = "tremors"
action = "clear"
duration = 0
enabled = true

Several patterns may share one event_type (the defaults ship four for stun). If the message carries no number, drop duration_capture and set a fixed duration in seconds instead. Patterns match script output too, so a Lich script can drive a countdown by echoing a line the pattern recognizes.

Script-driven timers: <vellumTimer>

Lich scripts can skip the regex entirely and feed a countdown directly by sending a tag to the client:

<vellumTimer id='dark-cataclyst' value='1764904999'/>

id is the countdown window’s feed id (same matching as event_type above, case-sensitive) and value is the absolute epoch end time in seconds — compute it in the script as Time.now.to_i + duration, the same convention <roundTime> uses, so it stays correct under lag. value='0' clears the timer. The tag never renders as text. From a Lich script:

puts "<vellumTimer id='dark-cataclyst' value='#{Time.now.to_i + 90}'/>"

Script-driven images: <vellumImg>

Lich scripts can float a real image into a text window, with the text wrapping around it:

_respond "<compDef id='sprite' src='sunset' rows='4' align='left'/>"

Art lives in ~/.vellum-fe/global/images/inline/. See Inline Images for the full tag reference, room-window examples, and supported formats.

layout.toml

Defines window positions, sizes, and properties for the TUI. (The Desktop GUI keeps its own layout separately.)

Layouts are saved with .savelayout [name] to ~/.vellum-fe/layouts/, and the current layout auto-saves per character to profiles/<name>/layout.toml.

Basic Structure

terminal_width = 120
terminal_height = 40

[[windows]]
name = "main"
widget_type = "text"
row = 0
col = 0
rows = 37
cols = 120

Window Properties

Required

PropertyTypeDescription
namestringUnique identifier
widget_typestringWidget type (see Widgets)
rowintegerTop row position (0 = top)
colintegerLeft column position (0 = left)
rowsintegerHeight in rows
colsintegerWidth in columns

Optional

PropertyTypeDefaultDescription
visiblebooltrueShow window
show_borderbooltrueDraw border
border_stylestring"single"single, double, rounded, thick, quadrant_inside, quadrant_outside
border_colorstring"#808080"Border color
border_sidesarrayall sidesWhich sides to draw, e.g. ["top", "bottom"]; [] for none
titlestring-Custom title
show_titlebooltrueShow title in border
title_positionstring"top-left"Where the title sits on the border
buffer_sizeinteger10000Lines to keep (text windows)
background_colorstring-Background color
text_colorstring-Default text color
transparent_backgroundboolfalseSee-through background
text_sizefloat-Per-window font point size (GUI)
font_familystring-Per-window font (GUI)
content_alignstring-Content alignment override
tts_speakboolfalseSpeak new lines routed here (TTS)

Size Constraints

[[windows]]
name = "compass"
widget_type = "compass"
min_rows = 3
max_rows = 5
min_cols = 7
max_cols = 15

Widget-Specific Properties

Text Windows

[[windows]]
name = "main"
widget_type = "text"
streams = ["main"]              # Streams to display
buffer_size = 10000
compact = false                 # Remove blank lines
wordwrap = true                 # Wrap long lines (also on inventory/tabbed)

Tabbed Text

[[windows]]
name = "channels"
widget_type = "tabbedtext"
buffer_size = 5000

[[windows.tabs]]
name = "Speech"
streams = ["speech"]
show_timestamps = true

[[windows.tabs]]
name = "Thoughts"
streams = ["thoughts"]

Progress Bars

[[windows]]
name = "health"
widget_type = "progress"
id = "health"                   # health, mana, stamina, spirit, concentration, pbarStance
color = "#00FF00"
numbers_only = false            # true: show only current/max numbers

Countdowns

[[windows]]
name = "roundtime"
widget_type = "countdown"
id = "roundtime"                # roundtime, casttime, stuntime

Room Window

[[windows]]
name = "room"
widget_type = "room"
show_desc = true
show_objs = true
show_players = true
show_exits = true
show_name = true

Example Layout

terminal_width = 160
terminal_height = 50

# Main game text - left side
[[windows]]
name = "main"
widget_type = "text"
streams = ["main"]
row = 0
col = 0
rows = 45
cols = 100
buffer_size = 10000

# Channels - right side
[[windows]]
name = "channels"
widget_type = "tabbedtext"
row = 0
col = 100
rows = 30
cols = 60
buffer_size = 2000

[[windows.tabs]]
name = "Speech"
streams = ["speech"]

[[windows.tabs]]
name = "Thoughts"
streams = ["thoughts"]

# Status bars
[[windows]]
name = "health"
widget_type = "progress"
id = "health"
row = 30
col = 100
rows = 1
cols = 60

# Command input - bottom
[[windows]]
name = "command_input"
widget_type = "command_input"
row = 47
col = 0
rows = 3
cols = 160
completion_color = "#808080" # Optional; defaults to theme text_secondary (TUI; GUI/web use theme colors)

Hidden Windows

Set visible = false to define windows that can be shown later via the menu:

[[windows]]
name = "society"
widget_type = "text"
streams = ["society"]
visible = false
# ... position and size still required

Show via: Menu → Windows → Add Window → Text Windows → Society

keybinds.toml

Keyboard shortcuts, organized into three sections by priority. You can edit this file directly (then .reload keybinds) or use the in-app editor (.keybinds to browse, .addkeybind to add).

File Structure

The file maps keys to actions in three sections:

[app]      # Always active, highest priority (quit, search, close)
quit = "ctrl+c"
start_search = "ctrl+f"
close_window = "esc"

[menu]     # Active inside menus, forms, browsers, editors
navigate_up = "up"
select = "enter"
save = "ctrl+s"

[user]     # Game mode only — your customizations go here
enter = "send_command"
up = "previous_command"
"ctrl+r" = "send_last_command"
num_8 = { macro_text = "n\r" }

Note the orientation differs: [app] and [menu] are action = "key", while [user] is "key" = "action".

Key Names

Combine modifiers with +: "ctrl+shift+a", "alt+page_up". Quote any name containing + or symbols.

GroupNames
Modifiersctrl, alt, shift
Function keysf1f12
Arrowsup, down, left, right
Navigationhome, end, page_up, page_down
Editinginsert, delete, backspace, enter, tab, esc, space
Numpadnum_0num_9, num_plus, num_minus, num_multiply, num_divide, num_decimal, num_enter

Numpad keys accept any modifier combination — ctrl+num_8, alt+num_divide, ctrl+alt+shift+num_plus. They use word-form names because a literal + in a key name would collide with the + that separates modifiers. The older symbol spellings ("num_+", "num_.", …) are still read, so existing files keep working, but they cannot take modifiers — switch to word form if you want chords.

Shift+numpad on Windows: Windows temporarily overrides NumLock while Shift is held, so the numpad reports its navigation twin. VellumFE recovers the numpad identity, so shift+num_8 does bind — but it is indistinguishable from pressing that same physical key with NumLock off.

Tip: If backspace doesn’t work, your terminal may send delete instead. Run with RUST_LOG=debug and check the log for KEY EVENT lines to see what your terminal actually sends.

Actions

Bind any of these in [user]:

ActionDescription
send_commandSend the input line to the game
previous_command / next_commandCommand history
send_last_command / send_second_last_commandRepeat recent commands
cursor_left / cursor_right / cursor_home / cursor_endMove cursor
cursor_word_left / cursor_word_rightMove by word
cursor_backspace / cursor_deleteDelete characters
switch_current_windowFocus next window
scroll_current_window_up_page / ..._down_pageScroll by page
scroll_current_window_up_one / ..._down_oneScroll by line
start_search / next_search_match / prev_search_match / clear_searchIn-window search
toggle_performance_statsPerformance overlay
stop_travelCancel the active .go2 trip (while traveling, Esc does this by default)
interact_modeToggle interact mode (GUI; default F6)
tts_next / tts_previous / tts_next_unread / tts_stopText-to-speech navigation
tts_mute_toggle / tts_increase_volume / tts_decrease_volume / tts_increase_rate / tts_decrease_rateTTS controls

Controller Bindings

Controller (gamepad) configuration lives in its own file — see controller.toml. It was split out of keybinds.toml so a controller setup is one shareable, version-controllable file and a bad edit there can’t take keyboard input down with it.

Macros

Send text with a keypress using the inline-table form. \r presses Enter:

[user]
num_8 = { macro_text = "n\r" }              # north
num_2 = { macro_text = "s\r" }              # south
f5 = { macro_text = "stance defensive\r" }
f6 = { macro_text = "hide\r" }              # omit \r to just type it

The default file ships numpad movement macros (num_1num_9 for directions, num_0 down, num_decimal up, num_plus look, and so on).

A macro can chain several commands with \r between them, and pause between commands with a sleep segment: a segment that is just s followed by seconds (s0.5, s2, s90 — decimals fine, no upper bound). Everything after the sleep goes out when the pause elapses; consecutive sleeps add up. Spaces around segments are ignored, and a bare s is still south.

f7 = { macro_text = "stand\rs1.5\rout\r" }   # stand, wait 1.5s, leave
f8 = { macro_text = "prep 101\r s3.2 \rcast\r" }

Sleep segments work anywhere command text is dispatched — keyboard macros, controller binds, hotbar buttons, macro buttons (including taps from the phone client), and wheel slices.

While interact mode is focused on an entity, macros may reference it with <target_id> (exist id) and <target_noun>:

[controller]
west = { macro_text = "target #<target_id>\rincant 611\r" }

A placeholder macro pressed with nothing focused is dropped (with a message) rather than sent literally. Exits have no exist id.

Keybind Profiles

Save and swap whole keybind sets:

.savekeybinds hunting
.loadkeybinds hunting
.keybindprofiles          # list saved profiles

controller.toml

Controller (GUI gamepad) configuration lives in its own controller.toml — separate from keybinds.toml so a controller setup can be shared or version-controlled as one file, and a malformed edit here cannot take keyboard input down with it. Everything below is edited in the .controller editor (GUI) — hand-editing is never required.

Global and per-character layers

There are two layers, base first:

  • global/controller.toml — the shared setup, used by every character.
  • profiles/<character>/controller.toml — that character’s overrides.

At load time the two are merged, character over global: binds override per button, named wheels override per name, and the whole-value sections (the default wheel ring, the overlay list, [controller_rumble], [controller_tuning]) are replaced wholesale by the character’s copy when present. A character file therefore holds only the diffs — a swashbuckler and a wizard can drive the same pad differently while sharing a global base, and a character with no override file just uses global.

In the .controller editor a Save to: switch at the top picks where edits land — Global (all characters) or This character. It routes every save (binds, wheels, rumble, tuning, overlay); loading always merges, so switching scope changes only where new edits are written. The character option is disabled until a character is active.

Installs that predate the keybinds/controller split are migrated automatically on first run: the [controller*] tables are moved out of keybinds.toml into global/controller.toml (both files backed up), so existing controller setups carry over untouched.

Bindings

The [controller] table maps gamepad buttons to the same actions and macros as [user]. Edit with .controller in the GUI. Buttons: south, east, north, west, dpad_up/down/left/right, l1, r1, l2, r2, l3, r3, select, start, guide.

[controller]
start = "interact_mode"
dpad_up = { macro_text = "n\r" }

Inside interact mode and context menus the d-pad, South, and East are fixed navigation keys; bindings apply outside those modes.

Controller-specific actions: controller_shift (hold: buttons use [controller_shift], a second bank in the same format), controller_wheel / controller_wheel:<name> (hold: radial command wheel — default ring in [[controller_wheel]], named rings in [controller_wheels.<name>]; slices take label, command, optional color, optional span and inner (geometry), and nested slices for folders — the phone client renders these same wheels for its wheel binds; the name portals is reserved: controller_wheel:portals (r3 by default — clicking l3 would nudge the movement stick into stray steps, while the aim stick’s click settles harmlessly) builds its slices from the current room’s noun exits at open time — the same list .portal resolves — shadowing any static wheel of that name), and controller_overlay (toggle the binding legend — curated by [controller_overlay] buttons, with shift/<button> entries for the shift bank). Rumble lives in [controller_rumble]: a pattern per event (off, the built-ins short/long/double, or the name of one of your own), plus custom patterns under [[controller_rumble.patterns]] — each has a name, strength (0–1), pulse_ms (length of each buzz), pulses (how many), and gap_ms (silence between them). The editor’s Rumble tab has a row per pattern with a Test button that plays it on the pad immediately, unsaved edits included. Custom patterns are also selectable on any highlight rule, so any text match can buzz the pad — see highlights.toml. Built-in names win if a custom pattern reuses one. All of it is edited in the .controller editor’s tabs — hand-editing is never required.

Interact mode and popup-menu navigation are configurable too. interact_select activates the interact focus (walk an exit, open a creature/object menu) and confirms a menu item; menu_up / menu_down / menu_left / menu_right move a menu selection; menu_cancel closes a menu. By default these live on the d-pad (navigate), south (select), and east (cancel) — binding one to another button moves the role there and frees the physical default. Two escape hatches are always guaranteed no matter how you rebind: the interact_mode button always exits interact mode, and east always cancels a menu.

Input feel lives in [controller_tuning] (Tuning tab). movement_stick ("left"/"right") chooses which stick walks the compass; the other stick aims the wheel and scrolls the story. The radial wheel is dwell-driven: aim a slice, rest on it, and it commits — leaves after aim_dwell_ms, folders and the reserved Back slice after nav_dwell_ms (folders auto-descend, Back auto-ascends). Releasing the wheel button fires the committed leaf; returning the stick to center before releasing cancels. Sweeping across the ring never commits the slices you pass through, so a far slice is safe to reach. Inside a folder a Back slice is reserved at the back_slice screen anchor (up/down/left/right and the four diagonals) — or back_slice = "none" drops the reserved seat entirely and you back out with East. deadzone (percent) is how far the stick must deflect before a slice registers; a 0 dwell means instant commit. fire_debounce_ms suppresses double-fires and release_grace_ms keeps a still-deflected stick from walking as the wheel closes. south/east remain optional accelerators while the wheel is up (fire/descend now, back up now).

fire_mode chooses how a committed leaf fires (folders always descend on dwell and are never fired by these modes; cancel is unchanged):

  • "release" (default) — the behavior above: dwell to commit, fire when the wheel button comes up.
  • "edge" — fire the instant deflection crosses edge_threshold (percent), no dwell. Fastest on sparse wheels. The re-arm-until-center guard means it fires once per hold, not repeatedly as you sweep the ring.
  • "retract" — dwell to commit, then fire as soon as deflection drops retract_delta (percent) below its peak — a small inward flick, without waiting for a full return to center. Best when recenter-based firing feels sluggish.

Both thresholds are exposed so you can tune the feel. Every field is optional and defaults to the shipped feel. Fire modes apply to the native controller (they read the analog stick); the phone client’s touch wheel is dwell/release only.

Wheel slices resolve <target_id>/<target_noun> against the interact focus, exactly like bound interact macros — so a combat wheel slice such as cast at <target_id> fires at the creature currently selected in interact mode. Slices without a placeholder are sent as-is; a slice that needs a target with nothing focused is dropped (not sent literally) with a note.

Each wheel declares, in the Wheels tab, which button opens it and which stick aims it (stored in [controller_wheels_meta.<name>]). The Wheels tab is the single place to set a wheel’s button — the controller_wheel / controller_wheel:<name> actions are no longer in the Base tab’s action dropdown to avoid two sources of truth. The reserved portals wheel has its own permanent, non-deletable Wheels-tab entry (portals (dynamic)): it exposes the same Opens with and Aim stick fields, but no slice list, because its slices are generated from the room every time it opens. The button field is a convenience: saving it writes the matching [controller] entry, which remains the runtime authority — so if the two ever disagree, [controller] wins and a note says which button really opens the wheel, and two wheels claiming one button are flagged. When a wheel’s meta doesn’t record a button (e.g. it was bound before the Wheels tab existed), the editor back-fills Opens with from [controller] so you always see the real key. The stick field overrides the global movement_stick while that wheel is open: name the movement stick and walking is silenced for the wheel’s duration; name the other and movement stays live (e.g. an exits wheel aimed with the right stick while you keep walking on the left). Left unset, a wheel aims with the non-movement stick as before.

Wheel geometry

Wedges don’t have to be even. Per-slice span fixes a wedge’s width in degrees; whatever remains of the 360° splits evenly among the span-less slices, so a wheel with no spans keeps the classic even ring. Bad numbers are never rejected — spans below 30° clamp up and a ring that doesn’t close rescales to fit, with a warning at load and in the editor telling you what was adjusted. Per-slice inner sets that slice’s aim floor as a percent of full stick deflection: below it the slice can’t be aimed or committed, so a destructive command can demand a deliberately deep throw. Unset slices use the global deadzone, and the editor caps the value below the fire thresholds so a slice always has travel left to fire. A per-wheel start in [controller_wheels_meta.<name>] rotates the whole ring (degrees, 0 = up, clockwise). Folder rings anchor to their Back seat instead and ignore start — unless back_slice = "none", in which case they rotate with it too. The portals wheel always keeps an even ring, since its slices are rebuilt per room.

Inside a folder, the “go up a level” seat is normally synthesized for you and pinned at the back_slice anchor set globally in [controller_tuning] (the Tuning tab). That anchor is one of the eight compass directions, or none to drop the synthesized Back from every folder — you then ascend with the East/B button. To place the seat yourself on one ring instead, mark a slice back = true: it becomes a real slice you can position, size, color, and floor like any other — dwelling it still ascends instead of firing, and it never sends a command (any command on it is ignored). A folder with an explicit Back uses your ring verbatim: the synthesized seat, the back_slice anchor, and the anchor rotation all step aside for that folder only. A back slice on the top ring does nothing (there’s no level to go up to) and is flagged; only one Back per ring is useful.

All of this is drawn and edited live in the Wheels tab’s Visual designer — see the GUI chapter — or typed exactly in its Numeric view; both edit the same wheel. On the wheel itself (designer and live alike), a slice’s colored fill covers only its activation zone — from its aim floor out to the rim — so the empty ring inside the fill is exactly the stick travel that does nothing.

highlights.toml

Text highlighting rules: coloring, sounds, line filtering (squelch), redirects, and text replacement. Edit the file directly (then .reload highlights) or use the in-app browser (.highlights / .addhighlight).

Basic Format

Each highlight is a named TOML table:

[stunned]
pattern = "You are stunned"
fg = "#ff4500"
bold = true
category = "Combat"

Patterns are regular expressions. For plain-word lists, set fast_parse = true and separate alternatives with | — this uses a much faster literal matcher:

[friends]
pattern = "Mandrill|Monolis|Chiora"
fg = "#ff00ff"
bold = true
fast_parse = true
category = "Players"

All Fields

FieldTypeDescription
patternstringRegex (or |-separated literals with fast_parse)
fg / bgcolorText / background color — hex or a palette color name
boldboolBold text
color_entire_lineboolColor the whole line, not just the match
fast_parseboolLiteral matching via Aho-Corasick (much faster)
soundstringSound file to play (in global/sounds/)
sound_volumefloatPer-sound volume override (0.0–1.0)
rumblestringController rumble pattern to play (short/long/double or a custom pattern from the Controller editor’s Rumble tab)
categorystringGrouping in the highlights browser (e.g. "Combat")
squelchboolHide matching lines entirely
silent_promptboolSuppress the prompt after squelched lines
redirect_tostringSend matching lines to this window
redirect_modestring"redirect_only" (move, default) or "redirect_copy" (show in both)
replacestringReplace matched text (supports $1, $2 capture groups)
streamstringOnly apply to lines from this stream (e.g. "thoughts")
windowstringWith replace: only replace in this window

Squelch (Filtering Spam)

Hide lines you never want to see:

[ambient_spam]
pattern = "A cool breeze|The wind blows|A leaf falls"
fast_parse = true
squelch = true
category = "Squelch"

[arrival_spam]
pattern = "^[A-Z][a-z]+ (arrives|departs)"
squelch = true
category = "Squelch"

Sounds and Rumble

[death_alert]
pattern = "appears dead"
fg = "#00ff00"
sound = "kill.wav"        # in ~/.vellum-fe/global/sounds/
sound_volume = 0.8
rumble = "double"         # buzz the controller too

See Sound Alerts. rumble names a controller vibration pattern — a built-in (short/long/double) or a custom one defined on the Controller editor’s Rumble tab (keybinds.toml). Highlight rumbles are rate-limited (one per 1.5s) so a pattern matching every line can’t vibrate the pad continuously, and the Rumble tab’s master switch silences them along with everything else.

Redirects

Route matching lines to another window:

[loot_lines]
pattern = "^You gather"
redirect_to = "loot"
redirect_mode = "redirect_copy"    # also keep it in the original window

Text Replacement

[shorten_deaths]
pattern = "The death cry of (\\w+)"
replace = "† $1"
fg = "#ff0000"

Testing

Inject a fake game line to test your patterns without waiting for the game:

.testline You are stunned for 3 rounds!

Global Toggles

Disable whole features without deleting patterns, in config.toml:

[highlights]
sounds_enabled = true
replace_enabled = true
redirect_enabled = true
coloring_enabled = true

System highlights (monsterbold, links, room names) are not affected by these toggles.

Highlight Profiles

Save and swap whole highlight sets:

.savehighlights hunting
.loadhighlights hunting
.highlightprofiles        # list saved profiles

Importing from Wrayth/StormFront

Convert an existing Wrayth or StormFront settings file:

vellum-fe import-highlights settings.xml --out my-highlights.toml

colors.toml

The central color file: a named color palette, game-text stream colors, prompt colors, UI element colors, and spell-circle colors. Edit in-app with .colors (palette), .uicolors, and .spellcolors — or edit the file and .reload colors.

Themes are a separate system — they control widget UI colors and are covered in Themes. colors.toml controls game text and palette colors, and is not switched by .settheme.

Color Palette

Named colors you can reference anywhere a color is accepted (highlights fg/bg, layout border_color, theme files, …):

[[color_palette]]
name = "Link"
color = "#477ab3"
category = "presets"
slot = 16
  • name — the name you reference elsewhere (case-insensitive)
  • color — hex value
  • category — grouping in the .colors browser
  • slot — optional terminal palette slot (16–231) for 256-color mode

In color_mode = "slot" (see config.toml), run .setpalette to load every slotted color into your terminal’s palette, and .resetpalette to undo it. The default palette pre-loads color sets for the built-in themes so theme switching works instantly in slot mode.

Stream Presets

Colors for game text streams. Values can be hex or palette names:

[presets.speech]
fg = "Speech"

[presets.roomName]
fg = "Room Name"
bg = "Room Name BG"

[presets.monsterbold]
fg = "Monsterbold"

Available presets include links, commands, speech, whisper, thought, roomName, monsterbold, familiar, voln, percWindow, and target_indicator.

Prompt Colors

Color individual prompt status characters:

[[prompt_colors]]
character = "R"   # roundtime
fg = "#ff0000"    # bg = "..." also supported ("color" is a legacy alias for fg)

Defaults cover R (roundtime), S (stunned), H (hiding), > (prompt), ! (bleeding).

UI Colors

Default colors for UI elements. Edit with .uicolors:

[ui]
command_echo_color = "#ffffff"
border_color = "#00ffff"
focused_border_color = "#ffff00"
text_color = "#ffffff"
background_color = "#000000"
selection_bg_color = "#4a4a4a"
textarea_background = "-"       # "-" or unchanged = fall through to theme

These sit in the middle of the window color chain: a per-window color in layout.toml wins, then a [ui] value you have changed applies, and anything left at its default (or set to "-") falls through to the active theme.

Spell Colors

Color active-spell indicators by spell circle. Edit with .spellcolors, add with .addspellcolor:

[[spell_colors]]
spells = [601, 602, 604, 605, 606]   # spell numbers
color = "#1c731c"                    # indicator color
bar_color = "#1c731c"                # progress bar color
text_color = "#909090"
bg_color = "#000000"

Color Values

Anywhere a color is accepted:

color1 = "#RRGGBB"      # hex (6-digit)
color2 = "#abc"         # hex (3-digit, expanded)
color3 = "Link Blue"    # palette color name from [[color_palette]]

Harmony Recipe

Generating preset colors with .harmony (or the GUI Colors editor’s Generate tab) also stores the generation recipe, so the look stays reproducible and re-tunable instead of frozen as opaque hex:

[harmony]
seed = "#bf616a"        # theme swatch the set was seeded from
background = "#2e3440"  # theme background it was generated against
scheme = "triadic"      # color-theory scheme (.harmony schemes lists them)
variance = 1.0          # hue spread: 0.7 low / 1.0 medium / 1.4 high
min_contrast = 4.5      # WCAG floor vs background: 3.0 / 4.5 / 7.0
separation = 0.09       # min perceptual distance between roles
room_title_spread = 2.5 # room title vs its background plate: 2.5 / 7.0

[harmony.pins]          # roles held verbatim while the rest regenerate
speech = "#53a684"

The recipe is written for you; there is no need to hand-edit it. It is ignored (and re-seeded) once the active theme’s background changes.

macros.toml

Macro buttons for the mobile web frontend. These are the buttons shown on your phone — the desktop client doesn’t use this file (desktop macros live in keybinds.toml).

You never need to edit this file: everything here can be created and edited from the phone itself (the + button on the macro rail). This page documents the format for hand-authoring, bulk edits, and version control.

After editing, apply with .reloadmacros — connected phones update live.

Two Button Shapes

Action button — has a command, fires immediately on tap:

[[group]]
name = "Basics"

  [[group.button]]
  label = "Look"
  command = "look"

Menu button — has options instead, tap opens a bottom-sheet picker:

  [[group.button]]
  label = "Travel"
  color = "#d9b44f"

    [[group.button.option]]
    label = "To the bank"
    command = ";go2 bank"

    [[group.button.option]]
    label = "To the gate"
    command = ";go2 gate"

command can be anything you could type — including Lich ;scripts.

Type-in Buttons (Composing Commands)

Either shape can set insert = true: instead of sending, the tap types the text into the command input, so a tray of word buttons composes phrases tap by tap. Spacing is automatic (go + secondgo second). A trailing \r means “then press Send” — it submits the whole composed line, which makes a good finisher button:

[[group]]
name = "Words"

  [[group.button]]
  label = "go"
  command = "go"
  insert = true

  [[group.button]]
  label = "second"
  command = "second"
  insert = true

  # tapping this sends e.g. "go second door" in one go
  [[group.button]]
  label = "door"
  command = "door\r"
  insert = true

In TOML basic strings "door\r" is a real carriage return — write it exactly like that. In the phone editor you never type \r: the On tap picker offers Send the command, Type into input, and Type, then send.

Type-in taps are handled entirely on the phone (the text never rounds through the server as a command), so composing is instant.

Floating Buttons

Always-visible overlay buttons on the text pane. Tap to fire; hold and drag to reposition (positions are remembered per device, not in this file):

[[floating]]
label = "Atk"
color = "#d9534f"
command = ";bigshot"
x = 0.85     # starting position, fraction of screen (0.0-1.0)
y = 0.6

Optional Fields

FieldApplies toDescription
colorbutton, optionButton face color, "#rrggbb"
confirmbutton, optionAsk before sending (for dangerous commands)
insertbutton, optionType into the command input instead of sending; trailing \r submits

Groups and the Macro Rail

Each [[group]] is a page of buttons in the phone’s macro rail; switch groups from the rail’s left-hand button.

File Locations

  • Global: ~/.vellum-fe/global/macros.toml
  • Per-character: profiles/<name>/macros.toml — replaces the global file wholesale (no merging)
  • profiles/<name>/macros-local.toml — buttons created on the phone are saved here as an overlay; your hand-written macros.toml is never rewritten by the app

Buttons created on the phone are editable from the phone; hand-written buttons are read-only there.

hotbars.toml

Named bars of buttons for the hotbars widget.

Prefer the built-in editor over hand-editing: .hotbars opens the hotbar editor in both TUI and GUI.

Location: ~/.vellum-fe/global/hotbars.toml, plus an optional per-character ~/.vellum-fe/profiles/<name>/hotbars.toml. A character bar with the same name replaces the global one entirely.

Bars and Buttons

[[bars]]
name = "default"      # referenced by hotkeybar windows in layout.toml
title = "Actions"
icon_size = 48        # optional: icon face edge in px (GUI; default 24)

[[bars.buttons]]
id = "hide"           # stable id, unique within the bar
label = "Hide"        # text shown on the button
command = "hide"      # game command sent on click or hotkey
hotkey = "alt+h"      # optional (keybinds.toml syntax); keybinds.toml wins on conflict
tooltip = "Attempt to hide"   # hover text (GUI)
category = "Stealth"  # editor grouping only

Countdowns

An optional [bars.buttons.countdown] overlays remaining seconds on the button:

[bars.buttons.countdown]
source = "roundtime"          # "roundtime" | "casttime" | "effect"

For source = "effect", name the effect to track:

[bars.buttons.countdown]
source = "effect"
category = "Buffs"            # Buffs | Debuffs | Cooldowns | ActiveSpells
name = "Celerity"
name_match = "exact"          # "exact" or "contains"

Conditional States

[[bars.buttons.states]] entries restyle a button while a condition holds. The first matching state wins. Each state has a when condition and a style:

# Show the Hide button green and relabeled while hidden
[[bars.buttons.states]]
[bars.buttons.states.when]
type = "indicator"
id = "hidden"
active = true
[bars.buttons.states.style]
label = "Hidden"
fg = "#80ff80"

# Dim it during roundtime
[[bars.buttons.states]]
[bars.buttons.states.when]
type = "rt_active"
[bars.buttons.states.style]
dim = true

Condition Types

typeFieldsMatches when
effect_activecategory, name, name_matchThe effect is up
effect_inactivecategory, name, name_matchThe effect is not up
effect_timecategory, name, name_match, cmp, secondsRemaining time compares true (e.g. cmp = "<", seconds = 30)
rt_activeRoundtime is running
ct_activeCasttime is running
indicatorid, activeA status indicator matches: standing, kneeling, sitting, prone, stunned, bleeding, hidden, invisible, webbed, joined, dead
vitalvital, cmp, value, unitA vital compares true: health/mana/stamina/spirit, cmp one of < <= > >=, unit "percent" or "absolute"
spell_affordablenumberThe bundled spell table lists static costs for that spell number and your current absolute vitals cover them. Works without Lich. Fails closed on unknown numbers, variable (formula) costs, or before vitals data arrives; unlike Lich’s affordable? it does not model feats or debuffs.
all / anyconditions = [ ... ]All / any of the nested conditions match

Style Fields

FieldEffect
labelReplace the button text
fg / bgText / background color (#rrggbb)
dimRender the button dimmed
iconIcon override for this state (see Icons below)

A state may also carry its own [bars.buttons.states.countdown] (same schema as the button-level countdown); while the state is active it replaces the button-level source — e.g. show the cooldown timer only in the “on cooldown” state. Likewise command = "..." on a state replaces the button’s command while active (literal text — ;eq ... commands are evaluated by Lich, so dynamic behavior belongs in the command itself).

Icons (GUI)

Buttons can show an image face from a registered sprite sheet. Sheets are tiled into fixed-size cells with no padding, numbered 1-based left→right then top→bottom — the barbar convention. The TUI always renders the text label; icons are ignored there, and the GUI falls back to text when no registered sheet matches.

Sheets live in one of two places:

  • Shared store (~/.vellum-fe/global/images/icons/ with an icons.toml manifest) — available to every skin, and with no skin active at all. This is the default and the right choice for personal icon sets.
  • Active skin (the skin’s [sheets] table in skin.toml, see the skin docs) — travels with the skin; a skin sheet overrides a shared sheet with the same name.

The .hotbars editor does all of this without TOML: the button form has a Face selector (Text / Icon / Icon + label), a sheet dropdown, a clickable “Pick cell from sheet” grid, grayscale/border controls, and the same icon controls on every state card. The editor’s “Icon sheets” section registers a new sheet — give it a name and an image path and it copies the image into the shared store (or, with “All skins” unticked, into the active skin) and records it in the matching manifest for you. Each state card also carries its own countdown source (“Countdown while active”) that replaces the button-level one while the state matches.

[[bars.buttons]]
id = "hide"
label = "Hide"
command = "hide"
icon_mode = "icon"            # "text" (default) | "icon" | "icon_and_label"

[bars.buttons.icon]
sheet = "rogue"               # sheet name from the skin's [sheets] table
cell = 5                      # 1-based cell index
grayscale = false             # desaturate (e.g. for a "not ready" look)
border = "#00ff00"            # optional solid border over the icon
border_width = 3              # pixels, 1-10 (default 2)
border_end = "#004400"        # optional: gradient border toward this color
border_dir = "radial"         # horizontal (default) | vertical |
                              # diagonal_down | diagonal_up | radial | square

With icon_mode = "icon" the label moves into the hover tooltip. States can swap the icon (a different cell, or the same cell grayscaled) via [bars.buttons.states.style.icon] — dimmed states automatically use the grayscale variant. The sheet entry itself — in the shared store’s icons.toml (paths relative to global/images/icons/) or a skin’s skin.toml (paths relative to the skin directory) — looks the same either way:

[sheets.rogue]
path = "icons/rogue.png"      # image path; absolute paths allowed
cell = 64                     # cell edge in pixels (default 64)

Reloading

.reload hotbars re-reads the file; the editor saves and applies immediately.

Performance Monitor

VellumFE ships with a built-in performance monitor: a window of live metrics about the client itself — how fast it draws, how much data the game is sending, how hard it’s working your CPU, and where the slow spots are. When the client feels sluggish, this window turns “it stutters sometimes” into an actual diagnosis.

Opening it

  • .performance (or .perf) toggles the monitor in any frontend.
  • The toggle_performance_stats keybind action does the same thing.
  • “Performance” is also in the window catalog (Windows menu), so you can add it as a permanent window like any other.
  • .performance dump writes a full diagnostic report to a file — see Diagnostic dumps below.

Each metric row can be shown or hidden in Settings → Performance, or in the TUI by right-clicking the monitor window. The toggles control what the window displays; collection itself is always on and costs almost nothing (a few timestamps per redraw and one CPU/memory sample per second). That’s deliberate: when the client hitches, .performance dump has the history — including the spike that already happened — even if the monitor was never open.

The TUI and GUI each show only the metrics that frontend actually measures, so the two lists differ slightly. A metric you don’t see in one frontend isn’t broken — it doesn’t apply there.

Reading the rows

Timing rows show three numbers: average, p95, and max. The average is what a typical frame/operation costs. The p95 (“95th percentile”) is the cost that 95% of samples beat — it answers “how bad is it when it’s bad?”. The max is the single worst recent sample. A low average with a low p95 and one high max means a single freak spike — usually ignorable. A low average with a high p95 means one operation in twenty is slow: that’s a real, recurring stutter worth chasing.

Rows with thresholds change color when a value crosses them — yellow for “worth a look”, red for “there’s your problem”. Rows with a small graph next to them (sparklines, toggleable in settings) show the last minute or so of history: a flat line is calm, a ramp that keeps climbing is a leak or a growing backlog, regular spikes point at something periodic.

What each stat tells you

Draws/s

How many times per second the window is actually redrawn, averaged over the last five seconds. VellumFE redraws on demand, not at a fixed frame rate — so near-zero while you’re idle is normal and good (the client is asleep). During scrolling combat expect roughly 10–60. If draws/s is pegged high while nothing is happening on screen, something is requesting repaints in a loop and burning CPU for nothing — check the CPU row, and include a .performance dump if you report it.

Render

What one repaint costs. In the TUI this is the whole terminal pass: drawing every widget plus flushing the result to the terminal. In the GUI it is the CPU cost of building and painting the frame, as reported by the UI toolkit.

Healthy values are a few milliseconds. The row turns yellow when p95 passes 10 ms and red past 25 ms — at 25 ms per frame, dragging a window visibly judders. If Render is red, look at Windows (below) to find which window is expensive, and at the spike log to see when it spikes and what was happening.

Draw (TUI only)

The widget-drawing part of Render, measured before the terminal flush. Comparing Draw against Render splits the blame: if Render is high but Draw is low, the time is going into the terminal itself (flushing cells to the emulator) — try a faster terminal emulator or a smaller window. If Draw is high too, the cost is in VellumFE’s widgets — usually one huge text window; check Windows.

Wrap (TUI only)

Time spent word-wrapping incoming text into window-width lines, in microseconds. Normally double digits. It grows with very wide windows and very long unbroken lines (walls of travel spam). High wrap times show up as sluggishness while text is arriving, not while idle.

Net

Bytes per second in and out. This is the game feed itself — during normal play expect a few KB/s in and near-zero out. Its real value is context: a Render or Parse spike that lines up with a burst on the Net sparkline means the game sent something enormous (an inventory dump, a busy room) and the client choked digesting it — which is a different problem from a spike with no network burst behind it.

Parse

What it costs to turn the game’s XML protocol into client state: per-chunk parse time (avg and p95, in microseconds), plus chunks and XML elements processed per second. Parse time should stay well under a millisecond. Elements/s is the best “how busy is the game feed” gauge — idle rooms tick along in the tens, invasions can push thousands. If parse p95 climbs into milliseconds, note what was on screen and grab a dump — that’s a protocol edge case worth reporting.

Events

How long the client takes to process each unit of incoming work, and how many are waiting. In the TUI, events are input events (keys, mouse); in the GUI, each queued server message counts as one event. Queue is the backlog right now; peak is the worst backlog since the monitor was opened (peaks reset each time it opens, so the login flood doesn’t pin them forever).

Queue near zero means the client is keeping up. A queue that grows and stays yellow/red (>10 / >50) means messages are arriving faster than they are processed — the game will feel like it’s lagging even though the network is fine. That’s a client-side problem: check CPU and Render.

CPU

VellumFE’s own CPU use, with the whole system’s in parentheses. A few percent is normal; the row warns past 30% and goes red past 70%. High VellumFE CPU with low draws/s means something is spinning without painting. High system CPU with normal VellumFE CPU means the problem is elsewhere on the machine — the client can feel laggy because it isn’t getting scheduled, and no client-side setting will fix that.

Memory

Real process memory: RSS (what the OS says the client actually occupies) with virtual size in parentheses. Steady RSS after an hour of play is what you want; the number depends on skins, fonts, and buffer sizes, so absolute values vary. What matters is the trend — RSS that climbs continuously without leveling off is a leak: note how long the session ran, save a dump, and report it. Warns at 750 MB, red at 1.5 GB.

Buffers

Total text lines held in scrollback across all windows, and how many windows exist. This is the main thing you control that drives memory: buffer_size in settings caps lines per window. Huge buffer counts with high RSS is expected; high RSS with small buffers points at something else (textures, leaks).

Uptime

How long this session has been running — mostly context for the other numbers (“RSS after 6 hours” means something different than after 6 minutes).

Windows

The three most expensive windows to draw, by average cost over their last 30 draws. This is the “which of my 20 windows is the slow one” answer: before blaming the client, check whether one window dominates — a huge unwrapped text window, a map at high zoom, a WebUI panel. Windows that haven’t drawn in the last 5 seconds drop off the list, so it reflects what’s actually on screen.

Spikes

The last 10 operations that blew past their threshold (renders over 10 ms, event processing over 10 ms, parses over 5 ms), each with a timestamp and a snapshot of what the client was doing: network bytes that second, XML elements parsed, and the event queue depth. This is the single most useful row for diagnosing “it hitched a minute ago”:

14:32:07  render    42.1 ms  18.2 KB, 312 elems, queue 27

reads as “at 14:32:07 a redraw took 42 ms while digesting an 18 KB burst with 27 messages backed up” — i.e. the game sent something huge. A spike with no bytes and no queue behind it is the client’s own fault — those are the ones worth reporting. The log keeps the 10 most recent spikes for the whole session; the timestamps tell you whether an entry is news or ancient history.

Diagnostic dumps (.performance dump)

.performance dump writes everything — every metric the current frontend measures (whether or not its row is visible), the full spike log, and per-window costs — to a timestamped file:

~/.vellum-fe/perf-dump-20260731-143512.txt

In the GUI the dump also includes a graphics internals section: allocated texture count and bytes (skins and images live here — a runaway texture count means art is being re-uploaded instead of cached), visible areas, and the DPI/zoom factors, which explain most “it looks blurry/huge on my monitor” reports.

The dump works whether or not the monitor is open — collection runs all session, so a hitch that already happened is still in the spike log. When reporting a performance problem on Discord, .performance dump right after the hitch and attach the file — it carries the whole diagnosis.

CLI Reference

vellum-fe [OPTIONS] [SUBCOMMAND]

Options

Run with no arguments to open the Launcher.

FlagDescription
--launcherOpen the graphical launcher (also the no-args default)
--launch-profile <NAME>Launch a saved launcher profile from launcher.toml
-f, --frontend <tui|gui|headless>Frontend to run (default tui); headless = core + web server only, browser is the UI
-p, --port <PORT>Lich proxy port (overrides config.toml)
--host <HOST>Lich proxy host (overrides config.toml)
--character <NAME>Character name (login + per-character profile)
--profile <NAME>Use a different profile directory than the character name
--key <KEY>Login key from Lich launcher (%key%)
--directConnect directly via eAccess (no Lich)
--account <ACCOUNT>Account name (direct mode)
--password <PASSWORD>Password (direct mode; omit to be prompted securely)
--game <GAME>World for direct mode: prime, platinum, shattered, test, dr, drplatinum, drfallen, drtest
-c, --config <FILE>Use a specific config.toml
--data-dir <DIR>Data directory (default ~/.vellum-fe; also VELLUM_FE_DIR env var)
--web-port <PORT>Enable the mobile web server on this port
--color-mode <direct|slot|indexed>Override color rendering mode
--setup-paletteLoad the terminal palette at startup (use with --color-mode slot)
--nosoundDisable the sound system entirely

Subcommands

validate-layout

Check a layout file for errors:

vellum-fe validate-layout                 # default layout for --character
vellum-fe validate-layout mylayout.toml

migrate-layout

Convert layouts from older VellumFE versions:

vellum-fe migrate-layout --src <DIR> [--out <DIR>] [--dry-run] [-v]

import-highlights

Convert a Wrayth/StormFront settings XML into highlights.toml format:

vellum-fe import-highlights settings.xml [--out FILE] [--dry-run]

Common Invocations

# Lich, most common
vellum-fe --port 8000 --character Rolfard

# Lich launcher integration
vellum-fe --port %port% --key %key%

# Direct, prompted for password
vellum-fe --direct --account MYACCT --character Rolfard --game prime

# GUI frontend
vellum-fe --frontend gui --port 8000 --character Rolfard

# TUI plus phone access
vellum-fe --port 8000 --character Rolfard --web-port 8040

# Headless: no local UI, log in from the browser
vellum-fe --frontend headless

# Debug logging
RUST_LOG=debug vellum-fe --port 8000

FAQ

General

What is VellumFE? A modern, multi-frontend client for GemStone IV built in Rust — terminal (TUI), desktop GUI, mobile web, and Android/iOS apps, all driving the same core. DragonRealms is supported by the parser and connection layer but less battle-tested.

Do I need Lich? No. VellumFE can connect through Lich (recommended, for scripting) or directly via eAccess with --direct. VellumFE itself doesn’t run scripts — use Lich for that.

Can I play without a PC at all? Yes — the Android or iOS app runs the whole client on your phone, or run --frontend headless on any machine and play from a browser. They connect via direct eAccess, or — for scripted characters — the login screen’s Lich tab attaches to a Lich session running on another machine (Lich itself never runs on the phone). See Mobile Web.

Is it free? Yes, open source: github.com/Nisugi/VellumFE.

Connection

How do I connect via Lich? Start Lich, then vellum-fe --port 8000 --character Name. VellumFE identifies as Stormfront to Lich, so scripts behave as they would under Wrayth. See First Launch.

Can I save my login credentials? Yes — use the Launcher: its “Save password” option stores the password in your OS’s secure credential store (keyring), never in a file. Storing a password in config.toml’s [connection] section also works but is plain text; if you store nothing, VellumFE prompts at startup.

Configuration

Where are config files stored? ~/.vellum-fe/ on every platform (Windows: C:\Users\you\.vellum-fe\). Override with --data-dir or the VELLUM_FE_DIR environment variable. See Configuration for the directory layout.

Can I have per-character settings? Yes — files in profiles/<name>/ override the global ones when you launch with --character (or --profile).

Can I have multiple layouts? Yes: .savelayout hunting, then .loadlayout hunting — in both the TUI and the GUI (each keeps its own set).

How do I reset to defaults? Delete the file (or the whole ~/.vellum-fe/ directory); defaults are recreated on next launch.

Features

Does VellumFE support macros? Two kinds: keyboard macros in keybinds.toml (f5 = { macro_text = "stance defensive\r" }), and tap-button macros for the phone in macros.toml.

Sound alerts? Yes — add sound = "alert.wav" to any highlight. See Sound Alerts.

Text-to-speech? Yes — set enabled = true in config.toml’s [tts] section. Navigation and volume/rate keys are in keybinds.toml.

Can I hide spammy lines? Yes — squelch = true on a highlight pattern. See highlights.toml.

Can I import my Wrayth highlights? Yes: vellum-fe import-highlights settings.xml. See the CLI Reference.

Where can I get help or show off my setup? The VellumFE Discord — help, bug reports, layout/skin/wheel showcases, beta testing, and release announcements. GitHub issues work too.

Troubleshooting

Connection

Can’t connect to Lich (“connection refused”)

  1. Make sure Lich is running and logged in
  2. Check the port Lich is listening on and match it: --port 8000

Direct eAccess authentication fails

  1. Verify credentials (test them with another client first)
  2. Delete the cached certificate and retry: remove ~/.vellum-fe/simu.pem
  3. Check ~/.vellum-fe/vellum-fe.log for details

Display

Colors look wrong

  1. Use a true-color terminal with color_mode = "direct" (Windows Terminal, kitty, alacritty, WezTerm)
  2. On 256-color terminals, set color_mode = "indexed", or "slot" plus .setpalette
  3. On Unix, check TERM is something like xterm-256color

Text or borders garbled

  1. Ensure the terminal uses UTF-8 and a font with box-drawing glyphs (Nerd Fonts work well)
  2. Try a different terminal emulator

Layout looks broken after resizing the terminal Run .resize to refit the layout, or .savelayout a size that works. vellum-fe validate-layout checks a layout file for errors.

Input

Backspace doesn’t work Your terminal sends delete instead. In keybinds.toml [user], change backspace = "cursor_backspace" to delete = "cursor_backspace".

A keybind does nothing

  1. Check the key isn’t captured by your terminal or OS
  2. Run with RUST_LOG=debug and check the log for KEY EVENT lines to see what your terminal actually sends
  3. Check for conflicts with [app]/[menu] bindings, which take priority

Highlights

Pattern doesn’t match

  1. Patterns are regexes — escape literals: \. \( \[
  2. Use (?i) for case-insensitive matching
  3. Test live with .testline some text that should match

No sound plays

  1. [sound] enabled = true and [highlights] sounds_enabled = true in config.toml
  2. The file must exist in ~/.vellum-fe/global/sounds/
  3. Launch without --nosound

Performance

Slow or high CPU

  1. Convert big |-lists of literal words to fast_parse = true
  2. Simplify complex regexes; anchor them (^...) where possible
  3. Reduce buffer_size on text windows

Startup

Crash or config error at startup

  1. The error usually names the file and line — check TOML syntax there
  2. Move the offending file aside to regenerate defaults
  3. Check ~/.vellum-fe/vellum-fe.log; run with RUST_LOG=debug for more

Still Stuck?

Open an issue at github.com/Nisugi/VellumFE/issues with your version (vellum-fe --version), OS/terminal, and the relevant log lines from ~/.vellum-fe/vellum-fe.log.

Privacy Policy

Effective date: July 8, 2026

VellumFE is an open-source client for GemStone IV. This policy covers the VellumFE applications (desktop, terminal, Android, and iOS) published by the VellumFE project.

What we collect

Nothing. VellumFE contains no analytics, telemetry, advertising, or tracking of any kind. The app makes no network connections other than the ones you explicitly initiate: connecting to the game (directly or via Lich), and — only when you run .mapdb download or use the equivalent settings button — fetching map data from GitHub.

Your login credentials

  • Your play.net account name and password are used solely to authenticate with Simutronics’ servers (eaccess.play.net) over an encrypted TLS connection, or with a Lich proxy you configure yourself.
  • If you enable Remember this login, credentials are stored locally on your device only. They are never transmitted anywhere except to the game servers during login.
  • Credentials are removed when you delete the saved login or uninstall the app.

Game traffic

Everything you type and everything the game sends is exchanged directly between your device and Simutronics’ servers (or your own Lich proxy). The VellumFE project operates no servers and cannot see your gameplay. Your interaction with the game itself is governed by Simutronics’ policies.

Local data

Settings, window layouts, highlights, and optional session logs are stored on your device and never leave it. You can delete them at any time from the app or by removing its data.

Third parties

VellumFE embeds no third-party SDKs and shares no data with third parties.

Changes

If this policy changes, the updated version will be published at this page with a new effective date.

Contact

Questions or concerns: open an issue at github.com/Nisugi/VellumFE or email nisugi.gs4@gmail.com.