j
This commit is contained in:
@@ -1828,3 +1828,153 @@ def format_result(result: Any, title: str = "") -> str:
|
|||||||
table.add_result(result)
|
table.add_result(result)
|
||||||
|
|
||||||
return str(table)
|
return str(table)
|
||||||
|
|
||||||
|
def extract_item_metadata(item: Any) -> Dict[str, Any]:
|
||||||
|
"""Extract a comprehensive set of metadata from an item for the ItemDetailView."""
|
||||||
|
if item is None:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
out = {}
|
||||||
|
|
||||||
|
# Use existing extractors from match-standard result table columns
|
||||||
|
title = extract_title_value(item)
|
||||||
|
if title: out["Title"] = title
|
||||||
|
|
||||||
|
hv = extract_hash_value(item)
|
||||||
|
if hv: out["Hash"] = hv
|
||||||
|
|
||||||
|
store = extract_store_value(item)
|
||||||
|
if store: out["Store"] = store
|
||||||
|
|
||||||
|
# Path/Target
|
||||||
|
data = _as_dict(item) or {}
|
||||||
|
path = data.get("path") or data.get("target") or data.get("filename")
|
||||||
|
if path: out["Path"] = path
|
||||||
|
|
||||||
|
ext = extract_ext_value(item)
|
||||||
|
if ext: out["Ext"] = ext
|
||||||
|
|
||||||
|
size = extract_size_bytes_value(item)
|
||||||
|
if size: out["Size"] = size
|
||||||
|
|
||||||
|
# Duration
|
||||||
|
dur = _get_first_dict_value(data, ["duration_seconds", "duration"])
|
||||||
|
if dur:
|
||||||
|
out["Duration"] = _format_duration_hms(dur)
|
||||||
|
|
||||||
|
# URL
|
||||||
|
url = _get_first_dict_value(data, ["url", "URL"])
|
||||||
|
if url: out["Url"] = url
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
rels = _get_first_dict_value(data, ["relationships", "rel"])
|
||||||
|
if rels: out["Relations"] = rels
|
||||||
|
|
||||||
|
# Tags Summary
|
||||||
|
tags = _get_first_dict_value(data, ["tags", "tag"])
|
||||||
|
if tags: out["Tags"] = tags
|
||||||
|
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
class ItemDetailView(ResultTable):
|
||||||
|
"""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
|
||||||
|
what is being operated on (the main item) along with the selection list.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
title: str = "",
|
||||||
|
item_metadata: Optional[Dict[str, Any]] = None,
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
|
super().__init__(title, **kwargs)
|
||||||
|
self.item_metadata = item_metadata or {}
|
||||||
|
|
||||||
|
def to_rich(self):
|
||||||
|
"""Render the item details panel above the standard results table."""
|
||||||
|
from rich.table import Table as RichTable
|
||||||
|
from rich.panel import Panel
|
||||||
|
from rich.console import Group, 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")
|
||||||
|
|
||||||
|
# Canonical display order for metadata
|
||||||
|
order = ["Title", "Hash", "Store", "Path", "Ext", "Size", "Duration", "Url", "Relations"]
|
||||||
|
|
||||||
|
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())
|
||||||
|
|
||||||
|
# Special formatting for certain types
|
||||||
|
if key == "Size" and val and isinstance(val, (int, float, str)) and str(val).isdigit():
|
||||||
|
val = _format_size(int(val), integer_only=False)
|
||||||
|
|
||||||
|
if key == "Relations" and isinstance(val, list) and val:
|
||||||
|
if isinstance(val[0], dict):
|
||||||
|
val = "\n".join([f"[dim]→[/dim] {r.get('type','rel')}: {r.get('title','?')}" for r in val])
|
||||||
|
else:
|
||||||
|
val = "\n".join([f"[dim]→[/dim] {r}" for r in val])
|
||||||
|
|
||||||
|
if val:
|
||||||
|
details_table.add_row(f"{key}:", str(val))
|
||||||
|
has_details = True
|
||||||
|
elif key in ["Url", "Relations"]:
|
||||||
|
# User requested <null> for these if blank
|
||||||
|
details_table.add_row(f"{key}:", "[dim]<null>[/dim]")
|
||||||
|
has_details = True
|
||||||
|
|
||||||
|
# Add any remaining metadata not in the canonical list
|
||||||
|
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))
|
||||||
|
has_details = True
|
||||||
|
|
||||||
|
# Tags Summary
|
||||||
|
tags = self.item_metadata.get("Tags") or self.item_metadata.get("tags") or self.item_metadata.get("tag")
|
||||||
|
if tags and isinstance(tags, (list, str)):
|
||||||
|
if isinstance(tags, str):
|
||||||
|
tags = [t.strip() for t in tags.split(",") if t.strip()]
|
||||||
|
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)
|
||||||
|
has_details = True
|
||||||
|
|
||||||
|
# 2. Get the standard table render
|
||||||
|
original_title = self.title
|
||||||
|
original_header_lines = self.header_lines
|
||||||
|
self.title = ""
|
||||||
|
self.header_lines = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
results_renderable = super().to_rich()
|
||||||
|
finally:
|
||||||
|
self.title = original_title
|
||||||
|
self.header_lines = original_header_lines
|
||||||
|
|
||||||
|
# 3. Assemble components
|
||||||
|
elements = []
|
||||||
|
|
||||||
|
if has_details:
|
||||||
|
elements.append(Panel(details_table, title="Item Details", border_style="blue"))
|
||||||
|
|
||||||
|
# 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"))
|
||||||
|
|
||||||
|
return Group(*elements)
|
||||||
|
|||||||
1831
SYS/result_table_new.py
Normal file
1831
SYS/result_table_new.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -265,6 +265,7 @@ def render_item_details_panel(item: Dict[str, Any]) -> None:
|
|||||||
"""Render a comprehensive details panel for a result item."""
|
"""Render a comprehensive details panel for a result item."""
|
||||||
from rich.table import Table
|
from rich.table import Table
|
||||||
from rich.columns import Columns
|
from rich.columns import Columns
|
||||||
|
from rich.panel import Panel
|
||||||
|
|
||||||
title = (
|
title = (
|
||||||
item.get("title")
|
item.get("title")
|
||||||
@@ -274,31 +275,35 @@ def render_item_details_panel(item: Dict[str, Any]) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Main layout table for the panel
|
# Main layout table for the panel
|
||||||
details_table = Table.grid(expand=True)
|
details_table = Table.grid(expand=True, padding=(0, 2))
|
||||||
details_table.add_column(style="cyan", no_wrap=True, width=15)
|
details_table.add_column(style="cyan", no_wrap=True, width=15, justify="right")
|
||||||
details_table.add_column(style="white")
|
details_table.add_column(style="white")
|
||||||
|
|
||||||
# Basic Info
|
# Canonical order
|
||||||
details_table.add_row("Title", f"[bold]{title}[/bold]")
|
details_table.add_row("Title:", f"[bold]{title}[/bold]")
|
||||||
|
|
||||||
if "store" in item:
|
if "hash" in item or "hash_hex" in item or "file_hash" in item:
|
||||||
details_table.add_row("Store", str(item["store"]))
|
h = item.get("hash") or item.get("hash_hex") or item.get("file_hash")
|
||||||
|
details_table.add_row("Hash:", str(h))
|
||||||
|
|
||||||
if "hash" in item:
|
if "store" in item or "table" in item:
|
||||||
details_table.add_row("Hash", str(item["hash"]))
|
s = item.get("store") or item.get("table")
|
||||||
|
details_table.add_row("Store:", str(s))
|
||||||
|
|
||||||
# Metadata / Path
|
|
||||||
if "path" in item or "target" in item:
|
if "path" in item or "target" in item:
|
||||||
path = item.get("path") or item.get("target")
|
path = item.get("path") or item.get("target")
|
||||||
details_table.add_row("Path", str(path))
|
# 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))
|
||||||
|
|
||||||
if "ext" in item or "extension" in item:
|
if "ext" in item or "extension" in item:
|
||||||
ext = item.get("ext") or item.get("extension")
|
ext = item.get("ext") or item.get("extension")
|
||||||
details_table.add_row("Extension", str(ext))
|
details_table.add_row("Ext:", str(ext))
|
||||||
|
|
||||||
if "size_bytes" in item or "size" in item:
|
if "size_bytes" in item or "size" in item:
|
||||||
size = item.get("size_bytes") or item.get("size")
|
size = item.get("size_bytes") or item.get("size")
|
||||||
if isinstance(size, (int, float)):
|
if isinstance(size, (int, float, str)) and str(size).isdigit():
|
||||||
|
size = int(size)
|
||||||
if size > 1024 * 1024 * 1024:
|
if size > 1024 * 1024 * 1024:
|
||||||
size_str = f"{size / (1024*1024*1024):.1f} GB"
|
size_str = f"{size / (1024*1024*1024):.1f} GB"
|
||||||
elif size > 1024 * 1024:
|
elif size > 1024 * 1024:
|
||||||
@@ -307,33 +312,40 @@ def render_item_details_panel(item: Dict[str, Any]) -> None:
|
|||||||
size_str = f"{size / 1024:.1f} KB"
|
size_str = f"{size / 1024:.1f} KB"
|
||||||
else:
|
else:
|
||||||
size_str = f"{size} bytes"
|
size_str = f"{size} bytes"
|
||||||
details_table.add_row("Size", size_str)
|
details_table.add_row("Size:", size_str)
|
||||||
|
|
||||||
# URL(s)
|
# URL(s)
|
||||||
urls = item.get("url") or item.get("URL") or []
|
urls = item.get("url") or item.get("URL") or []
|
||||||
if isinstance(urls, str):
|
if isinstance(urls, str):
|
||||||
urls = [urls]
|
urls = [urls]
|
||||||
if isinstance(urls, list) and urls:
|
valid_urls = [str(u).strip() for u in urls if str(u).strip()]
|
||||||
url_text = "\n".join(map(str, urls))
|
if valid_urls:
|
||||||
details_table.add_row("URL(s)", url_text)
|
url_text = "\n".join(valid_urls)
|
||||||
|
details_table.add_row("Url:", url_text)
|
||||||
|
else:
|
||||||
|
details_table.add_row("Url:", "[dim]<null>[/dim]")
|
||||||
|
|
||||||
# Tags
|
# Tags
|
||||||
tags = item.get("tag") or item.get("tags") or []
|
tags = item.get("tag") or item.get("tags") or []
|
||||||
if isinstance(tags, str):
|
if isinstance(tags, str):
|
||||||
tags = [tags]
|
tags = [t.strip() for t in tags.split(",") if t.strip()]
|
||||||
if isinstance(tags, list) and tags:
|
if isinstance(tags, list) and tags:
|
||||||
# Sort and filter tags to look nice
|
|
||||||
tags_sorted = sorted(map(str, tags))
|
tags_sorted = sorted(map(str, tags))
|
||||||
# Group tags by namespace if they have them
|
|
||||||
tag_cols = Columns([f"[dim]#[/dim]{t}" for t in tags_sorted], equal=True, expand=True)
|
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("", "") # Spacer
|
||||||
details_table.add_row("Tags", tag_cols)
|
details_table.add_row("Tags:", tag_cols)
|
||||||
|
|
||||||
# Relationships (if any)
|
# Relationships (if any)
|
||||||
rels = item.get("relationships") or item.get("rel") or []
|
rels = item.get("relationships") or item.get("rel") or []
|
||||||
if isinstance(rels, list) and rels:
|
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])
|
rel_text = "\n".join([f"[dim]→[/dim] {r}" for r in rels])
|
||||||
details_table.add_row("Relations", rel_text)
|
details_table.add_row("Relations:", rel_text)
|
||||||
|
else:
|
||||||
|
details_table.add_row("Relations:", "[dim]<null>[/dim]")
|
||||||
|
|
||||||
panel = Panel(
|
panel = Panel(
|
||||||
details_table,
|
details_table,
|
||||||
|
|||||||
@@ -669,17 +669,34 @@ class Add_Tag(Cmdlet):
|
|||||||
# treat add-tag as a pipeline mutation (carry tags forward for add-file) instead of a store write.
|
# treat add-tag as a pipeline mutation (carry tags forward for add-file) instead of a store write.
|
||||||
if not store_override:
|
if not store_override:
|
||||||
store_name_str = str(store_name) if store_name is not None else ""
|
store_name_str = str(store_name) if store_name is not None else ""
|
||||||
local_mode_requested = (
|
|
||||||
(not store_name_str) or (store_name_str.upper() == "PATH")
|
is_known_backend = False
|
||||||
or (store_name_str.lower() == "local")
|
try:
|
||||||
)
|
|
||||||
is_known_backend = bool(store_name_str) and store_registry.is_available(
|
is_known_backend = bool(store_name_str) and store_registry.is_available(
|
||||||
store_name_str
|
store_name_str
|
||||||
)
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
if local_mode_requested and raw_path:
|
# If the item isn't in a configured store backend yet (e.g., store=PATH),
|
||||||
|
# treat add-tag as a pipeline mutation (carry tags forward for add-file)
|
||||||
|
# instead of a store write.
|
||||||
|
if not is_known_backend:
|
||||||
try:
|
try:
|
||||||
if Path(str(raw_path)).expanduser().exists():
|
# We allow metadata updates even if file doesn't exist locally,
|
||||||
|
# but check path existence if valid path provided.
|
||||||
|
proceed_local = True
|
||||||
|
if raw_path:
|
||||||
|
try:
|
||||||
|
if not Path(str(raw_path)).expanduser().exists():
|
||||||
|
# If path is provided but missing, we might prefer skipping?
|
||||||
|
# But for pipeline metadata, purely missing file shouldn't block tagging.
|
||||||
|
# So we allow it.
|
||||||
|
pass
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if proceed_local:
|
||||||
existing_tag_list = _extract_item_tags(res)
|
existing_tag_list = _extract_item_tags(res)
|
||||||
existing_lower = {
|
existing_lower = {
|
||||||
t.lower()
|
t.lower()
|
||||||
@@ -799,14 +816,9 @@ class Add_Tag(Cmdlet):
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if local_mode_requested:
|
|
||||||
log(
|
|
||||||
"[add_tag] Error: Missing usable local path for tagging (or provide -store)",
|
|
||||||
file=sys.stderr,
|
|
||||||
)
|
|
||||||
return 1
|
|
||||||
|
|
||||||
if store_name_str and not is_known_backend:
|
if store_name_str and not is_known_backend:
|
||||||
|
# If it's not a known backend and we didn't handle it above as a local/pipeline
|
||||||
|
# metadata edit, then it's an error.
|
||||||
log(
|
log(
|
||||||
f"[add_tag] Error: Unknown store '{store_name_str}'. Available: {store_registry.list_backends()}",
|
f"[add_tag] Error: Unknown store '{store_name_str}'. Available: {store_registry.list_backends()}",
|
||||||
file=sys.stderr,
|
file=sys.stderr,
|
||||||
|
|||||||
@@ -514,7 +514,19 @@ def _run(result: Any, _args: Sequence[str], config: Dict[str, Any]) -> int:
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
# Display results
|
# Display results
|
||||||
table = ResultTable(f"Relationships: {source_title}"
|
from SYS.result_table import ItemDetailView, extract_item_metadata
|
||||||
|
|
||||||
|
# Prepare metadata for the detail view
|
||||||
|
metadata = extract_item_metadata(result)
|
||||||
|
|
||||||
|
if hash_hex:
|
||||||
|
metadata["Hash"] = hash_hex
|
||||||
|
|
||||||
|
# Overlays
|
||||||
|
if source_title and source_title != "Unknown":
|
||||||
|
metadata["Title"] = source_title
|
||||||
|
|
||||||
|
table = ItemDetailView(f"Relationships", item_metadata=metadata
|
||||||
).init_command("get-relationship",
|
).init_command("get-relationship",
|
||||||
[])
|
[])
|
||||||
|
|
||||||
|
|||||||
@@ -322,15 +322,23 @@ def _emit_tags_as_table(
|
|||||||
This replaces _print_tag_list to make tags pipe-able.
|
This replaces _print_tag_list to make tags pipe-able.
|
||||||
Stores the table via ctx.set_last_result_table_overlay (or ctx.set_last_result_table) for downstream @ selection.
|
Stores the table via ctx.set_last_result_table_overlay (or ctx.set_last_result_table) for downstream @ selection.
|
||||||
"""
|
"""
|
||||||
from SYS.result_table import ResultTable
|
from SYS.result_table import ItemDetailView, extract_item_metadata
|
||||||
|
|
||||||
# Create ResultTable with just tag column (no title)
|
# Prepare metadata for the detail view
|
||||||
# Keep the title stable and avoid including hash fragments.
|
metadata = extract_item_metadata(subject)
|
||||||
table_title = "tag"
|
|
||||||
|
# Overlays/Overrides from explicit args if subject was partial
|
||||||
if item_title:
|
if item_title:
|
||||||
table_title = f"tag: {item_title}"
|
metadata["Title"] = item_title
|
||||||
|
if file_hash:
|
||||||
|
metadata["Hash"] = file_hash
|
||||||
|
if store:
|
||||||
|
metadata["Store"] = service_name if service_name else store
|
||||||
|
if path:
|
||||||
|
metadata["Path"] = path
|
||||||
|
|
||||||
table = ResultTable(table_title, max_columns=1)
|
# Create ItemDetailView
|
||||||
|
table = ItemDetailView("Tags", item_metadata=metadata, max_columns=1)
|
||||||
table.set_source_command("get-tag", [])
|
table.set_source_command("get-tag", [])
|
||||||
|
|
||||||
# Create TagItem for each tag
|
# Create TagItem for each tag
|
||||||
|
|||||||
@@ -421,14 +421,20 @@ class Get_Url(Cmdlet):
|
|||||||
from SYS.metadata import normalize_urls
|
from SYS.metadata import normalize_urls
|
||||||
urls = normalize_urls(urls)
|
urls = normalize_urls(urls)
|
||||||
|
|
||||||
title = str(get_field(result, "title") or "").strip()
|
from SYS.result_table import ItemDetailView, extract_item_metadata
|
||||||
table_title = "Title"
|
|
||||||
if title:
|
# Prepare metadata for the detail view
|
||||||
table_title = f"Title: {title}"
|
metadata = extract_item_metadata(result)
|
||||||
|
|
||||||
|
if file_hash:
|
||||||
|
metadata["Hash"] = file_hash
|
||||||
|
if store_name:
|
||||||
|
metadata["Store"] = store_name
|
||||||
|
|
||||||
table = (
|
table = (
|
||||||
ResultTable(
|
ItemDetailView(
|
||||||
table_title,
|
"Urls",
|
||||||
|
item_metadata=metadata,
|
||||||
max_columns=1
|
max_columns=1
|
||||||
).set_preserve_order(True).set_table("url").set_value_case("preserve")
|
).set_preserve_order(True).set_table("url").set_value_case("preserve")
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user