diff --git a/src/tui/editor.py b/src/tui/editor.py new file mode 100644 index 0000000..dfb2326 --- /dev/null +++ b/src/tui/editor.py @@ -0,0 +1,266 @@ +"""TUI editor module for interactive data editing.""" + +from typing import Any +from rich.console import Console +from rich.text import Text +from rich.prompt import Prompt +from rich.box import ROUNDED +from rich.panel import Panel + + +class DataEditor: + """Interactive editor for data manipulation.""" + + def __init__(self, console: Console = None): + """Initialize the data editor. + + Args: + console: Rich Console instance + """ + self.console = console or Console() + self.data = None + self.format_name = None + self.modified = False + + def load_data(self, data: Any, format_name: str) -> None: + """Load data for editing. + + Args: + data: Data object to edit + format_name: Format of the data + """ + self.data = data + self.format_name = format_name + self.modified = False + + def _display_header(self) -> None: + """Display editor header.""" + header = Text() + header.append("Data Editor\n", style='bold cyan') + header.append("─" * 40 + "\n", style='dim') + header.append("Commands:\n", style='yellow') + header.append(" edit - Edit a value\n", style='white') + header.append(" add - Add a new key\n", style='white') + header.append(" delete - Delete a key\n", style='white') + header.append(" show - Show current data\n", style='white') + header.append(" save - Save changes\n", style='white') + header.append(" quit - Exit without saving\n", style='white') + header.append(" help - Show this help\n", style='white') + + panel = Panel(header, title="Help", box=ROUNDED, expand=False) + self.console.print(panel) + + def _parse_path(self, path: str) -> list: + """Parse a dot-separated path. + + Args: + path: Dot-separated path string + + Returns: + List of path segments + """ + return path.split('.') + + def _get_nested_value(self, data: Any, path: list) -> Any: + """Get value from nested dictionary. + + Args: + data: Data structure + path: List of path segments + + Returns: + Value at the path + """ + current = data + for segment in path: + if isinstance(current, dict) and segment in current: + current = current[segment] + else: + return None + return current + + def _set_nested_value(self, data: Any, path: list, value: Any) -> bool: + """Set value in nested dictionary. + + Args: + data: Data structure + path: List of path segments + value: Value to set + + Returns: + True if successful, False otherwise + """ + current = data + for i, segment in enumerate(path[:-1]): + if isinstance(current, dict) and segment in current: + current = current[segment] + else: + return False + + if isinstance(current, dict): + current[path[-1]] = value + return True + return False + + def _delete_nested_value(self, data: Any, path: list) -> bool: + """Delete value from nested dictionary. + + Args: + data: Data structure + path: List of path segments + + Returns: + True if successful, False otherwise + """ + current = data + for i, segment in enumerate(path[:-1]): + if isinstance(current, dict) and segment in current: + current = current[segment] + else: + return False + + if isinstance(current, dict) and path[-1] in current: + del current[path[-1]] + return True + return False + + def _cmd_edit(self, args: list) -> bool: + """Handle edit command. + + Args: + args: Command arguments + + Returns: + True if command was handled + """ + if len(args) < 2: + self.console.print("Usage: edit ", style='red') + return True + + path = self._parse_path(args[0]) + value = ' '.join(args[1:]) + + try: + import json + value = json.loads(value) + except json.JSONDecodeError: + pass + + if self._set_nested_value(self.data, path, value): + self.modified = True + self.console.print(f"Updated: {'.'.join(path)}", style='green') + else: + self.console.print(f"Path not found: {'.'.join(path)}", style='red') + + return True + + def _cmd_add(self, args: list) -> bool: + """Handle add command. + + Args: + args: Command arguments + + Returns: + True if command was handled + """ + if len(args) < 2: + self.console.print("Usage: add ", style='red') + return True + + path = self._parse_path(args[0]) + value = ' '.join(args[1:]) + + try: + import json + value = json.loads(value) + except json.JSONDecodeError: + pass + + if self._set_nested_value(self.data, path, value): + self.modified = True + self.console.print(f"Added: {'.'.join(path)}", style='green') + else: + self.console.print(f"Could not add: {'.'.join(path)}", style='red') + + return True + + def _cmd_delete(self, args: list) -> bool: + """Handle delete command. + + Args: + args: Command arguments + + Returns: + True if command was handled + """ + if not args: + self.console.print("Usage: delete ", style='red') + return True + + path = self._parse_path(args[0]) + + if self._delete_nested_value(self.data, path): + self.modified = True + self.console.print(f"Deleted: {'.'.join(path)}", style='green') + else: + self.console.print(f"Path not found: {'.'.join(path)}", style='red') + + return True + + def _cmd_show(self) -> bool: + """Handle show command.""" + from .viewer import DataViewer + + viewer = DataViewer() + viewer.view_data(self.data, self.format_name, title="Current Data") + return True + + def _cmd_save(self) -> bool: + """Handle save command.""" + self.console.print("Data saved (in-memory)", style='green') + return True + + def run(self) -> bool: + """Run the interactive editor. + + Returns: + True if data was modified, False otherwise + """ + self._display_header() + self._cmd_show() + + while True: + try: + command = Prompt.ask("\n[cyan]Command[/cyan]").strip() + + if not command: + continue + + parts = command.split() + cmd = parts[0].lower() + args = parts[1:] + + if cmd in ('quit', 'exit', 'q'): + break + elif cmd in ('help', 'h', '?'): + self._display_header() + elif cmd == 'edit': + self._cmd_edit(args) + elif cmd == 'add': + self._cmd_add(args) + elif cmd == 'delete': + self._cmd_delete(args) + elif cmd in ('show', 's'): + self._cmd_show() + elif cmd in ('save', 'w'): + self._cmd_save() + else: + self.console.print(f"Unknown command: {cmd}", style='red') + + except KeyboardInterrupt: + self.console.print("\nExiting...", style='yellow') + break + except Exception as e: + self.console.print(f"Error: {e}", style='red') + + return self.modified