· 4 months ago · May 18, 2025, 03:20 PM
1
2
3// FOLDER: /
4Ścieżka: /src/__init__.py
5Rozmiar: 0,03 KB
6Zawartość:
7# Plik inicjalizujący moduł src
8
9Ścieżka: app.py:
10Zawartość:
11# Główny plik aplikacji IDE
12#/app.py
13
14import sys
15from PyQt6.QtWidgets import QApplication
16from PyQt6.QtCore import QLocale
17from src.ui import IDEWindow
18
19if __name__ == '__main__':
20 app = QApplication(sys.argv)
21 QLocale.setDefault(QLocale(QLocale.Language.Polish, QLocale.Country.Poland))
22 main_window = IDEWindow()
23 main_window.show()
24 sys.exit(app.exec())
25
26Ścieżka: /src/config.py
27Rozmiar: 7,52 KB
28Zawartość:
29# Konfiguracja i stałe aplikacji
30#/src/config.py
31
32import os
33import re
34from PyQt6.QtGui import QTextCharFormat, QColor, QFont
35
36APP_DIR = os.path.dirname(os.path.abspath(os.path.dirname(__file__)))
37DATA_DIR = os.path.join(APP_DIR, 'userdata')
38PROJECTS_DIR = os.path.join(APP_DIR, 'projects')
39SETTINGS_FILE = os.path.join(DATA_DIR, 'settings.json')
40RECENTS_FILE = os.path.join(DATA_DIR, 'recents.json')
41os.makedirs(DATA_DIR, exist_ok=True)
42os.makedirs(PROJECTS_DIR, exist_ok=True)
43
44FORMAT_DEFAULT = QTextCharFormat()
45FORMAT_KEYWORD = QTextCharFormat()
46FORMAT_KEYWORD.setForeground(QColor("#000080")) # Navy
47FORMAT_STRING = QTextCharFormat()
48FORMAT_STRING.setForeground(QColor("#008000")) # Green
49FORMAT_COMMENT = QTextCharFormat()
50FORMAT_COMMENT.setForeground(QColor("#808080")) # Gray
51FORMAT_COMMENT.setFontItalic(True)
52FORMAT_FUNCTION = QTextCharFormat()
53FORMAT_FUNCTION.setForeground(QColor("#0000FF")) # Blue
54FORMAT_CLASS = QTextCharFormat()
55FORMAT_CLASS.setForeground(QColor("#A52A2A")) # Brown
56FORMAT_CLASS.setFontWeight(QFont.Weight.Bold)
57FORMAT_NUMBERS = QTextCharFormat()
58FORMAT_NUMBERS.setForeground(QColor("#FF0000")) # Red
59FORMAT_OPERATOR = QTextCharFormat()
60FORMAT_OPERATOR.setForeground(QColor("#A62929")) # Dark Red
61FORMAT_BUILTIN = QTextCharFormat()
62FORMAT_BUILTIN.setForeground(QColor("#008080")) # Teal
63FORMAT_SECTION = QTextCharFormat() # Dla sekcji w INI
64FORMAT_SECTION.setForeground(QColor("#800080")) # Purple
65FORMAT_SECTION.setFontWeight(QFont.Weight.Bold)
66FORMAT_PROPERTY = QTextCharFormat() # Dla kluczy/właściwości w INI/JSON
67FORMAT_PROPERTY.setForeground(QColor("#B8860B")) # DarkGoldenrod
68
69HIGHLIGHTING_RULES = {
70 'python': {
71 'keywords': ['and', 'as', 'assert', 'async', 'await', 'break', 'class', 'continue', 'def', 'del', 'elif', 'else',
72 'except', 'False', 'finally', 'for', 'from', 'global', 'if', 'import', 'in', 'is', 'lambda', 'None',
73 'nonlocal', 'not', 'or', 'pass', 'raise', 'return', 'True', 'try', 'while', 'with', 'yield'],
74 'builtins': ['print', 'len', 'range', 'list', 'dict', 'tuple', 'set', 'str', 'int', 'float', 'bool', 'open', 'isinstance'],
75 'patterns': [
76 (r'\b[A-Za-z_][A-Za-z0-9_]*\s*\(', FORMAT_FUNCTION),
77 (r'\bclass\s+([A-Za-z_][A-Za-z0-9_]*)\b', FORMAT_CLASS),
78 (r'\b\d+(\.\d*)?\b', FORMAT_NUMBERS),
79 (r'[+\-*/=<>!&|]', FORMAT_OPERATOR),
80 (r'".*?"', FORMAT_STRING),
81 (r"'.*?'", FORMAT_STRING),
82 (r'#.*', FORMAT_COMMENT),
83 ]
84 },
85 'javascript': {
86 'keywords': ['abstract', 'arguments', 'await', 'boolean', 'break', 'byte', 'case', 'catch', 'char', 'class', 'const', 'continue',
87 'debugger', 'default', 'delete', 'do', 'double', 'else', 'enum', 'eval', 'export', 'extends', 'false', 'final',
88 'finally', 'float', 'for', 'function', 'goto', 'if', 'implements', 'import', 'in', 'instanceof', 'int', 'interface',
89 'let', 'long', 'native', 'new', 'null', 'package', 'private', 'protected', 'public', 'return', 'short', 'static',
90 'super', 'switch', 'synchronized', 'this', 'throw', 'throws', 'transient', 'true', 'try', 'typeof', 'var', 'void',
91 'volatile', 'while', 'with', 'yield'],
92 'builtins': ['console', 'log', 'warn', 'error', 'info', 'Math', 'Date', 'Array', 'Object', 'String', 'Number', 'Boolean', 'RegExp', 'JSON', 'Promise', 'setTimeout', 'setInterval'],
93 'patterns': [
94 (r'\b[A-Za-z_][A-Za-z0-9_]*\s*\(', FORMAT_FUNCTION),
95 (r'\bclass\s+([A-Za-z_][A-ZaZ0-9_]*)\b', FORMAT_CLASS),
96 (r'\b\d+(\.\d*)?\b', FORMAT_NUMBERS),
97 (r'[+\-*/=<>!&|]', FORMAT_OPERATOR),
98 (r'".*?"', FORMAT_STRING),
99 (r"'.*?'", FORMAT_STRING),
100 (r'//.*', FORMAT_COMMENT),
101 ]
102 },
103 'html': {
104 'keywords': [],
105 'builtins': [],
106 'patterns': [
107 (r'<[^>]+>', FORMAT_KEYWORD),
108 (r'[a-zA-Z0-9_-]+\s*=', FORMAT_OPERATOR),
109 (r'".*?"', FORMAT_STRING),
110 (r"'.*?'", FORMAT_STRING),
111 (r'&[a-zA-Z0-9]+;', FORMAT_BUILTIN),
112 (r'<!--.*?-->', FORMAT_COMMENT, re.DOTALL),
113 ]
114 },
115 'css': {
116 'keywords': [],
117 'builtins': [],
118 'patterns': [
119 (r'\.[a-zA-Z0-9_-]+', FORMAT_CLASS),
120 (r'#[a-zA-Z0-9_-]+', FORMAT_BUILTIN),
121 (r'[a-zA-Z0-9_-]+\s*:', FORMAT_KEYWORD),
122 (r';', FORMAT_OPERATOR),
123 (r'\{|\}', FORMAT_OPERATOR),
124 (r'\(|\)', FORMAT_OPERATOR),
125 (r'\b\d+(\.\d*)?(px|em|%|vh|vw|rem|pt|cm|mm)?\b', FORMAT_NUMBERS),
126 (r'#[0-9a-fA-F]{3,6}', FORMAT_NUMBERS),
127 (r'".*?"', FORMAT_STRING),
128 (r"'.*?'", FORMAT_STRING),
129 ]
130 },
131 'c++': {
132 'keywords': ['alignas', 'alignof', 'and', 'and_eq', 'asm', 'atomic_cancel', 'atomic_commit', 'atomic_noexcept', 'auto',
133 'bitand', 'bitor', 'bool', 'break', 'case', 'catch', 'char', 'char8_t', 'char16_t', 'char32_t', 'class',
134 'compl', 'concept', 'const', 'consteval', 'constexpr', 'constinit', 'const_cast', 'continue', 'co_await',
135 'co_return', 'decltype', 'default', 'delete', 'do', 'double', 'dynamic_cast', 'else', 'enum',
136 'explicit', 'export', 'extern', 'false', 'float', 'for', 'friend', 'goto', 'if', 'inline', 'int', 'long',
137 'mutable', 'namespace', 'new', 'noexcept', 'not', 'not_eq', 'nullptr', 'operator', 'or', 'or_eq', 'private',
138 'protected', 'public', 'reflexpr', 'register', 'reinterpret_cast', 'requires', 'return', 'short', 'signed',
139 'sizeof', 'static', 'static_assert', 'static_cast', 'struct', 'switch', 'synchronized', 'template',
140 'this', 'thread_local', 'throw', 'true', 'try', 'typedef', 'typeid', 'typename', 'union', 'unsigned',
141 'using', 'virtual', 'void', 'volatile', 'wchar_t', 'while', 'xor', 'xor_eq'],
142 'builtins': ['cout', 'cin', 'endl', 'string', 'vector', 'map', 'set', 'array', 'queue', 'stack', 'pair', 'algorithm', 'iostream', 'fstream', 'sstream', 'cmath', 'cstdlib', 'cstdio', 'ctime'],
143 'patterns': [
144 (r'\b[A-Za-z_][A-ZaZ0-9_]*\s*\(', FORMAT_FUNCTION),
145 (r'\bclass\s+([A-Za-z_][A-ZaZ0-9_]*)\b', FORMAT_CLASS),
146 (r'\bstruct\s+([A-Za-z_][A-ZaZ0-9_]*)\b', FORMAT_CLASS),
147 (r'\b\d+(\.\d*)?\b', FORMAT_NUMBERS),
148 (r'[+\-*/=<>!&|%^~?:]', FORMAT_OPERATOR),
149 (r'".*?"', FORMAT_STRING),
150 (r"'.*?'", FORMAT_STRING),
151 (r'//.*', FORMAT_COMMENT),
152 ]
153 },
154 'ini': {
155 'keywords': [],
156 'builtins': [],
157 'patterns': [
158 (r'^\[.*?\]', FORMAT_SECTION),
159 (r'^[a-zA-Z0-9_-]+\s*=', FORMAT_PROPERTY),
160 (r';.*', FORMAT_COMMENT),
161 (r'#.*', FORMAT_COMMENT),
162 (r'[+\-*/=<>!&|]', FORMAT_OPERATOR),
163 (r'=\s*".*?"', FORMAT_STRING),
164 (r"=\s*'.*?'", FORMAT_STRING),
165 (r'=\s*[^;#"\'].*', FORMAT_STRING),
166 ]
167 },
168 'json': {
169 'keywords': ['true', 'false', 'null'],
170 'builtins': [],
171 'patterns': [
172 (r'"(?:[^"\\]|\\.)*"\s*:', FORMAT_PROPERTY),
173 (r'".*?"', FORMAT_STRING),
174 (r'\b-?\d+(\.\d+)?([eE][+-]?\d+)?\b', FORMAT_NUMBERS),
175 (r'\{|\}|\[|\]|:|,', FORMAT_OPERATOR),
176 ]
177 }
178}
179
180
181Ścieżka: /src/console.py
182Rozmiar: 23,59 KB
183Zawartość:
184# Zarządzanie konsolą i chatem AI
185# /src/console.py
186
187import os
188import sys
189import shlex
190import json
191import requests
192import platform
193import markdown2
194from PyQt6.QtCore import QProcess, QProcessEnvironment, pyqtSignal, QObject, QTimer, QThread, pyqtSlot
195from PyQt6.QtGui import QTextCharFormat, QColor, QFont
196from PyQt6.QtWidgets import QPlainTextEdit, QLineEdit, QVBoxLayout, QWidget, QPushButton, QComboBox, QHBoxLayout, QTabWidget, QApplication, QTextEdit
197
198class ConsoleManager(QObject):
199 output_received = pyqtSignal(str, bool) # tekst, is_error
200 status_updated = pyqtSignal(str)
201
202 def __init__(self, parent=None):
203 super().__init__(parent)
204 self.process = QProcess(self)
205 self.process.readyReadStandardOutput.connect(self._handle_stdout)
206 self.process.readyReadStandardError.connect(self._handle_stderr)
207 self.process.finished.connect(self._handle_process_finished)
208
209 def run_command(self, command_text, working_dir, python_path="", node_path=""):
210 if self.process.state() != QProcess.ProcessState.NotRunning:
211 self.output_received.emit("Inny proces już działa. Zakończ go najpierw.", True)
212 self.status_updated.emit("Błąd: Inny proces aktywny.")
213 return
214
215 try:
216 command = shlex.split(command_text)
217 except ValueError as e:
218 self.output_received.emit(f"Błąd parsowania komendy: {e}", True)
219 self.status_updated.emit("Błąd parsowania komendy.")
220 return
221
222 if not command:
223 self.output_received.emit("Błąd: Pusta komenda.", True)
224 self.status_updated.emit("Błąd: Pusta komenda.")
225 return
226
227 command_str = shlex.join(command)
228 self.output_received.emit(f"Uruchamianie: {command_str}\nw katalogu: {working_dir}\n---", False)
229 self.status_updated.emit("Proces uruchomiony...")
230
231 try:
232 self.process.setWorkingDirectory(working_dir)
233 env = QProcessEnvironment.systemEnvironment()
234 current_path = env.value("PATH", "")
235 paths_to_prepend = []
236
237 if python_path and os.path.exists(python_path):
238 py_dir = os.path.dirname(python_path)
239 current_path_dirs = [os.path.normcase(p) for p in current_path.split(os.pathsep) if p]
240 if os.path.normcase(py_dir) not in current_path_dirs:
241 paths_to_prepend.append(py_dir)
242 if node_path and os.path.exists(node_path):
243 node_dir = os.path.dirname(node_path)
244 if os.path.normcase(node_dir) not in current_path_dirs:
245 paths_to_prepend.append(node_dir)
246
247 if paths_to_prepend:
248 new_path = os.pathsep.join(paths_to_prepend) + (os.pathsep + current_path if current_path else "")
249 env.insert("PATH", new_path)
250
251 self.process.setProcessEnvironment(env)
252
253 # Poprawka dla Windows: odpal komendy przez cmd.exe
254 if platform.system() == "Windows":
255 self.process.start("cmd.exe", ["/c", command_text])
256 else:
257 program = command[0]
258 arguments = command[1:]
259 self.process.start(program, arguments)
260
261 if not self.process.waitForStarted(1000):
262 error = self.process.errorString()
263 self.output_received.emit(f"Nie udało się uruchomić '{command_text}': {error}", True)
264 self.status_updated.emit(f"Błąd uruchamiania: {command_text}")
265 except Exception as e:
266 self.output_received.emit(f"Błąd podczas uruchamiania: {e}", True)
267 self.status_updated.emit("Błąd uruchamiania.")
268
269 def _handle_stdout(self):
270 while self.process.bytesAvailable():
271 data = self.process.readAllStandardOutput()
272 try:
273 text = bytes(data).decode('utf-8')
274 except UnicodeDecodeError:
275 text = bytes(data).decode('utf-8', errors='replace')
276 self.output_received.emit(text, False)
277
278 def _handle_stderr(self):
279 while self.process.bytesAvailable():
280 data = self.process.readAllStandardError()
281 try:
282 text = bytes(data).decode('utf-8')
283 except UnicodeDecodeError:
284 text = bytes(data).decode('utf-8', errors='replace')
285 self.output_received.emit(text, True)
286
287 def _handle_process_finished(self, exit_code, exit_status):
288 self._handle_stdout()
289 self._handle_stderr()
290 self.output_received.emit("\n--- Zakończono proces ---", False)
291 if exit_status == QProcess.ExitStatus.NormalExit and exit_code == 0:
292 self.output_received.emit(f"Kod wyjścia: {exit_code}", False)
293 self.status_updated.emit("Proces zakończony pomyślnie.")
294 else:
295 self.output_received.emit(f"Proces zakończony z błędem (kod: {exit_code}).", True)
296 self.status_updated.emit(f"Błąd procesu. Kod wyjścia: {exit_code}")
297
298class AIWorker(QThread):
299 result = pyqtSignal(str, bool)
300 status = pyqtSignal(str)
301 def __init__(self, ai_manager, message, file_content=None):
302 super().__init__()
303 self.ai_manager = ai_manager
304 self.message = message
305 self.file_content = file_content
306 def run(self):
307 try:
308 # Dołącz plik do prompta jeśli jest
309 if self.file_content:
310 prompt = f"[Kontekst pliku poniżej]\n\n{self.file_content}\n\n[Twoja wiadomość]\n{self.message}"
311 else:
312 prompt = self.message
313 response, is_error = self.ai_manager._send_message_internal(prompt)
314 self.result.emit(response, is_error)
315 self.status.emit("Otrzymano odpowiedź od AI.")
316 except Exception as e:
317 self.result.emit(f"Błąd komunikacji z AI: {e}", True)
318 self.status.emit("Błąd komunikacji z AI.")
319
320class AIChatManager(QObject):
321 output_received = pyqtSignal(str, bool) # tekst, is_error
322 status_updated = pyqtSignal(str)
323 models_updated = pyqtSignal(list) # lista modeli
324
325 def __init__(self, settings, parent=None):
326 super().__init__(parent)
327 self.settings = settings
328 self.api_key = settings.get("api_key", "")
329 self.gemini_api_key = settings.get("gemini_api_key", "")
330 self.mistral_api_key = settings.get("mistral_api_key", "")
331 self.provider = settings.get("ai_provider", "grok")
332 self.base_url = self._get_base_url(self.provider)
333 self.current_model = settings.get("ai_model", "grok-3")
334 self.conversation_history = []
335 self.mistral_available = self._check_mistral_import()
336 self._fetch_models()
337
338 def _check_mistral_import(self):
339 try:
340 import mistralai
341 # Sprawdź wersję
342 from pkg_resources import get_distribution
343 version = get_distribution("mistralai").version
344 if version.startswith("0."):
345 self.output_received.emit("Zła wersja mistralai! Potrzebujesz 1.0.0+, masz " + version, True)
346 self.status_updated.emit("Zła wersja biblioteki mistralai!")
347 return False
348 return True
349 except ImportError:
350 self.output_received.emit("Biblioteka mistralai nie jest zainstalowana! Zainstaluj: pip install mistralai", True)
351 return False
352
353 def update_settings(self, settings):
354 self.settings = settings
355 self.api_key = settings.get("api_key", "")
356 self.gemini_api_key = settings.get("gemini_api_key", "")
357 self.mistral_api_key = settings.get("mistral_api_key", "")
358 self.provider = settings.get("ai_provider", "grok")
359 self.base_url = self._get_base_url(self.provider)
360 self.current_model = settings.get("ai_model", "grok-3")
361 self.mistral_available = self._check_mistral_import()
362 self._fetch_models()
363
364 def _get_base_url(self, provider):
365 if provider == "grok":
366 return "https://api.x.ai/v1"
367 elif provider == "gemini":
368 return "https://generativelanguage.googleapis.com/v1beta"
369 elif provider == "mistral":
370 return "https://api.mistral.ai/v1"
371 return ""
372
373 def _fetch_models(self):
374 self.models_updated.emit([])
375 if self.provider == "grok":
376 try:
377 headers = {"Authorization": f"Bearer {self.api_key}"}
378 response = requests.get(f"{self.base_url}/models", headers=headers)
379 response.raise_for_status()
380 models_data = response.json()
381 models = [model["id"] for model in models_data.get("data", []) if model.get("id")]
382 if models:
383 self.models_updated.emit(models)
384 self.current_model = models[0] if models else "grok-3"
385 self.status_updated.emit("Pobrano listę modeli AI (Grok).")
386 else:
387 self.output_received.emit("Brak dostępnych modeli w API Grok.", True)
388 self.status_updated.emit("Błąd: Brak modeli AI Grok.")
389 except Exception as e:
390 self.output_received.emit(f"Błąd pobierania modeli Grok: {e}", True)
391 self.status_updated.emit("Błąd pobierania modeli Grok.")
392 elif self.provider == "gemini":
393 try:
394 models = [
395 "gemini-1.5-flash-latest",
396 "gemini-1.5-pro-latest",
397 "gemini-2.0-flash-thinking-exp-1219",
398 "gemini-2.5-flash-preview-04-17"
399 ]
400 self.models_updated.emit(models)
401 self.current_model = models[0]
402 self.status_updated.emit("Dostępne modele Gemini.")
403 except Exception as e:
404 self.output_received.emit(f"Błąd pobierania modeli Gemini: {e}", True)
405 self.status_updated.emit("Błąd pobierania modeli Gemini.")
406 elif self.provider == "mistral":
407 if not self.mistral_available:
408 self.models_updated.emit([])
409 self.status_updated.emit("Biblioteka mistralai nie jest zainstalowana!")
410 return
411 try:
412 from mistralai import Mistral
413 client = Mistral(api_key=self.mistral_api_key)
414 models_data = client.models.list()
415 # Poprawka: models_data może być listą Model lub mieć atrybut .data
416 if hasattr(models_data, 'data'):
417 models = [m.id for m in models_data.data]
418 else:
419 models = [m.id for m in models_data]
420 self.models_updated.emit(models)
421 if models:
422 self.current_model = models[0]
423 self.status_updated.emit(f"Pobrano modele Mistral: {', '.join(models)}")
424 else:
425 self.output_received.emit("Brak dostępnych modeli w API Mistral.", True)
426 self.status_updated.emit("Brak modeli Mistral.")
427 except Exception as e:
428 self.output_received.emit(f"Błąd pobierania modeli Mistral: {e}", True)
429 self.status_updated.emit("Błąd pobierania modeli Mistral.")
430
431 def set_model(self, model):
432 self.current_model = model
433 self.status_updated.emit(f"Zmieniono model na: {model}")
434
435 def set_provider(self, provider):
436 self.provider = provider
437 self.base_url = self._get_base_url(self.provider)
438 self.mistral_available = self._check_mistral_import()
439 self._fetch_models()
440
441 def send_message(self, message, file_content=None):
442 if not message.strip():
443 self.output_received.emit("Wiadomość nie może być pusta.", True)
444 return
445 self.conversation_history.append({"role": "user", "content": message})
446 self.output_received.emit(f"Użytkownik: {message}", False)
447 # Uruchom AIWorker w tle
448 self.worker = AIWorker(self, message, file_content)
449 self.worker.result.connect(self._handle_ai_result)
450 self.worker.status.connect(self.status_updated)
451 self.worker.start()
452
453 def _handle_ai_result(self, text, is_error):
454 if not is_error:
455 self.conversation_history.append({"role": "assistant", "content": text})
456 self.output_received.emit(f"AI: {text}", is_error)
457
458 def _send_message_internal(self, prompt):
459 # To jest wywoływane w wątku!
460 try:
461 if self.provider == "grok":
462 headers = {
463 "Authorization": f"Bearer {self.api_key}",
464 "Content-Type": "application/json"
465 }
466 payload = {
467 "model": self.current_model,
468 "messages": self.conversation_history[:-1] + [{"role": "user", "content": prompt}],
469 "max_tokens": 2048,
470 "stream": False
471 }
472 response = requests.post(f"{self.base_url}/chat/completions", headers=headers, json=payload)
473 response.raise_for_status()
474 response_data = response.json()
475 assistant_message = response_data["choices"][0]["message"]["content"]
476 return assistant_message, False
477 elif self.provider == "gemini":
478 headers = {"Content-Type": "application/json"}
479 params = {"key": self.gemini_api_key}
480 payload = {
481 "contents": [
482 {"role": "user", "parts": [{"text": prompt}]}
483 ]
484 }
485 url = f"{self.base_url}/models/{self.current_model}:generateContent"
486 response = requests.post(url, headers=headers, params=params, json=payload)
487 response.raise_for_status()
488 response_data = response.json()
489 try:
490 assistant_message = response_data["candidates"][0]["content"]["parts"][0]["text"]
491 except Exception:
492 assistant_message = str(response_data)
493 return assistant_message, False
494 elif self.provider == "mistral":
495 if not self.mistral_available:
496 return ("Biblioteka mistralai nie jest zainstalowana! Zainstaluj: pip install mistralai", True)
497 from mistralai import Mistral, UserMessage, AssistantMessage
498 client = Mistral(api_key=self.mistral_api_key)
499 messages = [
500 UserMessage(content=msg["content"]) if msg["role"] == "user"
501 else AssistantMessage(content=msg["content"])
502 for msg in self.conversation_history[:-1]
503 ]
504 messages.append(UserMessage(content=prompt))
505 response = client.chat.complete(
506 model=self.current_model,
507 messages=messages,
508 max_tokens=2048
509 )
510 assistant_message = response.choices[0].message.content
511 return assistant_message, False
512 else:
513 return ("Nieobsługiwany provider AI.", True)
514 except Exception as e:
515 return (f"Błąd komunikacji z API: {e}", True)
516
517 def clear_conversation(self):
518 self.conversation_history = []
519 self.output_received.emit("Historia rozmowy wyczyszczona.", False)
520 self.status_updated.emit("Wyczyszczono rozmowę.")
521
522class ConsoleWidget(QWidget):
523 def __init__(self, console_manager, ai_chat_manager, parent=None):
524 super().__init__(parent)
525 self.console_manager = console_manager
526 self.ai_chat_manager = ai_chat_manager
527 self.current_file_path = None # Dodane: ścieżka do otwartego pliku
528 self._setup_ui()
529 self._setup_connections()
530
531 def set_current_file(self, file_path):
532 self.current_file_path = file_path
533
534 def _setup_ui(self):
535 layout = QVBoxLayout(self)
536 layout.setContentsMargins(0, 0, 0, 0)
537 self.tab_widget = QTabWidget()
538 layout.addWidget(self.tab_widget)
539 self.console_tab = QWidget()
540 console_layout = QVBoxLayout(self.console_tab)
541 console_layout.setContentsMargins(0, 0, 0, 0)
542 self.console = QPlainTextEdit()
543 self.console.setReadOnly(True)
544 self.console.setFont(QFont("Courier New", 10))
545 console_layout.addWidget(self.console, 1)
546 self.console_input = QLineEdit()
547 self.console_input.setPlaceholderText("Wpisz polecenie...")
548 console_layout.addWidget(self.console_input, 0)
549 console_buttons_layout = QHBoxLayout()
550 console_buttons_layout.addStretch(1)
551 self.clear_console_button = QPushButton("Wyczyść konsolę")
552 console_buttons_layout.addWidget(self.clear_console_button)
553 self.copy_console_button = QPushButton("Skopiuj")
554 console_buttons_layout.addWidget(self.copy_console_button)
555 console_layout.addLayout(console_buttons_layout)
556 self.tab_widget.addTab(self.console_tab, "Konsola")
557 # Zakładka Chat AI
558 self.ai_tab = QWidget()
559 ai_layout = QVBoxLayout(self.ai_tab)
560 ai_layout.setContentsMargins(0, 0, 0, 0)
561 # ZAMIANA: QTextEdit zamiast QPlainTextEdit
562 self.ai_chat = QTextEdit()
563 self.ai_chat.setReadOnly(True)
564 self.ai_chat.setFont(QFont("Courier New", 10))
565 ai_layout.addWidget(self.ai_chat, 1)
566 ai_input_layout = QHBoxLayout()
567 # Dodaj wybór providera AI
568 self.ai_provider_combo = QComboBox()
569 self.ai_provider_combo.addItems(["grok", "gemini", "mistral"])
570 self.ai_provider_combo.setCurrentText(self.ai_chat_manager.provider)
571 # Blokada wyboru Mistral jeśli nie ma biblioteki
572 try:
573 import mistralai
574 mistral_ok = True
575 except ImportError:
576 mistral_ok = False
577 idx = self.ai_provider_combo.findText("mistral")
578 if idx != -1:
579 self.ai_provider_combo.model().item(idx).setEnabled(mistral_ok)
580 if not mistral_ok and self.ai_provider_combo.currentText() == "mistral":
581 self.ai_provider_combo.setCurrentText("grok")
582 ai_input_layout.addWidget(self.ai_provider_combo)
583 self.ai_model_combo = QComboBox()
584 self.ai_model_combo.setPlaceholderText("Wybierz model...")
585 ai_input_layout.addWidget(self.ai_model_combo)
586 self.ai_input = QLineEdit()
587 self.ai_input.setPlaceholderText("Wpisz wiadomość do AI...")
588 ai_input_layout.addWidget(self.ai_input)
589 self.ai_send_button = QPushButton("Wyślij")
590 ai_input_layout.addWidget(self.ai_send_button)
591 self.ai_clear_button = QPushButton("Wyczyść")
592 ai_input_layout.addWidget(self.ai_clear_button)
593 ai_layout.addLayout(ai_input_layout)
594 self.tab_widget.addTab(self.ai_tab, "Chat AI")
595
596 def _setup_connections(self):
597 self.console_manager.output_received.connect(self._append_console_output)
598 self.console_input.returnPressed.connect(self._run_console_command)
599 self.clear_console_button.clicked.connect(self.console.clear)
600 self.copy_console_button.clicked.connect(self._copy_console)
601 self.ai_chat_manager.output_received.connect(self._append_ai_output)
602 self.ai_chat_manager.models_updated.connect(self._update_model_combo)
603 self.ai_input.returnPressed.connect(self._send_ai_message)
604 self.ai_send_button.clicked.connect(self._send_ai_message)
605 self.ai_clear_button.clicked.connect(self.ai_chat_manager.clear_conversation)
606 self.ai_model_combo.currentTextChanged.connect(self.ai_chat_manager.set_model)
607 # Nowe: zmiana providera AI z poziomu UI
608 self.ai_provider_combo.currentTextChanged.connect(self._on_provider_changed)
609
610 def _on_provider_changed(self, provider):
611 self.ai_chat_manager.set_provider(provider)
612 # Po zmianie providera, pobierz modele tylko dla niego
613 # (AIChatManager sam wywołuje _fetch_models, a sygnał models_updated odświeża model_combo)
614
615 def _append_console_output(self, text, is_error):
616 cursor = self.console.textCursor()
617 cursor.movePosition(cursor.MoveOperation.End)
618 fmt = QTextCharFormat()
619 if is_error:
620 fmt.setForeground(QColor("#DC143C"))
621 cursor.setCharFormat(fmt)
622 text_to_insert = text + ('\n' if text and not text.endswith('\n') else '')
623 cursor.insertText(text_to_insert)
624 self.console.setTextCursor(cursor)
625 self.console.ensureCursorVisible()
626
627 def _append_ai_output(self, text, is_error):
628 # Jeśli to błąd, wyświetl na czerwono bez HTML
629 if is_error:
630 cursor = self.ai_chat.textCursor()
631 cursor.movePosition(cursor.MoveOperation.End)
632 fmt = QTextCharFormat()
633 fmt.setForeground(QColor("#DC143C"))
634 cursor.setCharFormat(fmt)
635 text_to_insert = text + ('\n' if text and not text.endswith('\n') else '')
636 cursor.insertText(text_to_insert)
637 self.ai_chat.setTextCursor(cursor)
638 self.ai_chat.ensureCursorVisible()
639 return
640 # Zamiana Markdown na HTML
641 html = markdown2.markdown(text, extras=["fenced-code-blocks", "tables", "strike", "cuddled-lists", "code-friendly"])
642 # Dodanie stylu dla bloków kodu i przycisku kopiowania
643 html = html.replace('<code>', '<code style="background:#f6f8fa; border-radius:4px; padding:2px 4px;">')
644 html = html.replace('<pre><code', '<div style="position:relative;"><button onclick=\'navigator.clipboard.writeText(this.nextElementSibling.innerText)\' style=\'position:absolute;top:4px;right:4px;z-index:2;font-size:10px;padding:2px 6px;\'>Kopiuj kod</button><pre style="background:#f6f8fa;border-radius:6px;padding:8px 12px;overflow:auto;"><code')
645 html = html.replace('</code></pre>', '</code></pre></div>')
646 self.ai_chat.moveCursor(self.ai_chat.textCursor().End)
647 self.ai_chat.insertHtml(html + "<br>")
648 self.ai_chat.ensureCursorVisible()
649
650 def _copy_console(self):
651 console_text = self.console.toPlainText()
652 if console_text:
653 QApplication.clipboard().setText(console_text)
654
655 def _run_console_command(self):
656 command = self.console_input.text().strip()
657 if command:
658 self.console_input.clear()
659 self.console_manager.run_command(command, os.getcwd())
660
661 def _send_ai_message(self):
662 message = self.ai_input.text().strip()
663 if message:
664 self.ai_input.clear()
665 file_content = None
666 if self.current_file_path:
667 try:
668 with open(self.current_file_path, 'r', encoding='utf-8') as f:
669 file_content = f.read()
670 except Exception:
671 file_content = None
672 self.ai_chat_manager.send_message(message, file_content=file_content)
673
674 def _update_model_combo(self, models):
675 self.ai_model_combo.clear()
676 if models:
677 self.ai_model_combo.addItems(models)
678 if self.ai_chat_manager.current_model in models:
679 self.ai_model_combo.setCurrentText(self.ai_chat_manager.current_model)
680
681Ścieżka: /src/dialogs.py
682Rozmiar: 12,94 KB
683Zawartość:
684# Dialogi dla IDE – tworzenie projektów, plików, zmiana nazw, ustawienia
685# /src/dialogs.py
686
687import os
688import re
689import sys
690sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
691from PyQt6.QtWidgets import (
692 QDialog, QFormLayout, QLineEdit, QDialogButtonBox, QHBoxLayout,
693 QPushButton, QComboBox, QFileDialog, QLabel, QSpinBox, QVBoxLayout,
694 QTableWidget, QTableWidgetItem, QHeaderView, QMessageBox
695)
696from PyQt6.QtCore import Qt
697from PyQt6.QtGui import QFont
698
699class NewProjectDialog(QDialog):
700 def __init__(self, projects_dir, parent=None):
701 super().__init__(parent)
702 self.setWindowTitle("Nowy projekt")
703 self.projects_dir = projects_dir
704 self.setModal(True)
705 layout = QFormLayout(self)
706 self.name_edit = QLineEdit()
707 layout.addRow("Nazwa projektu:", self.name_edit)
708 self.button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
709 self.button_box.button(QDialogButtonBox.StandardButton.Ok).setText("Utwórz")
710 self.button_box.accepted.connect(self.accept)
711 self.button_box.rejected.connect(self.reject)
712 layout.addRow(self.button_box)
713 self.name_edit.textChanged.connect(self._validate_name)
714 self.name_edit.textChanged.emit(self.name_edit.text())
715
716 def _validate_name(self, name):
717 name = name.strip()
718 is_empty = not name
719 is_valid_chars = re.fullmatch(r'[a-zA-Z0-9_-]+', name) is not None or name == ""
720 full_path = os.path.join(self.projects_dir, name)
721 dir_exists = os.path.exists(full_path)
722 enable_ok = not is_empty and is_valid_chars and not dir_exists
723 self.button_box.button(QDialogButtonBox.StandardButton.Ok).setEnabled(enable_ok)
724 if is_empty:
725 self.name_edit.setToolTip("Nazwa projektu nie może być pusta.")
726 elif not is_valid_chars:
727 self.name_edit.setToolTip("Nazwa projektu może zawierać tylko litery, cyfry, podkreślenia i myślniki.")
728 elif dir_exists:
729 self.name_edit.setToolTip(f"Projekt o nazwie '{name}' już istnieje w:\n{self.projects_dir}")
730 else:
731 self.name_edit.setToolTip(f"Katalog projektu zostanie utworzony w:\n{full_path}")
732 if not enable_ok and not is_empty:
733 self.name_edit.setStyleSheet("background-color: #ffe0e0;")
734 else:
735 self.name_edit.setStyleSheet("")
736
737 def get_project_name(self):
738 return self.name_edit.text().strip()
739
740 def get_project_path(self):
741 return os.path.join(self.projects_dir, self.get_project_name())
742
743class NewItemDialog(QDialog):
744 def __init__(self, parent_dir, is_folder=False, parent=None):
745 super().__init__(parent)
746 self.setWindowTitle("Nowy folder" if is_folder else "Nowy plik")
747 self.parent_dir = parent_dir
748 self.is_folder = is_folder
749 self.setModal(True)
750 layout = QFormLayout(self)
751 self.item_type_label = "Nazwa folderu:" if is_folder else "Nazwa pliku:"
752 self.name_edit = QLineEdit()
753 layout.addRow(self.item_type_label, self.name_edit)
754 self.button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
755 self.button_box.accepted.connect(self.accept)
756 self.button_box.rejected.connect(self.reject)
757 layout.addRow(self.button_box)
758 self.name_edit.textChanged.connect(self._validate_name)
759 self.name_edit.textChanged.emit(self.name_edit.text())
760
761 def _validate_name(self, name):
762 name = name.strip()
763 is_empty = not name
764 illegal_chars_pattern = r'[<>:"/\\|?*\x00-\x1F]'
765 is_valid_chars = re.search(illegal_chars_pattern, name) is None
766 full_path = os.path.join(self.parent_dir, name)
767 item_exists = os.path.exists(full_path)
768 enable_create = not is_empty and is_valid_chars and not item_exists
769 self.button_box.button(QDialogButtonBox.StandardButton.Ok).setEnabled(enable_create)
770 if is_empty:
771 self.name_edit.setToolTip(f"{self.item_type_label} nie może być pusta.")
772 elif not is_valid_chars:
773 self.name_edit.setToolTip("Nazwa zawiera niedozwolone znaki.")
774 elif item_exists:
775 self.name_edit.setToolTip(f"Element o nazwie '{name}' już istnieje w:\n{self.parent_dir}")
776 else:
777 self.name_edit.setToolTip("")
778 if not enable_create and not is_empty:
779 self.name_edit.setStyleSheet("background-color: #ffe0e0;")
780 else:
781 self.name_edit.setStyleSheet("")
782
783 def get_item_name(self):
784 return self.name_edit.text().strip()
785
786class RenameItemDialog(QDialog):
787 def __init__(self, current_path, parent=None):
788 super().__init__(parent)
789 self.current_path = current_path
790 self.is_folder = os.path.isdir(current_path)
791 old_name = os.path.basename(current_path)
792 self.setWindowTitle("Zmień nazwę")
793 layout = QFormLayout(self)
794 self.label = QLabel(f"Nowa nazwa dla '{old_name}':")
795 self.line_edit = QLineEdit(old_name)
796 layout.addRow(self.label, self.line_edit)
797 self.button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
798 self.button_box.accepted.connect(self.accept)
799 self.button_box.rejected.connect(self.reject)
800 layout.addRow(self.button_box)
801 self.line_edit.textChanged.connect(self._validate_name)
802 self._validate_name(self.line_edit.text())
803
804 def _validate_name(self, name):
805 name = name.strip()
806 is_empty = not name
807 illegal_chars_pattern = r'[<>:"/\\|?*\x00-\x1F]'
808 is_valid_chars = re.search(illegal_chars_pattern, name) is None
809 old_name = os.path.basename(self.current_path)
810 is_same_name = name == old_name
811 parent_dir = os.path.dirname(self.current_path)
812 new_full_path = os.path.join(parent_dir, name)
813 item_exists_at_new_path = os.path.exists(new_full_path)
814 enable_ok = not is_empty and is_valid_chars and (is_same_name or not item_exists_at_new_path)
815 self.button_box.button(QDialogButtonBox.StandardButton.Ok).setEnabled(enable_ok)
816
817 def get_new_name(self):
818 return self.line_edit.text().strip()
819
820class SettingsDialog(QDialog):
821 def __init__(self, current_settings, parent=None):
822 super().__init__(parent)
823 self.setWindowTitle("Ustawienia IDE")
824 self.current_settings = current_settings.copy()
825 self.setMinimumWidth(400)
826 self.setModal(True)
827 self._setup_ui()
828
829 def _setup_ui(self):
830 layout = QFormLayout(self)
831 layout.setContentsMargins(10, 10, 10, 10)
832 layout.setSpacing(10)
833
834 self.theme_combo = QComboBox()
835 self.theme_combo.addItems(["light", "dark"])
836 self.theme_combo.setCurrentText(self.current_settings.get("theme", "light"))
837 layout.addRow("Motyw:", self.theme_combo)
838
839 self.python_path_input = QLineEdit()
840 self.python_path_input.setText(self.current_settings.get("python_path", ""))
841 self.python_browse_button = QPushButton("Przeglądaj...")
842 self.python_browse_button.clicked.connect(self._browse_python_path)
843 python_layout = QHBoxLayout()
844 python_layout.addWidget(self.python_path_input)
845 python_layout.addWidget(self.python_browse_button)
846 layout.addRow("Ścieżka Python:", python_layout)
847
848 self.node_path_input = QLineEdit()
849 self.node_path_input.setText(self.current_settings.get("node_path", ""))
850 self.node_browse_button = QPushButton("Przeglądaj...")
851 self.node_browse_button.clicked.connect(self._browse_node_path)
852 node_layout = QHBoxLayout()
853 node_layout.addWidget(self.node_path_input)
854 node_layout.addWidget(self.node_browse_button)
855 layout.addRow("Ścieżka Node.js:", node_layout)
856
857 # Nowy wybór dostawcy AI
858 self.ai_provider_combo = QComboBox()
859 self.ai_provider_combo.addItems(["grok", "gemini", "mistral"])
860 self.ai_provider_combo.setCurrentText(self.current_settings.get("ai_provider", "grok"))
861 layout.addRow("Dostawca AI:", self.ai_provider_combo)
862
863 # Klucz API xAI
864 self.api_key_input = QLineEdit()
865 self.api_key_input.setText(self.current_settings.get("api_key", ""))
866 self.api_key_input.setEchoMode(QLineEdit.EchoMode.Password)
867 layout.addRow("Klucz API xAI:", self.api_key_input)
868 # Klucz API Gemini
869 self.gemini_api_key_input = QLineEdit()
870 self.gemini_api_key_input.setText(self.current_settings.get("gemini_api_key", ""))
871 self.gemini_api_key_input.setEchoMode(QLineEdit.EchoMode.Password)
872 layout.addRow("Klucz API Gemini:", self.gemini_api_key_input)
873 # Klucz API Mistral
874 self.mistral_api_key_input = QLineEdit()
875 self.mistral_api_key_input.setText(self.current_settings.get("mistral_api_key", ""))
876 self.mistral_api_key_input.setEchoMode(QLineEdit.EchoMode.Password)
877 layout.addRow("Klucz API Mistral:", self.mistral_api_key_input)
878
879 # Model AI – dynamicznie aktualizowany przez AIChatManager
880 self.ai_model_combo = QComboBox()
881 self.ai_model_combo.addItems([self.current_settings.get("ai_model", "grok-3")])
882 self.ai_model_combo.setCurrentText(self.current_settings.get("ai_model", "grok-3"))
883 layout.addRow("Model AI:", self.ai_model_combo)
884
885 self.font_size_combo = QComboBox()
886 self.font_size_combo.addItems([str(i) for i in range(8, 21)])
887 self.font_size_combo.setCurrentText(str(self.current_settings.get("editor_font_size", 10)))
888 layout.addRow("Rozmiar czcionki edytora:", self.font_size_combo)
889
890 self.button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
891 self.button_box.accepted.connect(self.accept)
892 self.button_box.rejected.connect(self.reject)
893 layout.addRow(self.button_box)
894
895 # Zmiana widoczności kluczy API w zależności od dostawcy
896 self.ai_provider_combo.currentTextChanged.connect(self._update_api_key_visibility)
897 self._update_api_key_visibility(self.ai_provider_combo.currentText())
898 # Blokada wyboru Mistral jeśli nie ma biblioteki
899 self._block_mistral_if_missing()
900
901 def _block_mistral_if_missing(self):
902 try:
903 import mistralai
904 mistral_ok = True
905 except ImportError:
906 mistral_ok = False
907 idx = self.ai_provider_combo.findText("mistral")
908 if idx != -1:
909 self.ai_provider_combo.model().item(idx).setEnabled(mistral_ok)
910 if not mistral_ok and self.ai_provider_combo.currentText() == "mistral":
911 self.ai_provider_combo.setCurrentText("grok")
912
913 def _update_api_key_visibility(self, provider):
914 self.api_key_input.setEnabled(provider == "grok")
915 self.gemini_api_key_input.setEnabled(provider == "gemini")
916 self.mistral_api_key_input.setEnabled(provider == "mistral")
917
918 def _browse_python_path(self):
919 # Wybierz Pythona, jakbyś wybierał psa na spacer 🐶
920 file_path, _ = QFileDialog.getOpenFileName(
921 self, "Wybierz plik wykonywalny Pythona", "",
922 "Pliki wykonywalne (*.exe);;Wszystkie pliki (*)"
923 )
924 if file_path:
925 self.python_path_input.setText(file_path)
926
927 def _browse_node_path(self):
928 # Node.js – dla fanów async chaosu 😜
929 file_path, _ = QFileDialog.getOpenFileName(
930 self, "Wybierz plik wykonywalny Node.js", "",
931 "Pliki wykonywalne (*.exe);;Wszystkie pliki (*)"
932 )
933 if file_path:
934 self.node_path_input.setText(file_path)
935
936 def get_settings(self):
937 # Zwraca ustawienia jak pizzę – wszystko, czego chciałeś 🍕
938 api_key = self.api_key_input.text()
939 gemini_api_key = self.gemini_api_key_input.text()
940 mistral_api_key = self.mistral_api_key_input.text()
941 # Jeśli przez przypadek pole zostało nadpisane funkcją, wymuś string
942 if not isinstance(api_key, str):
943 api_key = str(api_key)
944 if not isinstance(gemini_api_key, str):
945 gemini_api_key = str(gemini_api_key)
946 if not isinstance(mistral_api_key, str):
947 mistral_api_key = str(mistral_api_key)
948 return {
949 "theme": self.theme_combo.currentText(),
950 "python_path": self.python_path_input.text().strip(),
951 "node_path": self.node_path_input.text().strip(),
952 "ai_provider": self.ai_provider_combo.currentText(),
953 "api_key": api_key.strip(),
954 "gemini_api_key": gemini_api_key.strip(),
955 "mistral_api_key": mistral_api_key.strip(),
956 "ai_model": self.ai_model_combo.currentText(),
957 "editor_font_size": int(self.font_size_combo.currentText())
958 }
959
960Ścieżka: /src/filesystem.py
961Rozmiar: 2,23 KB
962Zawartość:
963# Model systemu plików
964#/src/filesystem.py
965
966import os
967from PyQt6.QtGui import QFileSystemModel
968from PyQt6.QtCore import Qt
969try:
970 import qtawesome as qta
971except ImportError:
972 qta = None
973
974class CustomFileSystemModel(QFileSystemModel):
975 def __init__(self, parent=None):
976 super().__init__(parent)
977 self.icon_map = {
978 '.py': 'fa5s.file-code',
979 '.js': 'fa5s.file-code',
980 '.json': 'fa5s.file-code',
981 '.html': 'fa5s.file-code',
982 '.css': 'fa5s.file-code',
983 '.ini': 'fa5s.file-alt',
984 '.txt': 'fa5s.file-alt',
985 '.md': 'fa5s.file-alt',
986 '.c': 'fa5s.file-code',
987 '.cpp': 'fa5s.file-code',
988 '.h': 'fa5s.file-code',
989 '.hpp': 'fa5s.file-code',
990 }
991 self.folder_icon_name = 'fa5s.folder'
992 self.default_file_icon_name = 'fa5s.file'
993 self._has_qtawesome = qta is not None
994
995 def rename(self, index, new_name):
996 if not index.isValid():
997 return False
998 old_path = self.filePath(index)
999 new_path = os.path.join(os.path.dirname(old_path), new_name)
1000 try:
1001 os.rename(old_path, new_path)
1002 self.refresh()
1003 return True
1004 except Exception as e:
1005 print(f"Błąd zmiany nazwy: {e}")
1006 return False
1007
1008 def data(self, index, role=Qt.ItemDataRole.DisplayRole):
1009 if not index.isValid():
1010 return None
1011 if role == Qt.ItemDataRole.DecorationRole:
1012 file_info = self.fileInfo(index)
1013 if file_info.isDir():
1014 return qta.icon(self.folder_icon_name) if self._has_qtawesome else super().data(index, role)
1015 elif file_info.isFile():
1016 extension = file_info.suffix().lower()
1017 dotted_extension = '.' + extension
1018 if dotted_extension in self.icon_map and self._has_qtawesome:
1019 return qta.icon(self.icon_map[dotted_extension])
1020 return qta.icon(self.default_file_icon_name) if self._has_qtawesome else super().data(index, role)
1021 return super().data(index, role)
1022
1023 def refresh(self, *args):
1024 self.setRootPath(self.rootPath())
1025
1026Ścieżka: /src/highlighter.py
1027Rozmiar: 3,31 KB
1028Zawartość:
1029# Kolorowanie składni dla edytora
1030#/src/highlighter.py
1031
1032import re
1033from PyQt6.QtGui import QSyntaxHighlighter, QTextDocument
1034from src.config import FORMAT_DEFAULT, FORMAT_COMMENT, HIGHLIGHTING_RULES
1035
1036class CodeSyntaxHighlighter(QSyntaxHighlighter):
1037 def __init__(self, parent: QTextDocument, language: str):
1038 super().__init__(parent)
1039 self._language = language.lower()
1040 self._rules = []
1041 lang_config = HIGHLIGHTING_RULES.get(self._language, {})
1042 keywords = lang_config.get('keywords', [])
1043 builtins = lang_config.get('builtins', [])
1044 patterns = lang_config.get('patterns', [])
1045 for keyword in keywords:
1046 pattern = r'\b' + re.escape(keyword) + r'\b'
1047 self._rules.append((re.compile(pattern), lang_config.get('keyword_format', FORMAT_DEFAULT)))
1048 for builtin in builtins:
1049 pattern = r'\b' + re.escape(builtin) + r'\b'
1050 self._rules.append((re.compile(pattern), lang_config.get('builtin_format', FORMAT_DEFAULT)))
1051 for pattern_str, format, *flags in patterns:
1052 try:
1053 pattern = re.compile(pattern_str, *flags)
1054 self._rules.append((pattern, format))
1055 except re.error as e:
1056 print(f"Błąd regex '{pattern_str}' dla {self._language}: {e}")
1057
1058 def highlightBlock(self, text: str):
1059 self.setFormat(0, len(text), FORMAT_DEFAULT)
1060 self.setCurrentBlockState(0)
1061 block_comment_delimiters = []
1062 if self._language in ['javascript', 'css', 'c++']:
1063 block_comment_delimiters.append(("/*", "*/", FORMAT_COMMENT))
1064 comment_start_in_prev_block = (self.previousBlockState() == 1)
1065 if comment_start_in_prev_block:
1066 end_delimiter_index = text.find("*/")
1067 if end_delimiter_index >= 0:
1068 self.setFormat(0, end_delimiter_index + 2, FORMAT_COMMENT)
1069 self.setCurrentBlockState(0)
1070 start_pos = end_delimiter_index + 2
1071 else:
1072 self.setFormat(0, len(text), FORMAT_COMMENT)
1073 self.setCurrentBlockState(1)
1074 return
1075 else:
1076 start_pos = 0
1077 start_delimiter = "/*"
1078 end_delimiter = "*/"
1079 startIndex = text.find(start_delimiter, start_pos)
1080 while startIndex >= 0:
1081 endIndex = text.find(end_delimiter, startIndex)
1082 if endIndex >= 0:
1083 length = endIndex - startIndex + len(end_delimiter)
1084 self.setFormat(startIndex, startIndex + length, FORMAT_COMMENT)
1085 startIndex = text.find(start_delimiter, startIndex + length)
1086 else:
1087 self.setFormat(startIndex, len(text) - startIndex, FORMAT_COMMENT)
1088 self.setCurrentBlockState(1)
1089 break
1090 for pattern, format in self._rules:
1091 if format == FORMAT_COMMENT and (pattern.pattern.startswith(re.escape('/*')) or pattern.pattern.startswith(re.escape('<!--'))):
1092 continue
1093 if format == FORMAT_COMMENT and pattern.pattern.startswith('//') and self.currentBlockState() == 1:
1094 continue
1095 for match in pattern.finditer(text):
1096 start, end = match.span()
1097 self.setFormat(start, end, format)
1098
1099
1100
1101Ścieżka: /src/models.py
1102Rozmiar: 1,48 KB
1103Zawartość:
1104# Modele danych aplikacji
1105#/src/models.py
1106
1107import os
1108import json
1109from src.config import SETTINGS_FILE, RECENTS_FILE
1110
1111class AppState:
1112 def __init__(self):
1113 self.settings = {
1114 "theme": "light",
1115 "python_path": "",
1116 "node_path": "",
1117 "show_tree": True,
1118 "show_console": True,
1119 "editor_font_size": 10
1120 }
1121 self.recents = {"last_project_dir": None, "open_files": []}
1122
1123 def load(self):
1124 try:
1125 if os.path.exists(SETTINGS_FILE):
1126 with open(SETTINGS_FILE, 'r', encoding='utf-8') as f:
1127 self.settings.update(json.load(f))
1128 if os.path.exists(RECENTS_FILE):
1129 with open(RECENTS_FILE, 'r', encoding='utf-8') as f:
1130 self.recents.update(json.load(f))
1131 except Exception as e:
1132 print(f"Błąd wczytywania stanu: {e}")
1133
1134 def save(self, open_files, project_dir):
1135 try:
1136 self.recents["open_files"] = list(open_files)
1137 if project_dir and os.path.isdir(project_dir):
1138 self.recents["last_project_dir"] = os.path.normpath(project_dir)
1139 with open(SETTINGS_FILE, 'w', encoding='utf-8') as f:
1140 json.dump(self.settings, f, indent=4)
1141 with open(RECENTS_FILE, 'w', encoding='utf-8') as f:
1142 json.dump(self.recents, f, indent=4)
1143 except Exception as e:
1144 print(f"Błąd zapisu stanu: {e}")
1145
1146Ścieżka: /src/package_manager.py
1147Rozmiar: 41,10 KB
1148Zawartość:
1149import os
1150import re
1151import requests
1152import zipfile
1153import tarfile
1154import json
1155import shutil
1156import subprocess
1157import py7zr
1158from PyQt6.QtWidgets import QDialog, QVBoxLayout, QLabel, QProgressBar, QTableWidget, QTableWidgetItem, QHeaderView, QPushButton, QApplication, QHBoxLayout, QWidget
1159from PyQt6.QtCore import QThread, pyqtSignal, Qt
1160from PyQt6.QtGui import QColor, QIcon
1161from src.theme import get_dark_package_manager_stylesheet, get_light_package_manager_stylesheet
1162
1163# Ustaw katalog do przechowywania pakietów, tworząc go, jeśli nie istnieje
1164PACKAGES_DIR = os.path.abspath("packages")
1165os.makedirs(PACKAGES_DIR, exist_ok=True)
1166
1167class DownloadWorker(QThread):
1168 """Klasa wątku do obsługi pobierania i instalacji pakietów w tle."""
1169 progress = pyqtSignal(int) # Sygnał wysyłający postęp operacji (0-100)
1170 finished = pyqtSignal(str) # Sygnał wysyłający komunikat o pomyślnym zakończeniu
1171 error = pyqtSignal(str) # Sygnał wysyłający komunikat o błędzie
1172
1173 def __init__(self, func, package_name):
1174 """
1175 Inicjalizuje wątek.
1176 :param func: Funkcja do wykonania w wątku (np. instalacja).
1177 :param package_name: Nazwa pakietu do wyświetlania w komunikatach.
1178 """
1179 super().__init__()
1180 self.func = func
1181 self.package_name = package_name
1182
1183 def run(self):
1184 """Główna pętla wątku, wykonująca przekazaną funkcję."""
1185 try:
1186 self.func(progress_callback=self.progress.emit)
1187 self.finished.emit(f"Operacja dla {self.package_name} zakończona pomyślnie.")
1188 except Exception as e:
1189 self.error.emit(f"Błąd podczas operacji dla {self.package_name}: {str(e)}")
1190
1191class PackageManager:
1192 """Klasa zarządzająca pakietami (pobieranie, instalacja, odinstalowanie)."""
1193 def __init__(self, parent=None):
1194 """
1195 Inicjalizuje menadżera pakietów.
1196 :param parent: Obiekt nadrzędny (opcjonalne, dla kontekstu ścieżki).
1197 """
1198 self.parent = parent
1199 base_dir = os.path.dirname(__file__) if '__file__' in locals() else os.getcwd()
1200 self.settings_path = os.path.abspath(os.path.join(base_dir, "..", "userdata", "settings.json"))
1201 os.makedirs(os.path.dirname(self.settings_path), exist_ok=True)
1202
1203 def _download_file(self, url, dest_path, progress_callback=None):
1204 """
1205 Pobiera plik z podanego URL do wskazanej ścieżki.
1206 :param url: Adres URL pliku.
1207 :param dest_path: Ścieżka docelowa zapisu pliku.
1208 :param progress_callback: Funkcja callback do raportowania postępu.
1209 """
1210 try:
1211 response = requests.get(url, stream=True, timeout=15)
1212 response.raise_for_status()
1213 total = int(response.headers.get('content-length', 0))
1214 downloaded = 0
1215 with open(dest_path, "wb") as f:
1216 for chunk in response.iter_content(chunk_size=8192):
1217 if chunk:
1218 f.write(chunk)
1219 downloaded += len(chunk)
1220 if total > 0 and progress_callback:
1221 percent = int(downloaded * 100 / total)
1222 progress_callback(percent)
1223 if progress_callback:
1224 progress_callback(100)
1225 except requests.exceptions.Timeout:
1226 raise RuntimeError(f"Upłynął czas oczekiwania na odpowiedź serwera podczas pobierania z {url}")
1227 except requests.RequestException as e:
1228 raise RuntimeError(f"Błąd podczas pobierania pliku z {url}: {str(e)}")
1229
1230 def _extract_archive(self, file_path, extract_to):
1231 """
1232 Rozpakowuje archiwum (ZIP, TAR.GZ lub 7Z) do wskazanego katalogu.
1233 :param file_path: Ścieżka do pliku archiwum.
1234 :param extract_to: Ścieżka do katalogu docelowego rozpakowania.
1235 """
1236 os.makedirs(extract_to, exist_ok=True)
1237 try:
1238 if file_path.lower().endswith(".zip"):
1239 with zipfile.ZipFile(file_path, 'r') as zip_ref:
1240 for member in zip_ref.namelist():
1241 fname = member.split('/', 1)[1] if '/' in member else member
1242 if fname:
1243 target_path = os.path.join(extract_to, fname)
1244 if member.endswith('/'):
1245 os.makedirs(target_path, exist_ok=True)
1246 else:
1247 os.makedirs(os.path.dirname(target_path), exist_ok=True)
1248 with open(target_path, 'wb') as f:
1249 f.write(zip_ref.read(member))
1250 elif file_path.lower().endswith((".tar.gz", ".tgz")):
1251 with tarfile.open(file_path, 'r:gz') as tar_ref:
1252 tar_ref.extractall(extract_to)
1253 elif file_path.lower().endswith(".7z"):
1254 with py7zr.SevenZipFile(file_path, mode='r') as z:
1255 z.extractall(extract_to)
1256 else:
1257 raise ValueError(f"Nieobsługiwany format archiwum: {file_path}")
1258 except (zipfile.BadZipFile, tarfile.TarError, py7zr.Py7zrError) as e:
1259 raise RuntimeError(f"Błąd podczas rozpakowywania archiwum {os.path.basename(file_path)}: {str(e)}")
1260 except Exception as e:
1261 raise RuntimeError(f"Nieoczekiwany błąd podczas rozpakowywania: {str(e)}")
1262
1263 def _get_local_version(self, package):
1264 """
1265 Pobiera lokalnie zainstalowaną wersję pakietu.
1266 :param package: Nazwa pakietu.
1267 :return: Wersja lub None.
1268 """
1269 exe_path = self._get_setting(f"{package.lower()}_path")
1270 if not exe_path or not os.path.exists(exe_path):
1271 return None
1272 try:
1273 if package == "Python":
1274 result = subprocess.run([exe_path, "--version"], capture_output=True, text=True, check=True, encoding='utf-8')
1275 return result.stdout.strip().split()[-1]
1276 elif package == "Node.js":
1277 result = subprocess.run([exe_path, "--version"], capture_output=True, text=True, check=True, encoding='utf-8')
1278 return result.stdout.strip().lstrip('v')
1279 elif package == "Go":
1280 result = subprocess.run([exe_path, "version"], capture_output=True, text=True, check=True, encoding='utf-8')
1281 return result.stdout.strip().split()[2].lstrip('go')
1282 elif package == "Java":
1283 result = subprocess.run([exe_path, "-version"], capture_output=True, text=True, check=True, encoding='utf-8')
1284 return result.stdout.strip().split()[2].strip('"')
1285 elif package == "Ruby":
1286 result = subprocess.run([exe_path, "--version"], capture_output=True, text=True, check=True, encoding='utf-8')
1287 return result.stdout.strip().split()[1]
1288 elif package == "Rust":
1289 result = subprocess.run([exe_path, "--version"], capture_output=True, text=True, check=True, encoding='utf-8')
1290 return result.stdout.strip().split()[1]
1291 elif package == "Git":
1292 result = subprocess.run([exe_path, "--version"], capture_output=True, text=True, check=True, encoding='utf-8')
1293 return result.stdout.strip().split()[-1]
1294 elif package == "Docker":
1295 result = subprocess.run([exe_path, "--version"], capture_output=True, text=True, check=True, encoding='utf-8')
1296 return result.stdout.strip().split()[2].rstrip(',')
1297 except (subprocess.CalledProcessError, FileNotFoundError, OSError):
1298 return None
1299 return None
1300
1301 def _get_latest_version(self, package):
1302 """
1303 Pobiera najnowszą wersję pakietu ze źródeł zewnętrznych.
1304 :param package: Nazwa pakietu.
1305 :return: Wersja lub None.
1306 """
1307 try:
1308 if package == "Python":
1309 url = "https://www.python.org/ftp/python/index-windows.json"
1310 response = requests.get(url, timeout=10)
1311 response.raise_for_status()
1312 data = response.json()
1313 versions = [v for v in data["versions"] if v.get("url", "").endswith(".zip") and "64" in v.get("id", "") and v.get("sort-version", "").replace(".", "").isdigit()]
1314 if not versions:
1315 return None
1316 return sorted(versions, key=lambda v: list(map(int, v["sort-version"].split("."))), reverse=True)[0]["sort-version"]
1317 elif package == "Node.js":
1318 shasums_url = "https://nodejs.org/dist/latest/SHASUMS256.txt"
1319 response = requests.get(shasums_url, timeout=10)
1320 response.raise_for_status()
1321 pattern = r"^[a-f0-9]{64}\s+(node-v([\d.]+)-win-x64\.zip)$"
1322 for line in response.text.splitlines():
1323 match = re.match(pattern, line, re.MULTILINE)
1324 if match:
1325 return match.group(2)
1326 return None
1327 elif package == "Go":
1328 url = "https://go.dev/dl/"
1329 response = requests.get(url, timeout=10)
1330 response.raise_for_status()
1331 pattern = r"go(\d+\.\d+\.\d+)\.windows-amd64\.zip"
1332 match = re.search(pattern, response.text)
1333 return match.group(1) if match else None
1334 elif package == "Java":
1335 url = "https://jdk.java.net/21/"
1336 response = requests.get(url, timeout=10)
1337 response.raise_for_status()
1338 pattern = r"openjdk-(\d+)_windows-x64_bin\.zip"
1339 match = re.search(pattern, response.text)
1340 return match.group(1) if match else None
1341 elif package == "Ruby":
1342 url = "https://rubyinstaller.org/downloads/"
1343 response = requests.get(url, timeout=10)
1344 response.raise_for_status()
1345 pattern = r"rubyinstaller-(\d+\.\d+\.\d+-\d+)-x64\.7z"
1346 match = re.search(pattern, response.text)
1347 return match.group(1) if match else None
1348 elif package == "Rust":
1349 url = "https://static.rust-lang.org/dist/channel-rust-stable.toml"
1350 response = requests.get(url, timeout=10)
1351 response.raise_for_status()
1352 pattern = r"version = \"(\d+\.\d+\.\d+)\""
1353 match = re.search(pattern, response.text)
1354 return match.group(1) if match else None
1355 elif package == "Git":
1356 url = "https://git-scm.com/download/win"
1357 response = requests.get(url, timeout=10)
1358 response.raise_for_status()
1359 pattern = r"Git-(\d+\.\d+\.\d+)-64-bit\.exe"
1360 match = re.search(pattern, response.text)
1361 return match.group(1) if match else None
1362 elif package == "Docker":
1363 url = "https://desktop.docker.com/win/stable/amd64/Docker%20Desktop%20Installer.exe"
1364 response = requests.head(url, timeout=10)
1365 response.raise_for_status()
1366 return "latest" # Docker Desktop nie publikuje wersji wprost
1367 except requests.RequestException:
1368 return None
1369 return None
1370
1371 def _is_installed(self, package):
1372 """
1373 Sprawdza, czy pakiet jest zainstalowany.
1374 :param package: Nazwa pakietu.
1375 :return: True, jeśli zainstalowany, inaczej False.
1376 """
1377 if package == "Python":
1378 return os.path.exists(os.path.join(PACKAGES_DIR, "python", "python.exe"))
1379 elif package == "Node.js":
1380 return os.path.exists(os.path.join(PACKAGES_DIR, "node.js", "node.exe"))
1381 elif package == "Go":
1382 return os.path.exists(os.path.join(PACKAGES_DIR, "go", "bin", "go.exe"))
1383 elif package == "Java":
1384 return os.path.exists(os.path.join(PACKAGES_DIR, "java", "bin", "java.exe"))
1385 elif package == "Ruby":
1386 return os.path.exists(os.path.join(PACKAGES_DIR, "ruby", "bin", "ruby.exe"))
1387 elif package == "Rust":
1388 return os.path.exists(os.path.join(PACKAGES_DIR, "rust", "bin", "cargo.exe"))
1389 elif package == "Git":
1390 return os.path.exists(os.path.join(PACKAGES_DIR, "git", "bin", "git.exe"))
1391 elif package == "Docker":
1392 return os.path.exists(os.path.join(PACKAGES_DIR, "docker", "Docker", "Docker Desktop.exe"))
1393 return False
1394
1395 def install_latest_python(self, progress_callback=None):
1396 """
1397 Instaluje najnowszą wersję Pythona.
1398 :param progress_callback: Funkcja callback dla postępu.
1399 """
1400 try:
1401 url_index = "https://www.python.org/ftp/python/index-windows.json"
1402 response = requests.get(url_index, timeout=10)
1403 response.raise_for_status()
1404 data = response.json()
1405 versions = [v for v in data["versions"] if v.get("url", "").endswith(".zip") and "64" in v.get("id", "") and v.get("sort-version", "").replace(".", "").isdigit()]
1406 if not versions:
1407 raise RuntimeError("Nie znaleziono stabilnej wersji 64-bitowej Pythona z plikiem ZIP.")
1408 latest = sorted(versions, key=lambda v: list(map(int, v["sort-version"].split("."))), reverse=True)[0]
1409 download_url = latest["url"]
1410 version = latest["sort-version"]
1411 filename = f"python-{version}-amd64.zip"
1412 python_dir = os.path.join(PACKAGES_DIR, "python")
1413 zip_path = os.path.join(PACKAGES_DIR, filename)
1414 if os.path.exists(python_dir):
1415 shutil.rmtree(python_dir)
1416 os.makedirs(python_dir, exist_ok=True)
1417 if progress_callback: progress_callback(1)
1418 self._download_file(download_url, zip_path, progress_callback)
1419 if progress_callback: progress_callback(95)
1420 self._extract_archive(zip_path, python_dir)
1421 if progress_callback: progress_callback(98)
1422 os.remove(zip_path)
1423 python_exe_path = os.path.join(python_dir, "python.exe")
1424 if not os.path.exists(python_exe_path):
1425 raise RuntimeError("Nie znaleziono pliku python.exe po rozpakowaniu.")
1426 self._update_settings("python_path", python_exe_path)
1427 self._update_settings("python_version", version)
1428 if progress_callback: progress_callback(100)
1429 except Exception as e:
1430 raise RuntimeError(f"Instalacja pakietu Python nieudana: {str(e)}")
1431
1432 def install_latest_nodejs(self, progress_callback=None):
1433 """
1434 Instaluje najnowszą wersję Node.js.
1435 :param progress_callback: Funkcja callback dla postępu.
1436 """
1437 try:
1438 shasums_url = "https://nodejs.org/dist/latest/SHASUMS256.txt"
1439 response = requests.get(shasums_url, timeout=10)
1440 response.raise_for_status()
1441 pattern = r"^[a-f0-9]{64}\s+(node-v([\d.]+)-win-x64\.zip)$"
1442 filename = None
1443 version = None
1444 for line in response.text.splitlines():
1445 match = re.match(pattern, line, re.MULTILINE)
1446 if match:
1447 filename = match.group(1)
1448 version = match.group(2)
1449 break
1450 if not filename or not version:
1451 raise RuntimeError("Nie znaleziono archiwum Node.js dla Windows x64 w pliku SHASUMS256.txt.")
1452 base_url = "https://nodejs.org/dist/latest/"
1453 download_url = f"{base_url}{filename}"
1454 zip_path = os.path.join(PACKAGES_DIR, filename)
1455 node_dir = os.path.join(PACKAGES_DIR, "node.js")
1456 if os.path.exists(node_dir):
1457 shutil.rmtree(node_dir)
1458 os.makedirs(node_dir, exist_ok=True)
1459 if progress_callback: progress_callback(1)
1460 self._download_file(download_url, zip_path, progress_callback)
1461 if progress_callback: progress_callback(95)
1462 self._extract_archive(zip_path, node_dir)
1463 if progress_callback: progress_callback(98)
1464 os.remove(zip_path)
1465 node_exe_path = os.path.join(node_dir, "node.exe")
1466 if not os.path.exists(node_exe_path):
1467 raise RuntimeError("Nie znaleziono pliku node.exe po rozpakowaniu.")
1468 self._update_settings("node_path", node_exe_path)
1469 self._update_settings("node_version", version)
1470 if progress_callback: progress_callback(100)
1471 except Exception as e:
1472 raise RuntimeError(f"Instalacja pakietu Node.js nieudana: {str(e)}")
1473
1474 def install_latest_go(self, progress_callback=None):
1475 """
1476 Instaluje najnowszą wersję Go.
1477 :param progress_callback: Funkcja callback dla postępu.
1478 """
1479 try:
1480 url = "https://go.dev/dl/"
1481 response = requests.get(url, timeout=10)
1482 response.raise_for_status()
1483 pattern = r"go(\d+\.\d+\.\d+)\.windows-amd64\.zip"
1484 match = re.search(pattern, response.text)
1485 if not match:
1486 raise RuntimeError("Nie znaleziono wersji Go dla Windows x64")
1487 version = match.group(1)
1488 filename = f"go{version}.windows-amd64.zip"
1489 download_url = f"{url}{filename}"
1490 zip_path = os.path.join(PACKAGES_DIR, filename)
1491 go_dir = os.path.join(PACKAGES_DIR, "go")
1492 if os.path.exists(go_dir):
1493 shutil.rmtree(go_dir)
1494 os.makedirs(go_dir, exist_ok=True)
1495 if progress_callback: progress_callback(1)
1496 self._download_file(download_url, zip_path, progress_callback)
1497 if progress_callback: progress_callback(95)
1498 self._extract_archive(zip_path, go_dir)
1499 if progress_callback: progress_callback(98)
1500 os.remove(zip_path)
1501 go_exe_path = os.path.join(go_dir, "bin", "go.exe")
1502 if not os.path.exists(go_exe_path):
1503 raise RuntimeError("Nie znaleziono pliku go.exe po rozpakowaniu.")
1504 self._update_settings("go_path", go_exe_path)
1505 self._update_settings("go_version", version)
1506 if progress_callback: progress_callback(100)
1507 except Exception as e:
1508 raise RuntimeError(f"Instalacja pakietu Go nieudana: {str(e)}")
1509
1510 def install_latest_java(self, progress_callback=None):
1511 """
1512 Instaluje najnowszą wersję OpenJDK.
1513 :param progress_callback: Funkcja callback dla postępu.
1514 """
1515 try:
1516 url = "https://jdk.java.net/21/"
1517 response = requests.get(url, timeout=10)
1518 response.raise_for_status()
1519 pattern = r"openjdk-(\d+)_windows-x64_bin\.zip"
1520 match = re.search(pattern, response.text)
1521 if not match:
1522 raise RuntimeError("Nie znaleziono OpenJDK dla Windows x64")
1523 version = match.group(1)
1524 filename = f"openjdk-{version}_windows-x64_bin.zip"
1525 download_url = f"https://download.java.net/java/GA/jdk{version}/{filename}"
1526 zip_path = os.path.join(PACKAGES_DIR, filename)
1527 java_dir = os.path.join(PACKAGES_DIR, "java")
1528 if os.path.exists(java_dir):
1529 shutil.rmtree(java_dir)
1530 os.makedirs(java_dir, exist_ok=True)
1531 if progress_callback: progress_callback(1)
1532 self._download_file(download_url, zip_path, progress_callback)
1533 if progress_callback: progress_callback(95)
1534 self._extract_archive(zip_path, java_dir)
1535 if progress_callback: progress_callback(98)
1536 os.remove(zip_path)
1537 java_exe_path = os.path.join(java_dir, "bin", "java.exe")
1538 if not os.path.exists(java_exe_path):
1539 raise RuntimeError("Nie znaleziono pliku java.exe po rozpakowaniu.")
1540 self._update_settings("java_path", java_exe_path)
1541 self._update_settings("java_version", version)
1542 if progress_callback: progress_callback(100)
1543 except Exception as e:
1544 raise RuntimeError(f"Instalacja pakietu Java nieudana: {str(e)}")
1545
1546 def install_latest_ruby(self, progress_callback=None):
1547 """
1548 Instaluje najnowszą wersję Ruby.
1549 :param progress_callback: Funkcja callback dla postępu.
1550 """
1551 try:
1552 url = "https://rubyinstaller.org/downloads/"
1553 response = requests.get(url, timeout=10)
1554 response.raise_for_status()
1555 pattern = r"rubyinstaller-(\d+\.\d+\.\d+-\d+)-x64\.7z"
1556 match = re.search(pattern, response.text)
1557 if not match:
1558 raise RuntimeError("Nie znaleziono Ruby dla Windows x64")
1559 version = match.group(1)
1560 filename = f"rubyinstaller-{version}-x64.7z"
1561 download_url = f"https://github.com/oneclick/rubyinstaller2/releases/download/RubyInstaller-{version}/{filename}"
1562 archive_path = os.path.join(PACKAGES_DIR, filename)
1563 ruby_dir = os.path.join(PACKAGES_DIR, "ruby")
1564 if os.path.exists(ruby_dir):
1565 shutil.rmtree(ruby_dir)
1566 os.makedirs(ruby_dir, exist_ok=True)
1567 if progress_callback: progress_callback(1)
1568 self._download_file(download_url, archive_path, progress_callback)
1569 if progress_callback: progress_callback(95)
1570 self._extract_archive(archive_path, ruby_dir)
1571 if progress_callback: progress_callback(98)
1572 os.remove(archive_path)
1573 ruby_exe_path = os.path.join(ruby_dir, "bin", "ruby.exe")
1574 if not os.path.exists(ruby_exe_path):
1575 raise RuntimeError("Nie znaleziono pliku ruby.exe po rozpakowaniu.")
1576 self._update_settings("ruby_path", ruby_exe_path)
1577 self._update_settings("ruby_version", version)
1578 if progress_callback: progress_callback(100)
1579 except Exception as e:
1580 raise RuntimeError(f"Instalacja pakietu Ruby nieudana: {str(e)}")
1581
1582 def install_latest_rust(self, progress_callback=None):
1583 """
1584 Instaluje najnowszą wersję Rust.
1585 :param progress_callback: Funkcja callback dla postępu.
1586 """
1587 try:
1588 url = "https://static.rust-lang.org/rustup/dist/x86_64-pc-windows-msvc/rustup-init.exe"
1589 exe_path = os.path.join(PACKAGES_DIR, "rustup-init.exe")
1590 rust_dir = os.path.join(PACKAGES_DIR, "rust")
1591 os.makedirs(rust_dir, exist_ok=True)
1592 if progress_callback: progress_callback(1)
1593 self._download_file(url, exe_path, progress_callback)
1594 if progress_callback: progress_callback(95)
1595 subprocess.run([exe_path, "--default-toolchain", "stable", "--profile", "minimal", "-y", f"--target-dir={rust_dir}"], check=True)
1596 if progress_callback: progress_callback(98)
1597 os.remove(exe_path)
1598 cargo_exe_path = os.path.join(rust_dir, "bin", "cargo.exe")
1599 if not os.path.exists(cargo_exe_path):
1600 raise RuntimeError("Nie znaleziono pliku cargo.exe po instalacji.")
1601 version = subprocess.run([cargo_exe_path, "--version"], capture_output=True, text=True).stdout.strip().split()[1]
1602 self._update_settings("rust_path", cargo_exe_path)
1603 self._update_settings("rust_version", version)
1604 if progress_callback: progress_callback(100)
1605 except Exception as e:
1606 raise RuntimeError(f"Instalacja pakietu Rust nieudana: {str(e)}")
1607
1608 def install_latest_git(self, progress_callback=None):
1609 """
1610 Instaluje najnowszą wersję Git.
1611 :param progress_callback: Funkcja callback dla postępu.
1612 """
1613 try:
1614 url = "https://git-scm.com/download/win"
1615 response = requests.get(url, timeout=10)
1616 response.raise_for_status()
1617 pattern = r"Git-(\d+\.\d+\.\d+)-64-bit\.exe"
1618 match = re.search(pattern, response.text)
1619 if not match:
1620 raise RuntimeError("Nie znaleziono Git dla Windows x64")
1621 version = match.group(1)
1622 filename = f"Git-{version}-64-bit.exe"
1623 download_url = f"https://github.com/git-for-windows/git/releases/download/v{version}.windows.1/{filename}"
1624 exe_path = os.path.join(PACKAGES_DIR, filename)
1625 git_dir = os.path.join(PACKAGES_DIR, "git")
1626 os.makedirs(git_dir, exist_ok=True)
1627 if progress_callback: progress_callback(1)
1628 self._download_file(download_url, exe_path, progress_callback)
1629 if progress_callback: progress_callback(95)
1630 subprocess.run([exe_path, "/VERYSILENT", f"/DIR={git_dir}"], check=True)
1631 if progress_callback: progress_callback(98)
1632 os.remove(exe_path)
1633 git_exe_path = os.path.join(git_dir, "bin", "git.exe")
1634 if not os.path.exists(git_exe_path):
1635 raise RuntimeError("Nie znaleziono pliku git.exe po instalacji.")
1636 self._update_settings("git_path", git_exe_path)
1637 self._update_settings("git_version", version)
1638 if progress_callback: progress_callback(100)
1639 except Exception as e:
1640 raise RuntimeError(f"Instalacja pakietu Git nieudana: {str(e)}")
1641
1642 def install_latest_docker(self, progress_callback=None):
1643 """
1644 Instaluje najnowszą wersję Docker Desktop.
1645 :param progress_callback: Funkcja callback dla postępu.
1646 """
1647 try:
1648 url = "https://desktop.docker.com/win/stable/amd64/Docker%20Desktop%20Installer.exe"
1649 exe_path = os.path.join(PACKAGES_DIR, "DockerDesktopInstaller.exe")
1650 docker_dir = os.path.join(PACKAGES_DIR, "docker")
1651 os.makedirs(docker_dir, exist_ok=True)
1652 if progress_callback: progress_callback(1)
1653 self._download_file(url, exe_path, progress_callback)
1654 if progress_callback: progress_callback(95)
1655 subprocess.run([exe_path, "install", "--quiet", f"--install-dir={docker_dir}"], check=True)
1656 if progress_callback: progress_callback(98)
1657 os.remove(exe_path)
1658 docker_exe_path = os.path.join(docker_dir, "Docker", "Docker Desktop.exe")
1659 if not os.path.exists(docker_exe_path):
1660 raise RuntimeError("Nie znaleziono pliku Docker Desktop po instalacji.")
1661 version = subprocess.run([docker_exe_path, "--version"], capture_output=True, text=True).stdout.strip().split()[2].rstrip(',')
1662 self._update_settings("docker_path", docker_exe_path)
1663 self._update_settings("docker_version", version)
1664 if progress_callback: progress_callback(100)
1665 except Exception as e:
1666 raise RuntimeError(f"Instalacja pakietu Docker nieudana: {str(e)}")
1667
1668 def uninstall_package(self, package):
1669 """
1670 Odinstalowuje pakiet, usuwając jego katalog i wpisy w ustawieniach.
1671 :param package: Nazwa pakietu.
1672 """
1673 try:
1674 folder = os.path.join(PACKAGES_DIR, package.lower())
1675 if os.path.exists(folder):
1676 shutil.rmtree(folder)
1677 self._remove_setting(f"{package.lower()}_path")
1678 self._remove_setting(f"{package.lower()}_version")
1679 except Exception as e:
1680 raise RuntimeError(f"Odinstalowanie pakietu {package} nieudane: {str(e)}")
1681
1682 def _update_settings(self, key, value):
1683 """
1684 Zapisuje ustawienie w pliku settings.json.
1685 :param key: Klucz ustawienia.
1686 :param value: Wartość ustawienia.
1687 """
1688 settings = {}
1689 try:
1690 if os.path.exists(self.settings_path):
1691 with open(self.settings_path, "r", encoding="utf-8") as f:
1692 settings = json.load(f)
1693 except (json.JSONDecodeError, IOError):
1694 settings = {}
1695 settings[key] = value
1696 try:
1697 with open(self.settings_path, "w", encoding="utf-8") as f:
1698 json.dump(settings, f, indent=4)
1699 except IOError as e:
1700 raise RuntimeError(f"Błąd zapisu ustawień do pliku {os.path.basename(self.settings_path)}: {str(e)}")
1701
1702 def _remove_setting(self, key):
1703 """
1704 Usuwa ustawienie z pliku settings.json.
1705 :param key: Klucz ustawienia do usunięcia.
1706 """
1707 settings = {}
1708 try:
1709 if os.path.exists(self.settings_path):
1710 with open(self.settings_path, "r", encoding="utf-8") as f:
1711 settings = json.load(f)
1712 if key in settings:
1713 del settings[key]
1714 with open(self.settings_path, "w", encoding="utf-8") as f:
1715 json.dump(settings, f, indent=4)
1716 except (json.JSONDecodeError, IOError):
1717 pass
1718 except Exception as e:
1719 raise RuntimeError(f"Błąd usuwania ustawienia '{key}' z pliku {os.path.basename(self.settings_path)}: {str(e)}")
1720
1721 def _get_setting(self, key):
1722 """
1723 Pobiera wartość ustawienia z pliku settings.json.
1724 :param key: Klucz ustawienia.
1725 :return: Wartość ustawienia lub None.
1726 """
1727 try:
1728 if os.path.exists(self.settings_path):
1729 with open(self.settings_path, "r", encoding="utf-8") as f:
1730 settings = json.load(f)
1731 return settings.get(key)
1732 return None
1733 except (json.JSONDecodeError, IOError):
1734 return None
1735 except Exception:
1736 return None
1737
1738class PackageManagerDialog(QDialog):
1739 """Okno dialogowe menadżera pakietów."""
1740 def __init__(self, project_dir, settings, parent=None):
1741 """Inicjalizuje okno dialogowe."""
1742 super().__init__(parent)
1743 self.project_dir = project_dir
1744 self.settings = settings
1745
1746 self.setWindowTitle("Menadżer Pakietów")
1747 self.setModal(True)
1748 self.resize(800, 500)
1749
1750 self.pkg_manager = PackageManager(self)
1751 self.worker = None
1752
1753 self.setup_ui()
1754 self.apply_styles()
1755 self.populate_table()
1756
1757 def setup_ui(self):
1758 """Konfiguruje interfejs użytkownika."""
1759 layout = QVBoxLayout(self)
1760 self.status_label = QLabel("Status: Gotowy")
1761 layout.addWidget(self.status_label)
1762 self.progress_bar = QProgressBar()
1763 self.progress_bar.setVisible(False)
1764 layout.addWidget(self.progress_bar)
1765 self.table = QTableWidget(0, 5)
1766 self.table.setHorizontalHeaderLabels(["Nazwa Pakietu", "Opis", "Wersja", "Status", "Akcje"])
1767 header = self.table.horizontalHeader()
1768 header.setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
1769 header.setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
1770 header.setSectionResizeMode(2, QHeaderView.ResizeMode.ResizeToContents)
1771 header.setSectionResizeMode(3, QHeaderView.ResizeMode.ResizeToContents)
1772 header.setSectionResizeMode(4, QHeaderView.ResizeMode.ResizeToContents)
1773 self.table.setSelectionMode(QTableWidget.SelectionMode.NoSelection)
1774 layout.addWidget(self.table)
1775
1776 def apply_styles(self):
1777 """Zastosuj style CSS zgodnie z motywem aplikacji."""
1778 # Pobierz motyw z ustawień lub domyślnie 'light'
1779 theme = 'light'
1780 if self.parent and hasattr(self.parent, 'settings'):
1781 theme = getattr(self.parent, 'settings', {}).get('theme', 'light')
1782 elif hasattr(self.parent, 'theme'):
1783 theme = getattr(self.parent, 'theme', 'light')
1784 if theme == 'dark':
1785 self.setStyleSheet(get_dark_package_manager_stylesheet())
1786 else:
1787 self.setStyleSheet(get_light_package_manager_stylesheet())
1788
1789 def populate_table(self):
1790 """Wypełnia tabelę informacjami o dostępnych pakietach."""
1791 packages = [
1792 {
1793 "name": "Python",
1794 "desc": "Język programowania Python (64-bit)",
1795 "size": "~30 MB",
1796 "install_func": self.download_python,
1797 "uninstall_func": lambda: self.uninstall_package("Python"),
1798 },
1799 {
1800 "name": "Node.js",
1801 "desc": "Środowisko uruchomieniowe JavaScript (64-bit)",
1802 "size": "~25 MB",
1803 "install_func": self.download_node,
1804 "uninstall_func": lambda: self.uninstall_package("Node.js"),
1805 },
1806 {
1807 "name": "Go",
1808 "desc": "Język programowania Go (64-bit)",
1809 "size": "~100 MB",
1810 "install_func": self.download_go,
1811 "uninstall_func": lambda: self.uninstall_package("Go"),
1812 },
1813 {
1814 "name": "Java",
1815 "desc": "Java Development Kit (OpenJDK, 64-bit)",
1816 "size": "~200 MB",
1817 "install_func": self.download_java,
1818 "uninstall_func": lambda: self.uninstall_package("Java"),
1819 },
1820 {
1821 "name": "Ruby",
1822 "desc": "Język programowania Ruby (64-bit)",
1823 "size": "~50 MB",
1824 "install_func": self.download_ruby,
1825 "uninstall_func": lambda: self.uninstall_package("Ruby"),
1826 },
1827 {
1828 "name": "Rust",
1829 "desc": "Język programowania Rust (64-bit)",
1830 "size": "~150 MB",
1831 "install_func": self.download_rust,
1832 "uninstall_func": lambda: self.uninstall_package("Rust"),
1833 },
1834 {
1835 "name": "Git",
1836 "desc": "System kontroli wersji Git (64-bit)",
1837 "size": "~50 MB",
1838 "install_func": self.download_git,
1839 "uninstall_func": lambda: self.uninstall_package("Git"),
1840 },
1841 {
1842 "name": "Docker",
1843 "desc": "Platforma do konteneryzacji Docker Desktop (64-bit)",
1844 "size": "~500 MB",
1845 "install_func": self.download_docker,
1846 "uninstall_func": lambda: self.uninstall_package("Docker"),
1847 },
1848 ]
1849 self.table.setRowCount(len(packages))
1850 for row, pkginfo in enumerate(packages):
1851 name = pkginfo["name"]
1852 local_version = self.pkg_manager._get_local_version(name) or "Brak"
1853 latest_version = self.pkg_manager._get_latest_version(name) or "Brak informacji"
1854 version_text = local_version
1855 is_installed = self.pkg_manager._is_installed(name)
1856 status = "Niezainstalowany"
1857 status_color = QColor("#ff4444")
1858 if is_installed:
1859 status = "Zainstalowano"
1860 status_color = QColor("#44ff44")
1861 update_available = False
1862 if latest_version != "Brak informacji" and local_version != "Brak":
1863 try:
1864 local_parts = list(map(int, local_version.split('.')))
1865 latest_parts = list(map(int, latest_version.split('.')))
1866 max_len = max(len(local_parts), len(latest_parts))
1867 local_parts += [0] * (max_len - len(local_parts))
1868 latest_parts += [0] * (max_len - len(latest_parts))
1869 if latest_parts > local_parts:
1870 update_available = True
1871 except ValueError:
1872 if latest_version > local_version:
1873 update_available = True
1874 if update_available:
1875 status = "Dostępna aktualizacja"
1876 status_color = QColor("#ffff44")
1877 version_text = f"{local_version} (Najnowsza: {latest_version})"
1878 self.table.setItem(row, 0, QTableWidgetItem(name))
1879 self.table.setItem(row, 1, QTableWidgetItem(pkginfo["desc"]))
1880 self.table.setItem(row, 2, QTableWidgetItem(version_text))
1881 status_item = QTableWidgetItem(status)
1882 status_item.setForeground(status_color)
1883 status_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
1884 self.table.setItem(row, 3, status_item)
1885 action_layout = QHBoxLayout()
1886 action_layout.setContentsMargins(0, 0, 0, 0)
1887 action_layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
1888 if is_installed:
1889 uninstall_btn = QPushButton("Odinstaluj")
1890 uninstall_btn.clicked.connect(pkginfo["uninstall_func"])
1891 action_layout.addWidget(uninstall_btn)
1892 if update_available:
1893 update_btn = QPushButton("Aktualizuj")
1894 update_btn.clicked.connect(pkginfo["install_func"])
1895 action_layout.addWidget(update_btn)
1896 else:
1897 install_btn = QPushButton("Zainstaluj")
1898 install_btn.clicked.connect(pkginfo["install_func"])
1899 action_layout.addWidget(install_btn)
1900 action_widget = QWidget()
1901 action_widget.setLayout(action_layout)
1902 self.table.setCellWidget(row, 4, action_widget)
1903
1904 def download_python(self):
1905 """Rozpoczyna instalację Pythona."""
1906 self.start_operation(self.pkg_manager.install_latest_python, "Python", "instalacji")
1907
1908 def download_node(self):
1909 """Rozpoczyna instalację Node.js."""
1910 self.start_operation(self.pkg_manager.install_latest_nodejs, "Node.js", "instalacji")
1911
1912 def download_go(self):
1913 """Rozpoczyna instalację Go."""
1914 self.start_operation(self.pkg_manager.install_latest_go, "Go", "instalacji")
1915
1916 def download_java(self):
1917 """Rozpoczyna instalację Java."""
1918 self.start_operation(self.pkg_manager.install_latest_java, "Java", "instalacji")
1919
1920 def download_ruby(self):
1921 """Rozpoczyna instalację Ruby."""
1922 self.start_operation(self.pkg_manager.install_latest_ruby, "Ruby", "instalacji")
1923
1924 def download_rust(self):
1925 """Rozpoczyna instalację Rust."""
1926 self.start_operation(self.pkg_manager.install_latest_rust, "Rust", "instalacji")
1927
1928 def download_git(self):
1929 """Rozpoczyna instalację Git."""
1930 self.start_operation(self.pkg_manager.install_latest_git, "Git", "instalacji")
1931
1932 def download_docker(self):
1933 """Rozpoczyna instalację Docker."""
1934 self.start_operation(self.pkg_manager.install_latest_docker, "Docker", "instalacji")
1935
1936 def uninstall_package(self, package_name):
1937 """Rozpoczyna odinstalowanie pakietu."""
1938 self.start_operation(lambda progress_callback=None: self.pkg_manager.uninstall_package(package_name), package_name, "odinstalowania")
1939
1940 def start_operation(self, func, package_name, operation_type):
1941 """
1942 Rozpoczyna operację na pakiecie w osobnym wątku.
1943 :param func: Funkcja do wykonania.
1944 :param package_name: Nazwa pakietu.
1945 :param operation_type: Typ operacji.
1946 """
1947 self.set_actions_enabled(False)
1948 self.status_label.setText(f"Rozpoczęto operację {operation_type} pakietu {package_name}...")
1949 self.progress_bar.setValue(0)
1950 self.progress_bar.setVisible(True)
1951 self.worker = DownloadWorker(func, package_name)
1952 self.worker.progress.connect(self.update_progress)
1953 self.worker.finished.connect(self.on_operation_finished)
1954 self.worker.error.connect(self.on_operation_error)
1955 self.worker.start()
1956
1957 def update_progress(self, value):
1958 """Aktualizuje pasek postępu."""
1959 self.progress_bar.setValue(value)
1960 QApplication.processEvents()
1961
1962 def on_operation_finished(self, message):
1963 """Obsługuje zakończenie operacji."""
1964 self.status_label.setText(message)
1965 self.progress_bar.setValue(100)
1966 self.progress_bar.setVisible(False)
1967 self.populate_table()
1968 self.set_actions_enabled(True)
1969
1970 def on_operation_error(self, message):
1971 """Obsługuje błąd operacji."""
1972 self.status_label.setText(f"Błąd: {message}")
1973 self.progress_bar.setVisible(False)
1974 self.populate_table()
1975 self.set_actions_enabled(True)
1976
1977 def set_actions_enabled(self, enabled):
1978 """Włącza/wyłącza przyciski akcji."""
1979 for row in range(self.table.rowCount()):
1980 widget = self.table.cellWidget(row, 4)
1981 if widget:
1982 for btn in widget.findChildren(QPushButton):
1983 btn.setEnabled(enabled)
1984
1985if __name__ == '__main__':
1986 import sys
1987 userdata_dir_test = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "userdata"))
1988 os.makedirs(userdata_dir_test, exist_ok=True)
1989 settings_path_test = os.path.join(userdata_dir_test, "settings.json")
1990 if not os.path.exists(settings_path_test):
1991 with open(settings_path_test, "w") as f:
1992 json.dump({}, f)
1993 app = QApplication(sys.argv)
1994 dialog = PackageManagerDialog()
1995 dialog.exec()
1996 sys.exit(app.exec())
1997
1998Ścieżka: /src/process.py
1999Rozmiar: 4,36 KB
2000Zawartość:
2001# Zarządzanie procesami zewnętrznymi
2002#/src/process.py
2003
2004import os
2005import platform
2006import shlex
2007from PyQt6.QtCore import QProcess, QProcessEnvironment
2008
2009class ProcessManager:
2010 def __init__(self, parent):
2011 self.process = QProcess(parent)
2012 self.process.readyReadStandardOutput.connect(self._handle_stdout)
2013 self.process.readyReadStandardError.connect(self._handle_stderr)
2014 self.process.finished.connect(self._handle_finished)
2015 self.console_output_callback = None
2016 self.status_bar_callback = None
2017
2018 def set_callbacks(self, console_output, status_bar):
2019 self.console_output_callback = console_output
2020 self.status_bar_callback = status_bar
2021
2022 def run_command(self, command, working_dir, python_path=None, node_path=None):
2023 if self.process.state() != QProcess.ProcessState.NotRunning:
2024 self._append_output("Inny proces już działa. Zakończ go najpierw.", is_error=True)
2025 return
2026 command_str = shlex.join(command)
2027 self._append_output(f"Uruchamianie: {command_str}\nw katalogu: {working_dir}\n---")
2028 self._update_status("Proces uruchomiony...")
2029 try:
2030 program = command[0]
2031 arguments = command[1:]
2032 self.process.setWorkingDirectory(working_dir)
2033 env = QProcessEnvironment.systemEnvironment()
2034 current_path = env.value("PATH", "")
2035 paths_to_prepend = []
2036 if python_path and os.path.exists(python_path):
2037 py_dir = os.path.dirname(python_path)
2038 if os.path.normcase(py_dir) not in [os.path.normcase(p) for p in current_path.split(os.pathsep)]:
2039 paths_to_prepend.append(py_dir)
2040 if node_path and os.path.exists(node_path):
2041 node_dir = os.path.dirname(node_path)
2042 if os.path.normcase(node_dir) not in [os.path.normcase(p) for p in current_path.split(os.pathsep)]:
2043 paths_to_prepend.append(node_dir)
2044 if paths_to_prepend:
2045 new_path = os.pathsep.join(paths_to_prepend) + (os.pathsep + current_path if current_path else "")
2046 env.insert("PATH", new_path)
2047 if platform.system() == "Windows":
2048 env.insert("Path", new_path)
2049 self.process.setProcessEnvironment(env)
2050 self.process.start(program, arguments)
2051 if not self.process.waitForStarted(1000):
2052 error = self.process.errorString()
2053 self._append_output(f"Nie udało się uruchomić '{program}': {error}", is_error=True)
2054 self._update_status(f"Błąd uruchamiania: {program}")
2055 except Exception as e:
2056 self._append_output(f"Błąd podczas uruchamiania: {e}", is_error=True)
2057 self._update_status("Błąd uruchamiania.")
2058
2059 def _append_output(self, text, is_error=False):
2060 if self.console_output_callback:
2061 self.console_output_callback(text, is_error)
2062
2063 def _update_status(self, message):
2064 if self.status_bar_callback:
2065 self.status_bar_callback(message)
2066
2067 def _handle_stdout(self):
2068 while self.process.bytesAvailable():
2069 data = self.process.readAllStandardOutput()
2070 try:
2071 text = bytes(data).decode('utf-8')
2072 except UnicodeDecodeError:
2073 text = bytes(data).decode('utf-8', errors='replace')
2074 self._append_output(text)
2075
2076 def _handle_stderr(self):
2077 while self.process.bytesAvailable():
2078 data = self.process.readAllStandardError()
2079 try:
2080 text = bytes(data).decode('utf-8')
2081 except UnicodeDecodeError:
2082 text = bytes(data).decode('utf-8', errors='replace')
2083 self._append_output(text, is_error=True)
2084
2085 def _handle_finished(self, exit_code, exit_status):
2086 self._handle_stdout()
2087 self._handle_stderr()
2088 self._append_output("\n--- Zakończono proces ---")
2089 if exit_status == QProcess.ExitStatus.NormalExit:
2090 self._append_output(f"Kod wyjścia: {exit_code}")
2091 self._update_status(f"Zakończono. Kod wyjścia: {exit_code}")
2092 else:
2093 self._append_output(f"Awaria procesu z kodem: {exit_code}", is_error=True)
2094 self._update_status(f"Awaria procesu. Kod wyjścia: {exit_code}")
2095
2096Ścieżka: /src/theme.py
2097Rozmiar: 12,98 KB
2098Zawartość:
2099# Zarządzanie motywami aplikacji – zajebiście stylowe! 😎
2100# /src/theme.py
2101
2102from PyQt6.QtWidgets import QMainWindow, QApplication
2103from PyQt6.QtGui import QPalette, QColor
2104
2105def get_dark_theme_stylesheet():
2106 return """
2107 QMainWindow, QWidget { background-color: #2E2E2E; color: #D3D3D3; }
2108 QMenuBar { background-color: #3C3C3C; color: #D3D3D3; }
2109 QMenuBar::item:selected { background-color: #505050; }
2110 QMenu { background-color: #3C3C3C; color: #D3D3D3; border: 1px solid #505050; }
2111 QMenu::item:selected { background-color: #505050; }
2112 QToolBar { background-color: #3C3C3C; color: #D3D3D3; spacing: 5px; padding: 2px; }
2113 QToolButton { background-color: transparent; border: 1px solid transparent; padding: 3px; border-radius: 4px; }
2114 QToolButton:hover { border: 1px solid #505050; background-color: #454545; }
2115 QToolButton:pressed { background-color: #404040; }
2116 QPushButton { background-color: #505050; color: #D3D3D3; border: 1px solid #606060; padding: 4px 8px; border-radius: 4px; }
2117 QPushButton:hover { background-color: #606060; }
2118 QStatusBar { background-color: #3C3C3C; color: #D3D3D3; }
2119 QSplitter::handle { background-color: #505050; }
2120 QTreeView { background-color: #1E1E1E; color: #D3D3D3; border: 1px solid #3C3C3C; alternate-background-color: #252525; }
2121 QTreeView::item:selected { background-color: #007acc; color: white; }
2122 QTabWidget::pane { border: 1px solid #3C3C3C; background-color: #1E1E1E; }
2123 QTabBar::tab {
2124 background: #3C3C3C;
2125 color: #D3D3D3;
2126 border: 1px solid #3C3C3C;
2127 border-bottom-color: #1E1E1E;
2128 border-top-left-radius: 4px;
2129 border-top-right-radius: 4px;
2130 padding: 6px 12px;
2131 margin-right: 2px;
2132 }
2133 QTabBar::tab:selected {
2134 background: #1E1E1E;
2135 border-bottom-color: #1E1E1E;
2136 color: #FFFFFF;
2137 }
2138 QTabBar::tab:hover {
2139 background: #454545;
2140 }
2141 QPlainTextEdit {
2142 background-color: #1E1E1E;
2143 color: #D3D3D3;
2144 border: none;
2145 selection-background-color: #007acc;
2146 selection-color: white;
2147 }
2148 QPlainTextEdit[readOnly="true"] {
2149 background-color: #1E1E1E;
2150 color: #CCCCCC;
2151 }
2152 QLineEdit {
2153 background-color: #3C3C3C;
2154 color: #D3D3D3;
2155 border: 1px solid #505050;
2156 padding: 4px;
2157 selection-background-color: #007acc;
2158 selection-color: white;
2159 border-radius: 3px;
2160 }
2161 QComboBox {
2162 background-color: #3C3C3C;
2163 color: #D3D3D3;
2164 border: 1px solid #505050;
2165 padding: 4px;
2166 border-radius: 3px;
2167 }
2168 QComboBox::drop-down {
2169 border: none;
2170 }
2171 QComboBox::down-arrow {
2172 image: url(:/icons/down_arrow_dark.png);
2173 }
2174 QDialog {
2175 background-color: #2E2E2E;
2176 color: #D3D3D3;
2177 }
2178 QLabel {
2179 color: #D3D3D3;
2180 }
2181 QDialogButtonBox QPushButton {
2182 background-color: #505050;
2183 color: #D3D3D3;
2184 border: 1px solid #606060;
2185 padding: 5px 10px;
2186 border-radius: 4px;
2187 }
2188 QSpinBox {
2189 background-color: #3C3C3C;
2190 color: #D3D3D3;
2191 border: 1px solid #505050;
2192 padding: 4px;
2193 selection-background-color: #007acc;
2194 selection-color: white;
2195 border-radius: 3px;
2196 }
2197 """
2198
2199def get_light_theme_stylesheet():
2200 return """
2201 QMainWindow, QWidget { background-color: #F5F5F5; color: #222222; }
2202 QMenuBar { background-color: #E0E0E0; color: #222222; }
2203 QMenuBar::item:selected { background-color: #B0B0B0; }
2204 QMenu { background-color: #FFFFFF; color: #222222; border: 1px solid #CCCCCC; }
2205 QMenu::item:selected { background-color: #B0B0B0; }
2206 QToolBar { background-color: #E0E0E0; color: #222222; spacing: 5px; padding: 2px; }
2207 QToolButton { background-color: transparent; border: 1px solid transparent; padding: 3px; border-radius: 4px; }
2208 QToolButton:hover { border: 1px solid #CCCCCC; background-color: #F0F0F0; }
2209 QToolButton:pressed { background-color: #DDDDDD; }
2210 QPushButton { background-color: #E0E0E0; color: #222222; border: 1px solid #CCCCCC; padding: 4px 8px; border-radius: 4px; }
2211 QPushButton:hover { background-color: #CCCCCC; }
2212 QStatusBar { background-color: #E0E0E0; color: #222222; }
2213 QSplitter::handle { background-color: #CCCCCC; }
2214 QTreeView { background-color: #FFFFFF; color: #222222; border: 1px solid #CCCCCC; alternate-background-color: #F0F0F0; }
2215 QTreeView::item:selected { background-color: #007acc; color: white; }
2216 QTabWidget::pane { border: 1px solid #CCCCCC; background-color: #FFFFFF; }
2217 QTabBar::tab {
2218 background: #E0E0E0;
2219 color: #222222;
2220 border: 1px solid #CCCCCC;
2221 border-bottom-color: #FFFFFF;
2222 border-top-left-radius: 4px;
2223 border-top-right-radius: 4px;
2224 padding: 6px 12px;
2225 margin-right: 2px;
2226 }
2227 QTabBar::tab:selected {
2228 background: #FFFFFF;
2229 border-bottom-color: #FFFFFF;
2230 color: #000000;
2231 }
2232 QTabBar::tab:hover {
2233 background: #F0F0F0;
2234 }
2235 QPlainTextEdit {
2236 background-color: #FFFFFF;
2237 color: #222222;
2238 border: none;
2239 selection-background-color: #007acc;
2240 selection-color: white;
2241 }
2242 QPlainTextEdit[readOnly="true"] {
2243 background-color: #F5F5F5;
2244 color: #444444;
2245 }
2246 QLineEdit {
2247 background-color: #FFFFFF;
2248 color: #222222;
2249 border: 1px solid #CCCCCC;
2250 padding: 4px;
2251 selection-background-color: #007acc;
2252 selection-color: white;
2253 border-radius: 3px;
2254 }
2255 QComboBox {
2256 background-color: #FFFFFF;
2257 color: #222222;
2258 border: 1px solid #CCCCCC;
2259 padding: 4px;
2260 border-radius: 3px;
2261 }
2262 QComboBox::drop-down {
2263 border: none;
2264 }
2265 QComboBox::down-arrow {
2266 image: url(:/icons/down_arrow_light.png);
2267 }
2268 QDialog {
2269 background-color: #f5f5f5;
2270 color: #222222;
2271 }
2272 QLabel {
2273 color: #222222;
2274 }
2275 QDialogButtonBox QPushButton {
2276 background-color: #e0e0e0;
2277 color: #222222;
2278 border: 1px solid #cccccc;
2279 padding: 5px 10px;
2280 border-radius: 4px;
2281 }
2282 QSpinBox {
2283 background-color: #FFFFFF;
2284 color: #222222;
2285 border: 1px solid #CCCCCC;
2286 padding: 4px;
2287 selection-background-color: #007acc;
2288 selection-color: white;
2289 border-radius: 3px;
2290 }
2291 """
2292
2293def get_dark_package_manager_stylesheet():
2294 return """
2295 QDialog {
2296 background-color: #23272e;
2297 color: #e0e0e0;
2298 font-family: 'Segoe UI', sans-serif;
2299 }
2300 QTableWidget {
2301 background-color: #2c313a;
2302 color: #e0e0e0;
2303 gridline-color: #444a56;
2304 border: 1px solid #444a56;
2305 selection-background-color: #3a3f4b;
2306 selection-color: #e0e0e0;
2307 }
2308 QTableWidget::item {
2309 padding: 5px;
2310 }
2311 QHeaderView::section {
2312 background-color: #23272e;
2313 color: #e0e0e0;
2314 padding: 5px;
2315 border: none;
2316 font-weight: bold;
2317 }
2318 QPushButton {
2319 background-color: #3a3f4b;
2320 color: #e0e0e0;
2321 border: 1px solid #444a56;
2322 padding: 5px 10px;
2323 border-radius: 4px;
2324 }
2325 QPushButton:hover {
2326 background-color: #444a56;
2327 border-color: #5a6272;
2328 }
2329 QPushButton:pressed {
2330 background-color: #23272e;
2331 }
2332 QPushButton:disabled {
2333 background-color: #2c313a;
2334 color: #888888;
2335 border-color: #444a56;
2336 }
2337 QProgressBar {
2338 background-color: #23272e;
2339 border: 1px solid #444a56;
2340 border-radius: 4px;
2341 text-align: center;
2342 color: #e0e0e0;
2343 }
2344 QProgressBar::chunk {
2345 background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #4CAF50, stop:1 #81C784);
2346 border-radius: 4px;
2347 }
2348 QLabel {
2349 color: #e0e0e0;
2350 font-size: 14px;
2351 margin-bottom: 5px;
2352 }
2353 QWidget#actionWidget {
2354 background-color: transparent;
2355 }
2356 """
2357
2358def get_light_package_manager_stylesheet():
2359 return """
2360 QDialog {
2361 background-color: #f5f5f5;
2362 color: #222222;
2363 font-family: 'Segoe UI', sans-serif;
2364 }
2365 QTableWidget {
2366 background-color: #ffffff;
2367 color: #222222;
2368 gridline-color: #cccccc;
2369 border: 1px solid #cccccc;
2370 selection-background-color: #b0d6fb;
2371 selection-color: #222222;
2372 }
2373 QTableWidget::item {
2374 padding: 5px;
2375 }
2376 QHeaderView::section {
2377 background-color: #e0e0e0;
2378 color: #222222;
2379 padding: 5px;
2380 border: none;
2381 font-weight: bold;
2382 }
2383 QPushButton {
2384 background-color: #e0e0e0;
2385 color: #222222;
2386 border: 1px solid #cccccc;
2387 padding: 5px 10px;
2388 border-radius: 4px;
2389 }
2390 QPushButton:hover {
2391 background-color: #cccccc;
2392 border-color: #999999;
2393 }
2394 QPushButton:pressed {
2395 background-color: #bbbbbb;
2396 }
2397 QPushButton:disabled {
2398 background-color: #dddddd;
2399 color: #aaaaaa;
2400 border-color: #cccccc;
2401 }
2402 QProgressBar {
2403 background-color: #e0e0e0;
2404 border: 1px solid #cccccc;
2405 border-radius: 4px;
2406 text-align: center;
2407 color: #222222;
2408 }
2409 QProgressBar::chunk {
2410 background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #4CAF50, stop:1 #81C784);
2411 border-radius: 4px;
2412 }
2413 QLabel {
2414 color: #222222;
2415 font-size: 14px;
2416 margin-bottom: 5px;
2417 }
2418 QWidget#actionWidget {
2419 background-color: transparent;
2420 }
2421 """
2422
2423def apply_theme(window: QMainWindow, theme_name: str):
2424 # Nakładamy motyw jak farbę na płótno, Paffcio! 🎨
2425 app = QApplication.instance()
2426 palette = QPalette()
2427
2428 if theme_name == "dark":
2429 # Ciemny motyw – jak Twój humor po debugowaniu w nocy 😜
2430 palette.setColor(QPalette.ColorRole.Window, QColor("#2E2E2E"))
2431 palette.setColor(QPalette.ColorRole.WindowText, QColor("#D3D3D3"))
2432 palette.setColor(QPalette.ColorRole.Base, QColor("#1E1E1E"))
2433 palette.setColor(QPalette.ColorRole.AlternateBase, QColor("#252525"))
2434 palette.setColor(QPalette.ColorRole.Text, QColor("#D3D3D3"))
2435 palette.setColor(QPalette.ColorRole.Button, QColor("#505050"))
2436 palette.setColor(QPalette.ColorRole.ButtonText, QColor("#D3D3D3"))
2437 palette.setColor(QPalette.ColorRole.Highlight, QColor("#007acc"))
2438 palette.setColor(QPalette.ColorRole.HighlightedText, QColor("#FFFFFF"))
2439 stylesheet = get_dark_theme_stylesheet()
2440 else:
2441 # Jasny motyw – dla tych, co kodzą przy kawie w słońcu ☕
2442 palette.setColor(QPalette.ColorRole.Window, QColor("#F5F5F5"))
2443 palette.setColor(QPalette.ColorRole.WindowText, QColor("#222222"))
2444 palette.setColor(QPalette.ColorRole.Base, QColor("#FFFFFF"))
2445 palette.setColor(QPalette.ColorRole.AlternateBase, QColor("#F0F0F0"))
2446 palette.setColor(QPalette.ColorRole.Text, QColor("#222222"))
2447 palette.setColor(QPalette.ColorRole.Button, QColor("#E0E0E0"))
2448 palette.setColor(QPalette.ColorRole.ButtonText, QColor("#222222"))
2449 palette.setColor(QPalette.ColorRole.Highlight, QColor("#007acc"))
2450 palette.setColor(QPalette.ColorRole.HighlightedText, QColor("#FFFFFF"))
2451 stylesheet = get_light_theme_stylesheet()
2452
2453 app.setPalette(palette)
2454 app.setStyleSheet(stylesheet)
2455 window.statusBar().showMessage(f"Zmieniono motyw na: {theme_name.capitalize()}")
2456
2457Ścieżka: /src/ui.py
2458Rozmiar: 69,99 KB
2459Zawartość:
2460import sys
2461import os
2462import json
2463import subprocess
2464import re
2465import platform
2466import shutil
2467import shlex
2468from src.console import ConsoleWidget, ConsoleManager, AIChatManager
2469from PyQt6.QtWidgets import (
2470 QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
2471 QSplitter, QTreeView, QTabWidget, QPlainTextEdit,
2472 QPushButton, QLineEdit, QFileDialog, QMenuBar, QToolBar, QStatusBar,
2473 QMessageBox, QMenu, QStyleFactory, QDialog, QFormLayout,
2474 QLabel, QDialogButtonBox, QComboBox, QToolButton,
2475 QInputDialog, QSpinBox, QSizePolicy, QAbstractItemView,
2476 QFrame
2477)
2478from PyQt6.QtGui import (
2479 QIcon, QAction, QKeySequence, QTextCharFormat, QFont,
2480 QSyntaxHighlighter, QTextDocument, QColor, QFileSystemModel,
2481 QDesktopServices, QPalette
2482)
2483from PyQt6.QtCore import (
2484 QDir, Qt, QProcess, QSettings, QFileInfo, QThread, pyqtSignal, QTimer, QSize,
2485 QStandardPaths, QUrl, QLocale, QCoreApplication, QProcessEnvironment
2486)
2487try:
2488 import qtawesome as qta
2489except ImportError:
2490 qta = None
2491 print("Zainstaluj qtawesome ('pip install qtawesome') dla lepszych ikon.", file=sys.stderr)
2492
2493try:
2494 from src.dialogs import NewProjectDialog, NewItemDialog, RenameItemDialog, SettingsDialog
2495 from src.package_manager import PackageManagerDialog
2496except ImportError:
2497 import sys, os
2498 sys.path.insert(0, os.path.join(os.path.dirname(__file__)))
2499 from dialogs import NewProjectDialog, NewItemDialog, RenameItemDialog, SettingsDialog
2500 from package_manager import PackageManagerDialog
2501from src.utils import load_package_json, get_file_language
2502from src.config import HIGHLIGHTING_RULES
2503from src.theme import apply_theme
2504
2505# USTAWIENIE ROOT_DIR
2506ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
2507DATA_DIR = os.path.join(ROOT_DIR, 'userdata')
2508PROJECTS_DIR = os.path.join(ROOT_DIR, 'projects')
2509SETTINGS_FILE = os.path.join(DATA_DIR, 'settings.json')
2510RECENTS_FILE = os.path.join(DATA_DIR, 'recents.json')
2511os.makedirs(DATA_DIR, exist_ok=True)
2512os.makedirs(PROJECTS_DIR, exist_ok=True)
2513
2514# Formatowanie podświetlania składni
2515FORMAT_DEFAULT = QTextCharFormat()
2516FORMAT_KEYWORD = QTextCharFormat()
2517FORMAT_KEYWORD.setForeground(QColor("#000080")) # Navy
2518FORMAT_STRING = QTextCharFormat()
2519FORMAT_STRING.setForeground(QColor("#008000")) # Green
2520FORMAT_COMMENT = QTextCharFormat()
2521FORMAT_COMMENT.setForeground(QColor("#808080")) # Gray
2522FORMAT_COMMENT.setFontItalic(True)
2523FORMAT_FUNCTION = QTextCharFormat()
2524FORMAT_FUNCTION.setForeground(QColor("#0000FF")) # Blue
2525FORMAT_CLASS = QTextCharFormat()
2526FORMAT_CLASS.setForeground(QColor("#A52A2A")) # Brown
2527FORMAT_CLASS.setFontWeight(QFont.Weight.Bold)
2528FORMAT_NUMBERS = QTextCharFormat()
2529FORMAT_NUMBERS.setForeground(QColor("#FF0000")) # Red
2530FORMAT_OPERATOR = QTextCharFormat()
2531FORMAT_OPERATOR.setForeground(QColor("#A62929")) # Dark Red
2532FORMAT_BUILTIN = QTextCharFormat()
2533FORMAT_BUILTIN.setForeground(QColor("#008080")) # Teal
2534FORMAT_SECTION = QTextCharFormat()
2535FORMAT_SECTION.setForeground(QColor("#800080")) # Purple
2536FORMAT_SECTION.setFontWeight(QFont.Weight.Bold)
2537FORMAT_PROPERTY = QTextCharFormat()
2538FORMAT_PROPERTY.setForeground(QColor("#B8860B")) # DarkGoldenrod
2539
2540class CodeSyntaxHighlighter(QSyntaxHighlighter):
2541 def __init__(self, parent: QTextDocument, language: str):
2542 super().__init__(parent)
2543 self._language = language.lower()
2544 self._rules = []
2545 lang_config = HIGHLIGHTING_RULES.get(self._language, {})
2546 keywords = lang_config.get('keywords', [])
2547 builtins = lang_config.get('builtins', [])
2548 patterns = lang_config.get('patterns', [])
2549 keyword_format = FORMAT_KEYWORD
2550 for keyword in keywords:
2551 pattern = r'\b' + re.escape(keyword) + r'\b'
2552 self._rules.append((re.compile(pattern), keyword_format))
2553 builtin_format = FORMAT_BUILTIN
2554 for builtin in builtins:
2555 pattern = r'\b' + re.escape(builtin) + r'\b'
2556 self._rules.append((re.compile(pattern), builtin_format))
2557 for pattern_str, format, *flags in patterns:
2558 try:
2559 pattern = re.compile(pattern_str, *flags)
2560 self._rules.append((pattern, format))
2561 except re.error as e:
2562 print(f"Błąd kompilacji regex '{pattern_str}' dla języka {self._language}: {e}", file=sys.stderr)
2563
2564 def highlightBlock(self, text: str):
2565 self.setFormat(0, len(text), FORMAT_DEFAULT)
2566 self.setCurrentBlockState(0)
2567 block_comment_delimiters = []
2568 if self._language in ['javascript', 'css', 'c++']:
2569 block_comment_delimiters.append(("/*", "*/", FORMAT_COMMENT))
2570 if self._language == 'html':
2571 pass # HTML comments handled by regex
2572 comment_start_in_prev_block = (self.previousBlockState() == 1)
2573 if comment_start_in_prev_block:
2574 end_delimiter_index = text.find("*/")
2575 if end_delimiter_index >= 0:
2576 self.setFormat(0, end_delimiter_index + 2, FORMAT_COMMENT)
2577 self.setCurrentBlockState(0)
2578 start_pos = end_delimiter_index + 2
2579 else:
2580 self.setFormat(0, len(text), FORMAT_COMMENT)
2581 self.setCurrentBlockState(1)
2582 return
2583 else:
2584 start_pos = 0
2585 start_delimiter = "/*"
2586 end_delimiter = "*/"
2587 startIndex = text.find(start_delimiter, start_pos)
2588 while startIndex >= 0:
2589 endIndex = text.find(end_delimiter, startIndex)
2590 if endIndex >= 0:
2591 length = endIndex - startIndex + len(end_delimiter)
2592 self.setFormat(startIndex, startIndex + length, FORMAT_COMMENT)
2593 startIndex = text.find(start_delimiter, startIndex + length)
2594 else:
2595 self.setFormat(startIndex, len(text) - startIndex, FORMAT_COMMENT)
2596 self.setCurrentBlockState(1)
2597 break
2598 for pattern, format in self._rules:
2599 if format == FORMAT_COMMENT and (pattern.pattern.startswith(re.escape('/*')) or pattern.pattern.startswith(re.escape('<!--'))):
2600 continue
2601 if format == FORMAT_COMMENT and pattern.pattern.startswith('//') and self.currentBlockState() == 1:
2602 continue
2603 for match in pattern.finditer(text):
2604 start, end = match.span()
2605 self.setFormat(start, end, format)
2606
2607class CustomFileSystemModel(QFileSystemModel):
2608 def __init__(self, parent=None):
2609 super().__init__(parent)
2610 self.icon_map = {
2611 '.py': 'fa5s.file-code',
2612 '.js': 'fa5s.file-code',
2613 '.json': 'fa5s.file-code',
2614 '.html': 'fa5s.file-code',
2615 '.css': 'fa5s.file-code',
2616 '.ini': 'fa5s.file-alt',
2617 '.txt': 'fa5s.file-alt',
2618 '.md': 'fa5s.file-alt',
2619 '.c': 'fa5s.file-code',
2620 '.cpp': 'fa5s.file-code',
2621 '.h': 'fa5s.file-code',
2622 '.hpp': 'fa5s.file-code',
2623 }
2624 self.folder_icon_name = 'fa5s.folder'
2625 self.default_file_icon_name = 'fa5s.file'
2626 self._has_qtawesome = qta is not None
2627
2628 def rename(self, index, new_name):
2629 if not index.isValid():
2630 return False
2631 old_path = self.filePath(index)
2632 new_path = os.path.join(os.path.dirname(old_path), new_name)
2633 try:
2634 os.rename(old_path, new_path)
2635 self.refresh()
2636 return True
2637 except Exception as e:
2638 print(f"Błąd podczas zmiany nazwy: {e}", file=sys.stderr)
2639 return False
2640
2641 def data(self, index, role=Qt.ItemDataRole.DisplayRole):
2642 if not index.isValid():
2643 return None
2644 if role == Qt.ItemDataRole.DecorationRole:
2645 file_info = self.fileInfo(index)
2646 if file_info.isDir():
2647 return qta.icon(self.folder_icon_name) if self._has_qtawesome else super().data(index, role)
2648 elif file_info.isFile():
2649 extension = file_info.suffix().lower()
2650 dotted_extension = '.' + extension
2651 if dotted_extension in self.icon_map and self._has_qtawesome:
2652 return qta.icon(self.icon_map[dotted_extension])
2653 return qta.icon(self.default_file_icon_name) if self._has_qtawesome else super().data(index, role)
2654 return super().data(index, role)
2655
2656 def refresh(self, *args):
2657 self.setRootPath(self.rootPath())
2658
2659class IDEWindow(QMainWindow):
2660 def __init__(self):
2661 super().__init__()
2662 self.settings = {
2663 "theme": "light",
2664 "python_path": "",
2665 "node_path": "",
2666 "show_tree": True,
2667 "show_console": True,
2668 "editor_font_size": 10,
2669 "api_key": os.getenv("XAI_API_KEY", ""),
2670 "gemini_api_key": "",
2671 "mistral_api_key": "",
2672 "ai_model": "grok-3",
2673 "ai_provider": "grok"
2674 }
2675 self.recents = {"last_project_dir": None, "open_files": []}
2676 self._load_app_state()
2677 self.setWindowTitle("Proste IDE - Bez nazwy")
2678 self.setGeometry(100, 100, 1200, 800)
2679 self.setWindowIcon(qta.icon('fa5s.code') if qta else QIcon.fromTheme("applications-development"))
2680 self.current_project_dir = self.recents.get("last_project_dir")
2681 self.open_files = {}
2682 self.base_editor_font = QFont("Courier New", 10)
2683 self._setup_ui()
2684 self._setup_menu()
2685 self._setup_toolbar()
2686 self._setup_status_bar()
2687 self._setup_connections()
2688 self._apply_theme(self.settings.get("theme", "light"))
2689 self._apply_editor_font_size()
2690 self.node_scripts = {}
2691 QTimer.singleShot(10, self._initial_setup)
2692
2693 def _setup_ui(self):
2694 central_widget = QWidget()
2695 main_layout = QVBoxLayout(central_widget)
2696 main_layout.setContentsMargins(0, 0, 0, 0)
2697 self.main_splitter = QSplitter(Qt.Orientation.Horizontal)
2698 main_layout.addWidget(self.main_splitter)
2699 self.project_model = CustomFileSystemModel()
2700 self.project_model.setFilter(QDir.Filter.AllDirs | QDir.Filter.Files | QDir.Filter.NoDotAndDotDot)
2701 self.project_tree = QTreeView()
2702 self.project_tree.setModel(self.project_model)
2703 self.project_tree.setHeaderHidden(True)
2704 self.project_tree.hideColumn(1)
2705 self.project_tree.hideColumn(2)
2706 self.project_tree.hideColumn(3)
2707 self.project_tree.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
2708 self.main_splitter.addWidget(self.project_tree)
2709 self.right_splitter = QSplitter(Qt.Orientation.Vertical)
2710 self.main_splitter.addWidget(self.right_splitter)
2711 self.tab_widget = QTabWidget()
2712 self.tab_widget.setTabsClosable(True)
2713 self.tab_widget.setMovable(True)
2714 self.right_splitter.addWidget(self.tab_widget)
2715 self.console_manager = ConsoleManager(self)
2716 self.ai_chat_manager = AIChatManager(self.settings, self)
2717 self.console_widget = ConsoleWidget(self.console_manager, self.ai_chat_manager)
2718 self.right_splitter.addWidget(self.console_widget)
2719 self.main_splitter.setSizes([200, 800])
2720 self.right_splitter.setSizes([600, 200])
2721 self.setCentralWidget(central_widget)
2722 self.action_toggle_tree = QAction("Pokaż/Ukryj drzewko", self)
2723 self.action_toggle_tree.setCheckable(True)
2724 self.action_toggle_tree.setChecked(True)
2725 self.action_toggle_tree.triggered.connect(self._toggle_tree_panel)
2726 self.action_toggle_console = QAction("Pokaż/Ukryj konsolę i chat ai", self)
2727 self.action_toggle_console.setCheckable(True)
2728 self.action_toggle_console.setChecked(True)
2729 self.action_toggle_console.triggered.connect(self._toggle_console_panel)
2730 self._apply_view_settings()
2731
2732 def _apply_view_settings(self):
2733 """Stosuje ustawienia widoczności paneli z ustawień."""
2734 show_tree = self.settings.get("show_tree", True)
2735 show_console = self.settings.get("show_console", True)
2736 self.main_splitter.widget(0).setVisible(show_tree)
2737 self.right_splitter.widget(1).setVisible(show_console)
2738 self.action_toggle_tree.setChecked(show_tree)
2739 self.action_toggle_console.setChecked(show_console)
2740
2741 def _toggle_tree_panel(self, checked):
2742 self.main_splitter.widget(0).setVisible(checked)
2743 self.settings["show_tree"] = checked
2744 self._save_app_state()
2745
2746 def _toggle_console_panel(self, checked):
2747 self.right_splitter.widget(1).setVisible(checked)
2748 self.settings["show_console"] = checked
2749 self._save_app_state()
2750
2751 def _setup_menu(self):
2752 menu_bar = self.menuBar()
2753 file_menu = menu_bar.addMenu("&Plik")
2754 self.action_new_project = QAction(qta.icon('fa5s.folder-plus') if qta else QIcon(), "&Nowy projekt...", self)
2755 self.action_new_project.triggered.connect(self._new_project)
2756 file_menu.addAction(self.action_new_project)
2757 self.action_open_folder = QAction(qta.icon('fa5s.folder-open') if qta else QIcon(), "Otwórz &folder projektu...", self)
2758 self.action_open_folder.triggered.connect(lambda: self._open_project_folder())
2759 file_menu.addAction(self.action_open_folder)
2760 self.action_open_file = QAction(qta.icon('fa5s.file-code') if qta else QIcon(), "Otwórz &plik...", self)
2761 self.action_open_file.triggered.connect(self._open_file_dialog)
2762 file_menu.addAction(self.action_open_file)
2763 file_menu.addSeparator()
2764 self.recent_files_menu = QMenu("Ostatnio otwierane", self)
2765 file_menu.addMenu(self.recent_files_menu)
2766 file_menu.addSeparator()
2767 self.action_save = QAction(qta.icon('fa5s.save') if qta else QIcon(), "&Zapisz", self)
2768 self.action_save.setShortcut(QKeySequence.StandardKey.Save)
2769 self.action_save.triggered.connect(self._save_current_file)
2770 file_menu.addAction(self.action_save)
2771 self.action_save_as = QAction(qta.icon('fa5s.file-export') if qta else QIcon(), "Zapisz &jako...", self)
2772 self.action_save_as.setShortcut(QKeySequence.StandardKey.SaveAs)
2773 self.action_save_as.triggered.connect(self._save_current_file_as)
2774 file_menu.addAction(self.action_save_as)
2775 self.action_save_all = QAction(qta.icon('fa5s.save') if qta else QIcon(), "Zapisz wszys&tko", self)
2776 self.action_save_all.setShortcut(QKeySequence("Ctrl+Shift+S"))
2777 self.action_save_all.triggered.connect(self._save_all_files)
2778 file_menu.addAction(self.action_save_all)
2779 file_menu.addSeparator()
2780 self.action_close_file = QAction(qta.icon('fa5s.window-close') if qta else QIcon(), "Zamknij ak&tualny plik", self)
2781 self.action_close_file.triggered.connect(self._close_current_tab)
2782 file_menu.addAction(self.action_close_file)
2783 file_menu.addSeparator()
2784 self.action_exit = QAction(qta.icon('fa5s.door-open') if qta else QIcon(), "&Zakończ", self)
2785 self.action_exit.setShortcut(QKeySequence.StandardKey.Quit)
2786 self.action_exit.triggered.connect(self.close)
2787 file_menu.addAction(self.action_exit)
2788 edit_menu = menu_bar.addMenu("&Edycja")
2789 view_menu = menu_bar.addMenu("&Widok")
2790 self.action_toggle_tree = QAction(qta.icon('fa5s.sitemap') if qta else QIcon(), "Pokaż &drzewko plików", self)
2791 self.action_toggle_tree.setCheckable(True)
2792 self.action_toggle_tree.setChecked(self.settings.get("show_tree", True))
2793 self.action_toggle_tree.triggered.connect(self._toggle_tree_panel)
2794 view_menu.addAction(self.action_toggle_tree)
2795 self.action_toggle_console = QAction(qta.icon('fa5s.terminal') if qta else QIcon(), "Pokaż &konsolę i chat ai", self)
2796 self.action_toggle_console.setCheckable(True)
2797 self.action_toggle_console.setChecked(self.settings.get("show_console", True))
2798 self.action_toggle_console.triggered.connect(self._toggle_console_panel)
2799 view_menu.addAction(self.action_toggle_console)
2800 search_menu = menu_bar.addMenu("&Wyszukaj")
2801 self.action_find = QAction(qta.icon('fa5s.search') if qta else QIcon(), "&Znajdź...", self)
2802 self.action_find.setShortcut(QKeySequence.StandardKey.Find)
2803 self.action_find.triggered.connect(self._show_find_bar)
2804 search_menu.addAction(self.action_find)
2805 run_menu = menu_bar.addMenu("&Uruchom")
2806 self.action_run_file = QAction(qta.icon('fa5s.play') if qta else QIcon(), "&Uruchom aktualny plik", self)
2807 self.action_run_file.setShortcut(QKeySequence("F5"))
2808 self.action_run_file.triggered.connect(self._run_current_file)
2809 run_menu.addAction(self.action_run_file)
2810 tools_menu = menu_bar.addMenu("&Narzędzia")
2811 self.action_settings = QAction(qta.icon('fa5s.cog') if qta else QIcon(), "&Ustawienia...", self)
2812 self.action_settings.triggered.connect(self._show_settings_dialog)
2813 tools_menu.addAction(self.action_settings)
2814 self.action_package_manager = QAction(qta.icon('fa5s.box-open') if qta else QIcon(), "Menadżer pakietów", self)
2815 self.action_package_manager.triggered.connect(self._show_package_manager)
2816 tools_menu.addAction(self.action_package_manager)
2817 help_menu = menu_bar.addMenu("&Pomoc")
2818 self.action_about = QAction(qta.icon('fa5s.info-circle') if qta else QIcon(), "&O programie...", self)
2819 self.action_about.triggered.connect(self._show_about_dialog)
2820 help_menu.addAction(self.action_about)
2821
2822 def _setup_toolbar(self):
2823 toolbar = self.addToolBar("Główne narzędzia")
2824 toolbar.setMovable(False)
2825 toolbar.setIconSize(QSize(16, 16))
2826 toolbar.addAction(self.action_new_project)
2827 toolbar.addAction(self.action_open_folder)
2828 toolbar.addAction(self.action_open_file)
2829 toolbar.addSeparator()
2830 toolbar.addAction(self.action_save)
2831 toolbar.addAction(self.action_save_all)
2832 toolbar.addSeparator()
2833 self.run_toolbutton = QToolButton(self)
2834 self.run_toolbutton.setDefaultAction(self.action_run_file)
2835 self.run_toolbutton.setPopupMode(QToolButton.ToolButtonPopupMode.MenuButtonPopup)
2836 toolbar.addWidget(self.run_toolbutton)
2837 toolbar.addSeparator()
2838 self.search_input = QLineEdit(self)
2839 self.search_input.setPlaceholderText("Szukaj w pliku...")
2840 self.search_input.setClearButtonEnabled(True)
2841 self.search_input.returnPressed.connect(lambda: self._find_text(self.search_input.text(), 'next'))
2842 self.find_next_button = QPushButton("Znajdź dalej")
2843 self.find_next_button.clicked.connect(lambda: self._find_text(self.search_input.text(), 'next'))
2844 self.find_prev_button = QPushButton("Znajdź poprzedni")
2845 self.find_prev_button.clicked.connect(lambda: self._find_text(self.search_input.text(), 'previous'))
2846 toolbar.addWidget(self.search_input)
2847 toolbar.addWidget(self.find_next_button)
2848 toolbar.addWidget(self.find_prev_button)
2849 self.search_input.setVisible(False)
2850 self.find_next_button.setVisible(False)
2851 self.find_prev_button.setVisible(False)
2852
2853 def _setup_status_bar(self):
2854 self.statusBar().showMessage("Gotowy.")
2855
2856 def _setup_connections(self):
2857 self.project_tree.doubleClicked.connect(self._handle_tree_item_double_click)
2858 self.tab_widget.tabCloseRequested.connect(self._close_tab_by_index)
2859 self.tab_widget.currentChanged.connect(self._handle_tab_change)
2860 self.project_tree.customContextMenuRequested.connect(self._show_project_tree_context_menu)
2861
2862 def _initial_setup(self):
2863 initial_dir = self.recents.get("last_project_dir")
2864 if not initial_dir or not os.path.isdir(initial_dir):
2865 initial_dir = PROJECTS_DIR
2866 os.makedirs(PROJECTS_DIR, exist_ok=True)
2867 if os.path.isdir(initial_dir):
2868 self._open_project_folder(initial_dir)
2869 else:
2870 self.statusBar().showMessage("Brak domyślnego katalogu projektu. Otwórz folder ręcznie lub utwórz nowy.")
2871 self.project_model.setRootPath("")
2872 self.current_project_dir = None
2873 self._update_run_button_menu()
2874 recent_files = self.recents.get("open_files", [])
2875 QTimer.singleShot(200, lambda: self._reopen_files(recent_files))
2876 self._update_recent_files_menu()
2877
2878 def _load_app_state(self):
2879 try:
2880 if os.path.exists(SETTINGS_FILE):
2881 with open(SETTINGS_FILE, 'r', encoding='utf-8') as f:
2882 loaded_settings = json.load(f)
2883 self.settings.update({
2884 "theme": loaded_settings.get("theme", "light"),
2885 "python_path": loaded_settings.get("python_path", ""),
2886 "node_path": loaded_settings.get("node_path", ""),
2887 "show_tree": loaded_settings.get("show_tree", True),
2888 "show_console": loaded_settings.get("show_console", True),
2889 "editor_font_size": loaded_settings.get("editor_font_size", 10),
2890 "api_key": loaded_settings.get("api_key", os.getenv("XAI_API_KEY", "")),
2891 "gemini_api_key": loaded_settings.get("gemini_api_key", ""),
2892 "mistral_api_key": loaded_settings.get("mistral_api_key", ""),
2893 "ai_model": loaded_settings.get("ai_model", "grok-3"),
2894 "ai_provider": loaded_settings.get("ai_provider", "grok")
2895 })
2896 if os.path.exists(RECENTS_FILE):
2897 with open(RECENTS_FILE, 'r', encoding='utf-8') as f:
2898 loaded_recents = json.load(f)
2899 self.recents.update({
2900 "last_project_dir": loaded_recents.get("last_project_dir"),
2901 "open_files": loaded_recents.get("open_files", [])
2902 })
2903 except (json.JSONDecodeError, Exception) as e:
2904 print(f"Błąd podczas wczytywania stanu aplikacji: {e}", file=sys.stderr)
2905
2906 def _save_app_state(self):
2907 try:
2908 self.recents["open_files"] = list(self.open_files.keys())
2909 if self.current_project_dir and os.path.isdir(self.current_project_dir):
2910 self.recents["last_project_dir"] = os.path.normpath(self.current_project_dir)
2911 else:
2912 self.recents["last_project_dir"] = None
2913 with open(SETTINGS_FILE, 'w', encoding='utf-8') as f:
2914 json.dump(self.settings, f, indent=4)
2915 with open(RECENTS_FILE, 'w', encoding='utf-8') as f:
2916 normalized_open_files = [os.path.normpath(p) for p in self.recents["open_files"]]
2917 unique_open_files = []
2918 for p in normalized_open_files:
2919 if p not in unique_open_files:
2920 unique_open_files.append(p)
2921 self.recents["open_files"] = unique_open_files[:20]
2922 json.dump(self.recents, f, indent=4)
2923 except Exception as e:
2924 print(f"Błąd podczas zapisu stanu aplikacji: {e}", file=sys.stderr)
2925
2926 def closeEvent(self, event):
2927 unsaved_files = [path for path, editor in self.open_files.items() if editor.document().isModified()]
2928 if unsaved_files:
2929 msg_box = QMessageBox(self)
2930 msg_box.setIcon(QMessageBox.Icon.Warning)
2931 msg_box.setWindowTitle("Niezapisane zmiany")
2932 msg_box.setText(f"Masz niezapisane zmiany w {len(unsaved_files)} plikach.\nCzy chcesz zapisać przed zamknięciem?")
2933 msg_box.setStandardButtons(QMessageBox.StandardButton.Save | QMessageBox.StandardButton.Discard | QMessageBox.StandardButton.Cancel)
2934 msg_box.setDefaultButton(QMessageBox.StandardButton.Save)
2935 reply = msg_box.exec()
2936 if reply == QMessageBox.StandardButton.Save:
2937 if self._save_all_files():
2938 self._save_app_state()
2939 event.accept()
2940 else:
2941 event.ignore()
2942 elif reply == QMessageBox.StandardButton.Discard:
2943 for i in range(self.tab_widget.count() - 1, -1, -1):
2944 widget = self.tab_widget.widget(i)
2945 if hasattr(widget, 'document'):
2946 widget.document().setModified(False)
2947 self._close_tab_by_index(i)
2948 self._save_app_state()
2949 event.accept()
2950 else:
2951 event.ignore()
2952 else:
2953 self._save_app_state()
2954 event.accept()
2955
2956 def _new_project(self):
2957 dialog = NewProjectDialog(PROJECTS_DIR, self)
2958 if dialog.exec() == QDialog.DialogCode.Accepted:
2959 project_name = dialog.get_project_name()
2960 project_path = dialog.get_project_path()
2961 try:
2962 if os.path.exists(project_path):
2963 QMessageBox.warning(self, "Projekt już istnieje", f"Projekt o nazwie '{project_name}' już istnieje.")
2964 return
2965 os.makedirs(project_path)
2966 self.statusBar().showMessage(f"Utworzono nowy projekt: {project_name}")
2967 self._open_project_folder(project_path)
2968 except OSError as e:
2969 QMessageBox.critical(self, "Błąd tworzenia projektu", f"Nie można utworzyć katalogu projektu:\n{e}")
2970 self.statusBar().showMessage("Błąd tworzenia projektu.")
2971
2972 def _open_project_folder(self, path=None):
2973 if path is None:
2974 start_path = self.current_project_dir if self.current_project_dir else PROJECTS_DIR
2975 dialog_path = QFileDialog.getExistingDirectory(self, "Otwórz folder projektu", start_path)
2976 if not dialog_path:
2977 return
2978 path = dialog_path
2979 path = os.path.normpath(path)
2980 if not os.path.isdir(path):
2981 QMessageBox.critical(self, "Błąd", f"Wybrana ścieżka nie jest katalogiem lub nie istnieje:\n{path}")
2982 self.statusBar().showMessage(f"Błąd: Nie można otworzyć folderu: {path}")
2983 return
2984 if self.current_project_dir and self.current_project_dir != path:
2985 unsaved_files_count = sum(1 for editor in self.open_files.values() if editor.document().isModified())
2986 if unsaved_files_count > 0:
2987 reply = QMessageBox.question(self, "Niezapisane zmiany",
2988 f"Obecny projekt ma {unsaved_files_count} niezapisanych plików.\n"
2989 "Czy chcesz zapisać zmiany przed otwarciem nowego folderu?",
2990 QMessageBox.StandardButton.Save | QMessageBox.StandardButton.Discard | QMessageBox.StandardButton.Cancel)
2991 if reply == QMessageBox.StandardButton.Cancel:
2992 self.statusBar().showMessage("Otwieranie folderu anulowane.")
2993 return
2994 if reply == QMessageBox.StandardButton.Save:
2995 if not self._save_all_files():
2996 self.statusBar().showMessage("Otwieranie folderu anulowane (błąd zapisu).")
2997 return
2998 self._close_all_files()
2999 self.current_project_dir = path
3000 self.project_model.setRootPath(path)
3001 root_index = self.project_model.index(path)
3002 if not root_index.isValid():
3003 QMessageBox.critical(self, "Błąd", f"Nie można ustawić katalogu głównego drzewka dla ścieżki:\n{path}")
3004 self.statusBar().showMessage(f"Błąd ustawienia katalogu głównego: {path}")
3005 self.project_tree.setRootIndex(self.project_model.index(""))
3006 self.current_project_dir = None
3007 self.recents["open_files"] = [p for p in self.recents["open_files"] if not os.path.normpath(p).startswith(os.path.normpath(path) + os.sep)]
3008 self._update_recent_files_menu()
3009 self._save_app_state()
3010 self._update_run_button_menu()
3011 return
3012 self.project_tree.setRootIndex(root_index)
3013 self.setWindowTitle(f"Proste IDE - {os.path.basename(path)}")
3014 self.statusBar().showMessage(f"Otwarto folder: {path}")
3015 self._check_package_json(path)
3016 self.recents["last_project_dir"] = path
3017 self._save_app_state()
3018
3019 def _close_all_files(self):
3020 for file_path in list(self.open_files.keys()):
3021 editor_widget = self.open_files.get(file_path)
3022 if editor_widget:
3023 tab_index = self.tab_widget.indexOf(editor_widget)
3024 if tab_index != -1:
3025 if hasattr(editor_widget, 'document'):
3026 editor_widget.document().setModified(False)
3027 self.tab_widget.removeTab(tab_index)
3028 if file_path in self.open_files:
3029 del self.open_files[file_path]
3030 self.recents["open_files"] = []
3031 self._update_recent_files_menu()
3032
3033 def _open_file_dialog(self):
3034 start_path = self.current_project_dir if self.current_project_dir else PROJECTS_DIR
3035 file_path, _ = QFileDialog.getOpenFileName(self, "Otwórz plik", start_path, "Wszystkie pliki (*);;Pliki Pythona (*.py);;Pliki JavaScript (*.js);;Pliki HTML (*.html);;Pliki CSS (*.css);;Pliki C++ (*.c *.cpp *.h *.hpp);;Pliki INI (*.ini);;Pliki JSON (*.json)")
3036 if file_path:
3037 self._open_file(file_path)
3038
3039 def _open_file(self, file_path):
3040 file_path = os.path.normpath(file_path)
3041 if not os.path.exists(file_path) or not os.path.isfile(file_path):
3042 self.statusBar().showMessage(f"Błąd: Plik nie istnieje lub nie jest plikiem: {file_path}")
3043 if file_path in self.recents["open_files"]:
3044 self.recents["open_files"].remove(file_path)
3045 self._update_recent_files_menu()
3046 self._save_app_state()
3047 return
3048 if file_path in self.open_files:
3049 index = -1
3050 for i in range(self.tab_widget.count()):
3051 widget = self.tab_widget.widget(i)
3052 if self.open_files.get(file_path) is widget:
3053 index = i
3054 break
3055 if index != -1:
3056 self.tab_widget.setCurrentIndex(index)
3057 self.statusBar().showMessage(f"Plik {os.path.basename(file_path)} jest już otwarty.")
3058 if file_path in self.recents["open_files"]:
3059 self.recents["open_files"].remove(file_path)
3060 self.recents["open_files"].insert(0, file_path)
3061 self._update_recent_files_menu()
3062 self._save_app_state()
3063 return
3064 try:
3065 content = ""
3066 try:
3067 with open(file_path, 'r', encoding='utf-8') as f:
3068 content = f.read()
3069 except UnicodeDecodeError:
3070 try:
3071 with open(file_path, 'r', encoding='latin-1') as f:
3072 content = f.read()
3073 except Exception:
3074 with open(file_path, 'r', encoding='utf-8', errors='replace') as f:
3075 content = f.read()
3076 except Exception as e:
3077 QMessageBox.critical(self, "Błąd otwarcia pliku", f"Nie można odczytać pliku {os.path.basename(file_path)}:\n{e}")
3078 self.statusBar().showMessage(f"Błąd otwarcia pliku: {os.path.basename(file_path)}")
3079 return
3080 editor = QPlainTextEdit()
3081 editor.setPlainText(content)
3082 editor.setFont(self.base_editor_font)
3083 editor.document().setModified(False)
3084 editor.document().modificationChanged.connect(self._handle_modification_changed)
3085 language = self._get_language_from_path(file_path)
3086 highlighter = CodeSyntaxHighlighter(editor.document(), language)
3087 setattr(editor.document(), '_syntax_highlighter', highlighter)
3088 tab_index = self.tab_widget.addTab(editor, os.path.basename(file_path))
3089 self.tab_widget.setCurrentIndex(tab_index)
3090 self.open_files[file_path] = editor
3091 self.statusBar().showMessage(f"Otwarto plik: {file_path}")
3092 if file_path in self.recents["open_files"]:
3093 self.recents["open_files"].remove(file_path)
3094 self.recents["open_files"].insert(0, file_path)
3095 self._update_recent_files_menu()
3096 self._save_app_state()
3097
3098 def _reopen_files(self, file_list):
3099 files_to_reopen = list(file_list)
3100 valid_files = [f for f in files_to_reopen if os.path.exists(f) and os.path.isfile(f)]
3101 self.recents["open_files"] = valid_files
3102 self._update_recent_files_menu()
3103 for file_path in valid_files:
3104 QTimer.singleShot(0, lambda path=file_path: self._open_file(path))
3105 invalid_files = [f for f in files_to_reopen if f not in valid_files]
3106 if invalid_files:
3107 msg = "Nie można ponownie otworzyć następujących plików (nie znaleziono):\n" + "\n".join(invalid_files)
3108 QMessageBox.warning(self, "Błąd otwarcia plików", msg)
3109
3110 def _update_recent_files_menu(self):
3111 self.recent_files_menu.clear()
3112 recent_items_to_show = list(self.recents.get("open_files", []))[:15]
3113 if not recent_items_to_show:
3114 self.recent_files_menu.addAction("Brak ostatnio otwieranych plików").setEnabled(False)
3115 return
3116 actions_to_add = []
3117 cleaned_recent_files = []
3118 for file_path in recent_items_to_show:
3119 if os.path.exists(file_path) and os.path.isfile(file_path):
3120 cleaned_recent_files.append(file_path)
3121 menu_text = os.path.basename(file_path)
3122 action = QAction(menu_text, self)
3123 action.setData(file_path)
3124 action.triggered.connect(lambda checked, path=file_path: self._open_file(path))
3125 actions_to_add.append(action)
3126 all_existing_recent_files = [p for p in self.recents.get("open_files", []) if os.path.exists(p) and os.path.isfile(p)]
3127 unique_recent_files = []
3128 for p in all_existing_recent_files:
3129 if p not in unique_recent_files:
3130 unique_recent_files.append(p)
3131 self.recents["open_files"] = unique_recent_files[:20]
3132 for action in actions_to_add:
3133 self.recent_files_menu.addAction(action)
3134 self._save_app_state()
3135
3136 def _show_project_tree_context_menu(self, point):
3137 index = self.project_tree.indexAt(point)
3138 menu = QMenu(self)
3139 if index.isValid():
3140 file_path = self.project_model.filePath(index)
3141 file_info = self.project_model.fileInfo(index)
3142 if file_info.isFile():
3143 open_action = QAction("Otwórz", self)
3144 open_action.triggered.connect(lambda: self._open_file(file_path))
3145 menu.addAction(open_action)
3146 # Dodaj opcję "Otwórz jako projekt" dla folderów
3147 if file_info.isDir():
3148 open_as_project_action = QAction("Otwórz jako projekt", self)
3149 open_as_project_action.triggered.connect(lambda: self._open_project_folder(file_path))
3150 menu.addAction(open_as_project_action)
3151 new_file_action = QAction("Nowy plik", self)
3152 new_file_action.triggered.connect(lambda: self._create_new_item(index, is_folder=False))
3153 menu.addAction(new_file_action)
3154 new_folder_action =QAction("Nowy folder", self)
3155 new_folder_action.triggered.connect(lambda: self._create_new_item(index, is_folder=True))
3156 menu.addAction(new_folder_action)
3157 rename_action = QAction("Zmień nazwę", self)
3158 rename_action.triggered.connect(lambda: self._rename_item(index))
3159 menu.addAction(rename_action)
3160 delete_action = QAction("Usuń", self)
3161 delete_action.triggered.connect(lambda: self._delete_item(index))
3162 menu.addAction(delete_action)
3163 if file_info.isFile():
3164 duplicate_action = QAction("Duplikuj", self)
3165 duplicate_action.triggered.connect(lambda: self._duplicate_file(index))
3166 menu.addAction(duplicate_action)
3167 else:
3168 new_file_action = QAction("Nowy plik", self)
3169 new_file_action.triggered.connect(lambda: self._create_new_item(None, is_folder=False))
3170 menu.addAction(new_file_action)
3171 new_folder_action = QAction("Nowy folder", self)
3172 new_folder_action.triggered.connect(lambda: self._create_new_item(None, is_folder=True))
3173 menu.addAction(new_folder_action)
3174 menu.exec(self.project_tree.mapToGlobal(point))
3175
3176 def _create_new_item(self, index, is_folder=False):
3177 parent_dir = self.current_project_dir
3178 if index and index.isValid():
3179 file_path = self.project_model.filePath(index)
3180 if self.project_model.fileInfo(index).isDir():
3181 parent_dir = file_path
3182 else:
3183 parent_dir = os.path.dirname(file_path)
3184 dialog = NewItemDialog(parent_dir, is_folder, self)
3185 if dialog.exec() == QDialog.DialogCode.Accepted:
3186 item_name = dialog.get_item_name()
3187 full_path = os.path.join(parent_dir, item_name)
3188 try:
3189 if is_folder:
3190 os.makedirs(full_path, exist_ok=True)
3191 else:
3192 with open(full_path, 'w', encoding='utf-8') as f:
3193 f.write('')
3194 self.statusBar().showMessage(f"Utworzono: {item_name}")
3195 parent_index = self.project_model.index(parent_dir)
3196 if parent_index.isValid():
3197 self.project_model.refresh(parent_index)
3198 except OSError as e:
3199 QMessageBox.critical(self, "Błąd tworzenia", f"Nie można utworzyć {item_name}:\n{e}")
3200 self.statusBar().showMessage("Błąd tworzenia.")
3201
3202 def _rename_item(self, index):
3203 if not index.isValid():
3204 return
3205 current_path = self.project_model.filePath(index)
3206 dialog = RenameItemDialog(current_path, self)
3207 if dialog.exec() == QDialog.DialogCode.Accepted:
3208 new_name = dialog.get_new_name()
3209 if self.project_model.rename(index, new_name):
3210 self.statusBar().showMessage(f"Zmieniono nazwę na: {new_name}")
3211 else:
3212 QMessageBox.critical(self, "Błąd zmiany nazwy", f"Nie można zmienić nazwy na '{new_name}'.")
3213 self.statusBar().showMessage("Błąd zmiany nazwy.")
3214
3215 def _delete_item(self, index):
3216 if not index.isValid():
3217 return
3218 file_path = self.project_model.filePath(index)
3219 file_info = self.project_model.fileInfo(index)
3220 item_name = file_info.fileName()
3221 is_dir = file_info.isDir()
3222 open_files_to_close = []
3223 if is_dir:
3224 for open_file_path in self.open_files:
3225 if os.path.normpath(open_file_path).startswith(os.path.normpath(file_path) + os.sep):
3226 open_files_to_close.append(open_file_path)
3227 else:
3228 if file_path in self.open_files:
3229 open_files_to_close.append(file_path)
3230 if open_files_to_close:
3231 reply_close = QMessageBox.question(self, "Otwarte pliki",
3232 f"Element '{item_name}' zawiera {len(open_files_to_close)} otwartych plików.\n"
3233 f"Czy chcesz zamknąć te pliki, aby kontynuować usuwanie '{item_name}'?",
3234 QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
3235 if reply_close == QMessageBox.StandardButton.No:
3236 self.statusBar().showMessage(f"Usuwanie '{item_name}' anulowane.")
3237 return
3238 unsaved_open_files = [p for p in open_files_to_close if self.open_files.get(p) and self.open_files[p].document().isModified()]
3239 if unsaved_open_files:
3240 save_reply = QMessageBox.question(self, "Niezapisane zmiany",
3241 f"Niektóre z plików ({len(unsaved_open_files)}) mają niezapisane zmiany. Czy chcesz je zapisać przed zamknięciem i usunięciem?",
3242 QMessageBox.StandardButton.Save | QMessageBox.StandardButton.Discard | QMessageBox.StandardButton.Cancel)
3243 if save_reply == QMessageBox.StandardButton.Cancel:
3244 self.statusBar().showMessage("Usuwanie anulowane (niezapisane zmiany).")
3245 return
3246 if save_reply == QMessageBox.StandardButton.Save:
3247 save_success = True
3248 for file_path_to_save in unsaved_open_files:
3249 editor = self.open_files.get(file_path_to_save)
3250 if editor and not self._save_file(editor, file_path_to_save):
3251 save_success = False
3252 break
3253 if not save_success:
3254 self.statusBar().showMessage("Usuwanie anulowane (błąd zapisu otwartych plików).")
3255 return
3256 for file_path_to_close in reversed(open_files_to_close):
3257 editor_widget = self.open_files.get(file_path_to_close)
3258 if editor_widget:
3259 tab_index = self.tab_widget.indexOf(editor_widget)
3260 if tab_index != -1:
3261 if hasattr(editor_widget, 'document'):
3262 editor_widget.document().setModified(False)
3263 self.tab_widget.removeTab(tab_index)
3264 del self.open_files[file_path_to_close]
3265 editor_widget.deleteLater()
3266 self.recents["open_files"] = [p for p in self.recents["open_files"] if p not in open_files_to_close]
3267 self._update_recent_files_menu()
3268 self._save_app_state()
3269 item_type = "folder" if is_dir else "plik"
3270 reply = QMessageBox.question(self, f"Usuń {item_type}",
3271 f"Czy na pewno chcesz usunąć {item_type} '{item_name}'?\n"
3272 "Ta operacja jest nieodwracalna!",
3273 QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
3274 if reply == QMessageBox.StandardButton.Yes:
3275 success = self.project_model.remove(index)
3276 if success:
3277 self.statusBar().showMessage(f"Usunięto {item_type}: {item_name}")
3278 else:
3279 QMessageBox.critical(self, f"Błąd usuwania {item_type}", f"Nie można usunąć {item_type} '{item_name}'.")
3280 self.statusBar().showMessage(f"Błąd usuwania {item_type}.")
3281
3282 def _duplicate_file(self, index):
3283 if not index.isValid():
3284 return
3285 file_path = self.project_model.filePath(index)
3286 file_info = self.project_model.fileInfo(index)
3287 if not file_info.isFile():
3288 self.statusBar().showMessage("Można duplikować tylko pliki.")
3289 return
3290 parent_dir = os.path.dirname(file_path)
3291 old_name = os.path.basename(file_path)
3292 name, ext = os.path.splitext(old_name)
3293 suggested_name = f"{name}_kopia{ext}"
3294 counter = 1
3295 while os.path.exists(os.path.join(parent_dir, suggested_name)):
3296 counter += 1
3297 suggested_name = f"{name}_kopia{counter}{ext}"
3298 new_name, ok = QInputDialog.getText(self, "Duplikuj plik", f"Podaj nazwę dla kopii '{old_name}':",
3299 QLineEdit.EchoMode.Normal, suggested_name)
3300 if ok and new_name:
3301 new_name = new_name.strip()
3302 if not new_name or re.search(r'[<>:"/\\|?*\x00-\x1F]', new_name) is not None:
3303 QMessageBox.warning(self, "Nieprawidłowa nazwa", "Podana nazwa jest pusta lub zawiera niedozwolone znaki.")
3304 self.statusBar().showMessage("Duplikowanie anulowane (nieprawidłowa nazwa).")
3305 return
3306 new_path = os.path.join(parent_dir, new_name)
3307 if os.path.exists(new_path):
3308 QMessageBox.warning(self, "Element już istnieje", f"Element o nazwie '{new_name}' już istnieje.")
3309 self.statusBar().showMessage("Duplikowanie anulowane (element już istnieje).")
3310 return
3311 try:
3312 os.makedirs(os.path.dirname(new_path), exist_ok=True)
3313 shutil.copy2(file_path, new_path)
3314 self.statusBar().showMessage(f"Utworzono kopię: {new_name}")
3315 parent_index = self.project_model.index(parent_dir)
3316 if parent_index.isValid():
3317 self.project_model.refresh(parent_index)
3318 else:
3319 root_path = self.project_model.rootPath()
3320 if root_path and os.path.isdir(root_path):
3321 self.project_model.refresh(self.project_model.index(root_path))
3322 except OSError as e:
3323 QMessageBox.critical(self, "Błąd duplikowania", f"Nie można zduplikować pliku '{old_name}':\n{e}")
3324 self.statusBar().showMessage("Błąd duplikowania pliku.")
3325
3326 def _close_tab_by_index(self, index):
3327 if index == -1:
3328 return
3329 widget = self.tab_widget.widget(index)
3330 if widget is None:
3331 return
3332 file_path_before_save = None
3333 for path, editor_widget in list(self.open_files.items()):
3334 if editor_widget is widget:
3335 file_path_before_save = path
3336 break
3337 if hasattr(widget, 'document') and widget.document().isModified():
3338 msg_box = QMessageBox(self)
3339 msg_box.setIcon(QMessageBox.Icon.Warning)
3340 msg_box.setWindowTitle("Niezapisane zmiany")
3341 tab_text = self.tab_widget.tabText(index).rstrip('*')
3342 display_name = os.path.basename(file_path_before_save) if file_path_before_save else tab_text
3343 msg_box.setText(f"Plik '{display_name}' ma niezapisane zmiany.\nCzy chcesz zapisać przed zamknięciem?")
3344 msg_box.setStandardButtons(QMessageBox.StandardButton.Save | QMessageBox.StandardButton.Discard | QMessageBox.StandardButton.Cancel)
3345 msg_box.setDefaultButton(QMessageBox.StandardButton.Save)
3346 reply = msg_box.exec()
3347 if reply == QMessageBox.StandardButton.Save:
3348 needs_save_as = (file_path_before_save is None or
3349 not os.path.exists(file_path_before_save) or
3350 not QFileInfo(file_path_before_save).isFile())
3351 if needs_save_as:
3352 original_index = self.tab_widget.currentIndex()
3353 self.tab_widget.setCurrentIndex(index)
3354 save_success = self._save_current_file_as()
3355 if original_index != -1 and original_index < self.tab_widget.count():
3356 self.tab_widget.setCurrentIndex(original_index)
3357 if not save_success:
3358 self.statusBar().showMessage(f"Zamknięcie anulowane (błąd zapisu '{display_name}').")
3359 return
3360 else:
3361 if not self._save_file(widget, file_path_before_save):
3362 self.statusBar().showMessage(f"Zamknięcie anulowane (błąd zapisu '{display_name}').")
3363 return
3364 elif reply == QMessageBox.StandardButton.Cancel:
3365 self.statusBar().showMessage(f"Zamknięcie '{tab_text}' anulowane.")
3366 return
3367 if file_path_before_save in self.open_files:
3368 del self.open_files[file_path_before_save]
3369 if file_path_before_save in self.recents["open_files"]:
3370 self.recents["open_files"].remove(file_path_before_save)
3371 self._update_recent_files_menu()
3372 self.tab_widget.removeTab(index)
3373 widget.deleteLater()
3374 if file_path_before_save:
3375 self.statusBar().showMessage(f"Zamknięto plik: {os.path.basename(file_path_before_save)}")
3376 else:
3377 self.statusBar().showMessage("Zamknięto plik.")
3378 self._save_app_state()
3379
3380 def _close_current_tab(self):
3381 current_index = self.tab_widget.currentIndex()
3382 if current_index != -1:
3383 self._close_tab_by_index(current_index)
3384
3385 def _save_current_file(self):
3386 current_widget = self.tab_widget.currentWidget()
3387 if not isinstance(current_widget, QPlainTextEdit):
3388 self.statusBar().showMessage("Brak aktywnego pliku do zapisu.")
3389 return False
3390 file_path = None
3391 for path, editor_widget in list(self.open_files.items()):
3392 if editor_widget is current_widget:
3393 file_path = path
3394 break
3395 is_existing_valid_file = file_path and os.path.exists(file_path) and QFileInfo(file_path).isFile()
3396 if is_existing_valid_file:
3397 return self._save_file(current_widget, file_path)
3398 else:
3399 return self._save_current_file_as()
3400
3401 def _save_current_file_as(self):
3402 current_widget = self.tab_widget.currentWidget()
3403 if not isinstance(current_widget, QPlainTextEdit):
3404 self.statusBar().showMessage("Brak aktywnego pliku do zapisu.")
3405 return False
3406 old_file_path = None
3407 for path, editor_widget in list(self.open_files.items()):
3408 if editor_widget is current_widget:
3409 old_file_path = path
3410 break
3411 suggested_name = "bez_nazwy.txt"
3412 current_tab_index = self.tab_widget.indexOf(current_widget)
3413 if current_tab_index != -1:
3414 original_tab_text = self.tab_widget.tabText(current_tab_index).rstrip('*')
3415 if original_tab_text and original_tab_text != "Nowy plik":
3416 suggested_name = original_tab_text
3417 elif current_widget.document().toPlainText().strip():
3418 first_line = current_widget.document().toPlainText().strip().split('\n')[0].strip()
3419 if first_line:
3420 suggested_name = re.sub(r'[\\/:*?"<>|]', '_', first_line)
3421 suggested_name = suggested_name[:50].strip()
3422 if not suggested_name:
3423 suggested_name = "bez_nazwy"
3424 if '.' not in os.path.basename(suggested_name):
3425 suggested_name += ".txt"
3426 else:
3427 suggested_name = "bez_nazwy.txt"
3428 start_path = self.current_project_dir if self.current_project_dir else PROJECTS_DIR
3429 if old_file_path and os.path.dirname(old_file_path):
3430 start_path = os.path.dirname(old_file_path)
3431 elif os.path.isdir(start_path):
3432 pass
3433 else:
3434 start_path = os.path.expanduser("~")
3435 file_filters = "Wszystkie pliki (*);;Pliki Pythona (*.py);;Pliki JavaScript (*.js);;Pliki HTML (*.html);;Pliki CSS (*.css);;Pliki C++ (*.c *.cpp *.h *.hpp);;Pliki INI (*.ini);;Pliki JSON (*.json)"
3436 new_file_path, _ = QFileDialog.getSaveFileName(self, "Zapisz plik jako...", os.path.join(start_path, suggested_name), file_filters)
3437 if not new_file_path:
3438 self.statusBar().showMessage("Zapisywanie anulowane.")
3439 return False
3440 new_file_path = os.path.normpath(new_file_path)
3441 if old_file_path and old_file_path != new_file_path:
3442 if old_file_path in self.open_files:
3443 del self.open_files[old_file_path]
3444 if old_file_path in self.recents["open_files"]:
3445 self.recents["open_files"].remove(old_file_path)
3446 self._update_recent_files_menu()
3447 self.open_files[new_file_path] = current_widget
3448 current_tab_index = self.tab_widget.indexOf(current_widget)
3449 if current_tab_index != -1:
3450 self.tab_widget.setTabText(current_tab_index, os.path.basename(new_file_path))
3451 if new_file_path in self.recents["open_files"]:
3452 self.recents["open_files"].remove(new_file_path)
3453 self.recents["open_files"].insert(0, new_file_path)
3454 self._update_recent_files_menu()
3455 language = self._get_language_from_path(new_file_path)
3456 old_highlighter = getattr(current_widget.document(), '_syntax_highlighter', None)
3457 if old_highlighter:
3458 old_highlighter.setDocument(None)
3459 new_highlighter = CodeSyntaxHighlighter(current_widget.document(), language)
3460 setattr(current_widget.document(), '_syntax_highlighter', new_highlighter)
3461 return self._save_file(current_widget, new_file_path)
3462
3463 def _save_file(self, editor_widget, file_path):
3464 if not file_path:
3465 print("Error: _save_file called with empty path.", file=sys.stderr)
3466 self.statusBar().showMessage("Błąd wewnętrzny: próba zapisu bez ścieżki.")
3467 return False
3468 try:
3469 os.makedirs(os.path.dirname(file_path), exist_ok=True)
3470 with open(file_path, 'w', encoding='utf-8') as f:
3471 f.write(editor_widget.toPlainText())
3472 editor_widget.document().setModified(False)
3473 self.statusBar().showMessage(f"Plik zapisano pomyślnie: {os.path.basename(file_path)}")
3474 tab_index = self.tab_widget.indexOf(editor_widget)
3475 if tab_index != -1:
3476 current_tab_text = self.tab_widget.tabText(tab_index).rstrip('*')
3477 self.tab_widget.setTabText(tab_index, current_tab_text)
3478 file_info = QFileInfo(file_path)
3479 dir_path = file_info.dir().path()
3480 root_path = self.project_model.rootPath()
3481 if root_path and dir_path.startswith(root_path):
3482 dir_index = self.project_model.index(dir_path)
3483 if dir_index.isValid():
3484 self.project_model.refresh(dir_index)
3485 file_index = self.project_model.index(file_path)
3486 if file_index.isValid():
3487 self.project_model.dataChanged.emit(file_index, file_index, [Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.DecorationRole])
3488 if file_path in self.recents["open_files"]:
3489 self.recents["open_files"].remove(file_path)
3490 self.recents["open_files"].insert(0, file_path)
3491 self._update_recent_files_menu()
3492 self._save_app_state()
3493 return True
3494 except Exception as e:
3495 QMessageBox.critical(self, "Błąd zapisu pliku", f"Nie można zapisać pliku {os.path.basename(file_path)}:\n{e}")
3496 self.statusBar().showMessage(f"Błąd zapisu pliku: {os.path.basename(file_path)}")
3497 return False
3498
3499 def _save_all_files(self):
3500 unsaved_files = [path for path, editor in self.open_files.items() if editor.document().isModified()]
3501 if not unsaved_files:
3502 self.statusBar().showMessage("Brak zmodyfikowanych plików do zapisu.")
3503 return True
3504 self.statusBar().showMessage(f"Zapisywanie wszystkich zmodyfikowanych plików ({len(unsaved_files)})...")
3505 total_saved = 0
3506 total_failed = 0
3507 files_to_save = list(unsaved_files)
3508 for file_path in files_to_save:
3509 editor_widget = self.open_files.get(file_path)
3510 if editor_widget is None or self.tab_widget.indexOf(editor_widget) == -1:
3511 print(f"Warning: Skipping save for {file_path} - editor widget not found or invalid.", file=sys.stderr)
3512 continue
3513 if not editor_widget.document().isModified():
3514 continue
3515 needs_save_as = (file_path is None or
3516 not os.path.exists(file_path) or
3517 not QFileInfo(file_path).isFile())
3518 save_success = False
3519 if needs_save_as:
3520 tab_index = self.tab_widget.indexOf(editor_widget)
3521 if tab_index != -1:
3522 original_index = self.tab_widget.currentIndex()
3523 self.tab_widget.setCurrentIndex(tab_index)
3524 save_success = self._save_current_file_as()
3525 if original_index != -1 and original_index < self.tab_widget.count():
3526 self.tab_widget.setCurrentIndex(original_index)
3527 else:
3528 print(f"Error: Cannot save '{os.path.basename(file_path if file_path else 'Nowy plik')}' (Save As needed) - widget not found in tabs.", file=sys.stderr)
3529 total_failed += 1
3530 continue
3531 else:
3532 save_success = self._save_file(editor_widget, file_path)
3533 if save_success:
3534 total_saved += 1
3535 else:
3536 total_failed += 1
3537 if total_saved > 0 and total_failed == 0:
3538 self.statusBar().showMessage(f"Zapisano pomyślnie wszystkie {total_saved} pliki.")
3539 return True
3540 elif total_saved > 0 and total_failed > 0:
3541 self.statusBar().showMessage(f"Zapisano {total_saved} plików, {total_failed} plików nie udało się zapisać.")
3542 QMessageBox.warning(self, "Błąd zapisu wszystkich plików", f"Nie udało się zapisać {total_failed} plików.")
3543 return False
3544 elif total_saved == 0 and total_failed > 0:
3545 self.statusBar().showMessage(f"Nie udało się zapisać żadnego z {total_failed} plików.")
3546 QMessageBox.critical(self, "Błąd zapisu wszystkich plików", f"Nie udało się zapisać żadnego z plików.")
3547 return False
3548 else:
3549 self.statusBar().showMessage("Brak zmodyfikowanych plików do zapisu.")
3550 return True
3551
3552 def _handle_modification_changed(self, modified):
3553 editor_document = self.sender()
3554 if not isinstance(editor_document, QTextDocument):
3555 return
3556 editor = None
3557 for editor_widget in self.open_files.values():
3558 if editor_widget.document() is editor_document:
3559 editor = editor_widget
3560 break
3561 if editor is None:
3562 return
3563 index = self.tab_widget.indexOf(editor)
3564 if index != -1:
3565 tab_text = self.tab_widget.tabText(index)
3566 if modified and not tab_text.endswith('*'):
3567 self.tab_widget.setTabText(index, tab_text + '*')
3568 elif not modified and tab_text.endswith('*'):
3569 self.tab_widget.setTabText(index, tab_text.rstrip('*'))
3570
3571 def _handle_tab_change(self, index):
3572 self._hide_find_bar()
3573 if index != -1:
3574 widget = self.tab_widget.widget(index)
3575 if isinstance(widget, QPlainTextEdit):
3576 file_path = next((path for path, ed in self.open_files.items() if ed is widget), None)
3577 if file_path:
3578 self.statusBar().showMessage(f"Edytujesz: {os.path.basename(file_path)}")
3579 else:
3580 self.statusBar().showMessage("Edytujesz: Nowy plik")
3581 else:
3582 self.statusBar().showMessage("Gotowy.")
3583
3584 def _find_text(self, text, direction='next'):
3585 editor = self.tab_widget.currentWidget()
3586 if not isinstance(editor, QPlainTextEdit):
3587 self.statusBar().showMessage("Brak aktywnego edytora do wyszukiwania.")
3588 return
3589 if not text:
3590 self.statusBar().showMessage("Wpisz tekst do wyszukiwania.")
3591 return
3592 flags = QTextDocument.FindFlag(0)
3593 if direction == 'previous':
3594 flags |= QTextDocument.FindFlag.FindBackward
3595 found = editor.find(text, flags)
3596 if found:
3597 self.statusBar().showMessage(f"Znaleziono '{text}'.")
3598 else:
3599 self.statusBar().showMessage(f"Nie znaleziono '{text}'. Zawijanie...")
3600 cursor = editor.textCursor()
3601 original_position = cursor.position()
3602 cursor.clearSelection()
3603 cursor.movePosition(cursor.MoveOperation.Start if direction == 'next' else cursor.MoveOperation.End)
3604 editor.setTextCursor(cursor)
3605 found_wrapped = editor.find(text, flags)
3606 if found_wrapped:
3607 self.statusBar().showMessage(f"Znaleziono '{text}' po zawinięciu.")
3608 else:
3609 self.statusBar().showMessage(f"Nie znaleziono '{text}' w całym pliku.")
3610 cursor.clearSelection()
3611 cursor.setPosition(original_position)
3612 editor.setTextCursor(cursor)
3613
3614 def _show_find_bar(self):
3615 if self.search_input.isVisible():
3616 self._hide_find_bar()
3617 return
3618 self.search_input.setVisible(True)
3619 self.find_next_button.setVisible(True)
3620 self.find_prev_button.setVisible(True)
3621 self.search_input.setFocus()
3622
3623 def _hide_find_bar(self):
3624 if self.search_input.isVisible():
3625 self.search_input.setVisible(False)
3626 self.find_next_button.setVisible(False)
3627 self.find_prev_button.setVisible(False)
3628 self.search_input.clear()
3629
3630 def _run_current_file(self):
3631 current_widget = self.tab_widget.currentWidget()
3632 if not isinstance(current_widget, QPlainTextEdit):
3633 self.console_widget.console.appendPlainText("Brak aktywnego pliku do uruchomienia.")
3634 self.statusBar().showMessage("Błąd: Żaden plik nie jest otwarty.")
3635 return
3636 file_path = next((path for path, editor_widget in self.open_files.items() if editor_widget is current_widget), None)
3637 if not file_path or not os.path.exists(file_path) or not os.path.isfile(file_path):
3638 self.console_widget.console.appendPlainText("Ścieżka aktywnego pliku jest nieprawidłowa lub plik nie istnieje.")
3639 self.statusBar().showMessage("Błąd: Plik nie istnieje.")
3640 return
3641 if current_widget.document().isModified():
3642 if not self._save_file(current_widget, file_path):
3643 self.console_widget.console.appendPlainText("Nie udało się zapisać pliku przed uruchomieniem.")
3644 self.statusBar().showMessage("Błąd: Nie zapisano pliku.")
3645 return
3646 language = self._get_language_from_path(file_path)
3647 working_dir = os.path.dirname(file_path) or self.current_project_dir or os.getcwd()
3648 command = None
3649 if language == "python":
3650 python_path = self.settings.get("python_path", "python")
3651 if not python_path:
3652 self.console_widget.console.appendPlainText("Błąd uruchamiania! Zainstaluj dodatek Python poprzez Menadżer Pakietów")
3653 self.statusBar().showMessage("Błąd: Brak interpretera Python.")
3654 return
3655 command = f'"{python_path}" "{file_path}"'
3656 elif language == "javascript":
3657 node_path = self.settings.get("node_path", "node")
3658 command = f'"{node_path}" "{file_path}"'
3659 elif language in ["c", "cpp"]:
3660 output_exe = os.path.splitext(file_path)[0] + (".exe" if platform.system() == "Windows" else "")
3661 compile_command = f'g++ "{file_path}" -o "{output_exe}"'
3662 self.console_widget.console.appendPlainText(f"Kompilowanie: {compile_command}")
3663 self.console_manager.run_command(compile_command, working_dir)
3664 # Czekaj na zakończenie kompilacji (może wymagać osobnego procesu)
3665 # Zakładam, że proces jest synchroniczny dla uproszczenia
3666 # Jeśli kompilacja się udała, uruchom program
3667 self.console_manager.run_command(f'"{output_exe}"', working_dir)
3668 return
3669 else:
3670 self.console_widget.console.appendPlainText(f"Uruchamianie nieobsługiwane dla języka: {language}")
3671 self.statusBar().showMessage(f"Błąd: Nie można uruchomić pliku {os.path.basename(file_path)}.")
3672 return
3673 if command:
3674 self.console_widget.console.appendPlainText(f"Uruchamianie: {command}")
3675 self.console_manager.run_command(command, working_dir, self.settings.get("python_path", ""), self.settings.get("node_path", ""))
3676 self.statusBar().showMessage(f"Uruchamianie: {os.path.basename(file_path)}")
3677
3678 def _get_language_from_path(self, file_path):
3679 return get_file_language(file_path)
3680
3681 def _apply_theme(self, theme_name):
3682 apply_theme(self, theme_name)
3683 self.settings["theme"] = theme_name
3684 self._save_app_state()
3685 self.statusBar().showMessage(f"Zastosowano motyw: {theme_name}")
3686
3687 def _apply_editor_font_size(self):
3688 font_size = self.settings.get("editor_font_size", 10)
3689 self.base_editor_font.setPointSize(font_size)
3690 for editor in self.open_files.values():
3691 editor.setFont(self.base_editor_font)
3692
3693 def _show_settings_dialog(self):
3694 dialog = SettingsDialog(self.settings, self)
3695 if dialog.exec() == QDialog.DialogCode.Accepted:
3696 new_settings = dialog.get_settings()
3697 self.settings.update(new_settings)
3698 self._apply_theme(self.settings["theme"])
3699 self._apply_editor_font_size()
3700 self.ai_chat_manager.update_settings(self.settings)
3701 self._save_app_state()
3702 self.statusBar().showMessage("Zapisano ustawienia.")
3703
3704 def _show_package_manager(self):
3705 if not self.current_project_dir:
3706 QMessageBox.warning(self, "Brak projektu", "Otwórz lub utwórz projekt, aby zarządzać pakietami.")
3707 return
3708 dialog = PackageManagerDialog(self.current_project_dir, self)
3709 dialog.exec()
3710 # Po zamknięciu menadżera pakietów wczytaj ponownie ustawienia z pliku settings.json
3711 try:
3712 if os.path.exists(SETTINGS_FILE):
3713 with open(SETTINGS_FILE, 'r', encoding='utf-8') as f:
3714 loaded_settings = json.load(f)
3715 # Automatyczne wyszukiwanie python.exe jeśli nie ma ścieżki lub jest pusta
3716 python_path = loaded_settings.get("python_path", "")
3717 if not python_path:
3718 import glob
3719 python_candidates = glob.glob(os.path.join(ROOT_DIR, "packages", "python", "**", "python.exe"), recursive=True)
3720 if python_candidates:
3721 python_path = python_candidates[0]
3722 loaded_settings["python_path"] = python_path
3723 # Zapisz poprawioną ścieżkę do settings.json
3724 with open(SETTINGS_FILE, 'w', encoding='utf-8') as fw:
3725 json.dump(loaded_settings, fw, indent=4)
3726 self.settings.update({
3727 "python_path": python_path or self.settings.get("python_path", ""),
3728 "node_path": loaded_settings.get("node_path", self.settings.get("node_path", "")),
3729 "theme": loaded_settings.get("theme", self.settings.get("theme", "light")),
3730 "editor_font_size": loaded_settings.get("editor_font_size", self.settings.get("editor_font_size", 10)),
3731 "show_tree": loaded_settings.get("show_tree", self.settings.get("show_tree", True)),
3732 "show_console": loaded_settings.get("show_console", self.settings.get("show_console", True)),
3733 "api_key": loaded_settings.get("api_key", self.settings.get("api_key", "")),
3734 "gemini_api_key": loaded_settings.get("gemini_api_key", self.settings.get("gemini_api_key", "")),
3735 "mistral_api_key": loaded_settings.get("mistral_api_key", self.settings.get("mistral_api_key", "")),
3736 "ai_model": loaded_settings.get("ai_model", self.settings.get("ai_model", "grok-3")),
3737 "ai_provider": loaded_settings.get("ai_provider", self.settings.get("ai_provider", "grok")),
3738 })
3739 self._apply_theme(self.settings.get("theme", "light"))
3740 self._apply_editor_font_size()
3741 except Exception as e:
3742 print(f"Błąd podczas ponownego wczytywania ustawień po menadżerze pakietów: {e}", file=sys.stderr)
3743
3744 def _show_about_dialog(self):
3745 QMessageBox.about(
3746 self,
3747 "O programie",
3748 "Proste IDE\nWersja 1.0\nStworzone dla zajebistych kodersów, którzy nie lubią komplikacji.\n© 2025 Paffcio & xAI"
3749 )
3750
3751 def _check_package_json(self, project_dir):
3752 self.node_scripts.clear()
3753 package_json_path = os.path.join(project_dir, "package.json")
3754 if os.path.exists(package_json_path):
3755 package_data = load_package_json(package_json_path)
3756 scripts = package_data.get("scripts", {})
3757 self.node_scripts.update(scripts)
3758 self._update_run_button_menu()
3759
3760 def _update_run_button_menu(self):
3761 menu = QMenu(self)
3762 menu.addAction(self.action_run_file)
3763 if self.node_scripts and self.current_project_dir:
3764 node_menu = menu.addMenu("Uruchom skrypt Node.js")
3765 node_path = self.settings.get("node_path", "node")
3766 for script_name in self.node_scripts:
3767 action = QAction(script_name, self)
3768 command = f'"{node_path}" run {script_name}'
3769 action.triggered.connect(
3770 lambda checked, cmd=command: self.process.start(cmd, working_dir=self.current_project_dir)
3771 )
3772 node_menu.addAction(action)
3773 self.run_toolbutton.setMenu(menu)
3774
3775 def _handle_tree_item_double_click(self, index):
3776 if not index.isValid():
3777 return
3778 file_path = self.project_model.filePath(index)
3779 file_info = self.project_model.fileInfo(index)
3780 if file_info.isFile():
3781 self._open_file(file_path)
3782
3783if __name__ == '__main__':
3784 app = QApplication(sys.argv)
3785 window = IDEWindow()
3786 window.show()
3787 sys.exit(app.exec())
3788
3789Ścieżka: /src/utils.py
3790Rozmiar: 1,71 KB
3791Zawartość:
3792import os
3793import json
3794import re
3795import sys
3796
3797
3798def load_package_json(folder_path):
3799 """Parsuje package.json i zwraca skrypty npm."""
3800 if not folder_path or not os.path.isdir(folder_path):
3801 return {}
3802 package_json_path = os.path.join(folder_path, 'package.json')
3803 scripts = {}
3804 if os.path.exists(package_json_path):
3805 try:
3806 with open(package_json_path, 'r', encoding='utf-8') as f:
3807 package_data = json.load(f)
3808 scripts = package_data.get('scripts', {})
3809 if not isinstance(scripts, dict):
3810 scripts = {}
3811 except (json.JSONDecodeError, Exception) as e:
3812 print(f"Błąd parsowania package.json: {e}", file=sys.stderr)
3813 return {}
3814 return scripts
3815
3816
3817def get_file_language(file_path):
3818 """Określa język programowania na podstawie rozszerzenia pliku."""
3819 extension = os.path.splitext(file_path)[1].lower()
3820 language_map = {
3821 '.py': 'python',
3822 '.pyw': 'python',
3823 '.js': 'javascript',
3824 '.ts': 'javascript',
3825 '.html': 'html',
3826 '.htm': 'html',
3827 '.css': 'css',
3828 '.c': 'c',
3829 '.cpp': 'cpp',
3830 '.cc': 'cpp',
3831 '.h': 'cpp',
3832 '.hpp': 'cpp',
3833 '.json': 'json',
3834 '.ini': 'ini',
3835 '.bat': 'batch',
3836 '.sh': 'bash',
3837 '.ps1': 'powershell',
3838 '.rb': 'ruby',
3839 '.java': 'java',
3840 '.go': 'go',
3841 '.rs': 'rust',
3842 '.php': 'php',
3843 '.xml': 'xml',
3844 '.md': 'markdown',
3845 '.txt': 'text',
3846 }
3847 return language_map.get(extension, 'text') # Domyślnie 'text' dla nieznanych
3848
3849__all__ = ['load_package_json', 'get_file_language']
3850
3851
3852// SKRYPT ZAKOŃCZONY: 18-05-2025 17:13:29
3853// RAPORT: Przetworzono 12 plików tekstowych, 0 nietekstowych, pominięto 1.
3854