mirror of
https://github.com/Benexl/FastAnime.git
synced 2025-12-12 07:40:41 -08:00
289 lines
8.2 KiB
Python
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}")
|