feat: rewrite FZF preview scripts to use ANSI utilities for improved formatting

This commit is contained in:
Benexl
2025-12-01 18:47:58 +03:00
parent 523766868c
commit 901d1e87c5
8 changed files with 348 additions and 200 deletions

View File

@@ -0,0 +1,152 @@
"""
ANSI utilities for FZF preview scripts.
Lightweight stdlib-only utilities to replace Rich dependency in preview scripts.
Provides RGB color formatting, table rendering, and markdown stripping.
"""
import re
import shutil
import textwrap
def rgb_color(r: int, g: int, b: int, text: str, bold: bool = False) -> str:
"""
Format text with RGB color using ANSI escape codes.
Args:
r: Red component (0-255)
g: Green component (0-255)
b: Blue component (0-255)
text: Text to colorize
bold: Whether to make text bold
Returns:
ANSI-escaped colored text
"""
color_code = f"\x1b[38;2;{r};{g};{b}m"
bold_code = "\x1b[1m" if bold else ""
reset = "\x1b[0m"
return f"{color_code}{bold_code}{text}{reset}"
def parse_color(color_csv: str) -> tuple[int, int, int]:
"""
Parse RGB color from comma-separated string.
Args:
color_csv: Color as 'R,G,B' string
Returns:
Tuple of (r, g, b) integers
"""
parts = color_csv.split(",")
return int(parts[0]), int(parts[1]), int(parts[2])
def print_rule(sep_color: str) -> None:
"""
Print a horizontal rule line.
Args:
sep_color: Color as 'R,G,B' string
"""
width = shutil.get_terminal_size((80, 24)).columns
r, g, b = parse_color(sep_color)
print(rgb_color(r, g, b, "" * width))
def print_table_row(
key: str, value: str, header_color: str, key_width: int, value_width: int
) -> None:
"""
Print a two-column table row with left-aligned key and right-aligned value.
Args:
key: Left column text (header/key)
value: Right column text (value)
header_color: Color for key as 'R,G,B' string
key_width: Width for key column
value_width: Width for value column
"""
r, g, b = parse_color(header_color)
key_styled = rgb_color(r, g, b, key, bold=True)
# Ensure minimum width to avoid textwrap errors
safe_value_width = max(20, value_width)
# Wrap value if it's too long
value_lines = textwrap.wrap(str(value), width=safe_value_width) if value else [""]
if not value_lines:
value_lines = [""]
# Print first line with right-aligned value
first_line = value_lines[0]
print(f"{key_styled:<{key_width + 20}} {first_line:>{safe_value_width}}")
# Print remaining wrapped lines (left-aligned, indented)
for line in value_lines[1:]:
print(f"{' ' * (key_width + 2)}{line}")
def strip_markdown(text: str) -> str:
"""
Strip markdown formatting from text.
Removes:
- Headers (# ## ###)
- Bold (**text** or __text__)
- Italic (*text* or _text_)
- Links ([text](url))
- Code blocks (```code```)
- Inline code (`code`)
Args:
text: Markdown-formatted text
Returns:
Plain text with markdown removed
"""
if not text:
return ""
# Remove code blocks first
text = re.sub(r"```[\s\S]*?```", "", text)
# Remove inline code
text = re.sub(r"`([^`]+)`", r"\1", text)
# Remove headers
text = re.sub(r"^#{1,6}\s+", "", text, flags=re.MULTILINE)
# Remove bold (** or __)
text = re.sub(r"\*\*(.+?)\*\*", r"\1", text)
text = re.sub(r"__(.+?)__", r"\1", text)
# Remove italic (* or _)
text = re.sub(r"\*(.+?)\*", r"\1", text)
text = re.sub(r"_(.+?)_", r"\1", text)
# Remove links, keep text
text = re.sub(r"\[(.+?)\]\(.+?\)", r"\1", text)
# Remove images
text = re.sub(r"!\[.*?\]\(.+?\)", "", text)
return text.strip()
def wrap_text(text: str, width: int | None = None) -> str:
"""
Wrap text to terminal width.
Args:
text: Text to wrap
width: Width to wrap to (defaults to terminal width)
Returns:
Wrapped text
"""
if width is None:
width = shutil.get_terminal_size((80, 24)).columns
return textwrap.fill(text, width=width)

