diff --git a/SYS/result_table.py b/SYS/result_table.py index 9cf23b2..7ab94bc 100644 --- a/SYS/result_table.py +++ b/SYS/result_table.py @@ -1838,7 +1838,13 @@ def extract_item_metadata(item: Any) -> Dict[str, Any]: # Use existing extractors from match-standard result table columns title = extract_title_value(item) - if title: out["Title"] = title + if title: + out["Title"] = title + else: + # Fallback for raw dicts + data = _as_dict(item) or {} + t = data.get("title") or data.get("name") or data.get("TITLE") + if t: out["Title"] = t hv = extract_hash_value(item) if hv: out["Hash"] = hv @@ -1852,10 +1858,18 @@ def extract_item_metadata(item: Any) -> Dict[str, Any]: if path: out["Path"] = path ext = extract_ext_value(item) - if ext: out["Ext"] = ext + if ext: + out["Ext"] = ext + else: + e = data.get("ext") or data.get("extension") + if e: out["Ext"] = e size = extract_size_bytes_value(item) - if size: out["Size"] = size + if size: + out["Size"] = size + else: + s = data.get("size") or data.get("size_bytes") + if s: out["Size"] = s # Duration dur = _get_first_dict_value(data, ["duration_seconds", "duration"]) @@ -1864,11 +1878,17 @@ def extract_item_metadata(item: Any) -> Dict[str, Any]: # URL url = _get_first_dict_value(data, ["url", "URL"]) - if url: out["Url"] = url + if url: + out["Url"] = url + else: + out["Url"] = None # Explicitly None for display # Relationships rels = _get_first_dict_value(data, ["relationships", "rel"]) - if rels: out["Relations"] = rels + if rels: + out["Relations"] = rels + else: + out["Relations"] = None # Tags Summary tags = _get_first_dict_value(data, ["tags", "tag"]) @@ -1901,10 +1921,10 @@ class ItemDetailView(ResultTable): from rich.columns import Columns from rich.text import Text - # 1. Create Detail Grid - details_table = RichTable(show_header=False, box=None, padding=(0, 2), expand=True) - details_table.add_column("Key", style="bold cyan", justify="right", width=12) - details_table.add_column("Value") + # 1. Create Detail Grid (matching rich_display.py style) + details_table = RichTable.grid(expand=True, padding=(0, 2)) + details_table.add_column("Key", style="cyan", justify="right", width=15) + details_table.add_column("Value", style="white") # Canonical display order for metadata order = ["Title", "Hash", "Store", "Path", "Ext", "Size", "Duration", "Url", "Relations"] @@ -1912,9 +1932,16 @@ class ItemDetailView(ResultTable): has_details = False # Add ordered items first for key in order: - val = self.item_metadata.get(key) or self.item_metadata.get(key.lower()) or self.item_metadata.get(key.upper()) + val = self.item_metadata.get(key) + if val is None: + val = self.item_metadata.get(key.lower()) + if val is None: + val = self.item_metadata.get(key.upper()) # Special formatting for certain types + if key == "Title" and val: + val = f"[bold]{val}[/bold]" + if key == "Size" and val and isinstance(val, (int, float, str)) and str(val).isdigit(): val = _format_size(int(val), integer_only=False) @@ -1924,7 +1951,7 @@ class ItemDetailView(ResultTable): else: val = "\n".join([f"[dim]→[/dim] {r}" for r in val]) - if val: + if val is not None and val != "": details_table.add_row(f"{key}:", str(val)) has_details = True elif key in ["Url", "Relations"]: @@ -1936,7 +1963,8 @@ class ItemDetailView(ResultTable): for k, v in self.item_metadata.items(): k_norm = k.lower() if k_norm not in [x.lower() for x in order] and v and k_norm not in ["tags", "tag"]: - details_table.add_row(f"{k.capitalize()}:", str(v)) + label = k.capitalize() if len(k) > 1 else k.upper() + details_table.add_row(f"{label}:", str(v)) has_details = True # Tags Summary @@ -1950,32 +1978,47 @@ class ItemDetailView(ResultTable): details_table.add_row("Tags:", tag_cols) has_details = True - # 2. Get the standard table render + # 2. Get the standard table render (if there are rows or a specific title) original_title = self.title original_header_lines = self.header_lines self.title = "" self.header_lines = [] - try: - results_renderable = super().to_rich() - finally: + results_renderable = None + # We only show the results panel if there's data OR if the user explicitly set a title (cmdlet mode) + if self.rows or original_title: self.title = original_title - self.header_lines = original_header_lines - + try: + results_renderable = super().to_rich() + finally: + self.title = "" # Keep it clean for element assembly + # 3. Assemble components elements = [] if has_details: - elements.append(Panel(details_table, title="Item Details", border_style="blue")) + elements.append(Panel( + details_table, + title="[bold green]Item Details[/bold green]", + border_style="green", + padding=(1, 2) + )) - # Wrap the results in a titled panel - display_title = "Items" - if original_title: - display_title = original_title - - # Add a bit of padding - results_group = Group(Text(""), results_renderable, Text("")) - - elements.append(Panel(results_group, title=display_title, border_style="green")) + if results_renderable: + # If it's a Panel already (from super().to_rich() with title), use it directly + # but force the border style to green for consistency + if isinstance(results_renderable, Panel): + results_renderable.border_style = "green" + # Add a bit of padding inside if it contains a table + elements.append(results_renderable) + else: + # Wrap the raw table/text in a titled panel + display_title = "Items" + if original_title: + display_title = original_title + + # Add a bit of padding + results_group = Group(Text(""), results_renderable, Text("")) + elements.append(Panel(results_group, title=display_title, border_style="green")) return Group(*elements) diff --git a/SYS/rich_display.py b/SYS/rich_display.py index 75299e9..9925702 100644 --- a/SYS/rich_display.py +++ b/SYS/rich_display.py @@ -262,100 +262,20 @@ def render_image_to_console(image_path: str | Path, max_width: int | None = None def render_item_details_panel(item: Dict[str, Any]) -> None: - """Render a comprehensive details panel for a result item.""" - from rich.table import Table - from rich.columns import Columns - from rich.panel import Panel - - title = ( - item.get("title") - or item.get("name") - or item.get("TITLE") - or "Unnamed Item" - ) - - # Main layout table for the panel - details_table = Table.grid(expand=True, padding=(0, 2)) - details_table.add_column(style="cyan", no_wrap=True, width=15, justify="right") - details_table.add_column(style="white") - - # Canonical order - details_table.add_row("Title:", f"[bold]{title}[/bold]") + """Render a comprehensive details panel for a result item using unified ItemDetailView.""" + from SYS.result_table import ItemDetailView, extract_item_metadata - if "hash" in item or "hash_hex" in item or "file_hash" in item: - h = item.get("hash") or item.get("hash_hex") or item.get("file_hash") - details_table.add_row("Hash:", str(h)) - - if "store" in item or "table" in item: - s = item.get("store") or item.get("table") - details_table.add_row("Store:", str(s)) + metadata = extract_item_metadata(item) - if "path" in item or "target" in item: - path = item.get("path") or item.get("target") - # Only show if it doesn't look like a URL (which would go in Url row) - if path and not str(path).startswith(("http://", "https://")): - details_table.add_row("Path:", str(path)) + # Create a specialized view with no results rows (only the metadata panel) + # We set no_choice=True to hide the "#" column (not that there are any rows). + view = ItemDetailView(item_metadata=metadata).set_no_choice(True) - if "ext" in item or "extension" in item: - ext = item.get("ext") or item.get("extension") - details_table.add_row("Ext:", str(ext)) - - if "size_bytes" in item or "size" in item: - size = item.get("size_bytes") or item.get("size") - if isinstance(size, (int, float, str)) and str(size).isdigit(): - size = int(size) - if size > 1024 * 1024 * 1024: - size_str = f"{size / (1024*1024*1024):.1f} GB" - elif size > 1024 * 1024: - size_str = f"{size / (1024*1024):.1f} MB" - elif size > 1024: - size_str = f"{size / 1024:.1f} KB" - else: - size_str = f"{size} bytes" - details_table.add_row("Size:", size_str) - - # URL(s) - urls = item.get("url") or item.get("URL") or [] - if isinstance(urls, str): - urls = [urls] - valid_urls = [str(u).strip() for u in urls if str(u).strip()] - if valid_urls: - url_text = "\n".join(valid_urls) - details_table.add_row("Url:", url_text) - else: - details_table.add_row("Url:", "[dim][/dim]") - - # Tags - tags = item.get("tag") or item.get("tags") or [] - if isinstance(tags, str): - tags = [t.strip() for t in tags.split(",") if t.strip()] - if isinstance(tags, list) and tags: - tags_sorted = sorted(map(str, tags)) - tag_cols = Columns([f"[dim]#[/dim]{t}" for t in tags_sorted], equal=True, expand=True) - details_table.add_row("", "") # Spacer - details_table.add_row("Tags:", tag_cols) - - # Relationships (if any) - rels = item.get("relationships") or item.get("rel") or [] - if isinstance(rels, list) and rels: - # Check for list of dicts (from get-relationship) or list of strings - if rels and isinstance(rels[0], dict): - rel_text = "\n".join([f"[dim]→[/dim] {r.get('type','rel')}: {r.get('title','?')}" for r in rels]) - else: - rel_text = "\n".join([f"[dim]→[/dim] {r}" for r in rels]) - details_table.add_row("Relations:", rel_text) - else: - details_table.add_row("Relations:", "[dim][/dim]") - - panel = Panel( - details_table, - title=f"[bold green]Item Details[/bold green]", - border_style="green", - padding=(1, 2), - expand=True - ) + # We want to print ONLY the elements from ItemDetailView, so we don't use stdout_console().print(view) + # as that would include the (empty) results panel. + # Actually, let's just use to_rich and print it. stdout_console().print() - stdout_console().print(panel) + stdout_console().print(view.to_rich())