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.
Files changed (24) hide show
  1. {handycode-2.1.6 → handycode-2.2.1}/PKG-INFO +1 -1
  2. {handycode-2.1.6 → handycode-2.2.1}/handycode/__init__.py +1 -1
  3. {handycode-2.1.6 → handycode-2.2.1}/handycode/assistant.py +212 -195
  4. {handycode-2.1.6 → handycode-2.2.1}/handycode/logo.py +20 -39
  5. handycode-2.2.1/handycode/utils.py +140 -0
  6. {handycode-2.1.6 → handycode-2.2.1}/handycode.egg-info/PKG-INFO +1 -1
  7. {handycode-2.1.6 → handycode-2.2.1}/setup.py +1 -1
  8. handycode-2.1.6/handycode/utils.py +0 -140
  9. {handycode-2.1.6 → handycode-2.2.1}/LICENSE +0 -0
  10. {handycode-2.1.6 → handycode-2.2.1}/README.md +0 -0
  11. {handycode-2.1.6 → handycode-2.2.1}/handycode/__main__.py +0 -0
  12. {handycode-2.1.6 → handycode-2.2.1}/handycode/cli.py +0 -0
  13. {handycode-2.1.6 → handycode-2.2.1}/handycode/config.py +0 -0
  14. {handycode-2.1.6 → handycode-2.2.1}/handycode/file_manager.py +0 -0
  15. {handycode-2.1.6 → handycode-2.2.1}/handycode/main.py +0 -0
  16. {handycode-2.1.6 → handycode-2.2.1}/handycode/models.py +0 -0
  17. {handycode-2.1.6 → handycode-2.2.1}/handycode/project_templates.py +0 -0
  18. {handycode-2.1.6 → handycode-2.2.1}/handycode/security.py +0 -0
  19. {handycode-2.1.6 → handycode-2.2.1}/handycode.egg-info/SOURCES.txt +0 -0
  20. {handycode-2.1.6 → handycode-2.2.1}/handycode.egg-info/dependency_links.txt +0 -0
  21. {handycode-2.1.6 → handycode-2.2.1}/handycode.egg-info/entry_points.txt +0 -0
  22. {handycode-2.1.6 → handycode-2.2.1}/handycode.egg-info/requires.txt +0 -0
  23. {handycode-2.1.6 → handycode-2.2.1}/handycode.egg-info/top_level.txt +0 -0
  24. {handycode-2.1.6 → handycode-2.2.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: handycode
3
- Version: 2.1.6
3
+ Version: 2.2.1
4
4
  Summary: AI Code Assistant for DeepSeek
5
5
  Home-page: https://github.com/AuraTechno/HandyCode
6
6
  Author: AuraTechno
@@ -3,7 +3,7 @@ HandyCode - AI Ассистент для разработки
3
3
  Аналог Claude Code для командной строки
4
4
  """
5
5
 
6
- __version__ = "2.1.6"
6
+ __version__ = "2.2.1"
7
7
  __author__ = "AURA Tec."
8
8
  __license__ = "MIT"
9
9
 
@@ -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, print_command, print_status
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
- "files_created": [],
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
- pass
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
- FORMAT:
125
- [[CREATE:path/file]]
126
- code here
127
- [[END]]
128
-
129
- [[MODIFY:path/file]]
130
- new code here
131
- [[END]]
132
-
133
- [[EXEC:command]]
134
-
135
- RULES:
136
- 1. CREATE + EXEC in ONE response
137
- 2. Use [[END]] after file content
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 ⚠ Press Ctrl+C again to exit")
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
- lines = content.count('\n') + 1
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
- lines = content.count('\n') + 1
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.api_url,
229
- headers={
230
- "Authorization": f"Bearer {self.api_key}",
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.api_url,
283
- data=json_data,
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
- with urllib.request.urlopen(req, context=ctx, timeout=120) as response:
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, Colors.BRIGHT_BLACK)
348
- print(colorize(" HandyCode", Colors.BRIGHT_CYAN + Colors.BOLD))
349
- print_divider("", 60, Colors.BRIGHT_BLACK)
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
- choice = 'A'
402
+ selected_commands = self.pending_commands
365
403
  else:
366
- print()
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 choice == 'A':
406
+ if selected_commands:
372
407
  print()
373
- for cmd in self.pending_commands:
408
+ print_section("⚡ Выполнение команд", [])
409
+ for cmd in selected_commands:
374
410
  if self.security.is_safe_command(cmd):
375
- print_status(f"Running: {cmd}")
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
- print()
390
- print(colorize(" Commands:", Colors.BRIGHT_CYAN + Colors.BOLD))
391
- print_divider("", 40, Colors.BRIGHT_BLACK)
392
- print(colorize(" /help Show this help", Colors.WHITE))
393
- print(colorize(" /scan Scan project files", Colors.WHITE))
394
- print(colorize(" /models List AI models", Colors.WHITE))
395
- print(colorize(" /model NAME Switch model", Colors.WHITE))
396
- print(colorize(" /clear Clear chat history", Colors.WHITE))
397
- print(colorize(" /save Save session to file", Colors.WHITE))
398
- print(colorize(" /stats Show statistics", Colors.WHITE))
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
- print()
408
- print(colorize(" Available Models:", Colors.BRIGHT_CYAN + Colors.BOLD))
409
- print_divider("─", 40, Colors.BRIGHT_BLACK)
410
- for name in MODELS:
411
- marker = colorize(" (current)", Colors.GREEN) if MODELS[name] == self.current_model else ""
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
- model_name = parts[1]
415
- if model_name in MODELS:
416
- self.current_model = MODELS[model_name]
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"Switched to {model_name}")
449
+ print_success(f"Модель изменена на {name}")
419
450
  elif cmd in ['/clear', '/c']:
420
451
  self.conversation_history = [self.conversation_history[0]]
421
- print_success("Chat history cleared")
452
+ print_success("История очищена")
422
453
  elif cmd in ['/stats']:
423
- print()
424
- print(colorize(" Session Stats:", Colors.BRIGHT_CYAN + Colors.BOLD))
425
- print_divider("─", 40, Colors.BRIGHT_BLACK)
426
- print(colorize(f" Messages: {self.stats['messages_sent']}", Colors.WHITE))
427
- print(colorize(f" Created: {len(self.stats['files_created'])} files", Colors.GREEN))
428
- print(colorize(f" Modified: {len(self.stats['files_modified'])} files", Colors.YELLOW))
429
- print(colorize(f" Deleted: {len(self.stats['files_deleted'])} files", Colors.RED))
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("Goodbye!")
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, Colors.BRIGHT_BLACK)
442
- print(colorize(f" 📁 Project: {self.project_path}", Colors.WHITE))
443
- print(colorize(f" 🤖 Model: {self.current_model}", Colors.WHITE))
444
- print(colorize(f" {Colors.BRIGHT_BLACK}/help for commands{Colors.RESET}", Colors.BRIGHT_BLACK))
445
- print_divider("─", 60, Colors.BRIGHT_BLACK)
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(" ❯ ", Colors.BRIGHT_CYAN + Colors.BOLD)).strip()
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}{C.BOLD}╔═════════════════════════════════════════════════════════════════════════════════════════════════╗
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
- """Возвращает простой ASCII логотип без цветов"""
35
- logo = r"""
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} - {C.GREEN}AI Ассистент{C.RESET} | {C.BRIGHT_BLACK}Prod. by AURA Tec.{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
- logo = f"""
76
- {C.CYAN}{C.BOLD}╔═════════════════════════════════════════════════════════════════════════════════════════════════╗
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}{C.BOLD}УСТАНОВКА HANDYCODE{C.CYAN} ║
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: handycode
3
- Version: 2.1.6
3
+ Version: 2.2.1
4
4
  Summary: AI Code Assistant for DeepSeek
5
5
  Home-page: https://github.com/AuraTechno/HandyCode
6
6
  Author: AuraTechno
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="handycode",
5
- version="2.1.6",
5
+ version="2.2.1",
6
6
  author="AuraTechno",
7
7
  description="AI Code Assistant for DeepSeek",
8
8
  long_description="HandyCode - AI Code Assistant",
@@ -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