""" Interactive shell for the Edison application. """ import logging import os import sys from termcolor import colored from prompt_toolkit import PromptSession from prompt_toolkit.history import FileHistory from prompt_toolkit.auto_suggest import AutoSuggestFromHistory from prompt_toolkit.lexers import PygmentsLexer from pygments.lexers.shell import BashLexer from edison.core import api_client, command_executor from edison.utils import os_utils, markdown_utils logger = logging.getLogger(__name__) def handle_streaming_output_interactive(token): """ Handle a token from the streaming API response in interactive mode. Args: token (str): A token from the streaming response. """ # Print the token without a newline and flush immediately print(token, end='', flush=True) def get_command_explanation(client, command, structured=False): """ Get an explanation for a command. Args: client: The OpenAI client. command (str): The command to explain. structured (bool): Whether to use structured explanation format. Returns: str: The explanation of the command. """ try: system_prompt = """Explain this shell command in simple terms, breaking down each part:""" if structured: system_prompt = """ Explain this shell command in a structured format with these sections: ## Overview A brief overview of what the command does ## Command Breakdown Break down each part of the command with explanations ## Options/Flags Explain any options or flags used ## Examples Provide 1-2 simple examples of variations ## Cautions Note any potential issues or cautions """ response = client.chat.completions.create( model="gpt-4o-mini", messages=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": command} ] ) return response.choices[0].message.content except Exception as e: logger.error(f"Error getting command explanation: {e}") return "Could not generate explanation." def interactive_mode(client, config): """ Start an interactive shell. Args: client: The OpenAI client. config (dict): The configuration dictionary. """ # Create history file path history_path = os.path.expanduser("~/.edison_history") history_dir = os.path.dirname(history_path) os.makedirs(history_dir, exist_ok=True) # Create prompt session history = FileHistory(history_path) session = PromptSession( history=history, auto_suggest=AutoSuggestFromHistory(), lexer=PygmentsLexer(BashLexer) ) # Print welcome message print(colored("Edison Interactive Shell", "blue", attrs=["bold"])) print("Type your queries in natural language.") print("Type '!help' for help, '!exit' or '!quit' to exit, or press Ctrl+D to exit.") while True: try: # Get user input query = session.prompt("edison> ") # Handle special commands if query.strip().lower() in ["!exit", "!quit", "exit", "quit"]: break elif query.strip() == "!help": show_help() continue elif not query.strip(): continue # Generate command try: # Use streaming if enabled in config if config.get("streaming", True): # Check if we should show the "Generating command: " prefix if config.get("show_generating_prefix", True): print(colored("Generating command: ", "yellow"), end='', flush=True) try: command = api_client.generate_command_streaming( client, config, query, handle_streaming_output_interactive ) print() # Add a newline after streaming is complete except KeyboardInterrupt: print("\nCommand generation cancelled.") continue else: print(colored("Generating command...", "yellow")) command = api_client.generate_command(client, config, query) # Print command with appropriate styling based on config ui_config = config.get("ui", {}) if ui_config.get("rich_formatting", True): command_style = ui_config.get("command_style", "panel") theme = ui_config.get("theme", "monokai") shell = config.get("shell", "bash") if command_style == "panel": # Use rich panel style from edison.utils.markdown_utils import print_command_rich print_command_rich(command, shell=shell, theme=theme) else: # Use simple highlighting print("Command: " + colored(command, 'blue')) else: # Use traditional styling print("Command: " + colored(command, 'blue')) # Ask user what to do with the same prompt as non-interactive mode prompt_text = "Execute command? [Y]es [n]o [m]odify (prompt/command) [c]opy to clipboard [x]plain ==> " if os_utils.missing_posix_display(): prompt_text = "Execute command? [Y]es [n]o [m]odify (prompt/command) [x]plain ==> " action = session.prompt(prompt_text) if action.lower() in ["y", "yes", "e", "execute", ""]: # Execute command shell = config.get("shell", os.environ.get("SHELL", "bash")) try: command_executor.execute_command(shell, command) except Exception as e: logger.error(f"Error executing command: {e}") print(colored(f"Error executing command: {e}", 'red')) elif action.lower() in ["m", "modify"]: # Ask whether to modify prompt or command mod_choice = session.prompt("Modify [p]rompt or [c]ommand? [c] ==> ") if mod_choice.lower() in ["p", "prompt"]: # Modify the prompt (natural language description) new_prompt = session.prompt("Modified prompt: ") if not new_prompt.strip(): print(colored("Empty prompt. Command execution cancelled.", "yellow")) continue # Generate new command from modified prompt print(colored("Generating new command from modified prompt...", "yellow")) # Use streaming if enabled if config.get("streaming", True): # Check if we should show the "Generating command: " prefix if config.get("show_generating_prefix", True): print(colored("Generating command: ", "yellow"), end='', flush=True) try: new_command = api_client.generate_command_streaming( client, config, new_prompt, handle_streaming_output_interactive ) print() # Add a newline after streaming is complete except KeyboardInterrupt: print("\nCommand generation cancelled.") continue else: new_command = api_client.generate_command(client, config, new_prompt) # Print the new command with styling ui_config = config.get("ui", {}) if ui_config.get("rich_formatting", True): command_style = ui_config.get("command_style", "panel") theme = ui_config.get("theme", "monokai") shell = config.get("shell", "bash") if command_style == "panel": # Use rich panel style from edison.utils.markdown_utils import print_command_rich print_command_rich(new_command, shell=shell, theme=theme) else: # Use simple highlighting print("Command: " + colored(new_command, 'blue')) else: # Use traditional styling print("Command: " + colored(new_command, 'blue')) # Ask for confirmation confirm = session.prompt("Execute this command? [Y/n] ") if confirm.lower() in ["n", "no"]: print(colored("Command execution cancelled.", "yellow")) continue # Execute the new command shell = config.get("shell", os.environ.get("SHELL", "bash")) try: command_executor.execute_command(shell, new_command) except Exception as e: logger.error(f"Error executing command: {e}") print(colored(f"Error executing command: {e}", 'red')) else: # Modify the command directly modified_command = session.prompt("Modified command: ", default=command) if not modified_command.strip(): print(colored("Empty command. Command execution cancelled.", "yellow")) continue # Print the modified command with styling ui_config = config.get("ui", {}) if ui_config.get("rich_formatting", True): command_style = ui_config.get("command_style", "panel") theme = ui_config.get("theme", "monokai") shell = config.get("shell", "bash") if command_style == "panel": # Use rich panel style from edison.utils.markdown_utils import print_command_rich print_command_rich(modified_command, shell=shell, theme=theme) else: # Use simple highlighting print("Command: " + colored(modified_command, 'blue')) else: # Use traditional styling print("Command: " + colored(modified_command, 'blue')) # Ask for confirmation confirm = session.prompt("Execute modified command? [Y/n] ") if confirm.lower() in ["n", "no"]: print(colored("Command execution cancelled.", "yellow")) continue # Execute the modified command shell = config.get("shell", os.environ.get("SHELL", "bash")) try: command_executor.execute_command(shell, modified_command) except Exception as e: logger.error(f"Error executing command: {e}") print(colored(f"Error executing command: {e}", 'red')) elif action.lower() in ["c", "copy"]: # Copy command to clipboard if not os_utils.missing_posix_display(): try: import pyperclip pyperclip.copy(command) print(colored("Command copied to clipboard.", "green")) except Exception as e: print(colored(f"Error copying to clipboard: {e}", "red")) elif action.lower() in ["x", "explain"]: # Explain command print(colored("Generating explanation...", "yellow")) # Get UI configuration ui_config = config.get("ui", {}) structured = ui_config.get("structured_explanations", True) use_rich = ui_config.get("rich_formatting", True) # Generate explanation explanation = get_command_explanation(client, command, structured=structured) if use_rich: print() # Add a blank line before the explanation # Format the explanation with rich formatting formatted_explanation = markdown_utils.format_command_explanation(explanation, use_rich=True) print(formatted_explanation) else: print(colored("\nExplanation:", "green", attrs=["bold"])) # Format the explanation with terminal-friendly markdown formatted_explanation = markdown_utils.format_command_explanation(explanation) print(formatted_explanation) elif action.lower() in ["n", "no", "s", "skip"]: # Skip print(colored("Command skipped.", "yellow")) else: # Skip if input not recognized print(colored("Command skipped - input not recognized.", "yellow")) except Exception as e: logger.error(f"Error: {e}") print(colored(f"Error: {e}", "red")) except KeyboardInterrupt: continue except EOFError: break print("Goodbye!") def show_help(): """ Show help information. """ help_text = """ Edison Interactive Shell Help Commands: !help - Show this help message !exit, !quit - Exit the interactive shell exit, quit - Exit the interactive shell Ctrl+D - Exit the interactive shell Actions: y, yes, - Execute the generated command n, no - Skip the command m, modify - Modify the prompt or command before executing When selected, you'll be asked whether to modify: - [p]rompt: Change the natural language description - [c]ommand: Directly edit the generated command c, copy - Copy the command to clipboard x, explain - Get an explanation of the command """ print(help_text)