Maximus BBS

Documentation for Maximus BBS — Next Generation

View on GitHub

Canned & Bounded Menus

Full reference for the [custom_menu] system: boundaries, layout, lightbar, and recipes

Every menu TOML file in config/menus/ can include a [custom_menu] table that controls how Maximus renders the canned option list. This is the mechanism that turns a plain text menu into a pixel-perfect, lightbar-driven BBS experience — and it’s entirely sysop-configurable.

This page is the authoritative reference for all [custom_menu] settings. For a quick-start on enabling lightbar navigation specifically, see Lightbar Menus.


What Custom Menus Do

The guiding idea:

This is the hybrid menu pattern: art + a reliable option list that stays correct as options change.


Canned vs. Fully Drawn

Hybrid Mode (default)

When skip_canned_menu = false, Maximus:

  1. Displays the menu file (ANSI art) first — if one is configured and applies to the caller’s help level.
  2. Then prints the canned option list on top — inside your boundary rectangle if configured, or at the cursor position if not.
[custom_menu]
skip_canned_menu = false

This is the most common setup. Your ANSI art provides the frame; Maximus fills in the options.

Fully Drawn Mode

When skip_canned_menu = true, Maximus displays the menu file and stops — the canned option list is not printed. Use this when your custom screen already contains all the menu text and you just want Maximus to handle input.

[custom_menu]
skip_canned_menu = true

If no menu file exists (or doesn’t apply to the caller’s help level), Maximus falls back to canned rendering regardless of this setting.


Boundaries

Boundaries define a rectangular area on screen where the canned option list is placed. Coordinates are 1-based and match the internal Goto(row,col) behavior. The rectangle is inclusive.

[custom_menu]
top_boundary = [8, 8]        # [row, col] — top-left corner
bottom_boundary = [20, 61]   # [row, col] — bottom-right corner

The usable area is:

Think of it like reserving a box on screen:

+------------------------------------------------------+
| (your ANSI art / custom screen / header)             |
|                                                      |
|       top_boundary ->  [8, 8]                        |
|                 +-------------------------+          |
|                 | option list goes here   |          |
|                 | inside the rectangle    |          |
|                 +-------------------------+          |
|                           <- bottom_boundary [20,61] |
|                                                      |
+------------------------------------------------------+

If boundaries are not set or invalid (top > bottom, coordinates < 1), Maximus falls back to normal unbounded canned rendering.

Bounded rendering is NOVICE-only. REGULAR and EXPERT help levels always use the classic unbounded output path regardless of boundary settings.


Title and Prompt Placement

show_title

Controls whether the menu title line is printed in bounded mode.

[custom_menu]
show_title = true
title_location = [22, 32]

prompt_location

For NOVICE menus, Maximus prints a selection prompt (typically “Select:”). Without explicit placement, the prompt can end up in awkward positions when you have a drawn screen and bounded options.

[custom_menu]
prompt_location = [23, 1]

Always set this when using boundaries — it prevents the prompt from overlapping your option area or disappearing off-screen.


Option Width

The top-level option_width key (outside [custom_menu]) controls how wide each option cell is in the canned list:

option_width = 14

This determines how many columns of options fit in your boundary. The number of columns per row is boundary_width / option_width (minimum 1).

When lightbar is enabled, the effective cell width becomes option_width + (lightbar_margin × 2), so account for the margin when sizing your boundary.


Layout Controls

These are all optional. Start with defaults and enable one at a time.

option_spacing (bool)

Adds an extra blank line between rows of options.

option_spacing = false    # default — tight rows
option_spacing = true     # extra vertical space

Visual comparison:

# option_spacing = false
A) First Option     B) Second Option
C) Third Option     D) Fourth Option

# option_spacing = true
A) First Option     B) Second Option

C) Third Option     D) Fourth Option

In bounded mode, spacing reduces how many rows fit in your boundary.

option_justify (string)

Controls how the option text is aligned within each cell.

Value Behavior
"left" A) Option Text (default)
"center" ` A) Option Text `
"right" ` A) Option Text`
option_justify = "left"

The hotkey character and label justify together as a unit — there’s no separate hotkey positioning.

Use "center" when your drawn menu has a symmetrical look and you want each cell to feel balanced. Use "left" for the most classic, legible style.

boundary_justify (string)

Controls where the entire option grid sits inside the boundary rectangle. This is not per-option — it moves the whole block.

Accepts one or two tokens:

One token (horizontal only):

Value Horizontal Vertical default
"left" Left Top
"center" Center Center
"right" Right Top

Two tokens (horizontal + vertical):

boundary_justify = "center center"
boundary_justify = "left top"
boundary_justify = "right bottom"

All nine combinations work: left/center/right × top/center/bottom.

Vertical justification only has a visible effect when there is extra vertical space in your boundary (fewer option rows than boundary height).

boundary_layout (string)

Controls the column distribution model inside the boundary.

grid (default)

Classic fixed-grid columns. Every row has the same column starting positions. If the boundary width doesn’t fit an exact number of columns, boundary_justify can shift the grid.

|[A]      [B]      [C]|
|[D]      [E]      [F]|

tight

Like grid, but the last row is justified based on how many options are actually in it. Useful when the last row has fewer items and you don’t want it stuck to the left.

|[A]      [B]      [C]|
|   [D]      [E]      |

spread

Distributes whitespace so the option list fills the boundary in both dimensions — adds computed inter-column gaps and inter-row gaps.

|[A]          [B]          [C]|
|                              |
|[D]          [E]          [F]|

spread_width

Spreads options across the boundary width only (inter-column gaps). Rows step normally with no extra vertical spacing.

