mirror of
https://github.com/Benexl/FastAnime.git
synced 2025-12-20 14:22:44 -08:00
142 lines
5.3 KiB
Python
142 lines
5.3 KiB
Python
import textwrap
|
|
from pathlib import Path
|
|
from typing import Any, Literal, get_args, get_origin
|
|
|
|
from InquirerPy import inquirer
|
|
from InquirerPy.validator import NumberValidator
|
|
from pydantic import BaseModel
|
|
from pydantic.fields import FieldInfo
|
|
from rich import print
|
|
|
|
from ...core.config.model import AppConfig
|
|
|
|
|
|
class InteractiveConfigEditor:
|
|
"""A wizard to guide users through setting up their configuration interactively."""
|
|
|
|
def __init__(self, current_config: AppConfig):
|
|
self.config = current_config.model_copy(deep=True) # Work on a copy
|
|
|
|
def run(self) -> AppConfig:
|
|
"""Starts the interactive configuration wizard."""
|
|
print("[bold cyan]Welcome to the Viu Interactive Configurator![/bold cyan]")
|
|
print("Let's set up your experience. Press Ctrl+C at any time to exit.")
|
|
print("Current values will be shown as defaults.")
|
|
|
|
try:
|
|
for section_name, section_model in self.config:
|
|
if not isinstance(section_model, BaseModel):
|
|
continue
|
|
|
|
if not inquirer.confirm(
|
|
message=f"Configure '{section_name.title()}' settings?",
|
|
default=True,
|
|
).execute():
|
|
continue
|
|
|
|
self._prompt_for_section(section_name, section_model)
|
|
|
|
print("\n[bold green]Configuration complete![/bold green]")
|
|
return self.config
|
|
|
|
except KeyboardInterrupt:
|
|
print("\n[bold yellow]Configuration cancelled.[/bold yellow]")
|
|
# Return original config if user cancels
|
|
return self.config
|
|
|
|
def _prompt_for_section(self, section_name: str, section_model: BaseModel):
|
|
"""Generates prompts for all fields in a given config section."""
|
|
print(f"\n--- [bold magenta]{section_name.title()} Settings[/bold magenta] ---")
|
|
|
|
for field_name, field_info in section_model.model_fields.items():
|
|
# Skip complex multi-line fields as agreed
|
|
if section_name == "fzf" and field_name in ["opts", "header_ascii_art"]:
|
|
continue
|
|
|
|
current_value = getattr(section_model, field_name)
|
|
prompt = self._create_prompt(field_name, field_info, current_value)
|
|
|
|
if prompt:
|
|
new_value = prompt.execute()
|
|
|
|
# Explicitly cast the value to the correct type before setting it.
|
|
field_type = field_info.annotation
|
|
if new_value is not None:
|
|
if field_type is Path:
|
|
new_value = Path(new_value).expanduser()
|
|
elif field_type is int:
|
|
new_value = int(new_value)
|
|
elif field_type is float:
|
|
new_value = float(new_value)
|
|
|
|
setattr(section_model, field_name, new_value)
|
|
|
|
def _create_prompt(
|
|
self, field_name: str, field_info: FieldInfo, current_value: Any
|
|
):
|
|
"""Creates the appropriate InquirerPy prompt for a given Pydantic field."""
|
|
field_type = field_info.annotation
|
|
help_text = textwrap.fill(
|
|
field_info.description or "No description available.", width=80
|
|
)
|
|
message = f"{field_name.replace('_', ' ').title()}:"
|
|
|
|
# Boolean fields
|
|
if field_type is bool:
|
|
return inquirer.confirm(
|
|
message=message, default=current_value, long_instruction=help_text
|
|
)
|
|
|
|
# Literal (Choice) fields
|
|
if hasattr(field_type, "__origin__") and get_origin(field_type) is Literal:
|
|
choices = list(get_args(field_type))
|
|
return inquirer.select(
|
|
message=message,
|
|
choices=choices,
|
|
default=current_value,
|
|
long_instruction=help_text,
|
|
)
|
|
|
|
# Numeric fields
|
|
if field_type is int:
|
|
return inquirer.number(
|
|
message=message,
|
|
default=int(current_value),
|
|
long_instruction=help_text,
|
|
min_allowed=getattr(field_info, "gt", None)
|
|
or getattr(field_info, "ge", None),
|
|
max_allowed=getattr(field_info, "lt", None)
|
|
or getattr(field_info, "le", None),
|
|
validate=NumberValidator(),
|
|
)
|
|
if field_type is float:
|
|
return inquirer.number(
|
|
message=message,
|
|
default=float(current_value),
|
|
float_allowed=True,
|
|
long_instruction=help_text,
|
|
)
|
|
|
|
# Path fields
|
|
if field_type is Path:
|
|
# Use text prompt for paths to allow '~' expansion, as FilePathPrompt can be tricky
|
|
return inquirer.text(
|
|
message=message, default=str(current_value), long_instruction=help_text
|
|
)
|
|
|
|
# String fields
|
|
if field_type is str:
|
|
# Check for 'examples' to provide choices
|
|
if hasattr(field_info, "examples") and field_info.examples:
|
|
return inquirer.fuzzy(
|
|
message=message,
|
|
choices=field_info.examples,
|
|
default=str(current_value),
|
|
long_instruction=help_text,
|
|
)
|
|
return inquirer.text(
|
|
message=message, default=str(current_value), long_instruction=help_text
|
|
)
|
|
|
|
return None
|