Files
FastAnime/viu_media/assets/scripts/fzf/preview.py

289 lines
8.2 KiB
Python

#!/usr/bin/env python3
#
# FZF Preview Script Template
#
# This script is a template. The placeholders in curly braces, like {NAME}
# are dynamically filled by python using .replace() during runtime.
import os
import shutil
import subprocess
import sys
from hashlib import sha256
from pathlib import Path
# --- Template Variables (Injected by Python) ---
PREVIEW_MODE = "{PREVIEW_MODE}"
IMAGE_CACHE_DIR = Path("{IMAGE_CACHE_DIR}")
INFO_CACHE_DIR = Path("{INFO_CACHE_DIR}")
IMAGE_RENDERER = "{IMAGE_RENDERER}"
HEADER_COLOR = "{HEADER_COLOR}"
SEPARATOR_COLOR = "{SEPARATOR_COLOR}"
PREFIX = "{PREFIX}"
SCALE_UP = "{SCALE_UP}" == "True"
# --- Arguments ---
# sys.argv[1] is usually the raw line from FZF (the anime title/key)
TITLE = sys.argv[1] if len(sys.argv) > 1 else ""
KEY = """{KEY}"""
KEY = KEY + "-" if KEY else KEY
# Generate the hash to find the cached files
hash_id = f"{PREFIX}-{sha256((KEY + TITLE).encode('utf-8')).hexdigest()}"
def get_terminal_dimensions():
"""
Determine the available dimensions (cols x lines) for the preview window.
Prioritizes FZF environment variables.
"""
fzf_cols = os.environ.get("FZF_PREVIEW_COLUMNS")
fzf_lines = os.environ.get("FZF_PREVIEW_LINES")
if fzf_cols and fzf_lines:
return int(fzf_cols), int(fzf_lines)
# Fallback to stty if FZF vars aren't set (unlikely in preview)
try:
rows, cols = (
subprocess.check_output(
["stty", "size"], text=True, stderr=subprocess.DEVNULL
)
.strip()
.split()
)
return int(cols), int(rows)
except Exception:
return 80, 24
def which(cmd):
"""Alias for shutil.which"""
return shutil.which(cmd)
def render_kitty(file_path, width, height, scale_up):
"""Render using the Kitty Graphics Protocol (kitten/icat)."""
# 1. Try 'kitten icat' (Modern)
# 2. Try 'icat' (Legacy/Alias)
# 3. Try 'kitty +kitten icat' (Fallback)
cmd = []
if which("kitten"):
cmd = ["kitten", "icat"]
elif which("icat"):
cmd = ["icat"]
elif which("kitty"):
cmd = ["kitty", "+kitten", "icat"]
if not cmd:
return False
# Build Arguments
args = [
"--clear",
"--transfer-mode=memory",
"--unicode-placeholder",
"--stdin=no",
f"--place={width}x{height}@0x0",
]
if scale_up:
args.append("--scale-up")
args.append(file_path)
subprocess.run(cmd + args, stdout=sys.stdout, stderr=sys.stderr)
return True
def render_sixel(file_path, width, height):
"""
Render using Sixel.
Prioritizes 'chafa' for Sixel as it handles text-cell sizing better than img2sixel.
"""
# Option A: Chafa (Best for Sixel sizing)
if which("chafa"):
# Chafa automatically detects Sixel support if terminal reports it,
# but we force it here if specifically requested via logic flow.
subprocess.run(
["chafa", "-f", "sixel", "-s", f"{width}x{height}", file_path],
stdout=sys.stdout,
stderr=sys.stderr,
)
return True
# Option B: img2sixel (Libsixel)
# Note: img2sixel uses pixels, not cells. We estimate 1 cell ~= 10px width, 20px height
if which("img2sixel"):
pixel_width = width * 10
pixel_height = height * 20
subprocess.run(
[
"img2sixel",
f"--width={pixel_width}",
f"--height={pixel_height}",
file_path,
],
stdout=sys.stdout,
stderr=sys.stderr,
)
return True
return False
def render_iterm(file_path, width, height):
"""Render using iTerm2 Inline Image Protocol."""
if which("imgcat"):
subprocess.run(
["imgcat", "-W", str(width), "-H", str(height), file_path],
stdout=sys.stdout,
stderr=sys.stderr,
)
return True
# Chafa also supports iTerm
if which("chafa"):
subprocess.run(
["chafa", "-f", "iterm", "-s", f"{width}x{height}", file_path],
stdout=sys.stdout,
stderr=sys.stderr,
)
return True
return False
def render_timg(file_path, width, height):
"""Render using timg (supports half-blocks, quarter-blocks, sixel, kitty, etc)."""
if which("timg"):
subprocess.run(
["timg", f"-g{width}x{height}", "--upscale", file_path],
stdout=sys.stdout,
stderr=sys.stderr,
)
return True
return False
def render_chafa_auto(file_path, width, height):
"""
Render using Chafa in auto mode.
It supports Sixel, Kitty, iTerm, and various unicode block modes.
"""
if which("chafa"):
subprocess.run(
["chafa", "-s", f"{width}x{height}", file_path],
stdout=sys.stdout,
stderr=sys.stderr,
)
return True
return False
def fzf_image_preview(file_path: str):
"""
Main dispatch function to choose the best renderer.
"""
cols, lines = get_terminal_dimensions()
# Heuristic: Reserve 1 line for prompt/status if needed, though FZF handles this.
# Some renderers behave better with a tiny bit of padding.
width = cols
height = lines
# --- 1. Check Explicit Configuration ---
if IMAGE_RENDERER == "icat" or IMAGE_RENDERER == "system-kitty":
if render_kitty(file_path, width, height, SCALE_UP):
return
elif IMAGE_RENDERER == "sixel" or IMAGE_RENDERER == "system-sixels":
if render_sixel(file_path, width, height):
return
elif IMAGE_RENDERER == "imgcat":
if render_iterm(file_path, width, height):
return
elif IMAGE_RENDERER == "timg":
if render_timg(file_path, width, height):
return
elif IMAGE_RENDERER == "chafa":
if render_chafa_auto(file_path, width, height):
return
# --- 2. Auto-Detection / Fallback Strategy ---
# If explicit failed or set to 'auto'/'system-default', try detecting environment
# Ghostty / Kitty Environment
if os.environ.get("KITTY_WINDOW_ID") or os.environ.get("GHOSTTY_BIN_DIR"):
if render_kitty(file_path, width, height, SCALE_UP):
return
# iTerm Environment
if os.environ.get("TERM_PROGRAM") == "iTerm.app":
if render_iterm(file_path, width, height):
return
# Try standard tools in order of quality/preference
if render_kitty(file_path, width, height, SCALE_UP):
return # Try kitty just in case
if render_sixel(file_path, width, height):
return
if render_timg(file_path, width, height):
return
if render_chafa_auto(file_path, width, height):
return
print("⚠️ No suitable image renderer found (icat, chafa, timg, img2sixel).")
def fzf_text_info_render():
"""Renders the text-based info via the cached python script."""
# Get terminal dimensions from FZF environment or fallback
cols, lines = get_terminal_dimensions()
# Print simple separator line with proper width
r, g, b = map(int, SEPARATOR_COLOR.split(","))
separator = f"\x1b[38;2;{r};{g};{b}m" + ("" * cols) + "\x1b[0m"
print(separator, flush=True)
if PREVIEW_MODE == "text" or PREVIEW_MODE == "full":
preview_info_path = INFO_CACHE_DIR / f"{hash_id}.py"
if preview_info_path.exists():
subprocess.run(
[sys.executable, str(preview_info_path), HEADER_COLOR, SEPARATOR_COLOR]
)
else:
# Print dim text
print("\x1b[2m📝 Loading details...\x1b[0m")
def main():
# 1. Image Preview
if (PREVIEW_MODE == "image" or PREVIEW_MODE == "full") and (
PREFIX not in ("character", "review", "airing-schedule")
):
preview_image_path = IMAGE_CACHE_DIR / f"{hash_id}.png"
if preview_image_path.exists():
fzf_image_preview(str(preview_image_path))
print() # Spacer
else:
print("🖼️ Loading image...")
# 2. Text Info Preview
fzf_text_info_render()
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
pass
except Exception as e:
print(f"Preview Error: {e}")