feat: implement other image renders

This commit is contained in:
Benexl
2025-12-01 17:44:55 +03:00
parent bd9bf24e1c
commit 523766868c

View File

@@ -3,18 +3,16 @@
# FZF Preview Script Template
#
# This script is a template. The placeholders in curly braces, like {NAME}
# are dynamically filled by python using .replace()
# are dynamically filled by python using .replace() during runtime.
from pathlib import Path
from hashlib import sha256
import subprocess
import os
import shutil
import subprocess
import sys
from rich.console import Console
from rich.rule import Rule
from hashlib import sha256
from pathlib import Path
# dynamically filled variables
# --- Template Variables (Injected by Python) ---
PREVIEW_MODE = "{PREVIEW_MODE}"
IMAGE_CACHE_DIR = Path("{IMAGE_CACHE_DIR}")
INFO_CACHE_DIR = Path("{INFO_CACHE_DIR}")
@@ -24,192 +22,264 @@ SEPARATOR_COLOR = "{SEPARATOR_COLOR}"
PREFIX = "{PREFIX}"
SCALE_UP = "{SCALE_UP}" == "True"
TITLE = sys.argv[1]
# --- 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
hash = f"{PREFIX}-{sha256((KEY + TITLE).encode('utf-8')).hexdigest()}"
# 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):
# Environment variables from fzf
FZF_PREVIEW_COLUMNS = os.environ.get("FZF_PREVIEW_COLUMNS")
FZF_PREVIEW_LINES = os.environ.get("FZF_PREVIEW_LINES")
FZF_PREVIEW_TOP = os.environ.get("FZF_PREVIEW_TOP")
KITTY_WINDOW_ID = os.environ.get("KITTY_WINDOW_ID")
GHOSTTY_BIN_DIR = os.environ.get("GHOSTTY_BIN_DIR")
PLATFORM = os.environ.get("PLATFORM")
"""
Main dispatch function to choose the best renderer.
"""
cols, lines = get_terminal_dimensions()
# Compute terminal dimensions
dim = (
f"{FZF_PREVIEW_COLUMNS}x{FZF_PREVIEW_LINES}"
if FZF_PREVIEW_COLUMNS and FZF_PREVIEW_LINES
else "x"
)
# 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
if dim == "x":
try:
rows, cols = (
subprocess.check_output(
["stty", "size"], text=True, stderr=subprocess.DEVNULL
)
.strip()
.split()
# --- 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."""
from rich.console import Console
from rich.rule import Rule
console = Console(force_terminal=True, color_system="truecolor")
console.print(Rule(style=f"rgb({SEPARATOR_COLOR})"))
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]
)
dim = f"{cols}x{rows}"
except Exception:
dim = "80x24"
else:
console.print("📝 Loading details...", style="dim")
# Adjust dimension if icat not used and preview area fills bottom of screen
if (
IMAGE_RENDERER != "icat"
and not KITTY_WINDOW_ID
and FZF_PREVIEW_TOP
and FZF_PREVIEW_LINES
def main():
# 1. Image Preview
if (PREVIEW_MODE == "image" or PREVIEW_MODE == "full") and (
PREFIX not in ("character", "review", "airing-schedule")
):
try:
term_rows = int(
subprocess.check_output(["stty", "size"], text=True).split()[0]
)
if int(FZF_PREVIEW_TOP) + int(FZF_PREVIEW_LINES) == term_rows:
dim = f"{FZF_PREVIEW_COLUMNS}x{int(FZF_PREVIEW_LINES) - 1}"
except Exception:
pass
# Helper to run commands
def run(cmd):
subprocess.run(cmd, stdout=sys.stdout, stderr=sys.stderr)
def command_exists(cmd):
return shutil.which(cmd) is not None
# ICAT / KITTY path
if IMAGE_RENDERER == "icat" and not GHOSTTY_BIN_DIR:
icat_cmd = None
if command_exists("kitten"):
icat_cmd = ["kitten", "icat"]
elif command_exists("icat"):
icat_cmd = ["icat"]
elif command_exists("kitty"):
icat_cmd = ["kitty", "icat"]
if icat_cmd:
run(
icat_cmd
+ [
"--clear",
"--transfer-mode=memory",
"--unicode-placeholder",
"--stdin=no",
f"--place={dim}@0x0",
file_path,
]
)
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("No icat-compatible viewer found (kitten/icat/kitty)")
print("🖼️ Loading image...")
elif GHOSTTY_BIN_DIR:
try:
cols = int(FZF_PREVIEW_COLUMNS or "80") - 1
lines = FZF_PREVIEW_LINES or "24"
dim = f"{cols}x{lines}"
except Exception:
pass
if command_exists("kitten"):
run(
[
"kitten",
"icat",
"--clear",
"--transfer-mode=memory",
"--unicode-placeholder",
"--stdin=no",
f"--place={dim}@0x0",
file_path,
]
)
elif command_exists("icat"):
run(
[
"icat",
"--clear",
"--transfer-mode=memory",
"--unicode-placeholder",
"--stdin=no",
f"--place={dim}@0x0",
file_path,
]
)
elif command_exists("chafa"):
run(["chafa", "-s", dim, file_path])
elif command_exists("chafa"):
# Platform specific rendering
if PLATFORM == "android":
run(["chafa", "-s", dim, file_path])
elif PLATFORM == "windows":
run(["chafa", "-f", "sixel", "-s", dim, file_path])
else:
run(["chafa", "-s", dim, file_path])
print()
elif command_exists("imgcat"):
width, height = dim.split("x")
run(["imgcat", "-W", width, "-H", height, file_path])
else:
print(
"⚠️ Please install a terminal image viewer (icat, kitten, imgcat, or chafa)."
)
# 2. Text Info Preview
fzf_text_info_render()
def fzf_text_preview(file_path: str):
from base64 import standard_b64encode
def serialize_gr_command(**cmd):
payload = cmd.pop("payload", None)
cmd = ",".join(f"{k}={v}" for k, v in cmd.items())
ans = []
w = ans.append
w(b"\033_G")
w(cmd.encode("ascii"))
if payload:
w(b";")
w(payload)
w(b"\033\\")
return b"".join(ans)
def write_chunked(**cmd):
data = standard_b64encode(cmd.pop("data"))
while data:
chunk, data = data[:4096], data[4096:]
m = 1 if data else 0
sys.stdout.buffer.write(serialize_gr_command(payload=chunk, m=m, **cmd))
sys.stdout.flush()
cmd.clear()
with open(file_path, "rb") as f:
write_chunked(a="T", f=100, data=f.read())
console = Console(force_terminal=True, color_system="truecolor")
if (PREVIEW_MODE == "image" or PREVIEW_MODE == "full") and (
PREFIX not in ("character", "review", "airing-schedule")
):
preview_image_path = IMAGE_CACHE_DIR / f"{hash}.png"
if preview_image_path.exists():
fzf_image_preview(str(preview_image_path))
print()
else:
print("🖼️ Loading image...")
console.print(Rule(style=f"rgb({SEPARATOR_COLOR})"))
if PREVIEW_MODE == "text" or PREVIEW_MODE == "full":
preview_info_path = INFO_CACHE_DIR / f"{hash}.py"
if preview_info_path.exists():
subprocess.run(
[sys.executable, str(preview_info_path), HEADER_COLOR, SEPARATOR_COLOR]
)
else:
console.print("📝 Loading details...")
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
pass
except Exception as e:
print(f"Preview Error: {e}")