f
This commit is contained in:
@@ -16,6 +16,8 @@ from typing import Any, Iterator, Sequence, TextIO
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
from rich.text import Text
|
||||
from pathlib import Path
|
||||
from SYS.utils import expand_path
|
||||
|
||||
# Configure Rich pretty-printing to avoid truncating long strings (hashes/paths).
|
||||
# This is version-safe: older Rich versions may not support the max_* arguments.
|
||||
@@ -93,3 +95,170 @@ def show_store_config_panel(
|
||||
"""Show a Rich panel explaining how to configure a storage backend."""
|
||||
pass
|
||||
|
||||
|
||||
IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".tiff"}
|
||||
|
||||
|
||||
def render_image_to_console(image_path: str | Path, max_width: int | None = None) -> None:
|
||||
"""Render an image to the console using Rich and half-block characters.
|
||||
|
||||
Requires Pillow (PIL). If not available or loading fails, does nothing.
|
||||
"""
|
||||
try:
|
||||
from PIL import Image
|
||||
from rich.color import Color
|
||||
from rich.style import Style
|
||||
from rich.text import Text
|
||||
|
||||
# Ensure we expand environment variables and resolve to an absolute path.
|
||||
path = expand_path(image_path).resolve()
|
||||
if not path.exists() or not path.is_file():
|
||||
# If absolute path fails, try relative to current directory as a last resort.
|
||||
path = Path(image_path).resolve()
|
||||
if not path.exists() or not path.is_file():
|
||||
return
|
||||
|
||||
with Image.open(path) as img:
|
||||
img = img.convert("RGB")
|
||||
orig_w, orig_h = img.size
|
||||
|
||||
# Determine target dimensions
|
||||
console = stdout_console()
|
||||
if max_width is None:
|
||||
max_width = console.width - 4
|
||||
|
||||
# Ensure max_width is at least something reasonable
|
||||
if max_width < 10:
|
||||
max_width = 80
|
||||
|
||||
# Terminal cells are approx 2x taller than wide.
|
||||
# Half-block characters (upper half block) let us treat one cell as two vertical pixels.
|
||||
aspect = orig_h / orig_w
|
||||
target_w = min(orig_w, max_width)
|
||||
|
||||
# Ensure target_h is even so we always have pairs of vertical pixels
|
||||
target_h = int(target_w * aspect)
|
||||
if target_h < 2:
|
||||
target_h = 2
|
||||
target_h = (target_h // 2) * 2
|
||||
|
||||
# Ensure we don't exceed a reasonable height either (e.g. 40 characters)
|
||||
max_height_chars = 40
|
||||
if (target_h // 2) > max_height_chars:
|
||||
target_h = max_height_chars * 2
|
||||
target_w = int(target_h / aspect)
|
||||
target_h = (target_h // 2) * 2 # Re-ensure even
|
||||
|
||||
if target_w < 1: target_w = 1
|
||||
|
||||
img = img.resize((target_w, target_h), Image.Resampling.BILINEAR)
|
||||
pixels = img.load()
|
||||
|
||||
# Render using upper half block (U+2580)
|
||||
# Each character row in terminal represents 2 pixel rows in image.
|
||||
for y in range(0, target_h - 1, 2):
|
||||
line = Text()
|
||||
for x in range(target_w):
|
||||
r1, g1, b1 = pixels[x, y]
|
||||
r2, g2, b2 = pixels[x, y + 1]
|
||||
# Foreground is top pixel, background is bottom pixel
|
||||
line.append(
|
||||
"▀",
|
||||
style=Style(
|
||||
color=Color.from_rgb(r1, g1, b1),
|
||||
bgcolor=Color.from_rgb(r2, g2, b2),
|
||||
),
|
||||
)
|
||||
console.print(line)
|
||||
|
||||
except Exception:
|
||||
# Silently fail if image cannot be rendered (e.g. missing PIL or corrupted file)
|
||||
pass
|
||||
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
details_table.add_column(style="cyan", no_wrap=True, width=15)
|
||||
details_table.add_column(style="white")
|
||||
|
||||
# Basic Info
|
||||
details_table.add_row("Title", f"[bold]{title}[/bold]")
|
||||
|
||||
if "store" in item:
|
||||
details_table.add_row("Store", str(item["store"]))
|
||||
|
||||
if "hash" in item:
|
||||
details_table.add_row("Hash", str(item["hash"]))
|
||||
|
||||
# Metadata / Path
|
||||
if "path" in item or "target" in item:
|
||||
path = item.get("path") or item.get("target")
|
||||
details_table.add_row("Path", str(path))
|
||||
|
||||
if "ext" in item or "extension" in item:
|
||||
ext = item.get("ext") or item.get("extension")
|
||||
details_table.add_row("Extension", 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)):
|
||||
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]
|
||||
if isinstance(urls, list) and urls:
|
||||
url_text = "\n".join(map(str, urls))
|
||||
details_table.add_row("URL(s)", url_text)
|
||||
|
||||
# Tags
|
||||
tags = item.get("tag") or item.get("tags") or []
|
||||
if isinstance(tags, str):
|
||||
tags = [tags]
|
||||
if isinstance(tags, list) and tags:
|
||||
# Sort and filter tags to look nice
|
||||
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)
|
||||
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:
|
||||
rel_text = "\n".join([f"[dim]→[/dim] {r}" for r in rels])
|
||||
details_table.add_row("Relations", rel_text)
|
||||
|
||||
panel = Panel(
|
||||
details_table,
|
||||
title=f"[bold green]Item Details[/bold green]",
|
||||
border_style="green",
|
||||
padding=(1, 2),
|
||||
expand=True
|
||||
)
|
||||
|
||||
stdout_console().print()
|
||||
stdout_console().print(panel)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user