greetings
 
You are here:

bbsengine6.listbox — scrollable list widget

Status: canonical. The widget lives at py/src/bbsengine6/listbox.py. handbook/listbox.md is the older user-facing reference (with Op instead of ListboxResult, keyhandler instead of custom_keys); this spec is the single source of truth for the live API.

bbsengine6.listbox is a paginated, curses-style TUI listbox with keyboard navigation, item highlighting, idle callbacks, and per-item key handlers. It is the building block for every interactive BBS prompt (member pickers, blurb lists, channel browser, etc.).

Contents

Classes

Class Purpose
ListboxItem Single row in the list. Carries content, pk, data, disabled, optional onkey callback
ListboxResult NamedTuple(status, item=None, data=None). status is one of "selected", "cancelled", "noitems", "redraw", "custom"
Listbox Main widget. Holds items, currentitem, currentindex, curpage, pos, the key_handlers dict, and the prompt string
ListboxCursor Helper class (see py/src/bbsengine6/listboxcursor.py) for cursor positioning math

Constructor

Listbox(
    args: Any,
    title: str = "",
    itemsperpage: int = 20,
    itemheight: int = 1,
    items: Optional[List[ListboxItem]] = None,
    idle: Optional[Callable[[], Optional[ListboxResult] | bool]] = None,
    custom_keys: Optional[Dict[str, Callable[[], Optional[ListboxResult]]]] = None,
    **kwargs,
) -> None

args is the application's argparse.Namespace (used for args.debug gates). The standard handlers are populated first; custom_keys are merged in via key_handlers.update(custom_keys), so callers can override standard keys ("KEY_UP", "KEY_DOWN", …) by passing the same key name.

Public API

Member Type Notes
items List[ListboxItem] Full list passed in __init__
currentitem property → Optional[ListboxItem] Currently selected item
currentindex property → int Index into the current page (0-based)
curpage property → int Current page (0-based)
pos int Cursor position within the current page
prompt str Set inside run(prompt)
key_handlers Dict[str, Callable[[], bool | Optional[ListboxResult]]] Effective key → handler mapping
fetchitems() -> List[ListboxItem] method Returns items for the current page; override in subclass
_highlight_item(idx) method Repaints item at idx as highlighted
onkey(ch) -> Optional[ListboxResult] | bool method Dispatch a keypress
run(prompt) -> ListboxResult method Drive the widget loop

Configuration

Field Type Default Description
itemsperpage int 20 Number of items per page (disabled or otherwise)
itemheight int 1 Uniform height of each item in rows
GETCH_TIMEOUT float 0.25 Key input timeout in seconds
BOTTOM_BORDER_HEIGHT int 1 Height of the bottom border line
CONTENT_PADDING int 4 Horizontal padding for item content (borders + spacing)
BORDER_WIDTH_LEFT int 3 Left border width
BORDER_WIDTH_RIGHT int 3 Right border width
BORDER_CORNER_WIDTH int 2 Width reduction for border lines (space + vline each side)
title str "" Title displayed at top of widget
idle callable None Called on idle keypress; see onkey() semantics
custom_keys Dict[str, Callable[[], Optional[ListboxResult]]] None Merged into key_handlers after the standard handlers

note: listbox_next is a UI widget component, not a bbsengine6 module. It is exempt from the standard lifecycle functions (detect, draw, init, buildargs, access, main).

Key handlers

Key Method Action
KEY_ESC _handle_key_esc Returns ListboxResult("cancelled")
KEY_ENTER _handle_key_enter Returns ListboxResult("selected", currentitem); False if disabled
KEY_UP _handle_key_up Previous enabled item (skip disabled; wrap to previous page)
KEY_DOWN _handle_key_down Next enabled item (skip disabled; wrap to next page)
KEY_PAGEUP _handle_key_pageup Previous page
KEY_PAGEDOWN _handle_key_pagedown Next page
KEY_HOME _handle_key_home First enabled item on the current page
KEY_END _handle_key_end Last enabled item on the current page

custom_keys entries are merged on top of the standard handlers, so a caller can override "KEY_UP" / "KEY_DOWN" by passing the same key in custom_keys.

Item colors

The itemcolors dict determines per-item color:

itemcolors = {
    "disabled":   "{bggray}",
    "highlighted": "{bgwhite}{black}",
    "normal":     "{normalcolor}",
}

Render logic:

Width and height math

terminalwidth = terminal.width()
contentwidth  = terminalwidth - BORDER_WIDTH_LEFT - BORDER_WIDTH_RIGHT
totalwidth    = contentwidth + BORDER_WIDTH_LEFT + BORDER_WIDTH_RIGHT
hline         = "{hline:" + str(contentwidth - BORDER_CORNER_WIDTH) + "}"

Left border: " {vline} " (BORDER_WIDTH_LEFT chars). Right border: same shape, BORDER_WIDTH_RIGHT chars. The asymmetric border widths let a caller intentionally bias left vs right content.

Border layout:

Content height (excluding borders) = itemsperpage × itemheight.

run() lifecycle

def run(self, prompt: str) -> ListboxResult:
    if not self.items:
        return ListboxResult("noitems")
    self.prompt = prompt
    self._display()
    io.echo(f"{{savecursor}} {promptcolor}{prompt}{{cha}}", end="", flush=True)
    while True:
        result = self.onkey(io.getch(GETCH_TIMEOUT))
        if isinstance(result, ListboxResult):
            if result.status == "redraw":
                # full redraw (title + content + borders + prompt)
                ...
                continue
            return result
        if result is True:
            io.echo("{restorecursor}", end="", flush=True)
        elif result is False:
            io.echo("{BEL}", end="", flush=True)

ListboxResult("redraw") is a request from a key handler for a complete refresh (title box, content, borders, prompt). The current item selection is preserved across a redraw.

onkey() semantics

onkey(ch) -> Optional[ListboxResult] | bool

If ch is None and idle is callable:

For non-standard keys, onkey delegates to currentitem.onkey(currentitem, key). The callback returns True (key handled) or False (key not handled → bell). Common uses: 'e' for edit, 'd' for delete, 'r' for refresh.

Multi-column (future)

bbsengine6/listbox_feature_multicolumn.md describes a multi-column layout extension:

The multi-column layout is not yet implemented in bbsengine6.listbox. The single-column widget above is the only live surface.