dfdkflj
This commit is contained in:
198
result_table.py
198
result_table.py
@@ -114,6 +114,8 @@ class ResultRow:
|
||||
columns: List[ResultColumn] = 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'])"""
|
||||
source_index: Optional[int] = None
|
||||
"""Original insertion order index (used to map sorted views back to source items)."""
|
||||
|
||||
def add_column(self, name: str, value: Any) -> None:
|
||||
"""Add a column to this row."""
|
||||
@@ -166,13 +168,14 @@ class ResultTable:
|
||||
>>> print(result_table)
|
||||
"""
|
||||
|
||||
def __init__(self, title: str = "", title_width: int = 80, max_columns: int = None):
|
||||
def __init__(self, title: str = "", title_width: int = 80, max_columns: int = None, preserve_order: bool = False):
|
||||
"""Initialize a result table.
|
||||
|
||||
Args:
|
||||
title: Optional title for the table
|
||||
title_width: Width for formatting the title line
|
||||
max_columns: Maximum number of columns to display (None for unlimited, default: 5 for search results)
|
||||
preserve_order: When True, skip automatic sorting so row order matches source
|
||||
"""
|
||||
self.title = title
|
||||
self.title_width = title_width
|
||||
@@ -187,10 +190,25 @@ 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
|
||||
"""If True, skip automatic sorting so display order matches input order."""
|
||||
self.no_choice: bool = False
|
||||
"""When True, suppress row numbers/selection to make the table non-interactive."""
|
||||
|
||||
def set_no_choice(self, no_choice: bool = True) -> "ResultTable":
|
||||
"""Mark the table as non-interactive (no row numbers, no selection parsing)."""
|
||||
self.no_choice = bool(no_choice)
|
||||
return self
|
||||
|
||||
def set_preserve_order(self, preserve: bool = True) -> "ResultTable":
|
||||
"""Configure whether this table should skip automatic sorting."""
|
||||
self.preserve_order = bool(preserve)
|
||||
return self
|
||||
|
||||
def add_row(self) -> ResultRow:
|
||||
"""Add a new row to the table and return it for configuration."""
|
||||
row = ResultRow()
|
||||
row.source_index = len(self.rows)
|
||||
self.rows.append(row)
|
||||
return row
|
||||
|
||||
@@ -210,6 +228,50 @@ class ResultTable:
|
||||
self.source_command = command
|
||||
self.source_args = args or []
|
||||
return self
|
||||
|
||||
def init_command(self, title: str, command: str, args: Optional[List[str]] = None, preserve_order: bool = False) -> "ResultTable":
|
||||
"""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)
|
||||
|
||||
Args:
|
||||
title: Table title
|
||||
command: Source command name
|
||||
args: Command arguments
|
||||
preserve_order: Whether to preserve input row order
|
||||
|
||||
Returns:
|
||||
self for method chaining
|
||||
"""
|
||||
self.title = title
|
||||
self.source_command = command
|
||||
self.source_args = args or []
|
||||
self.preserve_order = preserve_order
|
||||
return self
|
||||
|
||||
def copy_with_title(self, new_title: str) -> "ResultTable":
|
||||
"""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(...)
|
||||
Useful for intermediate processing that needs to preserve source command but update display title.
|
||||
|
||||
Args:
|
||||
new_title: New title for the copied table
|
||||
|
||||
Returns:
|
||||
New ResultTable with copied settings and new title
|
||||
"""
|
||||
new_table = ResultTable(
|
||||
title=new_title,
|
||||
title_width=self.title_width,
|
||||
max_columns=self.max_columns,
|
||||
preserve_order=self.preserve_order
|
||||
)
|
||||
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
|
||||
return new_table
|
||||
|
||||
def set_row_selection_args(self, row_index: int, selection_args: List[str]) -> None:
|
||||
"""Set the selection arguments for a specific row.
|
||||
@@ -252,6 +314,39 @@ class ResultTable:
|
||||
self.set_header_line(summary)
|
||||
return summary
|
||||
|
||||
def sort_by_title(self) -> "ResultTable":
|
||||
"""Sort rows alphabetically by Title or Name column.
|
||||
|
||||
Looks for columns named 'Title', 'Name', or 'Tag' (in that order).
|
||||
Case-insensitive sort. Returns self for chaining.
|
||||
|
||||
IMPORTANT: Updates source_index to match new sorted positions so that
|
||||
@N selections continue to work correctly after sorting.
|
||||
"""
|
||||
if getattr(self, "preserve_order", False):
|
||||
return self
|
||||
# Find the title column (try Title, Name, Tag in order)
|
||||
title_col_idx = None
|
||||
for row in self.rows:
|
||||
if not row.columns:
|
||||
continue
|
||||
for idx, col in enumerate(row.columns):
|
||||
col_lower = col.name.lower()
|
||||
if col_lower in ("title", "name", "tag"):
|
||||
title_col_idx = idx
|
||||
break
|
||||
if title_col_idx is not None:
|
||||
break
|
||||
|
||||
if title_col_idx is None:
|
||||
# No title column found, return unchanged
|
||||
return self
|
||||
|
||||
# Sort rows by the title column value (case-insensitive)
|
||||
self.rows.sort(key=lambda row: row.columns[title_col_idx].value.lower() if title_col_idx < len(row.columns) else "")
|
||||
|
||||
return self
|
||||
|
||||
def add_result(self, result: Any) -> "ResultTable":
|
||||
"""Add a result object (SearchResult, PipeObject, ResultItem, TagItem, or dict) as a row.
|
||||
|
||||
@@ -338,8 +433,7 @@ class ResultTable:
|
||||
|
||||
# Size (for files)
|
||||
if hasattr(result, 'size_bytes') and result.size_bytes:
|
||||
size_mb = result.size_bytes / (1024 * 1024)
|
||||
row.add_column("Size", f"{size_mb:.1f} MB")
|
||||
row.add_column("Size (Mb)", _format_size(result.size_bytes, integer_only=True))
|
||||
|
||||
# Annotations
|
||||
if hasattr(result, 'annotations') and result.annotations:
|
||||
@@ -385,8 +479,7 @@ class ResultTable:
|
||||
|
||||
# Size (for files) - integer MB only
|
||||
if hasattr(item, 'size_bytes') and item.size_bytes:
|
||||
size_mb = int(item.size_bytes / (1024 * 1024))
|
||||
row.add_column("Size", f"{size_mb} MB")
|
||||
row.add_column("Size (Mb)", _format_size(item.size_bytes, integer_only=True))
|
||||
|
||||
def _add_tag_item(self, row: ResultRow, item: Any) -> None:
|
||||
"""Extract and add TagItem fields to row (compact tag display).
|
||||
@@ -421,8 +514,8 @@ class ResultTable:
|
||||
row.add_column("Title", obj.title[:50] + ("..." if len(obj.title) > 50 else ""))
|
||||
|
||||
# File info
|
||||
if hasattr(obj, 'file_path') and obj.file_path:
|
||||
file_str = str(obj.file_path)
|
||||
if hasattr(obj, 'path') and obj.path:
|
||||
file_str = str(obj.path)
|
||||
if len(file_str) > 60:
|
||||
file_str = "..." + file_str[-57:]
|
||||
row.add_column("Path", file_str)
|
||||
@@ -467,8 +560,8 @@ class ResultTable:
|
||||
def is_hidden_field(field_name: Any) -> bool:
|
||||
# Hide internal/metadata fields
|
||||
hidden_fields = {
|
||||
'__', 'id', 'action', 'parent_id', 'is_temp', 'file_path', 'extra',
|
||||
'target', 'hash', 'hash_hex', 'file_hash'
|
||||
'__', 'id', 'action', 'parent_id', 'is_temp', 'path', 'extra',
|
||||
'target', 'hash', 'hash_hex', 'file_hash', 'tags', 'tag_summary', 'name'
|
||||
}
|
||||
if isinstance(field_name, str):
|
||||
if field_name.startswith('__'):
|
||||
@@ -551,15 +644,12 @@ class ResultTable:
|
||||
|
||||
# Only add priority groups if we haven't already filled columns from 'columns' field
|
||||
if column_count == 0:
|
||||
# Priority field groups - uses first matching field in each group
|
||||
# Explicitly set which columns to display in order
|
||||
priority_groups = [
|
||||
('title | name | filename', ['title', 'name', 'filename']),
|
||||
('title', ['title']),
|
||||
('ext', ['ext']),
|
||||
('origin | source | store', ['origin', 'source', 'store']),
|
||||
('size | size_bytes', ['size', 'size_bytes']),
|
||||
('type | media_kind | kind', ['type', 'media_kind', 'kind']),
|
||||
('tags | tag_summary', ['tags', 'tag_summary']),
|
||||
('detail | description', ['detail', 'description']),
|
||||
('size', ['size', 'size_bytes']),
|
||||
('store', ['store', 'origin', 'source']),
|
||||
]
|
||||
|
||||
# Add priority field groups first - use first match in each group
|
||||
@@ -568,14 +658,22 @@ class ResultTable:
|
||||
break
|
||||
for field in field_options:
|
||||
if field in visible_data and field not in added_fields:
|
||||
value_str = format_value(visible_data[field])
|
||||
# Special handling for size fields - format as MB integer
|
||||
if field in ['size', 'size_bytes']:
|
||||
value_str = _format_size(visible_data[field], integer_only=True)
|
||||
else:
|
||||
value_str = format_value(visible_data[field])
|
||||
|
||||
if len(value_str) > 60:
|
||||
value_str = value_str[:57] + "..."
|
||||
|
||||
# Special case for Origin/Source -> Store to match user preference
|
||||
col_name = field.replace('_', ' ').title()
|
||||
if field in ['origin', 'source']:
|
||||
# Map field names to display column names
|
||||
if field in ['store', 'origin', 'source']:
|
||||
col_name = "Store"
|
||||
elif field in ['size', 'size_bytes']:
|
||||
col_name = "Size (Mb)"
|
||||
else:
|
||||
col_name = field.replace('_', ' ').title()
|
||||
|
||||
row.add_column(col_name, value_str)
|
||||
added_fields.add(field)
|
||||
@@ -583,17 +681,7 @@ class ResultTable:
|
||||
break # Use first match in this group, skip rest
|
||||
|
||||
# Add remaining fields only if we haven't hit max_columns (and no explicit columns were set)
|
||||
if column_count < self.max_columns:
|
||||
for key, value in visible_data.items():
|
||||
if column_count >= self.max_columns:
|
||||
break
|
||||
if key not in added_fields: # Only add if not already added
|
||||
value_str = format_value(value)
|
||||
if len(value_str) > 40:
|
||||
value_str = value_str[:37] + "..."
|
||||
row.add_column(key.replace('_', ' ').title(), value_str)
|
||||
added_fields.add(key) # Track in added_fields to prevent re-adding
|
||||
column_count += 1
|
||||
# Don't add any remaining fields - only use priority_groups for dict results
|
||||
|
||||
# Check for selection args
|
||||
if '_selection_args' in data:
|
||||
@@ -637,8 +725,8 @@ class ResultTable:
|
||||
value_width
|
||||
)
|
||||
|
||||
# Calculate row number column width
|
||||
num_width = len(str(len(self.rows))) + 1 # +1 for padding
|
||||
# Calculate row number column width (skip if no-choice)
|
||||
num_width = 0 if self.no_choice else len(str(len(self.rows))) + 1
|
||||
|
||||
# Preserve column order
|
||||
column_names = list(col_widths.keys())
|
||||
@@ -647,7 +735,7 @@ class ResultTable:
|
||||
cap = 5 if name.lower() == "ext" else 90
|
||||
return min(col_widths[name], cap)
|
||||
|
||||
widths = [num_width] + [capped_width(name) for name in column_names]
|
||||
widths = ([] if self.no_choice else [num_width]) + [capped_width(name) for name in column_names]
|
||||
base_inner_width = sum(widths) + (len(widths) - 1) * 3 # account for " | " separators
|
||||
|
||||
# Compute final table width (with side walls) to accommodate headers/titles
|
||||
@@ -668,7 +756,7 @@ class ResultTable:
|
||||
# Title block
|
||||
if self.title:
|
||||
lines.append("|" + "=" * (table_width - 2) + "|")
|
||||
lines.append(wrap(self.title.center(table_width - 2)))
|
||||
lines.append(wrap(self.title.ljust(table_width - 2)))
|
||||
lines.append("|" + "=" * (table_width - 2) + "|")
|
||||
|
||||
# Optional header metadata lines
|
||||
@@ -676,8 +764,8 @@ class ResultTable:
|
||||
lines.append(wrap(meta))
|
||||
|
||||
# Add header with # column
|
||||
header_parts = ["#".ljust(num_width)]
|
||||
separator_parts = ["-" * num_width]
|
||||
header_parts = [] if self.no_choice else ["#".ljust(num_width)]
|
||||
separator_parts = [] if self.no_choice else ["-" * num_width]
|
||||
for col_name in column_names:
|
||||
width = capped_width(col_name)
|
||||
header_parts.append(col_name.ljust(width))
|
||||
@@ -688,7 +776,7 @@ class ResultTable:
|
||||
|
||||
# Add rows with row numbers
|
||||
for row_num, row in enumerate(self.rows, 1):
|
||||
row_parts = [str(row_num).ljust(num_width)]
|
||||
row_parts = [] if self.no_choice else [str(row_num).ljust(num_width)]
|
||||
for col_name in column_names:
|
||||
width = capped_width(col_name)
|
||||
col_value = row.get_column(col_name) or ""
|
||||
@@ -785,6 +873,11 @@ 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:
|
||||
print(f"\n{self}")
|
||||
print("Selection is disabled for this table.")
|
||||
return None
|
||||
|
||||
# Display the table
|
||||
print(f"\n{self}")
|
||||
|
||||
@@ -832,6 +925,9 @@ class ResultTable:
|
||||
Returns:
|
||||
List of 0-based indices, or None if invalid
|
||||
"""
|
||||
if self.no_choice:
|
||||
return None
|
||||
|
||||
indices = set()
|
||||
|
||||
# Split by comma for multiple selections
|
||||
@@ -1206,14 +1302,15 @@ def _format_duration(duration: Any) -> str:
|
||||
return ""
|
||||
|
||||
|
||||
def _format_size(size: Any) -> str:
|
||||
def _format_size(size: Any, integer_only: bool = False) -> str:
|
||||
"""Format file size as human-readable string.
|
||||
|
||||
Args:
|
||||
size: Size in bytes or already formatted string
|
||||
integer_only: If True, show MB as integer only (e.g., "250 MB" not "250.5 MB")
|
||||
|
||||
Returns:
|
||||
Formatted size string (e.g., "1.5 MB", "250 KB")
|
||||
Formatted size string (e.g., "250 MB", "1.5 MB" or "250 MB" if integer_only=True)
|
||||
"""
|
||||
if isinstance(size, str):
|
||||
return size if size else ""
|
||||
@@ -1223,11 +1320,22 @@ def _format_size(size: Any) -> str:
|
||||
if bytes_val < 0:
|
||||
return ""
|
||||
|
||||
for unit, divisor in [("GB", 1024**3), ("MB", 1024**2), ("KB", 1024)]:
|
||||
if bytes_val >= divisor:
|
||||
return f"{bytes_val / divisor:.1f} {unit}"
|
||||
|
||||
return f"{bytes_val} B"
|
||||
if integer_only:
|
||||
# For table display: always show as integer MB if >= 1MB
|
||||
mb_val = int(bytes_val / (1024 * 1024))
|
||||
if mb_val > 0:
|
||||
return str(mb_val)
|
||||
kb_val = int(bytes_val / 1024)
|
||||
if kb_val > 0:
|
||||
return str(kb_val)
|
||||
return str(bytes_val)
|
||||
else:
|
||||
# For descriptions: show with one decimal place
|
||||
for unit, divisor in [("GB", 1024**3), ("MB", 1024**2), ("KB", 1024)]:
|
||||
if bytes_val >= divisor:
|
||||
return f"{bytes_val / divisor:.1f} {unit}"
|
||||
|
||||
return f"{bytes_val} B"
|
||||
except (ValueError, TypeError):
|
||||
return ""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user