From 94529b9921081ae60a7c9b84b37e32dbb49e6f81 Mon Sep 17 00:00:00 2001 From: 7000pctAUTO Date: Sun, 1 Feb 2026 20:49:02 +0000 Subject: [PATCH] Initial upload with full project structure --- app/src/confgen/editor.py | 276 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 276 insertions(+) create mode 100644 app/src/confgen/editor.py diff --git a/app/src/confgen/editor.py b/app/src/confgen/editor.py new file mode 100644 index 0000000..a957afb --- /dev/null +++ b/app/src/confgen/editor.py @@ -0,0 +1,276 @@ +"""Interactive TUI editor using prompt_toolkit.""" + +from typing import Any, Optional + + +def console_print(text: str) -> None: + """Print text to console.""" + from rich.console import Console + + console = Console() + console.print(text) + + +def confirm(message: str) -> bool: + """Show a confirmation prompt.""" + from prompt_toolkit import PromptSession + + session = PromptSession() + result = session.prompt(f"{message} [y/N]: ") + return result.lower() in ("y", "yes") + + +def get_prompt_for_var(var_name: str, current_value: str) -> str: + """Get the prompt text for a variable.""" + if is_secret_variable(var_name): + return f"[secret] {var_name}" + elif is_boolean_variable(var_name, current_value): + return f"[boolean] {var_name} (yes/no)" + elif is_numeric_variable(var_name, current_value): + return f"[number] {var_name}" + else: + return var_name + + +def is_secret_variable(var_name: str) -> bool: + """Check if a variable name suggests a secret.""" + secret_patterns = [ + "SECRET", + "PASSWORD", + "API_KEY", + "API_SECRET", + "TOKEN", + "CREDENTIAL", + "PRIVATE_KEY", + "AUTH", + "DB_PASSWORD", + ] + return any(pattern in var_name.upper() for pattern in secret_patterns) + + +def is_boolean_variable(var_name: str, current_value: str) -> bool: + """Check if a variable is likely a boolean.""" + bool_names = ["debug", "enabled", "disabled", "verbose", "skip", "force"] + if var_name.lower() in bool_names: + return True + if current_value and current_value.lower() in ("true", "false", "yes", "no", "0", "1"): + return True + return False + + +def is_numeric_variable(var_name: str, current_value: str) -> bool: + """Check if a variable is likely numeric.""" + num_names = ["port", "timeout", "retries", "count", "size", "limit"] + if var_name.lower() in num_names: + return True + if "_port" in var_name.lower() or var_name.lower().endswith("port"): + return True + if current_value: + try: + float(current_value) + return True + except (ValueError, TypeError): + pass + return False + + +class InteractiveEditor: + """Interactive terminal UI for editing config values.""" + + def __init__(self, core, template_name: str, environment: str): + self.core = core + self.template_name = template_name + self.environment = environment + self.variables = {} + self._load_template_info() + + def _load_template_info(self) -> None: + """Load template and environment information.""" + tmpl = self.core.get_template(self.template_name) + if not tmpl: + raise ValueError(f"Template '{self.template_name}' not found") + + env_config = self.core.get_environment(self.environment) + if env_config: + self.variables = env_config.variables.copy() + + from .template import TemplateEngine + + engine = TemplateEngine() + if tmpl.path.exists(): + content = tmpl.path.read_text() + extracted = engine.extract_variables(content) + + for var in extracted: + if var not in self.variables: + self.variables[var] = "" + + def run(self) -> Optional[str]: + """Run the interactive editor.""" + from prompt_toolkit import PromptSession + + session = PromptSession() + + console_print("\n[bold]Interactive Configuration Editor[/bold]") + console_print(f"Template: {self.template_name}") + console_print(f"Environment: {self.environment}\n") + + for var_name, current_value in self.variables.items(): + prompt = get_prompt_for_var(var_name, current_value) + + if is_secret_variable(var_name): + value = session.prompt( + f"{prompt}: ", + is_password=True, + ) + else: + value = session.prompt( + f"{prompt}: ", + default=str(current_value) if current_value else "", + ) + + if value: + self.variables[var_name] = value + + console_print("\n[bold]Generated Configuration:[/bold]\n") + + from .template import TemplateEngine + from .parsers import ConfigParser + + engine = TemplateEngine() + tmpl = self.core.get_template(self.template_name) + + if tmpl and tmpl.path.exists(): + content = tmpl.path.read_text() + rendered = engine.render(content, self.variables) + + parser = ConfigParser() + parsed = parser.parse(rendered) + + from .formatter import OutputFormatter + + formatter = OutputFormatter() + formatted = formatter.format_dict(parsed) + + console_print(formatted) + + if confirm("\nSave this configuration?"): + return parser.to_yaml(parsed) + + return None + + +class ConfigEditor: + """Advanced config editor with type validation.""" + + def __init__(self): + self.values = {} + + def prompt_for_value( + self, + name: str, + current_value: Any, + vtype: str = "string", + required: bool = False, + choices: Optional[list] = None, + ) -> Any: + """Prompt for a value with type validation.""" + from prompt_toolkit import PromptSession + from prompt_toolkit.validation import ValidationError, Validator + + session = PromptSession() + + class TypeValidator(Validator): + def validate(self, document): + text = document.text + if required and not text: + return + + if vtype == "integer" and text and not text.isdigit(): + raise ValidationError(message="Must be a valid integer") + + if vtype == "float" and text: + try: + float(text) + except ValueError: + raise ValidationError(message="Must be a valid number") + + if vtype == "boolean" and text and text.lower() not in ( + "true", + "false", + "yes", + "no", + ): + raise ValidationError( + message="Must be true/false or yes/no" + ) + + if vtype == "port" and text: + if not text.isdigit() or not (1 <= int(text) <= 65535): + raise ValidationError(message="Port must be 1-65535") + + if choices and text and text not in choices: + raise ValidationError( + message=f"Must be one of: {', '.join(choices)}" + ) + + prompt_text = get_prompt_for_var(name, str(current_value) if current_value else "") + default = str(current_value) if current_value else "" + + validator = TypeValidator() + + while True: + try: + value = session.prompt( + f"{prompt_text}: ", + default=default, + validator=validator, + complete_while_typing=False, + ) + + if vtype == "boolean": + return value.lower() in ("true", "yes", "1") + elif vtype == "integer": + return int(value) if value else 0 + elif vtype == "float": + return float(value) if value else 0.0 + else: + return value + + except Exception: + continue + + def edit_config( + self, + config: dict[str, Any], + schema: Optional[dict] = None, + ) -> dict[str, Any]: + """Edit a configuration dictionary.""" + for key, value in config.items(): + if isinstance(value, dict): + console_print(f"\n[bold]{key}:[/bold]") + schema_props = schema.get("properties", {}) if schema else {} + config[key] = self.edit_config(value, schema_props.get(key)) + else: + vtype = self._get_type_from_schema(key, schema) if schema else "string" + config[key] = self.prompt_for_value(key, value, vtype) + return config + + def _get_type_from_schema(self, key: str, schema: Optional[dict]) -> str: + """Get the expected type from a JSON schema.""" + if not schema or "properties" not in schema: + return "string" + + prop = schema["properties"].get(key, {}) + prop_type = prop.get("type", "string") + + if prop_type == "integer": + return "integer" + elif prop_type == "number": + return "float" + elif prop_type == "boolean": + return "boolean" + elif prop_type == "object": + return "string" + else: + return "string"