|[A]          [B]          [C]|
|[D]          [E]          [F]|

spread_height

Spreads options across the boundary height only (inter-row gaps). Columns use the normal grid placement.

|[A]      [B]      [C]|
|                      |
|[D]      [E]      [F]|

Leftover handling: Spread uses integer math. When space can’t be evenly divided, the remainder is distributed according to boundary_justify.

Interaction with option_spacing:


Lightbar Settings

lightbar_menu (bool)

Enables arrow-key navigation with a highlight bar over the canned option list. Requires valid boundaries (top_boundary and bottom_boundary) and NOVICE help level. Without boundaries, this setting is silently ignored and Maximus falls back to the normal text menu.

[custom_menu]
lightbar_menu = true

lightbar_margin (int)

Padding (in characters) on each side of every lightbar item.

lightbar_margin = 1    # default

The lightbar painter reserves and paints an effective width of option_width + (lightbar_margin × 2). So if option_width = 14 and lightbar_margin = 1, each cell occupies 16 columns.

If the highlight bar reaches into your border, either enlarge the boundary or reduce option_width and/or lightbar_margin.

lightbar_color

Controls the four color states used by the lightbar painter. Each is a ["Foreground", "Background"] pair using the standard 16-color DOS palette names (case-insensitive):

[custom_menu.lightbar_color]
normal        = ["Light Gray", "Black"]
high          = ["Yellow", "Black"]
selected      = ["White", "Blue"]
high_selected = ["Yellow", "Blue"]
State What it colors
normal Non-selected option text
high The hotkey character in a non-selected option
selected The highlight bar (entire selected option)
high_selected The hotkey character inside the highlight bar

Defaults (when omitted):

Available color names: Black, Blue, Green, Cyan, Red, Magenta, Brown, Light Gray, Dark Gray, Light Blue, Light Green, Light Cyan, Light Red, Light Magenta, Yellow, White.


Recipes

Fully drawn menu (no canned list)

Your ANSI screen contains all the menu text. Maximus handles input only.

[custom_menu]
skip_canned_menu = true

Classic bounded grid

The most predictable layout. Options in a fixed box, no spreading.

[custom_menu]
top_boundary = [8, 8]
bottom_boundary = [20, 61]
prompt_location = [23, 1]

boundary_layout = "grid"
boundary_justify = "left top"
option_spacing = false
option_justify = "left"

Last row centered (tight)

When the last row has fewer options, center it instead of leaving it left-anchored.

[custom_menu]
boundary_layout = "tight"
boundary_justify = "center top"

Fill width only (spread_width)

Wide boundary, options distributed horizontally, rows near the top.

[custom_menu]
boundary_layout = "spread_width"
boundary_justify = "center top"

Options sit on the bottom edge of a large reserved area.

[custom_menu]
boundary_layout = "grid"
boundary_justify = "right bottom"

Spread full (centered both axes)

Large boundary, options evenly distributed in both dimensions.

[custom_menu]
boundary_layout = "spread"
boundary_justify = "center center"
option_spacing = false

Lightbar with custom colors

Full lightbar setup with the option grid centered.

[custom_menu]
skip_canned_menu = false
show_title = true
lightbar_menu = true
lightbar_margin = 1
top_boundary = [8, 8]
bottom_boundary = [20, 61]
title_location = [22, 32]
prompt_location = [23, 1]
option_spacing = false
option_justify = "left"
boundary_justify = "center center"
boundary_layout = "spread"

[custom_menu.lightbar_color]
normal        = ["Light Gray", "Black"]
high          = ["Yellow", "Black"]
selected      = ["White", "Blue"]
high_selected = ["Yellow", "Blue"]

Full Key Reference

Key Type Default Description
skip_canned_menu bool false Skip canned options when a menu file is displayed
show_title bool true Show the menu title in bounded mode
lightbar_menu bool false Enable lightbar navigation
lightbar_margin int 1 Padding on each side of lightbar items
top_boundary [row, col] Top-left corner of option area (1-based)
bottom_boundary [row, col] Bottom-right corner of option area (1-based)
title_location [row, col] Where to print the menu title
prompt_location [row, col] Where to print the selection prompt
option_spacing bool false Add blank rows between option rows
option_justify string "left" Per-cell text alignment: left/center/right
boundary_justify string "left top" Grid placement within boundary (H + V)
boundary_layout string "grid" Column model: grid/tight/spread/spread_width/spread_height
lightbar_color.normal [FG, BG] ["Light Gray","Black"] Non-selected option color
lightbar_color.high [FG, BG] Hotkey color in non-selected options
lightbar_color.selected [FG, BG] ["Yellow","Blue"] Highlight bar color
lightbar_color.high_selected [FG, BG] Hotkey color inside highlight bar

Troubleshooting

“My ANSI menu shows, but the option list / prompt is weird”

“The lightbar highlight bar overlaps my border”

Account for lightbar_margin consuming lightbar_margin × 2 columns per option cell. Either enlarge the boundary or reduce option_width and/or lightbar_margin.

“Vertical justification isn’t doing anything”

Your boundary may be tight to the content height (no extra vertical space to distribute). Make the boundary taller, or try boundary_layout = "spread_height".

“I enabled custom_menu and things changed more than I expected”

Start with the no-op settings and enable features one at a time:

[custom_menu]
option_spacing = false
option_justify = ""
boundary_justify = ""
boundary_layout = ""

Suggested safe progression:

  1. Start with grid + explicit bounds
  2. Add prompt_location
  3. Add boundary_justify (horizontal)
  4. Add the vertical token ("left bottom", etc.)
  5. Then experiment with tight and spread* modes

See Also