Advertisement
PaffcioStudio

Untitled

May 18th, 2025
538
0
Never
1
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 182.55 KB | None | 0 0
  1.  
  2.  
  3. // FOLDER: /
  4. Ścieżka: /src/__init__.py
  5. Rozmiar: 0,03 KB
  6. Zawartość:
  7. # Plik inicjalizujący moduł src
  8.  
  9. Ścieżka: app.py:
  10. Zawartość:
  11. # Główny plik aplikacji IDE
  12. #/app.py
  13.  
  14. import sys
  15. from PyQt6.QtWidgets import QApplication
  16. from PyQt6.QtCore import QLocale
  17. from src.ui import IDEWindow
  18.  
  19. if __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
  27. Rozmiar: 7,52 KB
  28. Zawartość:
  29. # Konfiguracja i stałe aplikacji
  30. #/src/config.py
  31.  
  32. import os
  33. import re
  34. from PyQt6.QtGui import QTextCharFormat, QColor, QFont
  35.  
  36. APP_DIR = os.path.dirname(os.path.abspath(os.path.dirname(__file__)))
  37. DATA_DIR = os.path.join(APP_DIR, 'userdata')
  38. PROJECTS_DIR = os.path.join(APP_DIR, 'projects')
  39. SETTINGS_FILE = os.path.join(DATA_DIR, 'settings.json')
  40. RECENTS_FILE = os.path.join(DATA_DIR, 'recents.json')
  41. os.makedirs(DATA_DIR, exist_ok=True)
  42. os.makedirs(PROJECTS_DIR, exist_ok=True)
  43.  
  44. FORMAT_DEFAULT = QTextCharFormat()
  45. FORMAT_KEYWORD = QTextCharFormat()
  46. FORMAT_KEYWORD.setForeground(QColor("#000080")) # Navy
  47. FORMAT_STRING = QTextCharFormat()
  48. FORMAT_STRING.setForeground(QColor("#008000")) # Green
  49. FORMAT_COMMENT = QTextCharFormat()
  50. FORMAT_COMMENT.setForeground(QColor("#808080")) # Gray
  51. FORMAT_COMMENT.setFontItalic(True)
  52. FORMAT_FUNCTION = QTextCharFormat()
  53. FORMAT_FUNCTION.setForeground(QColor("#0000FF")) # Blue
  54. FORMAT_CLASS = QTextCharFormat()
  55. FORMAT_CLASS.setForeground(QColor("#A52A2A")) # Brown
  56. FORMAT_CLASS.setFontWeight(QFont.Weight.Bold)
  57. FORMAT_NUMBERS = QTextCharFormat()
  58. FORMAT_NUMBERS.setForeground(QColor("#FF0000")) # Red
  59. FORMAT_OPERATOR = QTextCharFormat()
  60. FORMAT_OPERATOR.setForeground(QColor("#A62929")) # Dark Red
  61. FORMAT_BUILTIN = QTextCharFormat()
  62. FORMAT_BUILTIN.setForeground(QColor("#008080")) # Teal
  63. FORMAT_SECTION = QTextCharFormat() # Dla sekcji w INI
  64. FORMAT_SECTION.setForeground(QColor("#800080")) # Purple
  65. FORMAT_SECTION.setFontWeight(QFont.Weight.Bold)
  66. FORMAT_PROPERTY = QTextCharFormat() # Dla kluczy/właściwości w INI/JSON
  67. FORMAT_PROPERTY.setForeground(QColor("#B8860B")) # DarkGoldenrod
  68.  
  69. HIGHLIGHTING_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
  182. Rozmiar: 23,59 KB
  183. Zawartość:
  184. # Zarządzanie konsolą i chatem AI
  185. # /src/console.py
  186.  
  187. import os
  188. import sys
  189. import shlex
  190. import json
  191. import requests
  192. import platform
  193. import markdown2
  194. from PyQt6.QtCore import QProcess, QProcessEnvironment, pyqtSignal, QObject, QTimer, QThread, pyqtSlot
  195. from PyQt6.QtGui import QTextCharFormat, QColor, QFont
  196. from PyQt6.QtWidgets import QPlainTextEdit, QLineEdit, QVBoxLayout, QWidget, QPushButton, QComboBox, QHBoxLayout, QTabWidget, QApplication, QTextEdit
  197.  
  198. class 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.  
  298. class 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.  
  320. class 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.  
  522. class 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
  682. Rozmiar: 12,94 KB
  683. Zawartość:
  684. # Dialogi dla IDE – tworzenie projektów, plików, zmiana nazw, ustawienia
  685. # /src/dialogs.py
  686.  
  687. import os
  688. import re
  689. import sys
  690. sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
  691. from PyQt6.QtWidgets import (
  692.     QDialog, QFormLayout, QLineEdit, QDialogButtonBox, QHBoxLayout,
  693.     QPushButton, QComboBox, QFileDialog, QLabel, QSpinBox, QVBoxLayout,
  694.     QTableWidget, QTableWidgetItem, QHeaderView, QMessageBox
  695. )
  696. from PyQt6.QtCore import Qt
  697. from PyQt6.QtGui import QFont
  698.  
  699. class 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.  
  743. class 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.  
  786. class 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.  
  820. class 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
  961. Rozmiar: 2,23 KB
  962. Zawartość:
  963. # Model systemu plików
  964. #/src/filesystem.py
  965.  
  966. import os
  967. from PyQt6.QtGui import QFileSystemModel
  968. from PyQt6.QtCore import Qt
  969. try:
  970.     import qtawesome as qta
  971. except ImportError:
  972.     qta = None
  973.  
  974. class 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
  1027. Rozmiar: 3,31 KB
  1028. Zawartość:
  1029. # Kolorowanie składni dla edytora
  1030. #/src/highlighter.py
  1031.  
  1032. import re
  1033. from PyQt6.QtGui import QSyntaxHighlighter, QTextDocument
  1034. from src.config import FORMAT_DEFAULT, FORMAT_COMMENT, HIGHLIGHTING_RULES
  1035.  
  1036. class 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
  1102. Rozmiar: 1,48 KB
  1103. Zawartość:
  1104. # Modele danych aplikacji
  1105. #/src/models.py
  1106.  
  1107. import os
  1108. import json
  1109. from src.config import SETTINGS_FILE, RECENTS_FILE
  1110.  
  1111. class 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
  1147. Rozmiar: 41,10 KB
  1148. Zawartość:
  1149. import os
  1150. import re
  1151. import requests
  1152. import zipfile
  1153. import tarfile
  1154. import json
  1155. import shutil
  1156. import subprocess
  1157. import py7zr
  1158. from PyQt6.QtWidgets import QDialog, QVBoxLayout, QLabel, QProgressBar, QTableWidget, QTableWidgetItem, QHeaderView, QPushButton, QApplication, QHBoxLayout, QWidget
  1159. from PyQt6.QtCore import QThread, pyqtSignal, Qt
  1160. from PyQt6.QtGui import QColor, QIcon
  1161. from 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
  1164. PACKAGES_DIR = os.path.abspath("packages")
  1165. os.makedirs(PACKAGES_DIR, exist_ok=True)
  1166.  
  1167. class 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.  
  1191. class 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.  
  1738. class 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.  
  1985. if __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
  1999. Rozmiar: 4,36 KB
  2000. Zawartość:
  2001. # Zarządzanie procesami zewnętrznymi
  2002. #/src/process.py
  2003.  
  2004. import os
  2005. import platform
  2006. import shlex
  2007. from PyQt6.QtCore import QProcess, QProcessEnvironment
  2008.  
  2009. class 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
  2097. Rozmiar: 12,98 KB
  2098. Zawartość:
  2099. # Zarządzanie motywami aplikacji – zajebiście stylowe! 😎
  2100. # /src/theme.py
  2101.  
  2102. from PyQt6.QtWidgets import QMainWindow, QApplication
  2103. from PyQt6.QtGui import QPalette, QColor
  2104.  
  2105. def 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.  
  2199. def 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.  
  2293. def 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.  
  2358. def 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.  
  2423. def 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
  2458. Rozmiar: 69,99 KB
  2459. Zawartość:
  2460. import sys
  2461. import os
  2462. import json
  2463. import subprocess
  2464. import re
  2465. import platform
  2466. import shutil
  2467. import shlex
  2468. from src.console import ConsoleWidget, ConsoleManager, AIChatManager
  2469. from 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. )
  2478. from PyQt6.QtGui import (
  2479.     QIcon, QAction, QKeySequence, QTextCharFormat, QFont,
  2480.     QSyntaxHighlighter, QTextDocument, QColor, QFileSystemModel,
  2481.     QDesktopServices, QPalette
  2482. )
  2483. from PyQt6.QtCore import (
  2484.     QDir, Qt, QProcess, QSettings, QFileInfo, QThread, pyqtSignal, QTimer, QSize,
  2485.     QStandardPaths, QUrl, QLocale, QCoreApplication, QProcessEnvironment
  2486. )
  2487. try:
  2488.     import qtawesome as qta
  2489. except ImportError:
  2490.     qta = None
  2491.     print("Zainstaluj qtawesome ('pip install qtawesome') dla lepszych ikon.", file=sys.stderr)
  2492.  
  2493. try:
  2494.     from src.dialogs import NewProjectDialog, NewItemDialog, RenameItemDialog, SettingsDialog
  2495.     from src.package_manager import PackageManagerDialog
  2496. except 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
  2501. from src.utils import load_package_json, get_file_language
  2502. from src.config import HIGHLIGHTING_RULES
  2503. from src.theme import apply_theme
  2504.  
  2505. # USTAWIENIE ROOT_DIR
  2506. ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
  2507. DATA_DIR = os.path.join(ROOT_DIR, 'userdata')
  2508. PROJECTS_DIR = os.path.join(ROOT_DIR, 'projects')
  2509. SETTINGS_FILE = os.path.join(DATA_DIR, 'settings.json')
  2510. RECENTS_FILE = os.path.join(DATA_DIR, 'recents.json')
  2511. os.makedirs(DATA_DIR, exist_ok=True)
  2512. os.makedirs(PROJECTS_DIR, exist_ok=True)
  2513.  
  2514. # Formatowanie podświetlania składni
  2515. FORMAT_DEFAULT = QTextCharFormat()
  2516. FORMAT_KEYWORD = QTextCharFormat()
  2517. FORMAT_KEYWORD.setForeground(QColor("#000080"))  # Navy
  2518. FORMAT_STRING = QTextCharFormat()
  2519. FORMAT_STRING.setForeground(QColor("#008000"))  # Green
  2520. FORMAT_COMMENT = QTextCharFormat()
  2521. FORMAT_COMMENT.setForeground(QColor("#808080"))  # Gray
  2522. FORMAT_COMMENT.setFontItalic(True)
  2523. FORMAT_FUNCTION = QTextCharFormat()
  2524. FORMAT_FUNCTION.setForeground(QColor("#0000FF"))  # Blue
  2525. FORMAT_CLASS = QTextCharFormat()
  2526. FORMAT_CLASS.setForeground(QColor("#A52A2A"))  # Brown
  2527. FORMAT_CLASS.setFontWeight(QFont.Weight.Bold)
  2528. FORMAT_NUMBERS = QTextCharFormat()
  2529. FORMAT_NUMBERS.setForeground(QColor("#FF0000"))  # Red
  2530. FORMAT_OPERATOR = QTextCharFormat()
  2531. FORMAT_OPERATOR.setForeground(QColor("#A62929"))  # Dark Red
  2532. FORMAT_BUILTIN = QTextCharFormat()
  2533. FORMAT_BUILTIN.setForeground(QColor("#008080"))  # Teal
  2534. FORMAT_SECTION = QTextCharFormat()
  2535. FORMAT_SECTION.setForeground(QColor("#800080"))  # Purple
  2536. FORMAT_SECTION.setFontWeight(QFont.Weight.Bold)
  2537. FORMAT_PROPERTY = QTextCharFormat()
  2538. FORMAT_PROPERTY.setForeground(QColor("#B8860B"))  # DarkGoldenrod
  2539.  
  2540. class 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.  
  2607. class 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.  
  2659. class 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.  
  3783. if __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
  3790. Rozmiar: 1,71 KB
  3791. Zawartość:
  3792. import os
  3793. import json
  3794. import re
  3795. import sys
  3796.  
  3797.  
  3798. def 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.  
  3817. def 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.  
Advertisement
Comments
  • dutmdh
    5 days
    # text 0.29 KB | 0 0
    1. Leon West Accidental Goblin King Audiobooks 1-4
    2.  
    3. magnet:?xt=urn:btih:49d386821d7a4093ac6209084242fbdf979b0ac1
    4. magnet:?xt=urn:btih:9f49b631081256fdab2d7b13927ce27bd44cf683
    5. magnet:?xt=urn:btih:6ef04c5cd32428d63afbca8a5b754688082059d3
    6. magnet:?xt=urn:btih:feae4390a335f0743bc8852d70183ace64240e1a
Add Comment
Please, Sign In to add comment
Advertisement