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