booru-viewer/booru_viewer/tui/preview.py

93 lines
3.1 KiB
Python

"""Image preview widget with Kitty graphics protocol support."""
from __future__ import annotations
import base64
import os
import sys
from pathlib import Path
from textual.widgets import Static
from ..core.config import GREEN, DIM_GREEN, BG
def _supports_kitty() -> bool:
"""Check if the terminal likely supports the Kitty graphics protocol."""
term = os.environ.get("TERM", "")
term_program = os.environ.get("TERM_PROGRAM", "")
return "kitty" in term or "kitty" in term_program
def _kitty_display(path: str, cols: int = 80, rows: int = 24) -> str:
"""Generate Kitty graphics protocol escape sequence for an image."""
try:
data = Path(path).read_bytes()
b64 = base64.standard_b64encode(data).decode("ascii")
# Send in chunks (Kitty protocol requires chunked transfer for large images)
chunks = [b64[i:i + 4096] for i in range(0, len(b64), 4096)]
output = ""
for i, chunk in enumerate(chunks):
is_last = i == len(chunks) - 1
m = 0 if is_last else 1
if i == 0:
output += f"\033_Ga=T,f=100,m={m},c={cols},r={rows};{chunk}\033\\"
else:
output += f"\033_Gm={m};{chunk}\033\\"
return output
except Exception:
return ""
class ImagePreview(Static):
"""Image preview panel. Uses Kitty graphics protocol on supported terminals,
otherwise shows image metadata."""
def __init__(self, **kwargs) -> None:
super().__init__(**kwargs)
self._path: str | None = None
self._info: str = ""
self._use_kitty = _supports_kitty()
def show_image(self, path: str, info: str = "") -> None:
self._path = path
self._info = info
if self._use_kitty and self._path:
# Write Kitty escape directly to terminal, show info in widget
size = self.size
kitty_seq = _kitty_display(path, cols=size.width, rows=size.height - 2)
if kitty_seq:
sys.stdout.write(kitty_seq)
sys.stdout.flush()
self.update(f"\n{info}")
else:
# Fallback: show file info
try:
from PIL import Image
with Image.open(path) as img:
w, h = img.size
fmt = img.format or "unknown"
size_kb = Path(path).stat().st_size / 1024
text = (
f" Image: {Path(path).name}\n"
f" Size: {w}x{h} ({size_kb:.0f} KB)\n"
f" Format: {fmt}\n"
f"\n {info}\n"
f"\n (Kitty graphics protocol not detected;\n"
f" run in Kitty terminal for inline preview)"
)
except Exception:
text = f" {info}\n\n (Cannot read image)"
self.update(text)
def clear(self) -> None:
self._path = None
self._info = ""
if self._use_kitty:
# Clear Kitty images
sys.stdout.write("\033_Ga=d;\033\\")
sys.stdout.flush()
self.update("")