handycode 2.1.6__tar.gz → 2.2.1__tar.gz
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.
- {handycode-2.1.6 → handycode-2.2.1}/PKG-INFO +1 -1
- {handycode-2.1.6 → handycode-2.2.1}/handycode/__init__.py +1 -1
- {handycode-2.1.6 → handycode-2.2.1}/handycode/assistant.py +212 -195
- {handycode-2.1.6 → handycode-2.2.1}/handycode/logo.py +20 -39
- handycode-2.2.1/handycode/utils.py +140 -0
- {handycode-2.1.6 → handycode-2.2.1}/handycode.egg-info/PKG-INFO +1 -1
- {handycode-2.1.6 → handycode-2.2.1}/setup.py +1 -1
- handycode-2.1.6/handycode/utils.py +0 -140
- {handycode-2.1.6 → handycode-2.2.1}/LICENSE +0 -0
- {handycode-2.1.6 → handycode-2.2.1}/README.md +0 -0
- {handycode-2.1.6 → handycode-2.2.1}/handycode/__main__.py +0 -0
- {handycode-2.1.6 → handycode-2.2.1}/handycode/cli.py +0 -0
- {handycode-2.1.6 → handycode-2.2.1}/handycode/config.py +0 -0
- {handycode-2.1.6 → handycode-2.2.1}/handycode/file_manager.py +0 -0
- {handycode-2.1.6 → handycode-2.2.1}/handycode/main.py +0 -0
- {handycode-2.1.6 → handycode-2.2.1}/handycode/models.py +0 -0
- {handycode-2.1.6 → handycode-2.2.1}/handycode/project_templates.py +0 -0
- {handycode-2.1.6 → handycode-2.2.1}/handycode/security.py +0 -0
- {handycode-2.1.6 → handycode-2.2.1}/handycode.egg-info/SOURCES.txt +0 -0
- {handycode-2.1.6 → handycode-2.2.1}/handycode.egg-info/dependency_links.txt +0 -0
- {handycode-2.1.6 → handycode-2.2.1}/handycode.egg-info/entry_points.txt +0 -0
- {handycode-2.1.6 → handycode-2.2.1}/handycode.egg-info/requires.txt +0 -0
- {handycode-2.1.6 → handycode-2.2.1}/handycode.egg-info/top_level.txt +0 -0
- {handycode-2.1.6 → handycode-2.2.1}/setup.cfg +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Основной класс ассистента HandyCode
|
|
2
|
+
Основной класс ассистента HandyCode с интерактивным меню
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
import os
|
|
@@ -14,14 +14,12 @@ from datetime import datetime
|
|
|
14
14
|
|
|
15
15
|
try:
|
|
16
16
|
import readline
|
|
17
|
-
|
|
18
17
|
HAS_READLINE = True
|
|
19
18
|
except ImportError:
|
|
20
19
|
HAS_READLINE = False
|
|
21
20
|
|
|
22
21
|
try:
|
|
23
22
|
import requests
|
|
24
|
-
|
|
25
23
|
HAS_REQUESTS = True
|
|
26
24
|
except ImportError:
|
|
27
25
|
HAS_REQUESTS = False
|
|
@@ -34,61 +32,164 @@ from handycode.models import MODELS, get_model_settings
|
|
|
34
32
|
from handycode.file_manager import FileManager
|
|
35
33
|
from handycode.security import SecurityChecker
|
|
36
34
|
from handycode.utils import (
|
|
37
|
-
Colors, print_colored, print_header, print_success,
|
|
35
|
+
Colors, Theme, colorize, print_colored, print_header, print_success,
|
|
38
36
|
print_error, print_warning, print_info, print_logo,
|
|
39
|
-
print_divider, print_file_action,
|
|
37
|
+
print_divider, print_file_action, print_status, print_section, print_box
|
|
40
38
|
)
|
|
41
39
|
|
|
42
40
|
|
|
41
|
+
def interactive_confirm(commands):
|
|
42
|
+
"""
|
|
43
|
+
Интерактивное меню выбора команд.
|
|
44
|
+
Управление: ↑/↓ для навигации, ПРОБЕЛ для выбора, ENTER для подтверждения.
|
|
45
|
+
Возвращает список выбранных команд.
|
|
46
|
+
"""
|
|
47
|
+
if not commands:
|
|
48
|
+
return []
|
|
49
|
+
|
|
50
|
+
# Настройка для Windows
|
|
51
|
+
if os.name == 'nt':
|
|
52
|
+
import msvcrt
|
|
53
|
+
|
|
54
|
+
def get_key():
|
|
55
|
+
key = msvcrt.getch()
|
|
56
|
+
if key == b'\xe0': # стрелки
|
|
57
|
+
key = msvcrt.getch()
|
|
58
|
+
if key == b'H': return 'up'
|
|
59
|
+
if key == b'P': return 'down'
|
|
60
|
+
if key == b'\r': return 'enter'
|
|
61
|
+
if key == b' ': return 'space'
|
|
62
|
+
if key == b'a': return 'a'
|
|
63
|
+
if key == b'A': return 'A'
|
|
64
|
+
if key == b's': return 's'
|
|
65
|
+
if key == b'S': return 'S'
|
|
66
|
+
if key == b'c': return 'c'
|
|
67
|
+
if key == b'C': return 'C'
|
|
68
|
+
if key == b'\x1b': return 'escape'
|
|
69
|
+
return key.decode('utf-8', errors='ignore')
|
|
70
|
+
else:
|
|
71
|
+
import tty
|
|
72
|
+
import termios
|
|
73
|
+
|
|
74
|
+
def get_key():
|
|
75
|
+
fd = sys.stdin.fileno()
|
|
76
|
+
old = termios.tcgetattr(fd)
|
|
77
|
+
try:
|
|
78
|
+
tty.setraw(fd)
|
|
79
|
+
key = sys.stdin.read(1)
|
|
80
|
+
if key == '\x1b':
|
|
81
|
+
key += sys.stdin.read(2)
|
|
82
|
+
if key == '\x1b[A': return 'up'
|
|
83
|
+
if key == '\x1b[B': return 'down'
|
|
84
|
+
return 'escape'
|
|
85
|
+
if key == '\r': return 'enter'
|
|
86
|
+
if key == ' ': return 'space'
|
|
87
|
+
return key
|
|
88
|
+
finally:
|
|
89
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, old)
|
|
90
|
+
|
|
91
|
+
selected = [True] * len(commands) # все выбраны по умолчанию
|
|
92
|
+
current = 0
|
|
93
|
+
|
|
94
|
+
def render():
|
|
95
|
+
# Очищаем предыдущий вывод
|
|
96
|
+
print(f"\033[{len(commands) + 4}A\033[J", end="")
|
|
97
|
+
|
|
98
|
+
print()
|
|
99
|
+
print(colorize(" ⚡ Команды для выполнения:", Theme.HIGHLIGHT + Colors.BOLD))
|
|
100
|
+
print(colorize(" ─────────────────────────────────────────────────", Theme.MUTED))
|
|
101
|
+
|
|
102
|
+
for i, cmd in enumerate(commands):
|
|
103
|
+
if i == current:
|
|
104
|
+
prefix = colorize(" ›", Theme.PRIMARY + Colors.BOLD)
|
|
105
|
+
else:
|
|
106
|
+
prefix = " "
|
|
107
|
+
|
|
108
|
+
if selected[i]:
|
|
109
|
+
checkbox = colorize("◉", Theme.SUCCESS)
|
|
110
|
+
cmd_color = Theme.SUCCESS
|
|
111
|
+
else:
|
|
112
|
+
checkbox = colorize("○", Theme.MUTED)
|
|
113
|
+
cmd_color = Theme.MUTED
|
|
114
|
+
|
|
115
|
+
print(f"{prefix} {checkbox} {colorize(cmd, cmd_color)}")
|
|
116
|
+
|
|
117
|
+
print()
|
|
118
|
+
print(colorize(" Управление:", Theme.MUTED))
|
|
119
|
+
print(colorize(" ↑↓ Навигация ПРОБЕЛ Выбрать A Все S Пропустить ENTER Подтвердить", Theme.MUTED))
|
|
120
|
+
|
|
121
|
+
# Рендерим первый раз
|
|
122
|
+
print()
|
|
123
|
+
print()
|
|
124
|
+
print()
|
|
125
|
+
print()
|
|
126
|
+
print()
|
|
127
|
+
print()
|
|
128
|
+
for _ in range(len(commands) + 4):
|
|
129
|
+
print()
|
|
130
|
+
|
|
131
|
+
render()
|
|
132
|
+
|
|
133
|
+
while True:
|
|
134
|
+
key = get_key()
|
|
135
|
+
|
|
136
|
+
if key == 'up':
|
|
137
|
+
current = (current - 1) % len(commands)
|
|
138
|
+
render()
|
|
139
|
+
elif key == 'down':
|
|
140
|
+
current = (current + 1) % len(commands)
|
|
141
|
+
render()
|
|
142
|
+
elif key == 'space':
|
|
143
|
+
selected[current] = not selected[current]
|
|
144
|
+
render()
|
|
145
|
+
elif key in ['a', 'A']:
|
|
146
|
+
selected = [True] * len(commands)
|
|
147
|
+
render()
|
|
148
|
+
elif key in ['s', 'S']:
|
|
149
|
+
selected = [False] * len(commands)
|
|
150
|
+
render()
|
|
151
|
+
elif key in ['c', 'C', 'escape']:
|
|
152
|
+
return []
|
|
153
|
+
elif key == 'enter':
|
|
154
|
+
print()
|
|
155
|
+
return [cmd for cmd, sel in zip(commands, selected) if sel]
|
|
156
|
+
|
|
157
|
+
|
|
43
158
|
class HandyCode:
|
|
44
159
|
def __init__(self, project_path, model="deepseek", auto_approve=False, config=None):
|
|
45
160
|
self.project_path = project_path
|
|
46
161
|
self.auto_approve = auto_approve
|
|
47
162
|
self.config = config or Config()
|
|
48
|
-
|
|
49
163
|
self.api_key = self.config.get_api_key()
|
|
50
164
|
if not self.api_key:
|
|
51
165
|
raise ValueError("API key not found")
|
|
52
|
-
|
|
53
166
|
self.api_url = "https://openrouter.ai/api/v1/chat/completions"
|
|
54
167
|
self.current_model = MODELS.get(model, MODELS["deepseek"])
|
|
55
168
|
self.model_settings = get_model_settings(self.current_model)
|
|
56
|
-
|
|
57
169
|
self.file_manager = FileManager(self.project_path)
|
|
58
170
|
self.security = SecurityChecker(self.project_path)
|
|
59
|
-
|
|
60
171
|
project_context = self._build_project_context()
|
|
61
|
-
|
|
62
172
|
self.conversation_history = [
|
|
63
173
|
{"role": "system", "content": self._get_system_prompt() + project_context}
|
|
64
174
|
]
|
|
65
|
-
|
|
66
175
|
self.stats = {
|
|
67
|
-
"messages_sent": 0,
|
|
68
|
-
"
|
|
69
|
-
"files_modified": [],
|
|
70
|
-
"files_deleted": [],
|
|
71
|
-
"files_read": [],
|
|
72
|
-
"commands_executed": [],
|
|
176
|
+
"messages_sent": 0, "files_created": [], "files_modified": [],
|
|
177
|
+
"files_deleted": [], "files_read": [], "commands_executed": [],
|
|
73
178
|
"start_time": datetime.now()
|
|
74
179
|
}
|
|
75
|
-
|
|
76
180
|
self.stream_buffer = ""
|
|
77
181
|
self.pending_commands = []
|
|
78
|
-
|
|
79
182
|
self._setup_readline()
|
|
80
183
|
signal.signal(signal.SIGINT, self._signal_handler)
|
|
81
184
|
self._interrupt_count = 0
|
|
82
185
|
|
|
83
186
|
def _build_project_context(self):
|
|
84
|
-
context = f"\n\n=== CURRENT PROJECT ===\n"
|
|
85
|
-
context += f"Directory: {self.project_path}\n"
|
|
187
|
+
context = f"\n\n=== CURRENT PROJECT ===\nDirectory: {self.project_path}\n"
|
|
86
188
|
try:
|
|
87
189
|
all_files = []
|
|
88
190
|
for ext in self.file_manager.allowed_extensions:
|
|
89
191
|
all_files.extend(self.project_path.rglob(f"*{ext}"))
|
|
90
192
|
all_files.extend(self.project_path.rglob("*"))
|
|
91
|
-
|
|
92
193
|
seen = set()
|
|
93
194
|
files = []
|
|
94
195
|
for f in sorted(all_files):
|
|
@@ -98,75 +199,61 @@ class HandyCode:
|
|
|
98
199
|
if not any(rel.startswith(ex) for ex in self.file_manager.excluded_dirs):
|
|
99
200
|
files.append(f)
|
|
100
201
|
seen.add(f)
|
|
101
|
-
|
|
102
202
|
context += f"\nFiles ({len(files)}):\n"
|
|
103
203
|
for file in files:
|
|
104
204
|
try:
|
|
105
205
|
rel_path = file.relative_to(self.project_path)
|
|
106
206
|
size = file.stat().st_size
|
|
107
207
|
context += f" {rel_path} ({self._format_size(size)})\n"
|
|
108
|
-
except:
|
|
109
|
-
|
|
110
|
-
except:
|
|
111
|
-
pass
|
|
208
|
+
except: pass
|
|
209
|
+
except: pass
|
|
112
210
|
return context
|
|
113
211
|
|
|
114
212
|
def _format_size(self, size):
|
|
115
213
|
for unit in ['B', 'KB', 'MB', 'GB']:
|
|
116
|
-
if size < 1024:
|
|
117
|
-
return f"{size:.1f}{unit}"
|
|
214
|
+
if size < 1024: return f"{size:.1f}{unit}"
|
|
118
215
|
size /= 1024
|
|
119
216
|
return f"{size:.1f}TB"
|
|
120
217
|
|
|
121
218
|
def _get_system_prompt(self):
|
|
122
219
|
return """You are HandyCode - AI coding assistant. Create/modify/delete files and run commands.
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
3. NO comments inside [[CREATE]]...[[END]]
|
|
139
|
-
4. Explanations BEFORE [[CREATE]] blocks
|
|
140
|
-
5. Files create automatically, commands need confirmation
|
|
141
|
-
|
|
142
|
-
Speak Russian. Write code in English."""
|
|
220
|
+
FORMAT:
|
|
221
|
+
[[CREATE:path/file]]
|
|
222
|
+
code here
|
|
223
|
+
[[END]]
|
|
224
|
+
[[MODIFY:path/file]]
|
|
225
|
+
new code here
|
|
226
|
+
[[END]]
|
|
227
|
+
[[EXEC:command]]
|
|
228
|
+
RULES:
|
|
229
|
+
1. CREATE + EXEC in ONE response
|
|
230
|
+
2. Use [[END]] after file content
|
|
231
|
+
3. NO comments inside [[CREATE]]...[[END]]
|
|
232
|
+
4. Explanations BEFORE [[CREATE]] blocks
|
|
233
|
+
5. Files create automatically, commands need confirmation
|
|
234
|
+
Speak Russian. Write code in English."""
|
|
143
235
|
|
|
144
236
|
def _setup_readline(self):
|
|
145
|
-
if not HAS_READLINE:
|
|
146
|
-
return
|
|
237
|
+
if not HAS_READLINE: return
|
|
147
238
|
try:
|
|
148
239
|
histfile = os.path.join(os.path.expanduser("~"), ".handycode", "history")
|
|
149
240
|
os.makedirs(os.path.dirname(histfile), exist_ok=True)
|
|
150
241
|
readline.read_history_file(histfile)
|
|
151
242
|
readline.set_history_length(1000)
|
|
152
243
|
atexit.register(readline.write_history_file, histfile)
|
|
153
|
-
except:
|
|
154
|
-
pass
|
|
244
|
+
except: pass
|
|
155
245
|
|
|
156
246
|
def _signal_handler(self, sig, frame):
|
|
157
247
|
self._interrupt_count += 1
|
|
158
248
|
if self._interrupt_count == 1:
|
|
159
|
-
print("\n\n ⚠
|
|
249
|
+
print("\n\n ⚠ Нажмите Ctrl+C ещё раз для выхода")
|
|
160
250
|
else:
|
|
161
251
|
os._exit(0)
|
|
162
252
|
|
|
163
|
-
def reset_interrupt(self):
|
|
164
|
-
self._interrupt_count = 0
|
|
253
|
+
def reset_interrupt(self): self._interrupt_count = 0
|
|
165
254
|
|
|
166
255
|
def _process_stream_chunk(self, chunk):
|
|
167
256
|
self.stream_buffer += chunk
|
|
168
|
-
|
|
169
|
-
# CREATE файлы в реальном времени
|
|
170
257
|
while True:
|
|
171
258
|
match = re.search(r'\[\[CREATE:(.+?)\]\](.*?)\[\[END\]\]', self.stream_buffer, re.DOTALL)
|
|
172
259
|
if match:
|
|
@@ -174,18 +261,12 @@ class HandyCode:
|
|
|
174
261
|
content = match.group(2).strip()
|
|
175
262
|
content = re.sub(r'^```[\w]*\n', '', content)
|
|
176
263
|
content = re.sub(r'\n```$', '', content)
|
|
177
|
-
|
|
178
264
|
if content and self.security.is_safe_path(path):
|
|
179
265
|
self.file_manager.create_file(path, content)
|
|
180
266
|
self.stats["files_created"].append(path)
|
|
181
|
-
|
|
182
|
-
print_file_action('create', path, f"({lines} lines)")
|
|
183
|
-
|
|
267
|
+
print_file_action('create', path, f"({content.count(chr(10))+1} lines)")
|
|
184
268
|
self.stream_buffer = self.stream_buffer[match.end():]
|
|
185
|
-
else:
|
|
186
|
-
break
|
|
187
|
-
|
|
188
|
-
# MODIFY файлы
|
|
269
|
+
else: break
|
|
189
270
|
while True:
|
|
190
271
|
match = re.search(r'\[\[MODIFY:(.+?)\]\](.*?)\[\[END\]\]', self.stream_buffer, re.DOTALL)
|
|
191
272
|
if match:
|
|
@@ -193,30 +274,22 @@ class HandyCode:
|
|
|
193
274
|
content = match.group(2).strip()
|
|
194
275
|
content = re.sub(r'^```[\w]*\n', '', content)
|
|
195
276
|
content = re.sub(r'\n```$', '', content)
|
|
196
|
-
|
|
197
277
|
if content and self.security.is_safe_path(path):
|
|
198
278
|
self.file_manager.modify_file(path, content)
|
|
199
279
|
self.stats["files_modified"].append(path)
|
|
200
|
-
|
|
201
|
-
print_file_action('modify', path, f"({lines} lines)")
|
|
202
|
-
|
|
280
|
+
print_file_action('modify', path, f"({content.count(chr(10))+1} lines)")
|
|
203
281
|
self.stream_buffer = self.stream_buffer[match.end():]
|
|
204
|
-
else:
|
|
205
|
-
break
|
|
206
|
-
|
|
207
|
-
# EXEC команды
|
|
282
|
+
else: break
|
|
208
283
|
while True:
|
|
209
284
|
match = re.search(r'\[\[EXEC:(.+?)\]\]', self.stream_buffer)
|
|
210
285
|
if match:
|
|
211
286
|
self.pending_commands.append(match.group(1).strip())
|
|
212
287
|
self.stream_buffer = self.stream_buffer[match.end():]
|
|
213
|
-
else:
|
|
214
|
-
break
|
|
288
|
+
else: break
|
|
215
289
|
|
|
216
290
|
def _make_request_streaming(self, data):
|
|
217
291
|
self.stream_buffer = ""
|
|
218
292
|
self.pending_commands = []
|
|
219
|
-
|
|
220
293
|
if HAS_REQUESTS:
|
|
221
294
|
return self._stream_requests(data)
|
|
222
295
|
else:
|
|
@@ -224,28 +297,19 @@ class HandyCode:
|
|
|
224
297
|
|
|
225
298
|
def _stream_requests(self, data):
|
|
226
299
|
try:
|
|
227
|
-
response = requests.post(
|
|
228
|
-
self.
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
"Content-Type": "application/json"
|
|
232
|
-
},
|
|
233
|
-
json={**data, "stream": True},
|
|
234
|
-
timeout=120,
|
|
235
|
-
stream=True
|
|
236
|
-
)
|
|
300
|
+
response = requests.post(self.api_url,
|
|
301
|
+
headers={"Authorization": f"Bearer {self.api_key}",
|
|
302
|
+
"Content-Type": "application/json"},
|
|
303
|
+
json={**data, "stream": True}, timeout=120, stream=True)
|
|
237
304
|
response.raise_for_status()
|
|
238
|
-
|
|
239
305
|
full_response = ""
|
|
240
306
|
in_code = False
|
|
241
|
-
|
|
242
307
|
for line in response.iter_lines():
|
|
243
308
|
if line:
|
|
244
309
|
line = line.decode('utf-8')
|
|
245
310
|
if line.startswith('data: '):
|
|
246
311
|
data_str = line[6:]
|
|
247
|
-
if data_str.strip() == '[DONE]':
|
|
248
|
-
break
|
|
312
|
+
if data_str.strip() == '[DONE]': break
|
|
249
313
|
try:
|
|
250
314
|
chunk = json.loads(data_str)
|
|
251
315
|
if 'choices' in chunk and chunk['choices']:
|
|
@@ -253,22 +317,16 @@ class HandyCode:
|
|
|
253
317
|
content = delta.get('content', '')
|
|
254
318
|
if content:
|
|
255
319
|
full_response += content
|
|
256
|
-
|
|
257
320
|
if '[[CREATE:' in content or '[[MODIFY:' in content:
|
|
258
321
|
in_code = True
|
|
259
322
|
if '[[END]]' in content:
|
|
260
323
|
in_code = False
|
|
261
|
-
|
|
262
324
|
if not in_code:
|
|
263
|
-
clean = content.replace('[[CREATE:', '').replace('[[MODIFY:', '').replace(
|
|
264
|
-
'[[END]]', '').replace('[[EXEC:', '').replace(']]', '')
|
|
325
|
+
clean = content.replace('[[CREATE:', '').replace('[[MODIFY:', '').replace('[[END]]', '').replace('[[EXEC:', '').replace(']]', '')
|
|
265
326
|
if clean.strip():
|
|
266
327
|
print(clean, end="", flush=True)
|
|
267
|
-
|
|
268
328
|
self._process_stream_chunk(content)
|
|
269
|
-
except:
|
|
270
|
-
continue
|
|
271
|
-
|
|
329
|
+
except: continue
|
|
272
330
|
print()
|
|
273
331
|
return full_response
|
|
274
332
|
except Exception as e:
|
|
@@ -278,27 +336,18 @@ class HandyCode:
|
|
|
278
336
|
def _stream_urllib(self, data):
|
|
279
337
|
try:
|
|
280
338
|
json_data = json.dumps({**data, "stream": True}).encode('utf-8')
|
|
281
|
-
req = urllib.request.Request(
|
|
282
|
-
self.
|
|
283
|
-
|
|
284
|
-
headers={
|
|
285
|
-
"Authorization": f"Bearer {self.api_key}",
|
|
286
|
-
"Content-Type": "application/json"
|
|
287
|
-
},
|
|
288
|
-
method='POST'
|
|
289
|
-
)
|
|
339
|
+
req = urllib.request.Request(self.api_url, data=json_data,
|
|
340
|
+
headers={"Authorization": f"Bearer {self.api_key}",
|
|
341
|
+
"Content-Type": "application/json"}, method='POST')
|
|
290
342
|
ctx = ssl.create_default_context()
|
|
291
|
-
|
|
292
343
|
full_response = ""
|
|
293
344
|
in_code = False
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
for line in response:
|
|
345
|
+
with urllib.request.urlopen(req, context=ctx, timeout=120) as resp:
|
|
346
|
+
for line in resp:
|
|
297
347
|
line = line.decode('utf-8').strip()
|
|
298
348
|
if line.startswith('data: '):
|
|
299
349
|
data_str = line[6:]
|
|
300
|
-
if data_str == '[DONE]':
|
|
301
|
-
break
|
|
350
|
+
if data_str == '[DONE]': break
|
|
302
351
|
try:
|
|
303
352
|
chunk = json.loads(data_str)
|
|
304
353
|
if 'choices' in chunk and chunk['choices']:
|
|
@@ -306,21 +355,16 @@ class HandyCode:
|
|
|
306
355
|
content = delta.get('content', '')
|
|
307
356
|
if content:
|
|
308
357
|
full_response += content
|
|
309
|
-
|
|
310
358
|
if '[[CREATE:' in content or '[[MODIFY:' in content:
|
|
311
359
|
in_code = True
|
|
312
360
|
if '[[END]]' in content:
|
|
313
361
|
in_code = False
|
|
314
|
-
|
|
315
362
|
if not in_code:
|
|
316
|
-
clean = content.replace('[[CREATE:', '').replace('[[MODIFY:', '').replace(
|
|
317
|
-
'[[END]]', '').replace('[[EXEC:', '').replace(']]', '')
|
|
363
|
+
clean = content.replace('[[CREATE:', '').replace('[[MODIFY:', '').replace('[[END]]', '').replace('[[EXEC:', '').replace(']]', '')
|
|
318
364
|
if clean.strip():
|
|
319
365
|
print(clean, end="", flush=True)
|
|
320
|
-
|
|
321
366
|
self._process_stream_chunk(content)
|
|
322
|
-
except:
|
|
323
|
-
continue
|
|
367
|
+
except: continue
|
|
324
368
|
print()
|
|
325
369
|
return full_response
|
|
326
370
|
except Exception as e:
|
|
@@ -332,7 +376,6 @@ class HandyCode:
|
|
|
332
376
|
return self._handle_command(user_input)
|
|
333
377
|
|
|
334
378
|
self.conversation_history.append({"role": "user", "content": user_input})
|
|
335
|
-
|
|
336
379
|
if len(self.conversation_history) > 20:
|
|
337
380
|
self.conversation_history = [self.conversation_history[0]] + self.conversation_history[-19:]
|
|
338
381
|
|
|
@@ -344,9 +387,10 @@ class HandyCode:
|
|
|
344
387
|
}
|
|
345
388
|
|
|
346
389
|
try:
|
|
347
|
-
print_divider("─", 60,
|
|
348
|
-
print(colorize(" HandyCode",
|
|
349
|
-
|
|
390
|
+
print_divider("─", 60, Theme.MUTED)
|
|
391
|
+
print(colorize(" HandyCode", Theme.PRIMARY + Colors.BOLD), end="")
|
|
392
|
+
print(colorize(" ● ответ", Theme.MUTED))
|
|
393
|
+
print_divider("─", 60, Theme.MUTED)
|
|
350
394
|
|
|
351
395
|
response = self._make_request_streaming(payload)
|
|
352
396
|
|
|
@@ -354,27 +398,21 @@ class HandyCode:
|
|
|
354
398
|
self.conversation_history.append({"role": "assistant", "content": response})
|
|
355
399
|
|
|
356
400
|
if self.pending_commands:
|
|
357
|
-
print()
|
|
358
|
-
print_divider("─", 60, Colors.BRIGHT_BLACK)
|
|
359
|
-
print(colorize(" ⚡ Commands (confirmation required):", Colors.YELLOW))
|
|
360
|
-
for i, cmd in enumerate(self.pending_commands, 1):
|
|
361
|
-
print_command(cmd, i)
|
|
362
|
-
|
|
363
401
|
if self.auto_approve:
|
|
364
|
-
|
|
402
|
+
selected_commands = self.pending_commands
|
|
365
403
|
else:
|
|
366
|
-
|
|
367
|
-
print(colorize(f" {Colors.BRIGHT_BLACK}[A] Execute all [S] Skip [C] Cancel{Colors.RESET}",
|
|
368
|
-
Colors.BRIGHT_BLACK))
|
|
369
|
-
choice = input(colorize(" > ", Colors.WHITE)).strip().upper()
|
|
404
|
+
selected_commands = interactive_confirm(self.pending_commands)
|
|
370
405
|
|
|
371
|
-
if
|
|
406
|
+
if selected_commands:
|
|
372
407
|
print()
|
|
373
|
-
|
|
408
|
+
print_section("⚡ Выполнение команд", [])
|
|
409
|
+
for cmd in selected_commands:
|
|
374
410
|
if self.security.is_safe_command(cmd):
|
|
375
|
-
print_status(f"
|
|
411
|
+
print_status(f"Выполняется: {cmd}")
|
|
376
412
|
self.file_manager.execute_command(cmd)
|
|
377
413
|
self.stats["commands_executed"].append(cmd)
|
|
414
|
+
else:
|
|
415
|
+
print_warning("Команды пропущены")
|
|
378
416
|
|
|
379
417
|
self.stats["messages_sent"] += 1
|
|
380
418
|
return response
|
|
@@ -384,52 +422,44 @@ class HandyCode:
|
|
|
384
422
|
def _handle_command(self, user_input):
|
|
385
423
|
parts = user_input.split()
|
|
386
424
|
cmd = parts[0].lower()
|
|
387
|
-
|
|
388
425
|
if cmd in ['/help', '/h']:
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
print(colorize(" /exit Exit program", Colors.WHITE))
|
|
400
|
-
print()
|
|
426
|
+
print_box([
|
|
427
|
+
"/help Справка",
|
|
428
|
+
"/scan Показать проект",
|
|
429
|
+
"/models Модели",
|
|
430
|
+
"/model N Сменить модель",
|
|
431
|
+
"/clear Очистить историю",
|
|
432
|
+
"/save Сохранить сессию",
|
|
433
|
+
"/stats Статистика",
|
|
434
|
+
"/exit Выход"
|
|
435
|
+
], Theme.PRIMARY)
|
|
401
436
|
elif cmd in ['/scan', '/s']:
|
|
402
|
-
print()
|
|
403
|
-
print(colorize(" Project Files:", Colors.BRIGHT_CYAN + Colors.BOLD))
|
|
404
|
-
print_divider("─", 40, Colors.BRIGHT_BLACK)
|
|
405
437
|
print(self.file_manager.scan_project())
|
|
406
438
|
elif cmd in ['/models', '/m']:
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
print(f" • {name}{marker}")
|
|
439
|
+
lines = []
|
|
440
|
+
for name, mid in MODELS.items():
|
|
441
|
+
mark = " (текущая)" if mid == self.current_model else ""
|
|
442
|
+
lines.append(f"{name}{mark}")
|
|
443
|
+
print_section("🤖 Модели", lines)
|
|
413
444
|
elif cmd in ['/model'] and len(parts) > 1:
|
|
414
|
-
|
|
415
|
-
if
|
|
416
|
-
self.current_model = MODELS[
|
|
445
|
+
name = parts[1]
|
|
446
|
+
if name in MODELS:
|
|
447
|
+
self.current_model = MODELS[name]
|
|
417
448
|
self.model_settings = get_model_settings(self.current_model)
|
|
418
|
-
print_success(f"
|
|
449
|
+
print_success(f"Модель изменена на {name}")
|
|
419
450
|
elif cmd in ['/clear', '/c']:
|
|
420
451
|
self.conversation_history = [self.conversation_history[0]]
|
|
421
|
-
print_success("
|
|
452
|
+
print_success("История очищена")
|
|
422
453
|
elif cmd in ['/stats']:
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
print(colorize(f" Commands: {len(self.stats['commands_executed'])}", Colors.CYAN))
|
|
454
|
+
print_box([
|
|
455
|
+
f"Сообщений: {self.stats['messages_sent']}",
|
|
456
|
+
f"Создано файлов: {len(self.stats['files_created'])}",
|
|
457
|
+
f"Изменено: {len(self.stats['files_modified'])}",
|
|
458
|
+
f"Удалено: {len(self.stats['files_deleted'])}",
|
|
459
|
+
f"Команд выполнено: {len(self.stats['commands_executed'])}"
|
|
460
|
+
], Theme.SECONDARY)
|
|
431
461
|
elif cmd in ['/exit', '/q']:
|
|
432
|
-
print_success("
|
|
462
|
+
print_success("До свидания!")
|
|
433
463
|
os._exit(0)
|
|
434
464
|
return ""
|
|
435
465
|
|
|
@@ -438,32 +468,19 @@ class HandyCode:
|
|
|
438
468
|
|
|
439
469
|
def run(self):
|
|
440
470
|
print_logo()
|
|
441
|
-
print_divider("─", 60,
|
|
442
|
-
print(colorize(f" 📁
|
|
443
|
-
print(colorize(f" 🤖
|
|
444
|
-
print(colorize(f" {
|
|
445
|
-
print_divider("─", 60,
|
|
471
|
+
print_divider("─", 60, Theme.MUTED)
|
|
472
|
+
print(colorize(f" 📁 Проект: {self.project_path}", Theme.TEXT))
|
|
473
|
+
print(colorize(f" 🤖 Модель: {self.current_model}", Theme.TEXT))
|
|
474
|
+
print(colorize(f" {Theme.MUTED}/help для команд{Colors.RESET}", Theme.MUTED))
|
|
475
|
+
print_divider("─", 60, Theme.MUTED)
|
|
446
476
|
print()
|
|
447
|
-
|
|
448
477
|
while True:
|
|
449
478
|
try:
|
|
450
479
|
self.reset_interrupt()
|
|
451
|
-
user_input = input(colorize(" ❯ ",
|
|
480
|
+
user_input = input(colorize(" ❯ ", Theme.PRIMARY + Colors.BOLD)).strip()
|
|
452
481
|
if user_input:
|
|
453
482
|
self.send_message(user_input)
|
|
454
483
|
except KeyboardInterrupt:
|
|
455
484
|
continue
|
|
456
485
|
except EOFError:
|
|
457
|
-
break
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
# Добавляем colorize как глобальную функцию для удобства
|
|
461
|
-
def colorize(text, color):
|
|
462
|
-
if supports_color():
|
|
463
|
-
return f"{color}{text}{Colors.RESET}"
|
|
464
|
-
return text
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
def supports_color():
|
|
468
|
-
from handycode.utils import supports_color as sc
|
|
469
|
-
return sc()
|
|
486
|
+
break
|
|
@@ -5,15 +5,13 @@
|
|
|
5
5
|
import sys
|
|
6
6
|
from .utils import Colors, supports_color
|
|
7
7
|
|
|
8
|
-
|
|
9
8
|
def get_logo() -> str:
|
|
10
|
-
"""Возвращает ASCII логотип HandyCode"""
|
|
11
9
|
if not supports_color():
|
|
12
10
|
return get_logo_plain()
|
|
13
11
|
|
|
14
12
|
C = Colors
|
|
15
13
|
logo = f"""
|
|
16
|
-
{C.CYAN}
|
|
14
|
+
{C.CYAN}╔═════════════════════════════════════════════════════════════════════════════════════════════════╗
|
|
17
15
|
║ ║
|
|
18
16
|
║ {C.YELLOW}██╗ ██╗{C.CYAN} {C.GREEN}█████╗{C.CYAN} {C.BLUE}███╗ ██╗{C.CYAN} {C.MAGENTA}██████╗{C.CYAN} {C.RED}██╗ ██╗{C.CYAN} {C.WHITE}██████╗{C.CYAN} {C.GREEN}███████╗{C.CYAN} {C.BLUE}██████╗{C.CYAN} {C.MAGENTA}███████╗{C.CYAN} ║
|
|
19
17
|
║ {C.YELLOW}██║ ██║{C.CYAN} {C.GREEN}██╔══██╗{C.CYAN} {C.BLUE}████╗ ██║{C.CYAN} {C.MAGENTA}██╔══██╗{C.CYAN} {C.RED}╚██╗ ██╔╝{C.CYAN} {C.WHITE}██╔════╝{C.CYAN} {C.GREEN}██╔════██╗{C.CYAN} {C.BLUE}██╔══██╗{C.CYAN} {C.MAGENTA}██╔════╝{C.CYAN} ║
|
|
@@ -29,51 +27,35 @@ def get_logo() -> str:
|
|
|
29
27
|
"""
|
|
30
28
|
return logo
|
|
31
29
|
|
|
32
|
-
|
|
33
30
|
def get_logo_plain() -> str:
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
║
|
|
38
|
-
║
|
|
39
|
-
║ ██║ ██║
|
|
40
|
-
║
|
|
41
|
-
║
|
|
42
|
-
║
|
|
43
|
-
║
|
|
44
|
-
║
|
|
45
|
-
║
|
|
46
|
-
║
|
|
47
|
-
|
|
48
|
-
║ ██║ ██║ ██║ ██║ ██║ ██╔══╝ ║
|
|
49
|
-
║ ╚██████╗ ███████╔╝ ██████╔╝ ███████╗ ║
|
|
50
|
-
║ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝ ║
|
|
51
|
-
║ ║
|
|
52
|
-
║ AI Ассистент для разработки ║
|
|
53
|
-
║ Prod. by AURA Tec. ║
|
|
54
|
-
║ ║
|
|
55
|
-
╚══════════════════════════════════════════════════════════════╝
|
|
31
|
+
return r"""
|
|
32
|
+
╔═════════════════════════════════════════════════════════════════════════════════════════════════╗
|
|
33
|
+
║ ║
|
|
34
|
+
║ ██╗ ██╗ █████╗ ███╗ ██╗ ██████╗ ██╗ ██╗ ██████╗ ███████╗ ██████╗ ███████╗ ║
|
|
35
|
+
║ ██║ ██║ ██╔══██╗ ████╗ ██║ ██╔══██╗ ╚██╗ ██╔╝ ██╔════╝ ██╔════██╗ ██╔══██╗ ██╔════╝ ║
|
|
36
|
+
║ ███████║ ███████║ ██╔██╗ ██║ ██║ ██║ ╚████╔╝ ██║ ██║ ██║ ██║ ██║ █████╗ ║
|
|
37
|
+
║ ██╔══██║ ██╔══██║ ██║╚██╗██║ ██║ ██║ ╚██╔╝ ██║ ██║ ██║ ██║ ██║ ██╔══╝ ║
|
|
38
|
+
║ ██║ ██║ ██║ ██║ ██║ ╚████║ ██████╔╝ ██║ ╚██████╗ ███████╔╝ ██████╔╝ ███████╗ ║
|
|
39
|
+
║ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═══╝ ╚═════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝ ║
|
|
40
|
+
║ ║
|
|
41
|
+
║ AI Ассистент для разработки ║
|
|
42
|
+
║ Prod. by AURA Tec. ║
|
|
43
|
+
║ ║
|
|
44
|
+
╚═════════════════════════════════════════════════════════════════════════════════════════════════╝
|
|
56
45
|
"""
|
|
57
|
-
return logo
|
|
58
|
-
|
|
59
46
|
|
|
60
47
|
def get_small_logo() -> str:
|
|
61
|
-
"""Возвращает маленький логотип"""
|
|
62
48
|
if not supports_color():
|
|
63
49
|
return "HandyCode v2.1.3"
|
|
64
|
-
|
|
65
50
|
C = Colors
|
|
66
|
-
return f"{C.CYAN}HandyCode{C.RESET} {C.WHITE}v2.1.3{C.RESET}
|
|
67
|
-
|
|
51
|
+
return f"{C.CYAN}HandyCode{C.RESET} {C.WHITE}v2.1.3{C.RESET} – {C.GREEN}AI Ассистент{C.RESET} {C.BRIGHT_BLACK}Prod. by AURA Tec.{C.RESET}"
|
|
68
52
|
|
|
69
53
|
def get_install_logo() -> str:
|
|
70
|
-
"""Возвращает логотип для установки"""
|
|
71
54
|
if not supports_color():
|
|
72
55
|
return get_logo_plain()
|
|
73
|
-
|
|
74
56
|
C = Colors
|
|
75
|
-
|
|
76
|
-
{C.CYAN}
|
|
57
|
+
return f"""
|
|
58
|
+
{C.CYAN}╔═════════════════════════════════════════════════════════════════════════════════════════════════╗
|
|
77
59
|
║ ║
|
|
78
60
|
║ {C.YELLOW}██╗ ██╗{C.CYAN} {C.GREEN}█████╗{C.CYAN} {C.BLUE}███╗ ██╗{C.CYAN} {C.MAGENTA}██████╗{C.CYAN} {C.RED}██╗ ██╗{C.CYAN} {C.WHITE}██████╗{C.CYAN} {C.GREEN}███████╗{C.CYAN} {C.BLUE}██████╗{C.CYAN} {C.MAGENTA}███████╗{C.CYAN} ║
|
|
79
61
|
║ {C.YELLOW}██║ ██║{C.CYAN} {C.GREEN}██╔══██╗{C.CYAN} {C.BLUE}████╗ ██║{C.CYAN} {C.MAGENTA}██╔══██╗{C.CYAN} {C.RED}╚██╗ ██╔╝{C.CYAN} {C.WHITE}██╔════╝{C.CYAN} {C.GREEN}██╔════██╗{C.CYAN} {C.BLUE}██╔══██╗{C.CYAN} {C.MAGENTA}██╔════╝{C.CYAN} ║
|
|
@@ -82,10 +64,9 @@ def get_install_logo() -> str:
|
|
|
82
64
|
║ {C.YELLOW}██║ ██║{C.CYAN} {C.GREEN}██║ ██║{C.CYAN} {C.BLUE}██║ ╚████║{C.CYAN} {C.MAGENTA}██████╔╝{C.CYAN} {C.RED}██║{C.CYAN} {C.WHITE}╚██████╗{C.CYAN} {C.GREEN}███████╔╝{C.CYAN} {C.BLUE}██████╔╝{C.CYAN} {C.MAGENTA}███████╗{C.CYAN} ║
|
|
83
65
|
║ {C.YELLOW}╚═╝ ╚═╝{C.CYAN} {C.GREEN}╚═╝ ╚═╝{C.CYAN} {C.BLUE}╚═╝ ╚═══╝{C.CYAN} {C.MAGENTA}╚═════╝{C.CYAN} {C.RED}╚═╝{C.CYAN} {C.WHITE} ╚═════╝{C.CYAN} {C.GREEN}╚═════╝{C.CYAN} {C.BLUE}╚═════╝{C.CYAN} {C.MAGENTA}╚══════╝{C.CYAN} ║
|
|
84
66
|
║ ║
|
|
85
|
-
║ {C.WHITE}
|
|
67
|
+
║ {C.WHITE}УСТАНОВКА HANDYCODE{C.CYAN} ║
|
|
86
68
|
║ {C.WHITE}AI Ассистент для разработки{C.CYAN} ║
|
|
87
69
|
║ {C.WHITE}Prod. by AURA Tec.{C.CYAN} ║
|
|
88
70
|
║ ║
|
|
89
71
|
╚═════════════════════════════════════════════════════════════════════════════════════════════════╝{C.RESET}
|
|
90
|
-
"""
|
|
91
|
-
return logo
|
|
72
|
+
"""
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Вспомогательные функции для HandyCode
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
|
+
class Colors:
|
|
9
|
+
# Базовые
|
|
10
|
+
RESET = '\033[0m'
|
|
11
|
+
BOLD = '\033[1m'
|
|
12
|
+
DIM = '\033[2m'
|
|
13
|
+
ITALIC = '\033[3m'
|
|
14
|
+
UNDERLINE = '\033[4m'
|
|
15
|
+
|
|
16
|
+
# Стандартные мягкие тона (не яркие)
|
|
17
|
+
BLACK = '\033[30m'
|
|
18
|
+
RED = '\033[31m'
|
|
19
|
+
GREEN = '\033[32m'
|
|
20
|
+
YELLOW = '\033[33m'
|
|
21
|
+
BLUE = '\033[34m'
|
|
22
|
+
MAGENTA = '\033[35m'
|
|
23
|
+
CYAN = '\033[36m'
|
|
24
|
+
WHITE = '\033[37m'
|
|
25
|
+
|
|
26
|
+
# Яркие (для акцентов)
|
|
27
|
+
BRIGHT_BLACK = '\033[90m'
|
|
28
|
+
BRIGHT_RED = '\033[91m'
|
|
29
|
+
BRIGHT_GREEN = '\033[92m'
|
|
30
|
+
BRIGHT_YELLOW = '\033[93m'
|
|
31
|
+
BRIGHT_BLUE = '\033[94m'
|
|
32
|
+
BRIGHT_MAGENTA = '\033[95m'
|
|
33
|
+
BRIGHT_CYAN = '\033[96m'
|
|
34
|
+
BRIGHT_WHITE = '\033[97m'
|
|
35
|
+
|
|
36
|
+
# Тема HandyCode (премиальная пастель)
|
|
37
|
+
class Theme:
|
|
38
|
+
PRIMARY = Colors.CYAN # мягкий циан
|
|
39
|
+
SECONDARY = Colors.BLUE # спокойный синий
|
|
40
|
+
ACCENT = Colors.MAGENTA # акцент
|
|
41
|
+
SUCCESS = Colors.GREEN
|
|
42
|
+
WARNING = Colors.YELLOW
|
|
43
|
+
ERROR = Colors.RED
|
|
44
|
+
TEXT = Colors.WHITE
|
|
45
|
+
MUTED = Colors.BRIGHT_BLACK # серый для подписей
|
|
46
|
+
HIGHLIGHT = Colors.BRIGHT_WHITE
|
|
47
|
+
|
|
48
|
+
def supports_color():
|
|
49
|
+
if os.name == 'nt':
|
|
50
|
+
try:
|
|
51
|
+
import ctypes
|
|
52
|
+
kernel32 = ctypes.windll.kernel32
|
|
53
|
+
kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7)
|
|
54
|
+
return True
|
|
55
|
+
except:
|
|
56
|
+
return False
|
|
57
|
+
return hasattr(sys.stdout, 'isatty') and sys.stdout.isatty()
|
|
58
|
+
|
|
59
|
+
def colorize(text, color):
|
|
60
|
+
if supports_color():
|
|
61
|
+
return f"{color}{text}{Colors.RESET}"
|
|
62
|
+
return text
|
|
63
|
+
|
|
64
|
+
def print_colored(text, color):
|
|
65
|
+
print(colorize(text, color))
|
|
66
|
+
|
|
67
|
+
def print_header(text):
|
|
68
|
+
print(colorize(text, Theme.PRIMARY + Colors.BOLD))
|
|
69
|
+
|
|
70
|
+
def print_success(text):
|
|
71
|
+
print(colorize(f" ✔ {text}", Theme.SUCCESS))
|
|
72
|
+
|
|
73
|
+
def print_error(text):
|
|
74
|
+
print(colorize(f" ✘ {text}", Theme.ERROR))
|
|
75
|
+
return text
|
|
76
|
+
|
|
77
|
+
def print_warning(text):
|
|
78
|
+
print(colorize(f" ⚠ {text}", Theme.WARNING))
|
|
79
|
+
|
|
80
|
+
def print_info(text):
|
|
81
|
+
print(colorize(f" ℹ {text}", Theme.SECONDARY))
|
|
82
|
+
|
|
83
|
+
def print_logo():
|
|
84
|
+
from .logo import get_logo
|
|
85
|
+
print(get_logo())
|
|
86
|
+
|
|
87
|
+
def print_install_logo():
|
|
88
|
+
from .logo import get_install_logo
|
|
89
|
+
print(get_install_logo())
|
|
90
|
+
|
|
91
|
+
def truncate(text, max_length=100):
|
|
92
|
+
if len(text) <= max_length:
|
|
93
|
+
return text
|
|
94
|
+
return text[:max_length-3] + "..."
|
|
95
|
+
|
|
96
|
+
def format_size(size_bytes):
|
|
97
|
+
for unit in ['B', 'KB', 'MB', 'GB']:
|
|
98
|
+
if size_bytes < 1024:
|
|
99
|
+
return f"{size_bytes:.1f} {unit}"
|
|
100
|
+
size_bytes /= 1024
|
|
101
|
+
return f"{size_bytes:.1f} TB"
|
|
102
|
+
|
|
103
|
+
def print_divider(char="─", width=60, color=Theme.MUTED):
|
|
104
|
+
print(colorize(char * width, color))
|
|
105
|
+
|
|
106
|
+
def print_box(lines, color=Theme.PRIMARY):
|
|
107
|
+
"""Рамка вокруг списка строк"""
|
|
108
|
+
max_len = max((len(line) for line in lines), default=0) + 2
|
|
109
|
+
top = "┌" + "─" * max_len + "┐"
|
|
110
|
+
bottom = "└" + "─" * max_len + "┘"
|
|
111
|
+
print(colorize(top, color))
|
|
112
|
+
for line in lines:
|
|
113
|
+
print(colorize(f"│ {line.ljust(max_len-1)}│", color))
|
|
114
|
+
print(colorize(bottom, color))
|
|
115
|
+
|
|
116
|
+
def print_section(title, content_lines):
|
|
117
|
+
"""Секция с заголовком и содержимым"""
|
|
118
|
+
print_divider("─", 50, Theme.MUTED)
|
|
119
|
+
print(colorize(f" {title}", Theme.HIGHLIGHT + Colors.BOLD))
|
|
120
|
+
for line in content_lines:
|
|
121
|
+
print(colorize(f" {line}", Theme.TEXT))
|
|
122
|
+
print()
|
|
123
|
+
|
|
124
|
+
def print_status(text):
|
|
125
|
+
print(colorize(f" ● {text}", Theme.PRIMARY))
|
|
126
|
+
|
|
127
|
+
def print_file_action(action, path, details=""):
|
|
128
|
+
icons = {'create': '📄', 'modify': '✎', 'delete': '🗑', 'read': '📖'}
|
|
129
|
+
colors_map = {
|
|
130
|
+
'create': Theme.SUCCESS,
|
|
131
|
+
'modify': Theme.WARNING,
|
|
132
|
+
'delete': Theme.ERROR,
|
|
133
|
+
'read': Theme.SECONDARY,
|
|
134
|
+
}
|
|
135
|
+
icon = icons.get(action, '•')
|
|
136
|
+
color = colors_map.get(action, Theme.TEXT)
|
|
137
|
+
msg = f" {icon} {path}"
|
|
138
|
+
if details:
|
|
139
|
+
msg += f" {colorize(details, Theme.MUTED)}"
|
|
140
|
+
print(colorize(msg, color))
|
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Вспомогательные функции для HandyCode
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
import sys
|
|
6
|
-
import os
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class Colors:
|
|
10
|
-
RESET = '\033[0m'
|
|
11
|
-
RED = '\033[91m'
|
|
12
|
-
GREEN = '\033[92m'
|
|
13
|
-
YELLOW = '\033[93m'
|
|
14
|
-
BLUE = '\033[94m'
|
|
15
|
-
MAGENTA = '\033[95m'
|
|
16
|
-
CYAN = '\033[96m'
|
|
17
|
-
WHITE = '\033[97m'
|
|
18
|
-
BRIGHT_BLACK = '\033[90m'
|
|
19
|
-
BRIGHT_RED = '\033[91m'
|
|
20
|
-
BRIGHT_GREEN = '\033[92m'
|
|
21
|
-
BRIGHT_YELLOW = '\033[93m'
|
|
22
|
-
BRIGHT_BLUE = '\033[94m'
|
|
23
|
-
BRIGHT_MAGENTA = '\033[95m'
|
|
24
|
-
BRIGHT_CYAN = '\033[96m'
|
|
25
|
-
BRIGHT_WHITE = '\033[97m'
|
|
26
|
-
BOLD = '\033[1m'
|
|
27
|
-
DIM = '\033[2m'
|
|
28
|
-
ITALIC = '\033[3m'
|
|
29
|
-
UNDERLINE = '\033[4m'
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def supports_color():
|
|
33
|
-
if os.name == 'nt':
|
|
34
|
-
try:
|
|
35
|
-
import ctypes
|
|
36
|
-
kernel32 = ctypes.windll.kernel32
|
|
37
|
-
kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7)
|
|
38
|
-
return True
|
|
39
|
-
except:
|
|
40
|
-
return False
|
|
41
|
-
return hasattr(sys.stdout, 'isatty') and sys.stdout.isatty()
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def colorize(text, color):
|
|
45
|
-
if supports_color():
|
|
46
|
-
return f"{color}{text}{Colors.RESET}"
|
|
47
|
-
return text
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def print_colored(text, color):
|
|
51
|
-
print(colorize(text, color))
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def print_header(text):
|
|
55
|
-
print(colorize(text, Colors.CYAN + Colors.BOLD))
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def print_success(text):
|
|
59
|
-
print(colorize(f" ✓ {text}", Colors.GREEN))
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
def print_error(text):
|
|
63
|
-
print(colorize(f" ✗ {text}", Colors.RED))
|
|
64
|
-
return text
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def print_warning(text):
|
|
68
|
-
print(colorize(f" ⚠ {text}", Colors.YELLOW))
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
def print_info(text):
|
|
72
|
-
print(colorize(f" ℹ {text}", Colors.BLUE))
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
def print_logo():
|
|
76
|
-
from .logo import get_small_logo
|
|
77
|
-
print(get_small_logo())
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
def truncate(text, max_length=100):
|
|
81
|
-
if len(text) <= max_length:
|
|
82
|
-
return text
|
|
83
|
-
return text[:max_length - 3] + "..."
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
def format_size(size_bytes):
|
|
87
|
-
for unit in ['B', 'KB', 'MB', 'GB']:
|
|
88
|
-
if size_bytes < 1024:
|
|
89
|
-
return f"{size_bytes:.1f} {unit}"
|
|
90
|
-
size_bytes /= 1024
|
|
91
|
-
return f"{size_bytes:.1f} TB"
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
def print_box(text, color=Colors.CYAN):
|
|
95
|
-
"""Рисует рамку вокруг текста"""
|
|
96
|
-
lines = text.strip().split('\n')
|
|
97
|
-
width = max(len(line) for line in lines) + 4
|
|
98
|
-
print(colorize(f"╭{'─' * (width - 2)}╮", color))
|
|
99
|
-
for line in lines:
|
|
100
|
-
print(colorize(f"│ {line.ljust(width - 4)} │", color))
|
|
101
|
-
print(colorize(f"╰{'─' * (width - 2)}╯", color))
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
def print_divider(char="─", width=60, color=Colors.BRIGHT_BLACK):
|
|
105
|
-
"""Рисует разделитель"""
|
|
106
|
-
print(colorize(char * width, color))
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
def print_file_action(action_type, path, details=""):
|
|
110
|
-
"""Красиво показывает действие с файлом"""
|
|
111
|
-
icons = {
|
|
112
|
-
'create': '📄',
|
|
113
|
-
'modify': '✏️',
|
|
114
|
-
'delete': '🗑️',
|
|
115
|
-
'read': '📖',
|
|
116
|
-
}
|
|
117
|
-
colors_map = {
|
|
118
|
-
'create': Colors.GREEN,
|
|
119
|
-
'modify': Colors.YELLOW,
|
|
120
|
-
'delete': Colors.RED,
|
|
121
|
-
'read': Colors.BLUE,
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
icon = icons.get(action_type, '•')
|
|
125
|
-
color = colors_map.get(action_type, Colors.WHITE)
|
|
126
|
-
|
|
127
|
-
if details:
|
|
128
|
-
print(colorize(f" {icon} {path} {Colors.BRIGHT_BLACK}{details}{Colors.RESET}", color))
|
|
129
|
-
else:
|
|
130
|
-
print(colorize(f" {icon} {path}", color))
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
def print_command(cmd, index=1):
|
|
134
|
-
"""Красиво показывает команду"""
|
|
135
|
-
print(colorize(f" {index}. ⚡ {cmd}", Colors.YELLOW))
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
def print_status(msg):
|
|
139
|
-
"""Показывает статус"""
|
|
140
|
-
print(colorize(f" ● {msg}", Colors.CYAN))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|