codemate-cli 1.0.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.
- codemate/__init__.py +17 -0
- codemate/__main__.py +10 -0
- codemate/cli.py +815 -0
- codemate/client.py +311 -0
- codemate/commands/__init__.py +6 -0
- codemate/commands/chat.py +0 -0
- codemate/commands/config.py +103 -0
- codemate/commands/help.py +298 -0
- codemate/commands/kb_commands.py +749 -0
- codemate/config.py +233 -0
- codemate/ui/__init__.py +10 -0
- codemate/ui/markdown.py +212 -0
- codemate/ui/renderer.py +159 -0
- codemate/ui/streaming.py +436 -0
- codemate/utils/__init__.py +21 -0
- codemate/utils/auth.py +164 -0
- codemate/utils/error_handler.py +277 -0
- codemate/utils/errors.py +156 -0
- codemate/utils/kb_parser.py +111 -0
- codemate_cli-1.0.0.dist-info/METADATA +452 -0
- codemate_cli-1.0.0.dist-info/RECORD +25 -0
- codemate_cli-1.0.0.dist-info/WHEEL +5 -0
- codemate_cli-1.0.0.dist-info/entry_points.txt +3 -0
- codemate_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
- codemate_cli-1.0.0.dist-info/top_level.txt +1 -0
codemate/config.py
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional, Dict, Any
|
|
5
|
+
from dotenv import load_dotenv
|
|
6
|
+
|
|
7
|
+
# Load environment variables
|
|
8
|
+
load_dotenv()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Config:
|
|
12
|
+
"""Configuration manager for CodeMate CLI"""
|
|
13
|
+
|
|
14
|
+
def __init__(self):
|
|
15
|
+
self.config_dir = self._get_config_dir()
|
|
16
|
+
self.config_file = self.config_dir / "cli_config.json"
|
|
17
|
+
self.session_file = self.config_dir / "session.txt"
|
|
18
|
+
self._ensure_config_exists()
|
|
19
|
+
self._load_config()
|
|
20
|
+
|
|
21
|
+
def _get_config_dir(self) -> Path:
|
|
22
|
+
"""Get or create config directory in user home"""
|
|
23
|
+
# Use user home directory + .codemate/user
|
|
24
|
+
home = Path.home()
|
|
25
|
+
config_dir = home / ".codemate" / "user"
|
|
26
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
|
27
|
+
return config_dir
|
|
28
|
+
|
|
29
|
+
def _ensure_config_exists(self):
|
|
30
|
+
"""Create config file if it doesn't exist"""
|
|
31
|
+
if not self.config_file.exists():
|
|
32
|
+
default_config = {
|
|
33
|
+
"endpoint": "http://localhost:45223",
|
|
34
|
+
"default_model": "chat_c0_cli",
|
|
35
|
+
"stream": True,
|
|
36
|
+
"theme": "monokai",
|
|
37
|
+
"save_history": True,
|
|
38
|
+
}
|
|
39
|
+
self._save_config(default_config)
|
|
40
|
+
|
|
41
|
+
def _load_config(self):
|
|
42
|
+
"""Load configuration from file"""
|
|
43
|
+
try:
|
|
44
|
+
with open(self.config_file, 'r') as f:
|
|
45
|
+
self.config = json.load(f)
|
|
46
|
+
except Exception as e:
|
|
47
|
+
print(f"Warning: Could not load config: {e}")
|
|
48
|
+
self.config = {}
|
|
49
|
+
|
|
50
|
+
def _save_config(self, config: Dict[str, Any]):
|
|
51
|
+
"""Save configuration to file"""
|
|
52
|
+
try:
|
|
53
|
+
with open(self.config_file, 'w') as f:
|
|
54
|
+
json.dump(config, f, indent=2)
|
|
55
|
+
self.config = config
|
|
56
|
+
except Exception as e:
|
|
57
|
+
print(f"Error saving config: {e}")
|
|
58
|
+
|
|
59
|
+
def is_logged_in(self) -> bool:
|
|
60
|
+
"""Check if user is logged in by checking session.txt existence"""
|
|
61
|
+
return self.session_file.exists()
|
|
62
|
+
|
|
63
|
+
def get_session(self) -> Optional[str]:
|
|
64
|
+
"""Get session from session.txt file"""
|
|
65
|
+
if not self.session_file.exists():
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
with open(self.session_file, 'r') as f:
|
|
70
|
+
session = f.read().strip()
|
|
71
|
+
return session if session else None
|
|
72
|
+
except Exception:
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
def save_session(self, session: str):
|
|
76
|
+
"""Save session to session.txt file"""
|
|
77
|
+
try:
|
|
78
|
+
with open(self.session_file, 'w') as f:
|
|
79
|
+
f.write(session)
|
|
80
|
+
except Exception as e:
|
|
81
|
+
print(f"Error saving session: {e}")
|
|
82
|
+
|
|
83
|
+
def clear_session(self):
|
|
84
|
+
"""Clear session (logout)"""
|
|
85
|
+
if self.session_file.exists():
|
|
86
|
+
self.session_file.unlink()
|
|
87
|
+
|
|
88
|
+
def get_api_key(self) -> Optional[str]:
|
|
89
|
+
"""Get API key from session.txt (for backward compatibility)"""
|
|
90
|
+
return self.get_session()
|
|
91
|
+
|
|
92
|
+
def set_api_key(self, api_key: str):
|
|
93
|
+
"""Set API key in session.txt (for backward compatibility)"""
|
|
94
|
+
self.save_session(api_key)
|
|
95
|
+
|
|
96
|
+
def get_endpoint(self) -> str:
|
|
97
|
+
"""Get API endpoint"""
|
|
98
|
+
return os.environ.get('CODEMATE_ENDPOINT') or self.config.get(
|
|
99
|
+
'endpoint',
|
|
100
|
+
'http://localhost:45223'
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
def set_endpoint(self, endpoint: str):
|
|
104
|
+
"""Set API endpoint"""
|
|
105
|
+
self.config['endpoint'] = endpoint
|
|
106
|
+
self._save_config(self.config)
|
|
107
|
+
|
|
108
|
+
def get_default_model(self) -> str:
|
|
109
|
+
"""Get default model"""
|
|
110
|
+
return self.config.get('default_model', 'chat_c0_cli')
|
|
111
|
+
|
|
112
|
+
def set_default_model(self, model: str):
|
|
113
|
+
"""Set default model"""
|
|
114
|
+
self.config['default_model'] = model
|
|
115
|
+
self._save_config(self.config)
|
|
116
|
+
|
|
117
|
+
def get_stream_enabled(self) -> bool:
|
|
118
|
+
"""Check if streaming is enabled"""
|
|
119
|
+
return self.config.get('stream', True)
|
|
120
|
+
|
|
121
|
+
def set_stream_enabled(self, enabled: bool):
|
|
122
|
+
"""Enable/disable streaming"""
|
|
123
|
+
self.config['stream'] = enabled
|
|
124
|
+
self._save_config(self.config)
|
|
125
|
+
|
|
126
|
+
def get_theme(self) -> str:
|
|
127
|
+
"""Get code theme"""
|
|
128
|
+
return self.config.get('theme', 'monokai')
|
|
129
|
+
|
|
130
|
+
def set_theme(self, theme: str):
|
|
131
|
+
"""Set code theme"""
|
|
132
|
+
self.config['theme'] = theme
|
|
133
|
+
self._save_config(self.config)
|
|
134
|
+
|
|
135
|
+
def get_save_history(self) -> bool:
|
|
136
|
+
"""Check if history saving is enabled"""
|
|
137
|
+
return self.config.get('save_history', True)
|
|
138
|
+
|
|
139
|
+
def set_save_history(self, enabled: bool):
|
|
140
|
+
"""Enable/disable history saving"""
|
|
141
|
+
self.config['save_history'] = enabled
|
|
142
|
+
self._save_config(self.config)
|
|
143
|
+
|
|
144
|
+
def get_all(self) -> Dict[str, Any]:
|
|
145
|
+
"""Get all configuration values"""
|
|
146
|
+
session = self.get_session()
|
|
147
|
+
return {
|
|
148
|
+
"logged_in": self.is_logged_in(),
|
|
149
|
+
"session": session[:10] + "..." + session[-4:] if session and len(session) > 14 else "Not logged in",
|
|
150
|
+
"endpoint": self.get_endpoint(),
|
|
151
|
+
"default_model": self.get_default_model(),
|
|
152
|
+
"stream": self.get_stream_enabled(),
|
|
153
|
+
"theme": self.get_theme(),
|
|
154
|
+
"save_history": self.get_save_history(),
|
|
155
|
+
"config_location": str(self.config_file),
|
|
156
|
+
"session_location": str(self.session_file),
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
def reset(self):
|
|
160
|
+
"""Reset configuration to defaults"""
|
|
161
|
+
self.config_file.unlink(missing_ok=True)
|
|
162
|
+
self._ensure_config_exists()
|
|
163
|
+
self._load_config()
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class HistoryManager:
|
|
167
|
+
"""Manage conversation history"""
|
|
168
|
+
|
|
169
|
+
def __init__(self, config: Config):
|
|
170
|
+
self.config = config
|
|
171
|
+
self.history_dir = config.config_dir / "history"
|
|
172
|
+
self.history_dir.mkdir(exist_ok=True)
|
|
173
|
+
self.current_session_file = self.history_dir / "current_session.json"
|
|
174
|
+
|
|
175
|
+
def save_message(self, role: str, content: str, metadata: Optional[Dict] = None):
|
|
176
|
+
"""Save a message to current session"""
|
|
177
|
+
if not self.config.get_save_history():
|
|
178
|
+
return
|
|
179
|
+
|
|
180
|
+
try:
|
|
181
|
+
# Load existing history
|
|
182
|
+
history = []
|
|
183
|
+
if self.current_session_file.exists():
|
|
184
|
+
with open(self.current_session_file, 'r') as f:
|
|
185
|
+
history = json.load(f)
|
|
186
|
+
|
|
187
|
+
# Add new message
|
|
188
|
+
message = {
|
|
189
|
+
"role": role,
|
|
190
|
+
"content": content,
|
|
191
|
+
"timestamp": self._get_timestamp(),
|
|
192
|
+
}
|
|
193
|
+
if metadata:
|
|
194
|
+
message["metadata"] = metadata
|
|
195
|
+
|
|
196
|
+
history.append(message)
|
|
197
|
+
|
|
198
|
+
# Save updated history
|
|
199
|
+
with open(self.current_session_file, 'w') as f:
|
|
200
|
+
json.dump(history, f, indent=2)
|
|
201
|
+
|
|
202
|
+
except Exception as e:
|
|
203
|
+
print(f"Warning: Could not save history: {e}")
|
|
204
|
+
|
|
205
|
+
def load_current_session(self) -> list:
|
|
206
|
+
"""Load current session history"""
|
|
207
|
+
try:
|
|
208
|
+
if self.current_session_file.exists():
|
|
209
|
+
with open(self.current_session_file, 'r') as f:
|
|
210
|
+
return json.load(f)
|
|
211
|
+
except Exception:
|
|
212
|
+
pass
|
|
213
|
+
return []
|
|
214
|
+
|
|
215
|
+
def clear_current_session(self):
|
|
216
|
+
"""Clear current session history"""
|
|
217
|
+
self.current_session_file.unlink(missing_ok=True)
|
|
218
|
+
|
|
219
|
+
def archive_session(self, name: Optional[str] = None):
|
|
220
|
+
"""Archive current session with optional name"""
|
|
221
|
+
if not self.current_session_file.exists():
|
|
222
|
+
return
|
|
223
|
+
|
|
224
|
+
timestamp = self._get_timestamp()
|
|
225
|
+
archive_name = name or f"session_{timestamp}.json"
|
|
226
|
+
archive_path = self.history_dir / archive_name
|
|
227
|
+
|
|
228
|
+
self.current_session_file.rename(archive_path)
|
|
229
|
+
|
|
230
|
+
def _get_timestamp(self) -> str:
|
|
231
|
+
"""Get current timestamp"""
|
|
232
|
+
from datetime import datetime
|
|
233
|
+
return datetime.now().isoformat()
|
codemate/ui/__init__.py
ADDED
codemate/ui/markdown.py
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Enhanced Markdown rendering utilities
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
from rich.markdown import Markdown
|
|
7
|
+
from rich.panel import Panel
|
|
8
|
+
from rich.syntax import Syntax
|
|
9
|
+
import re
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class MarkdownProcessor:
|
|
14
|
+
"""Process and enhance markdown content"""
|
|
15
|
+
|
|
16
|
+
def __init__(self):
|
|
17
|
+
self.code_block_pattern = re.compile(r'```(\w+)?\n(.*?)```', re.DOTALL)
|
|
18
|
+
|
|
19
|
+
def extract_code_blocks(self, text: str):
|
|
20
|
+
"""Extract code blocks from markdown"""
|
|
21
|
+
blocks = []
|
|
22
|
+
for match in self.code_block_pattern.finditer(text):
|
|
23
|
+
language = match.group(1) or "text"
|
|
24
|
+
code = match.group(2)
|
|
25
|
+
blocks.append({
|
|
26
|
+
"language": language,
|
|
27
|
+
"code": code,
|
|
28
|
+
"start": match.start(),
|
|
29
|
+
"end": match.end()
|
|
30
|
+
})
|
|
31
|
+
return blocks
|
|
32
|
+
|
|
33
|
+
def replace_code_blocks(self, text: str, placeholder: str = "[CODE_BLOCK]"):
|
|
34
|
+
"""Replace code blocks with placeholder"""
|
|
35
|
+
return self.code_block_pattern.sub(placeholder, text)
|
|
36
|
+
|
|
37
|
+
def split_by_code_blocks(self, text: str):
|
|
38
|
+
"""Split text into markdown and code sections"""
|
|
39
|
+
sections = []
|
|
40
|
+
last_end = 0
|
|
41
|
+
|
|
42
|
+
for match in self.code_block_pattern.finditer(text):
|
|
43
|
+
# Add markdown before code block
|
|
44
|
+
if match.start() > last_end:
|
|
45
|
+
sections.append({
|
|
46
|
+
"type": "markdown",
|
|
47
|
+
"content": text[last_end:match.start()]
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
# Add code block
|
|
51
|
+
sections.append({
|
|
52
|
+
"type": "code",
|
|
53
|
+
"language": match.group(1) or "text",
|
|
54
|
+
"content": match.group(2)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
last_end = match.end()
|
|
58
|
+
|
|
59
|
+
# Add remaining markdown
|
|
60
|
+
if last_end < len(text):
|
|
61
|
+
sections.append({
|
|
62
|
+
"type": "markdown",
|
|
63
|
+
"content": text[last_end:]
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
return sections
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class EnhancedMarkdownRenderer:
|
|
70
|
+
"""Enhanced markdown renderer with custom styling"""
|
|
71
|
+
|
|
72
|
+
def __init__(self, console: Console, theme: str = "monokai"):
|
|
73
|
+
self.console = console
|
|
74
|
+
self.theme = theme
|
|
75
|
+
self.processor = MarkdownProcessor()
|
|
76
|
+
|
|
77
|
+
def render(
|
|
78
|
+
self,
|
|
79
|
+
markdown_text: str,
|
|
80
|
+
title: Optional[str] = None,
|
|
81
|
+
show_panel: bool = True
|
|
82
|
+
):
|
|
83
|
+
"""
|
|
84
|
+
Render markdown with enhanced styling
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
markdown_text: Markdown text to render
|
|
88
|
+
title: Optional title for panel
|
|
89
|
+
show_panel: Whether to wrap in a panel
|
|
90
|
+
"""
|
|
91
|
+
# Split content into sections
|
|
92
|
+
sections = self.processor.split_by_code_blocks(markdown_text)
|
|
93
|
+
|
|
94
|
+
if show_panel:
|
|
95
|
+
self.console.print() # Add spacing
|
|
96
|
+
|
|
97
|
+
for section in sections:
|
|
98
|
+
if section["type"] == "markdown":
|
|
99
|
+
# Render markdown
|
|
100
|
+
if section["content"].strip():
|
|
101
|
+
md = Markdown(section["content"], code_theme=self.theme)
|
|
102
|
+
if show_panel:
|
|
103
|
+
self.console.print(md)
|
|
104
|
+
else:
|
|
105
|
+
self.console.print(md)
|
|
106
|
+
|
|
107
|
+
elif section["type"] == "code":
|
|
108
|
+
# Render code with syntax highlighting
|
|
109
|
+
self.render_code_block(
|
|
110
|
+
section["content"],
|
|
111
|
+
section["language"]
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
if show_panel:
|
|
115
|
+
self.console.print() # Add spacing
|
|
116
|
+
|
|
117
|
+
def render_code_block(self, code: str, language: str = "python"):
|
|
118
|
+
"""Render a single code block with syntax highlighting"""
|
|
119
|
+
syntax = Syntax(
|
|
120
|
+
code.strip(),
|
|
121
|
+
language,
|
|
122
|
+
theme=self.theme,
|
|
123
|
+
line_numbers=True,
|
|
124
|
+
word_wrap=False,
|
|
125
|
+
background_color="default"
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
self.console.print(Panel(
|
|
129
|
+
syntax,
|
|
130
|
+
border_style="#48AEF3",
|
|
131
|
+
padding=(0, 1),
|
|
132
|
+
title=f"[bold color(#48AEF3)]{language.upper()}[/bold color(#48AEF3)]",
|
|
133
|
+
title_align="left"
|
|
134
|
+
))
|
|
135
|
+
|
|
136
|
+
def render_inline(self, markdown_text: str):
|
|
137
|
+
"""Render markdown inline without panels"""
|
|
138
|
+
md = Markdown(markdown_text, code_theme=self.theme)
|
|
139
|
+
self.console.print(md)
|
|
140
|
+
|
|
141
|
+
def render_with_title(self, markdown_text: str, title: str):
|
|
142
|
+
"""Render markdown with a title panel"""
|
|
143
|
+
md = Markdown(markdown_text, code_theme=self.theme)
|
|
144
|
+
self.console.print(Panel(
|
|
145
|
+
md,
|
|
146
|
+
title=f"[bold color(#2FCACE)]{title}[/bold color(#2FCACE)]",
|
|
147
|
+
border_style="#2FCACE",
|
|
148
|
+
padding=(1, 2)
|
|
149
|
+
))
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class MarkdownFormatter:
|
|
153
|
+
"""Format text into markdown"""
|
|
154
|
+
|
|
155
|
+
@staticmethod
|
|
156
|
+
def heading(text: str, level: int = 1) -> str:
|
|
157
|
+
"""Create markdown heading"""
|
|
158
|
+
return f"{'#' * level} {text}\n"
|
|
159
|
+
|
|
160
|
+
@staticmethod
|
|
161
|
+
def bold(text: str) -> str:
|
|
162
|
+
"""Make text bold"""
|
|
163
|
+
return f"**{text}**"
|
|
164
|
+
|
|
165
|
+
@staticmethod
|
|
166
|
+
def italic(text: str) -> str:
|
|
167
|
+
"""Make text italic"""
|
|
168
|
+
return f"*{text}*"
|
|
169
|
+
|
|
170
|
+
@staticmethod
|
|
171
|
+
def code(text: str) -> str:
|
|
172
|
+
"""Make inline code"""
|
|
173
|
+
return f"`{text}`"
|
|
174
|
+
|
|
175
|
+
@staticmethod
|
|
176
|
+
def code_block(code: str, language: str = "python") -> str:
|
|
177
|
+
"""Create code block"""
|
|
178
|
+
return f"```{language}\n{code}\n```\n"
|
|
179
|
+
|
|
180
|
+
@staticmethod
|
|
181
|
+
def link(text: str, url: str) -> str:
|
|
182
|
+
"""Create link"""
|
|
183
|
+
return f"[{text}]({url})"
|
|
184
|
+
|
|
185
|
+
@staticmethod
|
|
186
|
+
def list_item(text: str, ordered: bool = False, number: int = 1) -> str:
|
|
187
|
+
"""Create list item"""
|
|
188
|
+
if ordered:
|
|
189
|
+
return f"{number}. {text}\n"
|
|
190
|
+
return f"- {text}\n"
|
|
191
|
+
|
|
192
|
+
@staticmethod
|
|
193
|
+
def table(headers: list, rows: list) -> str:
|
|
194
|
+
"""Create markdown table"""
|
|
195
|
+
# Header row
|
|
196
|
+
table = "| " + " | ".join(headers) + " |\n"
|
|
197
|
+
# Separator
|
|
198
|
+
table += "| " + " | ".join(["---"] * len(headers)) + " |\n"
|
|
199
|
+
# Data rows
|
|
200
|
+
for row in rows:
|
|
201
|
+
table += "| " + " | ".join(str(cell) for cell in row) + " |\n"
|
|
202
|
+
return table
|
|
203
|
+
|
|
204
|
+
@staticmethod
|
|
205
|
+
def quote(text: str) -> str:
|
|
206
|
+
"""Create blockquote"""
|
|
207
|
+
return f"> {text}\n"
|
|
208
|
+
|
|
209
|
+
@staticmethod
|
|
210
|
+
def horizontal_rule() -> str:
|
|
211
|
+
"""Create horizontal rule"""
|
|
212
|
+
return "---\n"
|
codemate/ui/renderer.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Rich rendering utilities for beautiful terminal output
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
from rich.panel import Panel
|
|
7
|
+
from rich.table import Table
|
|
8
|
+
from rich.syntax import Syntax
|
|
9
|
+
from rich.markdown import Markdown
|
|
10
|
+
from rich.text import Text
|
|
11
|
+
from typing import List, Dict, Any, Optional
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class UIRenderer:
|
|
15
|
+
"""Utility class for rendering rich UI components"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, console: Console):
|
|
18
|
+
self.console = console
|
|
19
|
+
|
|
20
|
+
def render_panel(
|
|
21
|
+
self,
|
|
22
|
+
content: str,
|
|
23
|
+
title: Optional[str] = None,
|
|
24
|
+
border_style: str = "#2FCACE",
|
|
25
|
+
padding: tuple = (1, 2)
|
|
26
|
+
):
|
|
27
|
+
"""Render content in a panel"""
|
|
28
|
+
panel = Panel(
|
|
29
|
+
content,
|
|
30
|
+
title=title,
|
|
31
|
+
border_style=border_style,
|
|
32
|
+
padding=padding
|
|
33
|
+
)
|
|
34
|
+
self.console.print(panel)
|
|
35
|
+
|
|
36
|
+
def render_table(
|
|
37
|
+
self,
|
|
38
|
+
data: List[Dict[str, Any]],
|
|
39
|
+
title: Optional[str] = None,
|
|
40
|
+
columns: Optional[List[str]] = None
|
|
41
|
+
):
|
|
42
|
+
"""Render data as a table"""
|
|
43
|
+
if not data:
|
|
44
|
+
self.console.print("[yellow]No data to display[/yellow]")
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
# Get columns from first row if not provided
|
|
48
|
+
if columns is None:
|
|
49
|
+
columns = list(data[0].keys())
|
|
50
|
+
|
|
51
|
+
table = Table(title=title, show_header=True, header_style="bold magenta")
|
|
52
|
+
|
|
53
|
+
# Add columns
|
|
54
|
+
for col in columns:
|
|
55
|
+
table.add_column(col, style="#2FCACE")
|
|
56
|
+
|
|
57
|
+
# Add rows
|
|
58
|
+
for row in data:
|
|
59
|
+
table.add_row(*[str(row.get(col, "")) for col in columns])
|
|
60
|
+
|
|
61
|
+
self.console.print(table)
|
|
62
|
+
|
|
63
|
+
def render_code(
|
|
64
|
+
self,
|
|
65
|
+
code: str,
|
|
66
|
+
language: str = "python",
|
|
67
|
+
theme: str = "monokai",
|
|
68
|
+
line_numbers: bool = True
|
|
69
|
+
):
|
|
70
|
+
"""Render syntax-highlighted code"""
|
|
71
|
+
syntax = Syntax(
|
|
72
|
+
code,
|
|
73
|
+
language,
|
|
74
|
+
theme=theme,
|
|
75
|
+
line_numbers=line_numbers,
|
|
76
|
+
word_wrap=False
|
|
77
|
+
)
|
|
78
|
+
self.console.print(syntax)
|
|
79
|
+
|
|
80
|
+
def render_markdown(self, markdown_text: str, code_theme: str = "monokai"):
|
|
81
|
+
"""Render markdown content"""
|
|
82
|
+
markdown = Markdown(markdown_text, code_theme=code_theme)
|
|
83
|
+
self.console.print(markdown)
|
|
84
|
+
|
|
85
|
+
def render_success(self, message: str):
|
|
86
|
+
"""Render success message"""
|
|
87
|
+
self.console.print(f"[green]✓ {message}[/green]")
|
|
88
|
+
|
|
89
|
+
def render_error(self, message: str):
|
|
90
|
+
"""Render error message"""
|
|
91
|
+
self.console.print(f"[red]✗ {message}[/red]")
|
|
92
|
+
|
|
93
|
+
def render_warning(self, message: str):
|
|
94
|
+
"""Render warning message"""
|
|
95
|
+
self.console.print(f"[yellow]⚠ {message}[/yellow]")
|
|
96
|
+
|
|
97
|
+
def render_info(self, message: str):
|
|
98
|
+
"""Render info message"""
|
|
99
|
+
self.console.print(f"[color(#48AEF3)]ℹ {message}[/color(#48AEF3)]")
|
|
100
|
+
|
|
101
|
+
def render_list(
|
|
102
|
+
self,
|
|
103
|
+
items: List[str],
|
|
104
|
+
title: Optional[str] = None,
|
|
105
|
+
style: str = "#2FCACE"
|
|
106
|
+
):
|
|
107
|
+
"""Render a list of items"""
|
|
108
|
+
if title:
|
|
109
|
+
self.console.print(f"\n[bold {style}]{title}[/bold {style}]")
|
|
110
|
+
|
|
111
|
+
for item in items:
|
|
112
|
+
self.console.print(f" • {item}")
|
|
113
|
+
|
|
114
|
+
def render_key_value_pairs(
|
|
115
|
+
self,
|
|
116
|
+
data: Dict[str, Any],
|
|
117
|
+
title: Optional[str] = None
|
|
118
|
+
):
|
|
119
|
+
"""Render key-value pairs"""
|
|
120
|
+
table = Table(title=title, show_header=False, box=None)
|
|
121
|
+
table.add_column("Key", style="#2FCACE", justify="right")
|
|
122
|
+
table.add_column("Value", style="white")
|
|
123
|
+
|
|
124
|
+
for key, value in data.items():
|
|
125
|
+
table.add_row(f"{key}:", str(value))
|
|
126
|
+
|
|
127
|
+
self.console.print(table)
|
|
128
|
+
|
|
129
|
+
def clear_screen(self):
|
|
130
|
+
"""Clear the terminal screen"""
|
|
131
|
+
self.console.clear()
|
|
132
|
+
|
|
133
|
+
def render_separator(self, char: str = "─", style: str = "dim"):
|
|
134
|
+
"""Render a horizontal separator"""
|
|
135
|
+
width = self.console.width
|
|
136
|
+
self.console.print(char * width, style=style)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class ProgressRenderer:
|
|
140
|
+
"""Renderer for progress indicators"""
|
|
141
|
+
|
|
142
|
+
def __init__(self, console: Console):
|
|
143
|
+
self.console = console
|
|
144
|
+
|
|
145
|
+
def show_spinner(self, message: str = "Processing...", spinner: str = "dots"):
|
|
146
|
+
"""Show a spinner with message"""
|
|
147
|
+
return self.console.status(f"[color(#2FCACE)]{message}[/color(#2FCACE)]", spinner=spinner)
|
|
148
|
+
|
|
149
|
+
def show_progress_bar(self, total: int, description: str = "Progress"):
|
|
150
|
+
"""Show a progress bar"""
|
|
151
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn
|
|
152
|
+
|
|
153
|
+
return Progress(
|
|
154
|
+
SpinnerColumn(),
|
|
155
|
+
TextColumn("[progress.description]{task.description}"),
|
|
156
|
+
BarColumn(),
|
|
157
|
+
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
|
|
158
|
+
console=self.console
|
|
159
|
+
)
|