Add validators, TUI, and file watcher modules
Some checks failed
CI / test (push) Has been cancelled
Some checks failed
CI / test (push) Has been cancelled
This commit is contained in:
266
src/tui/editor.py
Normal file
266
src/tui/editor.py
Normal file
@@ -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 <path> <value> - Edit a value\n", style='white')
|
||||||
|
header.append(" add <path> <value> - Add a new key\n", style='white')
|
||||||
|
header.append(" delete <path> - 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 <path> <value>", 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 <path> <value>", 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 <path>", 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
|
||||||
Reference in New Issue
Block a user