omnimancer-cli 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- omnimancer/__init__.py +42 -0
- omnimancer/__main__.py +9 -0
- omnimancer/cli/__init__.py +8 -0
- omnimancer/cli/agent_manager.py +826 -0
- omnimancer/cli/agent_wizard.py +944 -0
- omnimancer/cli/approval_formatter.py +734 -0
- omnimancer/cli/approval_integration.py +734 -0
- omnimancer/cli/approval_prompt.py +1286 -0
- omnimancer/cli/batch_approval_display.py +617 -0
- omnimancer/cli/batch_approval_filters.py +729 -0
- omnimancer/cli/batch_operation_monitor.py +775 -0
- omnimancer/cli/commands/performance_cmd.py +624 -0
- omnimancer/cli/commands.py +757 -0
- omnimancer/cli/completion_handler.py +281 -0
- omnimancer/cli/config_setup_wizard.py +783 -0
- omnimancer/cli/dynamic_commands.py +371 -0
- omnimancer/cli/handlers/__init__.py +18 -0
- omnimancer/cli/handlers/agent_handler.py +865 -0
- omnimancer/cli/handlers/agent_persona_handler.py +1048 -0
- omnimancer/cli/handlers/custom_agent_handler.py +352 -0
- omnimancer/cli/handlers/permissions_handler.py +1609 -0
- omnimancer/cli/interface.py +6695 -0
- omnimancer/cli/performance_dashboard.py +612 -0
- omnimancer/core/__init__.py +34 -0
- omnimancer/core/agent/__init__.py +256 -0
- omnimancer/core/agent/agent_config.py +715 -0
- omnimancer/core/agent/agent_switcher.py +668 -0
- omnimancer/core/agent/approval_context.py +460 -0
- omnimancer/core/agent/approval_dialog.py +902 -0
- omnimancer/core/agent/approval_interface.py +628 -0
- omnimancer/core/agent/approval_manager.py +760 -0
- omnimancer/core/agent/cli_integration.py +470 -0
- omnimancer/core/agent/config.py +845 -0
- omnimancer/core/agent/custom_agent_integration.py +654 -0
- omnimancer/core/agent/diff_renderer.py +706 -0
- omnimancer/core/agent/file_content_display.py +503 -0
- omnimancer/core/agent/file_modification_workflow.py +658 -0
- omnimancer/core/agent/file_system_manager.py +1790 -0
- omnimancer/core/agent/input_handler.py +614 -0
- omnimancer/core/agent/metrics_collector.py +771 -0
- omnimancer/core/agent/optimization_engine.py +729 -0
- omnimancer/core/agent/performance_monitor.py +717 -0
- omnimancer/core/agent/persona.py +870 -0
- omnimancer/core/agent/persona_validator.py +672 -0
- omnimancer/core/agent/program_executor.py +748 -0
- omnimancer/core/agent/proposed_changes_integration.py +663 -0
- omnimancer/core/agent/read_before_write_errors.py +492 -0
- omnimancer/core/agent/read_before_write_ui.py +647 -0
- omnimancer/core/agent/rich_renderer.py +763 -0
- omnimancer/core/agent/status_config.py +238 -0
- omnimancer/core/agent/status_core.py +446 -0
- omnimancer/core/agent/status_integrated_engine.py +540 -0
- omnimancer/core/agent/status_manager.py +878 -0
- omnimancer/core/agent/status_ui_components.py +535 -0
- omnimancer/core/agent/template_applicator.py +548 -0
- omnimancer/core/agent/terminal_status_listener.py +394 -0
- omnimancer/core/agent/token_tracker.py +679 -0
- omnimancer/core/agent/types.py +60 -0
- omnimancer/core/agent/web_client.py +745 -0
- omnimancer/core/agent/workflow_orchestrator.py +826 -0
- omnimancer/core/agent_engine.py +1625 -0
- omnimancer/core/agent_mode_manager.py +706 -0
- omnimancer/core/agent_progress_ui.py +545 -0
- omnimancer/core/chat_manager.py +141 -0
- omnimancer/core/config_manager.py +2199 -0
- omnimancer/core/config_migration.py +772 -0
- omnimancer/core/config_provider.py +1974 -0
- omnimancer/core/config_security_validator.py +393 -0
- omnimancer/core/config_validator.py +768 -0
- omnimancer/core/conversation_manager.py +352 -0
- omnimancer/core/engine.py +787 -0
- omnimancer/core/enhanced_input.py +523 -0
- omnimancer/core/env_loader.py +83 -0
- omnimancer/core/fallback_manager.py +568 -0
- omnimancer/core/health_monitor.py +462 -0
- omnimancer/core/history_manager.py +462 -0
- omnimancer/core/mcp_integration_layer.py +878 -0
- omnimancer/core/models.py +1889 -0
- omnimancer/core/provider_capabilities.py +403 -0
- omnimancer/core/provider_initializer.py +450 -0
- omnimancer/core/provider_registry.py +432 -0
- omnimancer/core/security/__init__.py +16 -0
- omnimancer/core/security/approval_workflow.py +406 -0
- omnimancer/core/security/audit_logger.py +511 -0
- omnimancer/core/security/permission_controller.py +511 -0
- omnimancer/core/security/sandbox_manager.py +379 -0
- omnimancer/core/security/security_manager.py +530 -0
- omnimancer/core/setup_wizard.py +345 -0
- omnimancer/core/setup_wizard_core.py +449 -0
- omnimancer/core/setup_wizard_guide_generator.py +376 -0
- omnimancer/core/setup_wizard_provider_setup.py +454 -0
- omnimancer/core/setup_wizard_ui.py +317 -0
- omnimancer/core/setup_wizard_validation.py +371 -0
- omnimancer/core/signal_handler.py +174 -0
- omnimancer/mcp/__init__.py +11 -0
- omnimancer/mcp/client.py +725 -0
- omnimancer/mcp/manager.py +649 -0
- omnimancer/providers/__init__.py +24 -0
- omnimancer/providers/azure.py +608 -0
- omnimancer/providers/base.py +262 -0
- omnimancer/providers/bedrock.py +711 -0
- omnimancer/providers/claude.py +426 -0
- omnimancer/providers/claude_code.py +406 -0
- omnimancer/providers/cohere.py +412 -0
- omnimancer/providers/factory.py +988 -0
- omnimancer/providers/gemini.py +708 -0
- omnimancer/providers/mistral.py +554 -0
- omnimancer/providers/ollama.py +510 -0
- omnimancer/providers/openai.py +362 -0
- omnimancer/providers/openrouter.py +709 -0
- omnimancer/providers/perplexity.py +476 -0
- omnimancer/providers/vertex.py +617 -0
- omnimancer/providers/xai.py +484 -0
- omnimancer/ui/__init__.py +6 -0
- omnimancer/ui/cancellation_handler.py +235 -0
- omnimancer/ui/progress_indicator.py +318 -0
- omnimancer/utils/__init__.py +22 -0
- omnimancer/utils/error_handler.py +589 -0
- omnimancer/utils/errors.py +667 -0
- omnimancer/utils/retry.py +238 -0
- omnimancer_cli-0.1.0.dist-info/METADATA +413 -0
- omnimancer_cli-0.1.0.dist-info/RECORD +126 -0
- omnimancer_cli-0.1.0.dist-info/WHEEL +5 -0
- omnimancer_cli-0.1.0.dist-info/entry_points.txt +4 -0
- omnimancer_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- omnimancer_cli-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,763 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Rich Text Rendering System for Omnimancer Terminal Interface.
|
|
3
|
+
|
|
4
|
+
This module provides comprehensive rich text rendering capabilities for the
|
|
5
|
+
approval interface and other terminal outputs, with color support, syntax
|
|
6
|
+
highlighting, and responsive layouts.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import shutil
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from enum import Enum
|
|
13
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
14
|
+
|
|
15
|
+
from rich.box import ROUNDED, Box
|
|
16
|
+
from rich.columns import Columns
|
|
17
|
+
from rich.console import Console
|
|
18
|
+
from rich.layout import Layout
|
|
19
|
+
from rich.panel import Panel
|
|
20
|
+
from rich.progress import BarColumn, Progress, TextColumn, TimeRemainingColumn
|
|
21
|
+
from rich.rule import Rule
|
|
22
|
+
from rich.syntax import Syntax
|
|
23
|
+
from rich.table import Table
|
|
24
|
+
from rich.text import Text
|
|
25
|
+
from rich.theme import Theme
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class RiskLevel(Enum):
|
|
29
|
+
"""Risk levels for operations with associated colors."""
|
|
30
|
+
|
|
31
|
+
NONE = "none"
|
|
32
|
+
LOW = "low"
|
|
33
|
+
MEDIUM = "medium"
|
|
34
|
+
HIGH = "high"
|
|
35
|
+
CRITICAL = "critical"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class OperationType(Enum):
|
|
39
|
+
"""Types of operations for color coding."""
|
|
40
|
+
|
|
41
|
+
FILE_READ = "file_read"
|
|
42
|
+
FILE_WRITE = "file_write"
|
|
43
|
+
FILE_DELETE = "file_delete"
|
|
44
|
+
COMMAND_EXECUTE = "command_execute"
|
|
45
|
+
WEB_REQUEST = "web_request"
|
|
46
|
+
SYSTEM_CHANGE = "system_change"
|
|
47
|
+
DATA_ACCESS = "data_access"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class ColorScheme:
|
|
52
|
+
"""Color scheme configuration for different UI elements."""
|
|
53
|
+
|
|
54
|
+
# Risk level colors
|
|
55
|
+
risk_none: str = "dim white"
|
|
56
|
+
risk_low: str = "green"
|
|
57
|
+
risk_medium: str = "yellow"
|
|
58
|
+
risk_high: str = "bright_red"
|
|
59
|
+
risk_critical: str = "bold red on white"
|
|
60
|
+
|
|
61
|
+
# Operation type colors
|
|
62
|
+
op_read: str = "cyan"
|
|
63
|
+
op_write: str = "yellow"
|
|
64
|
+
op_delete: str = "red"
|
|
65
|
+
op_execute: str = "magenta"
|
|
66
|
+
op_web: str = "blue"
|
|
67
|
+
op_system: str = "bright_red"
|
|
68
|
+
|
|
69
|
+
# Diff colors
|
|
70
|
+
diff_added: str = "green"
|
|
71
|
+
diff_removed: str = "red"
|
|
72
|
+
diff_modified: str = "yellow"
|
|
73
|
+
diff_context: str = "dim white"
|
|
74
|
+
diff_header: str = "bold cyan"
|
|
75
|
+
diff_line_number: str = "dim blue"
|
|
76
|
+
|
|
77
|
+
# UI element colors
|
|
78
|
+
header: str = "bold cyan"
|
|
79
|
+
subheader: str = "cyan"
|
|
80
|
+
label: str = "bright_blue"
|
|
81
|
+
value: str = "white"
|
|
82
|
+
success: str = "bold green"
|
|
83
|
+
error: str = "bold red"
|
|
84
|
+
warning: str = "yellow"
|
|
85
|
+
info: str = "blue"
|
|
86
|
+
dim: str = "dim white"
|
|
87
|
+
|
|
88
|
+
# Interactive element colors
|
|
89
|
+
shortcut_key: str = "bold yellow"
|
|
90
|
+
shortcut_desc: str = "white"
|
|
91
|
+
selected: str = "reverse"
|
|
92
|
+
focused: str = "bold cyan"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dataclass
|
|
96
|
+
class TerminalCapabilities:
|
|
97
|
+
"""Terminal capabilities detection and configuration."""
|
|
98
|
+
|
|
99
|
+
width: int = 80
|
|
100
|
+
height: int = 24
|
|
101
|
+
supports_color: bool = True
|
|
102
|
+
supports_unicode: bool = True
|
|
103
|
+
supports_emoji: bool = True
|
|
104
|
+
color_depth: int = 256 # 8, 16, 256, or 16777216 (true color)
|
|
105
|
+
|
|
106
|
+
@classmethod
|
|
107
|
+
def detect(cls) -> "TerminalCapabilities":
|
|
108
|
+
"""Detect current terminal capabilities."""
|
|
109
|
+
# Get terminal dimensions
|
|
110
|
+
size = shutil.get_terminal_size((80, 24))
|
|
111
|
+
|
|
112
|
+
# Detect color support
|
|
113
|
+
supports_color = (
|
|
114
|
+
os.environ.get("COLORTERM") in ("truecolor", "24bit")
|
|
115
|
+
or os.environ.get("TERM", "").endswith("256color")
|
|
116
|
+
or "color" in os.environ.get("TERM", "").lower()
|
|
117
|
+
or os.environ.get("TERM", "").startswith("xterm")
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# Detect Unicode support
|
|
121
|
+
supports_unicode = True
|
|
122
|
+
try:
|
|
123
|
+
"✓".encode(
|
|
124
|
+
os.environ.get("LANG", "ascii").split(".")[1]
|
|
125
|
+
if "." in os.environ.get("LANG", "")
|
|
126
|
+
else "utf-8"
|
|
127
|
+
)
|
|
128
|
+
except (UnicodeEncodeError, LookupError):
|
|
129
|
+
supports_unicode = False
|
|
130
|
+
|
|
131
|
+
# Detect emoji support (simplified)
|
|
132
|
+
supports_emoji = (
|
|
133
|
+
supports_unicode and os.name != "nt"
|
|
134
|
+
) # Windows terminal emoji support is limited
|
|
135
|
+
|
|
136
|
+
# Detect color depth
|
|
137
|
+
color_depth = 8
|
|
138
|
+
if os.environ.get("COLORTERM") in ("truecolor", "24bit"):
|
|
139
|
+
color_depth = 16777216
|
|
140
|
+
elif "256color" in os.environ.get("TERM", ""):
|
|
141
|
+
color_depth = 256
|
|
142
|
+
elif supports_color:
|
|
143
|
+
color_depth = 16
|
|
144
|
+
|
|
145
|
+
return cls(
|
|
146
|
+
width=size.columns,
|
|
147
|
+
height=size.lines,
|
|
148
|
+
supports_color=supports_color,
|
|
149
|
+
supports_unicode=supports_unicode,
|
|
150
|
+
supports_emoji=supports_emoji,
|
|
151
|
+
color_depth=color_depth,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class RichTextRenderer:
|
|
156
|
+
"""
|
|
157
|
+
Rich text rendering system for terminal output.
|
|
158
|
+
|
|
159
|
+
Provides comprehensive formatting, color coding, and layout capabilities
|
|
160
|
+
for the approval interface and other terminal displays.
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
def __init__(
|
|
164
|
+
self,
|
|
165
|
+
console: Optional[Console] = None,
|
|
166
|
+
color_scheme: Optional[ColorScheme] = None,
|
|
167
|
+
auto_detect: bool = True,
|
|
168
|
+
):
|
|
169
|
+
"""
|
|
170
|
+
Initialize the rich text renderer.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
console: Rich console instance (creates new if not provided)
|
|
174
|
+
color_scheme: Color scheme to use (uses default if not provided)
|
|
175
|
+
auto_detect: Whether to auto-detect terminal capabilities
|
|
176
|
+
"""
|
|
177
|
+
self.console = console or Console()
|
|
178
|
+
self.color_scheme = color_scheme or ColorScheme()
|
|
179
|
+
|
|
180
|
+
# Detect terminal capabilities
|
|
181
|
+
if auto_detect:
|
|
182
|
+
self.capabilities = TerminalCapabilities.detect()
|
|
183
|
+
else:
|
|
184
|
+
self.capabilities = TerminalCapabilities()
|
|
185
|
+
|
|
186
|
+
# Create theme from color scheme
|
|
187
|
+
self.theme = self._create_theme()
|
|
188
|
+
|
|
189
|
+
# Apply theme to console
|
|
190
|
+
self.console = Console(theme=self.theme)
|
|
191
|
+
|
|
192
|
+
# Layout cache for responsive designs
|
|
193
|
+
self._layout_cache: Dict[str, Layout] = {}
|
|
194
|
+
|
|
195
|
+
def _create_theme(self) -> Theme:
|
|
196
|
+
"""Create Rich theme from color scheme."""
|
|
197
|
+
theme_dict = {
|
|
198
|
+
# Risk levels
|
|
199
|
+
"risk.none": self.color_scheme.risk_none,
|
|
200
|
+
"risk.low": self.color_scheme.risk_low,
|
|
201
|
+
"risk.medium": self.color_scheme.risk_medium,
|
|
202
|
+
"risk.high": self.color_scheme.risk_high,
|
|
203
|
+
"risk.critical": self.color_scheme.risk_critical,
|
|
204
|
+
# Operations
|
|
205
|
+
"op.read": self.color_scheme.op_read,
|
|
206
|
+
"op.write": self.color_scheme.op_write,
|
|
207
|
+
"op.delete": self.color_scheme.op_delete,
|
|
208
|
+
"op.execute": self.color_scheme.op_execute,
|
|
209
|
+
"op.web": self.color_scheme.op_web,
|
|
210
|
+
"op.system": self.color_scheme.op_system,
|
|
211
|
+
# Diff
|
|
212
|
+
"diff.added": self.color_scheme.diff_added,
|
|
213
|
+
"diff.removed": self.color_scheme.diff_removed,
|
|
214
|
+
"diff.modified": self.color_scheme.diff_modified,
|
|
215
|
+
"diff.context": self.color_scheme.diff_context,
|
|
216
|
+
"diff.header": self.color_scheme.diff_header,
|
|
217
|
+
"diff.line_number": self.color_scheme.diff_line_number,
|
|
218
|
+
# UI elements
|
|
219
|
+
"header": self.color_scheme.header,
|
|
220
|
+
"subheader": self.color_scheme.subheader,
|
|
221
|
+
"label": self.color_scheme.label,
|
|
222
|
+
"value": self.color_scheme.value,
|
|
223
|
+
"success": self.color_scheme.success,
|
|
224
|
+
"error": self.color_scheme.error,
|
|
225
|
+
"warning": self.color_scheme.warning,
|
|
226
|
+
"info": self.color_scheme.info,
|
|
227
|
+
"dim": self.color_scheme.dim,
|
|
228
|
+
# Interactive
|
|
229
|
+
"shortcut.key": self.color_scheme.shortcut_key,
|
|
230
|
+
"shortcut.desc": self.color_scheme.shortcut_desc,
|
|
231
|
+
"selected": self.color_scheme.selected,
|
|
232
|
+
"focused": self.color_scheme.focused,
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return Theme(theme_dict)
|
|
236
|
+
|
|
237
|
+
def get_risk_color(self, risk_level: Union[RiskLevel, str, int, None]) -> str:
|
|
238
|
+
"""
|
|
239
|
+
Get color style for risk level.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
risk_level: Risk level (enum, string, or numeric 0-10)
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
Style string for the risk level
|
|
246
|
+
"""
|
|
247
|
+
if risk_level is None:
|
|
248
|
+
return "risk.none"
|
|
249
|
+
|
|
250
|
+
if isinstance(risk_level, int):
|
|
251
|
+
# Convert numeric risk to level
|
|
252
|
+
if risk_level <= 2:
|
|
253
|
+
risk_level = RiskLevel.LOW
|
|
254
|
+
elif risk_level <= 4:
|
|
255
|
+
risk_level = RiskLevel.MEDIUM
|
|
256
|
+
elif risk_level <= 7:
|
|
257
|
+
risk_level = RiskLevel.HIGH
|
|
258
|
+
else:
|
|
259
|
+
risk_level = RiskLevel.CRITICAL
|
|
260
|
+
elif isinstance(risk_level, str):
|
|
261
|
+
try:
|
|
262
|
+
risk_level = RiskLevel(risk_level.lower())
|
|
263
|
+
except ValueError:
|
|
264
|
+
risk_level = RiskLevel.NONE
|
|
265
|
+
|
|
266
|
+
risk_styles = {
|
|
267
|
+
RiskLevel.NONE: "risk.none",
|
|
268
|
+
RiskLevel.LOW: "risk.low",
|
|
269
|
+
RiskLevel.MEDIUM: "risk.medium",
|
|
270
|
+
RiskLevel.HIGH: "risk.high",
|
|
271
|
+
RiskLevel.CRITICAL: "risk.critical",
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return risk_styles.get(risk_level, "risk.none")
|
|
275
|
+
|
|
276
|
+
def get_operation_color(self, operation_type: Union[OperationType, str]) -> str:
|
|
277
|
+
"""
|
|
278
|
+
Get color style for operation type.
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
operation_type: Type of operation
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
Style string for the operation type
|
|
285
|
+
"""
|
|
286
|
+
if isinstance(operation_type, str):
|
|
287
|
+
# Map string to operation type
|
|
288
|
+
op_mapping = {
|
|
289
|
+
"read": OperationType.FILE_READ,
|
|
290
|
+
"write": OperationType.FILE_WRITE,
|
|
291
|
+
"delete": OperationType.FILE_DELETE,
|
|
292
|
+
"execute": OperationType.COMMAND_EXECUTE,
|
|
293
|
+
"web": OperationType.WEB_REQUEST,
|
|
294
|
+
"system": OperationType.SYSTEM_CHANGE,
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
for key, value in op_mapping.items():
|
|
298
|
+
if key in operation_type.lower():
|
|
299
|
+
operation_type = value
|
|
300
|
+
break
|
|
301
|
+
else:
|
|
302
|
+
operation_type = OperationType.FILE_READ # Default
|
|
303
|
+
|
|
304
|
+
op_styles = {
|
|
305
|
+
OperationType.FILE_READ: "op.read",
|
|
306
|
+
OperationType.FILE_WRITE: "op.write",
|
|
307
|
+
OperationType.FILE_DELETE: "op.delete",
|
|
308
|
+
OperationType.COMMAND_EXECUTE: "op.execute",
|
|
309
|
+
OperationType.WEB_REQUEST: "op.web",
|
|
310
|
+
OperationType.SYSTEM_CHANGE: "op.system",
|
|
311
|
+
OperationType.DATA_ACCESS: "op.read",
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return op_styles.get(operation_type, "op.read")
|
|
315
|
+
|
|
316
|
+
def render_code_block(
|
|
317
|
+
self,
|
|
318
|
+
code: str,
|
|
319
|
+
language: Optional[str] = None,
|
|
320
|
+
theme: str = "monokai",
|
|
321
|
+
line_numbers: bool = True,
|
|
322
|
+
highlight_lines: Optional[List[int]] = None,
|
|
323
|
+
) -> Syntax:
|
|
324
|
+
"""
|
|
325
|
+
Render a code block with syntax highlighting.
|
|
326
|
+
|
|
327
|
+
Args:
|
|
328
|
+
code: Code content to render
|
|
329
|
+
language: Programming language for syntax highlighting
|
|
330
|
+
theme: Pygments theme to use
|
|
331
|
+
line_numbers: Whether to show line numbers
|
|
332
|
+
highlight_lines: Lines to highlight
|
|
333
|
+
|
|
334
|
+
Returns:
|
|
335
|
+
Syntax object for rendering
|
|
336
|
+
"""
|
|
337
|
+
if not language:
|
|
338
|
+
# Try to auto-detect language
|
|
339
|
+
if "import " in code or "def " in code or "class " in code:
|
|
340
|
+
language = "python"
|
|
341
|
+
elif "function" in code or "const " in code or "var " in code:
|
|
342
|
+
language = "javascript"
|
|
343
|
+
elif "{" in code and "}" in code and ":" in code:
|
|
344
|
+
language = "json"
|
|
345
|
+
else:
|
|
346
|
+
language = "text"
|
|
347
|
+
|
|
348
|
+
syntax = Syntax(
|
|
349
|
+
code,
|
|
350
|
+
language,
|
|
351
|
+
theme=theme,
|
|
352
|
+
line_numbers=line_numbers,
|
|
353
|
+
highlight_lines=set(highlight_lines) if highlight_lines else None,
|
|
354
|
+
word_wrap=True,
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
return syntax
|
|
358
|
+
|
|
359
|
+
def render_table(
|
|
360
|
+
self,
|
|
361
|
+
headers: List[str],
|
|
362
|
+
rows: List[List[Any]],
|
|
363
|
+
title: Optional[str] = None,
|
|
364
|
+
box_style: Box = ROUNDED,
|
|
365
|
+
header_style: str = "header",
|
|
366
|
+
show_footer: bool = False,
|
|
367
|
+
) -> Table:
|
|
368
|
+
"""
|
|
369
|
+
Render a formatted table.
|
|
370
|
+
|
|
371
|
+
Args:
|
|
372
|
+
headers: Column headers
|
|
373
|
+
rows: Table data rows
|
|
374
|
+
title: Optional table title
|
|
375
|
+
box_style: Box drawing style
|
|
376
|
+
header_style: Style for headers
|
|
377
|
+
show_footer: Whether to show footer
|
|
378
|
+
|
|
379
|
+
Returns:
|
|
380
|
+
Table object for rendering
|
|
381
|
+
"""
|
|
382
|
+
table = Table(
|
|
383
|
+
title=title,
|
|
384
|
+
box=box_style,
|
|
385
|
+
header_style=header_style,
|
|
386
|
+
show_footer=show_footer,
|
|
387
|
+
width=(
|
|
388
|
+
None if self.capabilities.width > 120 else self.capabilities.width - 2
|
|
389
|
+
),
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
# Add columns
|
|
393
|
+
for header in headers:
|
|
394
|
+
table.add_column(header, justify="left")
|
|
395
|
+
|
|
396
|
+
# Add rows
|
|
397
|
+
for row in rows:
|
|
398
|
+
# Convert all values to strings and apply appropriate styles
|
|
399
|
+
styled_row = []
|
|
400
|
+
for i, value in enumerate(row):
|
|
401
|
+
if isinstance(value, bool):
|
|
402
|
+
styled_value = Text(
|
|
403
|
+
"✓" if value else "✗",
|
|
404
|
+
style="success" if value else "error",
|
|
405
|
+
)
|
|
406
|
+
elif (
|
|
407
|
+
isinstance(value, (int, float)) and i > 0
|
|
408
|
+
): # Numeric columns (not first)
|
|
409
|
+
styled_value = Text(str(value), style="value")
|
|
410
|
+
else:
|
|
411
|
+
styled_value = str(value)
|
|
412
|
+
styled_row.append(styled_value)
|
|
413
|
+
|
|
414
|
+
table.add_row(*styled_row)
|
|
415
|
+
|
|
416
|
+
return table
|
|
417
|
+
|
|
418
|
+
def render_panel(
|
|
419
|
+
self,
|
|
420
|
+
content: Union[str, Text, Any],
|
|
421
|
+
title: Optional[str] = None,
|
|
422
|
+
subtitle: Optional[str] = None,
|
|
423
|
+
box_style: Box = ROUNDED,
|
|
424
|
+
border_style: Optional[str] = None,
|
|
425
|
+
padding: Tuple[int, int] = (0, 1),
|
|
426
|
+
expand: bool = True,
|
|
427
|
+
) -> Panel:
|
|
428
|
+
"""
|
|
429
|
+
Render a panel with content.
|
|
430
|
+
|
|
431
|
+
Args:
|
|
432
|
+
content: Content to display in panel
|
|
433
|
+
title: Panel title
|
|
434
|
+
subtitle: Panel subtitle
|
|
435
|
+
box_style: Box drawing style
|
|
436
|
+
border_style: Border color/style
|
|
437
|
+
padding: Padding (vertical, horizontal)
|
|
438
|
+
expand: Whether to expand to full width
|
|
439
|
+
|
|
440
|
+
Returns:
|
|
441
|
+
Panel object for rendering
|
|
442
|
+
"""
|
|
443
|
+
# Apply responsive width
|
|
444
|
+
width = None
|
|
445
|
+
if not expand and self.capabilities.width < 100:
|
|
446
|
+
width = self.capabilities.width - 4
|
|
447
|
+
|
|
448
|
+
panel = Panel(
|
|
449
|
+
content,
|
|
450
|
+
title=title,
|
|
451
|
+
subtitle=subtitle,
|
|
452
|
+
box=box_style,
|
|
453
|
+
border_style=border_style or "dim",
|
|
454
|
+
padding=padding,
|
|
455
|
+
expand=expand,
|
|
456
|
+
width=width,
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
return panel
|
|
460
|
+
|
|
461
|
+
def render_risk_indicator(
|
|
462
|
+
self,
|
|
463
|
+
risk_level: Union[RiskLevel, int],
|
|
464
|
+
show_label: bool = True,
|
|
465
|
+
show_bar: bool = True,
|
|
466
|
+
) -> Union[Text, Columns]:
|
|
467
|
+
"""
|
|
468
|
+
Render a risk level indicator.
|
|
469
|
+
|
|
470
|
+
Args:
|
|
471
|
+
risk_level: Risk level to display
|
|
472
|
+
show_label: Whether to show text label
|
|
473
|
+
show_bar: Whether to show visual bar
|
|
474
|
+
|
|
475
|
+
Returns:
|
|
476
|
+
Formatted risk indicator
|
|
477
|
+
"""
|
|
478
|
+
if isinstance(risk_level, int):
|
|
479
|
+
numeric_risk = risk_level
|
|
480
|
+
# Convert to RiskLevel
|
|
481
|
+
if risk_level <= 2:
|
|
482
|
+
risk_enum = RiskLevel.LOW
|
|
483
|
+
elif risk_level <= 4:
|
|
484
|
+
risk_enum = RiskLevel.MEDIUM
|
|
485
|
+
elif risk_level <= 7:
|
|
486
|
+
risk_enum = RiskLevel.HIGH
|
|
487
|
+
else:
|
|
488
|
+
risk_enum = RiskLevel.CRITICAL
|
|
489
|
+
else:
|
|
490
|
+
risk_enum = risk_level
|
|
491
|
+
# Estimate numeric value
|
|
492
|
+
risk_values = {
|
|
493
|
+
RiskLevel.NONE: 0,
|
|
494
|
+
RiskLevel.LOW: 2,
|
|
495
|
+
RiskLevel.MEDIUM: 5,
|
|
496
|
+
RiskLevel.HIGH: 7,
|
|
497
|
+
RiskLevel.CRITICAL: 9,
|
|
498
|
+
}
|
|
499
|
+
numeric_risk = risk_values.get(risk_enum, 0)
|
|
500
|
+
|
|
501
|
+
elements = []
|
|
502
|
+
|
|
503
|
+
if show_label:
|
|
504
|
+
risk_text = Text()
|
|
505
|
+
risk_text.append("Risk: ", style="label")
|
|
506
|
+
risk_text.append(
|
|
507
|
+
risk_enum.value.upper(), style=self.get_risk_color(risk_enum)
|
|
508
|
+
)
|
|
509
|
+
risk_text.append(f" ({numeric_risk}/10)", style="dim")
|
|
510
|
+
elements.append(risk_text)
|
|
511
|
+
|
|
512
|
+
if show_bar:
|
|
513
|
+
# Create visual risk bar
|
|
514
|
+
bar_width = min(20, self.capabilities.width // 4)
|
|
515
|
+
filled = int((numeric_risk / 10) * bar_width)
|
|
516
|
+
empty = bar_width - filled
|
|
517
|
+
|
|
518
|
+
bar = Text()
|
|
519
|
+
bar.append("[", style="dim")
|
|
520
|
+
|
|
521
|
+
# Gradient coloring for filled portion
|
|
522
|
+
for i in range(filled):
|
|
523
|
+
progress = (i + 1) / bar_width
|
|
524
|
+
if progress < 0.3:
|
|
525
|
+
style = "green"
|
|
526
|
+
elif progress < 0.6:
|
|
527
|
+
style = "yellow"
|
|
528
|
+
else:
|
|
529
|
+
style = "red"
|
|
530
|
+
bar.append("█", style=style)
|
|
531
|
+
|
|
532
|
+
bar.append("░" * empty, style="dim")
|
|
533
|
+
bar.append("]", style="dim")
|
|
534
|
+
elements.append(bar)
|
|
535
|
+
|
|
536
|
+
if len(elements) == 1:
|
|
537
|
+
return elements[0]
|
|
538
|
+
else:
|
|
539
|
+
return Columns(elements, padding=1)
|
|
540
|
+
|
|
541
|
+
def render_progress_bar(
|
|
542
|
+
self,
|
|
543
|
+
task_description: str,
|
|
544
|
+
total: Optional[int] = None,
|
|
545
|
+
completed: Optional[int] = None,
|
|
546
|
+
) -> Progress:
|
|
547
|
+
"""
|
|
548
|
+
Create a progress bar for operations.
|
|
549
|
+
|
|
550
|
+
Args:
|
|
551
|
+
task_description: Description of the task
|
|
552
|
+
total: Total number of steps
|
|
553
|
+
completed: Number of completed steps
|
|
554
|
+
|
|
555
|
+
Returns:
|
|
556
|
+
Progress bar object
|
|
557
|
+
"""
|
|
558
|
+
progress = Progress(
|
|
559
|
+
TextColumn("[progress.description]{task.description}"),
|
|
560
|
+
BarColumn(),
|
|
561
|
+
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
|
|
562
|
+
TimeRemainingColumn(),
|
|
563
|
+
console=self.console,
|
|
564
|
+
transient=True,
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
task = progress.add_task(task_description, total=total)
|
|
568
|
+
|
|
569
|
+
if completed is not None and total is not None:
|
|
570
|
+
progress.update(task, completed=completed)
|
|
571
|
+
|
|
572
|
+
return progress
|
|
573
|
+
|
|
574
|
+
def create_responsive_layout(
|
|
575
|
+
self, sections: Dict[str, Any], vertical_threshold: int = 100
|
|
576
|
+
) -> Union[Columns, Layout]:
|
|
577
|
+
"""
|
|
578
|
+
Create a responsive layout that adapts to terminal width.
|
|
579
|
+
|
|
580
|
+
Args:
|
|
581
|
+
sections: Dictionary of section name to content
|
|
582
|
+
vertical_threshold: Width below which to use vertical layout
|
|
583
|
+
|
|
584
|
+
Returns:
|
|
585
|
+
Layout object appropriate for terminal width
|
|
586
|
+
"""
|
|
587
|
+
if self.capabilities.width < vertical_threshold:
|
|
588
|
+
# Vertical layout for narrow terminals
|
|
589
|
+
layout = Layout()
|
|
590
|
+
for i, (name, content) in enumerate(sections.items()):
|
|
591
|
+
if i > 0:
|
|
592
|
+
layout.add_split(Layout(Rule(style="dim")))
|
|
593
|
+
layout.add_split(Layout(content, name=name))
|
|
594
|
+
return layout
|
|
595
|
+
else:
|
|
596
|
+
# Horizontal layout for wide terminals
|
|
597
|
+
columns = []
|
|
598
|
+
for name, content in sections.items():
|
|
599
|
+
columns.append(content)
|
|
600
|
+
return Columns(columns, equal=True, padding=1)
|
|
601
|
+
|
|
602
|
+
def format_shortcut(self, key: str, description: str, enabled: bool = True) -> Text:
|
|
603
|
+
"""
|
|
604
|
+
Format a keyboard shortcut with description.
|
|
605
|
+
|
|
606
|
+
Args:
|
|
607
|
+
key: Keyboard shortcut key(s)
|
|
608
|
+
description: Description of the action
|
|
609
|
+
enabled: Whether the shortcut is currently enabled
|
|
610
|
+
|
|
611
|
+
Returns:
|
|
612
|
+
Formatted shortcut text
|
|
613
|
+
"""
|
|
614
|
+
text = Text()
|
|
615
|
+
|
|
616
|
+
if enabled:
|
|
617
|
+
text.append(f"[{key}]", style="shortcut.key")
|
|
618
|
+
text.append(f" {description}", style="shortcut.desc")
|
|
619
|
+
else:
|
|
620
|
+
text.append(f"[{key}]", style="dim")
|
|
621
|
+
text.append(f" {description}", style="dim")
|
|
622
|
+
|
|
623
|
+
return text
|
|
624
|
+
|
|
625
|
+
def render_shortcuts_help(
|
|
626
|
+
self, shortcuts: Dict[str, str], columns: int = 2
|
|
627
|
+
) -> Union[Table, Columns]:
|
|
628
|
+
"""
|
|
629
|
+
Render a help panel for keyboard shortcuts.
|
|
630
|
+
|
|
631
|
+
Args:
|
|
632
|
+
shortcuts: Dictionary of key to description
|
|
633
|
+
columns: Number of columns for display
|
|
634
|
+
|
|
635
|
+
Returns:
|
|
636
|
+
Formatted shortcuts help
|
|
637
|
+
"""
|
|
638
|
+
if self.capabilities.width < 60:
|
|
639
|
+
# Single column for narrow terminals
|
|
640
|
+
table = Table(show_header=False, box=None, padding=0)
|
|
641
|
+
table.add_column("Shortcut", style="shortcut.key")
|
|
642
|
+
table.add_column("Action", style="shortcut.desc")
|
|
643
|
+
|
|
644
|
+
for key, desc in shortcuts.items():
|
|
645
|
+
table.add_row(f"[{key}]", desc)
|
|
646
|
+
|
|
647
|
+
return table
|
|
648
|
+
else:
|
|
649
|
+
# Multi-column layout
|
|
650
|
+
shortcut_texts = []
|
|
651
|
+
for key, desc in shortcuts.items():
|
|
652
|
+
shortcut_texts.append(self.format_shortcut(key, desc))
|
|
653
|
+
|
|
654
|
+
# Split into columns
|
|
655
|
+
items_per_col = len(shortcut_texts) // columns + (
|
|
656
|
+
1 if len(shortcut_texts) % columns else 0
|
|
657
|
+
)
|
|
658
|
+
columns_list = []
|
|
659
|
+
|
|
660
|
+
for i in range(columns):
|
|
661
|
+
start = i * items_per_col
|
|
662
|
+
end = min(start + items_per_col, len(shortcut_texts))
|
|
663
|
+
if start < len(shortcut_texts):
|
|
664
|
+
col_items = Text("\n").join(shortcut_texts[start:end])
|
|
665
|
+
columns_list.append(col_items)
|
|
666
|
+
|
|
667
|
+
return Columns(columns_list, equal=True, padding=2)
|
|
668
|
+
|
|
669
|
+
def apply_diff_highlighting(
|
|
670
|
+
self, diff_text: str, file_type: Optional[str] = None
|
|
671
|
+
) -> Text:
|
|
672
|
+
"""
|
|
673
|
+
Apply syntax highlighting to diff text.
|
|
674
|
+
|
|
675
|
+
Args:
|
|
676
|
+
diff_text: Diff content to highlight
|
|
677
|
+
file_type: File type for syntax highlighting
|
|
678
|
+
|
|
679
|
+
Returns:
|
|
680
|
+
Highlighted diff text
|
|
681
|
+
"""
|
|
682
|
+
result = Text()
|
|
683
|
+
|
|
684
|
+
for line in diff_text.split("\n"):
|
|
685
|
+
if line.startswith("+++") or line.startswith("---"):
|
|
686
|
+
# File headers
|
|
687
|
+
result.append(line + "\n", style="diff.header")
|
|
688
|
+
elif line.startswith("@@"):
|
|
689
|
+
# Hunk headers
|
|
690
|
+
result.append(line + "\n", style="diff.line_number")
|
|
691
|
+
elif line.startswith("+"):
|
|
692
|
+
# Added lines
|
|
693
|
+
result.append(line + "\n", style="diff.added")
|
|
694
|
+
elif line.startswith("-"):
|
|
695
|
+
# Removed lines
|
|
696
|
+
result.append(line + "\n", style="diff.removed")
|
|
697
|
+
else:
|
|
698
|
+
# Context lines
|
|
699
|
+
result.append(line + "\n", style="diff.context")
|
|
700
|
+
|
|
701
|
+
return result
|
|
702
|
+
|
|
703
|
+
def render_with_fallback(self, rich_content: Any, plain_text: str) -> None:
|
|
704
|
+
"""
|
|
705
|
+
Render content with fallback for limited terminals.
|
|
706
|
+
|
|
707
|
+
Args:
|
|
708
|
+
rich_content: Rich formatted content
|
|
709
|
+
plain_text: Plain text fallback
|
|
710
|
+
"""
|
|
711
|
+
if self.capabilities.supports_color and self.capabilities.supports_unicode:
|
|
712
|
+
self.console.print(rich_content)
|
|
713
|
+
else:
|
|
714
|
+
# Fallback to plain text
|
|
715
|
+
print(plain_text)
|
|
716
|
+
|
|
717
|
+
def get_terminal_width(self) -> int:
|
|
718
|
+
"""Get current terminal width."""
|
|
719
|
+
return self.capabilities.width
|
|
720
|
+
|
|
721
|
+
def get_terminal_height(self) -> int:
|
|
722
|
+
"""Get current terminal height."""
|
|
723
|
+
return self.capabilities.height
|
|
724
|
+
|
|
725
|
+
def supports_emoji(self) -> bool:
|
|
726
|
+
"""Check if terminal supports emoji."""
|
|
727
|
+
return self.capabilities.supports_emoji
|
|
728
|
+
|
|
729
|
+
def supports_color(self) -> bool:
|
|
730
|
+
"""Check if terminal supports color."""
|
|
731
|
+
return self.capabilities.supports_color
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
# Utility functions for easy use
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
def create_renderer(auto_detect: bool = True) -> RichTextRenderer:
|
|
738
|
+
"""Create a rich text renderer with auto-detection."""
|
|
739
|
+
return RichTextRenderer(auto_detect=auto_detect)
|
|
740
|
+
|
|
741
|
+
|
|
742
|
+
def render_risk_badge(
|
|
743
|
+
risk_level: Union[int, str], renderer: Optional[RichTextRenderer] = None
|
|
744
|
+
) -> Text:
|
|
745
|
+
"""Quick function to render a risk badge."""
|
|
746
|
+
renderer = renderer or create_renderer()
|
|
747
|
+
return renderer.render_risk_indicator(risk_level, show_bar=False)
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
def render_operation_badge(
|
|
751
|
+
operation_type: str, renderer: Optional[RichTextRenderer] = None
|
|
752
|
+
) -> Text:
|
|
753
|
+
"""Quick function to render an operation type badge."""
|
|
754
|
+
renderer = renderer or create_renderer()
|
|
755
|
+
|
|
756
|
+
text = Text()
|
|
757
|
+
text.append("⚡ ", style=renderer.get_operation_color(operation_type))
|
|
758
|
+
text.append(
|
|
759
|
+
operation_type.replace("_", " ").title(),
|
|
760
|
+
style=renderer.get_operation_color(operation_type),
|
|
761
|
+
)
|
|
762
|
+
|
|
763
|
+
return text
|