Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- // FOLDER: /
- Ścieżka: /src/__init__.py
- Rozmiar: 0,03 KB
- Zawartość:
- # Plik inicjalizujący moduł src
- Ścieżka: app.py:
- Zawartość:
- # Główny plik aplikacji IDE
- #/app.py
- import sys
- from PyQt6.QtWidgets import QApplication
- from PyQt6.QtCore import QLocale
- from src.ui import IDEWindow
- if __name__ == '__main__':
- app = QApplication(sys.argv)
- QLocale.setDefault(QLocale(QLocale.Language.Polish, QLocale.Country.Poland))
- main_window = IDEWindow()
- main_window.show()
- sys.exit(app.exec())
- Ścieżka: /src/config.py
- Rozmiar: 7,52 KB
- Zawartość:
- # Konfiguracja i stałe aplikacji
- #/src/config.py
- import os
- import re
- from PyQt6.QtGui import QTextCharFormat, QColor, QFont
- APP_DIR = os.path.dirname(os.path.abspath(os.path.dirname(__file__)))
- DATA_DIR = os.path.join(APP_DIR, 'userdata')
- PROJECTS_DIR = os.path.join(APP_DIR, 'projects')
- SETTINGS_FILE = os.path.join(DATA_DIR, 'settings.json')
- RECENTS_FILE = os.path.join(DATA_DIR, 'recents.json')
- os.makedirs(DATA_DIR, exist_ok=True)
- os.makedirs(PROJECTS_DIR, exist_ok=True)
- FORMAT_DEFAULT = QTextCharFormat()
- FORMAT_KEYWORD = QTextCharFormat()
- FORMAT_KEYWORD.setForeground(QColor("#000080")) # Navy
- FORMAT_STRING = QTextCharFormat()
- FORMAT_STRING.setForeground(QColor("#008000")) # Green
- FORMAT_COMMENT = QTextCharFormat()
- FORMAT_COMMENT.setForeground(QColor("#808080")) # Gray
- FORMAT_COMMENT.setFontItalic(True)
- FORMAT_FUNCTION = QTextCharFormat()
- FORMAT_FUNCTION.setForeground(QColor("#0000FF")) # Blue
- FORMAT_CLASS = QTextCharFormat()
- FORMAT_CLASS.setForeground(QColor("#A52A2A")) # Brown
- FORMAT_CLASS.setFontWeight(QFont.Weight.Bold)
- FORMAT_NUMBERS = QTextCharFormat()
- FORMAT_NUMBERS.setForeground(QColor("#FF0000")) # Red
- FORMAT_OPERATOR = QTextCharFormat()
- FORMAT_OPERATOR.setForeground(QColor("#A62929")) # Dark Red
- FORMAT_BUILTIN = QTextCharFormat()
- FORMAT_BUILTIN.setForeground(QColor("#008080")) # Teal
- FORMAT_SECTION = QTextCharFormat() # Dla sekcji w INI
- FORMAT_SECTION.setForeground(QColor("#800080")) # Purple
- FORMAT_SECTION.setFontWeight(QFont.Weight.Bold)
- FORMAT_PROPERTY = QTextCharFormat() # Dla kluczy/właściwości w INI/JSON
- FORMAT_PROPERTY.setForeground(QColor("#B8860B")) # DarkGoldenrod
- HIGHLIGHTING_RULES = {
- 'python': {
- 'keywords': ['and', 'as', 'assert', 'async', 'await', 'break', 'class', 'continue', 'def', 'del', 'elif', 'else',
- 'except', 'False', 'finally', 'for', 'from', 'global', 'if', 'import', 'in', 'is', 'lambda', 'None',
- 'nonlocal', 'not', 'or', 'pass', 'raise', 'return', 'True', 'try', 'while', 'with', 'yield'],
- 'builtins': ['print', 'len', 'range', 'list', 'dict', 'tuple', 'set', 'str', 'int', 'float', 'bool', 'open', 'isinstance'],
- 'patterns': [
- (r'\b[A-Za-z_][A-Za-z0-9_]*\s*\(', FORMAT_FUNCTION),
- (r'\bclass\s+([A-Za-z_][A-Za-z0-9_]*)\b', FORMAT_CLASS),
- (r'\b\d+(\.\d*)?\b', FORMAT_NUMBERS),
- (r'[+\-*/=<>!&|]', FORMAT_OPERATOR),
- (r'".*?"', FORMAT_STRING),
- (r"'.*?'", FORMAT_STRING),
- (r'#.*', FORMAT_COMMENT),
- ]
- },
- 'javascript': {
- 'keywords': ['abstract', 'arguments', 'await', 'boolean', 'break', 'byte', 'case', 'catch', 'char', 'class', 'const', 'continue',
- 'debugger', 'default', 'delete', 'do', 'double', 'else', 'enum', 'eval', 'export', 'extends', 'false', 'final',
- 'finally', 'float', 'for', 'function', 'goto', 'if', 'implements', 'import', 'in', 'instanceof', 'int', 'interface',
- 'let', 'long', 'native', 'new', 'null', 'package', 'private', 'protected', 'public', 'return', 'short', 'static',
- 'super', 'switch', 'synchronized', 'this', 'throw', 'throws', 'transient', 'true', 'try', 'typeof', 'var', 'void',
- 'volatile', 'while', 'with', 'yield'],
- 'builtins': ['console', 'log', 'warn', 'error', 'info', 'Math', 'Date', 'Array', 'Object', 'String', 'Number', 'Boolean', 'RegExp', 'JSON', 'Promise', 'setTimeout', 'setInterval'],
- 'patterns': [
- (r'\b[A-Za-z_][A-Za-z0-9_]*\s*\(', FORMAT_FUNCTION),
- (r'\bclass\s+([A-Za-z_][A-ZaZ0-9_]*)\b', FORMAT_CLASS),
- (r'\b\d+(\.\d*)?\b', FORMAT_NUMBERS),
- (r'[+\-*/=<>!&|]', FORMAT_OPERATOR),
- (r'".*?"', FORMAT_STRING),
- (r"'.*?'", FORMAT_STRING),
- (r'//.*', FORMAT_COMMENT),
- ]
- },
- 'html': {
- 'keywords': [],
- 'builtins': [],
- 'patterns': [
- (r'<[^>]+>', FORMAT_KEYWORD),
- (r'[a-zA-Z0-9_-]+\s*=', FORMAT_OPERATOR),
- (r'".*?"', FORMAT_STRING),
- (r"'.*?'", FORMAT_STRING),
- (r'&[a-zA-Z0-9]+;', FORMAT_BUILTIN),
- (r'<!--.*?-->', FORMAT_COMMENT, re.DOTALL),
- ]
- },
- 'css': {
- 'keywords': [],
- 'builtins': [],
- 'patterns': [
- (r'\.[a-zA-Z0-9_-]+', FORMAT_CLASS),
- (r'#[a-zA-Z0-9_-]+', FORMAT_BUILTIN),
- (r'[a-zA-Z0-9_-]+\s*:', FORMAT_KEYWORD),
- (r';', FORMAT_OPERATOR),
- (r'\{|\}', FORMAT_OPERATOR),
- (r'\(|\)', FORMAT_OPERATOR),
- (r'\b\d+(\.\d*)?(px|em|%|vh|vw|rem|pt|cm|mm)?\b', FORMAT_NUMBERS),
- (r'#[0-9a-fA-F]{3,6}', FORMAT_NUMBERS),
- (r'".*?"', FORMAT_STRING),
- (r"'.*?'", FORMAT_STRING),
- ]
- },
- 'c++': {
- 'keywords': ['alignas', 'alignof', 'and', 'and_eq', 'asm', 'atomic_cancel', 'atomic_commit', 'atomic_noexcept', 'auto',
- 'bitand', 'bitor', 'bool', 'break', 'case', 'catch', 'char', 'char8_t', 'char16_t', 'char32_t', 'class',
- 'compl', 'concept', 'const', 'consteval', 'constexpr', 'constinit', 'const_cast', 'continue', 'co_await',
- 'co_return', 'decltype', 'default', 'delete', 'do', 'double', 'dynamic_cast', 'else', 'enum',
- 'explicit', 'export', 'extern', 'false', 'float', 'for', 'friend', 'goto', 'if', 'inline', 'int', 'long',
- 'mutable', 'namespace', 'new', 'noexcept', 'not', 'not_eq', 'nullptr', 'operator', 'or', 'or_eq', 'private',
- 'protected', 'public', 'reflexpr', 'register', 'reinterpret_cast', 'requires', 'return', 'short', 'signed',
- 'sizeof', 'static', 'static_assert', 'static_cast', 'struct', 'switch', 'synchronized', 'template',
- 'this', 'thread_local', 'throw', 'true', 'try', 'typedef', 'typeid', 'typename', 'union', 'unsigned',
- 'using', 'virtual', 'void', 'volatile', 'wchar_t', 'while', 'xor', 'xor_eq'],
- 'builtins': ['cout', 'cin', 'endl', 'string', 'vector', 'map', 'set', 'array', 'queue', 'stack', 'pair', 'algorithm', 'iostream', 'fstream', 'sstream', 'cmath', 'cstdlib', 'cstdio', 'ctime'],
- 'patterns': [
- (r'\b[A-Za-z_][A-ZaZ0-9_]*\s*\(', FORMAT_FUNCTION),
- (r'\bclass\s+([A-Za-z_][A-ZaZ0-9_]*)\b', FORMAT_CLASS),
- (r'\bstruct\s+([A-Za-z_][A-ZaZ0-9_]*)\b', FORMAT_CLASS),
- (r'\b\d+(\.\d*)?\b', FORMAT_NUMBERS),
- (r'[+\-*/=<>!&|%^~?:]', FORMAT_OPERATOR),
- (r'".*?"', FORMAT_STRING),
- (r"'.*?'", FORMAT_STRING),
- (r'//.*', FORMAT_COMMENT),
- ]
- },
- 'ini': {
- 'keywords': [],
- 'builtins': [],
- 'patterns': [
- (r'^\[.*?\]', FORMAT_SECTION),
- (r'^[a-zA-Z0-9_-]+\s*=', FORMAT_PROPERTY),
- (r';.*', FORMAT_COMMENT),
- (r'#.*', FORMAT_COMMENT),
- (r'[+\-*/=<>!&|]', FORMAT_OPERATOR),
- (r'=\s*".*?"', FORMAT_STRING),
- (r"=\s*'.*?'", FORMAT_STRING),
- (r'=\s*[^;#"\'].*', FORMAT_STRING),
- ]
- },
- 'json': {
- 'keywords': ['true', 'false', 'null'],
- 'builtins': [],
- 'patterns': [
- (r'"(?:[^"\\]|\\.)*"\s*:', FORMAT_PROPERTY),
- (r'".*?"', FORMAT_STRING),
- (r'\b-?\d+(\.\d+)?([eE][+-]?\d+)?\b', FORMAT_NUMBERS),
- (r'\{|\}|\[|\]|:|,', FORMAT_OPERATOR),
- ]
- }
- }
- Ścieżka: /src/console.py
- Rozmiar: 23,59 KB
- Zawartość:
- # Zarządzanie konsolą i chatem AI
- # /src/console.py
- import os
- import sys
- import shlex
- import json
- import requests
- import platform
- import markdown2
- from PyQt6.QtCore import QProcess, QProcessEnvironment, pyqtSignal, QObject, QTimer, QThread, pyqtSlot
- from PyQt6.QtGui import QTextCharFormat, QColor, QFont
- from PyQt6.QtWidgets import QPlainTextEdit, QLineEdit, QVBoxLayout, QWidget, QPushButton, QComboBox, QHBoxLayout, QTabWidget, QApplication, QTextEdit
- class ConsoleManager(QObject):
- output_received = pyqtSignal(str, bool) # tekst, is_error
- status_updated = pyqtSignal(str)
- def __init__(self, parent=None):
- super().__init__(parent)
- self.process = QProcess(self)
- self.process.readyReadStandardOutput.connect(self._handle_stdout)
- self.process.readyReadStandardError.connect(self._handle_stderr)
- self.process.finished.connect(self._handle_process_finished)
- def run_command(self, command_text, working_dir, python_path="", node_path=""):
- if self.process.state() != QProcess.ProcessState.NotRunning:
- self.output_received.emit("Inny proces już działa. Zakończ go najpierw.", True)
- self.status_updated.emit("Błąd: Inny proces aktywny.")
- return
- try:
- command = shlex.split(command_text)
- except ValueError as e:
- self.output_received.emit(f"Błąd parsowania komendy: {e}", True)
- self.status_updated.emit("Błąd parsowania komendy.")
- return
- if not command:
- self.output_received.emit("Błąd: Pusta komenda.", True)
- self.status_updated.emit("Błąd: Pusta komenda.")
- return
- command_str = shlex.join(command)
- self.output_received.emit(f"Uruchamianie: {command_str}\nw katalogu: {working_dir}\n---", False)
- self.status_updated.emit("Proces uruchomiony...")
- try:
- self.process.setWorkingDirectory(working_dir)
- env = QProcessEnvironment.systemEnvironment()
- current_path = env.value("PATH", "")
- paths_to_prepend = []
- if python_path and os.path.exists(python_path):
- py_dir = os.path.dirname(python_path)
- current_path_dirs = [os.path.normcase(p) for p in current_path.split(os.pathsep) if p]
- if os.path.normcase(py_dir) not in current_path_dirs:
- paths_to_prepend.append(py_dir)
- if node_path and os.path.exists(node_path):
- node_dir = os.path.dirname(node_path)
- if os.path.normcase(node_dir) not in current_path_dirs:
- paths_to_prepend.append(node_dir)
- if paths_to_prepend:
- new_path = os.pathsep.join(paths_to_prepend) + (os.pathsep + current_path if current_path else "")
- env.insert("PATH", new_path)
- self.process.setProcessEnvironment(env)
- # Poprawka dla Windows: odpal komendy przez cmd.exe
- if platform.system() == "Windows":
- self.process.start("cmd.exe", ["/c", command_text])
- else:
- program = command[0]
- arguments = command[1:]
- self.process.start(program, arguments)
- if not self.process.waitForStarted(1000):
- error = self.process.errorString()
- self.output_received.emit(f"Nie udało się uruchomić '{command_text}': {error}", True)
- self.status_updated.emit(f"Błąd uruchamiania: {command_text}")
- except Exception as e:
- self.output_received.emit(f"Błąd podczas uruchamiania: {e}", True)
- self.status_updated.emit("Błąd uruchamiania.")
- def _handle_stdout(self):
- while self.process.bytesAvailable():
- data = self.process.readAllStandardOutput()
- try:
- text = bytes(data).decode('utf-8')
- except UnicodeDecodeError:
- text = bytes(data).decode('utf-8', errors='replace')
- self.output_received.emit(text, False)
- def _handle_stderr(self):
- while self.process.bytesAvailable():
- data = self.process.readAllStandardError()
- try:
- text = bytes(data).decode('utf-8')
- except UnicodeDecodeError:
- text = bytes(data).decode('utf-8', errors='replace')
- self.output_received.emit(text, True)
- def _handle_process_finished(self, exit_code, exit_status):
- self._handle_stdout()
- self._handle_stderr()
- self.output_received.emit("\n--- Zakończono proces ---", False)
- if exit_status == QProcess.ExitStatus.NormalExit and exit_code == 0:
- self.output_received.emit(f"Kod wyjścia: {exit_code}", False)
- self.status_updated.emit("Proces zakończony pomyślnie.")
- else:
- self.output_received.emit(f"Proces zakończony z błędem (kod: {exit_code}).", True)
- self.status_updated.emit(f"Błąd procesu. Kod wyjścia: {exit_code}")
- class AIWorker(QThread):
- result = pyqtSignal(str, bool)
- status = pyqtSignal(str)
- def __init__(self, ai_manager, message, file_content=None):
- super().__init__()
- self.ai_manager = ai_manager
- self.message = message
- self.file_content = file_content
- def run(self):
- try:
- # Dołącz plik do prompta jeśli jest
- if self.file_content:
- prompt = f"[Kontekst pliku poniżej]\n\n{self.file_content}\n\n[Twoja wiadomość]\n{self.message}"
- else:
- prompt = self.message
- response, is_error = self.ai_manager._send_message_internal(prompt)
- self.result.emit(response, is_error)
- self.status.emit("Otrzymano odpowiedź od AI.")
- except Exception as e:
- self.result.emit(f"Błąd komunikacji z AI: {e}", True)
- self.status.emit("Błąd komunikacji z AI.")
- class AIChatManager(QObject):
- output_received = pyqtSignal(str, bool) # tekst, is_error
- status_updated = pyqtSignal(str)
- models_updated = pyqtSignal(list) # lista modeli
- def __init__(self, settings, parent=None):
- super().__init__(parent)
- self.settings = settings
- self.api_key = settings.get("api_key", "")
- self.gemini_api_key = settings.get("gemini_api_key", "")
- self.mistral_api_key = settings.get("mistral_api_key", "")
- self.provider = settings.get("ai_provider", "grok")
- self.base_url = self._get_base_url(self.provider)
- self.current_model = settings.get("ai_model", "grok-3")
- self.conversation_history = []
- self.mistral_available = self._check_mistral_import()
- self._fetch_models()
- def _check_mistral_import(self):
- try:
- import mistralai
- # Sprawdź wersję
- from pkg_resources import get_distribution
- version = get_distribution("mistralai").version
- if version.startswith("0."):
- self.output_received.emit("Zła wersja mistralai! Potrzebujesz 1.0.0+, masz " + version, True)
- self.status_updated.emit("Zła wersja biblioteki mistralai!")
- return False
- return True
- except ImportError:
- self.output_received.emit("Biblioteka mistralai nie jest zainstalowana! Zainstaluj: pip install mistralai", True)
- return False
- def update_settings(self, settings):
- self.settings = settings
- self.api_key = settings.get("api_key", "")
- self.gemini_api_key = settings.get("gemini_api_key", "")
- self.mistral_api_key = settings.get("mistral_api_key", "")
- self.provider = settings.get("ai_provider", "grok")
- self.base_url = self._get_base_url(self.provider)
- self.current_model = settings.get("ai_model", "grok-3")
- self.mistral_available = self._check_mistral_import()
- self._fetch_models()
- def _get_base_url(self, provider):
- if provider == "grok":
- return "https://api.x.ai/v1"
- elif provider == "gemini":
- return "https://generativelanguage.googleapis.com/v1beta"
- elif provider == "mistral":
- return "https://api.mistral.ai/v1"
- return ""
- def _fetch_models(self):
- self.models_updated.emit([])
- if self.provider == "grok":
- try:
- headers = {"Authorization": f"Bearer {self.api_key}"}
- response = requests.get(f"{self.base_url}/models", headers=headers)
- response.raise_for_status()
- models_data = response.json()
- models = [model["id"] for model in models_data.get("data", []) if model.get("id")]
- if models:
- self.models_updated.emit(models)
- self.current_model = models[0] if models else "grok-3"
- self.status_updated.emit("Pobrano listę modeli AI (Grok).")
- else:
- self.output_received.emit("Brak dostępnych modeli w API Grok.", True)
- self.status_updated.emit("Błąd: Brak modeli AI Grok.")
- except Exception as e:
- self.output_received.emit(f"Błąd pobierania modeli Grok: {e}", True)
- self.status_updated.emit("Błąd pobierania modeli Grok.")
- elif self.provider == "gemini":
- try:
- models = [
- "gemini-1.5-flash-latest",
- "gemini-1.5-pro-latest",
- "gemini-2.0-flash-thinking-exp-1219",
- "gemini-2.5-flash-preview-04-17"
- ]
- self.models_updated.emit(models)
- self.current_model = models[0]
- self.status_updated.emit("Dostępne modele Gemini.")
- except Exception as e:
- self.output_received.emit(f"Błąd pobierania modeli Gemini: {e}", True)
- self.status_updated.emit("Błąd pobierania modeli Gemini.")
- elif self.provider == "mistral":
- if not self.mistral_available:
- self.models_updated.emit([])
- self.status_updated.emit("Biblioteka mistralai nie jest zainstalowana!")
- return
- try:
- from mistralai import Mistral
- client = Mistral(api_key=self.mistral_api_key)
- models_data = client.models.list()
- # Poprawka: models_data może być listą Model lub mieć atrybut .data
- if hasattr(models_data, 'data'):
- models = [m.id for m in models_data.data]
- else:
- models = [m.id for m in models_data]
- self.models_updated.emit(models)
- if models:
- self.current_model = models[0]
- self.status_updated.emit(f"Pobrano modele Mistral: {', '.join(models)}")
- else:
- self.output_received.emit("Brak dostępnych modeli w API Mistral.", True)
- self.status_updated.emit("Brak modeli Mistral.")
- except Exception as e:
- self.output_received.emit(f"Błąd pobierania modeli Mistral: {e}", True)
- self.status_updated.emit("Błąd pobierania modeli Mistral.")
- def set_model(self, model):
- self.current_model = model
- self.status_updated.emit(f"Zmieniono model na: {model}")
- def set_provider(self, provider):
- self.provider = provider
- self.base_url = self._get_base_url(self.provider)
- self.mistral_available = self._check_mistral_import()
- self._fetch_models()
- def send_message(self, message, file_content=None):
- if not message.strip():
- self.output_received.emit("Wiadomość nie może być pusta.", True)
- return
- self.conversation_history.append({"role": "user", "content": message})
- self.output_received.emit(f"Użytkownik: {message}", False)
- # Uruchom AIWorker w tle
- self.worker = AIWorker(self, message, file_content)
- self.worker.result.connect(self._handle_ai_result)
- self.worker.status.connect(self.status_updated)
- self.worker.start()
- def _handle_ai_result(self, text, is_error):
- if not is_error:
- self.conversation_history.append({"role": "assistant", "content": text})
- self.output_received.emit(f"AI: {text}", is_error)
- def _send_message_internal(self, prompt):
- # To jest wywoływane w wątku!
- try:
- if self.provider == "grok":
- headers = {
- "Authorization": f"Bearer {self.api_key}",
- "Content-Type": "application/json"
- }
- payload = {
- "model": self.current_model,
- "messages": self.conversation_history[:-1] + [{"role": "user", "content": prompt}],
- "max_tokens": 2048,
- "stream": False
- }
- response = requests.post(f"{self.base_url}/chat/completions", headers=headers, json=payload)
- response.raise_for_status()
- response_data = response.json()
- assistant_message = response_data["choices"][0]["message"]["content"]
- return assistant_message, False
- elif self.provider == "gemini":
- headers = {"Content-Type": "application/json"}
- params = {"key": self.gemini_api_key}
- payload = {
- "contents": [
- {"role": "user", "parts": [{"text": prompt}]}
- ]
- }
- url = f"{self.base_url}/models/{self.current_model}:generateContent"
- response = requests.post(url, headers=headers, params=params, json=payload)
- response.raise_for_status()
- response_data = response.json()
- try:
- assistant_message = response_data["candidates"][0]["content"]["parts"][0]["text"]
- except Exception:
- assistant_message = str(response_data)
- return assistant_message, False
- elif self.provider == "mistral":
- if not self.mistral_available:
- return ("Biblioteka mistralai nie jest zainstalowana! Zainstaluj: pip install mistralai", True)
- from mistralai import Mistral, UserMessage, AssistantMessage
- client = Mistral(api_key=self.mistral_api_key)
- messages = [
- UserMessage(content=msg["content"]) if msg["role"] == "user"
- else AssistantMessage(content=msg["content"])
- for msg in self.conversation_history[:-1]
- ]
- messages.append(UserMessage(content=prompt))
- response = client.chat.complete(
- model=self.current_model,
- messages=messages,
- max_tokens=2048
- )
- assistant_message = response.choices[0].message.content
- return assistant_message, False
- else:
- return ("Nieobsługiwany provider AI.", True)
- except Exception as e:
- return (f"Błąd komunikacji z API: {e}", True)
- def clear_conversation(self):
- self.conversation_history = []
- self.output_received.emit("Historia rozmowy wyczyszczona.", False)
- self.status_updated.emit("Wyczyszczono rozmowę.")
- class ConsoleWidget(QWidget):
- def __init__(self, console_manager, ai_chat_manager, parent=None):
- super().__init__(parent)
- self.console_manager = console_manager
- self.ai_chat_manager = ai_chat_manager
- self.current_file_path = None # Dodane: ścieżka do otwartego pliku
- self._setup_ui()
- self._setup_connections()
- def set_current_file(self, file_path):
- self.current_file_path = file_path
- def _setup_ui(self):
- layout = QVBoxLayout(self)
- layout.setContentsMargins(0, 0, 0, 0)
- self.tab_widget = QTabWidget()
- layout.addWidget(self.tab_widget)
- self.console_tab = QWidget()
- console_layout = QVBoxLayout(self.console_tab)
- console_layout.setContentsMargins(0, 0, 0, 0)
- self.console = QPlainTextEdit()
- self.console.setReadOnly(True)
- self.console.setFont(QFont("Courier New", 10))
- console_layout.addWidget(self.console, 1)
- self.console_input = QLineEdit()
- self.console_input.setPlaceholderText("Wpisz polecenie...")
- console_layout.addWidget(self.console_input, 0)
- console_buttons_layout = QHBoxLayout()
- console_buttons_layout.addStretch(1)
- self.clear_console_button = QPushButton("Wyczyść konsolę")
- console_buttons_layout.addWidget(self.clear_console_button)
- self.copy_console_button = QPushButton("Skopiuj")
- console_buttons_layout.addWidget(self.copy_console_button)
- console_layout.addLayout(console_buttons_layout)
- self.tab_widget.addTab(self.console_tab, "Konsola")
- # Zakładka Chat AI
- self.ai_tab = QWidget()
- ai_layout = QVBoxLayout(self.ai_tab)
- ai_layout.setContentsMargins(0, 0, 0, 0)
- # ZAMIANA: QTextEdit zamiast QPlainTextEdit
- self.ai_chat = QTextEdit()
- self.ai_chat.setReadOnly(True)
- self.ai_chat.setFont(QFont("Courier New", 10))
- ai_layout.addWidget(self.ai_chat, 1)
- ai_input_layout = QHBoxLayout()
- # Dodaj wybór providera AI
- self.ai_provider_combo = QComboBox()
- self.ai_provider_combo.addItems(["grok", "gemini", "mistral"])
- self.ai_provider_combo.setCurrentText(self.ai_chat_manager.provider)
- # Blokada wyboru Mistral jeśli nie ma biblioteki
- try:
- import mistralai
- mistral_ok = True
- except ImportError:
- mistral_ok = False
- idx = self.ai_provider_combo.findText("mistral")
- if idx != -1:
- self.ai_provider_combo.model().item(idx).setEnabled(mistral_ok)
- if not mistral_ok and self.ai_provider_combo.currentText() == "mistral":
- self.ai_provider_combo.setCurrentText("grok")
- ai_input_layout.addWidget(self.ai_provider_combo)
- self.ai_model_combo = QComboBox()
- self.ai_model_combo.setPlaceholderText("Wybierz model...")
- ai_input_layout.addWidget(self.ai_model_combo)
- self.ai_input = QLineEdit()
- self.ai_input.setPlaceholderText("Wpisz wiadomość do AI...")
- ai_input_layout.addWidget(self.ai_input)
- self.ai_send_button = QPushButton("Wyślij")
- ai_input_layout.addWidget(self.ai_send_button)
- self.ai_clear_button = QPushButton("Wyczyść")
- ai_input_layout.addWidget(self.ai_clear_button)
- ai_layout.addLayout(ai_input_layout)
- self.tab_widget.addTab(self.ai_tab, "Chat AI")
- def _setup_connections(self):
- self.console_manager.output_received.connect(self._append_console_output)
- self.console_input.returnPressed.connect(self._run_console_command)
- self.clear_console_button.clicked.connect(self.console.clear)
- self.copy_console_button.clicked.connect(self._copy_console)
- self.ai_chat_manager.output_received.connect(self._append_ai_output)
- self.ai_chat_manager.models_updated.connect(self._update_model_combo)
- self.ai_input.returnPressed.connect(self._send_ai_message)
- self.ai_send_button.clicked.connect(self._send_ai_message)
- self.ai_clear_button.clicked.connect(self.ai_chat_manager.clear_conversation)
- self.ai_model_combo.currentTextChanged.connect(self.ai_chat_manager.set_model)
- # Nowe: zmiana providera AI z poziomu UI
- self.ai_provider_combo.currentTextChanged.connect(self._on_provider_changed)
- def _on_provider_changed(self, provider):
- self.ai_chat_manager.set_provider(provider)
- # Po zmianie providera, pobierz modele tylko dla niego
- # (AIChatManager sam wywołuje _fetch_models, a sygnał models_updated odświeża model_combo)
- def _append_console_output(self, text, is_error):
- cursor = self.console.textCursor()
- cursor.movePosition(cursor.MoveOperation.End)
- fmt = QTextCharFormat()
- if is_error:
- fmt.setForeground(QColor("#DC143C"))
- cursor.setCharFormat(fmt)
- text_to_insert = text + ('\n' if text and not text.endswith('\n') else '')
- cursor.insertText(text_to_insert)
- self.console.setTextCursor(cursor)
- self.console.ensureCursorVisible()
- def _append_ai_output(self, text, is_error):
- # Jeśli to błąd, wyświetl na czerwono bez HTML
- if is_error:
- cursor = self.ai_chat.textCursor()
- cursor.movePosition(cursor.MoveOperation.End)
- fmt = QTextCharFormat()
- fmt.setForeground(QColor("#DC143C"))
- cursor.setCharFormat(fmt)
- text_to_insert = text + ('\n' if text and not text.endswith('\n') else '')
- cursor.insertText(text_to_insert)
- self.ai_chat.setTextCursor(cursor)
- self.ai_chat.ensureCursorVisible()
- return
- # Zamiana Markdown na HTML
- html = markdown2.markdown(text, extras=["fenced-code-blocks", "tables", "strike", "cuddled-lists", "code-friendly"])
- # Dodanie stylu dla bloków kodu i przycisku kopiowania
- html = html.replace('<code>', '<code style="background:#f6f8fa; border-radius:4px; padding:2px 4px;">')
- 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')
- html = html.replace('</code></pre>', '</code></pre></div>')
- self.ai_chat.moveCursor(self.ai_chat.textCursor().End)
- self.ai_chat.insertHtml(html + "<br>")
- self.ai_chat.ensureCursorVisible()
- def _copy_console(self):
- console_text = self.console.toPlainText()
- if console_text:
- QApplication.clipboard().setText(console_text)
- def _run_console_command(self):
- command = self.console_input.text().strip()
- if command:
- self.console_input.clear()
- self.console_manager.run_command(command, os.getcwd())
- def _send_ai_message(self):
- message = self.ai_input.text().strip()
- if message:
- self.ai_input.clear()
- file_content = None
- if self.current_file_path:
- try:
- with open(self.current_file_path, 'r', encoding='utf-8') as f:
- file_content = f.read()
- except Exception:
- file_content = None
- self.ai_chat_manager.send_message(message, file_content=file_content)
- def _update_model_combo(self, models):
- self.ai_model_combo.clear()
- if models:
- self.ai_model_combo.addItems(models)
- if self.ai_chat_manager.current_model in models:
- self.ai_model_combo.setCurrentText(self.ai_chat_manager.current_model)
- Ścieżka: /src/dialogs.py
- Rozmiar: 12,94 KB
- Zawartość:
- # Dialogi dla IDE – tworzenie projektów, plików, zmiana nazw, ustawienia
- # /src/dialogs.py
- import os
- import re
- import sys
- sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
- from PyQt6.QtWidgets import (
- QDialog, QFormLayout, QLineEdit, QDialogButtonBox, QHBoxLayout,
- QPushButton, QComboBox, QFileDialog, QLabel, QSpinBox, QVBoxLayout,
- QTableWidget, QTableWidgetItem, QHeaderView, QMessageBox
- )
- from PyQt6.QtCore import Qt
- from PyQt6.QtGui import QFont
- class NewProjectDialog(QDialog):
- def __init__(self, projects_dir, parent=None):
- super().__init__(parent)
- self.setWindowTitle("Nowy projekt")
- self.projects_dir = projects_dir
- self.setModal(True)
- layout = QFormLayout(self)
- self.name_edit = QLineEdit()
- layout.addRow("Nazwa projektu:", self.name_edit)
- self.button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
- self.button_box.button(QDialogButtonBox.StandardButton.Ok).setText("Utwórz")
- self.button_box.accepted.connect(self.accept)
- self.button_box.rejected.connect(self.reject)
- layout.addRow(self.button_box)
- self.name_edit.textChanged.connect(self._validate_name)
- self.name_edit.textChanged.emit(self.name_edit.text())
- def _validate_name(self, name):
- name = name.strip()
- is_empty = not name
- is_valid_chars = re.fullmatch(r'[a-zA-Z0-9_-]+', name) is not None or name == ""
- full_path = os.path.join(self.projects_dir, name)
- dir_exists = os.path.exists(full_path)
- enable_ok = not is_empty and is_valid_chars and not dir_exists
- self.button_box.button(QDialogButtonBox.StandardButton.Ok).setEnabled(enable_ok)
- if is_empty:
- self.name_edit.setToolTip("Nazwa projektu nie może być pusta.")
- elif not is_valid_chars:
- self.name_edit.setToolTip("Nazwa projektu może zawierać tylko litery, cyfry, podkreślenia i myślniki.")
- elif dir_exists:
- self.name_edit.setToolTip(f"Projekt o nazwie '{name}' już istnieje w:\n{self.projects_dir}")
- else:
- self.name_edit.setToolTip(f"Katalog projektu zostanie utworzony w:\n{full_path}")
- if not enable_ok and not is_empty:
- self.name_edit.setStyleSheet("background-color: #ffe0e0;")
- else:
- self.name_edit.setStyleSheet("")
- def get_project_name(self):
- return self.name_edit.text().strip()
- def get_project_path(self):
- return os.path.join(self.projects_dir, self.get_project_name())
- class NewItemDialog(QDialog):
- def __init__(self, parent_dir, is_folder=False, parent=None):
- super().__init__(parent)
- self.setWindowTitle("Nowy folder" if is_folder else "Nowy plik")
- self.parent_dir = parent_dir
- self.is_folder = is_folder
- self.setModal(True)
- layout = QFormLayout(self)
- self.item_type_label = "Nazwa folderu:" if is_folder else "Nazwa pliku:"
- self.name_edit = QLineEdit()
- layout.addRow(self.item_type_label, self.name_edit)
- self.button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
- self.button_box.accepted.connect(self.accept)
- self.button_box.rejected.connect(self.reject)
- layout.addRow(self.button_box)
- self.name_edit.textChanged.connect(self._validate_name)
- self.name_edit.textChanged.emit(self.name_edit.text())
- def _validate_name(self, name):
- name = name.strip()
- is_empty = not name
- illegal_chars_pattern = r'[<>:"/\\|?*\x00-\x1F]'
- is_valid_chars = re.search(illegal_chars_pattern, name) is None
- full_path = os.path.join(self.parent_dir, name)
- item_exists = os.path.exists(full_path)
- enable_create = not is_empty and is_valid_chars and not item_exists
- self.button_box.button(QDialogButtonBox.StandardButton.Ok).setEnabled(enable_create)
- if is_empty:
- self.name_edit.setToolTip(f"{self.item_type_label} nie może być pusta.")
- elif not is_valid_chars:
- self.name_edit.setToolTip("Nazwa zawiera niedozwolone znaki.")
- elif item_exists:
- self.name_edit.setToolTip(f"Element o nazwie '{name}' już istnieje w:\n{self.parent_dir}")
- else:
- self.name_edit.setToolTip("")
- if not enable_create and not is_empty:
- self.name_edit.setStyleSheet("background-color: #ffe0e0;")
- else:
- self.name_edit.setStyleSheet("")
- def get_item_name(self):
- return self.name_edit.text().strip()
- class RenameItemDialog(QDialog):
- def __init__(self, current_path, parent=None):
- super().__init__(parent)
- self.current_path = current_path
- self.is_folder = os.path.isdir(current_path)
- old_name = os.path.basename(current_path)
- self.setWindowTitle("Zmień nazwę")
- layout = QFormLayout(self)
- self.label = QLabel(f"Nowa nazwa dla '{old_name}':")
- self.line_edit = QLineEdit(old_name)
- layout.addRow(self.label, self.line_edit)
- self.button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
- self.button_box.accepted.connect(self.accept)
- self.button_box.rejected.connect(self.reject)
- layout.addRow(self.button_box)
- self.line_edit.textChanged.connect(self._validate_name)
- self._validate_name(self.line_edit.text())
- def _validate_name(self, name):
- name = name.strip()
- is_empty = not name
- illegal_chars_pattern = r'[<>:"/\\|?*\x00-\x1F]'
- is_valid_chars = re.search(illegal_chars_pattern, name) is None
- old_name = os.path.basename(self.current_path)
- is_same_name = name == old_name
- parent_dir = os.path.dirname(self.current_path)
- new_full_path = os.path.join(parent_dir, name)
- item_exists_at_new_path = os.path.exists(new_full_path)
- enable_ok = not is_empty and is_valid_chars and (is_same_name or not item_exists_at_new_path)
- self.button_box.button(QDialogButtonBox.StandardButton.Ok).setEnabled(enable_ok)
- def get_new_name(self):
- return self.line_edit.text().strip()
- class SettingsDialog(QDialog):
- def __init__(self, current_settings, parent=None):
- super().__init__(parent)
- self.setWindowTitle("Ustawienia IDE")
- self.current_settings = current_settings.copy()
- self.setMinimumWidth(400)
- self.setModal(True)
- self._setup_ui()
- def _setup_ui(self):
- layout = QFormLayout(self)
- layout.setContentsMargins(10, 10, 10, 10)
- layout.setSpacing(10)
- self.theme_combo = QComboBox()
- self.theme_combo.addItems(["light", "dark"])
- self.theme_combo.setCurrentText(self.current_settings.get("theme", "light"))
- layout.addRow("Motyw:", self.theme_combo)
- self.python_path_input = QLineEdit()
- self.python_path_input.setText(self.current_settings.get("python_path", ""))
- self.python_browse_button = QPushButton("Przeglądaj...")
- self.python_browse_button.clicked.connect(self._browse_python_path)
- python_layout = QHBoxLayout()
- python_layout.addWidget(self.python_path_input)
- python_layout.addWidget(self.python_browse_button)
- layout.addRow("Ścieżka Python:", python_layout)
- self.node_path_input = QLineEdit()
- self.node_path_input.setText(self.current_settings.get("node_path", ""))
- self.node_browse_button = QPushButton("Przeglądaj...")
- self.node_browse_button.clicked.connect(self._browse_node_path)
- node_layout = QHBoxLayout()
- node_layout.addWidget(self.node_path_input)
- node_layout.addWidget(self.node_browse_button)
- layout.addRow("Ścieżka Node.js:", node_layout)
- # Nowy wybór dostawcy AI
- self.ai_provider_combo = QComboBox()
- self.ai_provider_combo.addItems(["grok", "gemini", "mistral"])
- self.ai_provider_combo.setCurrentText(self.current_settings.get("ai_provider", "grok"))
- layout.addRow("Dostawca AI:", self.ai_provider_combo)
- # Klucz API xAI
- self.api_key_input = QLineEdit()
- self.api_key_input.setText(self.current_settings.get("api_key", ""))
- self.api_key_input.setEchoMode(QLineEdit.EchoMode.Password)
- layout.addRow("Klucz API xAI:", self.api_key_input)
- # Klucz API Gemini
- self.gemini_api_key_input = QLineEdit()
- self.gemini_api_key_input.setText(self.current_settings.get("gemini_api_key", ""))
- self.gemini_api_key_input.setEchoMode(QLineEdit.EchoMode.Password)
- layout.addRow("Klucz API Gemini:", self.gemini_api_key_input)
- # Klucz API Mistral
- self.mistral_api_key_input = QLineEdit()
- self.mistral_api_key_input.setText(self.current_settings.get("mistral_api_key", ""))
- self.mistral_api_key_input.setEchoMode(QLineEdit.EchoMode.Password)
- layout.addRow("Klucz API Mistral:", self.mistral_api_key_input)
- # Model AI – dynamicznie aktualizowany przez AIChatManager
- self.ai_model_combo = QComboBox()
- self.ai_model_combo.addItems([self.current_settings.get("ai_model", "grok-3")])
- self.ai_model_combo.setCurrentText(self.current_settings.get("ai_model", "grok-3"))
- layout.addRow("Model AI:", self.ai_model_combo)
- self.font_size_combo = QComboBox()
- self.font_size_combo.addItems([str(i) for i in range(8, 21)])
- self.font_size_combo.setCurrentText(str(self.current_settings.get("editor_font_size", 10)))
- layout.addRow("Rozmiar czcionki edytora:", self.font_size_combo)
- self.button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
- self.button_box.accepted.connect(self.accept)
- self.button_box.rejected.connect(self.reject)
- layout.addRow(self.button_box)
- # Zmiana widoczności kluczy API w zależności od dostawcy
- self.ai_provider_combo.currentTextChanged.connect(self._update_api_key_visibility)
- self._update_api_key_visibility(self.ai_provider_combo.currentText())
- # Blokada wyboru Mistral jeśli nie ma biblioteki
- self._block_mistral_if_missing()
- def _block_mistral_if_missing(self):
- try:
- import mistralai
- mistral_ok = True
- except ImportError:
- mistral_ok = False
- idx = self.ai_provider_combo.findText("mistral")
- if idx != -1:
- self.ai_provider_combo.model().item(idx).setEnabled(mistral_ok)
- if not mistral_ok and self.ai_provider_combo.currentText() == "mistral":
- self.ai_provider_combo.setCurrentText("grok")
- def _update_api_key_visibility(self, provider):
- self.api_key_input.setEnabled(provider == "grok")
- self.gemini_api_key_input.setEnabled(provider == "gemini")
- self.mistral_api_key_input.setEnabled(provider == "mistral")
- def _browse_python_path(self):
- # Wybierz Pythona, jakbyś wybierał psa na spacer 🐶
- file_path, _ = QFileDialog.getOpenFileName(
- self, "Wybierz plik wykonywalny Pythona", "",
- "Pliki wykonywalne (*.exe);;Wszystkie pliki (*)"
- )
- if file_path:
- self.python_path_input.setText(file_path)
- def _browse_node_path(self):
- # Node.js – dla fanów async chaosu 😜
- file_path, _ = QFileDialog.getOpenFileName(
- self, "Wybierz plik wykonywalny Node.js", "",
- "Pliki wykonywalne (*.exe);;Wszystkie pliki (*)"
- )
- if file_path:
- self.node_path_input.setText(file_path)
- def get_settings(self):
- # Zwraca ustawienia jak pizzę – wszystko, czego chciałeś 🍕
- api_key = self.api_key_input.text()
- gemini_api_key = self.gemini_api_key_input.text()
- mistral_api_key = self.mistral_api_key_input.text()
- # Jeśli przez przypadek pole zostało nadpisane funkcją, wymuś string
- if not isinstance(api_key, str):
- api_key = str(api_key)
- if not isinstance(gemini_api_key, str):
- gemini_api_key = str(gemini_api_key)
- if not isinstance(mistral_api_key, str):
- mistral_api_key = str(mistral_api_key)
- return {
- "theme": self.theme_combo.currentText(),
- "python_path": self.python_path_input.text().strip(),
- "node_path": self.node_path_input.text().strip(),
- "ai_provider": self.ai_provider_combo.currentText(),
- "api_key": api_key.strip(),
- "gemini_api_key": gemini_api_key.strip(),
- "mistral_api_key": mistral_api_key.strip(),
- "ai_model": self.ai_model_combo.currentText(),
- "editor_font_size": int(self.font_size_combo.currentText())
- }
- Ścieżka: /src/filesystem.py
- Rozmiar: 2,23 KB
- Zawartość:
- # Model systemu plików
- #/src/filesystem.py
- import os
- from PyQt6.QtGui import QFileSystemModel
- from PyQt6.QtCore import Qt
- try:
- import qtawesome as qta
- except ImportError:
- qta = None
- class CustomFileSystemModel(QFileSystemModel):
- def __init__(self, parent=None):
- super().__init__(parent)
- self.icon_map = {
- '.py': 'fa5s.file-code',
- '.js': 'fa5s.file-code',
- '.json': 'fa5s.file-code',
- '.html': 'fa5s.file-code',
- '.css': 'fa5s.file-code',
- '.ini': 'fa5s.file-alt',
- '.txt': 'fa5s.file-alt',
- '.md': 'fa5s.file-alt',
- '.c': 'fa5s.file-code',
- '.cpp': 'fa5s.file-code',
- '.h': 'fa5s.file-code',
- '.hpp': 'fa5s.file-code',
- }
- self.folder_icon_name = 'fa5s.folder'
- self.default_file_icon_name = 'fa5s.file'
- self._has_qtawesome = qta is not None
- def rename(self, index, new_name):
- if not index.isValid():
- return False
- old_path = self.filePath(index)
- new_path = os.path.join(os.path.dirname(old_path), new_name)
- try:
- os.rename(old_path, new_path)
- self.refresh()
- return True
- except Exception as e:
- print(f"Błąd zmiany nazwy: {e}")
- return False
- def data(self, index, role=Qt.ItemDataRole.DisplayRole):
- if not index.isValid():
- return None
- if role == Qt.ItemDataRole.DecorationRole:
- file_info = self.fileInfo(index)
- if file_info.isDir():
- return qta.icon(self.folder_icon_name) if self._has_qtawesome else super().data(index, role)
- elif file_info.isFile():
- extension = file_info.suffix().lower()
- dotted_extension = '.' + extension
- if dotted_extension in self.icon_map and self._has_qtawesome:
- return qta.icon(self.icon_map[dotted_extension])
- return qta.icon(self.default_file_icon_name) if self._has_qtawesome else super().data(index, role)
- return super().data(index, role)
- def refresh(self, *args):
- self.setRootPath(self.rootPath())
- Ścieżka: /src/highlighter.py
- Rozmiar: 3,31 KB
- Zawartość:
- # Kolorowanie składni dla edytora
- #/src/highlighter.py
- import re
- from PyQt6.QtGui import QSyntaxHighlighter, QTextDocument
- from src.config import FORMAT_DEFAULT, FORMAT_COMMENT, HIGHLIGHTING_RULES
- class CodeSyntaxHighlighter(QSyntaxHighlighter):
- def __init__(self, parent: QTextDocument, language: str):
- super().__init__(parent)
- self._language = language.lower()
- self._rules = []
- lang_config = HIGHLIGHTING_RULES.get(self._language, {})
- keywords = lang_config.get('keywords', [])
- builtins = lang_config.get('builtins', [])
- patterns = lang_config.get('patterns', [])
- for keyword in keywords:
- pattern = r'\b' + re.escape(keyword) + r'\b'
- self._rules.append((re.compile(pattern), lang_config.get('keyword_format', FORMAT_DEFAULT)))
- for builtin in builtins:
- pattern = r'\b' + re.escape(builtin) + r'\b'
- self._rules.append((re.compile(pattern), lang_config.get('builtin_format', FORMAT_DEFAULT)))
- for pattern_str, format, *flags in patterns:
- try:
- pattern = re.compile(pattern_str, *flags)
- self._rules.append((pattern, format))
- except re.error as e:
- print(f"Błąd regex '{pattern_str}' dla {self._language}: {e}")
- def highlightBlock(self, text: str):
- self.setFormat(0, len(text), FORMAT_DEFAULT)
- self.setCurrentBlockState(0)
- block_comment_delimiters = []
- if self._language in ['javascript', 'css', 'c++']:
- block_comment_delimiters.append(("/*", "*/", FORMAT_COMMENT))
- comment_start_in_prev_block = (self.previousBlockState() == 1)
- if comment_start_in_prev_block:
- end_delimiter_index = text.find("*/")
- if end_delimiter_index >= 0:
- self.setFormat(0, end_delimiter_index + 2, FORMAT_COMMENT)
- self.setCurrentBlockState(0)
- start_pos = end_delimiter_index + 2
- else:
- self.setFormat(0, len(text), FORMAT_COMMENT)
- self.setCurrentBlockState(1)
- return
- else:
- start_pos = 0
- start_delimiter = "/*"
- end_delimiter = "*/"
- startIndex = text.find(start_delimiter, start_pos)
- while startIndex >= 0:
- endIndex = text.find(end_delimiter, startIndex)
- if endIndex >= 0:
- length = endIndex - startIndex + len(end_delimiter)
- self.setFormat(startIndex, startIndex + length, FORMAT_COMMENT)
- startIndex = text.find(start_delimiter, startIndex + length)
- else:
- self.setFormat(startIndex, len(text) - startIndex, FORMAT_COMMENT)
- self.setCurrentBlockState(1)
- break
- for pattern, format in self._rules:
- if format == FORMAT_COMMENT and (pattern.pattern.startswith(re.escape('/*')) or pattern.pattern.startswith(re.escape('<!--'))):
- continue
- if format == FORMAT_COMMENT and pattern.pattern.startswith('//') and self.currentBlockState() == 1:
- continue
- for match in pattern.finditer(text):
- start, end = match.span()
- self.setFormat(start, end, format)
- Ścieżka: /src/models.py
- Rozmiar: 1,48 KB
- Zawartość:
- # Modele danych aplikacji
- #/src/models.py
- import os
- import json
- from src.config import SETTINGS_FILE, RECENTS_FILE
- class AppState:
- def __init__(self):
- self.settings = {
- "theme": "light",
- "python_path": "",
- "node_path": "",
- "show_tree": True,
- "show_console": True,
- "editor_font_size": 10
- }
- self.recents = {"last_project_dir": None, "open_files": []}
- def load(self):
- try:
- if os.path.exists(SETTINGS_FILE):
- with open(SETTINGS_FILE, 'r', encoding='utf-8') as f:
- self.settings.update(json.load(f))
- if os.path.exists(RECENTS_FILE):
- with open(RECENTS_FILE, 'r', encoding='utf-8') as f:
- self.recents.update(json.load(f))
- except Exception as e:
- print(f"Błąd wczytywania stanu: {e}")
- def save(self, open_files, project_dir):
- try:
- self.recents["open_files"] = list(open_files)
- if project_dir and os.path.isdir(project_dir):
- self.recents["last_project_dir"] = os.path.normpath(project_dir)
- with open(SETTINGS_FILE, 'w', encoding='utf-8') as f:
- json.dump(self.settings, f, indent=4)
- with open(RECENTS_FILE, 'w', encoding='utf-8') as f:
- json.dump(self.recents, f, indent=4)
- except Exception as e:
- print(f"Błąd zapisu stanu: {e}")
- Ścieżka: /src/package_manager.py
- Rozmiar: 41,10 KB
- Zawartość:
- import os
- import re
- import requests
- import zipfile
- import tarfile
- import json
- import shutil
- import subprocess
- import py7zr
- from PyQt6.QtWidgets import QDialog, QVBoxLayout, QLabel, QProgressBar, QTableWidget, QTableWidgetItem, QHeaderView, QPushButton, QApplication, QHBoxLayout, QWidget
- from PyQt6.QtCore import QThread, pyqtSignal, Qt
- from PyQt6.QtGui import QColor, QIcon
- from src.theme import get_dark_package_manager_stylesheet, get_light_package_manager_stylesheet
- # Ustaw katalog do przechowywania pakietów, tworząc go, jeśli nie istnieje
- PACKAGES_DIR = os.path.abspath("packages")
- os.makedirs(PACKAGES_DIR, exist_ok=True)
- class DownloadWorker(QThread):
- """Klasa wątku do obsługi pobierania i instalacji pakietów w tle."""
- progress = pyqtSignal(int) # Sygnał wysyłający postęp operacji (0-100)
- finished = pyqtSignal(str) # Sygnał wysyłający komunikat o pomyślnym zakończeniu
- error = pyqtSignal(str) # Sygnał wysyłający komunikat o błędzie
- def __init__(self, func, package_name):
- """
- Inicjalizuje wątek.
- :param func: Funkcja do wykonania w wątku (np. instalacja).
- :param package_name: Nazwa pakietu do wyświetlania w komunikatach.
- """
- super().__init__()
- self.func = func
- self.package_name = package_name
- def run(self):
- """Główna pętla wątku, wykonująca przekazaną funkcję."""
- try:
- self.func(progress_callback=self.progress.emit)
- self.finished.emit(f"Operacja dla {self.package_name} zakończona pomyślnie.")
- except Exception as e:
- self.error.emit(f"Błąd podczas operacji dla {self.package_name}: {str(e)}")
- class PackageManager:
- """Klasa zarządzająca pakietami (pobieranie, instalacja, odinstalowanie)."""
- def __init__(self, parent=None):
- """
- Inicjalizuje menadżera pakietów.
- :param parent: Obiekt nadrzędny (opcjonalne, dla kontekstu ścieżki).
- """
- self.parent = parent
- base_dir = os.path.dirname(__file__) if '__file__' in locals() else os.getcwd()
- self.settings_path = os.path.abspath(os.path.join(base_dir, "..", "userdata", "settings.json"))
- os.makedirs(os.path.dirname(self.settings_path), exist_ok=True)
- def _download_file(self, url, dest_path, progress_callback=None):
- """
- Pobiera plik z podanego URL do wskazanej ścieżki.
- :param url: Adres URL pliku.
- :param dest_path: Ścieżka docelowa zapisu pliku.
- :param progress_callback: Funkcja callback do raportowania postępu.
- """
- try:
- response = requests.get(url, stream=True, timeout=15)
- response.raise_for_status()
- total = int(response.headers.get('content-length', 0))
- downloaded = 0
- with open(dest_path, "wb") as f:
- for chunk in response.iter_content(chunk_size=8192):
- if chunk:
- f.write(chunk)
- downloaded += len(chunk)
- if total > 0 and progress_callback:
- percent = int(downloaded * 100 / total)
- progress_callback(percent)
- if progress_callback:
- progress_callback(100)
- except requests.exceptions.Timeout:
- raise RuntimeError(f"Upłynął czas oczekiwania na odpowiedź serwera podczas pobierania z {url}")
- except requests.RequestException as e:
- raise RuntimeError(f"Błąd podczas pobierania pliku z {url}: {str(e)}")
- def _extract_archive(self, file_path, extract_to):
- """
- Rozpakowuje archiwum (ZIP, TAR.GZ lub 7Z) do wskazanego katalogu.
- :param file_path: Ścieżka do pliku archiwum.
- :param extract_to: Ścieżka do katalogu docelowego rozpakowania.
- """
- os.makedirs(extract_to, exist_ok=True)
- try:
- if file_path.lower().endswith(".zip"):
- with zipfile.ZipFile(file_path, 'r') as zip_ref:
- for member in zip_ref.namelist():
- fname = member.split('/', 1)[1] if '/' in member else member
- if fname:
- target_path = os.path.join(extract_to, fname)
- if member.endswith('/'):
- os.makedirs(target_path, exist_ok=True)
- else:
- os.makedirs(os.path.dirname(target_path), exist_ok=True)
- with open(target_path, 'wb') as f:
- f.write(zip_ref.read(member))
- elif file_path.lower().endswith((".tar.gz", ".tgz")):
- with tarfile.open(file_path, 'r:gz') as tar_ref:
- tar_ref.extractall(extract_to)
- elif file_path.lower().endswith(".7z"):
- with py7zr.SevenZipFile(file_path, mode='r') as z:
- z.extractall(extract_to)
- else:
- raise ValueError(f"Nieobsługiwany format archiwum: {file_path}")
- except (zipfile.BadZipFile, tarfile.TarError, py7zr.Py7zrError) as e:
- raise RuntimeError(f"Błąd podczas rozpakowywania archiwum {os.path.basename(file_path)}: {str(e)}")
- except Exception as e:
- raise RuntimeError(f"Nieoczekiwany błąd podczas rozpakowywania: {str(e)}")
- def _get_local_version(self, package):
- """
- Pobiera lokalnie zainstalowaną wersję pakietu.
- :param package: Nazwa pakietu.
- :return: Wersja lub None.
- """
- exe_path = self._get_setting(f"{package.lower()}_path")
- if not exe_path or not os.path.exists(exe_path):
- return None
- try:
- if package == "Python":
- result = subprocess.run([exe_path, "--version"], capture_output=True, text=True, check=True, encoding='utf-8')
- return result.stdout.strip().split()[-1]
- elif package == "Node.js":
- result = subprocess.run([exe_path, "--version"], capture_output=True, text=True, check=True, encoding='utf-8')
- return result.stdout.strip().lstrip('v')
- elif package == "Go":
- result = subprocess.run([exe_path, "version"], capture_output=True, text=True, check=True, encoding='utf-8')
- return result.stdout.strip().split()[2].lstrip('go')
- elif package == "Java":
- result = subprocess.run([exe_path, "-version"], capture_output=True, text=True, check=True, encoding='utf-8')
- return result.stdout.strip().split()[2].strip('"')
- elif package == "Ruby":
- result = subprocess.run([exe_path, "--version"], capture_output=True, text=True, check=True, encoding='utf-8')
- return result.stdout.strip().split()[1]
- elif package == "Rust":
- result = subprocess.run([exe_path, "--version"], capture_output=True, text=True, check=True, encoding='utf-8')
- return result.stdout.strip().split()[1]
- elif package == "Git":
- result = subprocess.run([exe_path, "--version"], capture_output=True, text=True, check=True, encoding='utf-8')
- return result.stdout.strip().split()[-1]
- elif package == "Docker":
- result = subprocess.run([exe_path, "--version"], capture_output=True, text=True, check=True, encoding='utf-8')
- return result.stdout.strip().split()[2].rstrip(',')
- except (subprocess.CalledProcessError, FileNotFoundError, OSError):
- return None
- return None
- def _get_latest_version(self, package):
- """
- Pobiera najnowszą wersję pakietu ze źródeł zewnętrznych.
- :param package: Nazwa pakietu.
- :return: Wersja lub None.
- """
- try:
- if package == "Python":
- url = "https://www.python.org/ftp/python/index-windows.json"
- response = requests.get(url, timeout=10)
- response.raise_for_status()
- data = response.json()
- 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()]
- if not versions:
- return None
- return sorted(versions, key=lambda v: list(map(int, v["sort-version"].split("."))), reverse=True)[0]["sort-version"]
- elif package == "Node.js":
- shasums_url = "https://nodejs.org/dist/latest/SHASUMS256.txt"
- response = requests.get(shasums_url, timeout=10)
- response.raise_for_status()
- pattern = r"^[a-f0-9]{64}\s+(node-v([\d.]+)-win-x64\.zip)$"
- for line in response.text.splitlines():
- match = re.match(pattern, line, re.MULTILINE)
- if match:
- return match.group(2)
- return None
- elif package == "Go":
- url = "https://go.dev/dl/"
- response = requests.get(url, timeout=10)
- response.raise_for_status()
- pattern = r"go(\d+\.\d+\.\d+)\.windows-amd64\.zip"
- match = re.search(pattern, response.text)
- return match.group(1) if match else None
- elif package == "Java":
- url = "https://jdk.java.net/21/"
- response = requests.get(url, timeout=10)
- response.raise_for_status()
- pattern = r"openjdk-(\d+)_windows-x64_bin\.zip"
- match = re.search(pattern, response.text)
- return match.group(1) if match else None
- elif package == "Ruby":
- url = "https://rubyinstaller.org/downloads/"
- response = requests.get(url, timeout=10)
- response.raise_for_status()
- pattern = r"rubyinstaller-(\d+\.\d+\.\d+-\d+)-x64\.7z"
- match = re.search(pattern, response.text)
- return match.group(1) if match else None
- elif package == "Rust":
- url = "https://static.rust-lang.org/dist/channel-rust-stable.toml"
- response = requests.get(url, timeout=10)
- response.raise_for_status()
- pattern = r"version = \"(\d+\.\d+\.\d+)\""
- match = re.search(pattern, response.text)
- return match.group(1) if match else None
- elif package == "Git":
- url = "https://git-scm.com/download/win"
- response = requests.get(url, timeout=10)
- response.raise_for_status()
- pattern = r"Git-(\d+\.\d+\.\d+)-64-bit\.exe"
- match = re.search(pattern, response.text)
- return match.group(1) if match else None
- elif package == "Docker":
- url = "https://desktop.docker.com/win/stable/amd64/Docker%20Desktop%20Installer.exe"
- response = requests.head(url, timeout=10)
- response.raise_for_status()
- return "latest" # Docker Desktop nie publikuje wersji wprost
- except requests.RequestException:
- return None
- return None
- def _is_installed(self, package):
- """
- Sprawdza, czy pakiet jest zainstalowany.
- :param package: Nazwa pakietu.
- :return: True, jeśli zainstalowany, inaczej False.
- """
- if package == "Python":
- return os.path.exists(os.path.join(PACKAGES_DIR, "python", "python.exe"))
- elif package == "Node.js":
- return os.path.exists(os.path.join(PACKAGES_DIR, "node.js", "node.exe"))
- elif package == "Go":
- return os.path.exists(os.path.join(PACKAGES_DIR, "go", "bin", "go.exe"))
- elif package == "Java":
- return os.path.exists(os.path.join(PACKAGES_DIR, "java", "bin", "java.exe"))
- elif package == "Ruby":
- return os.path.exists(os.path.join(PACKAGES_DIR, "ruby", "bin", "ruby.exe"))
- elif package == "Rust":
- return os.path.exists(os.path.join(PACKAGES_DIR, "rust", "bin", "cargo.exe"))
- elif package == "Git":
- return os.path.exists(os.path.join(PACKAGES_DIR, "git", "bin", "git.exe"))
- elif package == "Docker":
- return os.path.exists(os.path.join(PACKAGES_DIR, "docker", "Docker", "Docker Desktop.exe"))
- return False
- def install_latest_python(self, progress_callback=None):
- """
- Instaluje najnowszą wersję Pythona.
- :param progress_callback: Funkcja callback dla postępu.
- """
- try:
- url_index = "https://www.python.org/ftp/python/index-windows.json"
- response = requests.get(url_index, timeout=10)
- response.raise_for_status()
- data = response.json()
- 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()]
- if not versions:
- raise RuntimeError("Nie znaleziono stabilnej wersji 64-bitowej Pythona z plikiem ZIP.")
- latest = sorted(versions, key=lambda v: list(map(int, v["sort-version"].split("."))), reverse=True)[0]
- download_url = latest["url"]
- version = latest["sort-version"]
- filename = f"python-{version}-amd64.zip"
- python_dir = os.path.join(PACKAGES_DIR, "python")
- zip_path = os.path.join(PACKAGES_DIR, filename)
- if os.path.exists(python_dir):
- shutil.rmtree(python_dir)
- os.makedirs(python_dir, exist_ok=True)
- if progress_callback: progress_callback(1)
- self._download_file(download_url, zip_path, progress_callback)
- if progress_callback: progress_callback(95)
- self._extract_archive(zip_path, python_dir)
- if progress_callback: progress_callback(98)
- os.remove(zip_path)
- python_exe_path = os.path.join(python_dir, "python.exe")
- if not os.path.exists(python_exe_path):
- raise RuntimeError("Nie znaleziono pliku python.exe po rozpakowaniu.")
- self._update_settings("python_path", python_exe_path)
- self._update_settings("python_version", version)
- if progress_callback: progress_callback(100)
- except Exception as e:
- raise RuntimeError(f"Instalacja pakietu Python nieudana: {str(e)}")
- def install_latest_nodejs(self, progress_callback=None):
- """
- Instaluje najnowszą wersję Node.js.
- :param progress_callback: Funkcja callback dla postępu.
- """
- try:
- shasums_url = "https://nodejs.org/dist/latest/SHASUMS256.txt"
- response = requests.get(shasums_url, timeout=10)
- response.raise_for_status()
- pattern = r"^[a-f0-9]{64}\s+(node-v([\d.]+)-win-x64\.zip)$"
- filename = None
- version = None
- for line in response.text.splitlines():
- match = re.match(pattern, line, re.MULTILINE)
- if match:
- filename = match.group(1)
- version = match.group(2)
- break
- if not filename or not version:
- raise RuntimeError("Nie znaleziono archiwum Node.js dla Windows x64 w pliku SHASUMS256.txt.")
- base_url = "https://nodejs.org/dist/latest/"
- download_url = f"{base_url}{filename}"
- zip_path = os.path.join(PACKAGES_DIR, filename)
- node_dir = os.path.join(PACKAGES_DIR, "node.js")
- if os.path.exists(node_dir):
- shutil.rmtree(node_dir)
- os.makedirs(node_dir, exist_ok=True)
- if progress_callback: progress_callback(1)
- self._download_file(download_url, zip_path, progress_callback)
- if progress_callback: progress_callback(95)
- self._extract_archive(zip_path, node_dir)
- if progress_callback: progress_callback(98)
- os.remove(zip_path)
- node_exe_path = os.path.join(node_dir, "node.exe")
- if not os.path.exists(node_exe_path):
- raise RuntimeError("Nie znaleziono pliku node.exe po rozpakowaniu.")
- self._update_settings("node_path", node_exe_path)
- self._update_settings("node_version", version)
- if progress_callback: progress_callback(100)
- except Exception as e:
- raise RuntimeError(f"Instalacja pakietu Node.js nieudana: {str(e)}")
- def install_latest_go(self, progress_callback=None):
- """
- Instaluje najnowszą wersję Go.
- :param progress_callback: Funkcja callback dla postępu.
- """
- try:
- url = "https://go.dev/dl/"
- response = requests.get(url, timeout=10)
- response.raise_for_status()
- pattern = r"go(\d+\.\d+\.\d+)\.windows-amd64\.zip"
- match = re.search(pattern, response.text)
- if not match:
- raise RuntimeError("Nie znaleziono wersji Go dla Windows x64")
- version = match.group(1)
- filename = f"go{version}.windows-amd64.zip"
- download_url = f"{url}{filename}"
- zip_path = os.path.join(PACKAGES_DIR, filename)
- go_dir = os.path.join(PACKAGES_DIR, "go")
- if os.path.exists(go_dir):
- shutil.rmtree(go_dir)
- os.makedirs(go_dir, exist_ok=True)
- if progress_callback: progress_callback(1)
- self._download_file(download_url, zip_path, progress_callback)
- if progress_callback: progress_callback(95)
- self._extract_archive(zip_path, go_dir)
- if progress_callback: progress_callback(98)
- os.remove(zip_path)
- go_exe_path = os.path.join(go_dir, "bin", "go.exe")
- if not os.path.exists(go_exe_path):
- raise RuntimeError("Nie znaleziono pliku go.exe po rozpakowaniu.")
- self._update_settings("go_path", go_exe_path)
- self._update_settings("go_version", version)
- if progress_callback: progress_callback(100)
- except Exception as e:
- raise RuntimeError(f"Instalacja pakietu Go nieudana: {str(e)}")
- def install_latest_java(self, progress_callback=None):
- """
- Instaluje najnowszą wersję OpenJDK.
- :param progress_callback: Funkcja callback dla postępu.
- """
- try:
- url = "https://jdk.java.net/21/"
- response = requests.get(url, timeout=10)
- response.raise_for_status()
- pattern = r"openjdk-(\d+)_windows-x64_bin\.zip"
- match = re.search(pattern, response.text)
- if not match:
- raise RuntimeError("Nie znaleziono OpenJDK dla Windows x64")
- version = match.group(1)
- filename = f"openjdk-{version}_windows-x64_bin.zip"
- download_url = f"https://download.java.net/java/GA/jdk{version}/{filename}"
- zip_path = os.path.join(PACKAGES_DIR, filename)
- java_dir = os.path.join(PACKAGES_DIR, "java")
- if os.path.exists(java_dir):
- shutil.rmtree(java_dir)
- os.makedirs(java_dir, exist_ok=True)
- if progress_callback: progress_callback(1)
- self._download_file(download_url, zip_path, progress_callback)
- if progress_callback: progress_callback(95)
- self._extract_archive(zip_path, java_dir)
- if progress_callback: progress_callback(98)
- os.remove(zip_path)
- java_exe_path = os.path.join(java_dir, "bin", "java.exe")
- if not os.path.exists(java_exe_path):
- raise RuntimeError("Nie znaleziono pliku java.exe po rozpakowaniu.")
- self._update_settings("java_path", java_exe_path)
- self._update_settings("java_version", version)
- if progress_callback: progress_callback(100)
- except Exception as e:
- raise RuntimeError(f"Instalacja pakietu Java nieudana: {str(e)}")
- def install_latest_ruby(self, progress_callback=None):
- """
- Instaluje najnowszą wersję Ruby.
- :param progress_callback: Funkcja callback dla postępu.
- """
- try:
- url = "https://rubyinstaller.org/downloads/"
- response = requests.get(url, timeout=10)
- response.raise_for_status()
- pattern = r"rubyinstaller-(\d+\.\d+\.\d+-\d+)-x64\.7z"
- match = re.search(pattern, response.text)
- if not match:
- raise RuntimeError("Nie znaleziono Ruby dla Windows x64")
- version = match.group(1)
- filename = f"rubyinstaller-{version}-x64.7z"
- download_url = f"https://github.com/oneclick/rubyinstaller2/releases/download/RubyInstaller-{version}/{filename}"
- archive_path = os.path.join(PACKAGES_DIR, filename)
- ruby_dir = os.path.join(PACKAGES_DIR, "ruby")
- if os.path.exists(ruby_dir):
- shutil.rmtree(ruby_dir)
- os.makedirs(ruby_dir, exist_ok=True)
- if progress_callback: progress_callback(1)
- self._download_file(download_url, archive_path, progress_callback)
- if progress_callback: progress_callback(95)
- self._extract_archive(archive_path, ruby_dir)
- if progress_callback: progress_callback(98)
- os.remove(archive_path)
- ruby_exe_path = os.path.join(ruby_dir, "bin", "ruby.exe")
- if not os.path.exists(ruby_exe_path):
- raise RuntimeError("Nie znaleziono pliku ruby.exe po rozpakowaniu.")
- self._update_settings("ruby_path", ruby_exe_path)
- self._update_settings("ruby_version", version)
- if progress_callback: progress_callback(100)
- except Exception as e:
- raise RuntimeError(f"Instalacja pakietu Ruby nieudana: {str(e)}")
- def install_latest_rust(self, progress_callback=None):
- """
- Instaluje najnowszą wersję Rust.
- :param progress_callback: Funkcja callback dla postępu.
- """
- try:
- url = "https://static.rust-lang.org/rustup/dist/x86_64-pc-windows-msvc/rustup-init.exe"
- exe_path = os.path.join(PACKAGES_DIR, "rustup-init.exe")
- rust_dir = os.path.join(PACKAGES_DIR, "rust")
- os.makedirs(rust_dir, exist_ok=True)
- if progress_callback: progress_callback(1)
- self._download_file(url, exe_path, progress_callback)
- if progress_callback: progress_callback(95)
- subprocess.run([exe_path, "--default-toolchain", "stable", "--profile", "minimal", "-y", f"--target-dir={rust_dir}"], check=True)
- if progress_callback: progress_callback(98)
- os.remove(exe_path)
- cargo_exe_path = os.path.join(rust_dir, "bin", "cargo.exe")
- if not os.path.exists(cargo_exe_path):
- raise RuntimeError("Nie znaleziono pliku cargo.exe po instalacji.")
- version = subprocess.run([cargo_exe_path, "--version"], capture_output=True, text=True).stdout.strip().split()[1]
- self._update_settings("rust_path", cargo_exe_path)
- self._update_settings("rust_version", version)
- if progress_callback: progress_callback(100)
- except Exception as e:
- raise RuntimeError(f"Instalacja pakietu Rust nieudana: {str(e)}")
- def install_latest_git(self, progress_callback=None):
- """
- Instaluje najnowszą wersję Git.
- :param progress_callback: Funkcja callback dla postępu.
- """
- try:
- url = "https://git-scm.com/download/win"
- response = requests.get(url, timeout=10)
- response.raise_for_status()
- pattern = r"Git-(\d+\.\d+\.\d+)-64-bit\.exe"
- match = re.search(pattern, response.text)
- if not match:
- raise RuntimeError("Nie znaleziono Git dla Windows x64")
- version = match.group(1)
- filename = f"Git-{version}-64-bit.exe"
- download_url = f"https://github.com/git-for-windows/git/releases/download/v{version}.windows.1/{filename}"
- exe_path = os.path.join(PACKAGES_DIR, filename)
- git_dir = os.path.join(PACKAGES_DIR, "git")
- os.makedirs(git_dir, exist_ok=True)
- if progress_callback: progress_callback(1)
- self._download_file(download_url, exe_path, progress_callback)
- if progress_callback: progress_callback(95)
- subprocess.run([exe_path, "/VERYSILENT", f"/DIR={git_dir}"], check=True)
- if progress_callback: progress_callback(98)
- os.remove(exe_path)
- git_exe_path = os.path.join(git_dir, "bin", "git.exe")
- if not os.path.exists(git_exe_path):
- raise RuntimeError("Nie znaleziono pliku git.exe po instalacji.")
- self._update_settings("git_path", git_exe_path)
- self._update_settings("git_version", version)
- if progress_callback: progress_callback(100)
- except Exception as e:
- raise RuntimeError(f"Instalacja pakietu Git nieudana: {str(e)}")
- def install_latest_docker(self, progress_callback=None):
- """
- Instaluje najnowszą wersję Docker Desktop.
- :param progress_callback: Funkcja callback dla postępu.
- """
- try:
- url = "https://desktop.docker.com/win/stable/amd64/Docker%20Desktop%20Installer.exe"
- exe_path = os.path.join(PACKAGES_DIR, "DockerDesktopInstaller.exe")
- docker_dir = os.path.join(PACKAGES_DIR, "docker")
- os.makedirs(docker_dir, exist_ok=True)
- if progress_callback: progress_callback(1)
- self._download_file(url, exe_path, progress_callback)
- if progress_callback: progress_callback(95)
- subprocess.run([exe_path, "install", "--quiet", f"--install-dir={docker_dir}"], check=True)
- if progress_callback: progress_callback(98)
- os.remove(exe_path)
- docker_exe_path = os.path.join(docker_dir, "Docker", "Docker Desktop.exe")
- if not os.path.exists(docker_exe_path):
- raise RuntimeError("Nie znaleziono pliku Docker Desktop po instalacji.")
- version = subprocess.run([docker_exe_path, "--version"], capture_output=True, text=True).stdout.strip().split()[2].rstrip(',')
- self._update_settings("docker_path", docker_exe_path)
- self._update_settings("docker_version", version)
- if progress_callback: progress_callback(100)
- except Exception as e:
- raise RuntimeError(f"Instalacja pakietu Docker nieudana: {str(e)}")
- def uninstall_package(self, package):
- """
- Odinstalowuje pakiet, usuwając jego katalog i wpisy w ustawieniach.
- :param package: Nazwa pakietu.
- """
- try:
- folder = os.path.join(PACKAGES_DIR, package.lower())
- if os.path.exists(folder):
- shutil.rmtree(folder)
- self._remove_setting(f"{package.lower()}_path")
- self._remove_setting(f"{package.lower()}_version")
- except Exception as e:
- raise RuntimeError(f"Odinstalowanie pakietu {package} nieudane: {str(e)}")
- def _update_settings(self, key, value):
- """
- Zapisuje ustawienie w pliku settings.json.
- :param key: Klucz ustawienia.
- :param value: Wartość ustawienia.
- """
- settings = {}
- try:
- if os.path.exists(self.settings_path):
- with open(self.settings_path, "r", encoding="utf-8") as f:
- settings = json.load(f)
- except (json.JSONDecodeError, IOError):
- settings = {}
- settings[key] = value
- try:
- with open(self.settings_path, "w", encoding="utf-8") as f:
- json.dump(settings, f, indent=4)
- except IOError as e:
- raise RuntimeError(f"Błąd zapisu ustawień do pliku {os.path.basename(self.settings_path)}: {str(e)}")
- def _remove_setting(self, key):
- """
- Usuwa ustawienie z pliku settings.json.
- :param key: Klucz ustawienia do usunięcia.
- """
- settings = {}
- try:
- if os.path.exists(self.settings_path):
- with open(self.settings_path, "r", encoding="utf-8") as f:
- settings = json.load(f)
- if key in settings:
- del settings[key]
- with open(self.settings_path, "w", encoding="utf-8") as f:
- json.dump(settings, f, indent=4)
- except (json.JSONDecodeError, IOError):
- pass
- except Exception as e:
- raise RuntimeError(f"Błąd usuwania ustawienia '{key}' z pliku {os.path.basename(self.settings_path)}: {str(e)}")
- def _get_setting(self, key):
- """
- Pobiera wartość ustawienia z pliku settings.json.
- :param key: Klucz ustawienia.
- :return: Wartość ustawienia lub None.
- """
- try:
- if os.path.exists(self.settings_path):
- with open(self.settings_path, "r", encoding="utf-8") as f:
- settings = json.load(f)
- return settings.get(key)
- return None
- except (json.JSONDecodeError, IOError):
- return None
- except Exception:
- return None
- class PackageManagerDialog(QDialog):
- """Okno dialogowe menadżera pakietów."""
- def __init__(self, project_dir, settings, parent=None):
- """Inicjalizuje okno dialogowe."""
- super().__init__(parent)
- self.project_dir = project_dir
- self.settings = settings
- self.setWindowTitle("Menadżer Pakietów")
- self.setModal(True)
- self.resize(800, 500)
- self.pkg_manager = PackageManager(self)
- self.worker = None
- self.setup_ui()
- self.apply_styles()
- self.populate_table()
- def setup_ui(self):
- """Konfiguruje interfejs użytkownika."""
- layout = QVBoxLayout(self)
- self.status_label = QLabel("Status: Gotowy")
- layout.addWidget(self.status_label)
- self.progress_bar = QProgressBar()
- self.progress_bar.setVisible(False)
- layout.addWidget(self.progress_bar)
- self.table = QTableWidget(0, 5)
- self.table.setHorizontalHeaderLabels(["Nazwa Pakietu", "Opis", "Wersja", "Status", "Akcje"])
- header = self.table.horizontalHeader()
- header.setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
- header.setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
- header.setSectionResizeMode(2, QHeaderView.ResizeMode.ResizeToContents)
- header.setSectionResizeMode(3, QHeaderView.ResizeMode.ResizeToContents)
- header.setSectionResizeMode(4, QHeaderView.ResizeMode.ResizeToContents)
- self.table.setSelectionMode(QTableWidget.SelectionMode.NoSelection)
- layout.addWidget(self.table)
- def apply_styles(self):
- """Zastosuj style CSS zgodnie z motywem aplikacji."""
- # Pobierz motyw z ustawień lub domyślnie 'light'
- theme = 'light'
- if self.parent and hasattr(self.parent, 'settings'):
- theme = getattr(self.parent, 'settings', {}).get('theme', 'light')
- elif hasattr(self.parent, 'theme'):
- theme = getattr(self.parent, 'theme', 'light')
- if theme == 'dark':
- self.setStyleSheet(get_dark_package_manager_stylesheet())
- else:
- self.setStyleSheet(get_light_package_manager_stylesheet())
- def populate_table(self):
- """Wypełnia tabelę informacjami o dostępnych pakietach."""
- packages = [
- {
- "name": "Python",
- "desc": "Język programowania Python (64-bit)",
- "size": "~30 MB",
- "install_func": self.download_python,
- "uninstall_func": lambda: self.uninstall_package("Python"),
- },
- {
- "name": "Node.js",
- "desc": "Środowisko uruchomieniowe JavaScript (64-bit)",
- "size": "~25 MB",
- "install_func": self.download_node,
- "uninstall_func": lambda: self.uninstall_package("Node.js"),
- },
- {
- "name": "Go",
- "desc": "Język programowania Go (64-bit)",
- "size": "~100 MB",
- "install_func": self.download_go,
- "uninstall_func": lambda: self.uninstall_package("Go"),
- },
- {
- "name": "Java",
- "desc": "Java Development Kit (OpenJDK, 64-bit)",
- "size": "~200 MB",
- "install_func": self.download_java,
- "uninstall_func": lambda: self.uninstall_package("Java"),
- },
- {
- "name": "Ruby",
- "desc": "Język programowania Ruby (64-bit)",
- "size": "~50 MB",
- "install_func": self.download_ruby,
- "uninstall_func": lambda: self.uninstall_package("Ruby"),
- },
- {
- "name": "Rust",
- "desc": "Język programowania Rust (64-bit)",
- "size": "~150 MB",
- "install_func": self.download_rust,
- "uninstall_func": lambda: self.uninstall_package("Rust"),
- },
- {
- "name": "Git",
- "desc": "System kontroli wersji Git (64-bit)",
- "size": "~50 MB",
- "install_func": self.download_git,
- "uninstall_func": lambda: self.uninstall_package("Git"),
- },
- {
- "name": "Docker",
- "desc": "Platforma do konteneryzacji Docker Desktop (64-bit)",
- "size": "~500 MB",
- "install_func": self.download_docker,
- "uninstall_func": lambda: self.uninstall_package("Docker"),
- },
- ]
- self.table.setRowCount(len(packages))
- for row, pkginfo in enumerate(packages):
- name = pkginfo["name"]
- local_version = self.pkg_manager._get_local_version(name) or "Brak"
- latest_version = self.pkg_manager._get_latest_version(name) or "Brak informacji"
- version_text = local_version
- is_installed = self.pkg_manager._is_installed(name)
- status = "Niezainstalowany"
- status_color = QColor("#ff4444")
- if is_installed:
- status = "Zainstalowano"
- status_color = QColor("#44ff44")
- update_available = False
- if latest_version != "Brak informacji" and local_version != "Brak":
- try:
- local_parts = list(map(int, local_version.split('.')))
- latest_parts = list(map(int, latest_version.split('.')))
- max_len = max(len(local_parts), len(latest_parts))
- local_parts += [0] * (max_len - len(local_parts))
- latest_parts += [0] * (max_len - len(latest_parts))
- if latest_parts > local_parts:
- update_available = True
- except ValueError:
- if latest_version > local_version:
- update_available = True
- if update_available:
- status = "Dostępna aktualizacja"
- status_color = QColor("#ffff44")
- version_text = f"{local_version} (Najnowsza: {latest_version})"
- self.table.setItem(row, 0, QTableWidgetItem(name))
- self.table.setItem(row, 1, QTableWidgetItem(pkginfo["desc"]))
- self.table.setItem(row, 2, QTableWidgetItem(version_text))
- status_item = QTableWidgetItem(status)
- status_item.setForeground(status_color)
- status_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
- self.table.setItem(row, 3, status_item)
- action_layout = QHBoxLayout()
- action_layout.setContentsMargins(0, 0, 0, 0)
- action_layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
- if is_installed:
- uninstall_btn = QPushButton("Odinstaluj")
- uninstall_btn.clicked.connect(pkginfo["uninstall_func"])
- action_layout.addWidget(uninstall_btn)
- if update_available:
- update_btn = QPushButton("Aktualizuj")
- update_btn.clicked.connect(pkginfo["install_func"])
- action_layout.addWidget(update_btn)
- else:
- install_btn = QPushButton("Zainstaluj")
- install_btn.clicked.connect(pkginfo["install_func"])
- action_layout.addWidget(install_btn)
- action_widget = QWidget()
- action_widget.setLayout(action_layout)
- self.table.setCellWidget(row, 4, action_widget)
- def download_python(self):
- """Rozpoczyna instalację Pythona."""
- self.start_operation(self.pkg_manager.install_latest_python, "Python", "instalacji")
- def download_node(self):
- """Rozpoczyna instalację Node.js."""
- self.start_operation(self.pkg_manager.install_latest_nodejs, "Node.js", "instalacji")
- def download_go(self):
- """Rozpoczyna instalację Go."""
- self.start_operation(self.pkg_manager.install_latest_go, "Go", "instalacji")
- def download_java(self):
- """Rozpoczyna instalację Java."""
- self.start_operation(self.pkg_manager.install_latest_java, "Java", "instalacji")
- def download_ruby(self):
- """Rozpoczyna instalację Ruby."""
- self.start_operation(self.pkg_manager.install_latest_ruby, "Ruby", "instalacji")
- def download_rust(self):
- """Rozpoczyna instalację Rust."""
- self.start_operation(self.pkg_manager.install_latest_rust, "Rust", "instalacji")
- def download_git(self):
- """Rozpoczyna instalację Git."""
- self.start_operation(self.pkg_manager.install_latest_git, "Git", "instalacji")
- def download_docker(self):
- """Rozpoczyna instalację Docker."""
- self.start_operation(self.pkg_manager.install_latest_docker, "Docker", "instalacji")
- def uninstall_package(self, package_name):
- """Rozpoczyna odinstalowanie pakietu."""
- self.start_operation(lambda progress_callback=None: self.pkg_manager.uninstall_package(package_name), package_name, "odinstalowania")
- def start_operation(self, func, package_name, operation_type):
- """
- Rozpoczyna operację na pakiecie w osobnym wątku.
- :param func: Funkcja do wykonania.
- :param package_name: Nazwa pakietu.
- :param operation_type: Typ operacji.
- """
- self.set_actions_enabled(False)
- self.status_label.setText(f"Rozpoczęto operację {operation_type} pakietu {package_name}...")
- self.progress_bar.setValue(0)
- self.progress_bar.setVisible(True)
- self.worker = DownloadWorker(func, package_name)
- self.worker.progress.connect(self.update_progress)
- self.worker.finished.connect(self.on_operation_finished)
- self.worker.error.connect(self.on_operation_error)
- self.worker.start()
- def update_progress(self, value):
- """Aktualizuje pasek postępu."""
- self.progress_bar.setValue(value)
- QApplication.processEvents()
- def on_operation_finished(self, message):
- """Obsługuje zakończenie operacji."""
- self.status_label.setText(message)
- self.progress_bar.setValue(100)
- self.progress_bar.setVisible(False)
- self.populate_table()
- self.set_actions_enabled(True)
- def on_operation_error(self, message):
- """Obsługuje błąd operacji."""
- self.status_label.setText(f"Błąd: {message}")
- self.progress_bar.setVisible(False)
- self.populate_table()
- self.set_actions_enabled(True)
- def set_actions_enabled(self, enabled):
- """Włącza/wyłącza przyciski akcji."""
- for row in range(self.table.rowCount()):
- widget = self.table.cellWidget(row, 4)
- if widget:
- for btn in widget.findChildren(QPushButton):
- btn.setEnabled(enabled)
- if __name__ == '__main__':
- import sys
- userdata_dir_test = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "userdata"))
- os.makedirs(userdata_dir_test, exist_ok=True)
- settings_path_test = os.path.join(userdata_dir_test, "settings.json")
- if not os.path.exists(settings_path_test):
- with open(settings_path_test, "w") as f:
- json.dump({}, f)
- app = QApplication(sys.argv)
- dialog = PackageManagerDialog()
- dialog.exec()
- sys.exit(app.exec())
- Ścieżka: /src/process.py
- Rozmiar: 4,36 KB
- Zawartość:
- # Zarządzanie procesami zewnętrznymi
- #/src/process.py
- import os
- import platform
- import shlex
- from PyQt6.QtCore import QProcess, QProcessEnvironment
- class ProcessManager:
- def __init__(self, parent):
- self.process = QProcess(parent)
- self.process.readyReadStandardOutput.connect(self._handle_stdout)
- self.process.readyReadStandardError.connect(self._handle_stderr)
- self.process.finished.connect(self._handle_finished)
- self.console_output_callback = None
- self.status_bar_callback = None
- def set_callbacks(self, console_output, status_bar):
- self.console_output_callback = console_output
- self.status_bar_callback = status_bar
- def run_command(self, command, working_dir, python_path=None, node_path=None):
- if self.process.state() != QProcess.ProcessState.NotRunning:
- self._append_output("Inny proces już działa. Zakończ go najpierw.", is_error=True)
- return
- command_str = shlex.join(command)
- self._append_output(f"Uruchamianie: {command_str}\nw katalogu: {working_dir}\n---")
- self._update_status("Proces uruchomiony...")
- try:
- program = command[0]
- arguments = command[1:]
- self.process.setWorkingDirectory(working_dir)
- env = QProcessEnvironment.systemEnvironment()
- current_path = env.value("PATH", "")
- paths_to_prepend = []
- if python_path and os.path.exists(python_path):
- py_dir = os.path.dirname(python_path)
- if os.path.normcase(py_dir) not in [os.path.normcase(p) for p in current_path.split(os.pathsep)]:
- paths_to_prepend.append(py_dir)
- if node_path and os.path.exists(node_path):
- node_dir = os.path.dirname(node_path)
- if os.path.normcase(node_dir) not in [os.path.normcase(p) for p in current_path.split(os.pathsep)]:
- paths_to_prepend.append(node_dir)
- if paths_to_prepend:
- new_path = os.pathsep.join(paths_to_prepend) + (os.pathsep + current_path if current_path else "")
- env.insert("PATH", new_path)
- if platform.system() == "Windows":
- env.insert("Path", new_path)
- self.process.setProcessEnvironment(env)
- self.process.start(program, arguments)
- if not self.process.waitForStarted(1000):
- error = self.process.errorString()
- self._append_output(f"Nie udało się uruchomić '{program}': {error}", is_error=True)
- self._update_status(f"Błąd uruchamiania: {program}")
- except Exception as e:
- self._append_output(f"Błąd podczas uruchamiania: {e}", is_error=True)
- self._update_status("Błąd uruchamiania.")
- def _append_output(self, text, is_error=False):
- if self.console_output_callback:
- self.console_output_callback(text, is_error)
- def _update_status(self, message):
- if self.status_bar_callback:
- self.status_bar_callback(message)
- def _handle_stdout(self):
- while self.process.bytesAvailable():
- data = self.process.readAllStandardOutput()
- try:
- text = bytes(data).decode('utf-8')
- except UnicodeDecodeError:
- text = bytes(data).decode('utf-8', errors='replace')
- self._append_output(text)
- def _handle_stderr(self):
- while self.process.bytesAvailable():
- data = self.process.readAllStandardError()
- try:
- text = bytes(data).decode('utf-8')
- except UnicodeDecodeError:
- text = bytes(data).decode('utf-8', errors='replace')
- self._append_output(text, is_error=True)
- def _handle_finished(self, exit_code, exit_status):
- self._handle_stdout()
- self._handle_stderr()
- self._append_output("\n--- Zakończono proces ---")
- if exit_status == QProcess.ExitStatus.NormalExit:
- self._append_output(f"Kod wyjścia: {exit_code}")
- self._update_status(f"Zakończono. Kod wyjścia: {exit_code}")
- else:
- self._append_output(f"Awaria procesu z kodem: {exit_code}", is_error=True)
- self._update_status(f"Awaria procesu. Kod wyjścia: {exit_code}")
- Ścieżka: /src/theme.py
- Rozmiar: 12,98 KB
- Zawartość:
- # Zarządzanie motywami aplikacji – zajebiście stylowe! 😎
- # /src/theme.py
- from PyQt6.QtWidgets import QMainWindow, QApplication
- from PyQt6.QtGui import QPalette, QColor
- def get_dark_theme_stylesheet():
- return """
- QMainWindow, QWidget { background-color: #2E2E2E; color: #D3D3D3; }
- QMenuBar { background-color: #3C3C3C; color: #D3D3D3; }
- QMenuBar::item:selected { background-color: #505050; }
- QMenu { background-color: #3C3C3C; color: #D3D3D3; border: 1px solid #505050; }
- QMenu::item:selected { background-color: #505050; }
- QToolBar { background-color: #3C3C3C; color: #D3D3D3; spacing: 5px; padding: 2px; }
- QToolButton { background-color: transparent; border: 1px solid transparent; padding: 3px; border-radius: 4px; }
- QToolButton:hover { border: 1px solid #505050; background-color: #454545; }
- QToolButton:pressed { background-color: #404040; }
- QPushButton { background-color: #505050; color: #D3D3D3; border: 1px solid #606060; padding: 4px 8px; border-radius: 4px; }
- QPushButton:hover { background-color: #606060; }
- QStatusBar { background-color: #3C3C3C; color: #D3D3D3; }
- QSplitter::handle { background-color: #505050; }
- QTreeView { background-color: #1E1E1E; color: #D3D3D3; border: 1px solid #3C3C3C; alternate-background-color: #252525; }
- QTreeView::item:selected { background-color: #007acc; color: white; }
- QTabWidget::pane { border: 1px solid #3C3C3C; background-color: #1E1E1E; }
- QTabBar::tab {
- background: #3C3C3C;
- color: #D3D3D3;
- border: 1px solid #3C3C3C;
- border-bottom-color: #1E1E1E;
- border-top-left-radius: 4px;
- border-top-right-radius: 4px;
- padding: 6px 12px;
- margin-right: 2px;
- }
- QTabBar::tab:selected {
- background: #1E1E1E;
- border-bottom-color: #1E1E1E;
- color: #FFFFFF;
- }
- QTabBar::tab:hover {
- background: #454545;
- }
- QPlainTextEdit {
- background-color: #1E1E1E;
- color: #D3D3D3;
- border: none;
- selection-background-color: #007acc;
- selection-color: white;
- }
- QPlainTextEdit[readOnly="true"] {
- background-color: #1E1E1E;
- color: #CCCCCC;
- }
- QLineEdit {
- background-color: #3C3C3C;
- color: #D3D3D3;
- border: 1px solid #505050;
- padding: 4px;
- selection-background-color: #007acc;
- selection-color: white;
- border-radius: 3px;
- }
- QComboBox {
- background-color: #3C3C3C;
- color: #D3D3D3;
- border: 1px solid #505050;
- padding: 4px;
- border-radius: 3px;
- }
- QComboBox::drop-down {
- border: none;
- }
- QComboBox::down-arrow {
- image: url(:/icons/down_arrow_dark.png);
- }
- QDialog {
- background-color: #2E2E2E;
- color: #D3D3D3;
- }
- QLabel {
- color: #D3D3D3;
- }
- QDialogButtonBox QPushButton {
- background-color: #505050;
- color: #D3D3D3;
- border: 1px solid #606060;
- padding: 5px 10px;
- border-radius: 4px;
- }
- QSpinBox {
- background-color: #3C3C3C;
- color: #D3D3D3;
- border: 1px solid #505050;
- padding: 4px;
- selection-background-color: #007acc;
- selection-color: white;
- border-radius: 3px;
- }
- """
- def get_light_theme_stylesheet():
- return """
- QMainWindow, QWidget { background-color: #F5F5F5; color: #222222; }
- QMenuBar { background-color: #E0E0E0; color: #222222; }
- QMenuBar::item:selected { background-color: #B0B0B0; }
- QMenu { background-color: #FFFFFF; color: #222222; border: 1px solid #CCCCCC; }
- QMenu::item:selected { background-color: #B0B0B0; }
- QToolBar { background-color: #E0E0E0; color: #222222; spacing: 5px; padding: 2px; }
- QToolButton { background-color: transparent; border: 1px solid transparent; padding: 3px; border-radius: 4px; }
- QToolButton:hover { border: 1px solid #CCCCCC; background-color: #F0F0F0; }
- QToolButton:pressed { background-color: #DDDDDD; }
- QPushButton { background-color: #E0E0E0; color: #222222; border: 1px solid #CCCCCC; padding: 4px 8px; border-radius: 4px; }
- QPushButton:hover { background-color: #CCCCCC; }
- QStatusBar { background-color: #E0E0E0; color: #222222; }
- QSplitter::handle { background-color: #CCCCCC; }
- QTreeView { background-color: #FFFFFF; color: #222222; border: 1px solid #CCCCCC; alternate-background-color: #F0F0F0; }
- QTreeView::item:selected { background-color: #007acc; color: white; }
- QTabWidget::pane { border: 1px solid #CCCCCC; background-color: #FFFFFF; }
- QTabBar::tab {
- background: #E0E0E0;
- color: #222222;
- border: 1px solid #CCCCCC;
- border-bottom-color: #FFFFFF;
- border-top-left-radius: 4px;
- border-top-right-radius: 4px;
- padding: 6px 12px;
- margin-right: 2px;
- }
- QTabBar::tab:selected {
- background: #FFFFFF;
- border-bottom-color: #FFFFFF;
- color: #000000;
- }
- QTabBar::tab:hover {
- background: #F0F0F0;
- }
- QPlainTextEdit {
- background-color: #FFFFFF;
- color: #222222;
- border: none;
- selection-background-color: #007acc;
- selection-color: white;
- }
- QPlainTextEdit[readOnly="true"] {
- background-color: #F5F5F5;
- color: #444444;
- }
- QLineEdit {
- background-color: #FFFFFF;
- color: #222222;
- border: 1px solid #CCCCCC;
- padding: 4px;
- selection-background-color: #007acc;
- selection-color: white;
- border-radius: 3px;
- }
- QComboBox {
- background-color: #FFFFFF;
- color: #222222;
- border: 1px solid #CCCCCC;
- padding: 4px;
- border-radius: 3px;
- }
- QComboBox::drop-down {
- border: none;
- }
- QComboBox::down-arrow {
- image: url(:/icons/down_arrow_light.png);
- }
- QDialog {
- background-color: #f5f5f5;
- color: #222222;
- }
- QLabel {
- color: #222222;
- }
- QDialogButtonBox QPushButton {
- background-color: #e0e0e0;
- color: #222222;
- border: 1px solid #cccccc;
- padding: 5px 10px;
- border-radius: 4px;
- }
- QSpinBox {
- background-color: #FFFFFF;
- color: #222222;
- border: 1px solid #CCCCCC;
- padding: 4px;
- selection-background-color: #007acc;
- selection-color: white;
- border-radius: 3px;
- }
- """
- def get_dark_package_manager_stylesheet():
- return """
- QDialog {
- background-color: #23272e;
- color: #e0e0e0;
- font-family: 'Segoe UI', sans-serif;
- }
- QTableWidget {
- background-color: #2c313a;
- color: #e0e0e0;
- gridline-color: #444a56;
- border: 1px solid #444a56;
- selection-background-color: #3a3f4b;
- selection-color: #e0e0e0;
- }
- QTableWidget::item {
- padding: 5px;
- }
- QHeaderView::section {
- background-color: #23272e;
- color: #e0e0e0;
- padding: 5px;
- border: none;
- font-weight: bold;
- }
- QPushButton {
- background-color: #3a3f4b;
- color: #e0e0e0;
- border: 1px solid #444a56;
- padding: 5px 10px;
- border-radius: 4px;
- }
- QPushButton:hover {
- background-color: #444a56;
- border-color: #5a6272;
- }
- QPushButton:pressed {
- background-color: #23272e;
- }
- QPushButton:disabled {
- background-color: #2c313a;
- color: #888888;
- border-color: #444a56;
- }
- QProgressBar {
- background-color: #23272e;
- border: 1px solid #444a56;
- border-radius: 4px;
- text-align: center;
- color: #e0e0e0;
- }
- QProgressBar::chunk {
- background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #4CAF50, stop:1 #81C784);
- border-radius: 4px;
- }
- QLabel {
- color: #e0e0e0;
- font-size: 14px;
- margin-bottom: 5px;
- }
- QWidget#actionWidget {
- background-color: transparent;
- }
- """
- def get_light_package_manager_stylesheet():
- return """
- QDialog {
- background-color: #f5f5f5;
- color: #222222;
- font-family: 'Segoe UI', sans-serif;
- }
- QTableWidget {
- background-color: #ffffff;
- color: #222222;
- gridline-color: #cccccc;
- border: 1px solid #cccccc;
- selection-background-color: #b0d6fb;
- selection-color: #222222;
- }
- QTableWidget::item {
- padding: 5px;
- }
- QHeaderView::section {
- background-color: #e0e0e0;
- color: #222222;
- padding: 5px;
- border: none;
- font-weight: bold;
- }
- QPushButton {
- background-color: #e0e0e0;
- color: #222222;
- border: 1px solid #cccccc;
- padding: 5px 10px;
- border-radius: 4px;
- }
- QPushButton:hover {
- background-color: #cccccc;
- border-color: #999999;
- }
- QPushButton:pressed {
- background-color: #bbbbbb;
- }
- QPushButton:disabled {
- background-color: #dddddd;
- color: #aaaaaa;
- border-color: #cccccc;
- }
- QProgressBar {
- background-color: #e0e0e0;
- border: 1px solid #cccccc;
- border-radius: 4px;
- text-align: center;
- color: #222222;
- }
- QProgressBar::chunk {
- background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #4CAF50, stop:1 #81C784);
- border-radius: 4px;
- }
- QLabel {
- color: #222222;
- font-size: 14px;
- margin-bottom: 5px;
- }
- QWidget#actionWidget {
- background-color: transparent;
- }
- """
- def apply_theme(window: QMainWindow, theme_name: str):
- # Nakładamy motyw jak farbę na płótno, Paffcio! 🎨
- app = QApplication.instance()
- palette = QPalette()
- if theme_name == "dark":
- # Ciemny motyw – jak Twój humor po debugowaniu w nocy 😜
- palette.setColor(QPalette.ColorRole.Window, QColor("#2E2E2E"))
- palette.setColor(QPalette.ColorRole.WindowText, QColor("#D3D3D3"))
- palette.setColor(QPalette.ColorRole.Base, QColor("#1E1E1E"))
- palette.setColor(QPalette.ColorRole.AlternateBase, QColor("#252525"))
- palette.setColor(QPalette.ColorRole.Text, QColor("#D3D3D3"))
- palette.setColor(QPalette.ColorRole.Button, QColor("#505050"))
- palette.setColor(QPalette.ColorRole.ButtonText, QColor("#D3D3D3"))
- palette.setColor(QPalette.ColorRole.Highlight, QColor("#007acc"))
- palette.setColor(QPalette.ColorRole.HighlightedText, QColor("#FFFFFF"))
- stylesheet = get_dark_theme_stylesheet()
- else:
- # Jasny motyw – dla tych, co kodzą przy kawie w słońcu ☕
- palette.setColor(QPalette.ColorRole.Window, QColor("#F5F5F5"))
- palette.setColor(QPalette.ColorRole.WindowText, QColor("#222222"))
- palette.setColor(QPalette.ColorRole.Base, QColor("#FFFFFF"))
- palette.setColor(QPalette.ColorRole.AlternateBase, QColor("#F0F0F0"))
- palette.setColor(QPalette.ColorRole.Text, QColor("#222222"))
- palette.setColor(QPalette.ColorRole.Button, QColor("#E0E0E0"))
- palette.setColor(QPalette.ColorRole.ButtonText, QColor("#222222"))
- palette.setColor(QPalette.ColorRole.Highlight, QColor("#007acc"))
- palette.setColor(QPalette.ColorRole.HighlightedText, QColor("#FFFFFF"))
- stylesheet = get_light_theme_stylesheet()
- app.setPalette(palette)
- app.setStyleSheet(stylesheet)
- window.statusBar().showMessage(f"Zmieniono motyw na: {theme_name.capitalize()}")
- Ścieżka: /src/ui.py
- Rozmiar: 69,99 KB
- Zawartość:
- import sys
- import os
- import json
- import subprocess
- import re
- import platform
- import shutil
- import shlex
- from src.console import ConsoleWidget, ConsoleManager, AIChatManager
- from PyQt6.QtWidgets import (
- QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
- QSplitter, QTreeView, QTabWidget, QPlainTextEdit,
- QPushButton, QLineEdit, QFileDialog, QMenuBar, QToolBar, QStatusBar,
- QMessageBox, QMenu, QStyleFactory, QDialog, QFormLayout,
- QLabel, QDialogButtonBox, QComboBox, QToolButton,
- QInputDialog, QSpinBox, QSizePolicy, QAbstractItemView,
- QFrame
- )
- from PyQt6.QtGui import (
- QIcon, QAction, QKeySequence, QTextCharFormat, QFont,
- QSyntaxHighlighter, QTextDocument, QColor, QFileSystemModel,
- QDesktopServices, QPalette
- )
- from PyQt6.QtCore import (
- QDir, Qt, QProcess, QSettings, QFileInfo, QThread, pyqtSignal, QTimer, QSize,
- QStandardPaths, QUrl, QLocale, QCoreApplication, QProcessEnvironment
- )
- try:
- import qtawesome as qta
- except ImportError:
- qta = None
- print("Zainstaluj qtawesome ('pip install qtawesome') dla lepszych ikon.", file=sys.stderr)
- try:
- from src.dialogs import NewProjectDialog, NewItemDialog, RenameItemDialog, SettingsDialog
- from src.package_manager import PackageManagerDialog
- except ImportError:
- import sys, os
- sys.path.insert(0, os.path.join(os.path.dirname(__file__)))
- from dialogs import NewProjectDialog, NewItemDialog, RenameItemDialog, SettingsDialog
- from package_manager import PackageManagerDialog
- from src.utils import load_package_json, get_file_language
- from src.config import HIGHLIGHTING_RULES
- from src.theme import apply_theme
- # USTAWIENIE ROOT_DIR
- ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
- DATA_DIR = os.path.join(ROOT_DIR, 'userdata')
- PROJECTS_DIR = os.path.join(ROOT_DIR, 'projects')
- SETTINGS_FILE = os.path.join(DATA_DIR, 'settings.json')
- RECENTS_FILE = os.path.join(DATA_DIR, 'recents.json')
- os.makedirs(DATA_DIR, exist_ok=True)
- os.makedirs(PROJECTS_DIR, exist_ok=True)
- # Formatowanie podświetlania składni
- FORMAT_DEFAULT = QTextCharFormat()
- FORMAT_KEYWORD = QTextCharFormat()
- FORMAT_KEYWORD.setForeground(QColor("#000080")) # Navy
- FORMAT_STRING = QTextCharFormat()
- FORMAT_STRING.setForeground(QColor("#008000")) # Green
- FORMAT_COMMENT = QTextCharFormat()
- FORMAT_COMMENT.setForeground(QColor("#808080")) # Gray
- FORMAT_COMMENT.setFontItalic(True)
- FORMAT_FUNCTION = QTextCharFormat()
- FORMAT_FUNCTION.setForeground(QColor("#0000FF")) # Blue
- FORMAT_CLASS = QTextCharFormat()
- FORMAT_CLASS.setForeground(QColor("#A52A2A")) # Brown
- FORMAT_CLASS.setFontWeight(QFont.Weight.Bold)
- FORMAT_NUMBERS = QTextCharFormat()
- FORMAT_NUMBERS.setForeground(QColor("#FF0000")) # Red
- FORMAT_OPERATOR = QTextCharFormat()
- FORMAT_OPERATOR.setForeground(QColor("#A62929")) # Dark Red
- FORMAT_BUILTIN = QTextCharFormat()
- FORMAT_BUILTIN.setForeground(QColor("#008080")) # Teal
- FORMAT_SECTION = QTextCharFormat()
- FORMAT_SECTION.setForeground(QColor("#800080")) # Purple
- FORMAT_SECTION.setFontWeight(QFont.Weight.Bold)
- FORMAT_PROPERTY = QTextCharFormat()
- FORMAT_PROPERTY.setForeground(QColor("#B8860B")) # DarkGoldenrod
- class CodeSyntaxHighlighter(QSyntaxHighlighter):
- def __init__(self, parent: QTextDocument, language: str):
- super().__init__(parent)
- self._language = language.lower()
- self._rules = []
- lang_config = HIGHLIGHTING_RULES.get(self._language, {})
- keywords = lang_config.get('keywords', [])
- builtins = lang_config.get('builtins', [])
- patterns = lang_config.get('patterns', [])
- keyword_format = FORMAT_KEYWORD
- for keyword in keywords:
- pattern = r'\b' + re.escape(keyword) + r'\b'
- self._rules.append((re.compile(pattern), keyword_format))
- builtin_format = FORMAT_BUILTIN
- for builtin in builtins:
- pattern = r'\b' + re.escape(builtin) + r'\b'
- self._rules.append((re.compile(pattern), builtin_format))
- for pattern_str, format, *flags in patterns:
- try:
- pattern = re.compile(pattern_str, *flags)
- self._rules.append((pattern, format))
- except re.error as e:
- print(f"Błąd kompilacji regex '{pattern_str}' dla języka {self._language}: {e}", file=sys.stderr)
- def highlightBlock(self, text: str):
- self.setFormat(0, len(text), FORMAT_DEFAULT)
- self.setCurrentBlockState(0)
- block_comment_delimiters = []
- if self._language in ['javascript', 'css', 'c++']:
- block_comment_delimiters.append(("/*", "*/", FORMAT_COMMENT))
- if self._language == 'html':
- pass # HTML comments handled by regex
- comment_start_in_prev_block = (self.previousBlockState() == 1)
- if comment_start_in_prev_block:
- end_delimiter_index = text.find("*/")
- if end_delimiter_index >= 0:
- self.setFormat(0, end_delimiter_index + 2, FORMAT_COMMENT)
- self.setCurrentBlockState(0)
- start_pos = end_delimiter_index + 2
- else:
- self.setFormat(0, len(text), FORMAT_COMMENT)
- self.setCurrentBlockState(1)
- return
- else:
- start_pos = 0
- start_delimiter = "/*"
- end_delimiter = "*/"
- startIndex = text.find(start_delimiter, start_pos)
- while startIndex >= 0:
- endIndex = text.find(end_delimiter, startIndex)
- if endIndex >= 0:
- length = endIndex - startIndex + len(end_delimiter)
- self.setFormat(startIndex, startIndex + length, FORMAT_COMMENT)
- startIndex = text.find(start_delimiter, startIndex + length)
- else:
- self.setFormat(startIndex, len(text) - startIndex, FORMAT_COMMENT)
- self.setCurrentBlockState(1)
- break
- for pattern, format in self._rules:
- if format == FORMAT_COMMENT and (pattern.pattern.startswith(re.escape('/*')) or pattern.pattern.startswith(re.escape('<!--'))):
- continue
- if format == FORMAT_COMMENT and pattern.pattern.startswith('//') and self.currentBlockState() == 1:
- continue
- for match in pattern.finditer(text):
- start, end = match.span()
- self.setFormat(start, end, format)
- class CustomFileSystemModel(QFileSystemModel):
- def __init__(self, parent=None):
- super().__init__(parent)
- self.icon_map = {
- '.py': 'fa5s.file-code',
- '.js': 'fa5s.file-code',
- '.json': 'fa5s.file-code',
- '.html': 'fa5s.file-code',
- '.css': 'fa5s.file-code',
- '.ini': 'fa5s.file-alt',
- '.txt': 'fa5s.file-alt',
- '.md': 'fa5s.file-alt',
- '.c': 'fa5s.file-code',
- '.cpp': 'fa5s.file-code',
- '.h': 'fa5s.file-code',
- '.hpp': 'fa5s.file-code',
- }
- self.folder_icon_name = 'fa5s.folder'
- self.default_file_icon_name = 'fa5s.file'
- self._has_qtawesome = qta is not None
- def rename(self, index, new_name):
- if not index.isValid():
- return False
- old_path = self.filePath(index)
- new_path = os.path.join(os.path.dirname(old_path), new_name)
- try:
- os.rename(old_path, new_path)
- self.refresh()
- return True
- except Exception as e:
- print(f"Błąd podczas zmiany nazwy: {e}", file=sys.stderr)
- return False
- def data(self, index, role=Qt.ItemDataRole.DisplayRole):
- if not index.isValid():
- return None
- if role == Qt.ItemDataRole.DecorationRole:
- file_info = self.fileInfo(index)
- if file_info.isDir():
- return qta.icon(self.folder_icon_name) if self._has_qtawesome else super().data(index, role)
- elif file_info.isFile():
- extension = file_info.suffix().lower()
- dotted_extension = '.' + extension
- if dotted_extension in self.icon_map and self._has_qtawesome:
- return qta.icon(self.icon_map[dotted_extension])
- return qta.icon(self.default_file_icon_name) if self._has_qtawesome else super().data(index, role)
- return super().data(index, role)
- def refresh(self, *args):
- self.setRootPath(self.rootPath())
- class IDEWindow(QMainWindow):
- def __init__(self):
- super().__init__()
- self.settings = {
- "theme": "light",
- "python_path": "",
- "node_path": "",
- "show_tree": True,
- "show_console": True,
- "editor_font_size": 10,
- "api_key": os.getenv("XAI_API_KEY", ""),
- "gemini_api_key": "",
- "mistral_api_key": "",
- "ai_model": "grok-3",
- "ai_provider": "grok"
- }
- self.recents = {"last_project_dir": None, "open_files": []}
- self._load_app_state()
- self.setWindowTitle("Proste IDE - Bez nazwy")
- self.setGeometry(100, 100, 1200, 800)
- self.setWindowIcon(qta.icon('fa5s.code') if qta else QIcon.fromTheme("applications-development"))
- self.current_project_dir = self.recents.get("last_project_dir")
- self.open_files = {}
- self.base_editor_font = QFont("Courier New", 10)
- self._setup_ui()
- self._setup_menu()
- self._setup_toolbar()
- self._setup_status_bar()
- self._setup_connections()
- self._apply_theme(self.settings.get("theme", "light"))
- self._apply_editor_font_size()
- self.node_scripts = {}
- QTimer.singleShot(10, self._initial_setup)
- def _setup_ui(self):
- central_widget = QWidget()
- main_layout = QVBoxLayout(central_widget)
- main_layout.setContentsMargins(0, 0, 0, 0)
- self.main_splitter = QSplitter(Qt.Orientation.Horizontal)
- main_layout.addWidget(self.main_splitter)
- self.project_model = CustomFileSystemModel()
- self.project_model.setFilter(QDir.Filter.AllDirs | QDir.Filter.Files | QDir.Filter.NoDotAndDotDot)
- self.project_tree = QTreeView()
- self.project_tree.setModel(self.project_model)
- self.project_tree.setHeaderHidden(True)
- self.project_tree.hideColumn(1)
- self.project_tree.hideColumn(2)
- self.project_tree.hideColumn(3)
- self.project_tree.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
- self.main_splitter.addWidget(self.project_tree)
- self.right_splitter = QSplitter(Qt.Orientation.Vertical)
- self.main_splitter.addWidget(self.right_splitter)
- self.tab_widget = QTabWidget()
- self.tab_widget.setTabsClosable(True)
- self.tab_widget.setMovable(True)
- self.right_splitter.addWidget(self.tab_widget)
- self.console_manager = ConsoleManager(self)
- self.ai_chat_manager = AIChatManager(self.settings, self)
- self.console_widget = ConsoleWidget(self.console_manager, self.ai_chat_manager)
- self.right_splitter.addWidget(self.console_widget)
- self.main_splitter.setSizes([200, 800])
- self.right_splitter.setSizes([600, 200])
- self.setCentralWidget(central_widget)
- self.action_toggle_tree = QAction("Pokaż/Ukryj drzewko", self)
- self.action_toggle_tree.setCheckable(True)
- self.action_toggle_tree.setChecked(True)
- self.action_toggle_tree.triggered.connect(self._toggle_tree_panel)
- self.action_toggle_console = QAction("Pokaż/Ukryj konsolę i chat ai", self)
- self.action_toggle_console.setCheckable(True)
- self.action_toggle_console.setChecked(True)
- self.action_toggle_console.triggered.connect(self._toggle_console_panel)
- self._apply_view_settings()
- def _apply_view_settings(self):
- """Stosuje ustawienia widoczności paneli z ustawień."""
- show_tree = self.settings.get("show_tree", True)
- show_console = self.settings.get("show_console", True)
- self.main_splitter.widget(0).setVisible(show_tree)
- self.right_splitter.widget(1).setVisible(show_console)
- self.action_toggle_tree.setChecked(show_tree)
- self.action_toggle_console.setChecked(show_console)
- def _toggle_tree_panel(self, checked):
- self.main_splitter.widget(0).setVisible(checked)
- self.settings["show_tree"] = checked
- self._save_app_state()
- def _toggle_console_panel(self, checked):
- self.right_splitter.widget(1).setVisible(checked)
- self.settings["show_console"] = checked
- self._save_app_state()
- def _setup_menu(self):
- menu_bar = self.menuBar()
- file_menu = menu_bar.addMenu("&Plik")
- self.action_new_project = QAction(qta.icon('fa5s.folder-plus') if qta else QIcon(), "&Nowy projekt...", self)
- self.action_new_project.triggered.connect(self._new_project)
- file_menu.addAction(self.action_new_project)
- self.action_open_folder = QAction(qta.icon('fa5s.folder-open') if qta else QIcon(), "Otwórz &folder projektu...", self)
- self.action_open_folder.triggered.connect(lambda: self._open_project_folder())
- file_menu.addAction(self.action_open_folder)
- self.action_open_file = QAction(qta.icon('fa5s.file-code') if qta else QIcon(), "Otwórz &plik...", self)
- self.action_open_file.triggered.connect(self._open_file_dialog)
- file_menu.addAction(self.action_open_file)
- file_menu.addSeparator()
- self.recent_files_menu = QMenu("Ostatnio otwierane", self)
- file_menu.addMenu(self.recent_files_menu)
- file_menu.addSeparator()
- self.action_save = QAction(qta.icon('fa5s.save') if qta else QIcon(), "&Zapisz", self)
- self.action_save.setShortcut(QKeySequence.StandardKey.Save)
- self.action_save.triggered.connect(self._save_current_file)
- file_menu.addAction(self.action_save)
- self.action_save_as = QAction(qta.icon('fa5s.file-export') if qta else QIcon(), "Zapisz &jako...", self)
- self.action_save_as.setShortcut(QKeySequence.StandardKey.SaveAs)
- self.action_save_as.triggered.connect(self._save_current_file_as)
- file_menu.addAction(self.action_save_as)
- self.action_save_all = QAction(qta.icon('fa5s.save') if qta else QIcon(), "Zapisz wszys&tko", self)
- self.action_save_all.setShortcut(QKeySequence("Ctrl+Shift+S"))
- self.action_save_all.triggered.connect(self._save_all_files)
- file_menu.addAction(self.action_save_all)
- file_menu.addSeparator()
- self.action_close_file = QAction(qta.icon('fa5s.window-close') if qta else QIcon(), "Zamknij ak&tualny plik", self)
- self.action_close_file.triggered.connect(self._close_current_tab)
- file_menu.addAction(self.action_close_file)
- file_menu.addSeparator()
- self.action_exit = QAction(qta.icon('fa5s.door-open') if qta else QIcon(), "&Zakończ", self)
- self.action_exit.setShortcut(QKeySequence.StandardKey.Quit)
- self.action_exit.triggered.connect(self.close)
- file_menu.addAction(self.action_exit)
- edit_menu = menu_bar.addMenu("&Edycja")
- view_menu = menu_bar.addMenu("&Widok")
- self.action_toggle_tree = QAction(qta.icon('fa5s.sitemap') if qta else QIcon(), "Pokaż &drzewko plików", self)
- self.action_toggle_tree.setCheckable(True)
- self.action_toggle_tree.setChecked(self.settings.get("show_tree", True))
- self.action_toggle_tree.triggered.connect(self._toggle_tree_panel)
- view_menu.addAction(self.action_toggle_tree)
- self.action_toggle_console = QAction(qta.icon('fa5s.terminal') if qta else QIcon(), "Pokaż &konsolę i chat ai", self)
- self.action_toggle_console.setCheckable(True)
- self.action_toggle_console.setChecked(self.settings.get("show_console", True))
- self.action_toggle_console.triggered.connect(self._toggle_console_panel)
- view_menu.addAction(self.action_toggle_console)
- search_menu = menu_bar.addMenu("&Wyszukaj")
- self.action_find = QAction(qta.icon('fa5s.search') if qta else QIcon(), "&Znajdź...", self)
- self.action_find.setShortcut(QKeySequence.StandardKey.Find)
- self.action_find.triggered.connect(self._show_find_bar)
- search_menu.addAction(self.action_find)
- run_menu = menu_bar.addMenu("&Uruchom")
- self.action_run_file = QAction(qta.icon('fa5s.play') if qta else QIcon(), "&Uruchom aktualny plik", self)
- self.action_run_file.setShortcut(QKeySequence("F5"))
- self.action_run_file.triggered.connect(self._run_current_file)
- run_menu.addAction(self.action_run_file)
- tools_menu = menu_bar.addMenu("&Narzędzia")
- self.action_settings = QAction(qta.icon('fa5s.cog') if qta else QIcon(), "&Ustawienia...", self)
- self.action_settings.triggered.connect(self._show_settings_dialog)
- tools_menu.addAction(self.action_settings)
- self.action_package_manager = QAction(qta.icon('fa5s.box-open') if qta else QIcon(), "Menadżer pakietów", self)
- self.action_package_manager.triggered.connect(self._show_package_manager)
- tools_menu.addAction(self.action_package_manager)
- help_menu = menu_bar.addMenu("&Pomoc")
- self.action_about = QAction(qta.icon('fa5s.info-circle') if qta else QIcon(), "&O programie...", self)
- self.action_about.triggered.connect(self._show_about_dialog)
- help_menu.addAction(self.action_about)
- def _setup_toolbar(self):
- toolbar = self.addToolBar("Główne narzędzia")
- toolbar.setMovable(False)
- toolbar.setIconSize(QSize(16, 16))
- toolbar.addAction(self.action_new_project)
- toolbar.addAction(self.action_open_folder)
- toolbar.addAction(self.action_open_file)
- toolbar.addSeparator()
- toolbar.addAction(self.action_save)
- toolbar.addAction(self.action_save_all)
- toolbar.addSeparator()
- self.run_toolbutton = QToolButton(self)
- self.run_toolbutton.setDefaultAction(self.action_run_file)
- self.run_toolbutton.setPopupMode(QToolButton.ToolButtonPopupMode.MenuButtonPopup)
- toolbar.addWidget(self.run_toolbutton)
- toolbar.addSeparator()
- self.search_input = QLineEdit(self)
- self.search_input.setPlaceholderText("Szukaj w pliku...")
- self.search_input.setClearButtonEnabled(True)
- self.search_input.returnPressed.connect(lambda: self._find_text(self.search_input.text(), 'next'))
- self.find_next_button = QPushButton("Znajdź dalej")
- self.find_next_button.clicked.connect(lambda: self._find_text(self.search_input.text(), 'next'))
- self.find_prev_button = QPushButton("Znajdź poprzedni")
- self.find_prev_button.clicked.connect(lambda: self._find_text(self.search_input.text(), 'previous'))
- toolbar.addWidget(self.search_input)
- toolbar.addWidget(self.find_next_button)
- toolbar.addWidget(self.find_prev_button)
- self.search_input.setVisible(False)
- self.find_next_button.setVisible(False)
- self.find_prev_button.setVisible(False)
- def _setup_status_bar(self):
- self.statusBar().showMessage("Gotowy.")
- def _setup_connections(self):
- self.project_tree.doubleClicked.connect(self._handle_tree_item_double_click)
- self.tab_widget.tabCloseRequested.connect(self._close_tab_by_index)
- self.tab_widget.currentChanged.connect(self._handle_tab_change)
- self.project_tree.customContextMenuRequested.connect(self._show_project_tree_context_menu)
- def _initial_setup(self):
- initial_dir = self.recents.get("last_project_dir")
- if not initial_dir or not os.path.isdir(initial_dir):
- initial_dir = PROJECTS_DIR
- os.makedirs(PROJECTS_DIR, exist_ok=True)
- if os.path.isdir(initial_dir):
- self._open_project_folder(initial_dir)
- else:
- self.statusBar().showMessage("Brak domyślnego katalogu projektu. Otwórz folder ręcznie lub utwórz nowy.")
- self.project_model.setRootPath("")
- self.current_project_dir = None
- self._update_run_button_menu()
- recent_files = self.recents.get("open_files", [])
- QTimer.singleShot(200, lambda: self._reopen_files(recent_files))
- self._update_recent_files_menu()
- def _load_app_state(self):
- try:
- if os.path.exists(SETTINGS_FILE):
- with open(SETTINGS_FILE, 'r', encoding='utf-8') as f:
- loaded_settings = json.load(f)
- self.settings.update({
- "theme": loaded_settings.get("theme", "light"),
- "python_path": loaded_settings.get("python_path", ""),
- "node_path": loaded_settings.get("node_path", ""),
- "show_tree": loaded_settings.get("show_tree", True),
- "show_console": loaded_settings.get("show_console", True),
- "editor_font_size": loaded_settings.get("editor_font_size", 10),
- "api_key": loaded_settings.get("api_key", os.getenv("XAI_API_KEY", "")),
- "gemini_api_key": loaded_settings.get("gemini_api_key", ""),
- "mistral_api_key": loaded_settings.get("mistral_api_key", ""),
- "ai_model": loaded_settings.get("ai_model", "grok-3"),
- "ai_provider": loaded_settings.get("ai_provider", "grok")
- })
- if os.path.exists(RECENTS_FILE):
- with open(RECENTS_FILE, 'r', encoding='utf-8') as f:
- loaded_recents = json.load(f)
- self.recents.update({
- "last_project_dir": loaded_recents.get("last_project_dir"),
- "open_files": loaded_recents.get("open_files", [])
- })
- except (json.JSONDecodeError, Exception) as e:
- print(f"Błąd podczas wczytywania stanu aplikacji: {e}", file=sys.stderr)
- def _save_app_state(self):
- try:
- self.recents["open_files"] = list(self.open_files.keys())
- if self.current_project_dir and os.path.isdir(self.current_project_dir):
- self.recents["last_project_dir"] = os.path.normpath(self.current_project_dir)
- else:
- self.recents["last_project_dir"] = None
- with open(SETTINGS_FILE, 'w', encoding='utf-8') as f:
- json.dump(self.settings, f, indent=4)
- with open(RECENTS_FILE, 'w', encoding='utf-8') as f:
- normalized_open_files = [os.path.normpath(p) for p in self.recents["open_files"]]
- unique_open_files = []
- for p in normalized_open_files:
- if p not in unique_open_files:
- unique_open_files.append(p)
- self.recents["open_files"] = unique_open_files[:20]
- json.dump(self.recents, f, indent=4)
- except Exception as e:
- print(f"Błąd podczas zapisu stanu aplikacji: {e}", file=sys.stderr)
- def closeEvent(self, event):
- unsaved_files = [path for path, editor in self.open_files.items() if editor.document().isModified()]
- if unsaved_files:
- msg_box = QMessageBox(self)
- msg_box.setIcon(QMessageBox.Icon.Warning)
- msg_box.setWindowTitle("Niezapisane zmiany")
- msg_box.setText(f"Masz niezapisane zmiany w {len(unsaved_files)} plikach.\nCzy chcesz zapisać przed zamknięciem?")
- msg_box.setStandardButtons(QMessageBox.StandardButton.Save | QMessageBox.StandardButton.Discard | QMessageBox.StandardButton.Cancel)
- msg_box.setDefaultButton(QMessageBox.StandardButton.Save)
- reply = msg_box.exec()
- if reply == QMessageBox.StandardButton.Save:
- if self._save_all_files():
- self._save_app_state()
- event.accept()
- else:
- event.ignore()
- elif reply == QMessageBox.StandardButton.Discard:
- for i in range(self.tab_widget.count() - 1, -1, -1):
- widget = self.tab_widget.widget(i)
- if hasattr(widget, 'document'):
- widget.document().setModified(False)
- self._close_tab_by_index(i)
- self._save_app_state()
- event.accept()
- else:
- event.ignore()
- else:
- self._save_app_state()
- event.accept()
- def _new_project(self):
- dialog = NewProjectDialog(PROJECTS_DIR, self)
- if dialog.exec() == QDialog.DialogCode.Accepted:
- project_name = dialog.get_project_name()
- project_path = dialog.get_project_path()
- try:
- if os.path.exists(project_path):
- QMessageBox.warning(self, "Projekt już istnieje", f"Projekt o nazwie '{project_name}' już istnieje.")
- return
- os.makedirs(project_path)
- self.statusBar().showMessage(f"Utworzono nowy projekt: {project_name}")
- self._open_project_folder(project_path)
- except OSError as e:
- QMessageBox.critical(self, "Błąd tworzenia projektu", f"Nie można utworzyć katalogu projektu:\n{e}")
- self.statusBar().showMessage("Błąd tworzenia projektu.")
- def _open_project_folder(self, path=None):
- if path is None:
- start_path = self.current_project_dir if self.current_project_dir else PROJECTS_DIR
- dialog_path = QFileDialog.getExistingDirectory(self, "Otwórz folder projektu", start_path)
- if not dialog_path:
- return
- path = dialog_path
- path = os.path.normpath(path)
- if not os.path.isdir(path):
- QMessageBox.critical(self, "Błąd", f"Wybrana ścieżka nie jest katalogiem lub nie istnieje:\n{path}")
- self.statusBar().showMessage(f"Błąd: Nie można otworzyć folderu: {path}")
- return
- if self.current_project_dir and self.current_project_dir != path:
- unsaved_files_count = sum(1 for editor in self.open_files.values() if editor.document().isModified())
- if unsaved_files_count > 0:
- reply = QMessageBox.question(self, "Niezapisane zmiany",
- f"Obecny projekt ma {unsaved_files_count} niezapisanych plików.\n"
- "Czy chcesz zapisać zmiany przed otwarciem nowego folderu?",
- QMessageBox.StandardButton.Save | QMessageBox.StandardButton.Discard | QMessageBox.StandardButton.Cancel)
- if reply == QMessageBox.StandardButton.Cancel:
- self.statusBar().showMessage("Otwieranie folderu anulowane.")
- return
- if reply == QMessageBox.StandardButton.Save:
- if not self._save_all_files():
- self.statusBar().showMessage("Otwieranie folderu anulowane (błąd zapisu).")
- return
- self._close_all_files()
- self.current_project_dir = path
- self.project_model.setRootPath(path)
- root_index = self.project_model.index(path)
- if not root_index.isValid():
- QMessageBox.critical(self, "Błąd", f"Nie można ustawić katalogu głównego drzewka dla ścieżki:\n{path}")
- self.statusBar().showMessage(f"Błąd ustawienia katalogu głównego: {path}")
- self.project_tree.setRootIndex(self.project_model.index(""))
- self.current_project_dir = None
- 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)]
- self._update_recent_files_menu()
- self._save_app_state()
- self._update_run_button_menu()
- return
- self.project_tree.setRootIndex(root_index)
- self.setWindowTitle(f"Proste IDE - {os.path.basename(path)}")
- self.statusBar().showMessage(f"Otwarto folder: {path}")
- self._check_package_json(path)
- self.recents["last_project_dir"] = path
- self._save_app_state()
- def _close_all_files(self):
- for file_path in list(self.open_files.keys()):
- editor_widget = self.open_files.get(file_path)
- if editor_widget:
- tab_index = self.tab_widget.indexOf(editor_widget)
- if tab_index != -1:
- if hasattr(editor_widget, 'document'):
- editor_widget.document().setModified(False)
- self.tab_widget.removeTab(tab_index)
- if file_path in self.open_files:
- del self.open_files[file_path]
- self.recents["open_files"] = []
- self._update_recent_files_menu()
- def _open_file_dialog(self):
- start_path = self.current_project_dir if self.current_project_dir else PROJECTS_DIR
- 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)")
- if file_path:
- self._open_file(file_path)
- def _open_file(self, file_path):
- file_path = os.path.normpath(file_path)
- if not os.path.exists(file_path) or not os.path.isfile(file_path):
- self.statusBar().showMessage(f"Błąd: Plik nie istnieje lub nie jest plikiem: {file_path}")
- if file_path in self.recents["open_files"]:
- self.recents["open_files"].remove(file_path)
- self._update_recent_files_menu()
- self._save_app_state()
- return
- if file_path in self.open_files:
- index = -1
- for i in range(self.tab_widget.count()):
- widget = self.tab_widget.widget(i)
- if self.open_files.get(file_path) is widget:
- index = i
- break
- if index != -1:
- self.tab_widget.setCurrentIndex(index)
- self.statusBar().showMessage(f"Plik {os.path.basename(file_path)} jest już otwarty.")
- if file_path in self.recents["open_files"]:
- self.recents["open_files"].remove(file_path)
- self.recents["open_files"].insert(0, file_path)
- self._update_recent_files_menu()
- self._save_app_state()
- return
- try:
- content = ""
- try:
- with open(file_path, 'r', encoding='utf-8') as f:
- content = f.read()
- except UnicodeDecodeError:
- try:
- with open(file_path, 'r', encoding='latin-1') as f:
- content = f.read()
- except Exception:
- with open(file_path, 'r', encoding='utf-8', errors='replace') as f:
- content = f.read()
- except Exception as e:
- QMessageBox.critical(self, "Błąd otwarcia pliku", f"Nie można odczytać pliku {os.path.basename(file_path)}:\n{e}")
- self.statusBar().showMessage(f"Błąd otwarcia pliku: {os.path.basename(file_path)}")
- return
- editor = QPlainTextEdit()
- editor.setPlainText(content)
- editor.setFont(self.base_editor_font)
- editor.document().setModified(False)
- editor.document().modificationChanged.connect(self._handle_modification_changed)
- language = self._get_language_from_path(file_path)
- highlighter = CodeSyntaxHighlighter(editor.document(), language)
- setattr(editor.document(), '_syntax_highlighter', highlighter)
- tab_index = self.tab_widget.addTab(editor, os.path.basename(file_path))
- self.tab_widget.setCurrentIndex(tab_index)
- self.open_files[file_path] = editor
- self.statusBar().showMessage(f"Otwarto plik: {file_path}")
- if file_path in self.recents["open_files"]:
- self.recents["open_files"].remove(file_path)
- self.recents["open_files"].insert(0, file_path)
- self._update_recent_files_menu()
- self._save_app_state()
- def _reopen_files(self, file_list):
- files_to_reopen = list(file_list)
- valid_files = [f for f in files_to_reopen if os.path.exists(f) and os.path.isfile(f)]
- self.recents["open_files"] = valid_files
- self._update_recent_files_menu()
- for file_path in valid_files:
- QTimer.singleShot(0, lambda path=file_path: self._open_file(path))
- invalid_files = [f for f in files_to_reopen if f not in valid_files]
- if invalid_files:
- msg = "Nie można ponownie otworzyć następujących plików (nie znaleziono):\n" + "\n".join(invalid_files)
- QMessageBox.warning(self, "Błąd otwarcia plików", msg)
- def _update_recent_files_menu(self):
- self.recent_files_menu.clear()
- recent_items_to_show = list(self.recents.get("open_files", []))[:15]
- if not recent_items_to_show:
- self.recent_files_menu.addAction("Brak ostatnio otwieranych plików").setEnabled(False)
- return
- actions_to_add = []
- cleaned_recent_files = []
- for file_path in recent_items_to_show:
- if os.path.exists(file_path) and os.path.isfile(file_path):
- cleaned_recent_files.append(file_path)
- menu_text = os.path.basename(file_path)
- action = QAction(menu_text, self)
- action.setData(file_path)
- action.triggered.connect(lambda checked, path=file_path: self._open_file(path))
- actions_to_add.append(action)
- all_existing_recent_files = [p for p in self.recents.get("open_files", []) if os.path.exists(p) and os.path.isfile(p)]
- unique_recent_files = []
- for p in all_existing_recent_files:
- if p not in unique_recent_files:
- unique_recent_files.append(p)
- self.recents["open_files"] = unique_recent_files[:20]
- for action in actions_to_add:
- self.recent_files_menu.addAction(action)
- self._save_app_state()
- def _show_project_tree_context_menu(self, point):
- index = self.project_tree.indexAt(point)
- menu = QMenu(self)
- if index.isValid():
- file_path = self.project_model.filePath(index)
- file_info = self.project_model.fileInfo(index)
- if file_info.isFile():
- open_action = QAction("Otwórz", self)
- open_action.triggered.connect(lambda: self._open_file(file_path))
- menu.addAction(open_action)
- # Dodaj opcję "Otwórz jako projekt" dla folderów
- if file_info.isDir():
- open_as_project_action = QAction("Otwórz jako projekt", self)
- open_as_project_action.triggered.connect(lambda: self._open_project_folder(file_path))
- menu.addAction(open_as_project_action)
- new_file_action = QAction("Nowy plik", self)
- new_file_action.triggered.connect(lambda: self._create_new_item(index, is_folder=False))
- menu.addAction(new_file_action)
- new_folder_action =QAction("Nowy folder", self)
- new_folder_action.triggered.connect(lambda: self._create_new_item(index, is_folder=True))
- menu.addAction(new_folder_action)
- rename_action = QAction("Zmień nazwę", self)
- rename_action.triggered.connect(lambda: self._rename_item(index))
- menu.addAction(rename_action)
- delete_action = QAction("Usuń", self)
- delete_action.triggered.connect(lambda: self._delete_item(index))
- menu.addAction(delete_action)
- if file_info.isFile():
- duplicate_action = QAction("Duplikuj", self)
- duplicate_action.triggered.connect(lambda: self._duplicate_file(index))
- menu.addAction(duplicate_action)
- else:
- new_file_action = QAction("Nowy plik", self)
- new_file_action.triggered.connect(lambda: self._create_new_item(None, is_folder=False))
- menu.addAction(new_file_action)
- new_folder_action = QAction("Nowy folder", self)
- new_folder_action.triggered.connect(lambda: self._create_new_item(None, is_folder=True))
- menu.addAction(new_folder_action)
- menu.exec(self.project_tree.mapToGlobal(point))
- def _create_new_item(self, index, is_folder=False):
- parent_dir = self.current_project_dir
- if index and index.isValid():
- file_path = self.project_model.filePath(index)
- if self.project_model.fileInfo(index).isDir():
- parent_dir = file_path
- else:
- parent_dir = os.path.dirname(file_path)
- dialog = NewItemDialog(parent_dir, is_folder, self)
- if dialog.exec() == QDialog.DialogCode.Accepted:
- item_name = dialog.get_item_name()
- full_path = os.path.join(parent_dir, item_name)
- try:
- if is_folder:
- os.makedirs(full_path, exist_ok=True)
- else:
- with open(full_path, 'w', encoding='utf-8') as f:
- f.write('')
- self.statusBar().showMessage(f"Utworzono: {item_name}")
- parent_index = self.project_model.index(parent_dir)
- if parent_index.isValid():
- self.project_model.refresh(parent_index)
- except OSError as e:
- QMessageBox.critical(self, "Błąd tworzenia", f"Nie można utworzyć {item_name}:\n{e}")
- self.statusBar().showMessage("Błąd tworzenia.")
- def _rename_item(self, index):
- if not index.isValid():
- return
- current_path = self.project_model.filePath(index)
- dialog = RenameItemDialog(current_path, self)
- if dialog.exec() == QDialog.DialogCode.Accepted:
- new_name = dialog.get_new_name()
- if self.project_model.rename(index, new_name):
- self.statusBar().showMessage(f"Zmieniono nazwę na: {new_name}")
- else:
- QMessageBox.critical(self, "Błąd zmiany nazwy", f"Nie można zmienić nazwy na '{new_name}'.")
- self.statusBar().showMessage("Błąd zmiany nazwy.")
- def _delete_item(self, index):
- if not index.isValid():
- return
- file_path = self.project_model.filePath(index)
- file_info = self.project_model.fileInfo(index)
- item_name = file_info.fileName()
- is_dir = file_info.isDir()
- open_files_to_close = []
- if is_dir:
- for open_file_path in self.open_files:
- if os.path.normpath(open_file_path).startswith(os.path.normpath(file_path) + os.sep):
- open_files_to_close.append(open_file_path)
- else:
- if file_path in self.open_files:
- open_files_to_close.append(file_path)
- if open_files_to_close:
- reply_close = QMessageBox.question(self, "Otwarte pliki",
- f"Element '{item_name}' zawiera {len(open_files_to_close)} otwartych plików.\n"
- f"Czy chcesz zamknąć te pliki, aby kontynuować usuwanie '{item_name}'?",
- QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
- if reply_close == QMessageBox.StandardButton.No:
- self.statusBar().showMessage(f"Usuwanie '{item_name}' anulowane.")
- return
- unsaved_open_files = [p for p in open_files_to_close if self.open_files.get(p) and self.open_files[p].document().isModified()]
- if unsaved_open_files:
- save_reply = QMessageBox.question(self, "Niezapisane zmiany",
- f"Niektóre z plików ({len(unsaved_open_files)}) mają niezapisane zmiany. Czy chcesz je zapisać przed zamknięciem i usunięciem?",
- QMessageBox.StandardButton.Save | QMessageBox.StandardButton.Discard | QMessageBox.StandardButton.Cancel)
- if save_reply == QMessageBox.StandardButton.Cancel:
- self.statusBar().showMessage("Usuwanie anulowane (niezapisane zmiany).")
- return
- if save_reply == QMessageBox.StandardButton.Save:
- save_success = True
- for file_path_to_save in unsaved_open_files:
- editor = self.open_files.get(file_path_to_save)
- if editor and not self._save_file(editor, file_path_to_save):
- save_success = False
- break
- if not save_success:
- self.statusBar().showMessage("Usuwanie anulowane (błąd zapisu otwartych plików).")
- return
- for file_path_to_close in reversed(open_files_to_close):
- editor_widget = self.open_files.get(file_path_to_close)
- if editor_widget:
- tab_index = self.tab_widget.indexOf(editor_widget)
- if tab_index != -1:
- if hasattr(editor_widget, 'document'):
- editor_widget.document().setModified(False)
- self.tab_widget.removeTab(tab_index)
- del self.open_files[file_path_to_close]
- editor_widget.deleteLater()
- self.recents["open_files"] = [p for p in self.recents["open_files"] if p not in open_files_to_close]
- self._update_recent_files_menu()
- self._save_app_state()
- item_type = "folder" if is_dir else "plik"
- reply = QMessageBox.question(self, f"Usuń {item_type}",
- f"Czy na pewno chcesz usunąć {item_type} '{item_name}'?\n"
- "Ta operacja jest nieodwracalna!",
- QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
- if reply == QMessageBox.StandardButton.Yes:
- success = self.project_model.remove(index)
- if success:
- self.statusBar().showMessage(f"Usunięto {item_type}: {item_name}")
- else:
- QMessageBox.critical(self, f"Błąd usuwania {item_type}", f"Nie można usunąć {item_type} '{item_name}'.")
- self.statusBar().showMessage(f"Błąd usuwania {item_type}.")
- def _duplicate_file(self, index):
- if not index.isValid():
- return
- file_path = self.project_model.filePath(index)
- file_info = self.project_model.fileInfo(index)
- if not file_info.isFile():
- self.statusBar().showMessage("Można duplikować tylko pliki.")
- return
- parent_dir = os.path.dirname(file_path)
- old_name = os.path.basename(file_path)
- name, ext = os.path.splitext(old_name)
- suggested_name = f"{name}_kopia{ext}"
- counter = 1
- while os.path.exists(os.path.join(parent_dir, suggested_name)):
- counter += 1
- suggested_name = f"{name}_kopia{counter}{ext}"
- new_name, ok = QInputDialog.getText(self, "Duplikuj plik", f"Podaj nazwę dla kopii '{old_name}':",
- QLineEdit.EchoMode.Normal, suggested_name)
- if ok and new_name:
- new_name = new_name.strip()
- if not new_name or re.search(r'[<>:"/\\|?*\x00-\x1F]', new_name) is not None:
- QMessageBox.warning(self, "Nieprawidłowa nazwa", "Podana nazwa jest pusta lub zawiera niedozwolone znaki.")
- self.statusBar().showMessage("Duplikowanie anulowane (nieprawidłowa nazwa).")
- return
- new_path = os.path.join(parent_dir, new_name)
- if os.path.exists(new_path):
- QMessageBox.warning(self, "Element już istnieje", f"Element o nazwie '{new_name}' już istnieje.")
- self.statusBar().showMessage("Duplikowanie anulowane (element już istnieje).")
- return
- try:
- os.makedirs(os.path.dirname(new_path), exist_ok=True)
- shutil.copy2(file_path, new_path)
- self.statusBar().showMessage(f"Utworzono kopię: {new_name}")
- parent_index = self.project_model.index(parent_dir)
- if parent_index.isValid():
- self.project_model.refresh(parent_index)
- else:
- root_path = self.project_model.rootPath()
- if root_path and os.path.isdir(root_path):
- self.project_model.refresh(self.project_model.index(root_path))
- except OSError as e:
- QMessageBox.critical(self, "Błąd duplikowania", f"Nie można zduplikować pliku '{old_name}':\n{e}")
- self.statusBar().showMessage("Błąd duplikowania pliku.")
- def _close_tab_by_index(self, index):
- if index == -1:
- return
- widget = self.tab_widget.widget(index)
- if widget is None:
- return
- file_path_before_save = None
- for path, editor_widget in list(self.open_files.items()):
- if editor_widget is widget:
- file_path_before_save = path
- break
- if hasattr(widget, 'document') and widget.document().isModified():
- msg_box = QMessageBox(self)
- msg_box.setIcon(QMessageBox.Icon.Warning)
- msg_box.setWindowTitle("Niezapisane zmiany")
- tab_text = self.tab_widget.tabText(index).rstrip('*')
- display_name = os.path.basename(file_path_before_save) if file_path_before_save else tab_text
- msg_box.setText(f"Plik '{display_name}' ma niezapisane zmiany.\nCzy chcesz zapisać przed zamknięciem?")
- msg_box.setStandardButtons(QMessageBox.StandardButton.Save | QMessageBox.StandardButton.Discard | QMessageBox.StandardButton.Cancel)
- msg_box.setDefaultButton(QMessageBox.StandardButton.Save)
- reply = msg_box.exec()
- if reply == QMessageBox.StandardButton.Save:
- needs_save_as = (file_path_before_save is None or
- not os.path.exists(file_path_before_save) or
- not QFileInfo(file_path_before_save).isFile())
- if needs_save_as:
- original_index = self.tab_widget.currentIndex()
- self.tab_widget.setCurrentIndex(index)
- save_success = self._save_current_file_as()
- if original_index != -1 and original_index < self.tab_widget.count():
- self.tab_widget.setCurrentIndex(original_index)
- if not save_success:
- self.statusBar().showMessage(f"Zamknięcie anulowane (błąd zapisu '{display_name}').")
- return
- else:
- if not self._save_file(widget, file_path_before_save):
- self.statusBar().showMessage(f"Zamknięcie anulowane (błąd zapisu '{display_name}').")
- return
- elif reply == QMessageBox.StandardButton.Cancel:
- self.statusBar().showMessage(f"Zamknięcie '{tab_text}' anulowane.")
- return
- if file_path_before_save in self.open_files:
- del self.open_files[file_path_before_save]
- if file_path_before_save in self.recents["open_files"]:
- self.recents["open_files"].remove(file_path_before_save)
- self._update_recent_files_menu()
- self.tab_widget.removeTab(index)
- widget.deleteLater()
- if file_path_before_save:
- self.statusBar().showMessage(f"Zamknięto plik: {os.path.basename(file_path_before_save)}")
- else:
- self.statusBar().showMessage("Zamknięto plik.")
- self._save_app_state()
- def _close_current_tab(self):
- current_index = self.tab_widget.currentIndex()
- if current_index != -1:
- self._close_tab_by_index(current_index)
- def _save_current_file(self):
- current_widget = self.tab_widget.currentWidget()
- if not isinstance(current_widget, QPlainTextEdit):
- self.statusBar().showMessage("Brak aktywnego pliku do zapisu.")
- return False
- file_path = None
- for path, editor_widget in list(self.open_files.items()):
- if editor_widget is current_widget:
- file_path = path
- break
- is_existing_valid_file = file_path and os.path.exists(file_path) and QFileInfo(file_path).isFile()
- if is_existing_valid_file:
- return self._save_file(current_widget, file_path)
- else:
- return self._save_current_file_as()
- def _save_current_file_as(self):
- current_widget = self.tab_widget.currentWidget()
- if not isinstance(current_widget, QPlainTextEdit):
- self.statusBar().showMessage("Brak aktywnego pliku do zapisu.")
- return False
- old_file_path = None
- for path, editor_widget in list(self.open_files.items()):
- if editor_widget is current_widget:
- old_file_path = path
- break
- suggested_name = "bez_nazwy.txt"
- current_tab_index = self.tab_widget.indexOf(current_widget)
- if current_tab_index != -1:
- original_tab_text = self.tab_widget.tabText(current_tab_index).rstrip('*')
- if original_tab_text and original_tab_text != "Nowy plik":
- suggested_name = original_tab_text
- elif current_widget.document().toPlainText().strip():
- first_line = current_widget.document().toPlainText().strip().split('\n')[0].strip()
- if first_line:
- suggested_name = re.sub(r'[\\/:*?"<>|]', '_', first_line)
- suggested_name = suggested_name[:50].strip()
- if not suggested_name:
- suggested_name = "bez_nazwy"
- if '.' not in os.path.basename(suggested_name):
- suggested_name += ".txt"
- else:
- suggested_name = "bez_nazwy.txt"
- start_path = self.current_project_dir if self.current_project_dir else PROJECTS_DIR
- if old_file_path and os.path.dirname(old_file_path):
- start_path = os.path.dirname(old_file_path)
- elif os.path.isdir(start_path):
- pass
- else:
- start_path = os.path.expanduser("~")
- 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)"
- new_file_path, _ = QFileDialog.getSaveFileName(self, "Zapisz plik jako...", os.path.join(start_path, suggested_name), file_filters)
- if not new_file_path:
- self.statusBar().showMessage("Zapisywanie anulowane.")
- return False
- new_file_path = os.path.normpath(new_file_path)
- if old_file_path and old_file_path != new_file_path:
- if old_file_path in self.open_files:
- del self.open_files[old_file_path]
- if old_file_path in self.recents["open_files"]:
- self.recents["open_files"].remove(old_file_path)
- self._update_recent_files_menu()
- self.open_files[new_file_path] = current_widget
- current_tab_index = self.tab_widget.indexOf(current_widget)
- if current_tab_index != -1:
- self.tab_widget.setTabText(current_tab_index, os.path.basename(new_file_path))
- if new_file_path in self.recents["open_files"]:
- self.recents["open_files"].remove(new_file_path)
- self.recents["open_files"].insert(0, new_file_path)
- self._update_recent_files_menu()
- language = self._get_language_from_path(new_file_path)
- old_highlighter = getattr(current_widget.document(), '_syntax_highlighter', None)
- if old_highlighter:
- old_highlighter.setDocument(None)
- new_highlighter = CodeSyntaxHighlighter(current_widget.document(), language)
- setattr(current_widget.document(), '_syntax_highlighter', new_highlighter)
- return self._save_file(current_widget, new_file_path)
- def _save_file(self, editor_widget, file_path):
- if not file_path:
- print("Error: _save_file called with empty path.", file=sys.stderr)
- self.statusBar().showMessage("Błąd wewnętrzny: próba zapisu bez ścieżki.")
- return False
- try:
- os.makedirs(os.path.dirname(file_path), exist_ok=True)
- with open(file_path, 'w', encoding='utf-8') as f:
- f.write(editor_widget.toPlainText())
- editor_widget.document().setModified(False)
- self.statusBar().showMessage(f"Plik zapisano pomyślnie: {os.path.basename(file_path)}")
- tab_index = self.tab_widget.indexOf(editor_widget)
- if tab_index != -1:
- current_tab_text = self.tab_widget.tabText(tab_index).rstrip('*')
- self.tab_widget.setTabText(tab_index, current_tab_text)
- file_info = QFileInfo(file_path)
- dir_path = file_info.dir().path()
- root_path = self.project_model.rootPath()
- if root_path and dir_path.startswith(root_path):
- dir_index = self.project_model.index(dir_path)
- if dir_index.isValid():
- self.project_model.refresh(dir_index)
- file_index = self.project_model.index(file_path)
- if file_index.isValid():
- self.project_model.dataChanged.emit(file_index, file_index, [Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.DecorationRole])
- if file_path in self.recents["open_files"]:
- self.recents["open_files"].remove(file_path)
- self.recents["open_files"].insert(0, file_path)
- self._update_recent_files_menu()
- self._save_app_state()
- return True
- except Exception as e:
- QMessageBox.critical(self, "Błąd zapisu pliku", f"Nie można zapisać pliku {os.path.basename(file_path)}:\n{e}")
- self.statusBar().showMessage(f"Błąd zapisu pliku: {os.path.basename(file_path)}")
- return False
- def _save_all_files(self):
- unsaved_files = [path for path, editor in self.open_files.items() if editor.document().isModified()]
- if not unsaved_files:
- self.statusBar().showMessage("Brak zmodyfikowanych plików do zapisu.")
- return True
- self.statusBar().showMessage(f"Zapisywanie wszystkich zmodyfikowanych plików ({len(unsaved_files)})...")
- total_saved = 0
- total_failed = 0
- files_to_save = list(unsaved_files)
- for file_path in files_to_save:
- editor_widget = self.open_files.get(file_path)
- if editor_widget is None or self.tab_widget.indexOf(editor_widget) == -1:
- print(f"Warning: Skipping save for {file_path} - editor widget not found or invalid.", file=sys.stderr)
- continue
- if not editor_widget.document().isModified():
- continue
- needs_save_as = (file_path is None or
- not os.path.exists(file_path) or
- not QFileInfo(file_path).isFile())
- save_success = False
- if needs_save_as:
- tab_index = self.tab_widget.indexOf(editor_widget)
- if tab_index != -1:
- original_index = self.tab_widget.currentIndex()
- self.tab_widget.setCurrentIndex(tab_index)
- save_success = self._save_current_file_as()
- if original_index != -1 and original_index < self.tab_widget.count():
- self.tab_widget.setCurrentIndex(original_index)
- else:
- 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)
- total_failed += 1
- continue
- else:
- save_success = self._save_file(editor_widget, file_path)
- if save_success:
- total_saved += 1
- else:
- total_failed += 1
- if total_saved > 0 and total_failed == 0:
- self.statusBar().showMessage(f"Zapisano pomyślnie wszystkie {total_saved} pliki.")
- return True
- elif total_saved > 0 and total_failed > 0:
- self.statusBar().showMessage(f"Zapisano {total_saved} plików, {total_failed} plików nie udało się zapisać.")
- QMessageBox.warning(self, "Błąd zapisu wszystkich plików", f"Nie udało się zapisać {total_failed} plików.")
- return False
- elif total_saved == 0 and total_failed > 0:
- self.statusBar().showMessage(f"Nie udało się zapisać żadnego z {total_failed} plików.")
- QMessageBox.critical(self, "Błąd zapisu wszystkich plików", f"Nie udało się zapisać żadnego z plików.")
- return False
- else:
- self.statusBar().showMessage("Brak zmodyfikowanych plików do zapisu.")
- return True
- def _handle_modification_changed(self, modified):
- editor_document = self.sender()
- if not isinstance(editor_document, QTextDocument):
- return
- editor = None
- for editor_widget in self.open_files.values():
- if editor_widget.document() is editor_document:
- editor = editor_widget
- break
- if editor is None:
- return
- index = self.tab_widget.indexOf(editor)
- if index != -1:
- tab_text = self.tab_widget.tabText(index)
- if modified and not tab_text.endswith('*'):
- self.tab_widget.setTabText(index, tab_text + '*')
- elif not modified and tab_text.endswith('*'):
- self.tab_widget.setTabText(index, tab_text.rstrip('*'))
- def _handle_tab_change(self, index):
- self._hide_find_bar()
- if index != -1:
- widget = self.tab_widget.widget(index)
- if isinstance(widget, QPlainTextEdit):
- file_path = next((path for path, ed in self.open_files.items() if ed is widget), None)
- if file_path:
- self.statusBar().showMessage(f"Edytujesz: {os.path.basename(file_path)}")
- else:
- self.statusBar().showMessage("Edytujesz: Nowy plik")
- else:
- self.statusBar().showMessage("Gotowy.")
- def _find_text(self, text, direction='next'):
- editor = self.tab_widget.currentWidget()
- if not isinstance(editor, QPlainTextEdit):
- self.statusBar().showMessage("Brak aktywnego edytora do wyszukiwania.")
- return
- if not text:
- self.statusBar().showMessage("Wpisz tekst do wyszukiwania.")
- return
- flags = QTextDocument.FindFlag(0)
- if direction == 'previous':
- flags |= QTextDocument.FindFlag.FindBackward
- found = editor.find(text, flags)
- if found:
- self.statusBar().showMessage(f"Znaleziono '{text}'.")
- else:
- self.statusBar().showMessage(f"Nie znaleziono '{text}'. Zawijanie...")
- cursor = editor.textCursor()
- original_position = cursor.position()
- cursor.clearSelection()
- cursor.movePosition(cursor.MoveOperation.Start if direction == 'next' else cursor.MoveOperation.End)
- editor.setTextCursor(cursor)
- found_wrapped = editor.find(text, flags)
- if found_wrapped:
- self.statusBar().showMessage(f"Znaleziono '{text}' po zawinięciu.")
- else:
- self.statusBar().showMessage(f"Nie znaleziono '{text}' w całym pliku.")
- cursor.clearSelection()
- cursor.setPosition(original_position)
- editor.setTextCursor(cursor)
- def _show_find_bar(self):
- if self.search_input.isVisible():
- self._hide_find_bar()
- return
- self.search_input.setVisible(True)
- self.find_next_button.setVisible(True)
- self.find_prev_button.setVisible(True)
- self.search_input.setFocus()
- def _hide_find_bar(self):
- if self.search_input.isVisible():
- self.search_input.setVisible(False)
- self.find_next_button.setVisible(False)
- self.find_prev_button.setVisible(False)
- self.search_input.clear()
- def _run_current_file(self):
- current_widget = self.tab_widget.currentWidget()
- if not isinstance(current_widget, QPlainTextEdit):
- self.console_widget.console.appendPlainText("Brak aktywnego pliku do uruchomienia.")
- self.statusBar().showMessage("Błąd: Żaden plik nie jest otwarty.")
- return
- file_path = next((path for path, editor_widget in self.open_files.items() if editor_widget is current_widget), None)
- if not file_path or not os.path.exists(file_path) or not os.path.isfile(file_path):
- self.console_widget.console.appendPlainText("Ścieżka aktywnego pliku jest nieprawidłowa lub plik nie istnieje.")
- self.statusBar().showMessage("Błąd: Plik nie istnieje.")
- return
- if current_widget.document().isModified():
- if not self._save_file(current_widget, file_path):
- self.console_widget.console.appendPlainText("Nie udało się zapisać pliku przed uruchomieniem.")
- self.statusBar().showMessage("Błąd: Nie zapisano pliku.")
- return
- language = self._get_language_from_path(file_path)
- working_dir = os.path.dirname(file_path) or self.current_project_dir or os.getcwd()
- command = None
- if language == "python":
- python_path = self.settings.get("python_path", "python")
- if not python_path:
- self.console_widget.console.appendPlainText("Błąd uruchamiania! Zainstaluj dodatek Python poprzez Menadżer Pakietów")
- self.statusBar().showMessage("Błąd: Brak interpretera Python.")
- return
- command = f'"{python_path}" "{file_path}"'
- elif language == "javascript":
- node_path = self.settings.get("node_path", "node")
- command = f'"{node_path}" "{file_path}"'
- elif language in ["c", "cpp"]:
- output_exe = os.path.splitext(file_path)[0] + (".exe" if platform.system() == "Windows" else "")
- compile_command = f'g++ "{file_path}" -o "{output_exe}"'
- self.console_widget.console.appendPlainText(f"Kompilowanie: {compile_command}")
- self.console_manager.run_command(compile_command, working_dir)
- # Czekaj na zakończenie kompilacji (może wymagać osobnego procesu)
- # Zakładam, że proces jest synchroniczny dla uproszczenia
- # Jeśli kompilacja się udała, uruchom program
- self.console_manager.run_command(f'"{output_exe}"', working_dir)
- return
- else:
- self.console_widget.console.appendPlainText(f"Uruchamianie nieobsługiwane dla języka: {language}")
- self.statusBar().showMessage(f"Błąd: Nie można uruchomić pliku {os.path.basename(file_path)}.")
- return
- if command:
- self.console_widget.console.appendPlainText(f"Uruchamianie: {command}")
- self.console_manager.run_command(command, working_dir, self.settings.get("python_path", ""), self.settings.get("node_path", ""))
- self.statusBar().showMessage(f"Uruchamianie: {os.path.basename(file_path)}")
- def _get_language_from_path(self, file_path):
- return get_file_language(file_path)
- def _apply_theme(self, theme_name):
- apply_theme(self, theme_name)
- self.settings["theme"] = theme_name
- self._save_app_state()
- self.statusBar().showMessage(f"Zastosowano motyw: {theme_name}")
- def _apply_editor_font_size(self):
- font_size = self.settings.get("editor_font_size", 10)
- self.base_editor_font.setPointSize(font_size)
- for editor in self.open_files.values():
- editor.setFont(self.base_editor_font)
- def _show_settings_dialog(self):
- dialog = SettingsDialog(self.settings, self)
- if dialog.exec() == QDialog.DialogCode.Accepted:
- new_settings = dialog.get_settings()
- self.settings.update(new_settings)
- self._apply_theme(self.settings["theme"])
- self._apply_editor_font_size()
- self.ai_chat_manager.update_settings(self.settings)
- self._save_app_state()
- self.statusBar().showMessage("Zapisano ustawienia.")
- def _show_package_manager(self):
- if not self.current_project_dir:
- QMessageBox.warning(self, "Brak projektu", "Otwórz lub utwórz projekt, aby zarządzać pakietami.")
- return
- dialog = PackageManagerDialog(self.current_project_dir, self)
- dialog.exec()
- # Po zamknięciu menadżera pakietów wczytaj ponownie ustawienia z pliku settings.json
- try:
- if os.path.exists(SETTINGS_FILE):
- with open(SETTINGS_FILE, 'r', encoding='utf-8') as f:
- loaded_settings = json.load(f)
- # Automatyczne wyszukiwanie python.exe jeśli nie ma ścieżki lub jest pusta
- python_path = loaded_settings.get("python_path", "")
- if not python_path:
- import glob
- python_candidates = glob.glob(os.path.join(ROOT_DIR, "packages", "python", "**", "python.exe"), recursive=True)
- if python_candidates:
- python_path = python_candidates[0]
- loaded_settings["python_path"] = python_path
- # Zapisz poprawioną ścieżkę do settings.json
- with open(SETTINGS_FILE, 'w', encoding='utf-8') as fw:
- json.dump(loaded_settings, fw, indent=4)
- self.settings.update({
- "python_path": python_path or self.settings.get("python_path", ""),
- "node_path": loaded_settings.get("node_path", self.settings.get("node_path", "")),
- "theme": loaded_settings.get("theme", self.settings.get("theme", "light")),
- "editor_font_size": loaded_settings.get("editor_font_size", self.settings.get("editor_font_size", 10)),
- "show_tree": loaded_settings.get("show_tree", self.settings.get("show_tree", True)),
- "show_console": loaded_settings.get("show_console", self.settings.get("show_console", True)),
- "api_key": loaded_settings.get("api_key", self.settings.get("api_key", "")),
- "gemini_api_key": loaded_settings.get("gemini_api_key", self.settings.get("gemini_api_key", "")),
- "mistral_api_key": loaded_settings.get("mistral_api_key", self.settings.get("mistral_api_key", "")),
- "ai_model": loaded_settings.get("ai_model", self.settings.get("ai_model", "grok-3")),
- "ai_provider": loaded_settings.get("ai_provider", self.settings.get("ai_provider", "grok")),
- })
- self._apply_theme(self.settings.get("theme", "light"))
- self._apply_editor_font_size()
- except Exception as e:
- print(f"Błąd podczas ponownego wczytywania ustawień po menadżerze pakietów: {e}", file=sys.stderr)
- def _show_about_dialog(self):
- QMessageBox.about(
- self,
- "O programie",
- "Proste IDE\nWersja 1.0\nStworzone dla zajebistych kodersów, którzy nie lubią komplikacji.\n© 2025 Paffcio & xAI"
- )
- def _check_package_json(self, project_dir):
- self.node_scripts.clear()
- package_json_path = os.path.join(project_dir, "package.json")
- if os.path.exists(package_json_path):
- package_data = load_package_json(package_json_path)
- scripts = package_data.get("scripts", {})
- self.node_scripts.update(scripts)
- self._update_run_button_menu()
- def _update_run_button_menu(self):
- menu = QMenu(self)
- menu.addAction(self.action_run_file)
- if self.node_scripts and self.current_project_dir:
- node_menu = menu.addMenu("Uruchom skrypt Node.js")
- node_path = self.settings.get("node_path", "node")
- for script_name in self.node_scripts:
- action = QAction(script_name, self)
- command = f'"{node_path}" run {script_name}'
- action.triggered.connect(
- lambda checked, cmd=command: self.process.start(cmd, working_dir=self.current_project_dir)
- )
- node_menu.addAction(action)
- self.run_toolbutton.setMenu(menu)
- def _handle_tree_item_double_click(self, index):
- if not index.isValid():
- return
- file_path = self.project_model.filePath(index)
- file_info = self.project_model.fileInfo(index)
- if file_info.isFile():
- self._open_file(file_path)
- if __name__ == '__main__':
- app = QApplication(sys.argv)
- window = IDEWindow()
- window.show()
- sys.exit(app.exec())
- Ścieżka: /src/utils.py
- Rozmiar: 1,71 KB
- Zawartość:
- import os
- import json
- import re
- import sys
- def load_package_json(folder_path):
- """Parsuje package.json i zwraca skrypty npm."""
- if not folder_path or not os.path.isdir(folder_path):
- return {}
- package_json_path = os.path.join(folder_path, 'package.json')
- scripts = {}
- if os.path.exists(package_json_path):
- try:
- with open(package_json_path, 'r', encoding='utf-8') as f:
- package_data = json.load(f)
- scripts = package_data.get('scripts', {})
- if not isinstance(scripts, dict):
- scripts = {}
- except (json.JSONDecodeError, Exception) as e:
- print(f"Błąd parsowania package.json: {e}", file=sys.stderr)
- return {}
- return scripts
- def get_file_language(file_path):
- """Określa język programowania na podstawie rozszerzenia pliku."""
- extension = os.path.splitext(file_path)[1].lower()
- language_map = {
- '.py': 'python',
- '.pyw': 'python',
- '.js': 'javascript',
- '.ts': 'javascript',
- '.html': 'html',
- '.htm': 'html',
- '.css': 'css',
- '.c': 'c',
- '.cpp': 'cpp',
- '.cc': 'cpp',
- '.h': 'cpp',
- '.hpp': 'cpp',
- '.json': 'json',
- '.ini': 'ini',
- '.bat': 'batch',
- '.sh': 'bash',
- '.ps1': 'powershell',
- '.rb': 'ruby',
- '.java': 'java',
- '.go': 'go',
- '.rs': 'rust',
- '.php': 'php',
- '.xml': 'xml',
- '.md': 'markdown',
- '.txt': 'text',
- }
- return language_map.get(extension, 'text') # Domyślnie 'text' dla nieznanych
- __all__ = ['load_package_json', 'get_file_language']
- // SKRYPT ZAKOŃCZONY: 18-05-2025 17:13:29
- // RAPORT: Przetworzono 12 plików tekstowych, 0 nietekstowych, pominięto 1.
Advertisement
Comments
-
- Leon West Accidental Goblin King Audiobooks 1-4
- magnet:?xt=urn:btih:49d386821d7a4093ac6209084242fbdf979b0ac1
- magnet:?xt=urn:btih:9f49b631081256fdab2d7b13927ce27bd44cf683
- magnet:?xt=urn:btih:6ef04c5cd32428d63afbca8a5b754688082059d3
- magnet:?xt=urn:btih:feae4390a335f0743bc8852d70183ace64240e1a
Add Comment
Please, Sign In to add comment
Advertisement