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

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.