# ResultTable system — Overview & 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. ## 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. --- ## 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()`. - **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`. - **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). - **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. - **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. --- ## How to build a table (provider pattern) Typical provider flow (pseudocode): ```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", ["-provider","provider_name","query"]) for r in results: table.add_result(r) # r can be a SearchResult, dict, or PipeObject 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", ""])`. - For more advanced or interactive behavior (e.g., drill-into, fetch more rows), implement `provider.selector()` and return `True` when handled. --- ## Selection (@N) flow (brief) 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. --- ## 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. --- ## Provider-specific examples ### AllDebrid (debrid file hosting) 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. Example commands: ``` # List magnets in your account search-file -provider alldebrid "*" # Open magnet id 123 and list its files search-file -provider alldebrid -open 123 "*" # Or expand via @ selection (selector handles drilling): search-file -provider alldebrid "*" @3 # selector will open the magnet referenced by row #3 and show the file table ``` Illustrative folder (magnet) SearchResult: ```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", }, ) ``` 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. ```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": {...}, }, ) ``` Selection & download flows - 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. - `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. - `add-file` convenience: Piping a file row into `add-file -path ` 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: ``` # Expand magnet and add first file to local directory search-file -provider alldebrid "*" @3 # view files @1 | add-file -path C:\mydir ``` 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 search-file -provider 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). --- ## 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. --- ## 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). --- ## 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!