UI plugins¶
A UI plugin extends the terminal UI without a fork. The UI is a set of named slots, and every pane in it is a plugin that fills one slot. The built-in panes use the same mechanism, so a third-party plugin can sit next to them, come before them, or replace one of them.
A UI plugin ships in a plugin package with an entry point in the winslow.tui_plugins group (see
How Winslow finds a plugin).
Write a UI plugin¶
A plugin declares a slot and a label, and builds one widget:
from textual.containers import Center
from textual.widgets import Static
from winslow.ui.plugin import Slots, UIPlugin
class SampleWidget(Center):
def compose(self):
yield Static("Hello from sample plugin!")
class SampleDashboardPlugin(UIPlugin):
slot = Slots.DASHBOARD_WORKFLOWS
label = "Sample"
def create_widget(self, context):
return SampleWidget()
create_widget returns any Textual widget. The context argument carries the state of the screen:
| The screen | The context | The useful attributes |
|---|---|---|
| Dashboard | DashboardRenderContext |
orchestrator, orchestrator_config |
| Workflow | WorkflowRenderContext |
workflow, workflow_config, orchestrator_config |
| Task info modal | TaskDetailRenderContext |
task, logs |
| Confirmation modal | WorkflowConfirmationRenderContext |
workflow_kls, form_values |
A slot with one plugin shows the widget directly. A slot with two or more plugins becomes a tab bar, and
label names each tab. When one slot of a row becomes tabbed, the other slots of that row become tabbed
too, so the row keeps one visual line.
The slots¶
Each screen declares its slots on the Slots class. Press ctrl+g on a screen and Winslow covers every
slot with its name, so you can see what each slot spans:
The dashboard slots and the built-in plugins in them:
| The slot | The built-in content |
|---|---|
DASHBOARD_WORKFLOWS |
The workflow selector. |
DASHBOARD_WORKFLOW_FORM |
The workflow form. |
DASHBOARD_SESSIONS |
The session list, with a History tab. |
DASHBOARD_LOGS |
The application logs. |
DASHBOARD_RESOURCES |
The system resources. |
The workflow screen slots:
| The slot | The built-in content |
|---|---|
TASKS_PANE |
The task list, with a History tab. |
TASK_OVERVIEW |
The overview of the selected task. |
WORKFLOW_LOGS |
The session logs. |
WORKFLOW_RESOURCES |
The system resources. |
Two slots live in modals, and not on a screen: TASK_DETAIL fills the task info modal, and
WORKFLOW_CONFIRMATION fills the confirmation modal before a workflow starts.
Order the plugins in a slot¶
The priority value orders the plugins of one slot. A lower value comes first, and the first plugin is the
first tab. Each built-in plugin starts at 5, and the next one in the same slot is one higher. The values 0
to 4 are reserved for a plugin that must come before the built-in plugins:
class SampleDashboardPlugin(UIPlugin):
slot = Slots.DASHBOARD_WORKFLOWS
label = "Sample"
priority = 6 # After the built-in workflow selector.
Two plugins with the same priority order by their plugin name. Declare a priority instead of depending on that order.
Replace a built-in plugin¶
The replace argument evicts another plugin and takes its place. The target is the qualified plugin name
(see The name of a plugin). A built-in plugin has the prefix builtin.
This example puts a mood face on the system resources pane, and keeps the built-in stat widgets:
from textual.widgets import Static
from winslow.ui.builtin_plugins.common.system_resources import (
SystemStats,
CpuStat,
MemoryStat,
)
from winslow.ui.builtin_plugins.dashboard.resources import (
DashboardSystemResourcesPlugin,
)
def _face(pct):
if pct < 30:
return "😊"
if pct < 70:
return "😰"
return "😡"
class MoodSystemStats(SystemStats):
def compose(self):
yield Static(_face(0.0), id="mood-face")
yield from super().compose()
def on_mount(self):
self.set_interval(2, self._update_mood)
def _update_mood(self):
cpu = self.query_one(CpuStat).percentage
mem = self.query_one(MemoryStat).percentage
self.query_one("#mood-face", Static).update(_face(max(cpu, mem)))
class ResourcesMoodPlugin(DashboardSystemResourcesPlugin):
label = "System Mood"
replace = "builtin.dashboard-system-resources-plugin"
def create_widget(self, context):
return MoodSystemStats()
The plugin subclasses the built-in plugin, so it inherits the slot of its target. A replacement that declares its own slot must declare the same slot as its target. A different slot is an error, because a replacement changes the content of a pane and not the layout of the screen.
A complete example¶
The winslow-sample-tui-plugin package holds the two plugins of this page:
winslow-sample-tui-plugin/
├── pyproject.toml
└── winslow_sample_tui_plugin/
├── dashboard.py # adds a tab
└── mood.py # replaces the system resources pane
The pyproject.toml declares the entry points:
[project]
name = "winslow-sample-tui-plugin"
version = "0.1.0"
description = "Sample Winslow plugin: two UI plugins"
requires-python = ">=3.12"
dependencies = ["winslow"]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["winslow_sample_tui_plugin"]
[project.entry-points."winslow.tui_plugins"]
winslow-sample-tui-plugin = "winslow_sample_tui_plugin"
Install the package into the workflow project. An editable install keeps a local plugin live while you work on it:
The command adds the dependency and a local source to the pyproject.toml of the project:
[project]
dependencies = [
"winslow-sample-tui-plugin",
]
[tool.uv.sources]
winslow-sample-tui-plugin = { path = "../winslow-sample-tui-plugin", editable = true }
A plugin package can also add a command to the filter language (see Filter plugins).