Skip to content

Surfaces

A plugin declares where it may appear and hands over a description of what it wants. Podlibre builds the widgets from a fixed vocabulary and sends back events. A plugin cannot draw anything Podlibre cannot name — which is the price of never being able to freeze the window, and the reason writing a plugin needs no knowledge of a widget toolkit.

Where a plugin can land

Surface Lands Typical use
screen A full page in the content area, opened from a workflow step An importer
editor_pane A pane in the episode editor, beside tracks and transcript Chapters
side_panel The inspector on the right, while a given screen is open Audio filters
dialog A modal form, for a short question Publishing settings
palette Commands in the command palette, invisible until searched A show-notes writer
field_editor The widget used wherever a field it declares is edited Any plugin adding a field

The widget vocabulary

Widget For
heading, text Saying something
input, url, number, choice, switch Asking something
list Rows the user can pick from
button Running one of your commands
progress A task's state, bound to a task id
separator Shape

A widget with an id contributes its value to the command it triggers. Strings beginning @ are looked up in the plugin's own translation catalogue.

A list marked selectable = true contributes what the user chose. It arrives under selection_as if the widget names one, and under <id>_selected otherwise — so a list called episodes whose command wants guids says:

[[widgets]]
kind         = "list"
id           = "episodes"
selectable   = true
selection_as = "guids"

Saying what a row looks like

Podlibre fills a list from the key of the same name in whatever your command returned. It does not know what your rows contain, so the view says which key to show, which to add after it, and which identifies the row:

[[widgets]]
kind        = "list"
id          = "episodes"
selectable  = true
selection_as = "guids"
text_key    = "title"                    # the row's main text
detail_keys = ["pub_date", "duration"]   # appended after it, separated by dots
value_key   = "guid"                     # what the selection sends to the command

The defaults are text_key = "title" and value_key = "id", with no details. A row that is a plain string rather than a table is drawn as itself and sends itself.

A command is called with the arguments it accepts, and no others. A screen offers every field on it, and most commands want two of them; passing the lot would mean adding a field to a view breaks a command that never asked for it. A command taking **kwargs gets everything. A missing required argument still fails loudly, because that is a real mistake rather than a tidy-up.

What comes back

A command's return value is matched to the view by name: a key called episodes fills the list with that id, a key called progress moves that bar. Two keys are special:

Key Effect
message Written under the screen as the status line

Podlibre does not compose that sentence for you, and it should not: only your plugin knows whether twelve means episodes, minutes or failures, and only your catalogue has the words in the user's language. Translate it yourself and return it:

return {"episodes": rows,
        "message": ctx.t("import.found_count", count=len(rows))}

A command that returns nothing leaves the status line empty, which is the right outcome for a command whose effect is visible on screen anyway.

The escape hatch, and who gets it

Built-in plugins that need real drawing — the editor's waveform, the player's scrubber — are marked native and do run in the UI process. That flag is not available to installed plugins. The two that use it ship with Podlibre and are reviewed like the shell itself.

If the vocabulary cannot express something your plugin genuinely needs, that is worth an issue: the intended answer is a new surface kind that everyone gets, not an exception for one plugin.