Source code for ensemble_analyzer.cli.pickle_editor.tui

"""
Interactive Terminal User Interface for MatplotlibPickleEditor.

Interactive interface based on InquirerPy and Rich.
"""

import sys
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from .core import MatplotlibPickleEditor

# InquirerPy (required for TUI)
try:
    from InquirerPy import inquirer
    from InquirerPy.base.control import Choice
    from InquirerPy.separator import Separator
    INQUIRER_AVAILABLE = True
except ImportError:
    INQUIRER_AVAILABLE = False

# Rich (required for colored output)
try:
    from rich.console import Console
    from rich.table import Table
    from rich.panel import Panel
    RICH_AVAILABLE = True
    console = Console()
except ImportError:
    RICH_AVAILABLE = False
    console = None


[docs] class InteractiveTUI: """ Terminal User Interface for the graph editor using InquirerPy. """ def __init__(self, editor: 'MatplotlibPickleEditor'): """ Initialize the TUI with a loaded editor instance. Args: editor (MatplotlibPickleEditor): The backend editor instance. """ if not INQUIRER_AVAILABLE: raise RuntimeError( "InquirerPy not installed. Run: pip install InquirerPy" ) self.editor = editor self.console = console if RICH_AVAILABLE else None
[docs] def print_panel(self, message: str, title: str = "Info", style: str = "cyan") -> None: """ Print a formatted panel. Args: message: Message to display title: Panel title style: Border color style """ if self.console: self.console.print(Panel(message, title=title, border_style=style)) else: print(f"\n{title}: {message}\n")
[docs] def show_current_state(self) -> None: """ Display a formatted table of the current figure state (Labels, Colors). """ labels = self.editor.get_legend_labels() colors = self.editor.get_line_colors() if not labels: self.print_panel("No legend found", "Warning", "yellow") return if self.console: table = Table(title="Current Legend State", show_header=True, header_style="bold cyan") table.add_column("Index", style="dim", width=8) table.add_column("Label", style="bold") table.add_column("Color", style="magenta") for idx, label in labels.items(): color = colors.get(label, "N/A") table.add_row(str(idx), label, color) self.console.print(table) else: print("\n=== Current State ===") for idx, label in labels.items(): color = colors.get(label, "N/A") print(f" [{idx}] {label} (color: {color})") print()
[docs] def rename_labels_flow(self) -> None: """Interactive flow to rename labels.""" labels = self.editor.get_legend_labels() if not labels: self.print_panel("No labels to rename", "Error", "red") return # Select label to rename choices = [ Choice(value=label, name=f"{label} [{idx}]") for idx, label in labels.items() ] choices.append(Separator()) choices.append(Choice(value=None, name="← Back")) selected = inquirer.select( message="Select label to rename:", choices=choices, default=None ).execute() if selected is None: return # Enter new name new_name = inquirer.text( message=f"New name for '{selected}':", default=selected ).execute() if new_name and new_name != selected: self.editor.rename_legend_labels({selected: new_name}) self.print_panel(f"'{selected}' → '{new_name}'", "Success", "green")
[docs] def change_colors_flow(self) -> None: """Interactive flow to change colors.""" labels = self.editor.get_legend_labels() colors = self.editor.get_line_colors() if not labels: self.print_panel("No label found", "Error", "red") return # Select label choices = [ Choice(value=label, name=f"{label} (current: {colors.get(label, 'N/A')})") for label in labels.values() ] choices.append(Separator()) choices.append(Choice(value=None, name="← Back")) selected = inquirer.select( message="Select label to change color:", choices=choices, default=None ).execute() if selected is None: return # Choose color input method color_method = inquirer.select( message="How do you want to specify the color?", choices=[ Choice(value="preset", name="Choose from predefined palette"), Choice(value="custom", name="Enter manually (name or hex)"), Choice(value=None, name="← Cancel") ], default="preset" ).execute() if color_method is None: return if color_method == "preset": # Predefined palette with preview color_choices = [ Choice(value=c, name=f"{c}") for c in self.editor.COMMON_COLORS ] color_choices.append(Separator()) color_choices.append(Choice(value=None, name="← Cancel")) new_color = inquirer.select( message="Select color:", choices=color_choices, default=None ).execute() else: # Manual input new_color = inquirer.text( message="Color (name or hex #RRGGBB):", validate=lambda x: len(x) > 0 ).execute() if new_color: changed = self.editor.change_line_colors({selected: new_color}) if changed > 0: self.print_panel( f"Color of '{selected}' changed to {new_color}", "Success", "green" ) else: self.print_panel( f"Unable to change color (invalid color?)", "Error", "red" )
[docs] def change_linestyle_flow(self) -> None: """Interactive flow to change line style.""" labels = self.editor.get_legend_labels() if not labels: self.print_panel("No label found", "Error", "red") return choices = [Choice(value=label, name=label) for label in labels.values()] choices.append(Separator()) choices.append(Choice(value=None, name="← Back")) selected = inquirer.select( message="Select label to change line style:", choices=choices, default=None ).execute() if selected is None: return # Common styles style_choices = [ Choice(value='-', name="Solid (-)"), Choice(value='--', name="Dashed (--)"), Choice(value=':', name="Dotted (:)"), Choice(value='-.', name="Dash-dot (-.)"), Choice(value='custom', name="Enter manually"), Choice(value=None, name="← Cancel") ] style_choice = inquirer.select( message="Select style:", choices=style_choices, default='-' ).execute() if style_choice is None: return if style_choice == 'custom': new_ls = inquirer.text( message="New line style (e.g. '-', '--', ':', '-.'):", default='-' ).execute() else: new_ls = style_choice changed = self.editor.change_line_linestyle({selected: new_ls}) if changed > 0: self.print_panel( f"Line style of '{selected}' changed to {new_ls}", "Success", "green" ) else: self.print_panel("Unable to change style", "Error", "red")
[docs] def change_linewidth_flow(self) -> None: """Interactive flow to change line width.""" labels = self.editor.get_legend_labels() if not labels: self.print_panel("No label found", "Error", "red") return choices = [Choice(value=label, name=label) for label in labels.values()] choices.append(Separator()) choices.append(Choice(value=None, name="← Back")) selected = inquirer.select( message="Select label to change line width:", choices=choices, default=None ).execute() if selected is None: return new_width = inquirer.text( message="New width (float):", default="1.5" ).execute() try: width_val = float(new_width) changed = self.editor.change_line_linewidth({selected: width_val}) if changed > 0: self.print_panel( f"Width of '{selected}' changed to {width_val}", "Success", "green" ) else: self.print_panel("Unable to change width", "Error", "red") except ValueError: self.print_panel("Invalid width value", "Error", "red")
[docs] def change_alpha_flow(self) -> None: """Interactive flow to change transparency (alpha).""" labels = self.editor.get_legend_labels() if not labels: self.print_panel("No label found", "Error", "red") return choices = [Choice(value=label, name=label) for label in labels.values()] choices.append(Separator()) choices.append(Choice(value=None, name="← Back")) selected = inquirer.select( message="Select label to change transparency (alpha):", choices=choices, default=None ).execute() if selected is None: return new_alpha = inquirer.text( message="New alpha (0–1):", default="1.0" ).execute() try: alpha_val = float(new_alpha) changed = self.editor.change_line_alpha({selected: alpha_val}) if changed > 0: self.print_panel( f"Alpha of '{selected}' changed to {alpha_val}", "Success", "green" ) else: self.print_panel("Unable to change alpha", "Error", "red") except ValueError: self.print_panel("Invalid alpha value", "Error", "red")
[docs] def change_visibility_flow(self) -> None: """Interactive flow to change visibility.""" labels = self.editor.get_legend_labels() if not labels: self.print_panel("No label found", "Error", "red") return choices = [Choice(value=label, name=label) for label in labels.values()] choices.append(Separator()) choices.append(Choice(value=None, name="← Back")) selected = inquirer.select( message="Select label to toggle visibility:", choices=choices, default=None ).execute() if selected is None: return visible = inquirer.select( message=f"Set visibility for '{selected}':", choices=[ Choice(value=True, name="Visible (Show)"), Choice(value=False, name="Hidden (Hide)"), Choice(value=None, name="← Cancel") ], default=True ).execute() if visible is not None: changed = self.editor.change_line_visibility({selected: visible}) status = "Visible" if visible else "Hidden" if changed > 0: self.print_panel( f"'{selected}' is now {status}", "Success", "green" ) else: self.print_panel("Unable to change visibility", "Error", "red")
[docs] def save_flow(self) -> None: """Interactive flow to save.""" from pathlib import Path # Output format format_choice = inquirer.select( message="Save format:", choices=[ Choice(value="pickle", name="Pickle (editable later)"), Choice(value="png", name="PNG (image)"), Choice(value="pdf", name="PDF (vector)"), Choice(value="svg", name="SVG (vector)"), Choice(value=None, name="← Cancel") ], default="pickle" ).execute() if format_choice is None: return # Output path default_name = self.editor.pickle_path.stem if format_choice != "pickle": default_name = f"{default_name}_modified" custom_path = inquirer.confirm( message="Do you want to specify a custom path?", default=False ).execute() output_path = None if custom_path: path_str = inquirer.text( message="Output path:", default=f"{default_name}.{format_choice}" ).execute() output_path = Path(path_str) try: saved_path = self.editor.save(output_path, format_choice) self.print_panel(f"Saved: {saved_path}", "Success", "green") except Exception as e: self.print_panel(f"Error saving: {e}", "Error", "red")
[docs] def run(self) -> None: """ Start the main interactive event loop. Displays menus and handles user input until exit. """ if self.console: self.console.clear() self.console.print( Panel.fit( "[bold cyan]Interactive Matplotlib Pickle Editor[/]\n" f"File: {self.editor.pickle_path}", border_style="cyan" ) ) while True: # Show state self.show_current_state() # Main menu action = inquirer.select( message="What do you want to do?", choices=[ Choice(value="rename", name="📝 Rename legend labels"), Choice(value="color", name="🎨 Change line colors"), Choice(value="linestyle", name="⎯⎯ Change line style"), Choice(value="linewidth", name="➖ Change line width"), Choice(value="alpha", name="☰ Change transparency"), Choice(value="visibility", name="👁️ Change line visibility"), Separator(), # Choice(value="preview", name="👁️ Preview figure"), Choice(value="save", name="💾 Save changes"), Separator(), Choice(value="reload", name="🔄 Reload original file"), Choice(value="exit", name="🚪 Exit"), ], default="rename" ).execute() if action == "rename": self.rename_labels_flow() elif action == "color": self.change_colors_flow() elif action == "linestyle": self.change_linestyle_flow() elif action == "linewidth": self.change_linewidth_flow() elif action == "alpha": self.change_alpha_flow() elif action == "preview": self.print_panel( "Not implemented yet, without having a freeze of the TUI", "Error", "red" ) # self.editor.preview() elif action == 'visibility': self.change_visibility_flow() elif action == "save": self.save_flow() elif action == "reload": if self.editor.has_modifications(): confirm = inquirer.confirm( message="You have unsaved changes. Reload anyway?", default=False ).execute() if not confirm: continue self.editor.load() self.print_panel("File reloaded", "Success", "green") elif action == "exit": if self.editor.has_modifications(): confirm = inquirer.confirm( message="You have unsaved changes. Exit anyway?", default=False ).execute() if not confirm: continue if self.console: self.console.print("\n[cyan]Goodbye! 👋[/]\n") else: print("\nGoodbye!\n") break