Files
Medios-Macina/SYS/rich_display.py

282 lines
8.8 KiB
Python
Raw Normal View History

2025-12-20 23:57:44 -08:00
"""Central Rich output helpers.
Opinionated: `rich` is a required dependency.
This module centralizes Console instances so tables/panels render consistently and
so callers can choose stdout vs stderr explicitly (important for pipeline-safe
output).
"""
from __future__ import annotations
2025-12-24 02:13:21 -08:00
import contextlib
2025-12-20 23:57:44 -08:00
import sys
2026-01-01 20:37:27 -08:00
from typing import Any, Iterator, Sequence, TextIO
2025-12-20 23:57:44 -08:00
from rich.console import Console
2026-01-01 20:37:27 -08:00
from rich.panel import Panel
from rich.text import Text
2026-01-11 02:26:39 -08:00
from pathlib import Path
from SYS.utils import expand_path
2025-12-20 23:57:44 -08:00
2025-12-23 16:36:39 -08:00
# Configure Rich pretty-printing to avoid truncating long strings (hashes/paths).
# This is version-safe: older Rich versions may not support the max_* arguments.
try:
from rich.pretty import install as _pretty_install
try:
_pretty_install(max_string=100_000, max_length=100_000)
except TypeError:
_pretty_install()
except Exception:
pass
2025-12-20 23:57:44 -08:00
_STDOUT_CONSOLE = Console(file=sys.stdout)
_STDERR_CONSOLE = Console(file=sys.stderr)
def stdout_console() -> Console:
return _STDOUT_CONSOLE
def stderr_console() -> Console:
return _STDERR_CONSOLE
def console_for(file: TextIO | None) -> Console:
if file is None or file is sys.stdout:
return _STDOUT_CONSOLE
if file is sys.stderr:
return _STDERR_CONSOLE
return Console(file=file)
def rprint(renderable: Any = "", *, file: TextIO | None = None) -> None:
console_for(file).print(renderable)
2025-12-24 02:13:21 -08:00
@contextlib.contextmanager
def capture_rich_output(*, stdout: TextIO, stderr: TextIO) -> Iterator[None]:
"""Temporarily redirect Rich output helpers to provided streams.
Note: `stdout_console()` / `stderr_console()` use global Console instances,
so `contextlib.redirect_stdout` alone will not capture Rich output.
"""
global _STDOUT_CONSOLE, _STDERR_CONSOLE
previous_stdout = _STDOUT_CONSOLE
previous_stderr = _STDERR_CONSOLE
try:
_STDOUT_CONSOLE = Console(file=stdout)
_STDERR_CONSOLE = Console(file=stderr)
yield
finally:
_STDOUT_CONSOLE = previous_stdout
_STDERR_CONSOLE = previous_stderr
2026-01-01 20:37:27 -08:00
def show_provider_config_panel(
2026-01-11 04:54:27 -08:00
provider_names: str | List[str],
2026-01-01 20:37:27 -08:00
) -> None:
2026-01-11 04:54:27 -08:00
"""Show a Rich panel explaining how to configure providers."""
from rich.table import Table
from rich.text import Text
from rich.console import Group
if isinstance(provider_names, str):
providers = [p.strip() for p in provider_names.split(",")]
else:
providers = provider_names
table = Table.grid(padding=(0, 1))
table.add_column(style="bold red")
for provider in providers:
table.add_row(f"{provider}")
group = Group(
Text("The following providers are not configured and cannot be used:\n"),
table,
Text.from_markup("\nTo configure them, run the command with [bold cyan].config[/bold cyan] or use the [bold green]TUI[/bold green] config menu.")
)
panel = Panel(
group,
title="[bold red]Configuration Required[/bold red]",
border_style="red",
padding=(1, 2)
)
stdout_console().print()
stdout_console().print(panel)
2026-01-10 17:30:18 -08:00
def show_store_config_panel(
2026-01-11 04:54:27 -08:00
store_names: str | List[str],
2026-01-10 17:30:18 -08:00
) -> None:
2026-01-11 04:54:27 -08:00
"""Show a Rich panel explaining how to configure storage backends."""
from rich.table import Table
from rich.text import Text
from rich.console import Group
if isinstance(store_names, str):
stores = [s.strip() for s in store_names.split(",")]
else:
stores = store_names
table = Table.grid(padding=(0, 1))
table.add_column(style="bold yellow")
for store in stores:
table.add_row(f"{store}")
group = Group(
Text("The following stores are not configured or available:\n"),
table,
Text.from_markup("\nInitialize them using [bold cyan].config[/bold cyan] or ensure they are properly set up.")
)
panel = Panel(
group,
title="[bold yellow]Store Not Configured[/bold yellow]",
border_style="yellow",
padding=(1, 2)
)
stdout_console().print()
stdout_console().print(panel)
def show_available_providers_panel(provider_names: List[str]) -> None:
"""Show a Rich panel listing available/configured providers."""
from rich.columns import Columns
from rich.console import Group
from rich.text import Text
if not provider_names:
return
# Use Columns to display them efficiently in the panel
cols = Columns(
[f"[bold green] \u2713 [/bold green]{p}" for p in sorted(provider_names)],
equal=True,
column_first=True,
expand=True
)
group = Group(
Text("The following providers are configured and ready to use:\n"),
cols
)
panel = Panel(
group,
title="[bold green]Configured Providers[/bold green]",
border_style="green",
padding=(1, 2)
)
stdout_console().print()
stdout_console().print(panel)
2026-01-10 17:30:18 -08:00
2026-01-11 02:26:39 -08:00
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:
2026-01-12 20:50:29 -08:00
"""Render a comprehensive details panel for a result item using unified ItemDetailView."""
from SYS.result_table import ItemDetailView, extract_item_metadata
2026-01-11 02:26:39 -08:00
2026-01-12 20:50:29 -08:00
metadata = extract_item_metadata(item)
2026-01-11 02:26:39 -08:00
2026-01-12 20:50:29 -08:00
# 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)
2026-01-11 02:26:39 -08:00
2026-01-12 20:50:29 -08:00
# 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.
2026-01-11 02:26:39 -08:00
stdout_console().print()
2026-01-12 20:50:29 -08:00
stdout_console().print(view.to_rich())
2026-01-11 02:26:39 -08:00