161 lines
5.2 KiB
Markdown
161 lines
5.2 KiB
Markdown
# Output Rendering Framework
|
|
|
|
The output rendering framework decouples command output data from its visual presentation. Every CLI command produces structured output through a common abstraction layer, and the active **format** determines how that output is rendered to the terminal or piped to external consumers.
|
|
|
|
## Architecture
|
|
|
|
All CLI output flows through a five-stage reactive pipeline:
|
|
|
|
1. **Command Logic** opens an `OutputSession` and creates typed **element handles**.
|
|
2. **OutputSession** coordinates handles, tracks their lifecycle, and emits events to the active materialisation strategy.
|
|
3. **ElementHandles** are the producer-facing API — thread-safe, format-agnostic write interfaces.
|
|
4. **MaterializationStrategy** decides *when* and *how* to render content.
|
|
5. **Terminal / Pipe** receives the final byte stream.
|
|
|
|
## Quick Start
|
|
|
|
```python
|
|
from cleveragents.cli.output import OutputSession
|
|
|
|
with OutputSession(format="plain") as session:
|
|
panel = session.panel("Project Details")
|
|
panel.set_entry("Name", "local/api-service")
|
|
panel.set_entry("Status", "active")
|
|
panel.close()
|
|
|
|
table = session.table(columns=["Resource", "Type"])
|
|
table.add_row({"Resource": "api-repo", "Type": "git-checkout"})
|
|
table.close()
|
|
```
|
|
|
|
## OutputSession
|
|
|
|
The `OutputSession` is the central coordinator. It is used as a context manager and provides factory methods for each element type:
|
|
|
|
- `session.panel(title)` → `PanelHandle`
|
|
- `session.table(columns=[...])` → `TableHandle`
|
|
- `session.status(message)` → `StatusHandle`
|
|
- `session.progress(label)` → `ProgressHandle`
|
|
|
|
The session can be created with:
|
|
|
|
- `format` — the desired output format (`rich`, `color`, `table`, `plain`, `json`, `yaml`)
|
|
- `command` — the command name for metadata
|
|
- `strategy` — an explicit materialisation strategy (overrides format auto-selection)
|
|
|
|
## Element Handles
|
|
|
|
### PanelHandle
|
|
|
|
Key-value pair output with a title.
|
|
|
|
```python
|
|
panel = session.panel("Details")
|
|
panel.set_entry("Name", "my-project")
|
|
panel.set_entries({"A": "1", "B": "2"})
|
|
panel.remove_entry("A")
|
|
panel.close()
|
|
```
|
|
|
|
### TableHandle
|
|
|
|
Tabular data with typed columns.
|
|
|
|
```python
|
|
table = session.table(columns=["Name", "Status", "Count"])
|
|
table.add_row({"Name": "svc-1", "Status": "up", "Count": "42"})
|
|
table.add_rows([{"Name": "svc-2", "Status": "down", "Count": "0"}])
|
|
table.set_summary({"total": "42"})
|
|
table.set_sort_key("Name")
|
|
table.close()
|
|
```
|
|
|
|
### StatusHandle
|
|
|
|
Single-line status messages with level indicators.
|
|
|
|
```python
|
|
status = session.status("Processing...", level="info")
|
|
status.set_message("Done")
|
|
status.set_level("ok")
|
|
status.set_detail("All 42 items processed")
|
|
status.close()
|
|
```
|
|
|
|
### ProgressHandle
|
|
|
|
Progress bars and spinners with throttled updates (max 10 FPS in rich mode).
|
|
|
|
```python
|
|
prog = session.progress("Uploading", total=100)
|
|
prog.set_progress(50, 100)
|
|
prog.tick()
|
|
prog.increment(10)
|
|
prog.set_label("Finalising")
|
|
prog.set_indeterminate()
|
|
prog.set_step_status("validate", "done")
|
|
prog.close()
|
|
```
|
|
|
|
## Materialisation Strategies
|
|
|
|
| Format | Strategy | Behaviour |
|
|
|----------|------------------------|-------------------------------------------------|
|
|
| `rich` | `RichMaterializer` | In-place terminal updates via Rich library |
|
|
| `color` | `ColorMaterializer` | ANSI codes, scrolling output (no cursor moves) |
|
|
| `table` | `TableMaterializer` | ASCII box-drawing for tables |
|
|
| `plain` | `PlainMaterializer` | ASCII-only, no ANSI, no Unicode box characters |
|
|
| `json` | `JsonMaterializer` | Accumulate-then-serialize as JSON |
|
|
| `yaml` | `YamlMaterializer` | Accumulate-then-dump as YAML |
|
|
|
|
### Automatic Fallback
|
|
|
|
When terminal capabilities are insufficient, the framework automatically falls back:
|
|
|
|
- `rich` → `color` → `plain` (based on TTY, ANSI, cursor support)
|
|
- `color` → `plain` (when no ANSI support)
|
|
- `table` → `plain` (when not a TTY)
|
|
- `json` / `yaml` — always available (no terminal needed)
|
|
|
|
Use `--format <value>` to explicitly set a format and skip fallback.
|
|
|
|
## Error Envelope
|
|
|
|
JSON and YAML materializers support a unified error envelope:
|
|
|
|
```json
|
|
{
|
|
"error": {
|
|
"code": "NOT_FOUND",
|
|
"message": "Resource not found",
|
|
"details": {"id": "abc-123"}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Producer Integration Guide
|
|
|
|
### Writing Format-Agnostic Commands
|
|
|
|
Commands should create handles and write data without knowing which format is active:
|
|
|
|
```python
|
|
def my_command(session: OutputSession, items: list[Item]) -> None:
|
|
panel = session.panel("Summary")
|
|
panel.set_entry("Total", str(len(items)))
|
|
panel.close()
|
|
|
|
table = session.table(columns=["Name", "Status"])
|
|
for item in items:
|
|
table.add_row({"Name": item.name, "Status": item.status})
|
|
table.close()
|
|
```
|
|
|
|
### Thread Safety
|
|
|
|
Multiple handles can be written concurrently from different threads. The session ensures elements are rendered in declaration order even when handles close out of order.
|
|
|
|
### Backward Compatibility
|
|
|
|
The existing `format_output()` function in `cleveragents.cli.formatting` continues to work unchanged. A new `format_output_session()` function is also available that routes through the `OutputSession` framework.
|