Menus and the data-driven engine
Every menu shown to telnet / SSH / rlogin users is stored in the
database and editable from the web. There's no source code change
needed to add, remove, rename, or relink anything.
This covers the ANSI menu tree (BbsMenu/BbsMenuItem). PETSCII
(C64/128) sessions use a completely separate, sysop-buildable menu
tree at /admin/petscii-menus/ with a much smaller action-type set —
most ANSI actions (art, sixel, chat, most doors) have no PETSCII
equivalent, so the two trees are deliberately not shared. See
25 — PETSCII.
Where to edit
Admin → BBS Menus (/admin/bbs-menus/).
You'll see:
- Menu tree — entry-point menus and which submenus they goto
- All menus table with quick edit/delete
Anatomy of a menu
A menu has:
| field | meaning |
|---|---|
name |
unique key (e.g. main, games, sysop_tools) |
title |
shown in the auto-rendered banner |
prompt |
text before the input cursor (Choice:) |
ansi_screen |
raw ANSI/CP437 art shown above the menu (optional) |
is_default |
this is the entry point after login |
min_access |
hide menu from users below this access level |
Menu items
Each item has:
hotkey— single character or short sequencelabel— visible textaction_type— what to do when pickedaction_args— passed to the actionmin_access— hide if user is below thissort_order,is_visible
Action types
| action | args | what it does |
|---|---|---|
goto |
name of another menu | jump to another menu |
door |
Game.id | launch a registered door game |
exec |
shell cmd or JSON | run an external program (see doc 5) |
ansi |
slot name (welcome / weather_intro / ...) | display a sysop-defined ANSI screen |
boards |
— | message boards UI |
pm |
— | PM inbox |
pm_send |
— | compose new PM |
imsg |
— | Inter-BBS Instant Message (MSP) inbox |
imsg_send |
— | compose new Inter-BBS Instant Message |
bulletins |
— | bulletins |
echo |
— | echomail areas |
echo_post |
— | compose echomail |
files |
— | file library |
who |
— | who's online |
multinode |
— | interactive multinode chat |
oneliners |
— | last 10 callers + recent one-liners |
lastcallers |
— | full, paginated Last Callers list |
chat |
— | chat menu (IRC / shoutbox) |
rss |
— | RSS reader |
guru |
— | Ask Anet — searches the wiki (FTS5, not an LLM) |
ebooks |
— | ebook reader (Gutenberg) |
wall |
— | graffiti wall |
games |
— | game center |
dialout |
— | dial out to other BBSes |
page |
— | page sysop |
profile |
— | view profile |
edit_prof |
— | edit profile |
passwd |
— | change password |
sysop |
— | sysop tools (admin only) |
logoff |
— | end session |
A couple of action types in more detail
Most action types are self-explanatory from the table above, but two
are easy to underestimate from a one-line description:
wall— the graffiti wall (anetbbs/features/wall.py). A
shared, paginated bulletin board of short (1-2 line) messages any
user can post to, rendered as an ANSI box that adapts its width to
the caller's terminal (79 cols on standard 80-col, up to 131 wide).
Posts support Synchronet/Mystic-style|XXpipe color codes, with
an ASCII-only fallback for terminals that can't render CP437
box-drawing characters. Sysops get an extra[D]eloption to remove
a post by ID. Not networked or moderated beyond that — it's a purely
local, lightweight "who was here" wall, distinct from boards,
echomail, and the shoutbox.ebooks— the ebook reader (slugebooks,anetbbs/web/ebooks.py
for the web version,show_ebooks()inanetbbs/features/bbs_ui.py
for the terminal version — both share the same backend). Lets users
search and read free public-domain books from Project Gutenberg via
Gutendex (gutendex.com, a community JSON search API over Gutenberg's
catalog), plus a curated "classics" shelf for browsing without
searching first. Book text is fetched once per title and cached
forever (EbookCache); per-user bookmarks and reading history
persist across sessions. The terminal reader offers the same
classics/search/history/bookmarks flow as the web version, paginated
for the terminal. This action is gated per-Gamerow by a
terminal_enabledtoggle (Admin → Games) independent of the web
version's ownweb_enabledtoggle, so a sysop can offer one without
the other.
Adding a sub-menu
Click any menu's edit page → "Add a sub-menu (one click)" card.
Fill in:
- New menu key (games_arcade)
- Title (Arcade Games)
- Hotkey on this menu (A)
- Label (Arcade)
It creates the child menu, auto-adds a Q key on the child to come
back, and links from the parent. You can deep-nest as far as you
like.
Tips
- The
min_accessfield on items + menus implements
Mystic/Synchronet-style tiers. Set a user'saccess_levelin
Admin → Users → Manage. - Setting
is_default=Trueon a menu other thanmainlets you
override the entry point. - The "Add sample items" button on the menu list adds a working
weather door (curl wttr.in) + an ANSI replay so you can verify
execandansiactions work without writing your own.
Behavior at the prompt
- Single-key hotkeys. The prompt reads one keystroke and dispatches
immediately — no Enter required. Bare Enter just redraws the menu.
Backspace, arrow keys, and Ctrl-C are silently swallowed. - Screen is cleared between menus so the new menu doesn't render
on top of the previous one. Q(logoff) confirms withY/Nso a stray keystroke doesn't
drop your session.
Display codes inside ansi_screen
The menu's ansi_screen content runs through the same Synchronet
@CODE@ and Mystic |XX substitution as standalone ANSI screens.
See doc 4 — ANSI screens for the supported code
table. Color pipe-codes (|07 etc.) are unaffected.