Files
FastAnime/viu_media/cli/config/editor.py
2025-10-26 22:37:57 +03:00

143 lines
5.8 KiB
Python

import textwrap
from pathlib import Path
from typing import Any, Literal, get_args, get_origin
# TODO: should we maintain a separate dependency for InquirerPy or write our own simple prompt system?
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( # pyright: ignore[reportPrivateImportUsage]
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( # pyright: ignore[reportPrivateImportUsage]
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( # pyright: ignore[reportPrivateImportUsage]
message=message,
choices=choices,
default=current_value,
long_instruction=help_text,
)
# Numeric fields
if field_type is int:
return inquirer.number( # pyright: ignore[reportPrivateImportUsage]
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( # pyright: ignore[reportPrivateImportUsage]
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( # pyright: ignore[reportPrivateImportUsage]
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( # pyright: ignore[reportPrivateImportUsage]
message=message,
choices=field_info.examples,
default=str(current_value),
long_instruction=help_text,
)
return inquirer.text( # pyright: ignore[reportPrivateImportUsage]
message=message, default=str(current_value), long_instruction=help_text
)
return None