PANELITH DOCS Open the interactive docs ↗

Guide · Panelith 0.1.0 · plain HTML edition; the interactive edition has search and live examples

Lists and the drawer

On this page 4 sections
  1. Declaring a list
  2. Live lists: the server searches
  3. A list in a pane
  4. The cycler

A ListSource provides rows. The drawer on the right shows them with search, facets and a status mark; the command bar shows the same rows under the source's sigil; a ListPane renders them as a table in a pane. These docs' Contents is one: the drawer button in the top bar, the ? sigil and the search box all read it.

Declaring a list#

class LongWords(ListSource):
    id = "words"
    title = "Long words"
    sigil = "/"                   # type / in the bar to search this list
    columns = ["word", "length"]  # strings, or ListColumn(key, label, width, grow, mono)
    search = ["word"]
    facets = ["length"]
    row_action = "words.open"     # a command id, run with {"row": row}
    empty_text = "No word matches"

    def rows(self, ctx):
        return [{"id": w, "word": w, "length": len(w)} for w in LONG_WORDS]


class OpenWord(Command):
    id = "words.open"
    title = "Words: Open"

    def run(self, ctx):
        word = (ctx.args.get("row") or {}).get("word")
        return a.open_pane("word", name=word.upper(), state={"word": word})
The Contents list under its sigil. Rows arrive from the server already ranked, with an excerpt.

Rows are plain dicts with an id. search names the fields the search box matches; facets names fields that get a filter button each, with counts; status is a ListStatus(key, marks, tones) mapping a field's values to a glyph and a tone, which Math, You uses for solved and half-solved problems. rows(ctx) is called when the drawer opens and whenever a handler returns refresh_list("words"), so a list can reflect state that changes while the app runs. The reference lists every attribute.

The row action receives the row in ctx.args["row"]. It can open a pane, send an event to an existing one, or anything else a command can do.

TopBar(buttons=["list:contents", "commands", "hotkeys"])    # the list button
a.run_builtin("list:contents")                                # the same toggle, from a handler
The drawer: a search box, a facet per declared field, the order toggle, and the rows. A row runs the list's row_action.

Live lists: the server searches#

By default the shell fetches a list's rows once and searches them itself with a fuzzy, multi-term matcher: good for a few thousand rows. For a search the server must do (a full-text index, a database, an API), set live = True and override search_rows(ctx, query). Every keystroke in the bar or the drawer then fetches /api/lists/<id>?q=… (debounced, last answer wins) and the shell shows the rows in the order it got them, with no filtering of its own. snippet names a row key shown dim under the row, for an excerpt.

class Contents(ListSource):
    id = "contents"
    title = "Contents"
    sigil = "?"
    columns = [ListColumn(key="title", grow=True), ListColumn(key="where", width=170)]
    facets = ["section"]
    live = True
    snippet = "snippet"
    row_action = "docs.open"

    def rows(self, ctx):               # the table of contents, with no query
        return [...]

    def search_rows(self, ctx, query): # ranked hits, with a snippet each
        return INDEX.search(query) if query.strip() else self.rows(ctx)

The default search_rows is a case-insensitive substring match over the search fields, so live = True alone already moves the work to the server. Facets still work on whatever rows come back.

A list in a pane#

ListPane is a Pane preset to the table widget: set source = "words" and its initial state is the list's columns and rows, so one definition serves the drawer and a pane. A table pane's row click is a row event the kind handles itself; the drawer's row click runs row_action.

class Planets(Pane):
    kind = "planets"
    label = "Planets"
    glyph = "table"
    family = "assessment"
    widget = "table"
    tip = "A sortable table; a row click is an event"

    def initial_state(self, ctx):
        return widgets.table(PLANETS, columns={"planet": "Planet", "moons": "Moons", "au": "Distance (AU)"})

    @event("row")
    def row(self, ctx, payload):
        row = payload.get("row") or {}
        return a.notify(f"{row.get('planet')}: {row.get('moons')} moons", "info")

The cycler#

A kind with cycler = "<list id>" gets ‹ › buttons in its header. Each click sends the pane a cycle event with {"delta": ±1, "list": id}; what that means (the next problem, the previous file) is the kind's decision. Open the header variants to see one:

class Problem(Pane):
    cycler = "problems"

    def on_event(self, ctx, name, payload):
        if name == "cycle":
            return self.step(ctx, payload["delta"])