This commit is contained in:
2026-01-18 10:50:42 -08:00
parent 66132811e0
commit 66e6c6eb72
34 changed files with 718 additions and 516 deletions

View File

@@ -341,7 +341,7 @@ class TUIResultCard:
@dataclass
class ResultColumn:
class Column:
"""Represents a single column in a result table."""
name: str
@@ -361,10 +361,10 @@ class ResultColumn:
@dataclass
class ResultRow:
class Row:
"""Represents a single row in a result table."""
columns: List[ResultColumn] = field(default_factory=list)
columns: List[Column] = field(default_factory=list)
selection_args: Optional[List[str]] = None
"""Arguments to use for this row when selected via @N syntax (e.g., ['-item', '3'])"""
selection_action: Optional[List[str]] = None
@@ -398,7 +398,7 @@ class ResultRow:
if formatted:
str_value = formatted
self.columns.append(ResultColumn(normalized_name, str_value))
self.columns.append(Column(normalized_name, str_value))
def get_column(self, name: str) -> Optional[str]:
"""Get column value by name."""
@@ -420,7 +420,7 @@ class ResultRow:
return " | ".join(str(col) for col in self.columns)
class ResultTable:
class Table:
"""Unified table formatter for search results, metadata, and pipeline objects.
Provides a structured way to display results in the CLI with consistent formatting.
@@ -491,7 +491,7 @@ class ResultTable:
self.max_columns = (
max_columns if max_columns is not None else 5
) # Default 5 for cleaner display
self.rows: List[ResultRow] = []
self.rows: List[Row] = []
self.column_widths: Dict[str,
int] = {}
self.input_options: Dict[str,
@@ -503,9 +503,9 @@ class ResultTable:
"""Base arguments for the source command"""
self.header_lines: List[str] = []
"""Optional metadata lines rendered under the title"""
self.preserve_order: bool = preserve_order
self.perseverance: bool = preserve_order
"""If True, skip automatic sorting so display order matches input order."""
self.no_choice: bool = False
self.interactive: bool = False
"""When True, suppress row numbers/selection to make the table non-interactive."""
self.table: Optional[str] = None
"""Table type (e.g., 'youtube', 'soulseek') for context-aware selection logic."""
@@ -516,7 +516,7 @@ class ResultTable:
self.value_case: str = "lower"
"""Display-only value casing: 'lower' (default), 'upper', or 'preserve'."""
def set_value_case(self, value_case: str) -> "ResultTable":
def set_value_case(self, value_case: str) -> "Table":
"""Configure display-only casing for rendered cell values."""
case = str(value_case or "").strip().lower()
if case not in {"lower",
@@ -535,12 +535,12 @@ class ResultTable:
return text
return text.lower()
def set_table(self, table: str) -> "ResultTable":
def set_table(self, table: str) -> "Table":
"""Set the table type for context-aware selection logic."""
self.table = table
return self
def set_table_metadata(self, metadata: Optional[Dict[str, Any]]) -> "ResultTable":
def set_table_metadata(self, metadata: Optional[Dict[str, Any]]) -> "Table":
"""Attach provider/table metadata for downstream selection logic."""
self.table_metadata = dict(metadata or {})
return self
@@ -552,19 +552,19 @@ class ResultTable:
except Exception:
return {}
def set_no_choice(self, no_choice: bool = True) -> "ResultTable":
def _interactive(self, interactive: bool = True) -> "Table":
"""Mark the table as non-interactive (no row numbers, no selection parsing)."""
self.no_choice = bool(no_choice)
self.interactive = bool(interactive)
return self
def set_preserve_order(self, preserve: bool = True) -> "ResultTable":
def _perseverance(self, perseverance: bool = True) -> "Table":
"""Configure whether this table should skip automatic sorting."""
self.preserve_order = bool(preserve)
self.perseverance = bool(perseverance)
return self
def add_row(self) -> ResultRow:
def add_row(self) -> Row:
"""Add a new row to the table and return it for configuration."""
row = ResultRow()
row = Row()
row.source_index = len(self.rows)
self.rows.append(row)
return row
@@ -573,7 +573,7 @@ class ResultTable:
self,
command: str,
args: Optional[List[str]] = None
) -> "ResultTable":
) -> "Table":
"""Set the source command that generated this table.
This is used for @N expansion: when user runs @2 | next-cmd, it will expand to:
@@ -596,7 +596,7 @@ class ResultTable:
command: str,
args: Optional[List[str]] = None,
preserve_order: bool = False,
) -> "ResultTable":
) -> "Table":
"""Initialize table with title, command, args, and preserve_order in one call.
Consolidates common initialization pattern: ResultTable(title) + set_source_command(cmd, args) + set_preserve_order(preserve_order)
@@ -613,10 +613,10 @@ class ResultTable:
self.title = title
self.source_command = command
self.source_args = args or []
self.preserve_order = preserve_order
self.perseverance = preserve_order
return self
def copy_with_title(self, new_title: str) -> "ResultTable":
def copy_with_title(self, new_title: str) -> "Table":
"""Create a new table copying settings from this one but with a new title.
Consolidates pattern: new_table = ResultTable(title); new_table.set_source_command(...)
@@ -628,16 +628,16 @@ class ResultTable:
Returns:
New ResultTable with copied settings and new title
"""
new_table = ResultTable(
new_table = Table(
title=new_title,
title_width=self.title_width,
max_columns=self.max_columns,
preserve_order=self.preserve_order,
preserve_order=self.perseverance,
)
new_table.source_command = self.source_command
new_table.source_args = list(self.source_args) if self.source_args else []
new_table.input_options = dict(self.input_options) if self.input_options else {}
new_table.no_choice = self.no_choice
new_table.interactive = self.interactive
new_table.table = self.table
new_table.table_metadata = (
dict(self.table_metadata) if getattr(self, "table_metadata", None) else {}
@@ -663,12 +663,12 @@ class ResultTable:
if 0 <= row_index < len(self.rows):
self.rows[row_index].selection_action = selection_action
def set_header_lines(self, lines: List[str]) -> "ResultTable":
def set_header_lines(self, lines: List[str]) -> "Table":
"""Attach metadata lines that render beneath the title."""
self.header_lines = [line for line in lines if line]
return self
def set_header_line(self, line: str) -> "ResultTable":
def set_header_line(self, line: str) -> "Table":
"""Attach a single metadata line beneath the title."""
return self.set_header_lines([line] if line else [])
@@ -699,7 +699,7 @@ class ResultTable:
self.set_header_line(summary)
return summary
def sort_by_title(self) -> "ResultTable":
def sort_by_title(self) -> "Table":
"""Sort rows alphabetically by Title or Name column.
Looks for columns named 'Title', 'Name', or 'Tag' (in that order).
@@ -737,7 +737,7 @@ class ResultTable:
return self
def add_result(self, result: Any) -> "ResultTable":
def add_result(self, result: Any) -> "Table":
"""Add a result object (SearchResult, PipeObject, ResultItem, TagItem, or dict) as a row.
Args:
@@ -793,7 +793,7 @@ class ResultTable:
return payloads
@classmethod
def from_api_table(cls, api_table: Any) -> "ResultTable":
def from_api_table(cls, api_table: Any) -> "Table":
"""Convert a strict SYS.result_table_api.ResultTable into an interactive monolith ResultTable.
This allows providers using the new strict API to benefit from the monolith's
@@ -831,7 +831,7 @@ class ResultTable:
return instance
def _add_result_model(self, row: ResultRow, result: ResultModel) -> None:
def _add_result_model(self, row: Row, result: ResultModel) -> None:
"""Extract and add ResultModel fields from the new API to row."""
row.add_column("Title", result.title)
@@ -848,7 +848,7 @@ class ResultTable:
# Add a placeholder for metadata-like display if needed in the main table
# but usually metadata is handled by the detail panel now
def _add_search_result(self, row: ResultRow, result: Any) -> None:
def _add_search_result(self, row: Row, result: Any) -> None:
"""Extract and add SearchResult fields to row."""
cols = getattr(result, "columns", None)
used_explicit_columns = False
@@ -925,7 +925,7 @@ class ResultTable:
if selection_action:
row.selection_action = [str(a) for a in selection_action if a is not None]
def _add_result_item(self, row: ResultRow, item: Any) -> None:
def _add_result_item(self, row: Row, item: Any) -> None:
"""Extract and add ResultItem fields to row (compact display for search results).
Shows only essential columns:
@@ -970,7 +970,7 @@ class ResultTable:
if hasattr(item, "size_bytes") and item.size_bytes:
row.add_column("Size", _format_size(item.size_bytes, integer_only=False))
def _add_tag_item(self, row: ResultRow, item: Any) -> None:
def _add_tag_item(self, row: Row, item: Any) -> None:
"""Extract and add TagItem fields to row (compact tag display).
Shows the Tag column with the tag name and Source column to identify
@@ -986,7 +986,7 @@ class ResultTable:
if hasattr(item, "source") and item.source:
row.add_column("Store", item.source)
def _add_pipe_object(self, row: ResultRow, obj: Any) -> None:
def _add_pipe_object(self, row: Row, obj: Any) -> None:
"""Extract and add PipeObject fields to row."""
# Source and identifier
if hasattr(obj, "source") and obj.source:
@@ -1019,7 +1019,7 @@ class ResultTable:
warnings_str += f" (+{len(obj.warnings) - 2} more)"
row.add_column("Warnings", warnings_str)
def _add_dict(self, row: ResultRow, data: Dict[str, Any]) -> None:
def _add_dict(self, row: Row, data: Dict[str, Any]) -> None:
"""Extract and add dict fields to row using first-match priority groups.
Respects max_columns limit to keep table compact and readable.
@@ -1251,7 +1251,7 @@ class ResultTable:
# Don't display it
added_fields.add("_selection_args")
def _add_generic_object(self, row: ResultRow, obj: Any) -> None:
def _add_generic_object(self, row: Row, obj: Any) -> None:
"""Extract and add fields from generic objects."""
if hasattr(obj, "__dict__"):
for key, value in obj.__dict__.items():
@@ -1282,7 +1282,7 @@ class ResultTable:
show_lines=False,
)
if not self.no_choice:
if not self.interactive:
table.add_column("#", justify="right", no_wrap=True)
# Render headers in uppercase, but keep original column keys for lookup.
@@ -1301,7 +1301,7 @@ class ResultTable:
for row_idx, row in enumerate(self.rows, 1):
cells: List[str] = []
if not self.no_choice:
if not self.interactive:
cells.append(str(row_idx))
for name in col_names:
val = row.get_column(name) or ""
@@ -1381,7 +1381,7 @@ class ResultTable:
"""Iterate over rows."""
return iter(self.rows)
def __getitem__(self, index: int) -> ResultRow:
def __getitem__(self, index: int) -> Row:
"""Get row by index."""
return self.rows[index]
@@ -1410,7 +1410,7 @@ class ResultTable:
If accept_args=False: List of 0-based indices, or None if cancelled
If accept_args=True: Dict with "indices" and "args" keys, or None if cancelled
"""
if self.no_choice:
if self.interactive:
from SYS.rich_display import stdout_console
stdout_console().print(self)
@@ -1494,7 +1494,7 @@ class ResultTable:
Returns:
List of 0-based indices, or None if invalid
"""
if self.no_choice:
if self.interactive:
return None
indices = set()
@@ -1604,7 +1604,7 @@ class ResultTable:
"args": cmdlet_args
}
def add_input_option(self, option: InputOption) -> "ResultTable":
def add_input_option(self, option: InputOption) -> "Table":
"""Add an interactive input option to the table.
Input options allow users to specify cmdlet arguments interactively,
@@ -1708,7 +1708,7 @@ class ResultTable:
result[name] = value
return result
def select_by_index(self, index: int) -> Optional[ResultRow]:
def select_by_index(self, index: int) -> Optional[Row]:
"""Get a row by 1-based index (user-friendly).
Args:
@@ -1740,7 +1740,7 @@ class ResultTable:
return rows
def _format_datatable_row(self,
row: ResultRow,
row: Row,
source: str = "unknown") -> List[str]:
"""Format a ResultRow for DataTable display.
@@ -1769,7 +1769,7 @@ class ResultTable:
cards.append(card)
return cards
def _row_to_card(self, row: ResultRow) -> TUIResultCard:
def _row_to_card(self, row: Row) -> TUIResultCard:
"""Convert a ResultRow to a TUIResultCard.
Args:
@@ -1886,7 +1886,7 @@ def format_result(result: Any, title: str = "") -> str:
Returns:
Formatted string
"""
table = ResultTable(title)
table = Table(title)
if isinstance(result, list):
for item in result:
@@ -1997,7 +1997,7 @@ def extract_item_metadata(item: Any) -> Dict[str, Any]:
return out
class ItemDetailView(ResultTable):
class ItemDetailView(Table):
"""A specialized view that displays item details alongside a list of related items (tags, urls, etc).
This is used for 'get-tag', 'get-url' and similar cmdlets where we want to contextually show