2025-11-04 05:57:23 +01:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
"""
|
|
|
|
|
Advanced input handler for PR Assistant with editor mode, file inclusion, and image support.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import base64
|
|
|
|
|
import mimetypes
|
2025-11-04 08:09:12 +01:00
|
|
|
import re
|
2025-11-04 05:57:23 +01:00
|
|
|
import readline
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
from typing import Optional
|
2025-11-04 08:09:12 +01:00
|
|
|
|
2025-11-04 05:57:23 +01:00
|
|
|
# from pr.ui.colors import Colors # Avoid import issues
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class AdvancedInputHandler:
|
|
|
|
|
"""Handles advanced input with editor mode, file inclusion, and image support."""
|
|
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
|
self.editor_mode = False
|
|
|
|
|
self.setup_readline()
|
|
|
|
|
|
|
|
|
|
def setup_readline(self):
|
|
|
|
|
"""Setup readline with basic completer."""
|
|
|
|
|
try:
|
|
|
|
|
# Simple completer that doesn't interfere
|
|
|
|
|
def completer(text, state):
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
readline.set_completer(completer)
|
2025-11-04 08:09:12 +01:00
|
|
|
readline.parse_and_bind("tab: complete")
|
2025-11-04 05:57:23 +01:00
|
|
|
except:
|
|
|
|
|
pass # Readline not available
|
|
|
|
|
|
|
|
|
|
def toggle_editor_mode(self):
|
|
|
|
|
"""Toggle between simple and editor input modes."""
|
|
|
|
|
self.editor_mode = not self.editor_mode
|
|
|
|
|
mode = "Editor" if self.editor_mode else "Simple"
|
|
|
|
|
print(f"\nSwitched to {mode.lower()} input mode.")
|
|
|
|
|
|
|
|
|
|
def get_input(self, prompt: str = "You> ") -> Optional[str]:
|
|
|
|
|
"""Get input from user, handling different modes."""
|
|
|
|
|
try:
|
|
|
|
|
if self.editor_mode:
|
|
|
|
|
return self._get_editor_input(prompt)
|
|
|
|
|
else:
|
|
|
|
|
return self._get_simple_input(prompt)
|
|
|
|
|
except KeyboardInterrupt:
|
|
|
|
|
return None
|
|
|
|
|
except EOFError:
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
def _get_simple_input(self, prompt: str) -> Optional[str]:
|
|
|
|
|
"""Get simple input with file completion."""
|
|
|
|
|
try:
|
|
|
|
|
user_input = input(prompt).strip()
|
|
|
|
|
|
|
|
|
|
if not user_input:
|
|
|
|
|
return ""
|
|
|
|
|
|
|
|
|
|
# Check for special commands
|
2025-11-04 08:09:12 +01:00
|
|
|
if user_input.lower() == "/editor":
|
2025-11-04 05:57:23 +01:00
|
|
|
self.toggle_editor_mode()
|
|
|
|
|
return self.get_input(prompt) # Recurse to get new input
|
|
|
|
|
|
|
|
|
|
# Process file inclusions and images
|
|
|
|
|
processed_input = self._process_input(user_input)
|
|
|
|
|
return processed_input
|
|
|
|
|
|
|
|
|
|
except KeyboardInterrupt:
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
def _get_editor_input(self, prompt: str) -> Optional[str]:
|
|
|
|
|
"""Get multi-line input for editor mode."""
|
|
|
|
|
try:
|
2025-11-04 08:10:37 +01:00
|
|
|
print("Editor mode: Enter your message. Type 'END' on a new line to finish.")
|
2025-11-04 05:57:23 +01:00
|
|
|
print("Type '/simple' to switch back to simple mode.")
|
|
|
|
|
|
|
|
|
|
lines = []
|
|
|
|
|
while True:
|
|
|
|
|
try:
|
|
|
|
|
line = input()
|
2025-11-04 08:09:12 +01:00
|
|
|
if line.strip().lower() == "end":
|
2025-11-04 05:57:23 +01:00
|
|
|
break
|
2025-11-04 08:09:12 +01:00
|
|
|
elif line.strip().lower() == "/simple":
|
2025-11-04 05:57:23 +01:00
|
|
|
self.toggle_editor_mode()
|
|
|
|
|
return self.get_input(prompt) # Switch back and get input
|
|
|
|
|
lines.append(line)
|
|
|
|
|
except EOFError:
|
|
|
|
|
break
|
|
|
|
|
|
2025-11-04 08:09:12 +01:00
|
|
|
content = "\n".join(lines).strip()
|
2025-11-04 05:57:23 +01:00
|
|
|
|
|
|
|
|
if not content:
|
|
|
|
|
return ""
|
|
|
|
|
|
|
|
|
|
# Process file inclusions and images
|
|
|
|
|
processed_content = self._process_input(content)
|
|
|
|
|
return processed_content
|
|
|
|
|
|
|
|
|
|
except KeyboardInterrupt:
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
def _process_input(self, text: str) -> str:
|
|
|
|
|
"""Process input text for file inclusions and images."""
|
|
|
|
|
# Process @[filename] inclusions
|
|
|
|
|
text = self._process_file_inclusions(text)
|
|
|
|
|
|
|
|
|
|
# Process image inclusions (look for image file paths)
|
|
|
|
|
text = self._process_image_inclusions(text)
|
|
|
|
|
|
|
|
|
|
return text
|
|
|
|
|
|
|
|
|
|
def _process_file_inclusions(self, text: str) -> str:
|
|
|
|
|
"""Replace @[filename] with file contents."""
|
2025-11-04 08:09:12 +01:00
|
|
|
|
2025-11-04 05:57:23 +01:00
|
|
|
def replace_file(match):
|
|
|
|
|
filename = match.group(1).strip()
|
|
|
|
|
try:
|
|
|
|
|
path = Path(filename).expanduser().resolve()
|
|
|
|
|
if path.exists() and path.is_file():
|
2025-11-04 08:09:12 +01:00
|
|
|
with open(path, encoding="utf-8", errors="replace") as f:
|
2025-11-04 05:57:23 +01:00
|
|
|
content = f.read()
|
|
|
|
|
return f"\n--- File: {filename} ---\n{content}\n--- End of {filename} ---\n"
|
|
|
|
|
else:
|
|
|
|
|
return f"[File not found: {filename}]"
|
|
|
|
|
except Exception as e:
|
|
|
|
|
return f"[Error reading file {filename}: {e}]"
|
|
|
|
|
|
|
|
|
|
# Replace @[filename] patterns
|
2025-11-04 08:09:12 +01:00
|
|
|
pattern = r"@\[([^\]]+)\]"
|
2025-11-04 05:57:23 +01:00
|
|
|
return re.sub(pattern, replace_file, text)
|
|
|
|
|
|
|
|
|
|
def _process_image_inclusions(self, text: str) -> str:
|
|
|
|
|
"""Process image file references and encode them."""
|
|
|
|
|
# Find potential image file paths
|
|
|
|
|
words = text.split()
|
|
|
|
|
processed_parts = []
|
|
|
|
|
|
|
|
|
|
for word in words:
|
|
|
|
|
# Check if it's a file path that exists and is an image
|
|
|
|
|
try:
|
|
|
|
|
path = Path(word.strip()).expanduser().resolve()
|
|
|
|
|
if path.exists() and path.is_file():
|
|
|
|
|
mime_type, _ = mimetypes.guess_type(str(path))
|
2025-11-04 08:09:12 +01:00
|
|
|
if mime_type and mime_type.startswith("image/"):
|
2025-11-04 05:57:23 +01:00
|
|
|
# Encode image
|
2025-11-04 08:09:12 +01:00
|
|
|
with open(path, "rb") as f:
|
|
|
|
|
image_data = base64.b64encode(f.read()).decode("utf-8")
|
2025-11-04 05:57:23 +01:00
|
|
|
|
|
|
|
|
# Replace with data URL
|
2025-11-04 08:09:12 +01:00
|
|
|
processed_parts.append(
|
|
|
|
|
f"[Image: {path.name}]\ndata:{mime_type};base64,{image_data}\n"
|
|
|
|
|
)
|
2025-11-04 05:57:23 +01:00
|
|
|
continue
|
|
|
|
|
except:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
processed_parts.append(word)
|
|
|
|
|
|
2025-11-04 08:09:12 +01:00
|
|
|
return " ".join(processed_parts)
|
2025-11-04 05:57:23 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
# Global instance
|
|
|
|
|
input_handler = AdvancedInputHandler()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_advanced_input(prompt: str = "You> ") -> Optional[str]:
|
|
|
|
|
"""Get advanced input from user."""
|
|
|
|
|
return input_handler.get_input(prompt)
|