This commit is contained in:
2026-05-16 15:26:08 -07:00
parent 5048729b0c
commit 02d84f423e
10 changed files with 488 additions and 470 deletions
+108 -153
View File
@@ -1,229 +1,184 @@
# ResultTable system — Overview & usage
# ResultTable system: overview and usage
This document explains the `ResultTable` system used across the CLI and TUI: how tables are built, how providers integrate with them, and how `@N` selection/expansion and provider selectors work.
This document explains the `ResultTable` system used across the CLI: how tables
are built, how plugins integrate with them, and how `@N` selection, row replay,
and plugin selectors work.
## TL;DR
- `ResultTable` is the unified object used to render tabular results and drive selection (`@N`) behavior.
- Providers should return `SearchResult` objects (or dicts) and can either supply `selection_args` per row or implement a `selector()` method to handle `@N` selections.
- Table metadata (`set_table_metadata`) helps providers attach context (e.g., `provider_view`, `magnet_id`) that selectors can use.
## TL;DR
- `ResultTable` is the unified object used to render tabular results and drive `@N` behavior.
- Plugins return `SearchResult` objects or dicts and can either supply row selection args or implement a `selector()` method.
- Rows can also carry an explicit `selection_action`, which the CLI replays verbatim.
- Table metadata helps plugins attach context. Some metadata keys still use older `provider` names internally.
---
## Key concepts
- **ResultTable** (`SYS/result_table.py`)
- Renders rows as a rich table and stores metadata used for selection expansion.
- Important APIs: `add_result()`, `set_table()`, `set_source_command()`, `set_row_selection_args()`, `set_row_selection_action()`, `set_table_metadata()`, and `select_interactive()`.
### ResultTable
`SYS/result_table.py` renders rows as a rich table and stores metadata used for
selection expansion.
- **ResultRow**
- Holds columns plus `selection_args` (used for `@N` expansion) and `payload` (original object).
- Optionally stores `selection_action`, a full list of CLI tokens to run when `@N` selects this row. When present the CLI honors the explicit action instead of reconstructing it from `source_command` and `selection_args`.
Important APIs:
- `add_result()`
- `set_table()`
- `set_source_command()`
- `set_row_selection_args()`
- `set_row_selection_action()`
- `set_table_metadata()`
- **Provider selector**
- If a provider implements `selector(selected_items, ctx=..., stage_is_last=True)`, it is run first when `@N` is used; if the selector returns `True` it has handled the selection (e.g., drilling into a folder and publishing a new ResultTable).
### ResultRow
A row holds columns, payload data, selection args, and optionally a full
`selection_action` token list. When `selection_action` is present, `@N` uses it
instead of reconstructing a command from the source command and row args.
- **Pipeline / CLI expansion**
- When the user types `@N`, CLI tries provider selectors first. If none handle it, CLI re-runs `source_command + source_args + row_selection_args` (for single-selection) or pipes items downstream for multi-selection.
### Plugin selector
If a plugin implements `selector(selected_items, ctx=..., stage_is_last=True)`,
that selector runs first when `@N` is used. If the selector returns `True`, it
has handled the selection, for example by publishing a nested table.
- **Table metadata**
- `ResultTable.set_table_metadata(dict)` allows attaching provider-specific context (for example: `{"provider":"alldebrid","view":"files","magnet_id":123}`) for selectors and other code to use.
### Table metadata
`ResultTable.set_table_metadata(dict)` attaches plugin-specific context for
selectors and follow-up stages. You will still see legacy keys such as
`provider` in some metadata because parts of the runtime still consume them.
---
## How to build a table (provider pattern)
## Building a table
Typical provider flow (pseudocode):
Typical plugin flow:
```py
from SYS.result_table import ResultTable
table = ResultTable("Provider: X result").set_preserve_order(True)
table.set_table("provider_name")
table.set_table_metadata({"provider":"provider_name","view":"folders"})
table.set_source_command("search-file", ["-plugin","provider_name","query"])
table = ResultTable("Plugin: X results").set_preserve_order(True)
table.set_table("plugin_name")
table.set_table_metadata({"provider": "plugin_name", "view": "folders"})
table.set_source_command("search-file", ["-plugin", "plugin_name", "query"])
for r in results:
table.add_result(r) # r can be a SearchResult, dict, or PipeObject
for result in results:
table.add_result(result)
ctx.set_last_result_table(table, payloads)
ctx.set_current_stage_table(table)
```
Notes:
- To drive a direct `@N` re-run, call `table.set_row_selection_args(row_index, ["-open", "<id>"])`.
- For more advanced or interactive behavior (e.g., drill-into, fetch more rows), implement `provider.selector()` and return `True` when handled.
- To drive a direct replay, call `table.set_row_selection_args(row_index, ["-open", "<id>"])`.
- For drill-in or interactive behavior, implement `plugin.selector()` and return `True` when handled.
- For exact row replay, call `table.set_row_selection_action(row_index, tokens)`.
---
## Selection (@N) flow (brief)
## Selection flow
1. User enters `@N` in the CLI.
2. CLI chooses the appropriate table (overlay > last table > history) and gathers the selected payload(s).
3. `PipelineExecutor._maybe_run_class_selector()` runs provider `selector()` hooks for the provider inferred from table or payloads. If any selector returns `True`, expansion stops.
4. Otherwise, for single selections, CLI grabs `row.selection_args` and expands: `source_command + source_args + row_selection_args` and inserts it as the expanded stage. For multi-selections, items are piped downstream.
When a user enters `@N`:
1. The CLI chooses the active table.
2. It gathers the selected payloads.
3. `PipelineExecutor._maybe_run_class_selector()` runs plugin `selector()` hooks for the plugin inferred from the table or payloads.
4. If no selector handles the row, the CLI checks `row.selection_action` first.
5. If no explicit row action exists, the CLI expands `source_command + source_args + row_selection_args`.
6. Multi-selection results are piped downstream instead of being collapsed to one row replay.
---
## Columns & display
- Providers can pass a `columns` list ([(name, value), ...]) in the result dict/SearchResult to control which columns are shown and their order.
- Otherwise, `ResultTable` uses a priority list (title/store/size/ext) and sensible defaults.
- The table rendering functions (`to_rich`, `format_json`, `format_compact`) are available for different UIs.
## Columns and display
- Plugins can pass a `columns` list in the result dict or `SearchResult` to control column order and display.
- Otherwise, `ResultTable` uses sensible defaults.
- Rendering helpers such as `to_rich`, `format_json`, and `format_compact` support different CLI display paths.
---
## Provider-specific examples
## Plugin-specific examples
### AllDebrid (debrid file hosting)
### AllDebrid
AllDebrid exposes a list of magnets (folder rows) and the files inside each magnet. The provider returns `folder` SearchResults for magnets and `file` SearchResults for individual files. The provider includes a `selector()` that drills into a magnet by calling `search(..., filters={"view":"files","magnet_id":...})` and builds a new `ResultTable` of files.
AllDebrid exposes a list of magnets as folder rows and the files inside each
magnet as file rows. The plugin uses `selector()` to drill into a magnet and
publish a new `ResultTable` of files.
Example commands:
```
# List magnets in your account
```powershell
search-file -plugin alldebrid "*"
# Open magnet id 123 and list its files
search-file -plugin alldebrid -open 123 "*"
# Or expand via @ selection (selector handles drilling):
search-file -plugin alldebrid "*"
@3 # selector will open the magnet referenced by row #3 and show the file table
@3
```
Illustrative folder (magnet) SearchResult:
Illustrative magnet row:
```py
SearchResult(
table="alldebrid",
title="My Magnet Title",
path="alldebrid:magnet:123",
detail="OK",
annotations=["folder", "ready"],
media_kind="folder",
columns=[("Folder", "My Magnet Title"), ("ID", "123"), ("Status", "ready"), ("Ready", "yes")],
full_metadata={
"magnet": {...},
"magnet_id": 123,
"provider": "alldebrid",
"provider_view": "folders",
"magnet_name": "My Magnet Title",
},
table="alldebrid",
title="My Magnet Title",
path="alldebrid:magnet:123",
media_kind="folder",
full_metadata={
"magnet_id": 123,
"provider": "alldebrid",
"provider_view": "folders",
},
)
```
4. Otherwise, for single selections, CLI checks for `row.selection_action` and runs that verbatim if present; otherwise it expands `source_command + source_args + row_selection_args`. For multi-selections, items are piped downstream.
Illustrative file row:
```py
SearchResult(
table="alldebrid",
title="Episode 01.mkv",
path="https://.../unlocked_direct_url",
detail="My Magnet Title",
annotations=["file"],
media_kind="file",
size_bytes=123456789,
columns=[("File", "Episode 01.mkv"), ("Folder", "My Magnet Title"), ("ID", "123")],
full_metadata={
"magnet": {...},
"magnet_id": 123,
"magnet_name": "My Magnet Title",
"relpath": "Season 1/E01.mkv",
"provider": "alldebrid",
"provider_view": "files",
"file": {...},
},
table="alldebrid",
title="Episode 01.mkv",
path="https://.../unlocked_direct_url",
media_kind="file",
full_metadata={
"magnet_id": 123,
"provider": "alldebrid",
"provider_view": "files",
},
)
```
Selection & download flows
Selection and download behavior:
- `@3` on a magnet row runs the plugin selector and shows a file table.
- `@2 | download-file` downloads the selected file row.
- `@1 | add-file -plugin local -instance <dest>` triggers add-file's plugin-aware handoff flow.
- Drill-in (selector): `@3` on a magnet row runs the provider's `selector()` to build a new file table and show it. The selector uses `search(..., filters={"view":"files","magnet_id":...})` to fetch file rows.
Configure the AllDebrid plugin in `.config plugins` and add its API key before
testing these flows.
- `download-file` integration: With a file row (http(s) path), `@2 | download-file` will download the file. The `download-file` cmdlet expands AllDebrid magnet folders and will call the provider layer to fetch file bytes as appropriate.
### Bandcamp
- `add-file` convenience: Piping a file row into `add-file -plugin local -instance <dest>` will trigger add-file's provider-aware logic. If the piped item has `table == 'alldebrid'` and a http(s) `path`, `add-file` will call `provider.download()` into a temporary directory and then ingest the downloaded file, cleaning up the temp when done. Example:
Bandcamp search supports `artist:` queries. The Bandcamp plugin implements a
`selector()` that detects artist rows and expands them into a discography table.
```
# Expand magnet and add first file to local directory
search-file -plugin alldebrid "*"
@3 # view files
@1 | add-file -plugin local -instance C:\mydir
```
Example:
Notes & troubleshooting
- Configure an AllDebrid API key (see `Provider/alldebrid._get_debrid_api_key()`).
- If a magnet isn't ready the selector or `download-file` will log the magnet status and avoid attempting file downloads.
---
### Bandcamp (artist → discography drill-in)
Bandcamp search supports `artist:` queries. Bandcamp's provider implements a `selector()` that detects `artist` results and scrapes the artist's page using Playwright to build a discography `ResultTable`.
Example usage:
```
# Search for an artist
```powershell
search-file -plugin bandcamp "artist:radiohead"
# Select an artist row to expand into releases
@1
```
Bandcamp SearchResult (artist / album rows):
```py
SearchResult(
table="bandcamp",
title="Album Title",
path="https://bandcamp.com/album_url",
detail="By: Artist",
annotations=["album"],
media_kind="audio",
columns=[("Title","Album Title"), ("Location","Artist"), ("Type","album"), ("Url","https://...")],
full_metadata={"artist":"Artist","type":"album","url":"https://..."}
)
```
Notes:
- Playwright is required for Bandcamp scraping. The selector will log an informative message if Playwright is missing.
- Provider selectors are ideal when you need to replace one table with another (artist → discography).
Plugin selectors are the right tool when you need to replace one table with
another, such as artist to discography drill-in.
---
## Provider author checklist (short)
- Implement `search(query, limit, filters)` and return `SearchResult` objects or dicts; include useful `full_metadata` (IDs, view names) for selection/drilling.
- If you support fetching downloadable file bytes, implement `download(result, output_dir) -> Optional[Path]`.
- For drill-in or interactive transforms, implement `selector(selected_items, ctx=..., stage_is_last=True)` and call `ctx.set_last_result_table(...)` / `ctx.set_current_stage_table(...)`; return `True` when handled.
- Add tests (unit/integration) that exercise search select download flows.
## Plugin author checklist
- Implement `search(query, limit, filters)` and return `SearchResult` objects or dicts.
- Include useful `full_metadata` values for drill-in and selection replay.
- Implement `download(result, output_dir)` when the plugin can materialize file bytes.
- Implement `selector(...)` for drill-in or interactive transforms.
- Add tests that cover search, select, and download flows.
---
## Debugging tips
- Use `ctx.set_last_result_table(table, payloads)` to immediately show a table while developing a selector.
- Add `log(...)` messages in provider code to capture fail points.
- Check `full_metadata` attached to SearchResults to pass extra context (IDs, view names, provider names).
- Use `ctx.set_last_result_table(table, payloads)` to publish a table while developing a selector.
- Add `log(...)` messages in plugin code to capture fail points.
- Inspect `full_metadata` when a selector or replay path is missing required context.
---
## Quick reference
- ResultTable location: `SYS/result_table.py`
- Pipeline helpers: `SYS/pipeline.py` (`set_last_result_table`, `set_current_stage_table`, `get_current_stage_table_row_selection_args`)
- CLI expansion: `CLI.py` (handles `@N`, provider selectors, and insertion of expanded stages)
- Provider selector pattern: Implement `.selector(selected_items, ctx=..., stage_is_last=True)` in provider class.
---
For more detail on ResultTable provider authoring, see `docs/provider_authoring.md`.
If you'd like, I can also:
- Add provider-specific examples (AllDebrid, Bandcamp) into this doc ✅
- Add a short checklist for PR reviewers when adding new providers
---
Created by GitHub Copilot (Raptor mini - Preview) — brief guide to the ResultTable system. Feedback welcome!
- ResultTable implementation: `SYS/result_table.py`
- Pipeline helpers: `SYS/pipeline.py`
- Row replay and selection flow: `SYS/pipeline.py`
- Plugin authoring guide: `docs/provider_authoring.md`