View File

@@ -1,41 +1,31 @@
import sys
from rich.console import Console
from rich.table import Table
from rich.rule import Rule
from rich.markdown import Markdown
console = Console(force_terminal=True, color_system="truecolor")
import shutil
from _ansi_utils import print_rule, print_table_row, strip_markdown, wrap_text
HEADER_COLOR = sys.argv[1]
SEPARATOR_COLOR = sys.argv[2]
# Get terminal dimensions
term_width = shutil.get_terminal_size((80, 24)).columns
def rule(title: str | None = None):
console.print(Rule(style=f"rgb({SEPARATOR_COLOR})"))
# Print title centered
print("{ANIME_TITLE}".center(term_width))
console.print("{ANIME_TITLE}", justify="center")
left = [
("Total Episodes",),
("Upcoming Episodes",),
]
right = [
("{TOTAL_EPISODES}",),
("{UPCOMING_EPISODES}",),
rows = [
("Total Episodes", "{TOTAL_EPISODES}"),
]
print_rule(SEPARATOR_COLOR)
for key, value in rows:
print_table_row(key, value, HEADER_COLOR, 15, term_width - 20)
for L_grp, R_grp in zip(left, right):
table = Table.grid(expand=True)
table.add_column(justify="left", no_wrap=True)
table.add_column(justify="right", overflow="fold")
for L, R in zip(L_grp, R_grp):
table.add_row(f"[bold rgb({HEADER_COLOR})]{L} [/]", f"{R}")
rows = [
("Upcoming Episodes", "{UPCOMING_EPISODES}"),
]
rule()
console.print(table)
print_rule(SEPARATOR_COLOR)
for key, value in rows:
print_table_row(key, value, HEADER_COLOR, 15, term_width - 20)
rule()
console.print(Markdown("""{SCHEDULE_TABLE}"""))
print_rule(SEPARATOR_COLOR)
print(wrap_text(strip_markdown("""{SCHEDULE_TABLE}"""), term_width))

View File

@@ -1,43 +1,42 @@
import sys
from rich.console import Console
from rich.table import Table
from rich.rule import Rule
from rich.markdown import Markdown
console = Console(force_terminal=True, color_system="truecolor")
import shutil
from _ansi_utils import print_rule, print_table_row, strip_markdown, wrap_text
HEADER_COLOR = sys.argv[1]
SEPARATOR_COLOR = sys.argv[2]
# Get terminal dimensions
term_width = shutil.get_terminal_size((80, 24)).columns
def rule(title: str | None = None):
console.print(Rule(style=f"rgb({SEPARATOR_COLOR})"))
# Print title centered
print("{CHARACTER_NAME}".center(term_width))
console.print("{CHARACTER_NAME}", justify="center")
left = [
("Native Name", "Gender"),
("Age", "Blood Type"),
("Birthday", "Favourites"),
]
right = [
("{CHARACTER_NATIVE_NAME}", "{CHARACTER_GENDER}"),
("{CHARACTER_AGE}", "{CHARACTER_BLOOD_TYPE}"),
("{CHARACTER_BIRTHDAY}", "{CHARACTER_FAVOURITES}"),
rows = [
("Native Name", "{CHARACTER_NATIVE_NAME}"),
("Gender", "{CHARACTER_GENDER}"),
]
print_rule(SEPARATOR_COLOR)
for key, value in rows:
print_table_row(key, value, HEADER_COLOR, 15, term_width - 20)
for L_grp, R_grp in zip(left, right):
table = Table.grid(expand=True)
table.add_column(justify="left", no_wrap=True)
table.add_column(justify="right", overflow="fold")
for L, R in zip(L_grp, R_grp):
table.add_row(f"[bold rgb({HEADER_COLOR})]{L} [/]", f"{R}")
rows = [
("Age", "{CHARACTER_AGE}"),
("Blood Type", "{CHARACTER_BLOOD_TYPE}"),
]
rule()
console.print(table)
print_rule(SEPARATOR_COLOR)
for key, value in rows:
print_table_row(key, value, HEADER_COLOR, 15, term_width - 20)
rows = [
("Birthday", "{CHARACTER_BIRTHDAY}"),
("Favourites", "{CHARACTER_FAVOURITES}"),
]
rule()
console.print(Markdown("""{CHARACTER_DESCRIPTION}"""))
print_rule(SEPARATOR_COLOR)
for key, value in rows:
print_table_row(key, value, HEADER_COLOR, 15, term_width - 20)
print_rule(SEPARATOR_COLOR)
print(wrap_text(strip_markdown("""{CHARACTER_DESCRIPTION}"""), term_width))

View File

@@ -1,44 +1,50 @@
import sys
from rich.console import Console
from rich.table import Table
from rich.rule import Rule
from rich.markdown import Markdown
console = Console(force_terminal=True, color_system="truecolor")
import shutil
from _ansi_utils import print_rule, print_table_row
HEADER_COLOR = sys.argv[1]
SEPARATOR_COLOR = sys.argv[2]
# Get terminal dimensions
term_width = shutil.get_terminal_size((80, 24)).columns
def rule(title: str | None = None):
console.print(Rule(style=f"rgb({SEPARATOR_COLOR})"))
# Print title centered
print("{TITLE}".center(term_width))
console.print("{TITLE}", justify="center")
left = [
("Duration", "Status"),
("Total Episodes", "Next Episode"),
("Progress", "List Status"),
("Start Date", "End Date"),
]
right = [
("{DURATION}", "{STATUS}"),
("{EPISODES}", "{NEXT_EPISODE}"),
("{USER_PROGRESS}", "{USER_STATUS}"),
("{START_DATE}", "{END_DATE}"),
rows = [
("Duration", "{DURATION}"),
("Status", "{STATUS}"),
]
print_rule(SEPARATOR_COLOR)
for key, value in rows:
print_table_row(key, value, HEADER_COLOR, 15, term_width - 20)
for L_grp, R_grp in zip(left, right):
table = Table.grid(expand=True)
table.add_column(justify="left", no_wrap=True)
table.add_column(justify="right", overflow="fold")
for L, R in zip(L_grp, R_grp):
table.add_row(f"[bold rgb({HEADER_COLOR})]{L} [/]", f"{R}")
rows = [
("Total Episodes", "{EPISODES}"),
("Next Episode", "{NEXT_EPISODE}"),
]
rule()
console.print(table)
print_rule(SEPARATOR_COLOR)
for key, value in rows:
print_table_row(key, value, HEADER_COLOR, 15, term_width - 20)
rows = [
("Progress", "{USER_PROGRESS}"),
("List Status", "{USER_STATUS}"),
]
rule()
print_rule(SEPARATOR_COLOR)
for key, value in rows:
print_table_row(key, value, HEADER_COLOR, 15, term_width - 20)
rows = [
("Start Date", "{START_DATE}"),
("End Date", "{END_DATE}"),
]
print_rule(SEPARATOR_COLOR)
for key, value in rows:
print_table_row(key, value, HEADER_COLOR, 15, term_width - 20)
print_rule(SEPARATOR_COLOR)

View File

@@ -1,89 +1,88 @@
import sys
from rich.console import Console
from rich.table import Table
from rich.rule import Rule
from rich.markdown import Markdown
console = Console(force_terminal=True, color_system="truecolor")
import shutil
from _ansi_utils import print_rule, print_table_row, strip_markdown, wrap_text
HEADER_COLOR = sys.argv[1]
SEPARATOR_COLOR = sys.argv[2]
# Get terminal dimensions
term_width = shutil.get_terminal_size((80, 24)).columns
def rule(title: str | None = None):
console.print(Rule(style=f"rgb({SEPARATOR_COLOR})"))
# Print title centered
print("{TITLE}".center(term_width))
console.print("{TITLE}", justify="center")
left = [
(
"Score",
"Favorites",
"Popularity",
"Status",
),
(
"Episodes",
"Duration",
"Next Episode",
),
(
"Genres",
"Format",
),
(
"List Status",
"Progress",
),
(
"Start Date",
"End Date",
),
("Studios",),
("Synonymns",),
("Tags",),
]
right = [
(
"{SCORE}",
"{FAVOURITES}",
"{POPULARITY}",
"{STATUS}",
),
(
"{EPISODES}",
"{DURATION}",
"{NEXT_EPISODE}",
),
(
"{GENRES}",
"{FORMAT}",
),
(
"{USER_STATUS}",
"{USER_PROGRESS}",
),
(
"{START_DATE}",
"{END_DATE}",
),
("{STUDIOS}",),
("{SYNONYMNS}",),
("{TAGS}",),
# Define table data
rows = [
("Score", "{SCORE}"),
("Favorites", "{FAVOURITES}"),
("Popularity", "{POPULARITY}"),
("Status", "{STATUS}"),
]
print_rule(SEPARATOR_COLOR)
for key, value in rows:
print_table_row(key, value, HEADER_COLOR, 15, term_width - 20)
for L_grp, R_grp in zip(left, right):
table = Table.grid(expand=True)
table.add_column(justify="left", no_wrap=True)
table.add_column(justify="right", overflow="fold")
for L, R in zip(L_grp, R_grp):
table.add_row(f"[bold rgb({HEADER_COLOR})]{L} [/]", f"{R}")
rows = [
("Episodes", "{EPISODES}"),
("Duration", "{DURATION}"),
("Next Episode", "{NEXT_EPISODE}"),
]
rule()
console.print(table)
print_rule(SEPARATOR_COLOR)
for key, value in rows:
print_table_row(key, value, HEADER_COLOR, 15, term_width - 20)
rows = [
("Genres", "{GENRES}"),
("Format", "{FORMAT}"),
]
rule()
console.print(Markdown("""{SYNOPSIS}"""))
print_rule(SEPARATOR_COLOR)
for key, value in rows:
print_table_row(key, value, HEADER_COLOR, 15, term_width - 20)
rows = [
("List Status", "{USER_STATUS}"),
("Progress", "{USER_PROGRESS}"),
]
print_rule(SEPARATOR_COLOR)
for key, value in rows:
print_table_row(key, value, HEADER_COLOR, 15, term_width - 20)
rows = [
("Start Date", "{START_DATE}"),
("End Date", "{END_DATE}"),
]
print_rule(SEPARATOR_COLOR)
for key, value in rows:
print_table_row(key, value, HEADER_COLOR, 15, term_width - 20)
rows = [
("Studios", "{STUDIOS}"),
]
print_rule(SEPARATOR_COLOR)
for key, value in rows:
print_table_row(key, value, HEADER_COLOR, 15, term_width - 20)
rows = [
("Synonymns", "{SYNONYMNS}"),
]
print_rule(SEPARATOR_COLOR)
for key, value in rows:
print_table_row(key, value, HEADER_COLOR, 15, term_width - 20)
rows = [
("Tags", "{TAGS}"),
]
print_rule(SEPARATOR_COLOR)
for key, value in rows:
print_table_row(key, value, HEADER_COLOR, 15, term_width - 20)
print_rule(SEPARATOR_COLOR)
print(wrap_text(strip_markdown("""{SYNOPSIS}"""), term_width))

View File

@@ -244,11 +244,12 @@ def fzf_image_preview(file_path: str):
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
import shutil
console = Console(force_terminal=True, color_system="truecolor")
console.print(Rule(style=f"rgb({SEPARATOR_COLOR})"))
# Print simple separator line
width = shutil.get_terminal_size((80, 24)).columns
r, g, b = map(int, SEPARATOR_COLOR.split(","))
print(f"\x1b[38;2;{r};{g};{b}m" + "" * width + "\x1b[0m")
if PREVIEW_MODE == "text" or PREVIEW_MODE == "full":
preview_info_path = INFO_CACHE_DIR / f"{hash_id}.py"
@@ -257,7 +258,8 @@ def fzf_text_info_render():
[sys.executable, str(preview_info_path), HEADER_COLOR, SEPARATOR_COLOR]
)
else:
console.print("📝 Loading details...", style="dim")
# Print dim text
print("\x1b[2m📝 Loading details...\x1b[0m")
def main():

View File

@@ -1,39 +1,23 @@
import sys
from rich.console import Console
from rich.table import Table
from rich.rule import Rule
from rich.markdown import Markdown
console = Console(force_terminal=True, color_system="truecolor")
import shutil
from _ansi_utils import print_rule, print_table_row, strip_markdown, wrap_text
HEADER_COLOR = sys.argv[1]
SEPARATOR_COLOR = sys.argv[2]
# Get terminal dimensions
term_width = shutil.get_terminal_size((80, 24)).columns
def rule(title: str | None = None):
console.print(Rule(style=f"rgb({SEPARATOR_COLOR})"))
# Print title centered
print("{REVIEWER_NAME}".center(term_width))
console.print("{REVIEWER_NAME}", justify="center")
left = [
("Summary",),
]
right = [
("{REVIEW_SUMMARY}",),
rows = [
("Summary", "{REVIEW_SUMMARY}"),
]
print_rule(SEPARATOR_COLOR)
for key, value in rows:
print_table_row(key, value, HEADER_COLOR, 15, term_width - 20)
for L_grp, R_grp in zip(left, right):
table = Table.grid(expand=True)
table.add_column(justify="left", no_wrap=True)
table.add_column(justify="right", overflow="fold")
for L, R in zip(L_grp, R_grp):
table.add_row(f"[bold rgb({HEADER_COLOR})]{L} [/]", f"{R}")
rule()
console.print(table)
rule()
console.print(Markdown("""{REVIEW_BODY}"""))
print_rule(SEPARATOR_COLOR)
print(wrap_text(strip_markdown("""{REVIEW_BODY}"""), term_width))

View File

@@ -135,6 +135,20 @@ EPISODE_PATTERN = re.compile(r"^Episode\s+(\d+)\s-\s.*")
_preview_manager: Optional[PreviewWorkerManager] = None
def _ensure_ansi_utils_in_cache():
"""Copy _ansi_utils.py to the info cache directory so cached scripts can import it."""
source = FZF_SCRIPTS_DIR / "_ansi_utils.py"
dest = INFO_CACHE_DIR / "_ansi_utils.py"
if source.exists() and (not dest.exists() or source.stat().st_mtime > dest.stat().st_mtime):
try:
import shutil
shutil.copy2(source, dest)
logger.debug(f"Copied _ansi_utils.py to {INFO_CACHE_DIR}")
except Exception as e:
logger.warning(f"Failed to copy _ansi_utils.py to cache: {e}")
def create_preview_context():
"""
Create a context manager for preview operations.
@@ -270,6 +284,7 @@ def get_anime_preview(
# Ensure cache directories exist on startup
IMAGES_CACHE_DIR.mkdir(parents=True, exist_ok=True)
INFO_CACHE_DIR.mkdir(parents=True, exist_ok=True)
_ensure_ansi_utils_in_cache()
HEADER_COLOR = config.fzf.preview_header_color.split(",")
SEPARATOR_COLOR = config.fzf.preview_separator_color.split(",")
@@ -527,6 +542,7 @@ def get_dynamic_anime_preview(config: AppConfig) -> str:
# Ensure cache directories exist
IMAGES_CACHE_DIR.mkdir(parents=True, exist_ok=True)
INFO_CACHE_DIR.mkdir(parents=True, exist_ok=True)
_ensure_ansi_utils_in_cache()
HEADER_COLOR = config.fzf.preview_header_color.split(",")
SEPARATOR_COLOR = config.fzf.preview_separator_color.split(",")