Initial commit.
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
from pr.ui.colors import Colors
|
||||
from pr.ui.rendering import highlight_code, render_markdown
|
||||
from pr.ui.display import display_tool_call, print_autonomous_header
|
||||
|
||||
__all__ = ['Colors', 'highlight_code', 'render_markdown', 'display_tool_call', 'print_autonomous_header']
|
||||
@@ -0,0 +1,14 @@
|
||||
class Colors:
|
||||
RESET = '\033[0m'
|
||||
BOLD = '\033[1m'
|
||||
RED = '\033[91m'
|
||||
GREEN = '\033[92m'
|
||||
YELLOW = '\033[93m'
|
||||
BLUE = '\033[94m'
|
||||
MAGENTA = '\033[95m'
|
||||
CYAN = '\033[96m'
|
||||
GRAY = '\033[90m'
|
||||
WHITE = '\033[97m'
|
||||
BG_BLUE = '\033[44m'
|
||||
BG_GREEN = '\033[42m'
|
||||
BG_RED = '\033[41m'
|
||||
@@ -0,0 +1,199 @@
|
||||
import difflib
|
||||
from typing import List, Tuple, Dict, Optional
|
||||
from .colors import Colors
|
||||
|
||||
|
||||
class DiffStats:
|
||||
def __init__(self):
|
||||
self.insertions = 0
|
||||
self.deletions = 0
|
||||
self.modifications = 0
|
||||
self.files_changed = 0
|
||||
|
||||
@property
|
||||
def total_changes(self):
|
||||
return self.insertions + self.deletions
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.files_changed} file(s) changed, {self.insertions} insertions(+), {self.deletions} deletions(-)"
|
||||
|
||||
|
||||
class DiffLine:
|
||||
def __init__(self, line_type: str, content: str, old_line_num: Optional[int] = None,
|
||||
new_line_num: Optional[int] = None):
|
||||
self.line_type = line_type
|
||||
self.content = content
|
||||
self.old_line_num = old_line_num
|
||||
self.new_line_num = new_line_num
|
||||
|
||||
def format(self, show_line_nums: bool = True) -> str:
|
||||
color = {
|
||||
'add': Colors.GREEN,
|
||||
'delete': Colors.RED,
|
||||
'context': Colors.GRAY,
|
||||
'header': Colors.CYAN,
|
||||
'stats': Colors.BLUE
|
||||
}.get(self.line_type, Colors.RESET)
|
||||
|
||||
prefix = {
|
||||
'add': '+ ',
|
||||
'delete': '- ',
|
||||
'context': ' ',
|
||||
'header': '',
|
||||
'stats': ''
|
||||
}.get(self.line_type, ' ')
|
||||
|
||||
if show_line_nums and self.line_type in ('add', 'delete', 'context'):
|
||||
old_num = str(self.old_line_num) if self.old_line_num else ' '
|
||||
new_num = str(self.new_line_num) if self.new_line_num else ' '
|
||||
line_num_str = f"{Colors.YELLOW}{old_num:>4} {new_num:>4}{Colors.RESET} "
|
||||
else:
|
||||
line_num_str = ''
|
||||
|
||||
return f"{line_num_str}{color}{prefix}{self.content}{Colors.RESET}"
|
||||
|
||||
|
||||
class DiffDisplay:
|
||||
def __init__(self, context_lines: int = 3):
|
||||
self.context_lines = context_lines
|
||||
|
||||
def create_diff(self, old_content: str, new_content: str,
|
||||
filename: str = "file") -> Tuple[List[DiffLine], DiffStats]:
|
||||
old_lines = old_content.splitlines(keepends=True)
|
||||
new_lines = new_content.splitlines(keepends=True)
|
||||
|
||||
diff_lines = []
|
||||
stats = DiffStats()
|
||||
stats.files_changed = 1
|
||||
|
||||
diff = difflib.unified_diff(
|
||||
old_lines, new_lines,
|
||||
fromfile=f"a/{filename}",
|
||||
tofile=f"b/{filename}",
|
||||
n=self.context_lines
|
||||
)
|
||||
|
||||
old_line_num = 0
|
||||
new_line_num = 0
|
||||
|
||||
for line in diff:
|
||||
if line.startswith('---') or line.startswith('+++'):
|
||||
diff_lines.append(DiffLine('header', line.rstrip()))
|
||||
elif line.startswith('@@'):
|
||||
diff_lines.append(DiffLine('header', line.rstrip()))
|
||||
old_line_num, new_line_num = self._parse_hunk_header(line)
|
||||
elif line.startswith('+'):
|
||||
stats.insertions += 1
|
||||
diff_lines.append(DiffLine('add', line[1:].rstrip(), None, new_line_num))
|
||||
new_line_num += 1
|
||||
elif line.startswith('-'):
|
||||
stats.deletions += 1
|
||||
diff_lines.append(DiffLine('delete', line[1:].rstrip(), old_line_num, None))
|
||||
old_line_num += 1
|
||||
elif line.startswith(' '):
|
||||
diff_lines.append(DiffLine('context', line[1:].rstrip(), old_line_num, new_line_num))
|
||||
old_line_num += 1
|
||||
new_line_num += 1
|
||||
|
||||
stats.modifications = min(stats.insertions, stats.deletions)
|
||||
|
||||
return diff_lines, stats
|
||||
|
||||
def _parse_hunk_header(self, header: str) -> Tuple[int, int]:
|
||||
try:
|
||||
parts = header.split('@@')[1].strip().split()
|
||||
old_start = int(parts[0].split(',')[0].replace('-', ''))
|
||||
new_start = int(parts[1].split(',')[0].replace('+', ''))
|
||||
return old_start, new_start
|
||||
except (IndexError, ValueError):
|
||||
return 0, 0
|
||||
|
||||
def render_diff(self, diff_lines: List[DiffLine], stats: DiffStats,
|
||||
show_line_nums: bool = True, show_stats: bool = True) -> str:
|
||||
output = []
|
||||
|
||||
if show_stats:
|
||||
output.append(f"\n{Colors.BOLD}{Colors.BLUE}{'=' * 60}{Colors.RESET}")
|
||||
output.append(f"{Colors.BOLD}{Colors.BLUE}DIFF SUMMARY{Colors.RESET}")
|
||||
output.append(f"{Colors.BOLD}{Colors.BLUE}{'=' * 60}{Colors.RESET}")
|
||||
output.append(f"{Colors.BLUE}{stats}{Colors.RESET}\n")
|
||||
|
||||
for line in diff_lines:
|
||||
output.append(line.format(show_line_nums))
|
||||
|
||||
if show_stats:
|
||||
output.append(f"\n{Colors.BOLD}{Colors.BLUE}{'=' * 60}{Colors.RESET}\n")
|
||||
|
||||
return '\n'.join(output)
|
||||
|
||||
def display_file_diff(self, old_content: str, new_content: str,
|
||||
filename: str = "file", show_line_nums: bool = True) -> str:
|
||||
diff_lines, stats = self.create_diff(old_content, new_content, filename)
|
||||
|
||||
if not diff_lines:
|
||||
return f"{Colors.GRAY}No changes detected{Colors.RESET}"
|
||||
|
||||
return self.render_diff(diff_lines, stats, show_line_nums)
|
||||
|
||||
def display_side_by_side(self, old_content: str, new_content: str,
|
||||
filename: str = "file", width: int = 80) -> str:
|
||||
old_lines = old_content.splitlines()
|
||||
new_lines = new_content.splitlines()
|
||||
|
||||
matcher = difflib.SequenceMatcher(None, old_lines, new_lines)
|
||||
output = []
|
||||
|
||||
output.append(f"\n{Colors.BOLD}{Colors.BLUE}{'=' * width}{Colors.RESET}")
|
||||
output.append(f"{Colors.BOLD}{Colors.BLUE}SIDE-BY-SIDE COMPARISON: {filename}{Colors.RESET}")
|
||||
output.append(f"{Colors.BOLD}{Colors.BLUE}{'=' * width}{Colors.RESET}\n")
|
||||
|
||||
half_width = (width - 5) // 2
|
||||
|
||||
for tag, i1, i2, j1, j2 in matcher.get_opcodes():
|
||||
if tag == 'equal':
|
||||
for i, (old_line, new_line) in enumerate(zip(old_lines[i1:i2], new_lines[j1:j2])):
|
||||
old_display = old_line[:half_width].ljust(half_width)
|
||||
new_display = new_line[:half_width].ljust(half_width)
|
||||
output.append(f"{Colors.GRAY}{old_display} | {new_display}{Colors.RESET}")
|
||||
elif tag == 'replace':
|
||||
max_lines = max(i2 - i1, j2 - j1)
|
||||
for i in range(max_lines):
|
||||
old_line = old_lines[i1 + i] if i1 + i < i2 else ""
|
||||
new_line = new_lines[j1 + i] if j1 + i < j2 else ""
|
||||
old_display = old_line[:half_width].ljust(half_width)
|
||||
new_display = new_line[:half_width].ljust(half_width)
|
||||
output.append(f"{Colors.RED}{old_display}{Colors.RESET} | {Colors.GREEN}{new_display}{Colors.RESET}")
|
||||
elif tag == 'delete':
|
||||
for old_line in old_lines[i1:i2]:
|
||||
old_display = old_line[:half_width].ljust(half_width)
|
||||
output.append(f"{Colors.RED}{old_display} | {' ' * half_width}{Colors.RESET}")
|
||||
elif tag == 'insert':
|
||||
for new_line in new_lines[j1:j2]:
|
||||
new_display = new_line[:half_width].ljust(half_width)
|
||||
output.append(f"{' ' * half_width} | {Colors.GREEN}{new_display}{Colors.RESET}")
|
||||
|
||||
output.append(f"\n{Colors.BOLD}{Colors.BLUE}{'=' * width}{Colors.RESET}\n")
|
||||
return '\n'.join(output)
|
||||
|
||||
|
||||
def display_diff(old_content: str, new_content: str, filename: str = "file",
|
||||
format_type: str = "unified", context_lines: int = 3) -> str:
|
||||
displayer = DiffDisplay(context_lines)
|
||||
|
||||
if format_type == "side-by-side":
|
||||
return displayer.display_side_by_side(old_content, new_content, filename)
|
||||
else:
|
||||
return displayer.display_file_diff(old_content, new_content, filename)
|
||||
|
||||
|
||||
def get_diff_stats(old_content: str, new_content: str) -> Dict[str, int]:
|
||||
displayer = DiffDisplay()
|
||||
_, stats = displayer.create_diff(old_content, new_content)
|
||||
|
||||
return {
|
||||
'insertions': stats.insertions,
|
||||
'deletions': stats.deletions,
|
||||
'modifications': stats.modifications,
|
||||
'total_changes': stats.total_changes,
|
||||
'files_changed': stats.files_changed
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import json
|
||||
from typing import Dict, Any
|
||||
from pr.ui.colors import Colors
|
||||
|
||||
def display_tool_call(tool_name, arguments, status="running", result=None):
|
||||
status_icons = {
|
||||
"running": ("⚙", Colors.YELLOW),
|
||||
"success": ("✓", Colors.GREEN),
|
||||
"error": ("✗", Colors.RED)
|
||||
}
|
||||
|
||||
icon, color = status_icons.get(status, ("•", Colors.WHITE))
|
||||
|
||||
print(f"\n{Colors.BOLD}{'═' * 80}{Colors.RESET}")
|
||||
print(f"{color}{icon} {Colors.BOLD}{Colors.CYAN}TOOL: {tool_name}{Colors.RESET}")
|
||||
print(f"{Colors.BOLD}{'─' * 80}{Colors.RESET}")
|
||||
|
||||
if arguments:
|
||||
print(f"{Colors.YELLOW}Parameters:{Colors.RESET}")
|
||||
for key, value in arguments.items():
|
||||
value_str = str(value)
|
||||
if len(value_str) > 100:
|
||||
value_str = value_str[:100] + "..."
|
||||
print(f" {Colors.CYAN}• {key}:{Colors.RESET} {value_str}")
|
||||
|
||||
if result is not None and status != "running":
|
||||
print(f"\n{Colors.YELLOW}Result:{Colors.RESET}")
|
||||
result_str = json.dumps(result, indent=2) if isinstance(result, dict) else str(result)
|
||||
|
||||
if len(result_str) > 500:
|
||||
result_str = result_str[:500] + f"\n{Colors.GRAY}... (truncated){Colors.RESET}"
|
||||
|
||||
if status == "success":
|
||||
print(f"{Colors.GREEN}{result_str}{Colors.RESET}")
|
||||
elif status == "error":
|
||||
print(f"{Colors.RED}{result_str}{Colors.RESET}")
|
||||
else:
|
||||
print(result_str)
|
||||
|
||||
print(f"{Colors.BOLD}{'═' * 80}{Colors.RESET}\n")
|
||||
|
||||
def print_autonomous_header(task):
|
||||
print(f"{Colors.BOLD}Task:{Colors.RESET} {task}")
|
||||
print(f"{Colors.GRAY}r will work continuously until the task is complete.{Colors.RESET}")
|
||||
print(f"{Colors.GRAY}Press Ctrl+C twice to interrupt.{Colors.RESET}\n")
|
||||
print(f"{Colors.BOLD}{'═' * 80}{Colors.RESET}\n")
|
||||
@@ -0,0 +1,198 @@
|
||||
from typing import List, Dict, Optional
|
||||
from datetime import datetime
|
||||
from .colors import Colors
|
||||
from .progress import ProgressBar
|
||||
|
||||
|
||||
class EditOperation:
|
||||
def __init__(self, op_type: str, filepath: str, start_pos: int = 0,
|
||||
end_pos: int = 0, content: str = "", old_content: str = ""):
|
||||
self.op_type = op_type
|
||||
self.filepath = filepath
|
||||
self.start_pos = start_pos
|
||||
self.end_pos = end_pos
|
||||
self.content = content
|
||||
self.old_content = old_content
|
||||
self.timestamp = datetime.now()
|
||||
self.status = "pending"
|
||||
|
||||
def format_operation(self) -> str:
|
||||
op_colors = {
|
||||
'INSERT': Colors.GREEN,
|
||||
'REPLACE': Colors.YELLOW,
|
||||
'DELETE': Colors.RED,
|
||||
'WRITE': Colors.BLUE
|
||||
}
|
||||
|
||||
color = op_colors.get(self.op_type, Colors.RESET)
|
||||
status_icon = {
|
||||
'pending': '○',
|
||||
'in_progress': '◐',
|
||||
'completed': '●',
|
||||
'failed': '✗'
|
||||
}.get(self.status, '○')
|
||||
|
||||
return f"{color}{status_icon} [{self.op_type}]{Colors.RESET} {self.filepath}"
|
||||
|
||||
def format_details(self, show_content: bool = True) -> str:
|
||||
output = [self.format_operation()]
|
||||
|
||||
if self.op_type in ('INSERT', 'REPLACE'):
|
||||
output.append(f" {Colors.GRAY}Position: {self.start_pos}-{self.end_pos}{Colors.RESET}")
|
||||
|
||||
if show_content:
|
||||
if self.old_content:
|
||||
lines = self.old_content.split('\n')
|
||||
preview = lines[0][:60] + ('...' if len(lines[0]) > 60 or len(lines) > 1 else '')
|
||||
output.append(f" {Colors.RED}- {preview}{Colors.RESET}")
|
||||
|
||||
if self.content:
|
||||
lines = self.content.split('\n')
|
||||
preview = lines[0][:60] + ('...' if len(lines[0]) > 60 or len(lines) > 1 else '')
|
||||
output.append(f" {Colors.GREEN}+ {preview}{Colors.RESET}")
|
||||
|
||||
return '\n'.join(output)
|
||||
|
||||
|
||||
class EditTracker:
|
||||
def __init__(self):
|
||||
self.operations: List[EditOperation] = []
|
||||
self.current_file: Optional[str] = None
|
||||
|
||||
def add_operation(self, op_type: str, filepath: str, **kwargs) -> EditOperation:
|
||||
op = EditOperation(op_type, filepath, **kwargs)
|
||||
self.operations.append(op)
|
||||
self.current_file = filepath
|
||||
return op
|
||||
|
||||
def mark_in_progress(self, operation: EditOperation):
|
||||
operation.status = "in_progress"
|
||||
|
||||
def mark_completed(self, operation: EditOperation):
|
||||
operation.status = "completed"
|
||||
|
||||
def mark_failed(self, operation: EditOperation):
|
||||
operation.status = "failed"
|
||||
|
||||
def get_stats(self) -> Dict[str, int]:
|
||||
stats = {
|
||||
'total': len(self.operations),
|
||||
'completed': sum(1 for op in self.operations if op.status == 'completed'),
|
||||
'pending': sum(1 for op in self.operations if op.status == 'pending'),
|
||||
'in_progress': sum(1 for op in self.operations if op.status == 'in_progress'),
|
||||
'failed': sum(1 for op in self.operations if op.status == 'failed')
|
||||
}
|
||||
return stats
|
||||
|
||||
def get_completion_percentage(self) -> float:
|
||||
if not self.operations:
|
||||
return 0.0
|
||||
stats = self.get_stats()
|
||||
return (stats['completed'] / stats['total']) * 100
|
||||
|
||||
def display_progress(self) -> str:
|
||||
if not self.operations:
|
||||
return f"{Colors.GRAY}No edit operations tracked{Colors.RESET}"
|
||||
|
||||
output = []
|
||||
output.append(f"\n{Colors.BOLD}{Colors.BLUE}{'=' * 60}{Colors.RESET}")
|
||||
output.append(f"{Colors.BOLD}{Colors.BLUE}EDIT OPERATIONS PROGRESS{Colors.RESET}")
|
||||
output.append(f"{Colors.BOLD}{Colors.BLUE}{'=' * 60}{Colors.RESET}\n")
|
||||
|
||||
stats = self.get_stats()
|
||||
completion = self.get_completion_percentage()
|
||||
|
||||
progress_bar = ProgressBar(total=stats['total'], width=40)
|
||||
progress_bar.current = stats['completed']
|
||||
bar_display = progress_bar._get_bar_display()
|
||||
|
||||
output.append(f"Progress: {bar_display}")
|
||||
output.append(f"{Colors.BLUE}Total: {stats['total']}, Completed: {stats['completed']}, "
|
||||
f"Pending: {stats['pending']}, Failed: {stats['failed']}{Colors.RESET}\n")
|
||||
|
||||
output.append(f"{Colors.BOLD}Recent Operations:{Colors.RESET}")
|
||||
for i, op in enumerate(self.operations[-5:], 1):
|
||||
output.append(f"{i}. {op.format_operation()}")
|
||||
|
||||
output.append(f"\n{Colors.BOLD}{Colors.BLUE}{'=' * 60}{Colors.RESET}\n")
|
||||
return '\n'.join(output)
|
||||
|
||||
def display_timeline(self, show_content: bool = False) -> str:
|
||||
if not self.operations:
|
||||
return f"{Colors.GRAY}No edit operations tracked{Colors.RESET}"
|
||||
|
||||
output = []
|
||||
output.append(f"\n{Colors.BOLD}{Colors.BLUE}{'=' * 60}{Colors.RESET}")
|
||||
output.append(f"{Colors.BOLD}{Colors.BLUE}EDIT TIMELINE{Colors.RESET}")
|
||||
output.append(f"{Colors.BOLD}{Colors.BLUE}{'=' * 60}{Colors.RESET}\n")
|
||||
|
||||
for i, op in enumerate(self.operations, 1):
|
||||
timestamp = op.timestamp.strftime("%H:%M:%S")
|
||||
output.append(f"{Colors.GRAY}[{timestamp}]{Colors.RESET} {i}.")
|
||||
output.append(op.format_details(show_content))
|
||||
output.append("")
|
||||
|
||||
stats = self.get_stats()
|
||||
output.append(f"{Colors.BOLD}Summary:{Colors.RESET}")
|
||||
output.append(f"{Colors.BLUE}Total operations: {stats['total']}, "
|
||||
f"Completed: {stats['completed']}, Failed: {stats['failed']}{Colors.RESET}")
|
||||
|
||||
output.append(f"\n{Colors.BOLD}{Colors.BLUE}{'=' * 60}{Colors.RESET}\n")
|
||||
return '\n'.join(output)
|
||||
|
||||
def display_summary(self) -> str:
|
||||
if not self.operations:
|
||||
return f"{Colors.GRAY}No edits to summarize{Colors.RESET}"
|
||||
|
||||
stats = self.get_stats()
|
||||
files_modified = len(set(op.filepath for op in self.operations))
|
||||
|
||||
output = []
|
||||
output.append(f"\n{Colors.BOLD}{Colors.GREEN}{'=' * 60}{Colors.RESET}")
|
||||
output.append(f"{Colors.BOLD}{Colors.GREEN}EDIT SUMMARY{Colors.RESET}")
|
||||
output.append(f"{Colors.BOLD}{Colors.GREEN}{'=' * 60}{Colors.RESET}\n")
|
||||
|
||||
output.append(f"{Colors.GREEN}Files Modified: {files_modified}{Colors.RESET}")
|
||||
output.append(f"{Colors.GREEN}Total Operations: {stats['total']}{Colors.RESET}")
|
||||
output.append(f"{Colors.GREEN}Successful: {stats['completed']}{Colors.RESET}")
|
||||
|
||||
if stats['failed'] > 0:
|
||||
output.append(f"{Colors.RED}Failed: {stats['failed']}{Colors.RESET}")
|
||||
|
||||
output.append(f"\n{Colors.BOLD}Operations by Type:{Colors.RESET}")
|
||||
op_types = {}
|
||||
for op in self.operations:
|
||||
op_types[op.op_type] = op_types.get(op.op_type, 0) + 1
|
||||
|
||||
for op_type, count in sorted(op_types.items()):
|
||||
output.append(f" {op_type}: {count}")
|
||||
|
||||
output.append(f"\n{Colors.BOLD}{Colors.GREEN}{'=' * 60}{Colors.RESET}\n")
|
||||
return '\n'.join(output)
|
||||
|
||||
def clear(self):
|
||||
self.operations.clear()
|
||||
self.current_file = None
|
||||
|
||||
|
||||
tracker = EditTracker()
|
||||
|
||||
|
||||
def track_edit(op_type: str, filepath: str, **kwargs) -> EditOperation:
|
||||
return tracker.add_operation(op_type, filepath, **kwargs)
|
||||
|
||||
|
||||
def display_edit_progress() -> str:
|
||||
return tracker.display_progress()
|
||||
|
||||
|
||||
def display_edit_timeline(show_content: bool = False) -> str:
|
||||
return tracker.display_timeline(show_content)
|
||||
|
||||
|
||||
def display_edit_summary() -> str:
|
||||
return tracker.display_summary()
|
||||
|
||||
|
||||
def clear_tracker():
|
||||
tracker.clear()
|
||||
@@ -0,0 +1,69 @@
|
||||
import json
|
||||
import sys
|
||||
from typing import Any, Dict, List
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class OutputFormatter:
|
||||
|
||||
def __init__(self, format_type: str = 'text', quiet: bool = False):
|
||||
self.format_type = format_type
|
||||
self.quiet = quiet
|
||||
|
||||
def output(self, data: Any, message_type: str = 'response'):
|
||||
if self.quiet and message_type not in ['error', 'result']:
|
||||
return
|
||||
|
||||
if self.format_type == 'json':
|
||||
self._output_json(data, message_type)
|
||||
elif self.format_type == 'structured':
|
||||
self._output_structured(data, message_type)
|
||||
else:
|
||||
self._output_text(data, message_type)
|
||||
|
||||
def _output_json(self, data: Any, message_type: str):
|
||||
output = {
|
||||
'type': message_type,
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
'data': data
|
||||
}
|
||||
print(json.dumps(output, indent=2))
|
||||
|
||||
def _output_structured(self, data: Any, message_type: str):
|
||||
if isinstance(data, dict):
|
||||
for key, value in data.items():
|
||||
print(f"{key}: {value}")
|
||||
elif isinstance(data, list):
|
||||
for item in data:
|
||||
print(f"- {item}")
|
||||
else:
|
||||
print(data)
|
||||
|
||||
def _output_text(self, data: Any, message_type: str):
|
||||
if isinstance(data, (dict, list)):
|
||||
print(json.dumps(data, indent=2))
|
||||
else:
|
||||
print(data)
|
||||
|
||||
def error(self, message: str):
|
||||
if self.format_type == 'json':
|
||||
self._output_json({'error': message}, 'error')
|
||||
else:
|
||||
print(f"Error: {message}", file=sys.stderr)
|
||||
|
||||
def success(self, message: str):
|
||||
if not self.quiet:
|
||||
if self.format_type == 'json':
|
||||
self._output_json({'success': message}, 'success')
|
||||
else:
|
||||
print(message)
|
||||
|
||||
def info(self, message: str):
|
||||
if not self.quiet:
|
||||
if self.format_type == 'json':
|
||||
self._output_json({'info': message}, 'info')
|
||||
else:
|
||||
print(message)
|
||||
|
||||
def result(self, data: Any):
|
||||
self.output(data, 'result')
|
||||
@@ -0,0 +1,76 @@
|
||||
import sys
|
||||
import time
|
||||
import threading
|
||||
|
||||
|
||||
class ProgressIndicator:
|
||||
|
||||
def __init__(self, message: str = "Working", show: bool = True):
|
||||
self.message = message
|
||||
self.show = show
|
||||
self.running = False
|
||||
self.thread = None
|
||||
|
||||
def __enter__(self):
|
||||
if self.show:
|
||||
self.start()
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
if self.show:
|
||||
self.stop()
|
||||
|
||||
def start(self):
|
||||
self.running = True
|
||||
self.thread = threading.Thread(target=self._animate, daemon=True)
|
||||
self.thread.start()
|
||||
|
||||
def stop(self):
|
||||
if self.running:
|
||||
self.running = False
|
||||
if self.thread:
|
||||
self.thread.join(timeout=1.0)
|
||||
sys.stdout.write('\r' + ' ' * (len(self.message) + 10) + '\r')
|
||||
sys.stdout.flush()
|
||||
|
||||
def _animate(self):
|
||||
spinner = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
|
||||
idx = 0
|
||||
|
||||
while self.running:
|
||||
sys.stdout.write(f'\r{spinner[idx]} {self.message}...')
|
||||
sys.stdout.flush()
|
||||
idx = (idx + 1) % len(spinner)
|
||||
time.sleep(0.1)
|
||||
|
||||
|
||||
class ProgressBar:
|
||||
|
||||
def __init__(self, total: int, description: str = "Progress", width: int = 40):
|
||||
self.total = total
|
||||
self.description = description
|
||||
self.width = width
|
||||
self.current = 0
|
||||
|
||||
def update(self, amount: int = 1):
|
||||
self.current += amount
|
||||
self._display()
|
||||
|
||||
def _display(self):
|
||||
if self.total == 0:
|
||||
percent = 100
|
||||
else:
|
||||
percent = int((self.current / self.total) * 100)
|
||||
|
||||
filled = int((self.current / self.total) * self.width) if self.total > 0 else self.width
|
||||
bar = '█' * filled + '░' * (self.width - filled)
|
||||
|
||||
sys.stdout.write(f'\r{self.description}: |{bar}| {percent}% ({self.current}/{self.total})')
|
||||
sys.stdout.flush()
|
||||
|
||||
if self.current >= self.total:
|
||||
sys.stdout.write('\n')
|
||||
|
||||
def finish(self):
|
||||
self.current = self.total
|
||||
self._display()
|
||||
@@ -0,0 +1,90 @@
|
||||
import re
|
||||
from pr.ui.colors import Colors
|
||||
from pr.config import LANGUAGE_KEYWORDS
|
||||
|
||||
def highlight_code(code, language=None, syntax_highlighting=True):
|
||||
if not syntax_highlighting:
|
||||
return code
|
||||
|
||||
if not language:
|
||||
if 'def ' in code or 'import ' in code:
|
||||
language = 'python'
|
||||
elif 'function ' in code or 'const ' in code:
|
||||
language = 'javascript'
|
||||
elif 'public ' in code or 'class ' in code:
|
||||
language = 'java'
|
||||
|
||||
if language and language in LANGUAGE_KEYWORDS:
|
||||
keywords = LANGUAGE_KEYWORDS[language]
|
||||
for keyword in keywords:
|
||||
pattern = r'\b' + re.escape(keyword) + r'\b'
|
||||
code = re.sub(pattern, f"{Colors.BLUE}{keyword}{Colors.RESET}", code)
|
||||
|
||||
code = re.sub(r'"([^"]*)"', f'{Colors.GREEN}"\\1"{Colors.RESET}', code)
|
||||
code = re.sub(r"'([^']*)'", f"{Colors.GREEN}'\\1'{Colors.RESET}", code)
|
||||
|
||||
code = re.sub(r'#(.*)$', f'{Colors.GRAY}#\\1{Colors.RESET}', code, flags=re.MULTILINE)
|
||||
code = re.sub(r'//(.*)$', f'{Colors.GRAY}//\\1{Colors.RESET}', code, flags=re.MULTILINE)
|
||||
|
||||
return code
|
||||
|
||||
def render_markdown(text, syntax_highlighting=True):
|
||||
if not syntax_highlighting:
|
||||
return text
|
||||
|
||||
code_blocks = []
|
||||
def extract_code_block(match):
|
||||
lang = match.group(1) or ''
|
||||
code = match.group(2)
|
||||
highlighted_code = highlight_code(code.strip('\n'), lang, syntax_highlighting)
|
||||
placeholder = f"%%CODEBLOCK{len(code_blocks)}%%"
|
||||
full_block = f'{Colors.GRAY}```{lang}{Colors.RESET}\n{highlighted_code}\n{Colors.GRAY}```{Colors.RESET}'
|
||||
code_blocks.append(full_block)
|
||||
return placeholder
|
||||
|
||||
text = re.sub(r'```(\w*)\n(.*?)\n?```', extract_code_block, text, flags=re.DOTALL)
|
||||
|
||||
inline_codes = []
|
||||
def extract_inline_code(match):
|
||||
code = match.group(1)
|
||||
placeholder = f"%%INLINECODE{len(inline_codes)}%%"
|
||||
inline_codes.append(f'{Colors.YELLOW}{code}{Colors.RESET}')
|
||||
return placeholder
|
||||
|
||||
text = re.sub(r'`([^`]+)`', extract_inline_code, text)
|
||||
|
||||
lines = text.split('\n')
|
||||
processed_lines = []
|
||||
for line in lines:
|
||||
if line.startswith('### '):
|
||||
line = f'{Colors.BOLD}{Colors.GREEN}{line[4:]}{Colors.RESET}'
|
||||
elif line.startswith('## '):
|
||||
line = f'{Colors.BOLD}{Colors.BLUE}{line[3:]}{Colors.RESET}'
|
||||
elif line.startswith('# '):
|
||||
line = f'{Colors.BOLD}{Colors.MAGENTA}{line[2:]}{Colors.RESET}'
|
||||
elif line.startswith('> '):
|
||||
line = f'{Colors.CYAN}> {line[2:]}{Colors.RESET}'
|
||||
elif re.match(r'^\s*[\*\-\+]\s', line):
|
||||
match = re.match(r'^(\s*)([\*\-\+])(\s+.*)', line)
|
||||
if match:
|
||||
line = f"{match.group(1)}{Colors.YELLOW}{match.group(2)}{Colors.RESET}{match.group(3)}"
|
||||
elif re.match(r'^\s*\d+\.\s', line):
|
||||
match = re.match(r'^(\s*)(\d+\.)(\s+.*)', line)
|
||||
if match:
|
||||
line = f"{match.group(1)}{Colors.YELLOW}{match.group(2)}{Colors.RESET}{match.group(3)}"
|
||||
processed_lines.append(line)
|
||||
text = '\n'.join(processed_lines)
|
||||
|
||||
text = re.sub(r'\[(.*?)\]\((.*?)\)', f'{Colors.BLUE}\\1{Colors.RESET}{Colors.GRAY}(\\2){Colors.RESET}', text)
|
||||
text = re.sub(r'~~(.*?)~~', f'{Colors.GRAY}\\1{Colors.RESET}', text)
|
||||
text = re.sub(r'\*\*(.*?)\*\*', f'{Colors.BOLD}\\1{Colors.RESET}', text)
|
||||
text = re.sub(r'__(.*?)__', f'{Colors.BOLD}\\1{Colors.RESET}', text)
|
||||
text = re.sub(r'\*(.*?)\*', f'{Colors.CYAN}\\1{Colors.RESET}', text)
|
||||
text = re.sub(r'_(.*?)_', f'{Colors.CYAN}\\1{Colors.RESET}', text)
|
||||
|
||||
for i, code in enumerate(inline_codes):
|
||||
text = text.replace(f'%%INLINECODE{i}%%', code)
|
||||
for i, block in enumerate(code_blocks):
|
||||
text = text.replace(f'%%CODEBLOCK{i}%%', block)
|
||||
|
||||
return text
|
||||
Reference in New Issue
Block a user