mirror of
https://github.com/Benexl/FastAnime.git
synced 2025-12-30 06:30:37 -08:00
feat: interactively edit config
This commit is contained in:
145
fastanime/cli/config/interactive_editor.py
Normal file
145
fastanime/cli/config/interactive_editor.py
Normal file
@@ -0,0 +1,145 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import textwrap
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, 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 FastAnime 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
|
||||
Reference in New Issue
Block a user