diff --git a/.github/workflows/generate-manifest.yml b/.github/workflows/generate-manifest.yml new file mode 100644 index 00000000..21e319bf --- /dev/null +++ b/.github/workflows/generate-manifest.yml @@ -0,0 +1,55 @@ +name: Generate Manifest + +on: + push: + paths-ignore: + - '.gitattributes' + - '.gitignore' + - 'LICENSE' + - 'README.md' + workflow_dispatch: + release: + types: [published] + +jobs: + generate-manifest: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.13' + + - name: Generate manifest.json + run: | + python3 << 'EOF' + import os + import sys + + from Scripts import integrity_checker + from Scripts import utils + + checker = integrity_checker.IntegrityChecker() + + root_folder = os.getcwd() + manifest_path = os.path.join(root_folder, "manifest.json") + + print(f"Generating manifest from: {root_folder}") + manifest_data = checker.generate_folder_manifest(root_folder, manifest_path) + + if manifest_data: + print(f"Manifest generated successfully with {len(manifest_data)} files") + else: + print("Failed to generate manifest") + sys.exit(1) + EOF + + - name: Upload manifest.json to Artifacts + uses: actions/upload-artifact@v4 + with: + name: manifest.json + path: ./manifest.json \ No newline at end of file diff --git a/OpCore-Simplify.bat b/OpCore-Simplify.bat index 0763ce98..70d1d482 100644 --- a/OpCore-Simplify.bat +++ b/OpCore-Simplify.bat @@ -299,9 +299,56 @@ if /i "!just_installing!" == "TRUE" ( ) exit /b +:checkrequirements +REM Check and install Python requirements +set "requirements_file=!thisDir!requirements.txt" +if not exist "!requirements_file!" ( + echo Warning: requirements.txt not found. Skipping dependency check. + exit /b 0 +) +echo Checking Python dependencies... +"!pypath!" -m pip --version > nul 2>&1 +set "pip_check_error=!ERRORLEVEL!" +if not "!pip_check_error!" == "0" ( + echo Warning: pip is not available. Attempting to install pip... + "!pypath!" -m ensurepip --upgrade > nul 2>&1 + set "ensurepip_error=!ERRORLEVEL!" + if not "!ensurepip_error!" == "0" ( + echo Error: Could not install pip. Please install pip manually. + exit /b 1 + ) +) +REM Try to import key packages to check if they're installed +"!pypath!" -c "import PyQt6; import qfluentwidgets" > nul 2>&1 +set "import_check_error=!ERRORLEVEL!" +if not "!import_check_error!" == "0" ( + echo Installing required packages from requirements.txt... + "!pypath!" -m pip install --upgrade -r "!requirements_file!" + set "pip_install_error=!ERRORLEVEL!" + if not "!pip_install_error!" == "0" ( + echo. + echo Error: Failed to install requirements. Please install them manually: + echo !pypath! -m pip install -r !requirements_file! + echo. + echo Press [enter] to exit... + pause > nul + exit /b 1 + ) + echo Requirements installed successfully. +) else ( + echo All requirements are already installed. +) +exit /b 0 + :runscript REM Python found cls +REM Check and install requirements before running the script +call :checkrequirements +set "req_check_error=!ERRORLEVEL!" +if not "!req_check_error!" == "0" ( + exit /b 1 +) set "args=%*" set "args=!args:"=!" if "!args!"=="" ( diff --git a/OpCore-Simplify.command b/OpCore-Simplify.command index 3fbb13a0..582fc86a 100755 --- a/OpCore-Simplify.command +++ b/OpCore-Simplify.command @@ -283,6 +283,42 @@ prompt_and_download() { done } +check_and_install_requirements() { + local python="$1" + local requirements_file="$dir/requirements.txt" + + # Check if requirements.txt exists + if [ ! -f "$requirements_file" ]; then + echo "Warning: requirements.txt not found. Skipping dependency check." + return 0 + fi + + # Check if pip is available + if ! "$python" -m pip --version > /dev/null 2>&1; then + echo "Warning: pip is not available. Attempting to install pip..." + if ! "$python" -m ensurepip --upgrade > /dev/null 2>&1; then + echo "Error: Could not install pip. Please install pip manually." + return 1 + fi + fi + + # Check if requirements are installed by trying to import key packages + echo "Checking Python dependencies..." + if ! "$python" -c "import PyQt6; import qfluentwidgets" > /dev/null 2>&1; then + echo "Installing required packages from requirements.txt..." + if ! "$python" -m pip install --upgrade -r "$requirements_file"; then + echo "Error: Failed to install requirements. Please install them manually:" + echo " $python -m pip install -r $requirements_file" + return 1 + fi + echo "Requirements installed successfully." + else + echo "All requirements are already installed." + fi + + return 0 +} + main() { local python= version= # Verify our target exists @@ -310,6 +346,11 @@ main() { prompt_and_download return 1 fi + # Check and install requirements before running the script + if ! check_and_install_requirements "$python"; then + echo "Failed to install requirements. Exiting." + exit 1 + fi # Found it - start our script and pass all args "$python" "$dir/$target" "${args[@]}" } diff --git a/OpCore-Simplify.py b/OpCore-Simplify.py index 4f402ee0..bd789eb8 100644 --- a/OpCore-Simplify.py +++ b/OpCore-Simplify.py @@ -1,482 +1,318 @@ -from Scripts.datasets import os_data -from Scripts.datasets import chipset_data -from Scripts import acpi_guru -from Scripts import compatibility_checker -from Scripts import config_prodigy -from Scripts import gathering_files -from Scripts import hardware_customizer -from Scripts import kext_maestro -from Scripts import report_validator -from Scripts import run -from Scripts import smbios -from Scripts import utils -import updater import os import sys -import re -import shutil +import platform import traceback -import time - -class OCPE: - def __init__(self): - self.u = utils.Utils("OpCore Simplify") - self.u.clean_temporary_dir() - self.ac = acpi_guru.ACPIGuru() - self.c = compatibility_checker.CompatibilityChecker() - self.co = config_prodigy.ConfigProdigy() - self.o = gathering_files.gatheringFiles() - self.h = hardware_customizer.HardwareCustomizer() - self.k = kext_maestro.KextMaestro() - self.s = smbios.SMBIOS() - self.v = report_validator.ReportValidator() - self.r = run.Run() - self.result_dir = self.u.get_temporary_dir() - - def select_hardware_report(self): - self.ac.dsdt = self.ac.acpi.acpi_tables = None - while True: - self.u.head("Select hardware report") - print("") - if os.name == "nt": - print("\033[1;93mNote:\033[0m") - print("- Ensure you are using the latest version of Hardware Sniffer before generating the hardware report.") - print("- Hardware Sniffer will not collect information related to Resizable BAR option of GPU (disabled by default) and monitor connections in Windows PE.") - print("") - print("E. Export hardware report (Recommended)") - print("") - print("Q. Quit") - print("") - - user_input = self.u.request_input("Drag and drop your hardware report here (.JSON){}: ".format(" or type \"E\" to export" if os.name == "nt" else "")) - if user_input.lower() == "q": - self.u.exit_program() - if user_input.lower() == "e": - hardware_sniffer = self.o.gather_hardware_sniffer() - - if not hardware_sniffer: - continue +from PyQt6.QtCore import Qt, pyqtSignal +from PyQt6.QtGui import QFont +from PyQt6.QtWidgets import QApplication +from qfluentwidgets import FluentWindow, NavigationItemPosition, FluentIcon, InfoBar, InfoBarPosition - report_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "SysReport") +from Scripts.datasets import os_data +from Scripts.state import HardwareReportState, macOSVersionState, SMBIOSState, BuildState +from Scripts.pages import HomePage, SelectHardwareReportPage, CompatibilityPage, ConfigurationPage, BuildPage, SettingsPage +from Scripts.backend import Backend +from Scripts import ui_utils +from Scripts.custom_dialogs import set_default_gui_handler +import updater - self.u.head("Exporting Hardware Report") - print("") - print("Exporting hardware report to {}...".format(report_dir)) - - output = self.r.run({ - "args":[hardware_sniffer, "-e", "-o", report_dir] - }) +WINDOW_MIN_SIZE = (1000, 700) +WINDOW_DEFAULT_SIZE = (1200, 800) - if output[-1] != 0: - error_code = output[-1] - if error_code == 3: - error_message = "Error collecting hardware." - elif error_code == 4: - error_message = "Error generating hardware report." - elif error_code == 5: - error_message = "Error dumping ACPI tables." - else: - error_message = "Unknown error." - print("") - print("Could not export the hardware report. {}".format(error_message)) - print("Please try again or using Hardware Sniffer manually.") - print("") - self.u.request_input() - continue - else: - report_path = os.path.join(report_dir, "Report.json") - acpitables_dir = os.path.join(report_dir, "ACPI") +class OCS(FluentWindow): + open_result_folder_signal = pyqtSignal(str) + + PLATFORM_FONTS = { + "Windows": "Segoe UI", + "Darwin": "SF Pro Display", + "Linux": "Ubuntu" + } - report_data = self.u.read_file(report_path) - self.ac.read_acpi_tables(acpitables_dir) - - return report_path, report_data - - path = self.u.normalize_path(user_input) + def __init__(self, backend): + super().__init__() + self.backend = backend + self.settings = self.backend.settings + self.ui_utils = ui_utils.UIUtils() + + self._init_state() + self._setup_window() + self._connect_signals() + self._setup_backend_handlers() + self.init_navigation() + + def _init_state(self): + self.hardware_state = HardwareReportState() + self.macos_state = macOSVersionState() + self.smbios_state = SMBIOSState() + self.build_state = BuildState() + + self.build_btn = None + self.progress_bar = None + self.progress_label = None + self.build_log = None + self.open_result_btn = None + + def _setup_window(self): + self.setWindowTitle("OpCore Simplify") + self.setMinimumSize(*WINDOW_MIN_SIZE) + + self._restore_window_geometry() + + font = QFont() + system = platform.system() + font_family = self.PLATFORM_FONTS.get(system, "Ubuntu") + font.setFamily(font_family) + font.setStyleHint(QFont.StyleHint.SansSerif) + self.setFont(font) + + def _restore_window_geometry(self): + saved_geometry = self.settings.get("window_geometry") + + if saved_geometry and isinstance(saved_geometry, dict): + x = saved_geometry.get("x") + y = saved_geometry.get("y") + width = saved_geometry.get("width", WINDOW_DEFAULT_SIZE[0]) + height = saved_geometry.get("height", WINDOW_DEFAULT_SIZE[1]) - is_valid, errors, warnings, data = self.v.validate_report(path) + if x is not None and y is not None: + screen = QApplication.primaryScreen() + if screen: + screen_geometry = screen.availableGeometry() + if (screen_geometry.left() <= x <= screen_geometry.right() and + screen_geometry.top() <= y <= screen_geometry.bottom()): + self.setGeometry(x, y, width, height) + return + + self._center_window() + + def _center_window(self): + screen = QApplication.primaryScreen() + if screen: + screen_geometry = screen.availableGeometry() + window_width = WINDOW_DEFAULT_SIZE[0] + window_height = WINDOW_DEFAULT_SIZE[1] - self.v.show_validation_report(path, is_valid, errors, warnings) - if not is_valid or errors: - print("") - print("\033[32mSuggestion:\033[0m Please re-export the hardware report and try again.") - print("") - self.u.request_input("Press Enter to go back...") - else: - return path, data + x = screen_geometry.left() + (screen_geometry.width() - window_width) // 2 + y = screen_geometry.top() + (screen_geometry.height() - window_height) // 2 - def show_oclp_warning(self): - while True: - self.u.head("OpenCore Legacy Patcher Warning") - print("") - print("1. OpenCore Legacy Patcher is the only solution to enable dropped GPU and Broadcom WiFi") - print(" support in newer macOS versions, as well as to bring back AppleHDA for macOS Tahoe 26.") - print("") - print("2. OpenCore Legacy Patcher disables macOS security features including SIP and AMFI, which may") - print(" lead to issues such as requiring full installers for updates, application crashes, and") - print(" system instability.") - print("") - print("3. OpenCore Legacy Patcher is not officially supported for Hackintosh community.") - print("") - print("\033[1;91mImportant:\033[0m") - print("Please consider these risks carefully before proceeding.") - print("") - print("\033[1;96mSupport for macOS Tahoe 26:\033[0m") - print("To patch macOS Tahoe 26, you must download OpenCore-Patcher 3.0.0 or newer from") - print("my repository: \033[4mlzhoang2801/OpenCore-Legacy-Patcher\033[0m on GitHub.") - print("Older or official Dortania releases are NOT supported for Tahoe 26.") - print("") - option = self.u.request_input("Do you want to continue with OpenCore Legacy Patcher? (yes/No): ").strip().lower() - if option == "yes": - return True - elif option == "no": - return False - - def select_macos_version(self, hardware_report, native_macos_version, ocl_patched_macos_version): - suggested_macos_version = native_macos_version[1] - version_pattern = re.compile(r'^(\d+)(?:\.(\d+)(?:\.(\d+))?)?$') - - for device_type in ("GPU", "Network", "Bluetooth", "SD Controller"): - if device_type in hardware_report: - for device_name, device_props in hardware_report[device_type].items(): - if device_props.get("Compatibility", (None, None)) != (None, None): - if device_type == "GPU" and device_props.get("Device Type") == "Integrated GPU": - device_id = device_props.get("Device ID", ""*8)[5:] - - if device_props.get("Manufacturer") == "AMD" or device_id.startswith(("59", "87C0")): - suggested_macos_version = "22.99.99" - elif device_id.startswith(("09", "19")): - suggested_macos_version = "21.99.99" - - if self.u.parse_darwin_version(suggested_macos_version) > self.u.parse_darwin_version(device_props.get("Compatibility")[0]): - suggested_macos_version = device_props.get("Compatibility")[0] - - while True: - if "Beta" in os_data.get_macos_name_by_darwin(suggested_macos_version): - suggested_macos_version = "{}{}".format(int(suggested_macos_version[:2]) - 1, suggested_macos_version[2:]) - else: - break - - while True: - self.u.head("Select macOS Version") - if native_macos_version[1][:2] != suggested_macos_version[:2]: - print("") - print("\033[1;36mSuggested macOS version:\033[0m") - print("- For better compatibility and stability, we suggest you to use only {} or older.".format(os_data.get_macos_name_by_darwin(suggested_macos_version))) - print("") - print("Available macOS versions:") - print("") - - oclp_min = int(ocl_patched_macos_version[-1][:2]) if ocl_patched_macos_version else 99 - oclp_max = int(ocl_patched_macos_version[0][:2]) if ocl_patched_macos_version else 0 - min_version = min(int(native_macos_version[0][:2]), oclp_min) - max_version = max(int(native_macos_version[-1][:2]), oclp_max) - - for darwin_version in range(min_version, max_version + 1): - name = os_data.get_macos_name_by_darwin(str(darwin_version)) - label = " (\033[1;93mRequires OpenCore Legacy Patcher\033[0m)" if oclp_min <= darwin_version <= oclp_max else "" - print(" {}. {}{}".format(darwin_version, name, label)) - - print("") - print("\033[1;93mNote:\033[0m") - print("- To select a major version, enter the number (e.g., 19).") - print("- To specify a full version, use the Darwin version format (e.g., 22.4.6).") - print("") - print("Q. Quit") - print("") - option = self.u.request_input("Please enter the macOS version you want to use (default: {}): ".format(os_data.get_macos_name_by_darwin(suggested_macos_version))) or suggested_macos_version - if option.lower() == "q": - self.u.exit_program() - - match = version_pattern.match(option) - if match: - target_version = "{}.{}.{}".format(match.group(1), match.group(2) if match.group(2) else 99, match.group(3) if match.group(3) else 99) - - if ocl_patched_macos_version and self.u.parse_darwin_version(ocl_patched_macos_version[-1]) <= self.u.parse_darwin_version(target_version) <= self.u.parse_darwin_version(ocl_patched_macos_version[0]): - return target_version - elif self.u.parse_darwin_version(native_macos_version[0]) <= self.u.parse_darwin_version(target_version) <= self.u.parse_darwin_version(native_macos_version[-1]): - return target_version - - def build_opencore_efi(self, hardware_report, disabled_devices, smbios_model, macos_version, needs_oclp): - steps = [ - "Copying EFI base to results folder", - "Applying ACPI patches", - "Copying kexts and snapshotting to config.plist", - "Generating config.plist", - "Cleaning up unused drivers, resources, and tools" - ] - - title = "Building OpenCore EFI" - - self.u.progress_bar(title, steps, 0) - self.u.create_folder(self.result_dir, remove_content=True) - - if not os.path.exists(self.k.ock_files_dir): - raise Exception("Directory '{}' does not exist.".format(self.k.ock_files_dir)) + self.setGeometry(x, y, window_width, window_height) + else: + self.resize(*WINDOW_DEFAULT_SIZE) + + def _save_window_geometry(self): + geometry = self.geometry() + window_geometry = { + "x": geometry.x(), + "y": geometry.y(), + "width": geometry.width(), + "height": geometry.height() + } + self.settings.set("window_geometry", window_geometry) + + def closeEvent(self, event): + self._save_window_geometry() + super().closeEvent(event) + + def _connect_signals(self): + self.backend.log_message_signal.connect( + lambda message, level, to_build_log: ( + [ + self.build_log.append(line) + for line in (message.splitlines() or [""]) + ] + if to_build_log and getattr(self, "build_log", None) else None + ) + ) + self.backend.update_status_signal.connect(self.update_status) - source_efi_dir = os.path.join(self.k.ock_files_dir, "OpenCorePkg") - shutil.copytree(source_efi_dir, self.result_dir, dirs_exist_ok=True) - - config_file = os.path.join(self.result_dir, "EFI", "OC", "config.plist") - config_data = self.u.read_file(config_file) + self.open_result_folder_signal.connect(self._handle_open_result_folder) + + def _setup_backend_handlers(self): + self.backend.u.gui_handler = self + set_default_gui_handler(self) + + def init_navigation(self): + self.homePage = HomePage(self, ui_utils_instance=self.ui_utils) + self.SelectHardwareReportPage = SelectHardwareReportPage(self, ui_utils_instance=self.ui_utils) + self.compatibilityPage = CompatibilityPage(self, ui_utils_instance=self.ui_utils) + self.configurationPage = ConfigurationPage(self, ui_utils_instance=self.ui_utils) + self.buildPage = BuildPage(self, ui_utils_instance=self.ui_utils) + self.settingsPage = SettingsPage(self) + + self.addSubInterface( + self.homePage, + FluentIcon.HOME, + "Home", + NavigationItemPosition.TOP + ) + self.addSubInterface( + self.SelectHardwareReportPage, + FluentIcon.FOLDER_ADD, + "1. Select Hardware Report", + NavigationItemPosition.TOP + ) + self.addSubInterface( + self.compatibilityPage, + FluentIcon.CHECKBOX, + "2. Check Compatibility", + NavigationItemPosition.TOP + ) + self.addSubInterface( + self.configurationPage, + FluentIcon.EDIT, + "3. Configure OpenCore EFI", + NavigationItemPosition.TOP + ) + self.addSubInterface( + self.buildPage, + FluentIcon.DEVELOPER_TOOLS, + "4. Build & Review", + NavigationItemPosition.TOP + ) + + self.navigationInterface.addSeparator() + self.addSubInterface( + self.settingsPage, + FluentIcon.SETTING, + "Settings", + NavigationItemPosition.BOTTOM + ) + + def _handle_open_result_folder(self, folder_path): + self.backend.u.open_folder(folder_path) + + def update_status(self, message, status_type="INFO"): + if status_type == "success": + InfoBar.success( + title="Success", + content=message, + orient=Qt.Orientation.Horizontal, + isClosable=True, + position=InfoBarPosition.TOP_RIGHT, + duration=3000, + parent=self + ) + elif status_type == "ERROR": + InfoBar.error( + title="ERROR", + content=message, + orient=Qt.Orientation.Horizontal, + isClosable=True, + position=InfoBarPosition.TOP_RIGHT, + duration=5000, + parent=self + ) + elif status_type == "WARNING": + InfoBar.warning( + title="WARNING", + content=message, + orient=Qt.Orientation.Horizontal, + isClosable=True, + position=InfoBarPosition.TOP_RIGHT, + duration=4000, + parent=self + ) + else: + InfoBar.info( + title="INFO", + content=message, + orient=Qt.Orientation.Horizontal, + isClosable=True, + position=InfoBarPosition.TOP_RIGHT, + duration=3000, + parent=self + ) + + def validate_prerequisites(self, require_hardware_report=True, require_dsdt=True, require_darwin_version=True, check_compatibility_error=True, require_customized_hardware=True, show_status=True): + if require_hardware_report: + if not self.hardware_state.hardware_report: + if show_status: + self.update_status("Please select hardware report first", "WARNING") + return False + + if require_dsdt: + if not self.backend.ac._ensure_dsdt(): + if show_status: + self.update_status("Please load ACPI tables first", "WARNING") + return False - if not config_data: - raise Exception("Error: The file {} does not exist.".format(config_file)) + if check_compatibility_error: + if self.hardware_state.compatibility_error: + if show_status: + self.update_status("Incompatible hardware detected, please select different hardware report and try again", "WARNING") + return False - self.u.progress_bar(title, steps, 1) - config_data["ACPI"]["Add"] = [] - config_data["ACPI"]["Delete"] = [] - config_data["ACPI"]["Patch"] = [] - if self.ac.ensure_dsdt(): - self.ac.hardware_report = hardware_report - self.ac.disabled_devices = disabled_devices - self.ac.acpi_directory = os.path.join(self.result_dir, "EFI", "OC", "ACPI") - self.ac.smbios_model = smbios_model - self.ac.lpc_bus_device = self.ac.get_lpc_name() - - for patch in self.ac.patches: - if patch.checked: - if patch.name == "BATP": - patch.checked = getattr(self.ac, patch.function_name)() - self.k.kexts[kext_maestro.kext_data.kext_index_by_name.get("ECEnabler")].checked = patch.checked - continue - - acpi_load = getattr(self.ac, patch.function_name)() - - if not isinstance(acpi_load, dict): - continue + if require_darwin_version: + if not self.macos_state.darwin_version: + if show_status: + self.update_status("Please select target macOS version first", "WARNING") + return False - config_data["ACPI"]["Add"].extend(acpi_load.get("Add", [])) - config_data["ACPI"]["Delete"].extend(acpi_load.get("Delete", [])) - config_data["ACPI"]["Patch"].extend(acpi_load.get("Patch", [])) + if require_customized_hardware: + if not self.hardware_state.customized_hardware: + if show_status: + self.update_status("Please reload hardware report and select target macOS version to continue", "WARNING") + return False - config_data["ACPI"]["Patch"].extend(self.ac.dsdt_patches) - config_data["ACPI"]["Patch"] = self.ac.apply_acpi_patches(config_data["ACPI"]["Patch"]) - - self.u.progress_bar(title, steps, 2) - kexts_directory = os.path.join(self.result_dir, "EFI", "OC", "Kexts") - self.k.install_kexts_to_efi(macos_version, kexts_directory) - config_data["Kernel"]["Add"] = self.k.load_kexts(hardware_report, macos_version, kexts_directory) + return True - self.u.progress_bar(title, steps, 3) - self.co.genarate(hardware_report, disabled_devices, smbios_model, macos_version, needs_oclp, self.k.kexts, config_data) - self.u.write_file(config_file, config_data) + def apply_macos_version(self, version): + self.macos_state.darwin_version = version + self.macos_state.selected_version_name = os_data.get_macos_name_by_darwin(version) - self.u.progress_bar(title, steps, 4) - files_to_remove = [] + self.hardware_state.customized_hardware, self.hardware_state.disabled_devices, self.macos_state.needs_oclp = self.backend.h.hardware_customization(self.hardware_state.hardware_report, version) - drivers_directory = os.path.join(self.result_dir, "EFI", "OC", "Drivers") - driver_list = self.u.find_matching_paths(drivers_directory, extension_filter=".efi") - driver_loaded = [kext.get("Path") for kext in config_data.get("UEFI").get("Drivers")] - for driver_path, type in driver_list: - if not driver_path in driver_loaded: - files_to_remove.append(os.path.join(drivers_directory, driver_path)) - - resources_audio_dir = os.path.join(self.result_dir, "EFI", "OC", "Resources", "Audio") - if os.path.exists(resources_audio_dir): - files_to_remove.append(resources_audio_dir) - - picker_variant = config_data.get("Misc", {}).get("Boot", {}).get("PickerVariant") - if picker_variant in (None, "Auto"): - picker_variant = "Acidanthera/GoldenGate" - if os.name == "nt": - picker_variant = picker_variant.replace("/", "\\") - - resources_image_dir = os.path.join(self.result_dir, "EFI", "OC", "Resources", "Image") - available_picker_variants = self.u.find_matching_paths(resources_image_dir, type_filter="dir") - - for variant_name, variant_type in available_picker_variants: - variant_path = os.path.join(resources_image_dir, variant_name) - if ".icns" in ", ".join(os.listdir(variant_path)): - if picker_variant not in variant_name: - files_to_remove.append(variant_path) - - tools_directory = os.path.join(self.result_dir, "EFI", "OC", "Tools") - tool_list = self.u.find_matching_paths(tools_directory, extension_filter=".efi") - tool_loaded = [tool.get("Path") for tool in config_data.get("Misc").get("Tools")] - for tool_path, type in tool_list: - if not tool_path in tool_loaded: - files_to_remove.append(os.path.join(tools_directory, tool_path)) - - if "manifest.json" in os.listdir(self.result_dir): - files_to_remove.append(os.path.join(self.result_dir, "manifest.json")) - - for file_path in files_to_remove: - try: - if os.path.isdir(file_path): - shutil.rmtree(file_path) - else: - os.remove(file_path) - except Exception as e: - print("Failed to remove file: {}".format(e)) + self.smbios_state.model_name = self.backend.s.select_smbios_model(self.hardware_state.customized_hardware, version) - self.u.progress_bar(title, steps, len(steps), done=True) + self.backend.ac.select_acpi_patches(self.hardware_state.customized_hardware, self.hardware_state.disabled_devices) - print("OpenCore EFI build complete.") - time.sleep(2) + self.macos_state.needs_oclp, audio_layout_id, audio_controller_properties = self.backend.k.select_required_kexts(self.hardware_state.customized_hardware, version, self.macos_state.needs_oclp, self.backend.ac.patches) - def check_bios_requirements(self, org_hardware_report, hardware_report): - requirements = [] - - org_firmware_type = org_hardware_report.get("BIOS", {}).get("Firmware Type", "Unknown") - firmware_type = hardware_report.get("BIOS", {}).get("Firmware Type", "Unknown") - if org_firmware_type == "Legacy" and firmware_type == "UEFI": - requirements.append("Enable UEFI mode (disable Legacy/CSM (Compatibility Support Module))") - - secure_boot = hardware_report.get("BIOS", {}).get("Secure Boot", "Unknown") - if secure_boot != "Disabled": - requirements.append("Disable Secure Boot") - - if hardware_report.get("Motherboard", {}).get("Platform") == "Desktop" and hardware_report.get("Motherboard", {}).get("Chipset") in chipset_data.IntelChipsets[112:]: - resizable_bar_enabled = any(gpu_props.get("Resizable BAR", "Disabled") == "Enabled" for gpu_props in hardware_report.get("GPU", {}).values()) - if not resizable_bar_enabled: - requirements.append("Enable Above 4G Decoding") - requirements.append("Disable Resizable BAR/Smart Access Memory") - - return requirements - - def before_using_efi(self, org_hardware_report, hardware_report): - while True: - self.u.head("Before Using EFI") - print("") - print("\033[93mPlease complete the following steps:\033[0m") - print("") - - bios_requirements = self.check_bios_requirements(org_hardware_report, hardware_report) - if bios_requirements: - print("* BIOS/UEFI Settings Required:") - for requirement in bios_requirements: - print(" - {}".format(requirement)) - print("") - - print("* USB Mapping:") - print(" - Use USBToolBox tool to map USB ports.") - print(" - Add created UTBMap.kext into the {} folder.".format("EFI\\OC\\Kexts" if os.name == "nt" else "EFI/OC/Kexts")) - print(" - Remove UTBDefault.kext in the {} folder.".format("EFI\\OC\\Kexts" if os.name == "nt" else "EFI/OC/Kexts")) - print(" - Edit config.plist:") - print(" - Use ProperTree to open your config.plist.") - print(" - Run OC Snapshot by pressing Command/Ctrl + R.") - print(" - If you have more than 15 ports on a single controller, enable the XhciPortLimit patch.") - print(" - Save the file when finished.") - print("") - print("Type \"AGREE\" to open the built EFI for you\n") - response = self.u.request_input("") - if response.lower() == "agree": - self.u.open_folder(self.result_dir) - break - else: - print("\033[91mInvalid input. Please try again.\033[0m") - - def main(self): - hardware_report_path = None - native_macos_version = None - disabled_devices = None - macos_version = None - ocl_patched_macos_version = None - needs_oclp = False - smbios_model = None - - while True: - self.u.head() - print("") - print(" Hardware Report: {}".format(hardware_report_path or 'Not selected')) - if hardware_report_path: - print("") - print(" macOS Version: {}".format(os_data.get_macos_name_by_darwin(macos_version) if macos_version else 'Not selected') + (' (' + macos_version + ')' if macos_version else '') + ('. \033[1;93mRequires OpenCore Legacy Patcher\033[0m' if needs_oclp else '')) - print(" SMBIOS: {}".format(smbios_model or 'Not selected')) - if disabled_devices: - print(" Disabled Devices:") - for device, _ in disabled_devices.items(): - print(" - {}".format(device)) - print("") + if audio_layout_id is not None: + self.hardware_state.audio_layout_id = audio_layout_id + self.hardware_state.audio_controller_properties = audio_controller_properties - print("1. Select Hardware Report") - print("2. Select macOS Version") - print("3. Customize ACPI Patch") - print("4. Customize Kexts") - print("5. Customize SMBIOS Model") - print("6. Build OpenCore EFI") - print("") - print("Q. Quit") - print("") + self.backend.s.smbios_specific_options(self.hardware_state.customized_hardware, self.smbios_state.model_name, version, self.backend.ac.patches, self.backend.k) - option = self.u.request_input("Select an option: ") - if option.lower() == "q": - self.u.exit_program() - - if option == "1": - hardware_report_path, hardware_report = self.select_hardware_report() - hardware_report, native_macos_version, ocl_patched_macos_version = self.c.check_compatibility(hardware_report) - macos_version = self.select_macos_version(hardware_report, native_macos_version, ocl_patched_macos_version) - customized_hardware, disabled_devices, needs_oclp = self.h.hardware_customization(hardware_report, macos_version) - smbios_model = self.s.select_smbios_model(customized_hardware, macos_version) - if not self.ac.ensure_dsdt(): - self.ac.select_acpi_tables() - self.ac.select_acpi_patches(customized_hardware, disabled_devices) - needs_oclp = self.k.select_required_kexts(customized_hardware, macos_version, needs_oclp, self.ac.patches) - self.s.smbios_specific_options(customized_hardware, smbios_model, macos_version, self.ac.patches, self.k) + self.configurationPage.update_display() - if not hardware_report_path: - self.u.head() - print("\n\n") - print("\033[1;93mPlease select a hardware report first.\033[0m") - print("\n\n") - self.u.request_input("Press Enter to go back...") - continue + def setup_exception_hook(self): + def handle_exception(exc_type, exc_value, exc_traceback): + if issubclass(exc_type, KeyboardInterrupt): + sys.__excepthook__(exc_type, exc_value, exc_traceback) + return - if option == "2": - macos_version = self.select_macos_version(hardware_report, native_macos_version, ocl_patched_macos_version) - customized_hardware, disabled_devices, needs_oclp = self.h.hardware_customization(hardware_report, macos_version) - smbios_model = self.s.select_smbios_model(customized_hardware, macos_version) - needs_oclp = self.k.select_required_kexts(customized_hardware, macos_version, needs_oclp, self.ac.patches) - self.s.smbios_specific_options(customized_hardware, smbios_model, macos_version, self.ac.patches, self.k) - elif option == "3": - self.ac.customize_patch_selection() - elif option == "4": - self.k.kext_configuration_menu(macos_version) - elif option == "5": - smbios_model = self.s.customize_smbios_model(customized_hardware, smbios_model, macos_version) - self.s.smbios_specific_options(customized_hardware, smbios_model, macos_version, self.ac.patches, self.k) - elif option == "6": - if needs_oclp and not self.show_oclp_warning(): - macos_version = self.select_macos_version(hardware_report, native_macos_version, ocl_patched_macos_version) - customized_hardware, disabled_devices, needs_oclp = self.h.hardware_customization(hardware_report, macos_version) - smbios_model = self.s.select_smbios_model(customized_hardware, macos_version) - needs_oclp = self.k.select_required_kexts(customized_hardware, macos_version, needs_oclp, self.ac.patches) - self.s.smbios_specific_options(customized_hardware, smbios_model, macos_version, self.ac.patches, self.k) - continue - - try: - self.o.gather_bootloader_kexts(self.k.kexts, macos_version) - except Exception as e: - print("\033[91mError: {}\033[0m".format(e)) - print("") - self.u.request_input("Press Enter to continue...") - continue - - self.build_opencore_efi(customized_hardware, disabled_devices, smbios_model, macos_version, needs_oclp) - self.before_using_efi(hardware_report, customized_hardware) - - self.u.head("Result") - print("") - print("Your OpenCore EFI for {} has been built at:".format(customized_hardware.get("Motherboard").get("Name"))) - print("\t{}".format(self.result_dir)) - print("") - self.u.request_input("Press Enter to main menu...") - -if __name__ == '__main__': - update_flag = updater.Updater().run_update() - if update_flag: - os.execv(sys.executable, ['python3'] + sys.argv) - - o = OCPE() - while True: - try: - o.main() - except Exception as e: - o.u.head("An Error Occurred") - print("") - print(traceback.format_exc()) - o.u.request_input() \ No newline at end of file + error_details = "".join(traceback.format_exception(exc_type, exc_value, exc_traceback)) + error_message = "Uncaught exception detected:\n{}".format(error_details) + + self.backend.u.log_message(error_message, level="ERROR") + + try: + sys.__stderr__.write("\n[CRITICAL ERROR] {}\n".format(error_message)) + except: + pass + + sys.excepthook = handle_exception + + +if __name__ == "__main__": + backend = Backend() + + app = QApplication(sys.argv) + set_default_gui_handler(app) + + window = OCS(backend) + window.setup_exception_hook() + window.show() + + if backend.settings.get_auto_update_check(): + updater.Updater( + utils_instance=backend.u, + github_instance=backend.github, + resource_fetcher_instance=backend.resource_fetcher, + run_instance=backend.r, + integrity_checker_instance=backend.integrity_checker + ).run_update() + + sys.exit(app.exec()) \ No newline at end of file diff --git a/README.md b/README.md index 6de7b3f2..562760de 100644 --- a/README.md +++ b/README.md @@ -19,23 +19,6 @@

-> [!NOTE] -> **OpenCore Legacy Patcher 3.0.0 – Now Supports macOS Tahoe 26!** -> -> The long awaited version 3.0.0 of OpenCore Legacy Patcher is here, bringing **initial support for macOS Tahoe 26** to the community! -> -> 🚨 **Please Note:** -> - Only OpenCore-Patcher 3.0.0 **from the [lzhoang2801/OpenCore-Legacy-Patcher](https://github.com/lzhoang2801/OpenCore-Legacy-Patcher/releases/tag/3.0.0)** repository provides support for macOS Tahoe 26 with early patches. -> - Official Dortania releases or older patches **will NOT work** with macOS Tahoe 26. - -> [!WARNING] -> While OpCore Simplify significantly reduces setup time, the Hackintosh journey still requires: -> - Understanding basic concepts from the [Dortania Guide](https://dortania.github.io/OpenCore-Install-Guide/) -> - Testing and troubleshooting during the installation process -> - Patience and persistence in resolving any issues that arise -> -> Our tool does not guarantee a successful installation in the first attempt, but it should help you get started. - ## ✨ **Features** 1. **Comprehensive Hardware and macOS Support** @@ -101,39 +84,30 @@ - On **macOS**, run `OpCore-Simplify.command`. - On **Linux**, run `OpCore-Simplify.py` with existing Python interpreter. - ![OpCore Simplify Menu](https://i.imgur.com/vTr1V9D.png) + ![OpCore Simplify Main](https://private-user-images.githubusercontent.com/169338399/529304376-037b1b04-8f76-4a31-87f2-b2b779ff4cdb.png?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NjcwNzk2MzEsIm5iZiI6MTc2NzA3OTMzMSwicGF0aCI6Ii8xNjkzMzgzOTkvNTI5MzA0Mzc2LTAzN2IxYjA0LThmNzYtNGEzMS04N2YyLWIyYjc3OWZmNGNkYi5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjUxMjMwJTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI1MTIzMFQwNzIyMTFaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT1kNWNjYjU0NTY2Yjg0YWEwMjdhMTYzMTg1OTMxODFkNWZkMjc2NDcwYTViZDc1MDI0Mzc1ZTZlMGY4YzMyOWY3JlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.NseHM8-4XDQXewZyFR4Sp18dI2VYUTJHOzpMuIoVdB4) 3. **Selecting hardware report**: - - On Windows, there will be an option for `E. Export hardware report`. It's recommended to use this for the best results with your hardware configuration and BIOS at the time of building. - - Alternatively, use [**Hardware Sniffer**](https://github.com/lzhoang2801/Hardware-Sniffer) to create a `Report.json` and ACPI dump for configuration manully. - ![Selecting hardware report](https://i.imgur.com/MbRmIGJ.png) + ![Selecting hardware report](https://private-user-images.githubusercontent.com/169338399/529304594-b1e608a7-6428-4f49-8426-f4ad289a7484.png?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NjcwNzk2MzEsIm5iZiI6MTc2NzA3OTMzMSwicGF0aCI6Ii8xNjkzMzgzOTkvNTI5MzA0NTk0LWIxZTYwOGE3LTY0MjgtNGY0OS04NDI2LWY0YWQyODlhNzQ4NC5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjUxMjMwJTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI1MTIzMFQwNzIyMTFaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT04NWE0NzQ0YTUzMGEwZWEzZWE5NDZlNWI5MGYxYTA5MTNlYzYyYjUxZjhjMTQwZTk3M2Y4YTY0MjIzMmM5ZmU1JlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.T4d0IzapkLa_brVNsqzvw8KrikmExMZO4blT7_GDeuk) - ![Loading ACPI Tables](https://i.imgur.com/SbL6N6v.png) +4. **Verifying hardware compatibility**: - ![Compatibility Checker](https://i.imgur.com/kuDGMmp.png) + ![Compatibility Checker](https://private-user-images.githubusercontent.com/169338399/529304672-72d4ba8c-1d8e-4a59-80e2-23212b3213da.png?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NjcwNzk2MzEsIm5iZiI6MTc2NzA3OTMzMSwicGF0aCI6Ii8xNjkzMzgzOTkvNTI5MzA0NjcyLTcyZDRiYThjLTFkOGUtNGE1OS04MGUyLTIzMjEyYjMyMTNkYS5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjUxMjMwJTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI1MTIzMFQwNzIyMTFaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT0yZjJlNjNlZGI0OWE1YmJhZDk2Zjg1MzFkYjIyZmZjZGQzODYxYzcwMDFlZDU1Yjk3OGFhNGJiZjk5OTA3YzBmJlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.KEuUWw0xvVOTNBnzqqW-z7jSHoN5SmyqntkhzeBk9MU) -4. **Selecting macOS Version and Customizing OpenCore EFI**: +5. **Selecting macOS Version and Customizing OpenCore EFI**: - By default, the latest compatible macOS version will be selected for your hardware. - OpCore Simplify will automatically apply essential ACPI patches and kexts. - You can manually review and customize these settings as needed. - ![OpCore Simplify Menu](https://i.imgur.com/TSk9ejy.png) + ![Configuration Page](https://private-user-images.githubusercontent.com/169338399/530910046-81462033-696d-46e2-91f2-358ceff37199.png?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NjcwNzg5NjQsIm5iZiI6MTc2NzA3ODY2NCwicGF0aCI6Ii8xNjkzMzgzOTkvNTMwOTEwMDQ2LTgxNDYyMDMzLTY5NmQtNDZlMi05MWYyLTM1OGNlZmYzNzE5OS5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjUxMjMwJTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI1MTIzMFQwNzExMDRaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT1lOGNlNzQ4MmE3ZTkzMGI0MDU0MzliZTAyMzI0YzhkZTJjNDkwYjc5NmZmZTA4YjE2NjUwYmUyMWUyMThlYzc1JlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.XuqqC-VOk4SS9zSCgyGaGfrmNbjDm-MCGiK4l597ink) -5. **Building OpenCore EFI**: +6. **Building OpenCore EFI**: - Once you've customized all options, select **Build OpenCore EFI** to generate your EFI. - The tool will automatically download the necessary bootloader and kexts, which may take a few minutes. - ![WiFi Profile Extractor](https://i.imgur.com/71TkJkD.png) - - ![Choosing Codec Layout ID](https://i.imgur.com/Mcm20EQ.png) - - ![Building OpenCore EFI](https://i.imgur.com/deyj5de.png) - -6. **USB Mapping**: - - After building your EFI, follow the steps for mapping USB ports. + ![OCLP Warning](https://private-user-images.githubusercontent.com/169338399/530910077-88987465-2aab-47b9-adf8-e56f6248c88f.png?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NjcwNzg5NjQsIm5iZiI6MTc2NzA3ODY2NCwicGF0aCI6Ii8xNjkzMzgzOTkvNTMwOTEwMDc3LTg4OTg3NDY1LTJhYWItNDdiOS1hZGY4LWU1NmY2MjQ4Yzg4Zi5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjUxMjMwJTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI1MTIzMFQwNzExMDRaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT01OWUzNGM2MzZmMmMwYjA5OWU0YzYxMTQ0Yjg5M2RkM2QzNDcyZGVlMTVkZWQ1ZTE5OTU5MjYwZGQ0ODVlZGVmJlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.twu-QrH30NkDnqSVsvsmySf15ePAWhCStGjZDO3ia40) - ![Results](https://i.imgur.com/MIPigPF.png) + ![Build Result](https://private-user-images.githubusercontent.com/169338399/530910249-f91813db-b201-4d6a-b604-691014d29074.png?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NjcwNzg5NjQsIm5iZiI6MTc2NzA3ODY2NCwicGF0aCI6Ii8xNjkzMzgzOTkvNTMwOTEwMjQ5LWY5MTgxM2RiLWIyMDEtNGQ2YS1iNjA0LTY5MTAxNGQyOTA3NC5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjUxMjMwJTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI1MTIzMFQwNzExMDRaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT1mZDVjYjM2MzY2ZGJhMDcxODRlZGUzY2RhNGFjMjYzNGMyYWFiYWVmZGM4YzRmMDlkZTgzNzEwZjRjYWY2MDM1JlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.uq7WzyDJKImuoUFjgfQG41s4VfgMq7h64BaSjRpU6cg) 7. **Create USB and Install macOS**: - Use [**UnPlugged**](https://github.com/corpnewt/UnPlugged) on Windows to create a USB macOS installer, or follow [this guide](https://dortania.github.io/OpenCore-Install-Guide/installer-guide/mac-install.html) for macOS. @@ -158,6 +132,7 @@ Distributed under the BSD 3-Clause License. See `LICENSE` for more information. - [OpenCorePkg](https://github.com/acidanthera/OpenCorePkg) and [kexts](https://github.com/lzhoang2801/OpCore-Simplify/blob/main/Scripts/datasets/kext_data.py) – The backbone of this project. - [SSDTTime](https://github.com/corpnewt/SSDTTime) – SSDT patching utilities. +- [@rubentalstra](https://github.com/rubentalstra): Idea and code prototype [Implement GUI #471](https://github.com/lzhoang2801/OpCore-Simplify/pull/471) ## 📞 **Contact** diff --git a/Scripts/acpi_guru.py b/Scripts/acpi_guru.py index 358a0190..81a41e83 100644 --- a/Scripts/acpi_guru.py +++ b/Scripts/acpi_guru.py @@ -8,6 +8,7 @@ from Scripts import dsdt from Scripts import run from Scripts import utils +from Scripts.custom_dialogs import show_checklist_dialog import os import binascii import re @@ -17,11 +18,11 @@ import plistlib class ACPIGuru: - def __init__(self): - self.acpi = dsdt.DSDT() - self.smbios = smbios.SMBIOS() - self.run = run.Run().run - self.utils = utils.Utils() + def __init__(self, dsdt_instance=None, smbios_instance=None, run_instance=None, utils_instance=None): + self.acpi = dsdt_instance if dsdt_instance else dsdt.DSDT() + self.smbios = smbios_instance if smbios_instance else smbios.SMBIOS() + self.run = run_instance.run if run_instance else run.Run().run + self.utils = utils_instance if utils_instance else utils.Utils() self.patches = acpi_patch_data.patches self.hardware_report = None self.disabled_devices = None @@ -118,9 +119,7 @@ def sorted_nicely(self, l): def read_acpi_tables(self, path): if not path: return - self.utils.head("Loading ACPI Table(s)") - print("by CorpNewt") - print("") + self.utils.log_message("[ACPI GURU] Loading ACPI Table(s) from {}".format(path), level="INFO") tables = [] trouble_dsdt = None fixed = False @@ -129,10 +128,10 @@ def read_acpi_tables(self, path): # Clear any existing tables so we load anew self.acpi.acpi_tables = {} if os.path.isdir(path): - print("Gathering valid tables from {}...\n".format(os.path.basename(path))) + self.utils.log_message("[ACPI GURU] Gathering valid tables from {}".format(os.path.basename(path)), level="INFO") for t in self.sorted_nicely(os.listdir(path)): if not "Patched" in t and self.acpi.table_is_valid(path,t): - print(" - {}".format(t)) + self.utils.log_message("[ACPI GURU] Found valid table: {}".format(t), level="INFO") tables.append(t) if not tables: # Check if there's an ACPI directory within the passed @@ -140,50 +139,44 @@ def read_acpi_tables(self, path): if os.path.isdir(os.path.join(path,"ACPI")): # Rerun this function with that updated path return self.read_acpi_tables(os.path.join(path,"ACPI")) - print(" - No valid .aml files were found!") - print("") + self.utils.log_message("[ACPI GURU] No valid .aml files were found!", level="ERROR") #self.u.grab("Press [enter] to return...") - self.utils.request_input() # Restore any prior tables self.acpi.acpi_tables = prior_tables return - print("") + self.utils.log_message("[ACPI GURU] Found at least one valid table", level="INFO") # We got at least one file - let's look for the DSDT specifically # and try to load that as-is. If it doesn't load, we'll have to # manage everything with temp folders dsdt_list = [x for x in tables if self.acpi._table_signature(path,x) == "DSDT"] if len(dsdt_list) > 1: - print("Multiple files with DSDT signature passed:") + self.utils.log_message("[ACPI GURU] Multiple files with DSDT signature passed:", level="ERROR") for d in self.sorted_nicely(dsdt_list): - print(" - {}".format(d)) - print("\nOnly one is allowed at a time. Please remove one of the above and try again.") - print("") + self.utils.log_message("[ACPI GURU] Found DSDT file: {}".format(d), level="INFO") + self.utils.log_message("[ACPI GURU] Only one is allowed at a time. Please remove one of the above and try again.", level="ERROR") #self.u.grab("Press [enter] to return...") - self.utils.request_input() # Restore any prior tables self.acpi.acpi_tables = prior_tables return # Get the DSDT, if any dsdt = dsdt_list[0] if len(dsdt_list) else None if dsdt: # Try to load it and see if it causes problems - print("Disassembling {} to verify if pre-patches are needed...".format(dsdt)) + self.utils.log_message("[ACPI GURU] Disassembling {} to verify if pre-patches are needed...".format(dsdt), level="INFO") if not self.acpi.load(os.path.join(path,dsdt))[0]: trouble_dsdt = dsdt else: - print("\nDisassembled successfully!\n") + self.utils.log_message("[ACPI GURU] Disassembled successfully!", level="INFO") elif not "Patched" in path and os.path.isfile(path): - print("Loading {}...".format(os.path.basename(path))) + self.utils.log_message("[ACPI GURU] Loading {}...".format(os.path.basename(path)), level="INFO") if self.acpi.load(path)[0]: - print("\nDone.") + self.utils.log_message("[ACPI GURU] Done.", level="INFO") # If it loads fine - just return the path # to the parent directory return os.path.dirname(path) if not self.acpi._table_signature(path) == "DSDT": # Not a DSDT, we aren't applying pre-patches - print("\n{} could not be disassembled!".format(os.path.basename(path))) - print("") + self.utils.log_message("[ACPI GURU] {} could not be disassembled!".format(os.path.basename(path)), level="ERROR") #self.u.grab("Press [enter] to return...") - self.utils.request_input() # Restore any prior tables self.acpi.acpi_tables = prior_tables return @@ -194,10 +187,8 @@ def read_acpi_tables(self, path): tables.append(os.path.basename(path)) path = os.path.dirname(path) else: - print("Passed file/folder does not exist!") - print("") + self.utils.log_message("[ACPI GURU] Passed file/folder does not exist!", level="ERROR") #self.u.grab("Press [enter] to return...") - self.utils.request_input() # Restore any prior tables self.acpi.acpi_tables = prior_tables return @@ -214,22 +205,22 @@ def read_acpi_tables(self, path): # Get a reference to the new trouble file trouble_path = os.path.join(temp,trouble_dsdt) # Now we try patching it - print("Checking available pre-patches...") - print("Loading {} into memory...".format(trouble_dsdt)) + self.utils.log_message("[ACPI GURU] Checking available pre-patches...", level="INFO") + self.utils.log_message("[ACPI GURU] Loading {} into memory...".format(trouble_dsdt), level="INFO") with open(trouble_path,"rb") as f: d = f.read() res = self.acpi.check_output(path) target_name = self.get_unique_name(trouble_dsdt,res,name_append="-Patched") self.dsdt_patches = [] - print("Iterating patches...\n") + self.utils.log_message("[ACPI GURU] Iterating patches...", level="INFO") for p in self.pre_patches: if not all(x in p for x in ("PrePatch","Comment","Find","Replace")): continue - print(" - {}".format(p["PrePatch"])) + self.utils.log_message("[ACPI GURU] Found pre-patch: {}".format(p["PrePatch"]), level="INFO") find = binascii.unhexlify(p["Find"]) if d.count(find) == 1: self.dsdt_patches.append(p) # Retain the patch repl = binascii.unhexlify(p["Replace"]) - print(" --> Located - applying...") + self.utils.log_message("[ACPI GURU] Located pre-patch - applying...", level="INFO") d = d.replace(find,repl) # Replace it in memory with open(trouble_path,"wb") as f: f.write(d) # Write the updated file @@ -237,7 +228,7 @@ def read_acpi_tables(self, path): if self.acpi.load(trouble_path)[0]: fixed = True # We got it to load - let's write the patches - print("\nDisassembled successfully!\n") + self.utils.log_message("[ACPI GURU] Disassembled successfully!", level="INFO") #self.make_plist(None, None, patches) # Save to the local file #with open(os.path.join(res,target_name),"wb") as f: @@ -246,10 +237,8 @@ def read_acpi_tables(self, path): #self.patch_warn() break if not fixed: - print("\n{} could not be disassembled!".format(trouble_dsdt)) - print("") + self.utils.log_message("[ACPI GURU] {} could not be disassembled!".format(trouble_dsdt), level="ERROR") #self.u.grab("Press [enter] to return...") - self.utils.request_input() if temp: shutil.rmtree(temp,ignore_errors=True) # Restore any prior tables @@ -257,26 +246,26 @@ def read_acpi_tables(self, path): return # Let's load the rest of the tables if len(tables) > 1: - print("Loading valid tables in {}...".format(path)) + self.utils.log_message("[ACPI GURU] Loading valid tables in {}...".format(path), level="INFO") loaded_tables,failed = self.acpi.load(temp or path) if not loaded_tables or failed: - print("\nFailed to load tables in {}{}\n".format( + self.utils.log_message("[ACPI GURU] Failed to load tables in {}{}\n".format( os.path.dirname(path) if os.path.isfile(path) else path, ":" if failed else "" )) for t in self.sorted_nicely(failed): - print(" - {}".format(t)) + self.utils.log_message("[ACPI GURU] Failed to load table: {}".format(t), level="ERROR") # Restore any prior tables if not loaded_tables: self.acpi.acpi_tables = prior_tables else: - if len(tables) > 1: - print("") # Newline for readability - print("Done.") + #if len(tables) > 1: + # print("") # Newline for readability + self.utils.log_message("[ACPI GURU] Done.", level="INFO") # If we had to patch the DSDT, or if not all tables loaded, # make sure we get interaction from the user to continue if trouble_dsdt or not loaded_tables or failed: - print("") + pass #self.u.grab("Press [enter] to return...") #self.utils.request_input() if temp: @@ -293,7 +282,7 @@ def ensure_dsdt(self, allow_any=False): # Got it already return True # Need to prompt - self.select_acpi_tables() + #self.select_acpi_tables() self.dsdt = self.acpi.get_dsdt_or_only() if self._ensure_dsdt(allow_any=allow_any): return True @@ -3214,20 +3203,6 @@ def drop_cpu_tables(self): "Delete": deletes } - def select_acpi_tables(self): - while True: - self.utils.head("Select ACPI Tables") - print("") - print("Q. Quit") - print(" ") - menu = self.utils.request_input("Please drag and drop ACPI Tables folder here: ") - if menu.lower() == "q": - self.utils.exit_program() - path = self.utils.normalize_path(menu) - if not path: - continue - return self.read_acpi_tables(path) - def get_patch_index(self, name): for index, patch in enumerate(self.patches): if patch.name == name: @@ -3235,6 +3210,7 @@ def get_patch_index(self, name): return None def select_acpi_patches(self, hardware_report, disabled_devices): + self.utils.log_message("[ACPI GURU] Selecting ACPI patches...", level="INFO") selected_patches = [] if "Laptop" in hardware_report.get("Motherboard").get("Platform") and \ @@ -3315,42 +3291,22 @@ def select_acpi_patches(self, hardware_report, disabled_devices): if device_info.get("Bus Type") == "ACPI" and device_info.get("Device") in pci_data.YogaHIDs: selected_patches.append("WMIS") + self.utils.log_message("[ACPI GURU] Selected patches: {}".format(", ".join(selected_patches)), level="INFO") for patch in self.patches: patch.checked = patch.name in selected_patches def customize_patch_selection(self): - while True: - contents = [] - contents.append("") - contents.append("List of available patches:") - contents.append("") - for index, kext in enumerate(self.patches, start=1): - checkbox = "[*]" if kext.checked else "[ ]" + items = [] + checked_indices = [] + + for i, patch in enumerate(self.patches): + label = f"{patch.name} - {patch.description}" + items.append(label) + if patch.checked: + checked_indices.append(i) - line = "{} {:2}. {:15} - {:60}".format(checkbox, index, kext.name, kext.description) - if kext.checked: - line = "\033[1;32m{}\033[0m".format(line) - contents.append(line) - contents.append("") - contents.append("\033[1;93mNote:\033[0m You can select multiple kexts by entering their indices separated by commas (e.g., '1, 2, 3').") - contents.append("") - contents.append("B. Back") - contents.append("Q. Quit") - contents.append("") - content = "\n".join(contents) - - self.utils.adjust_window_size(content) - self.utils.head("Customize ACPI Patch Selections", resize=False) - print(content) - option = self.utils.request_input("Select your option: ") - if option.lower() == "q": - self.utils.exit_program() - if option.lower() == "b": - return - - indices = [int(i.strip()) -1 for i in option.split(",") if i.strip().isdigit()] - - for index in indices: - if index >= 0 and index < len(self.patches): - patch = self.patches[index] - patch.checked = not patch.checked \ No newline at end of file + result = show_checklist_dialog("Configure ACPI Patches", "Select ACPI patches you want to apply:", items, checked_indices) + + if result is not None: + for i, patch in enumerate(self.patches): + patch.checked = i in result \ No newline at end of file diff --git a/Scripts/backend.py b/Scripts/backend.py new file mode 100644 index 00000000..e44e55eb --- /dev/null +++ b/Scripts/backend.py @@ -0,0 +1,140 @@ +import os +import sys +import logging +from datetime import datetime + +from PyQt6.QtCore import QObject, pyqtSignal + +from Scripts import acpi_guru +from Scripts import compatibility_checker +from Scripts import config_prodigy +from Scripts import gathering_files +from Scripts import hardware_customizer +from Scripts import kext_maestro +from Scripts import report_validator +from Scripts import run +from Scripts import smbios +from Scripts import settings +from Scripts import utils +from Scripts import integrity_checker +from Scripts import resource_fetcher +from Scripts import github +from Scripts import wifi_profile_extractor +from Scripts import dsdt + +class LogSignalHandler(logging.Handler): + def __init__(self, signal): + super().__init__() + self.signal = signal + + def emit(self, record): + msg = self.format(record) + to_build_log = getattr(record, "to_build_log", False) + self.signal.emit(msg, record.levelname, to_build_log) + +class Backend(QObject): + log_message_signal = pyqtSignal(str, str, bool) + update_status_signal = pyqtSignal(str, str) + + def __init__(self): + super().__init__() + + self.u = utils.Utils() + self.settings = settings.Settings(utils_instance=self.u) + self.log_file_path = None + + self._setup_logging() + self.u.clean_temporary_dir() + + self.integrity_checker = integrity_checker.IntegrityChecker(utils_instance=self.u) + + self.resource_fetcher = resource_fetcher.ResourceFetcher( + utils_instance=self.u, + integrity_checker_instance=self.integrity_checker + ) + self.github = github.Github( + utils_instance=self.u, + resource_fetcher_instance=self.resource_fetcher + ) + + self.r = run.Run() + self.wifi_extractor = wifi_profile_extractor.WifiProfileExtractor( + run_instance=self.r, + utils_instance=self.u + ) + self.k = kext_maestro.KextMaestro(utils_instance=self.u) + self.c = compatibility_checker.CompatibilityChecker( + utils_instance=self.u, + settings_instance=self.settings + ) + self.h = hardware_customizer.HardwareCustomizer(utils_instance=self.u) + self.v = report_validator.ReportValidator(utils_instance=self.u) + self.dsdt = dsdt.DSDT( + utils_instance=self.u, + github_instance=self.github, + resource_fetcher_instance=self.resource_fetcher, + run_instance=self.r + ) + + self.o = gathering_files.gatheringFiles( + utils_instance=self.u, + github_instance=self.github, + kext_maestro_instance=self.k, + integrity_checker_instance=self.integrity_checker, + resource_fetcher_instance=self.resource_fetcher + ) + + self.s = smbios.SMBIOS( + gathering_files_instance=self.o, + run_instance=self.r, + utils_instance=self.u, + settings_instance=self.settings + ) + + self.ac = acpi_guru.ACPIGuru( + dsdt_instance=self.dsdt, + smbios_instance=self.s, + run_instance=self.r, + utils_instance=self.u + ) + + self.co = config_prodigy.ConfigProdigy( + gathering_files_instance=self.o, + smbios_instance=self.s, + utils_instance=self.u + ) + + custom_output_dir = self.settings.get_build_output_directory() + if custom_output_dir: + self.result_dir = self.u.create_folder(custom_output_dir, remove_content=True) + else: + self.result_dir = self.u.get_temporary_dir() + + def _setup_logging(self): + logger = logging.getLogger("OpCoreSimplify") + logger.setLevel(logging.DEBUG) + + logger.handlers = [] + + stream_handler = logging.StreamHandler(sys.stdout) + stream_handler.setLevel(logging.DEBUG) + stream_handler.setFormatter(logging.Formatter("[%(levelname)s] %(message)s")) + logger.addHandler(stream_handler) + + signal_handler = LogSignalHandler(self.log_message_signal) + signal_handler.setLevel(logging.DEBUG) + signal_handler.setFormatter(logging.Formatter("%(message)s")) + logger.addHandler(signal_handler) + + if self.settings.get_enable_debug_logging(): + try: + log_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "Logs") + os.makedirs(log_dir, exist_ok=True) + timestamp = datetime.now().strftime("%Y-%m-%d-%H%M%S") + self.log_file_path = os.path.join(log_dir, "ocs-{}.txt".format(timestamp)) + file_handler = logging.FileHandler(self.log_file_path, encoding="utf-8") + file_handler.setLevel(logging.DEBUG) + file_handler.setFormatter(logging.Formatter("[%(asctime)s] [%(levelname)s] %(message)s", "%Y-%m-%d %H:%M:%S")) + logger.addHandler(file_handler) + except Exception as e: + print("Failed to setup file logging: {}".format(e)) \ No newline at end of file diff --git a/Scripts/compatibility_checker.py b/Scripts/compatibility_checker.py index 2e11dc29..46be45b6 100644 --- a/Scripts/compatibility_checker.py +++ b/Scripts/compatibility_checker.py @@ -3,39 +3,14 @@ from Scripts.datasets import pci_data from Scripts.datasets import codec_layouts from Scripts import utils -import time +from Scripts import settings class CompatibilityChecker: - def __init__(self): - self.utils = utils.Utils() + def __init__(self, utils_instance=None, settings_instance=None): + self.utils = utils_instance if utils_instance else utils.Utils() + self.settings = settings_instance if settings_instance else settings.Settings() + self.error_codes = [] - def show_macos_compatibility(self, device_compatibility): - if not device_compatibility: - return "\033[90mUnchecked\033[0m" - - if not device_compatibility[0]: - return "\033[0;31mUnsupported\033[0m" - - max_compatibility = self.utils.parse_darwin_version(device_compatibility[0])[0] - min_compatibility = self.utils.parse_darwin_version(device_compatibility[-1])[0] - max_version = self.utils.parse_darwin_version(os_data.get_latest_darwin_version())[0] - min_version = self.utils.parse_darwin_version(os_data.get_lowest_darwin_version())[0] - - if max_compatibility == min_version: - return "\033[1;36mMaximum support up to {}\033[0m".format( - os_data.get_macos_name_by_darwin(device_compatibility[-1]) - ) - - if min_version < min_compatibility or max_compatibility < max_version: - return "\033[1;32m{} to {}\033[0m".format( - os_data.get_macos_name_by_darwin(device_compatibility[-1]), - os_data.get_macos_name_by_darwin(device_compatibility[0]) - ) - - return "\033[1;36mUp to {}\033[0m".format( - os_data.get_macos_name_by_darwin(device_compatibility[0]) - ) - def is_low_end_intel_cpu(self, processor_name): return any(cpu_branding in processor_name for cpu_branding in ("Celeron", "Pentium")) @@ -53,29 +28,14 @@ def check_cpu_compatibility(self): self.hardware_report["CPU"]["Compatibility"] = (max_version, min_version) - print("{}- {}: {}".format(" "*3, self.hardware_report.get("CPU").get("Processor Name"), self.show_macos_compatibility(self.hardware_report["CPU"].get("Compatibility")))) - if max_version == min_version and max_version == None: - print("") - print("Missing required SSE4.x instruction set.") - print("Your CPU is not supported by macOS versions newer than Sierra (10.12).") - print("") - self.utils.request_input() - self.utils.exit_program() + self.error_codes.append("ERROR_MISSING_SSE4") + return self.max_native_macos_version = max_version self.min_native_macos_version = min_version def check_gpu_compatibility(self): - if not self.hardware_report.get("GPU"): - print("") - print("No GPU found!") - print("Please make sure to export the hardware report with the GPU information") - print("and try again.") - print("") - self.utils.request_input() - self.utils.exit_program() - for gpu_name, gpu_props in self.hardware_report["GPU"].items(): gpu_manufacturer = gpu_props.get("Manufacturer") gpu_codename = gpu_props.get("Codename") @@ -155,21 +115,6 @@ def check_gpu_compatibility(self): if self.utils.parse_darwin_version(max_version) < self.utils.parse_darwin_version(ocl_patched_max_version): gpu_props["OCLP Compatibility"] = (ocl_patched_max_version, ocl_patched_min_version if self.utils.parse_darwin_version(ocl_patched_min_version) > self.utils.parse_darwin_version("{}.{}.{}".format(int(max_version[:2]) + 1, 0, 0)) else "{}.{}.{}".format(int(max_version[:2]) + 1, 0, 0)) - print("{}- {}: {}".format(" "*3, gpu_name, self.show_macos_compatibility(gpu_props.get("Compatibility")))) - - if "OCLP Compatibility" in gpu_props: - print("{}- OCLP Compatibility: {}".format(" "*6, self.show_macos_compatibility(gpu_props.get("OCLP Compatibility")))) - - connected_monitors = [] - for monitor_name, monitor_info in self.hardware_report.get("Monitor", {}).items(): - if monitor_info.get("Connected GPU") == gpu_name: - connected_monitors.append("{} ({})".format(monitor_name, monitor_info.get("Connector Type"))) - if "Intel" in gpu_manufacturer and device_id.startswith(("01", "04", "0A", "0C", "0D")): - if monitor_info.get("Connector Type") == "VGA": - connected_monitors[-1] = "\033[0;31m{}{}\033[0m".format(connected_monitors[-1][:-1], ", unsupported)") - if connected_monitors: - print("{}- Connected Monitor{}: {}".format(" "*6, "s" if len(connected_monitors) > 1 else "", ", ".join(connected_monitors))) - max_supported_gpu_version = min_supported_gpu_version = None for gpu_name, gpu_props in self.hardware_report.get("GPU").items(): @@ -189,18 +134,14 @@ def check_gpu_compatibility(self): self.ocl_patched_macos_version = (gpu_props.get("OCLP Compatibility")[0], self.ocl_patched_macos_version[-1] if self.ocl_patched_macos_version and self.utils.parse_darwin_version(self.ocl_patched_macos_version[-1]) < self.utils.parse_darwin_version(gpu_props.get("OCLP Compatibility")[-1]) else gpu_props.get("OCLP Compatibility")[-1]) if max_supported_gpu_version == min_supported_gpu_version and max_supported_gpu_version == None: - print("") - print("You cannot install macOS without a supported GPU.") - print("Please do NOT spam my inbox or issue tracker about this issue anymore!") - print("") - self.utils.request_input() - self.utils.exit_program() + self.error_codes.append("ERROR_NO_COMPATIBLE_GPU") + return self.max_native_macos_version = max_supported_gpu_version if self.utils.parse_darwin_version(max_supported_gpu_version) < self.utils.parse_darwin_version(self.max_native_macos_version) else self.max_native_macos_version self.min_native_macos_version = min_supported_gpu_version if self.utils.parse_darwin_version(min_supported_gpu_version) > self.utils.parse_darwin_version(self.min_native_macos_version) else self.min_native_macos_version def check_sound_compatibility(self): - for audio_device, audio_props in self.hardware_report.get("Sound", {}).items(): + for _, audio_props in self.hardware_report.get("Sound", {}).items(): codec_id = audio_props.get("Device ID") max_version = min_version = None @@ -213,19 +154,9 @@ def check_sound_compatibility(self): audio_props["Compatibility"] = (max_version, min_version) - print("{}- {}: {}".format(" "*3, audio_device, self.show_macos_compatibility(audio_props.get("Compatibility")))) - - audio_endpoints = audio_props.get("Audio Endpoints") - if audio_endpoints: - print("{}- Audio Endpoint{}: {}".format(" "*6, "s" if len(audio_endpoints) > 1 else "", ", ".join(audio_endpoints))) - def check_biometric_compatibility(self): - print(" \033[1;93mNote:\033[0m Biometric authentication in macOS requires Apple T2 Chip,") - print(" which is not available for Hackintosh systems.") - print("") - for biometric_device, biometric_props in self.hardware_report.get("Biometric", {}).items(): + for _, biometric_props in self.hardware_report.get("Biometric", {}).items(): biometric_props["Compatibility"] = (None, None) - print("{}- {}: {}".format(" "*3, biometric_device, self.show_macos_compatibility(biometric_props.get("Compatibility")))) def check_network_compatibility(self): for device_name, device_props in self.hardware_report.get("Network", {}).items(): @@ -265,31 +196,7 @@ def check_network_compatibility(self): if bus_type.startswith("PCI") and not device_props.get("Compatibility"): device_props["Compatibility"] = (None, None) - print("{}- {}: {}".format(" "*3, device_name, self.show_macos_compatibility(device_props.get("Compatibility")))) - - if device_id in pci_data.WirelessCardIDs: - if device_id in pci_data.BroadcomWiFiIDs: - print("{}- Continuity Support: \033[1;32mFull\033[0m (AirDrop, Handoff, Universal Clipboard, Instant Hotspot,...)".format(" "*6)) - elif device_id in pci_data.IntelWiFiIDs: - print("{}- Continuity Support: \033[1;33mPartial\033[0m (Handoff and Universal Clipboard with AirportItlwm)".format(" "*6)) - print("{}\033[1;93mNote:\033[0m AirDrop, Universal Clipboard, Instant Hotspot,... not available".format(" "*6)) - elif device_id in pci_data.AtherosWiFiIDs: - print("{}- Continuity Support: \033[1;31mLimited\033[0m (No Continuity features available)".format(" "*6)) - print("{}\033[1;93mNote:\033[0m Atheros cards are not recommended for macOS".format(" "*6)) - - if "OCLP Compatibility" in device_props: - print("{}- OCLP Compatibility: {}".format(" "*6, self.show_macos_compatibility(device_props.get("OCLP Compatibility")))) - def check_storage_compatibility(self): - if not self.hardware_report.get("Storage Controllers"): - print("") - print("No storage controller found!") - print("Please make sure to export the hardware report with the storage controller information") - print("and try again.") - print("") - self.utils.request_input() - self.utils.exit_program() - for controller_name, controller_props in self.hardware_report["Storage Controllers"].items(): if controller_props.get("Bus Type") != "PCI": continue @@ -301,28 +208,17 @@ def check_storage_compatibility(self): min_version = os_data.get_lowest_darwin_version() if device_id in pci_data.IntelVMDIDs: - print("") - print("Intel VMD controllers are not supported in macOS.") - print("Please disable Intel VMD in the BIOS settings and try again with new hardware report.") - print("") - self.utils.request_input() - self.utils.exit_program() + self.error_codes.append("ERROR_INTEL_VMD") + return if next((device for device in pci_data.UnsupportedNVMeSSDIDs if device_id == device[0] and subsystem_id in device[1]), None): max_version = min_version = None controller_props["Compatibility"] = (max_version, min_version) - - print("{}- {}: {}".format(" "*3, controller_name, self.show_macos_compatibility(controller_props.get("Compatibility")))) if all(controller_props.get("Compatibility") == (None, None) for controller_name, controller_props in self.hardware_report["Storage Controllers"].items()): - print("") - print("No compatible storage controller for macOS was found!") - print("Consider purchasing a compatible SSD NVMe for your system.") - print("Western Digital NVMe SSDs are generally recommended for good macOS compatibility.") - print("") - self.utils.request_input() - self.utils.exit_program() + self.error_codes.append("ERROR_NO_COMPATIBLE_STORAGE") + return def check_bluetooth_compatibility(self): for bluetooth_name, bluetooth_props in self.hardware_report.get("Bluetooth", {}).items(): @@ -339,8 +235,6 @@ def check_bluetooth_compatibility(self): max_version = min_version = None bluetooth_props["Compatibility"] = (max_version, min_version) - - print("{}- {}: {}".format(" "*3, bluetooth_name, self.show_macos_compatibility(bluetooth_props.get("Compatibility")))) def check_sd_controller_compatibility(self): for controller_name, controller_props in self.hardware_report.get("SD Controller", {}).items(): @@ -357,16 +251,12 @@ def check_sd_controller_compatibility(self): controller_props["Compatibility"] = (max_version, min_version) - print("{}- {}: {}".format(" "*3, controller_name, self.show_macos_compatibility(controller_props.get("Compatibility")))) - def check_compatibility(self, hardware_report): self.hardware_report = hardware_report self.ocl_patched_macos_version = None + self.error_codes = [] - self.utils.head("Compatibility Checker") - print("") - print("Checking compatibility with macOS for the following devices:") - print("") + self.utils.log_message("[COMPATIBILITY CHECKER] Starting compatibility check...", level="INFO") steps = [ ('CPU', self.check_cpu_compatibility), @@ -379,15 +269,13 @@ def check_compatibility(self, hardware_report): ('SD Controller', self.check_sd_controller_compatibility) ] - index = 0 for device_type, function in steps: if self.hardware_report.get(device_type): - index += 1 - print("{}. {}:".format(index, device_type)) - time.sleep(0.25) function() - print("") - self.utils.request_input() + if self.error_codes: + self.utils.log_message("[COMPATIBILITY CHECKER] Compatibility check that found errors: {}".format(", ".join(self.error_codes)), level="INFO") + return hardware_report, (None, None), None, self.error_codes - return hardware_report, (self.min_native_macos_version, self.max_native_macos_version), self.ocl_patched_macos_version \ No newline at end of file + self.utils.log_message("[COMPATIBILITY CHECKER] Compatibility check completed successfully", level="INFO") + return hardware_report, (self.min_native_macos_version, self.max_native_macos_version), self.ocl_patched_macos_version, self.error_codes \ No newline at end of file diff --git a/Scripts/config_prodigy.py b/Scripts/config_prodigy.py index 8007c0c3..36c2c69f 100644 --- a/Scripts/config_prodigy.py +++ b/Scripts/config_prodigy.py @@ -1,3 +1,5 @@ +import copy +import os from Scripts.datasets import chipset_data from Scripts.datasets import cpu_data from Scripts.datasets import mac_model_data @@ -8,13 +10,14 @@ from Scripts import gathering_files from Scripts import smbios from Scripts import utils +from Scripts.custom_dialogs import show_options_dialog import random class ConfigProdigy: - def __init__(self): - self.g = gathering_files.gatheringFiles() - self.smbios = smbios.SMBIOS() - self.utils = utils.Utils() + def __init__(self, gathering_files_instance=None, smbios_instance=None, utils_instance=None): + self.g = gathering_files_instance if gathering_files_instance else gathering_files.gatheringFiles() + self.smbios = smbios_instance if smbios_instance else smbios.SMBIOS() + self.utils = utils_instance if utils_instance else utils.Utils() self.cpuids = { "Ivy Bridge": "A9060300", "Haswell": "C3060300", @@ -237,76 +240,7 @@ def igpu_properties(self, platform, integrated_gpu, monitor, macos_version): return dict(sorted(igpu_properties.items(), key=lambda item: item[0])) - def select_audio_codec_layout(self, hardware_report, config=None, controller_required=False): - try: - for device_properties in config["DeviceProperties"]["Add"].values(): - if device_properties.get("layout-id"): - return None, None - except: - pass - - codec_id = None - audio_controller_properties = None - - for codec_properties in hardware_report.get("Sound", {}).values(): - if codec_properties.get("Device ID") in codec_layouts.data: - codec_id = codec_properties.get("Device ID") - - if codec_properties.get("Controller Device ID"): - for device_name, device_properties in hardware_report.get("System Devices").items(): - if device_properties.get("Device ID") == codec_properties.get("Controller Device ID"): - audio_controller_properties = device_properties - break - break - - if not codec_id: - return None, None - - if controller_required and not audio_controller_properties: - return None, None - - available_layouts = codec_layouts.data.get(codec_id) - - recommended_authors = ("Mirone", "InsanelyDeepak", "Toleda", "DalianSky") - recommended_layouts = [layout for layout in available_layouts if self.utils.contains_any(recommended_authors, layout.comment)] - - default_layout = random.choice(recommended_layouts or available_layouts) - - while True: - contents = [] - contents.append("") - contents.append("List of Codec Layouts:") - contents.append("") - contents.append("ID Comment") - contents.append("------------------------------------------------------------------") - for layout in available_layouts: - line = "{:<4} {}".format(layout.id, layout.comment[:60]) - if layout == default_layout: - contents.append("\033[1;32m{}\033[0m".format(line)) - else: - contents.append(line) - contents.append("") - contents.append("\033[1;93mNote:\033[0m") - contents.append("- The default layout may not be optimal.") - contents.append("- Test different layouts to find what works best for your system.") - contents.append("") - content = "\n".join(contents) - - self.utils.adjust_window_size(content) - self.utils.head("Choosing Codec Layout ID", resize=False) - print(content) - selected_layout_id = self.utils.request_input(f"Enter the ID of the codec layout you want to use (default: {default_layout.id}): ") or default_layout.id - - try: - selected_layout_id = int(selected_layout_id) - - for layout in available_layouts: - if layout.id == selected_layout_id: - return selected_layout_id, audio_controller_properties - except: - continue - - def deviceproperties(self, hardware_report, disabled_devices, macos_version, kexts): + def deviceproperties(self, hardware_report, disabled_devices, macos_version, kexts, audio_layout_id=None, audio_controller_properties=None): deviceproperties_add = {} def add_device_property(pci_path, properties): @@ -349,11 +283,8 @@ def add_device_property(pci_path, properties): "model": gpu_name }) - if kexts[kext_data.kext_index_by_name.get("AppleALC")].checked: - selected_layout_id, audio_controller_properties = self.select_audio_codec_layout(hardware_report, controller_required=True) - - if selected_layout_id and audio_controller_properties: - add_device_property(audio_controller_properties.get("PCI Path"), {"layout-id": selected_layout_id}) + if audio_layout_id is not None and audio_controller_properties is not None: + add_device_property(audio_controller_properties.get("PCI Path"), {"layout-id": audio_layout_id}) for network_name, network_props in hardware_report.get("Network", {}).items(): device_id = network_props.get("Device ID") @@ -502,7 +433,7 @@ def load_kernel_patch(self, motherboard_chipset, cpu_manufacturer, cpu_codename, return kernel_patch - def boot_args(self, hardware_report, macos_version, needs_oclp, kexts, config): + def boot_args(self, hardware_report, macos_version, needs_oclp, kexts, config, audio_layout_id=None, audio_controller_properties=None): boot_args = [ "-v", "debug=0x100", @@ -566,10 +497,8 @@ def boot_args(self, hardware_report, macos_version, needs_oclp, kexts, config): elif discrete_gpu.get("Manufacturer") == "NVIDIA" and not "Kepler" in discrete_gpu.get("Codename"): boot_args.extend(("nvda_drv_vrl=1", "ngfxcompat=1", "ngfxgl=1")) elif kext.name == "AppleALC": - selected_layout_id, _ = self.select_audio_codec_layout(hardware_report, config) - - if selected_layout_id: - boot_args.append("alcid={}".format(selected_layout_id)) + if audio_layout_id is not None and audio_controller_properties is None: + boot_args.append("alcid={}".format(audio_layout_id)) elif kext.name == "VoodooI2C": boot_args.append("-vi2c-force-polling") elif kext.name == "CpuTopologyRebuild": @@ -611,7 +540,7 @@ def load_drivers(self, firmware_type, cpu_codename, macos_version, picker_mode): return uefi_drivers - def genarate(self, hardware_report, disabled_devices, smbios_model, macos_version, needs_oclp, kexts, config): + def genarate(self, hardware_report, disabled_devices, smbios_model, macos_version, needs_oclp, kexts, config, audio_layout_id=None, audio_controller_properties=None): del config["#WARNING - 1"] del config["#WARNING - 2"] del config["#WARNING - 3"] @@ -636,7 +565,7 @@ def genarate(self, hardware_report, disabled_devices, smbios_model, macos_versio config["Booter"]["Quirks"]["SetupVirtualMap"] = hardware_report.get("BIOS").get("Firmware Type") == "UEFI" and not hardware_report.get("Motherboard").get("Chipset") in chipset_data.AMDChipsets[11:17] + chipset_data.IntelChipsets[90:100] config["Booter"]["Quirks"]["SyncRuntimePermissions"] = "AMD" in hardware_report.get("CPU").get("Manufacturer") or hardware_report.get("Motherboard").get("Chipset") in chipset_data.IntelChipsets[90:100] + chipset_data.IntelChipsets[104:] - config["DeviceProperties"]["Add"] = self.deviceproperties(hardware_report, disabled_devices, macos_version, kexts) + config["DeviceProperties"]["Add"] = self.deviceproperties(hardware_report, disabled_devices, macos_version, kexts, audio_layout_id, audio_controller_properties) config["Kernel"]["Block"] = self.block_kext_bundle(kexts) spoof_cpuid = self.spoof_cpuid( @@ -685,7 +614,7 @@ def genarate(self, hardware_report, disabled_devices, smbios_model, macos_versio config["Misc"]["Tools"] = [] del config["NVRAM"]["Add"]["7C436110-AB2A-4BBB-A880-FE41995C9F82"]["#INFO (prev-lang:kbd)"] - config["NVRAM"]["Add"]["7C436110-AB2A-4BBB-A880-FE41995C9F82"]["boot-args"] = self.boot_args(hardware_report, macos_version, needs_oclp, kexts, config) + config["NVRAM"]["Add"]["7C436110-AB2A-4BBB-A880-FE41995C9F82"]["boot-args"] = self.boot_args(hardware_report, macos_version, needs_oclp, kexts, config, audio_layout_id, audio_controller_properties) config["NVRAM"]["Add"]["7C436110-AB2A-4BBB-A880-FE41995C9F82"]["csr-active-config"] = self.utils.hex_to_bytes(self.csr_active_config(macos_version)) config["NVRAM"]["Add"]["7C436110-AB2A-4BBB-A880-FE41995C9F82"]["prev-lang:kbd"] = self.utils.hex_to_bytes("") diff --git a/Scripts/custom_dialogs.py b/Scripts/custom_dialogs.py new file mode 100644 index 00000000..cb52ba94 --- /dev/null +++ b/Scripts/custom_dialogs.py @@ -0,0 +1,490 @@ +import re +import functools +from PyQt6.QtCore import Qt, QObject, QThread, QMetaObject, QCoreApplication, pyqtSlot, pyqtSignal +from PyQt6.QtWidgets import QWidget, QHBoxLayout, QRadioButton, QButtonGroup, QVBoxLayout, QCheckBox, QScrollArea, QLabel, QSizePolicy, QLayout +from qfluentwidgets import MessageBoxBase, SubtitleLabel, BodyLabel, LineEdit, PushButton, ProgressBar + +from Scripts.datasets import os_data + +_default_gui_handler = None + +def set_default_gui_handler(handler): + global _default_gui_handler + _default_gui_handler = handler + +class ThreadRunner(QObject): + def __init__(self, func, *args, **kwargs): + super().__init__() + self.func = func + self.args = args + self.kwargs = kwargs + self.result = None + self.exception = None + + @pyqtSlot() + def run(self): + try: + self.result = self.func(*self.args, **self.kwargs) + except Exception as e: + self.exception = e + +def ensure_main_thread(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + if QThread.currentThread() == QCoreApplication.instance().thread(): + return func(*args, **kwargs) + + runner = ThreadRunner(func, *args, **kwargs) + runner.moveToThread(QCoreApplication.instance().thread()) + QMetaObject.invokeMethod(runner, "run", Qt.ConnectionType.BlockingQueuedConnection) + + if runner.exception: + raise runner.exception + return runner.result + return wrapper + +class CustomMessageDialog(MessageBoxBase): + def __init__(self, title, content): + super().__init__(_default_gui_handler) + + self.titleLabel = SubtitleLabel(title, self.widget) + self.contentLabel = BodyLabel(content, self.widget) + self.contentLabel.setWordWrap(True) + + is_html = bool(re.search(r"<[^>]+>", content)) + + if is_html: + self.contentLabel.setTextFormat(Qt.TextFormat.RichText) + self.contentLabel.setTextInteractionFlags(Qt.TextInteractionFlag.TextBrowserInteraction) + self.contentLabel.setOpenExternalLinks(True) + + self.viewLayout.addWidget(self.titleLabel) + self.viewLayout.addWidget(self.contentLabel) + + self.widget.setMinimumWidth(600) + + self.custom_widget = None + self.input_field = None + self.button_group = None + + def add_input(self, placeholder: str = "", default_value: str = ""): + self.input_field = LineEdit(self.widget) + if placeholder: + self.input_field.setPlaceholderText(placeholder) + if default_value: + self.input_field.setText(str(default_value)) + + self.viewLayout.addWidget(self.input_field) + self.input_field.setFocus() + return self.input_field + + def add_custom_widget(self, widget: QWidget): + self.custom_widget = widget + self.viewLayout.addWidget(widget) + + def add_radio_options(self, options, default_index=0): + self.button_group = QButtonGroup(self) + + scroll = QScrollArea(self.widget) + scroll.setWidgetResizable(True) + scroll.setStyleSheet("QScrollArea { background: transparent; border: none; }") + scroll.viewport().setStyleSheet("background: transparent;") + + container = QWidget() + layout = QVBoxLayout(container) + layout.setContentsMargins(10, 5, 10, 5) + layout.setSpacing(12) + + for i, option_text in enumerate(options): + row_widget = QWidget() + row_layout = QHBoxLayout(row_widget) + row_layout.setContentsMargins(0, 0, 0, 0) + row_layout.setSpacing(8) + + radio = QRadioButton(row_widget) + radio.setStyleSheet(""" + QRadioButton::indicator { + width: 16px; height: 16px; + border-radius: 8px; + } + QRadioButton::indicator:unchecked { + border: 1px solid #000000; + background-color: #ffffff; + } + QRadioButton::indicator:checked { + border: 1px solid #000000; + background-color: #0078D4; + } + """) + + label = BodyLabel(option_text, row_widget) + label.setTextFormat(Qt.TextFormat.RichText) + label.setTextInteractionFlags(Qt.TextInteractionFlag.TextBrowserInteraction) + label.setOpenExternalLinks(True) + label.setWordWrap(True) + label.setStyleSheet("color: #000000; background-color: transparent;") + + row_layout.addWidget(radio, 0, Qt.AlignmentFlag.AlignTop) + row_layout.addWidget(label, 1) + layout.addWidget(row_widget) + + self.button_group.addButton(radio, i) + if i == default_index: + radio.setChecked(True) + + scroll.setWidget(container) + self.viewLayout.addWidget(scroll) + + return self.button_group + + def add_checklist(self, items, checked_indices=None): + if checked_indices is None: + checked_indices = [] + + scroll = QScrollArea() + scroll.setWidgetResizable(True) + scroll.setFixedHeight(400) + + container = QWidget() + layout = QVBoxLayout(container) + + checkboxes = [] + current_category = None + + for i, item in enumerate(items): + label_text = item + category = None + supported = True + + if isinstance(item, dict): + label_text = item.get("label", "") + category = item.get("category") + supported = item.get("supported", True) + + if category and category != current_category: + current_category = category + + if i > 0: + layout.addSpacing(10) + + header = QLabel("Category: {}".format(category)) + header.setStyleSheet("font-weight: bold; color: #0078D4; padding-top: 5px; padding-bottom: 5px; border-bottom: 1px solid #E1DFDD;") + layout.addWidget(header) + + cb = QCheckBox(label_text) + if i in checked_indices: + cb.setChecked(True) + + if not supported: + cb.setStyleSheet("color: #A19F9D;") + + layout.addWidget(cb) + checkboxes.append(cb) + + layout.addStretch() + scroll.setWidget(container) + self.viewLayout.addWidget(scroll) + return checkboxes + + def configure_buttons(self, yes_text: str = "OK", no_text: str = "Cancel", show_cancel: bool = True): + self.yesButton.setText(yes_text) + self.cancelButton.setText(no_text) + self.cancelButton.setVisible(show_cancel) + +@ensure_main_thread +def show_info(title: str, content: str) -> None: + dialog = CustomMessageDialog(title, content) + dialog.configure_buttons(yes_text="OK", show_cancel=False) + dialog.exec() + +@ensure_main_thread +def show_confirmation(title: str, content: str, yes_text="Yes", no_text="No") -> bool: + dialog = CustomMessageDialog(title, content) + dialog.configure_buttons(yes_text=yes_text, no_text=no_text, show_cancel=True) + return dialog.exec() + +@ensure_main_thread +def show_options_dialog(title, content, options, default_index=0): + dialog = CustomMessageDialog(title, content) + dialog.add_radio_options(options, default_index) + dialog.configure_buttons(yes_text="OK", show_cancel=True) + + if dialog.exec(): + return dialog.button_group.checkedId() + return None + +@ensure_main_thread +def show_checklist_dialog(title, content, items, checked_indices=None): + dialog = CustomMessageDialog(title, content) + checkboxes = dialog.add_checklist(items, checked_indices) + dialog.configure_buttons(yes_text="OK", show_cancel=True) + + if dialog.exec(): + return [i for i, cb in enumerate(checkboxes) if cb.isChecked()] + return None + +@ensure_main_thread +def ask_network_count(total_networks): + content = ( + "Found {} WiFi networks on this device.

" + "How many networks would you like to process?
" + "" + ).format(total_networks, total_networks) + + dialog = CustomMessageDialog("WiFi Network Retrieval", content) + dialog.input_field = dialog.add_input(placeholder="1-{} (Default: 5)".format(total_networks), default_value="5") + + button_layout = QHBoxLayout() + all_btn = PushButton("Process All Networks", dialog.widget) + button_layout.addWidget(all_btn) + button_layout.addStretch() + dialog.viewLayout.addLayout(button_layout) + + result = {"value": 5} + + def on_all_clicked(): + result["value"] = "a" + dialog.accept() + + all_btn.clicked.connect(on_all_clicked) + + def on_accept(): + if result["value"] == "a": + return + + text = dialog.input_field.text().strip() + if not text: + result["value"] = 5 + elif text.lower() == "a": + result["value"] = "a" + else: + try: + val = int(text) + result["value"] = min(max(1, val), total_networks) + except ValueError: + result["value"] = 5 + + original_accept = dialog.accept + def custom_accept(): + on_accept() + original_accept() + + dialog.accept = custom_accept + + if dialog.exec(): + return result["value"] + + return 5 + +def show_smbios_selection_dialog(title, content, items, current_selection, default_selection): + dialog = CustomMessageDialog(title, content) + + # Top controls (Show all models + Restore default) + top_container = QWidget() + top_layout = QHBoxLayout(top_container) + top_layout.setContentsMargins(0, 0, 0, 0) + + # Checkbox indicator only + show_all_cb = QCheckBox(top_container) + show_all_cb.setText("") # indicator only + + # Separate label for the text + show_all_label = BodyLabel("Show all models", top_container) + show_all_label.setWordWrap(True) + show_all_label.setStyleSheet("color: #000000; background: transparent;") + + restore_btn = PushButton("Restore default ({})".format(default_selection)) + + top_layout.addWidget(show_all_cb, 0, Qt.AlignmentFlag.AlignTop) + top_layout.addWidget(show_all_label, 1) + top_layout.addStretch() + top_layout.addWidget(restore_btn) + + dialog.viewLayout.addWidget(top_container) + + # Scroll area for the list + scroll = QScrollArea() + scroll.setWidgetResizable(True) + scroll.setFixedHeight(400) + + container = QWidget() + layout = QVBoxLayout(container) + layout.setSpacing(5) + + button_group = QButtonGroup(dialog) + item_widgets = [] + current_category = None + + for i, item in enumerate(items): + category = item.get("category") + category_label = None + if category != current_category: + current_category = category + category_label = QLabel("Category: {}".format(category)) + category_label.setStyleSheet( + "font-weight: bold; color: #0078D4; margin-top: 10px; border-bottom: 1px solid #E1DFDD;" + ) + layout.addWidget(category_label) + + row_widget = QWidget() + row_layout = QHBoxLayout(row_widget) + row_layout.setContentsMargins(20, 0, 0, 0) + + radio = QRadioButton(item.get("label")) + if not item.get("is_supported"): + radio.setStyleSheet("color: #A19F9D;") + + row_layout.addWidget(radio) + layout.addWidget(row_widget) + + button_group.addButton(radio, i) + + if item.get("name") == current_selection: + radio.setChecked(True) + + widget_data = { + "row": row_widget, + "category_label": category_label, + "item": item, + "radio": radio + } + item_widgets.append(widget_data) + + layout.addStretch() + scroll.setWidget(container) + dialog.viewLayout.addWidget(scroll) + + def update_visibility(): + show_all = show_all_cb.isChecked() + visible_categories = set() + + for w in item_widgets: + item = w["item"] + is_current_or_default = item.get("name") in (current_selection, default_selection) + is_compatible = item.get("is_compatible") + + should_show = is_current_or_default or show_all or is_compatible + w["row"].setVisible(should_show) + if should_show: + visible_categories.add(item.get("category")) + + for w in item_widgets: + if w["category_label"]: + w["category_label"].setVisible(w["item"].get("category") in visible_categories) + + show_all_cb.stateChanged.connect(update_visibility) + + def restore_default(): + for i, item in enumerate(items): + if item.get("name") == default_selection: + button_group.button(i).setChecked(True) + break + + restore_btn.clicked.connect(restore_default) + + update_visibility() + + dialog.configure_buttons(yes_text="OK", show_cancel=True) + + if dialog.exec(): + selected_id = button_group.checkedId() + if selected_id >= 0: + return items[selected_id].get("name") + + return None + +def show_macos_version_dialog(native_macos_version, ocl_patched_macos_version, suggested_macos_version): + content = "" + + if native_macos_version[1][:2] != suggested_macos_version[:2]: + suggested_macos_name = os_data.get_macos_name_by_darwin(suggested_macos_version) + content += "Suggested macOS version: For better compatibility and stability, we suggest you to use only {} or older.

".format(suggested_macos_name) + + content += "Please select the macOS version you want to use:" + + options = [] + version_values = [] + default_index = None + + native_min = int(native_macos_version[0][:2]) + native_max = int(native_macos_version[-1][:2]) + oclp_min = int(ocl_patched_macos_version[-1][:2]) if ocl_patched_macos_version else 99 + oclp_max = int(ocl_patched_macos_version[0][:2]) if ocl_patched_macos_version else 0 + min_version = min(native_min, oclp_min) + max_version = max(native_max, oclp_max) + + for darwin_version in range(min_version, max_version + 1): + if not (native_min <= darwin_version <= native_max or oclp_min <= darwin_version <= oclp_max): + continue + + name = os_data.get_macos_name_by_darwin(str(darwin_version)) + + label = "" + if oclp_min <= darwin_version <= oclp_max: + label = " (Requires OpenCore Legacy Patcher)" + + options.append("{}{}".format(name, label)) + version_values.append(darwin_version) + + if darwin_version == int(suggested_macos_version[:2]): + default_index = len(options) - 1 + + result = show_options_dialog("Select macOS Version", content, options, default_index) + + if result is not None: + return "{}.99.99".format(version_values[result]) + + return None + +class UpdateDialog(MessageBoxBase): + progress_updated = pyqtSignal(int, str) + + def __init__(self, title="Update", initial_status="Checking for updates..."): + super().__init__(_default_gui_handler) + + self.titleLabel = SubtitleLabel(title, self.widget) + self.statusLabel = BodyLabel(initial_status, self.widget) + self.statusLabel.setWordWrap(True) + + self.progressBar = ProgressBar(self.widget) + self.progressBar.setRange(0, 100) + self.progressBar.setValue(0) + + self.viewLayout.addWidget(self.titleLabel) + self.viewLayout.addWidget(self.statusLabel) + self.viewLayout.addWidget(self.progressBar) + + self.widget.setMinimumWidth(600) + + self.cancelButton.setVisible(False) + self.yesButton.setVisible(False) + + self.progress_updated.connect(self._update_progress_safe) + + @pyqtSlot(int, str) + def _update_progress_safe(self, value, status_text): + self.progressBar.setValue(value) + if status_text: + self.statusLabel.setText(status_text) + QCoreApplication.processEvents() + + def update_progress(self, value, status_text=""): + self.progress_updated.emit(value, status_text) + + def set_status(self, status_text): + self.update_progress(self.progressBar.value(), status_text) + + def show_buttons(self, show_ok=False, show_cancel=False): + self.yesButton.setVisible(show_ok) + self.cancelButton.setVisible(show_cancel) + + def configure_buttons(self, ok_text="OK", cancel_text="Cancel"): + self.yesButton.setText(ok_text) + self.cancelButton.setText(cancel_text) + +def show_update_dialog(title="Update", initial_status="Checking for updates..."): + dialog = UpdateDialog(title, initial_status) + return dialog \ No newline at end of file diff --git a/Scripts/datasets/config_tooltips.py b/Scripts/datasets/config_tooltips.py new file mode 100644 index 00000000..c0fe3b89 --- /dev/null +++ b/Scripts/datasets/config_tooltips.py @@ -0,0 +1,47 @@ +from typing import Dict, Callable + +from Scripts.value_formatters import format_value, get_value_type + + +def get_tooltip(key_path, value, original_value = None, context = None): + context = context or {} + + if key_path in TOOLTIP_GENERATORS: + generator = TOOLTIP_GENERATORS[key_path] + return generator(key_path, value, original_value, context) + + path_parts = key_path.split(".") + for i in range(len(path_parts), 0, -1): + parent_path = ".".join(path_parts[:i]) + ".*" + if parent_path in TOOLTIP_GENERATORS: + generator = TOOLTIP_GENERATORS[parent_path] + return generator(key_path, value, original_value, context) + + return _default_tooltip(key_path, value, original_value, context) + +def _default_tooltip(key_path, value, original_value, context): + tooltip = f"{key_path}

" + + if original_value is not None and original_value != value: + tooltip += f"Original: {format_value(original_value)}
" + original_type = get_value_type(original_value) + if original_type: + tooltip += f"Type: {original_type}
" + tooltip += f"Modified: {format_value(value)}
" + modified_type = get_value_type(value) + if modified_type: + tooltip += f"Type: {modified_type}
" + tooltip += "
" + else: + tooltip += f"Value: {format_value(value)}
" + value_type = get_value_type(value) + if value_type: + tooltip += f"Type: {value_type}
" + tooltip += "
" + + return tooltip + +TOOLTIP_GENERATORS: Dict[str, Callable] = {} + +def _register_tooltip(path, generator): + TOOLTIP_GENERATORS[path] = generator \ No newline at end of file diff --git a/Scripts/datasets/os_data.py b/Scripts/datasets/os_data.py index 37d174b3..a75bdb2d 100644 --- a/Scripts/datasets/os_data.py +++ b/Scripts/datasets/os_data.py @@ -1,3 +1,9 @@ +from Scripts.settings import Settings + +settings = Settings() + +INCLUDE_BETA = settings.get_include_beta_versions() + class macOSVersionInfo: def __init__(self, name, macos_version, release_status = "final"): self.name = name @@ -17,7 +23,7 @@ def __init__(self, name, macos_version, release_status = "final"): macOSVersionInfo("Tahoe", "26") ] -def get_latest_darwin_version(include_beta=True): +def get_latest_darwin_version(include_beta=INCLUDE_BETA): for macos_version in macos_versions[::-1]: if include_beta: return "{}.{}.{}".format(macos_version.darwin_version, 99, 99) @@ -32,4 +38,4 @@ def get_macos_name_by_darwin(darwin_version): for data in macos_versions: if data.darwin_version == int(darwin_version[:2]): return "macOS {} {}{}".format(data.name, data.macos_version, "" if data.release_status == "final" else " (Beta)") - return None + return None \ No newline at end of file diff --git a/Scripts/dsdt.py b/Scripts/dsdt.py index 9572b710..9855751e 100644 --- a/Scripts/dsdt.py +++ b/Scripts/dsdt.py @@ -7,13 +7,11 @@ from Scripts import utils class DSDT: - def __init__(self, **kwargs): - #self.dl = downloader.Downloader() - self.github = github.Github() - self.fetcher = resource_fetcher.ResourceFetcher() - self.r = run.Run() - #self.u = utils.Utils("SSDT Time") - self.u = utils.Utils() + def __init__(self, utils_instance=None, github_instance=None, resource_fetcher_instance=None, run_instance=None): + self.u = utils_instance if utils_instance else utils.Utils() + self.github = github_instance if github_instance else github.Github() + self.fetcher = resource_fetcher_instance if resource_fetcher_instance else resource_fetcher.ResourceFetcher() + self.r = run_instance if run_instance else run.Run() self.iasl_url_macOS = "https://raw.githubusercontent.com/acidanthera/MaciASL/master/Dist/iasl-stable" self.iasl_url_macOS_legacy = "https://raw.githubusercontent.com/acidanthera/MaciASL/master/Dist/iasl-legacy" self.iasl_url_linux = "https://raw.githubusercontent.com/corpnewt/linux_iasl/main/iasl.zip" @@ -315,10 +313,7 @@ def check_iasl(self, legacy=False, try_downloading=True): return self.check_iasl(legacy=legacy,try_downloading=False) def _download_and_extract(self, temp, url): - self.u.head("Gathering Files") - print("") - print("Please wait for download iasl...") - print("") + self.u.log_message("[DSDT] Downloading iasl...", level="INFO") ztemp = tempfile.mkdtemp(dir=temp) zfile = os.path.basename(url) #print("Downloading {}".format(os.path.basename(url))) diff --git a/Scripts/gathering_files.py b/Scripts/gathering_files.py index 7ad11d19..f71afda0 100644 --- a/Scripts/gathering_files.py +++ b/Scripts/gathering_files.py @@ -1,3 +1,4 @@ +from Scripts.custom_dialogs import show_info from Scripts import github from Scripts import kext_maestro from Scripts import integrity_checker @@ -11,12 +12,12 @@ os_name = platform.system() class gatheringFiles: - def __init__(self): - self.utils = utils.Utils() - self.github = github.Github() - self.kext = kext_maestro.KextMaestro() - self.fetcher = resource_fetcher.ResourceFetcher() - self.integrity_checker = integrity_checker.IntegrityChecker() + def __init__(self, utils_instance=None, github_instance=None, kext_maestro_instance=None, integrity_checker_instance=None, resource_fetcher_instance=None): + self.utils = utils_instance if utils_instance else utils.Utils() + self.github = github_instance if github_instance else github.Github() + self.kext = kext_maestro_instance if kext_maestro_instance else kext_maestro.KextMaestro() + self.fetcher = resource_fetcher_instance if resource_fetcher_instance else resource_fetcher.ResourceFetcher() + self.integrity_checker = integrity_checker_instance if integrity_checker_instance else integrity_checker.IntegrityChecker() self.dortania_builds_url = "https://raw.githubusercontent.com/dortania/build-repo/builds/latest.json" self.ocbinarydata_url = "https://github.com/acidanthera/OcBinaryData/archive/refs/heads/master.zip" self.amd_vanilla_patches_url = "https://raw.githubusercontent.com/AMD-OSX/AMD_Vanilla/beta/patches.plist" @@ -85,6 +86,7 @@ def add_product_to_download_database(products): def move_bootloader_kexts_to_product_directory(self, product_name): if not os.path.exists(self.temporary_dir): + self.utils.log_message("[GATHERING FILES] The directory {} does not exist.".format(self.temporary_dir), level="ERROR", to_build_log=True) raise FileNotFoundError("The directory {} does not exist.".format(self.temporary_dir)) temp_product_dir = os.path.join(self.temporary_dir, product_name) @@ -139,9 +141,7 @@ def move_bootloader_kexts_to_product_directory(self, product_name): return True def gather_bootloader_kexts(self, kexts, macos_version): - self.utils.head("Gathering Files") - print("") - print("Please wait for download OpenCorePkg, kexts and macserial...") + self.utils.log_message("[GATHERING FILES] Please wait for download OpenCorePkg, kexts and macserial...", level="INFO", to_build_log=True) download_history = self.utils.read_file(self.download_history_file) if not isinstance(download_history, list): @@ -187,8 +187,7 @@ def gather_bootloader_kexts(self, kexts, macos_version): product_download_index = self.get_product_index(download_database, product.github_repo.get("repo")) if product_download_index is None: - print("\n") - print("Could not find download URL for {}.".format(product_name)) + self.utils.log_message("[GATHERING FILES] Could not find download URL for {}.".format(product_name), level="WARNING", to_build_log=True) continue product_info = download_database[product_download_index] @@ -210,20 +209,14 @@ def gather_bootloader_kexts(self, kexts, macos_version): folder_is_valid, _ = self.integrity_checker.verify_folder_integrity(asset_dir, manifest_path) if is_latest_id and folder_is_valid: - print(f"\nLatest version of {product_name} already downloaded.") + self.utils.log_message("[GATHERING FILES] Latest version of {} already downloaded.".format(product_name), level="INFO", to_build_log=True) continue - print("") - print("Updating" if product_history_index is not None else "Please wait for download", end=" ") - print("{}...".format(product_name)) + self.utils.log_message("[GATHERING FILES] Updating {}...".format(product_name), level="INFO", to_build_log=True) if product_download_url: - print("from {}".format(product_download_url)) - print("") + self.utils.log_message("[GATHERING FILES] Downloading from {}".format(product_download_url), level="INFO", to_build_log=True) else: - print("") - print("Could not find download URL for {}.".format(product_name)) - print("") - self.utils.request_input() + self.utils.log_message("[GATHERING FILES] Could not find download URL for {}.".format(product_name), level="ERROR", to_build_log=True) shutil.rmtree(self.temporary_dir, ignore_errors=True) return False @@ -231,9 +224,10 @@ def gather_bootloader_kexts(self, kexts, macos_version): if not self.fetcher.download_and_save_file(product_download_url, zip_path, sha256_hash): folder_is_valid, _ = self.integrity_checker.verify_folder_integrity(asset_dir, manifest_path) if product_history_index is not None and folder_is_valid: - print("Using previously downloaded version of {}.".format(product_name)) + self.utils.log_message("[GATHERING FILES] Using previously downloaded version of {}.".format(product_name), level="INFO", to_build_log=True) continue else: + self.utils.log_message("[GATHERING FILES] Could not download {} at this time. Please try again later.".format(product_name), level="ERROR", to_build_log=True) raise Exception("Could not download {} at this time. Please try again later.".format(product_name)) self.utils.extract_zip_file(zip_path) @@ -250,17 +244,12 @@ def gather_bootloader_kexts(self, kexts, macos_version): if "OpenCore" in product_name: oc_binary_data_zip_path = os.path.join(self.temporary_dir, "OcBinaryData.zip") - print("") - print("Please wait for download OcBinaryData...") - print("from {}".format(self.ocbinarydata_url)) - print("") + self.utils.log_message("[GATHERING FILES] Please wait for download OcBinaryData...", level="INFO", to_build_log=True) + self.utils.log_message("[GATHERING FILES] Downloading from {}".format(self.ocbinarydata_url), level="INFO", to_build_log=True) self.fetcher.download_and_save_file(self.ocbinarydata_url, oc_binary_data_zip_path) if not os.path.exists(oc_binary_data_zip_path): - print("") - print("Could not download OcBinaryData at this time.") - print("Please try again later.\n") - self.utils.request_input() + self.utils.log_message("[GATHERING FILES] Could not download OcBinaryData at this time. Please try again later.", level="ERROR", to_build_log=True) shutil.rmtree(self.temporary_dir, ignore_errors=True) return False @@ -278,14 +267,9 @@ def get_kernel_patches(self, patches_name, patches_url): response = self.fetcher.fetch_and_parse_content(patches_url, "plist") return response["Kernel"]["Patch"] - except: - print("") - print("Unable to download {} at this time".format(patches_name)) - print("from " + patches_url) - print("") - print("Please try again later or apply them manually.") - print("") - self.utils.request_input() + except: + self.utils.log_message("[GATHERING FILES] Unable to download {} at this time".format(patches_name), level="WARNING", to_build_log=True) + show_info("Download Failed", "Unable to download {} at this time. Please try again later or apply them manually.".format(patches_name)) return [] def _update_download_history(self, download_history, product_name, product_id, product_url, sha256_hash): @@ -310,7 +294,7 @@ def gather_hardware_sniffer(self): if os_name != "Windows": return - self.utils.head("Gathering Hardware Sniffer") + self.utils.log_message("[GATHERING FILES] Gathering Hardware Sniffer...", level="INFO") PRODUCT_NAME = "Hardware-Sniffer-CLI.exe" REPO_OWNER = "lzhoang2801" @@ -333,11 +317,7 @@ def gather_hardware_sniffer(self): break if not all([product_id, product_download_url, sha256_hash]): - print("") - print("Could not find release information for {}.".format(PRODUCT_NAME)) - print("Please try again later.") - print("") - self.utils.request_input() + show_info("Release Information Not Found", "Could not find release information for {}. Please try again later.".format(PRODUCT_NAME)) raise Exception("Could not find release information for {}.".format(PRODUCT_NAME)) download_history = self.utils.read_file(self.download_history_file) @@ -356,22 +336,14 @@ def gather_hardware_sniffer(self): file_is_valid = (sha256_hash == local_hash) if is_latest_id and file_is_valid: - print("") - print("Latest version of {} already downloaded.".format(PRODUCT_NAME)) + self.utils.log_message("[GATHERING FILES] Latest version of {} already downloaded.".format(PRODUCT_NAME), level="INFO") return destination_path - print("") - print("Updating" if product_history_index is not None else "Please wait for download", end=" ") - print("{}...".format(PRODUCT_NAME)) - print("") - print("from {}".format(product_download_url)) - print("") + self.utils.log_message("[GATHERING FILES] {} {}...".format("Updating" if product_history_index is not None else "Please wait for download", PRODUCT_NAME), level="INFO") if not self.fetcher.download_and_save_file(product_download_url, destination_path, sha256_hash): - manual_download_url = f"https://github.com/{REPO_OWNER}/{REPO_NAME}/releases/latest" - print("Go to {} to download {} manually.".format(manual_download_url, PRODUCT_NAME)) - print("") - self.utils.request_input() + manual_download_url = "https://github.com/{}/{}/releases/latest".format(REPO_OWNER, REPO_NAME) + show_info("Download Failed", "Go to {} to download {} manually.".format(manual_download_url, PRODUCT_NAME)) raise Exception("Failed to download {}.".format(PRODUCT_NAME)) self._update_download_history(download_history, PRODUCT_NAME, product_id, product_download_url, sha256_hash) diff --git a/Scripts/github.py b/Scripts/github.py index f0a03cc8..80fef9bb 100644 --- a/Scripts/github.py +++ b/Scripts/github.py @@ -4,9 +4,9 @@ import json class Github: - def __init__(self): - self.utils = utils.Utils() - self.fetcher = resource_fetcher.ResourceFetcher() + def __init__(self, utils_instance=None, resource_fetcher_instance=None): + self.utils = utils_instance if utils_instance else utils.Utils() + self.fetcher = resource_fetcher_instance if resource_fetcher_instance else resource_fetcher.ResourceFetcher() def extract_payload(self, response): for line in response.splitlines(): diff --git a/Scripts/hardware_customizer.py b/Scripts/hardware_customizer.py index 516f3965..595a8009 100644 --- a/Scripts/hardware_customizer.py +++ b/Scripts/hardware_customizer.py @@ -1,12 +1,38 @@ from Scripts.datasets import os_data from Scripts.datasets import pci_data -from Scripts import compatibility_checker +from Scripts.custom_dialogs import show_confirmation, show_info, show_options_dialog from Scripts import utils class HardwareCustomizer: - def __init__(self): - self.compatibility_checker = compatibility_checker.CompatibilityChecker() - self.utils = utils.Utils() + def __init__(self, utils_instance=None): + self.utils = utils_instance if utils_instance else utils.Utils() + + def show_macos_compatibility(self, device_compatibility): + if not device_compatibility: + return "Unchecked" + + if not device_compatibility[0]: + return "Unsupported" + + max_compatibility = self.utils.parse_darwin_version(device_compatibility[0])[0] + min_compatibility = self.utils.parse_darwin_version(device_compatibility[-1])[0] + max_version = self.utils.parse_darwin_version(os_data.get_latest_darwin_version())[0] + min_version = self.utils.parse_darwin_version(os_data.get_lowest_darwin_version())[0] + + if max_compatibility == min_version: + return "Maximum support up to {}".format( + os_data.get_macos_name_by_darwin(device_compatibility[-1]) + ) + + if min_version < min_compatibility or max_compatibility < max_version: + return "{} to {}".format( + os_data.get_macos_name_by_darwin(device_compatibility[-1]), + os_data.get_macos_name_by_darwin(device_compatibility[0]) + ) + + return "Up to {}".format( + os_data.get_macos_name_by_darwin(device_compatibility[0]) + ) def hardware_customization(self, hardware_report, macos_version): self.hardware_report = hardware_report @@ -16,7 +42,7 @@ def hardware_customization(self, hardware_report, macos_version): self.selected_devices = {} needs_oclp = False - self.utils.head("Hardware Customization") + self.utils.log_message("[HARDWARE CUSTOMIZATION] Starting hardware customization", level="INFO") for device_type, devices in self.hardware_report.items(): if not device_type in ("BIOS", "GPU", "Sound", "Biometric", "Network", "Storage Controllers", "Bluetooth", "SD Controller"): @@ -27,24 +53,20 @@ def hardware_customization(self, hardware_report, macos_version): if device_type == "BIOS": self.customized_hardware[device_type] = devices.copy() + if devices.get("Firmware Type") != "UEFI": - print("\n*** BIOS Firmware Type is not UEFI") - print("") - print("Do you want to build the EFI for UEFI?") - print("If yes, please make sure to update your BIOS and enable UEFI Boot Mode in your BIOS settings.") - print("You can still proceed with Legacy if you prefer.") - print("") - - while True: - answer = self.utils.request_input("Build EFI for UEFI? (Yes/no): ").strip().lower() - if answer == "yes": - self.customized_hardware[device_type]["Firmware Type"] = "UEFI" - break - elif answer == "no": - self.customized_hardware[device_type]["Firmware Type"] = "Legacy" - break - else: - print("\033[91mInvalid selection, please try again.\033[0m\n\n") + content = ( + "Would you like to build the EFI for UEFI?
" + "If yes, please make sure to update your BIOS and enable UEFI Boot Mode in your BIOS settings.
" + "You can still proceed with Legacy if you prefer." + ) + if show_confirmation("BIOS Firmware Type is not UEFI", content): + self.utils.log_message("[HARDWARE CUSTOMIZATION] BIOS Firmware Type is not UEFI, building EFI for UEFI", level="INFO") + self.customized_hardware[device_type]["Firmware Type"] = "UEFI" + else: + self.utils.log_message("[HARDWARE CUSTOMIZATION] BIOS Firmware Type is not UEFI, building EFI for Legacy", level="INFO") + self.customized_hardware[device_type]["Firmware Type"] = "Legacy" + continue for device_name in devices: @@ -72,21 +94,27 @@ def hardware_customization(self, hardware_report, macos_version): self._handle_device_selection(device_type if device_type != "Network" else "WiFi") if self.selected_devices: - self.utils.head("Device Selection Summary") - print("") - print("Selected devices:") - print("") - print("Type Device Device ID") - print("------------------------------------------------------------------") + content = "The following devices have been selected for your configuration:
" + content += "" + content += "" + content += "" + content += "" + content += "" + content += "" + for device_type, device_dict in self.selected_devices.items(): for device_name, device_props in device_dict.items(): device_id = device_props.get("Device ID", "Unknown") - print("{:<13} {:<42} {}".format(device_type, device_name[:38], device_id)) - print("") - print("All other devices of the same type have been disabled.") - print("") - self.utils.request_input() - + content += "" + content += "".format(device_type) + content += "".format(device_name) + content += "".format(device_id) + content += "" + + content += "
CategoryDevice NameDevice ID
{}{}{}
" + content += "

Note: Unselected devices in these categories have been disabled.

" + show_info("Hardware Configuration Summary", content) + return self.customized_hardware, self.disabled_devices, needs_oclp def _get_device_combinations(self, device_indices): @@ -114,10 +142,12 @@ def _handle_device_selection(self, device_type): devices = self._get_compatible_devices(device_type) device_groups = None + title = "Multiple {} Devices Detected".format(device_type) + content = [] + if len(devices) > 1: - print("\n*** Multiple {} Devices Detected".format(device_type)) if device_type == "WiFi" or device_type == "Bluetooth": - print(f"macOS works best with only one {device_type} device enabled.") + content.append("macOS works best with only one {} device enabled.
".format(device_type)) elif device_type == "GPU": _apu_index = None _navi_22_indices = set() @@ -148,7 +178,7 @@ def _handle_device_selection(self, device_type): _other_indices.add(index) if _apu_index or _navi_22_indices: - print("Multiple active GPUs can cause kext conflicts in macOS.") + content.append("Multiple active GPUs can cause kext conflicts in macOS.") device_groups = [] if _apu_index: @@ -158,7 +188,7 @@ def _handle_device_selection(self, device_type): if _navi_indices or _intel_gpu_indices or _other_indices: device_groups.append(_navi_indices | _intel_gpu_indices | _other_indices) - selected_devices = self._select_device(device_type, devices, device_groups) + selected_devices = self._select_device(device_type, devices, device_groups, title, content) if selected_devices: for selected_device in selected_devices: if not device_type in self.selected_devices: @@ -185,14 +215,15 @@ def _get_compatible_devices(self, device_type): return compatible_devices - def _select_device(self, device_type, devices, device_groups=None): - print("") + def _select_device(self, device_type, devices, device_groups=None, title=None, content=None): + self.utils.log_message("[HARDWARE CUSTOMIZATION] Starting device selection for {}".format(device_type), level="INFO") if device_groups: - print("Please select a {} combination configuration:".format(device_type)) + content.append("Please select a {} combination configuration:".format(device_type)) else: - print("Please select which {} device you want to use:".format(device_type)) - print("") - + content.append("Please select which {} device you want to use:".format(device_type)) + + options = [] + if device_groups: valid_combinations = [] @@ -230,67 +261,48 @@ def _select_device(self, device_type, devices, device_groups=None): valid_combinations.sort(key=lambda x: (len(x[0]), x[2][0])) - for idx, (group_devices, _, group_compatibility) in enumerate(valid_combinations, start=1): - print("{}. {}".format(idx, " + ".join(group_devices))) + for group_devices, group_indices, group_compatibility in valid_combinations: + option = "{}".format(" + ".join(group_devices)) if group_compatibility: - print(" Compatibility: {}".format(self.compatibility_checker.show_macos_compatibility(group_compatibility))) + option += "
Compatibility: {}".format(self.show_macos_compatibility(group_compatibility)) if len(group_devices) == 1: device_props = devices[group_devices[0]] if device_props.get("OCLP Compatibility"): - oclp_compatibility = device_props.get("OCLP Compatibility") - if self.utils.parse_darwin_version(oclp_compatibility[0]) > self.utils.parse_darwin_version(group_compatibility[0]): - print(" OCLP Compatibility: {}".format(self.compatibility_checker.show_macos_compatibility((oclp_compatibility[0], os_data.get_lowest_darwin_version())))) - print("") - - while True: - choice = self.utils.request_input(f"Select a {device_type} combination (1-{len(valid_combinations)}): ") - - try: - choice_num = int(choice) - if 1 <= choice_num <= len(valid_combinations): - selected_devices, _, _ = valid_combinations[choice_num - 1] - - for device in devices: - if device not in selected_devices: - self._disable_device(device_type, device, devices[device]) - - return selected_devices - else: - print("Invalid option. Please try again.") - except ValueError: - print("Please enter a valid number.") + option += "
OCLP Compatibility: {}".format(self.show_macos_compatibility((device_props.get("OCLP Compatibility")[0], os_data.get_lowest_darwin_version()))) + options.append(option) else: - for index, device_name in enumerate(devices, start=1): - device_props = devices[device_name] + for device_name, device_props in devices.items(): compatibility = device_props.get("Compatibility") - print("{}. {}".format(index, device_name)) - print(" Device ID: {}".format(device_props.get("Device ID", "Unknown"))) - print(" Compatibility: {}".format(self.compatibility_checker.show_macos_compatibility(compatibility))) + option = "{}".format(device_name) + option += "
Device ID: {}".format(device_props.get("Device ID", "Unknown")) + option += "
Compatibility: {}".format(self.show_macos_compatibility(compatibility)) if device_props.get("OCLP Compatibility"): oclp_compatibility = device_props.get("OCLP Compatibility") if self.utils.parse_darwin_version(oclp_compatibility[0]) > self.utils.parse_darwin_version(compatibility[0]): - print(" OCLP Compatibility: {}".format(self.compatibility_checker.show_macos_compatibility((oclp_compatibility[0], os_data.get_lowest_darwin_version())))) - print() + option += "
OCLP Compatibility: {}".format(self.show_macos_compatibility((oclp_compatibility[0], os_data.get_lowest_darwin_version()))) + options.append(option) + + self.utils.log_message("[HARDWARE CUSTOMIZATION] Options: {}".format(", ".join(option.split("
")[0].replace("", "").replace("", "").strip() for option in options)), level="INFO") - while True: - choice = self.utils.request_input(f"Select a {device_type} device (1-{len(devices)}): ") - - try: - choice_num = int(choice) - if 1 <= choice_num <= len(devices): - selected_device = list(devices)[choice_num - 1] - - for device in devices: - if device != selected_device: - self._disable_device(device_type, device, devices[device]) - - return [selected_device] - else: - print("Invalid option. Please try again.") - except ValueError: - print("Please enter a valid number.") + while True: + choice_num = show_options_dialog(title, "
".join(content), options, default_index=len(options) - 1) + + if choice_num is None: + continue + + if device_groups: + selected_devices, _, _ = valid_combinations[choice_num] + else: + selected_devices = [list(devices)[choice_num]] + + for device in devices: + if device not in selected_devices: + self._disable_device(device_type, device, devices[device]) + + self.utils.log_message("[HARDWARE CUSTOMIZATION] Selected devices: {}".format(", ".join(selected_devices)), level="INFO") + return selected_devices def _disable_device(self, device_type, device_name, device_props): if device_type == "WiFi": diff --git a/Scripts/integrity_checker.py b/Scripts/integrity_checker.py index 2ec8650e..9367f348 100644 --- a/Scripts/integrity_checker.py +++ b/Scripts/integrity_checker.py @@ -4,8 +4,8 @@ from Scripts import utils class IntegrityChecker: - def __init__(self): - self.utils = utils.Utils() + def __init__(self, utils_instance=None): + self.utils = utils_instance if utils_instance else utils.Utils() def get_sha256(self, file_path, block_size=65536): if not os.path.exists(file_path) or os.path.isdir(file_path): @@ -17,7 +17,7 @@ def get_sha256(self, file_path, block_size=65536): sha256.update(block) return sha256.hexdigest() - def generate_folder_manifest(self, folder_path, manifest_path=None): + def generate_folder_manifest(self, folder_path, manifest_path=None, save_manifest=True): if not os.path.isdir(folder_path): return None @@ -26,8 +26,15 @@ def generate_folder_manifest(self, folder_path, manifest_path=None): manifest_data = {} for root, _, files in os.walk(folder_path): + if '.git' in root or "__pycache__" in root: + continue + for name in files: + if '.git' in name or ".pyc" in name or ".md" in name or "LICENSE" in name: + continue + file_path = os.path.join(root, name) + relative_path = os.path.relpath(file_path, folder_path).replace('\\', '/') if relative_path == os.path.basename(manifest_path): @@ -35,7 +42,8 @@ def generate_folder_manifest(self, folder_path, manifest_path=None): manifest_data[relative_path] = self.get_sha256(file_path) - self.utils.write_file(manifest_path, manifest_data) + if save_manifest: + self.utils.write_file(manifest_path, manifest_data) return manifest_data def verify_folder_integrity(self, folder_path, manifest_path=None): @@ -83,4 +91,4 @@ def verify_folder_integrity(self, folder_path, manifest_path=None): is_valid = not any(issues.values()) - return is_valid, issues + return is_valid, issues \ No newline at end of file diff --git a/Scripts/kext_maestro.py b/Scripts/kext_maestro.py index e23de4fc..2ccafcf9 100644 --- a/Scripts/kext_maestro.py +++ b/Scripts/kext_maestro.py @@ -6,6 +6,7 @@ from Scripts import utils import os import shutil +import random try: long @@ -14,9 +15,11 @@ long = int unicode = str +from Scripts.custom_dialogs import show_options_dialog, show_info, show_confirmation, show_checklist_dialog + class KextMaestro: - def __init__(self): - self.utils = utils.Utils() + def __init__(self, utils_instance=None): + self.utils = utils_instance if utils_instance else utils.Utils() self.matching_keys = [ "IOPCIMatch", "IONameMatch", @@ -77,6 +80,52 @@ def is_intel_hedt_cpu(self, processor_name, cpu_codename): return False + def _select_audio_codec_layout(self, hardware_report, default_layout_id=None): + codec_id = None + audio_controller_properties = None + + for codec_properties in hardware_report.get("Sound", {}).values(): + if codec_properties.get("Device ID") in codec_layouts.data: + codec_id = codec_properties.get("Device ID") + + if codec_properties.get("Controller Device ID"): + for device_name, device_properties in hardware_report.get("System Devices", {}).items(): + if device_properties.get("Device ID") == codec_properties.get("Controller Device ID"): + audio_controller_properties = device_properties + break + break + + available_layouts = codec_layouts.data.get(codec_id) + + if not available_layouts: + return None, None + + options = [] + default_index = 0 + + if default_layout_id is None: + recommended_authors = ("Mirone", "InsanelyDeepak", "Toleda", "DalianSky") + recommended_layouts = [layout for layout in available_layouts if self.utils.contains_any(recommended_authors, layout.comment)] + default_layout_id = random.choice(recommended_layouts or available_layouts).id + + for i, layout in enumerate(available_layouts): + options.append("{} - {}".format(layout.id, layout.comment)) + if layout.id == default_layout_id: + default_index = i + + while True: + content = "For best audio quality, please try multiple layouts to determine which works best with your hardware in post-install." + + selected_index = show_options_dialog( + title="Choosing Codec Layout ID", + content=content, + options=options, + default_index=default_index + ) + + if selected_index is not None: + return available_layouts[selected_index].id, audio_controller_properties + def check_kext(self, index, target_darwin_version, allow_unsupported_kexts=False): kext = self.kexts[index] @@ -96,9 +145,7 @@ def check_kext(self, index, target_darwin_version, allow_unsupported_kexts=False other_kext.checked = False def select_required_kexts(self, hardware_report, macos_version, needs_oclp, acpi_patches): - self.utils.head("Select Required Kernel Extensions") - print("") - print("Checking for required kernel extensions...") + self.utils.log_message("[KEXT MAESTRO] Checking for required kernel extensions...", level="INFO") for kext in self.kexts: kext.checked = kext.required @@ -122,24 +169,26 @@ def select_required_kexts(self, hardware_report, macos_version, needs_oclp, acpi for codec_properties in hardware_report.get("Sound", {}).values(): if codec_properties.get("Device ID") in codec_layouts.data: if self.utils.parse_darwin_version(macos_version) >= self.utils.parse_darwin_version("25.0.0"): - print("\n\033[1;93mNote:\033[0m Since macOS Tahoe 26 DP2, Apple has removed AppleHDA kext and uses the Apple T2 chip for audio management.") - print("To use AppleALC, you must rollback AppleHDA. Alternatively, you can use VoodooHDA.") - print("") - print("1. \033[1mAppleALC\033[0m - Requires AppleHDA rollback with \033[1;93mOpenCore Legacy Patcher\033[0m") - print("2. \033[1mVoodooHDA\033[0m - Lower audio quality, manual injection to /Library/Extensions") - print("") - while True: - kext_option = self.utils.request_input("Select audio kext for your system: ").strip() - if kext_option == "1": - needs_oclp = True - selected_kexts.append("AppleALC") - break - elif kext_option == "2": - break - else: - print("\033[91mInvalid selection, please try again.\033[0m\n\n") + content = ( + "Since macOS Tahoe 26 DP2, Apple has removed AppleHDA and uses the Apple T2 chip for audio management.
" + "Therefore, AppleALC is no longer functional until you rollback AppleHDA." + ) + options = [ + "AppleALC - Requires rollback AppleHDA with OpenCore Legacy Patcher", + "VoodooHDA - Lower audio quality than use AppleHDA, injection kext into /Library/Extensions" + ] + result = show_options_dialog("Audio Kext Selection", content, options, default_index=0) + if result == 0: + needs_oclp = True + selected_kexts.append("AppleALC") else: selected_kexts.append("AppleALC") + + if "AppleALC" in selected_kexts: + audio_layout_id, audio_controller_properties = self._select_audio_codec_layout(hardware_report) + else: + audio_layout_id = None + audio_controller_properties = None if "AMD" in hardware_report.get("CPU").get("Manufacturer") and self.utils.parse_darwin_version(macos_version) >= self.utils.parse_darwin_version("21.4.0") or \ int(hardware_report.get("CPU").get("CPU Count")) > 1 and self.utils.parse_darwin_version(macos_version) >= self.utils.parse_darwin_version("19.0.0"): @@ -166,66 +215,54 @@ def select_required_kexts(self, hardware_report, macos_version, needs_oclp, acpi break if gpu_props.get("Codename") in {"Navi 21", "Navi 23"}: - print("\n*** Found {} is AMD {} GPU.".format(gpu_name, gpu_props.get("Codename"))) - print("") - print("\033[91mImportant: Black Screen Fix\033[0m") - print("If you experience a black screen after verbose mode:") - print(" 1. Use ProperTree to open config.plist") - print(" 2. Navigate to NVRAM -> Add -> 7C436110-AB2A-4BBB-A880-FE41995C9F82 -> boot-args") - print(" 3. Remove \"-v debug=0x100 keepsyms=1\" from boot-args") - print("") + content = ( + "Important: Black Screen Fix
" + "If you experience a black screen after verbose mode:
" + "1. Use ProperTree to open config.plist
" + "2. Navigate to NVRAM -> Add -> 7C436110-AB2A-4BBB-A880-FE41995C9F82 -> boot-args
" + "3. Remove \"-v debug=0x100 keepsyms=1\" from boot-args

" + ).format(gpu_name, gpu_props.get("Codename")) + + options = [ + "NootRX - Uses latest GPU firmware", + "WhateverGreen - Uses original Apple firmware", + ] + if self.utils.parse_darwin_version(macos_version) >= self.utils.parse_darwin_version("25.0.0"): - recommended_option = 1 - recommended_name = "NootRX" - max_option = 3 - print("\033[1;93mNote:\033[0m Since macOS Tahoe 26, WhateverGreen has known connector patching issues for AMD {} GPUs.".format(gpu_props.get("Codename"))) - print("To avoid this, you can use NootRX or choose not to install a GPU kext.") - print("") - print("1. \033[1mNootRX\033[0m - Uses latest GPU firmware") - print("2. \033[1mWhateverGreen\033[0m - Uses original Apple firmware") - print("3. \033[1mDon't use any kext\033[0m") + content += ( + "Since macOS Tahoe 26, WhateverGreen has known connector patching issues for AMD {} GPUs.
" + "To avoid this, you can use NootRX or choose not to install a GPU kext." + ).format(gpu_props.get("Codename")) + options.append("Don't use any kext") + recommended_option = 0 else: - recommended_option = 2 - recommended_name = "WhateverGreen" - max_option = 2 - print("\033[1;93mNote:\033[0m") - print("- AMD {} GPUs have two available kext options:".format(gpu_props.get("Codename"))) - print("- You can try different kexts after installation to find the best one for your system") - print("") - print("1. \033[1mNootRX\033[0m - Uses latest GPU firmware") - print("2. \033[1mWhateverGreen\033[0m - Uses original Apple firmware") - print("") + content += ( + "AMD {} GPUs have two available kext options:
" + "You can try different kexts after installation to find the best one for your system." + ).format(gpu_props.get("Codename")) + recommended_option = 1 if any(other_gpu_props.get("Manufacturer") == "Intel" for other_gpu_props in hardware_report.get("GPU", {}).values()): - print("\033[91mImportant:\033[0m NootRX kext is not compatible with Intel GPUs") - print("Automatically selecting WhateverGreen kext due to Intel GPU compatibility") - print("") - self.utils.request_input("Press Enter to continue...") + show_info("NootRX Kext Warning", "NootRX kext is not compatible with Intel GPUs.
Automatically selecting WhateverGreen kext due to Intel GPU compatibility.") + selected_kexts.append("WhateverGreen") continue - kext_option = self.utils.request_input("Select kext for your AMD {} GPU (default: {}): ".format(gpu_props.get("Codename"), recommended_name)).strip() or str(recommended_option) + result = show_options_dialog("AMD GPU Kext Selection", content, options, default_index=recommended_option) - if kext_option.isdigit() and 0 < int(kext_option) < max_option + 1: - selected_option = int(kext_option) - else: - print("\033[93mInvalid selection, using recommended option: {}\033[0m".format(recommended_option)) - selected_option = recommended_option - - if selected_option == 1: + if result == 0: selected_kexts.append("NootRX") - elif selected_option == 2: + elif result == 1: selected_kexts.append("WhateverGreen") - + continue if self.utils.parse_darwin_version(macos_version) >= self.utils.parse_darwin_version("25.0.0"): - print("\n*** Found {} is AMD {} GPU.".format(gpu_name, gpu_props.get("Codename"))) - print("") - print("\033[1;93mNote:\033[0m Since macOS Tahoe 26, WhateverGreen has known connector patching issues for AMD GPUs.") - print("The current recommendation is to not use WhateverGreen.") - print("However, you can still try adding it to see if it works on your system.") - print("") - self.utils.request_input("Press Enter to continue...") + content = ( + "Since macOS Tahoe 26, WhateverGreen has known connector patching issues for AMD GPUs.
" + "The current recommendation is to not use WhateverGreen.
" + "However, you can still try adding it to see if it works on your system." + ) + show_info("AMD GPU Kext Warning", content) break selected_kexts.append("WhateverGreen") @@ -252,44 +289,35 @@ def select_required_kexts(self, hardware_report, macos_version, needs_oclp, acpi elif device_id in pci_data.BroadcomWiFiIDs[16:18] and self.utils.parse_darwin_version(macos_version) >= self.utils.parse_darwin_version("20.0.0"): selected_kexts.append("AirportBrcmFixup") elif device_id in pci_data.IntelWiFiIDs: - print("\n*** Found {} is Intel WiFi device.".format(network_name)) - print("") - print("\033[1;93mNote:\033[0m Intel WiFi devices have two available kext options:") - print("") - print("1. \033[1mAirportItlwm\033[0m - Uses native WiFi settings menu") - print(" • Provides Handoff, Universal Clipboard, Location Services, Instant Hotspot support") - print(" • Supports enterprise-level security") + airport_itlwm_content = ( + "AirportItlwm - Uses native WiFi settings menu
" + "• Provides Handoff, Universal Clipboard, Location Services, Instant Hotspot support
" + "• Supports enterprise-level security
" + ) if self.utils.parse_darwin_version(macos_version) >= self.utils.parse_darwin_version("24.0.0"): - print(" • \033[91mSince macOS Sequoia 15\033[0m: Can work with OCLP root patch but may cause issues") + airport_itlwm_content += "• Since macOS Sequoia 15: Can work with OCLP root patch but may cause issues" elif self.utils.parse_darwin_version(macos_version) >= self.utils.parse_darwin_version("23.0.0"): - print(" • \033[91mOn macOS Sonoma 14\033[0m: iServices won't work unless using OCLP root patch") - - print("") - print("2. \033[1mitlwm\033[0m - More stable overall") - print(" • Works with HeliPort app instead of native WiFi settings menu") - print(" • No Apple Continuity features and enterprise-level security") - print(" • Can connect to Hidden Networks") - print("") + airport_itlwm_content += "• On macOS Sonoma 14: iServices won't work unless using OCLP root patch" + + itlwm_content = ( + "itlwm - More stable overall
" + "• Works with HeliPort app instead of native WiFi settings menu
" + "• No Apple Continuity features and enterprise-level security
" + "• Can connect to Hidden Networks" + ) - recommended_option = 2 if self.utils.parse_darwin_version(macos_version) >= self.utils.parse_darwin_version("23.0.0") else 1 - recommended_name = "itlwm" if recommended_option == 2 else "AirportItlwm" + recommended_option = 1 if self.utils.parse_darwin_version(macos_version) >= self.utils.parse_darwin_version("23.0.0") else 0 + options = [airport_itlwm_content, itlwm_content] if "Beta" in os_data.get_macos_name_by_darwin(macos_version): - print("\033[91mImportant:\033[0m For macOS Beta versions, only itlwm kext is supported") - print("") - self.utils.request_input("Press Enter to continue...") - selected_option = recommended_option + show_info("Intel WiFi Kext Selection", "For macOS Beta versions, only itlwm kext is supported.") + selected_option = 1 else: - kext_option = self.utils.request_input("Select kext for your Intel WiFi device (default: {}): ".format(recommended_name)).strip() or str(recommended_option) - - if kext_option.isdigit() and 0 < int(kext_option) < 3: - selected_option = int(kext_option) - else: - print("\033[91mInvalid selection, using recommended option: {}\033[0m".format(recommended_option)) - selected_option = recommended_option - - if selected_option == 2: + result = show_options_dialog("Intel WiFi Kext Selection", "Intel WiFi devices have two available kext options:", options, default_index=recommended_option) + selected_option = result if result is not None else recommended_option + + if selected_option == 1: selected_kexts.append("itlwm") else: selected_kexts.append("AirportItlwm") @@ -297,18 +325,12 @@ def select_required_kexts(self, hardware_report, macos_version, needs_oclp, acpi if self.utils.parse_darwin_version(macos_version) >= self.utils.parse_darwin_version("24.0.0"): selected_kexts.append("IOSkywalkFamily") elif self.utils.parse_darwin_version(macos_version) >= self.utils.parse_darwin_version("23.0.0"): - print("") - print("\033[1;93mNote:\033[0m Since macOS Sonoma 14, iServices won't work with AirportItlwm without patches") - print("") - while True: - option = self.utils.request_input("Apply OCLP root patch to fix iServices? (yes/No): ").strip().lower() - if option == "yes": - selected_kexts.append("IOSkywalkFamily") - break - elif option == "no": - break - else: - print("\033[91mInvalid selection, please try again.\033[0m\n\n") + content = ( + "Since macOS Sonoma 14, iServices won't work with AirportItlwm without patches.

" + "Apply OCLP root patch to fix iServices?" + ) + if show_confirmation("OpenCore Legacy Patcher Required", content): + selected_kexts.append("IOSkywalkFamily") elif device_id in pci_data.AtherosWiFiIDs[:8]: selected_kexts.append("corecaptureElCap") if self.utils.parse_darwin_version(macos_version) > self.utils.parse_darwin_version("20.99.99"): @@ -424,7 +446,7 @@ def select_required_kexts(self, hardware_report, macos_version, needs_oclp, acpi for name in selected_kexts: self.check_kext(kext_data.kext_index_by_name.get(name), macos_version, allow_unsupported_kexts) - return needs_oclp + return needs_oclp, audio_layout_id, audio_controller_properties def install_kexts_to_efi(self, macos_version, kexts_directory): for kext in self.kexts: @@ -639,72 +661,65 @@ def verify_kext_compatibility(self, selected_kexts, target_darwin_version): if not incompatible_kexts: return False - while True: - self.utils.head("Kext Compatibility Check") - print("\nIncompatible kexts for the current macOS version ({}):\n".format(target_darwin_version)) - - for index, (kext_name, is_lilu_dependent) in enumerate(incompatible_kexts, start=1): - print("{:2}. {:25}{}".format(index, kext_name, " - Lilu Plugin" if is_lilu_dependent else "")) - - print("\n\033[1;93mNote:\033[0m") - print("- With Lilu plugins, using the \"-lilubetaall\" boot argument will force them to load.") - print("- Forcing unsupported kexts can cause system instability. \033[0;31mProceed with caution.\033[0m") - print("") - - option = self.utils.request_input("Do you want to force load {} on the unsupported macOS version? (yes/No): ".format("these kexts" if len(incompatible_kexts) > 1 else "this kext")) - - if option.lower() == "yes": - return True - elif option.lower() == "no": - return False + content = ( + "List of incompatible kexts for the current macOS version ({}):
" + "
" + "Note:
" + "• With Lilu plugins, using the \"-lilubetaall\" boot argument will force them to load.
" + "• Forcing unsupported kexts can cause system instability. Proceed with caution.

" + "Do you want to force load {} on the unsupported macOS version?" + ).format("these kexts" if len(incompatible_kexts) > 1 else "this kext") + + return show_confirmation("Incompatible Kexts", content, yes_text="Yes", no_text="No") def kext_configuration_menu(self, macos_version): - current_category = None + content = ( + "Select kernel extensions (kexts) for your system.
" + "Grayed-out items are not supported by the current macOS version ({}).

" + "Note:
" + "• When a plugin of a kext is selected, the entire kext will be automatically selected." + ).format(macos_version) + + checklist_items = [] + + for kext in self.kexts: + is_supported = self.utils.parse_darwin_version(kext.min_darwin_version) <= self.utils.parse_darwin_version(macos_version) <= self.utils.parse_darwin_version(kext.max_darwin_version) + + display_text = "{} - {}".format(kext.name, kext.description) + if not is_supported: + display_text += " (Unsupported)" + + checklist_items.append({ + "label": display_text, + "category": kext.category if kext.category else "Uncategorized", + "supported": is_supported + }) + + checked_indices = [i for i, kext in enumerate(self.kexts) if kext.checked] + + selected_indices = show_checklist_dialog("Configure Kernel Extensions", content, checklist_items, checked_indices) + + self.utils.log_message("[KEXT MAESTRO] Selected kexts: {}".format(selected_indices), level="INFO") + if selected_indices is None: + return - while True: - contents = [] - contents.append("") - contents.append("List of available kexts:") - for index, kext in enumerate(self.kexts, start=1): - if kext.category != current_category: - current_category = kext.category - category_header = "Category: {}".format(current_category if current_category else "Uncategorized") - contents.append(f"\n{category_header}\n" + "=" * len(category_header)) - checkbox = "[*]" if kext.checked else "[ ]" - - line = "{} {:2}. {:35} - {:60}".format(checkbox, index, kext.name, kext.description) - if kext.checked: - line = "\033[1;32m{}\033[0m".format(line) - elif not self.utils.parse_darwin_version(kext.min_darwin_version) <= self.utils.parse_darwin_version(macos_version) <= self.utils.parse_darwin_version(kext.max_darwin_version): - line = "\033[90m{}\033[0m".format(line) - contents.append(line) - contents.append("") - contents.append("\033[1;93mNote:\033[0m") - contents.append("- Lines in gray indicate kexts that are not supported by the current macOS version ({}).".format(macos_version)) - contents.append("- When a plugin of a kext is selected, the entire kext will be automatically selected.") - contents.append("- You can select multiple kexts by entering their indices separated by commas (e.g., '1, 2, 3').") - contents.append("") - contents.append("B. Back") - contents.append("Q. Quit") - contents.append("") - content = "\n".join(contents) - - self.utils.adjust_window_size(content) - self.utils.head("Configure Kernel Extensions", resize=False) - print(content) - option = self.utils.request_input("Select your option: ") - if option.lower() == "b": - return - if option.lower() == "q": - self.utils.exit_program() - indices = [int(i.strip()) -1 for i in option.split(",") if i.strip().isdigit()] - - allow_unsupported_kexts = self.verify_kext_compatibility(indices, macos_version) - - for index in indices: - if index >= 0 and index < len(self.kexts): - kext = self.kexts[index] - if kext.checked and not kext.required: - self.uncheck_kext(index) - else: - self.check_kext(index, macos_version, allow_unsupported_kexts) \ No newline at end of file + newly_checked = [i for i in selected_indices if i not in checked_indices] + + allow_unsupported_kexts = self.verify_kext_compatibility(newly_checked, macos_version) + + for i, kext in enumerate(self.kexts): + if i not in selected_indices and kext.checked and not kext.required: + self.uncheck_kext(i) + + for i in selected_indices: + self.check_kext(i, macos_version, allow_unsupported_kexts) \ No newline at end of file diff --git a/Scripts/pages/__init__.py b/Scripts/pages/__init__.py new file mode 100644 index 00000000..a8378312 --- /dev/null +++ b/Scripts/pages/__init__.py @@ -0,0 +1,15 @@ +from .home_page import HomePage +from .select_hardware_report_page import SelectHardwareReportPage +from .compatibility_page import CompatibilityPage +from .configuration_page import ConfigurationPage +from .build_page import BuildPage +from .settings_page import SettingsPage + +__all__ = [ + "HomePage", + "SelectHardwareReportPage", + "CompatibilityPage", + "ConfigurationPage", + "BuildPage", + "SettingsPage", +] diff --git a/Scripts/pages/build_page.py b/Scripts/pages/build_page.py new file mode 100644 index 00000000..52614f20 --- /dev/null +++ b/Scripts/pages/build_page.py @@ -0,0 +1,552 @@ +import platform +import os +import shutil +import threading + +from PyQt6.QtCore import Qt, pyqtSignal +from PyQt6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel +from qfluentwidgets import ( + SubtitleLabel, BodyLabel, CardWidget, TextEdit, + StrongBodyLabel, ProgressBar, PrimaryPushButton, FluentIcon, + ScrollArea +) + +from Scripts.datasets import chipset_data +from Scripts.datasets import kext_data +from Scripts.custom_dialogs import show_confirmation +from Scripts.styles import SPACING, COLORS, RADIUS +from Scripts import ui_utils +from Scripts.widgets.config_editor import ConfigEditor + + +class BuildPage(ScrollArea): + build_progress_signal = pyqtSignal(str, list, int, int, bool) + build_complete_signal = pyqtSignal(bool, object) + + def __init__(self, parent, ui_utils_instance=None): + super().__init__(parent) + self.setObjectName("buildPage") + self.controller = parent + self.scrollWidget = QWidget() + self.expandLayout = QVBoxLayout(self.scrollWidget) + self.build_in_progress = False + self.build_successful = False + self.ui_utils = ui_utils_instance if ui_utils_instance else ui_utils.UIUtils() + + self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) + self.setWidget(self.scrollWidget) + self.setWidgetResizable(True) + self.enableTransparentBackground() + + self._init_ui() + self._connect_signals() + + def _init_ui(self): + self.expandLayout.setContentsMargins(SPACING["xxlarge"], SPACING["xlarge"], SPACING["xxlarge"], SPACING["xlarge"]) + self.expandLayout.setSpacing(SPACING["large"]) + + self.expandLayout.addWidget(self.ui_utils.create_step_indicator(4)) + + header_layout = QVBoxLayout() + header_layout.setSpacing(SPACING["small"]) + title = SubtitleLabel("Build OpenCore EFI") + subtitle = BodyLabel("Build your customized OpenCore EFI ready for installation") + subtitle.setStyleSheet("color: {};".format(COLORS["text_secondary"])) + header_layout.addWidget(title) + header_layout.addWidget(subtitle) + self.expandLayout.addLayout(header_layout) + + self.expandLayout.addSpacing(SPACING["medium"]) + + self.instructions_after_content = QWidget() + self.instructions_after_content_layout = QVBoxLayout(self.instructions_after_content) + self.instructions_after_content_layout.setContentsMargins(0, 0, 0, 0) + self.instructions_after_content_layout.setSpacing(SPACING["medium"]) + + self.instructions_after_build_card = self.ui_utils.custom_card( + card_type="warning", + title="Before Using Your EFI", + body="Please complete these important steps before using the built EFI:", + custom_widget=self.instructions_after_content, + parent=self.scrollWidget + ) + + self.instructions_after_build_card.setVisible(False) + self.expandLayout.addWidget(self.instructions_after_build_card) + + build_control_card = CardWidget(self.scrollWidget) + build_control_card.setBorderRadius(RADIUS["card"]) + build_control_layout = QVBoxLayout(build_control_card) + build_control_layout.setContentsMargins(SPACING["large"], SPACING["large"], SPACING["large"], SPACING["large"]) + build_control_layout.setSpacing(SPACING["medium"]) + + title = StrongBodyLabel("Build Control") + build_control_layout.addWidget(title) + + btn_layout = QHBoxLayout() + btn_layout.setSpacing(SPACING["medium"]) + + self.build_btn = PrimaryPushButton(FluentIcon.DEVELOPER_TOOLS, "Build OpenCore EFI") + self.build_btn.clicked.connect(self.start_build) + btn_layout.addWidget(self.build_btn) + self.controller.build_btn = self.build_btn + + self.open_result_btn = PrimaryPushButton(FluentIcon.FOLDER, "Open Result Folder") + self.open_result_btn.clicked.connect(self.open_result) + self.open_result_btn.setEnabled(False) + btn_layout.addWidget(self.open_result_btn) + self.controller.open_result_btn = self.open_result_btn + + build_control_layout.addLayout(btn_layout) + + self.progress_container = QWidget() + progress_layout = QVBoxLayout(self.progress_container) + progress_layout.setContentsMargins(0, SPACING["small"], 0, 0) + progress_layout.setSpacing(SPACING["medium"]) + + status_row = QHBoxLayout() + status_row.setSpacing(SPACING["medium"]) + + self.status_icon_label = QLabel() + self.status_icon_label.setFixedSize(28, 28) + status_row.addWidget(self.status_icon_label) + + self.progress_label = StrongBodyLabel("Ready to build") + self.progress_label.setStyleSheet("color: {}; font-size: 15px; font-weight: 600;".format(COLORS["text_secondary"])) + status_row.addWidget(self.progress_label) + status_row.addStretch() + + progress_layout.addLayout(status_row) + + self.progress_bar = ProgressBar() + self.progress_bar.setValue(0) + self.progress_bar.setFixedHeight(10) + self.progress_bar.setTextVisible(True) + self.controller.progress_bar = self.progress_bar + progress_layout.addWidget(self.progress_bar) + + self.controller.progress_label = self.progress_label + self.progress_container.setVisible(False) + + self.progress_helper = ui_utils.ProgressStatusHelper( + self.status_icon_label, + self.progress_label, + self.progress_bar, + self.progress_container + ) + + build_control_layout.addWidget(self.progress_container) + self.expandLayout.addWidget(build_control_card) + + log_card = CardWidget(self.scrollWidget) + log_card.setBorderRadius(RADIUS["card"]) + log_card_layout = QVBoxLayout(log_card) + log_card_layout.setContentsMargins(SPACING["large"], SPACING["large"], SPACING["large"], SPACING["large"]) + log_card_layout.setSpacing(SPACING["medium"]) + + log_title = StrongBodyLabel("Build Log") + log_card_layout.addWidget(log_title) + + log_description = BodyLabel("Detailed build process information and status updates") + log_description.setStyleSheet("color: {}; font-size: 13px;".format(COLORS["text_secondary"])) + log_card_layout.addWidget(log_description) + + self.build_log = TextEdit() + self.build_log.setReadOnly(True) + self.build_log.setMinimumHeight(400) + self.build_log.setStyleSheet(f""" + TextEdit {{ + background-color: rgba(0, 0, 0, 0.03); + border: 1px solid rgba(0, 0, 0, 0.08); + border-radius: {RADIUS["small"]}px; + padding: {SPACING["large"]}px; + font-family: "Consolas", "Monaco", "Courier New", monospace; + font-size: 13px; + line-height: 1.7; + }} + """) + self.controller.build_log = self.build_log + log_card_layout.addWidget(self.build_log) + + self.log_card = log_card + self.log_card.setVisible(False) + self.expandLayout.addWidget(log_card) + + self.config_editor = ConfigEditor(self.scrollWidget) + self.config_editor.setVisible(False) + self.expandLayout.addWidget(self.config_editor) + + self.expandLayout.addStretch() + + def _connect_signals(self): + self.build_progress_signal.connect(self._handle_build_progress) + self.build_complete_signal.connect(self._handle_build_complete) + + def _handle_build_progress(self, title, steps, current_step_index, progress, done): + status = "success" if done else "loading" + + if done: + message = "{} complete!".format(title) + else: + step_text = steps[current_step_index] if current_step_index < len(steps) else "Processing" + step_counter = "Step {}/{}".format(current_step_index + 1, len(steps)) + message = "{}: {}...".format(step_counter, step_text) + + if done: + final_progress = 100 + else: + if "Building" in title: + final_progress = 40 + int(progress * 0.6) + else: + final_progress = progress + + if hasattr(self, "progress_helper"): + self.progress_helper.update(status, message, final_progress) + + if done: + self.controller.backend.u.log_message("[BUILD] {} complete!".format(title), "SUCCESS", to_build_log=True) + else: + step_text = steps[current_step_index] if current_step_index < len(steps) else "Processing" + self.controller.backend.u.log_message("[BUILD] Step {}/{}: {}...".format(current_step_index + 1, len(steps), step_text), "INFO", to_build_log=True) + + def start_build(self): + if not self.controller.validate_prerequisites(): + return + + if self.controller.macos_state.needs_oclp: + content = ( + "1. OpenCore Legacy Patcher allows restoring support for dropped GPUs and Broadcom WiFi on newer versions of macOS, and also enables AppleHDA on macOS Tahoe 26.
" + "2. OpenCore Legacy Patcher needs SIP disabled for applying custom kernel patches, which can cause instability, security risks and update issues.
" + "3. OpenCore Legacy Patcher does not officially support the Hackintosh community.

" + "Support for macOS Tahoe 26:
" + "To patch macOS Tahoe 26, you must download OpenCore-Patcher 3.0.0 or newer from my repository: lzhoang2801/OpenCore-Legacy-Patcher.
" + "Official Dortania releases or older patches will NOT work with macOS Tahoe 26." + ).format(error_color=COLORS["error"], info_color="#00BCD4") + if not show_confirmation("OpenCore Legacy Patcher Warning", content): + return + + self.build_in_progress = True + self.build_successful = False + self.build_btn.setEnabled(False) + self.build_btn.setText("Building...") + self.open_result_btn.setEnabled(False) + + self.progress_helper.update("loading", "Preparing to build...", 0) + + self.instructions_after_build_card.setVisible(False) + self.build_log.clear() + self.log_card.setVisible(True) + + thread = threading.Thread(target=self._start_build_thread, daemon=True) + thread.start() + + def _start_build_thread(self): + try: + backend = self.controller.backend + backend.o.gather_bootloader_kexts(backend.k.kexts, self.controller.macos_state.darwin_version) + + self._build_opencore_efi( + self.controller.hardware_state.customized_hardware, + self.controller.hardware_state.disabled_devices, + self.controller.smbios_state.model_name, + self.controller.macos_state.darwin_version, + self.controller.macos_state.needs_oclp + ) + + bios_requirements = self._check_bios_requirements( + self.controller.hardware_state.customized_hardware, + self.controller.hardware_state.customized_hardware + ) + + self.build_complete_signal.emit(True, bios_requirements) + except Exception as e: + self.build_complete_signal.emit(False, None) + + def _check_bios_requirements(self, org_hardware_report, hardware_report): + requirements = [] + + org_firmware_type = org_hardware_report.get("BIOS", {}).get("Firmware Type", "Unknown") + firmware_type = hardware_report.get("BIOS", {}).get("Firmware Type", "Unknown") + if org_firmware_type == "Legacy" and firmware_type == "UEFI": + requirements.append("Enable UEFI mode (disable Legacy/CSM (Compatibility Support Module))") + + secure_boot = hardware_report.get("BIOS", {}).get("Secure Boot", "Unknown") + if secure_boot != "Disabled": + requirements.append("Disable Secure Boot") + + if hardware_report.get("Motherboard", {}).get("Platform") == "Desktop" and hardware_report.get("Motherboard", {}).get("Chipset") in chipset_data.IntelChipsets[112:]: + resizable_bar_enabled = any(gpu_props.get("Resizable BAR", "Disabled") == "Enabled" for gpu_props in hardware_report.get("GPU", {}).values()) + if not resizable_bar_enabled: + requirements.append("Enable Above 4G Decoding") + requirements.append("Disable Resizable BAR/Smart Access Memory") + + return requirements + + def _build_opencore_efi(self, hardware_report, disabled_devices, smbios_model, macos_version, needs_oclp): + steps = [ + "Copying EFI base to results folder", + "Applying ACPI patches", + "Copying kexts and snapshotting to config.plist", + "Generating config.plist", + "Cleaning up unused drivers, resources, and tools" + ] + + title = "Building OpenCore EFI" + current_step = 0 + + progress = int((current_step / len(steps)) * 100) + self.build_progress_signal.emit(title, steps, current_step, progress, False) + current_step += 1 + + backend = self.controller.backend + backend.u.create_folder(backend.result_dir, remove_content=True) + + if not os.path.exists(backend.k.ock_files_dir): + raise Exception("Directory \"{}\" does not exist.".format(backend.k.ock_files_dir)) + + source_efi_dir = os.path.join(backend.k.ock_files_dir, "OpenCorePkg") + shutil.copytree(source_efi_dir, backend.result_dir, dirs_exist_ok=True) + + config_file = os.path.join(backend.result_dir, "EFI", "OC", "config.plist") + config_data = backend.u.read_file(config_file) + + if not config_data: + raise Exception("Error: The file {} does not exist.".format(config_file)) + + progress = int((current_step / len(steps)) * 100) + self.build_progress_signal.emit(title, steps, current_step, progress, False) + current_step += 1 + + config_data["ACPI"]["Add"] = [] + config_data["ACPI"]["Delete"] = [] + config_data["ACPI"]["Patch"] = [] + + acpi_directory = os.path.join(backend.result_dir, "EFI", "OC", "ACPI") + + if backend.ac.ensure_dsdt(): + backend.ac.hardware_report = hardware_report + backend.ac.disabled_devices = disabled_devices + backend.ac.acpi_directory = acpi_directory + backend.ac.smbios_model = smbios_model + backend.ac.lpc_bus_device = backend.ac.get_lpc_name() + + for patch in backend.ac.patches: + if patch.checked: + if patch.name == "BATP": + patch.checked = getattr(backend.ac, patch.function_name)() + backend.k.kexts[kext_data.kext_index_by_name.get("ECEnabler")].checked = patch.checked + continue + + acpi_load = getattr(backend.ac, patch.function_name)() + if not isinstance(acpi_load, dict): + continue + + config_data["ACPI"]["Add"].extend(acpi_load.get("Add", [])) + config_data["ACPI"]["Delete"].extend(acpi_load.get("Delete", [])) + config_data["ACPI"]["Patch"].extend(acpi_load.get("Patch", [])) + + config_data["ACPI"]["Patch"].extend(backend.ac.dsdt_patches) + config_data["ACPI"]["Patch"] = backend.ac.apply_acpi_patches(config_data["ACPI"]["Patch"]) + + progress = int((current_step / len(steps)) * 100) + self.build_progress_signal.emit(title, steps, current_step, progress, False) + current_step += 1 + + kexts_directory = os.path.join(backend.result_dir, "EFI", "OC", "Kexts") + backend.k.install_kexts_to_efi(macos_version, kexts_directory) + config_data["Kernel"]["Add"] = backend.k.load_kexts(hardware_report, macos_version, kexts_directory) + + progress = int((current_step / len(steps)) * 100) + self.build_progress_signal.emit(title, steps, current_step, progress, False) + current_step += 1 + + audio_layout_id = self.controller.hardware_state.audio_layout_id + audio_controller_properties = self.controller.hardware_state.audio_controller_properties + + backend.co.genarate( + hardware_report, + disabled_devices, + smbios_model, + macos_version, + needs_oclp, + backend.k.kexts, + config_data, + audio_layout_id, + audio_controller_properties + ) + + backend.u.write_file(config_file, config_data) + + progress = int((current_step / len(steps)) * 100) + self.build_progress_signal.emit(title, steps, current_step, progress, False) + files_to_remove = [] + + drivers_directory = os.path.join(backend.result_dir, "EFI", "OC", "Drivers") + driver_list = backend.u.find_matching_paths(drivers_directory, extension_filter=".efi") + driver_loaded = [kext.get("Path") for kext in config_data.get("UEFI").get("Drivers")] + for driver_path, type in driver_list: + if not driver_path in driver_loaded: + files_to_remove.append(os.path.join(drivers_directory, driver_path)) + + resources_audio_dir = os.path.join(backend.result_dir, "EFI", "OC", "Resources", "Audio") + if os.path.exists(resources_audio_dir): + files_to_remove.append(resources_audio_dir) + + picker_variant = config_data.get("Misc", {}).get("Boot", {}).get("PickerVariant") + if picker_variant in (None, "Auto"): + picker_variant = "Acidanthera/GoldenGate" + if os.name == "nt": + picker_variant = picker_variant.replace("/", "\\") + + resources_image_dir = os.path.join(backend.result_dir, "EFI", "OC", "Resources", "Image") + available_picker_variants = backend.u.find_matching_paths(resources_image_dir, type_filter="dir") + + for variant_name, variant_type in available_picker_variants: + variant_path = os.path.join(resources_image_dir, variant_name) + if ".icns" in ", ".join(os.listdir(variant_path)): + if picker_variant not in variant_name: + files_to_remove.append(variant_path) + + tools_directory = os.path.join(backend.result_dir, "EFI", "OC", "Tools") + tool_list = backend.u.find_matching_paths(tools_directory, extension_filter=".efi") + tool_loaded = [tool.get("Path") for tool in config_data.get("Misc").get("Tools")] + for tool_path, type in tool_list: + if not tool_path in tool_loaded: + files_to_remove.append(os.path.join(tools_directory, tool_path)) + + if "manifest.json" in os.listdir(backend.result_dir): + files_to_remove.append(os.path.join(backend.result_dir, "manifest.json")) + + for file_path in files_to_remove: + try: + if os.path.isdir(file_path): + shutil.rmtree(file_path) + else: + os.remove(file_path) + except Exception as e: + backend.u.log_message("[BUILD] Failed to remove file {}: {}".format(os.path.basename(file_path), e), level="WARNING", to_build_log=True) + + self.build_progress_signal.emit(title, steps, len(steps) - 1, 100, True) + + def show_post_build_instructions(self, bios_requirements): + while self.instructions_after_content_layout.count(): + item = self.instructions_after_content_layout.takeAt(0) + if item.widget(): + item.widget().deleteLater() + + if bios_requirements: + bios_header = StrongBodyLabel("1. BIOS/UEFI Settings Required:") + bios_header.setStyleSheet("color: {}; font-size: 14px;".format(COLORS["warning_text"])) + self.instructions_after_content_layout.addWidget(bios_header) + + bios_text = "\n".join([" • {}".format(req) for req in bios_requirements]) + bios_label = BodyLabel(bios_text) + bios_label.setWordWrap(True) + bios_label.setStyleSheet("color: #424242; line-height: 1.6;") + self.instructions_after_content_layout.addWidget(bios_label) + + self.instructions_after_content_layout.addSpacing(SPACING["medium"]) + + usb_header = StrongBodyLabel("{}. USB Port Mapping:".format(2 if bios_requirements else 1)) + usb_header.setStyleSheet("color: {}; font-size: 14px;".format(COLORS["warning_text"])) + self.instructions_after_content_layout.addWidget(usb_header) + + path_sep = "\\" if platform.system() == "Windows" else "/" + + usb_mapping_instructions = ( + "1. Use USBToolBox tool to map USB ports
" + "2. Add created UTBMap.kext into the EFI{path_sep}OC{path_sep}Kexts folder
" + "3. Remove UTBDefault.kext from the EFI{path_sep}OC{path_sep}Kexts folder
" + "4. Edit config.plist using ProperTree:
" + " a. Run OC Snapshot (Command/Ctrl + R)
" + " b. Enable XhciPortLimit quirk if you have more than 15 ports per controller
" + " c. Save the file when finished." + ).format(path_sep=path_sep) + + usb_label = BodyLabel(usb_mapping_instructions) + usb_label.setWordWrap(True) + usb_label.setStyleSheet("color: #424242; line-height: 1.6;") + self.instructions_after_content_layout.addWidget(usb_label) + + self.instructions_after_build_card.setVisible(True) + + def _handle_build_complete(self, success, bios_requirements): + self.build_in_progress = False + self.build_successful = success + + if success: + self.log_card.setVisible(False) + self.progress_helper.update("success", "Build completed successfully!", 100) + + self.show_post_build_instructions(bios_requirements) + self._load_configs_after_build() + + self.build_btn.setText("Build OpenCore EFI") + self.build_btn.setEnabled(True) + self.open_result_btn.setEnabled(True) + + success_message = "Your OpenCore EFI has been built successfully!" + if bios_requirements is not None: + success_message += " Review the important instructions below." + + self.controller.update_status(success_message, "success") + else: + self.progress_helper.update("error", "Build OpenCore EFI failed", None) + + self.config_editor.setVisible(False) + + self.build_btn.setText("Retry Build OpenCore EFI") + self.build_btn.setEnabled(True) + self.open_result_btn.setEnabled(False) + + self.controller.update_status("An error occurred during the build. Check the log for details.", "error") + + def open_result(self): + result_dir = self.controller.backend.result_dir + try: + self.controller.backend.u.open_folder(result_dir) + except Exception as e: + self.controller.update_status("Failed to open result folder: {}".format(e), "warning") + + def _load_configs_after_build(self): + backend = self.controller.backend + + source_efi_dir = os.path.join(backend.k.ock_files_dir, "OpenCorePkg") + original_config_file = os.path.join(source_efi_dir, "EFI", "OC", "config.plist") + + if not os.path.exists(original_config_file): + return + + original_config = backend.u.read_file(original_config_file) + if not original_config: + return + + modified_config_file = os.path.join(backend.result_dir, "EFI", "OC", "config.plist") + + if not os.path.exists(modified_config_file): + return + + modified_config = backend.u.read_file(modified_config_file) + if not modified_config: + return + + context = { + "hardware_report": self.controller.hardware_state.hardware_report, + "macos_version": self.controller.macos_state.darwin_version, + "smbios_model": self.controller.smbios_state.model_name, + } + + self.config_editor.load_configs(original_config, modified_config, context) + self.config_editor.setVisible(True) + + def refresh(self): + if not self.build_in_progress: + if self.build_successful: + self.progress_container.setVisible(True) + self.open_result_btn.setEnabled(True) + else: + log_text = self.build_log.toPlainText() + if not log_text or log_text == DEFAULT_LOG_TEXT: + self.progress_container.setVisible(False) + self.log_card.setVisible(False) + self.open_result_btn.setEnabled(False) \ No newline at end of file diff --git a/Scripts/pages/compatibility_page.py b/Scripts/pages/compatibility_page.py new file mode 100644 index 00000000..bc169ff4 --- /dev/null +++ b/Scripts/pages/compatibility_page.py @@ -0,0 +1,557 @@ +from PyQt6.QtCore import Qt +from PyQt6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout +from qfluentwidgets import SubtitleLabel, BodyLabel, ScrollArea, FluentIcon, GroupHeaderCardWidget, CardWidget, StrongBodyLabel + +from Scripts.styles import COLORS, SPACING +from Scripts import ui_utils +from Scripts.datasets import os_data, pci_data + + +class CompatibilityStatusBanner: + def __init__(self, parent=None, ui_utils_instance=None, layout=None): + self.parent = parent + self.ui_utils = ui_utils_instance if ui_utils_instance else ui_utils.UIUtils() + self.layout = layout + self.card = None + self.body_label = None + self.note_label = None + + def _create_card(self, card_type, icon, title, message, note=""): + body_text = message + if note: + body_text += "

{}".format(COLORS["text_secondary"], note) + + if self.card: + if self.layout: + self.layout.removeWidget(self.card) + self.card.setParent(None) + self.card.deleteLater() + + self.card = self.ui_utils.custom_card( + card_type=card_type, + icon=icon, + title=title, + body=body_text, + parent=self.parent + ) + self.card.setVisible(True) + + if self.layout: + self.layout.insertWidget(2, self.card) + + return self.card + + def show_error(self, title, message, note=""): + self._create_card("error", FluentIcon.CLOSE, title, message, note) + + def show_success(self, title, message, note=""): + self._create_card("success", FluentIcon.ACCEPT, title, message, note) + + def setVisible(self, visible): + if self.card: + self.card.setVisible(visible) + +class CompatibilityPage(ScrollArea): + def __init__(self, parent, ui_utils_instance=None): + super().__init__(parent) + self.setObjectName("compatibilityPage") + self.controller = parent + self.scrollWidget = QWidget() + self.expandLayout = QVBoxLayout(self.scrollWidget) + self.ui_utils = ui_utils_instance if ui_utils_instance else ui_utils.UIUtils() + self.contentWidget = None + self.contentLayout = None + self.native_support_label = None + self.ocl_support_label = None + + self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) + self.setWidget(self.scrollWidget) + self.setWidgetResizable(True) + self.enableTransparentBackground() + + self._init_ui() + + def _init_ui(self): + self.expandLayout.setContentsMargins(SPACING["xxlarge"], SPACING["xlarge"], SPACING["xxlarge"], SPACING["xlarge"]) + self.expandLayout.setSpacing(SPACING["large"]) + + self.expandLayout.addWidget(self.ui_utils.create_step_indicator(2)) + + header_container = QWidget() + header_layout = QHBoxLayout(header_container) + header_layout.setContentsMargins(0, 0, 0, 0) + header_layout.setSpacing(SPACING["large"]) + + title_block = QWidget() + title_layout = QVBoxLayout(title_block) + title_layout.setContentsMargins(0, 0, 0, 0) + title_layout.setSpacing(SPACING["tiny"]) + + title_label = SubtitleLabel("Hardware Compatibility") + title_layout.addWidget(title_label) + + subtitle_label = BodyLabel("Review hardware compatibility with macOS") + subtitle_label.setStyleSheet("color: {};".format(COLORS["text_secondary"])) + title_layout.addWidget(subtitle_label) + + header_layout.addWidget(title_block, 1) + + self.expandLayout.addWidget(header_container) + + self.status_banner = CompatibilityStatusBanner(self.scrollWidget, self.ui_utils, self.expandLayout) + + self.expandLayout.addSpacing(SPACING["large"]) + + self.contentWidget = QWidget() + self.contentLayout = QVBoxLayout(self.contentWidget) + self.contentLayout.setContentsMargins(0, 0, 0, 0) + self.contentLayout.setSpacing(SPACING["large"]) + self.expandLayout.addWidget(self.contentWidget) + + self.placeholder_label = BodyLabel("Load a hardware report to see compatibility information") + self.placeholder_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.placeholder_label.setStyleSheet("color: #605E5C; padding: 40px;") + self.placeholder_label.setWordWrap(True) + self.contentLayout.addWidget(self.placeholder_label) + self.contentLayout.addStretch() + + def update_status_banner(self): + if not self.controller.hardware_state.hardware_report: + self.status_banner.setVisible(False) + return + + if self.controller.hardware_state.compatibility_error: + self._show_error_banner() + return + + self._show_support_banner() + + def _show_error_banner(self): + codes = self.controller.hardware_state.compatibility_error + if isinstance(codes, str): + codes = [codes] + + code_map = { + "ERROR_MISSING_SSE4": ( + "Missing required SSE4.x instruction set.", + "Your CPU is not supported by macOS versions newer than Sierra (10.12)." + ), + "ERROR_NO_COMPATIBLE_GPU": ( + "You cannot install macOS without a supported GPU.", + "Please do NOT spam my inbox or issue tracker about this issue anymore!" + ), + "ERROR_INTEL_VMD": ( + "Intel VMD controllers are not supported in macOS.", + "Please disable Intel VMD in the BIOS settings and try again with new hardware report." + ), + "ERROR_NO_COMPATIBLE_STORAGE": ( + "No compatible storage controller for macOS was found!", + "Consider purchasing a compatible SSD NVMe for your system." + ) + } + + title = "Hardware Compatibility Issue" + messages = [] + notes = [] + for code in codes: + msg, note = code_map.get(code, (code, "")) + messages.append(msg) + if note: + notes.append(note) + + self.status_banner.show_error( + title, + "\n".join(messages), + "\n".join(notes) + ) + + def _show_support_banner(self): + if self.controller.macos_state.native_version: + min_ver_name = os_data.get_macos_name_by_darwin(self.controller.macos_state.native_version[0]) + max_ver_name = os_data.get_macos_name_by_darwin(self.controller.macos_state.native_version[-1]) + native_range = min_ver_name if min_ver_name == max_ver_name else "{} to {}".format(min_ver_name, max_ver_name) + + message = "Native macOS support: {}".format(native_range) + + if self.controller.macos_state.ocl_patched_version: + oclp_max_name = os_data.get_macos_name_by_darwin(self.controller.macos_state.ocl_patched_version[0]) + oclp_min_name = os_data.get_macos_name_by_darwin(self.controller.macos_state.ocl_patched_version[-1]) + oclp_range = oclp_min_name if oclp_min_name == oclp_max_name else "{} to {}".format(oclp_min_name, oclp_max_name) + message += "\nOpenCore Legacy Patcher extended support: {}".format(oclp_range) + + self.status_banner.show_success("Hardware is Compatible", message) + else: + self.status_banner.show_error( + "Incompatible Hardware", + "No supported macOS version found for this hardware configuration." + ) + + def format_compatibility(self, compat_tuple): + if not compat_tuple or compat_tuple == (None, None): + return "Unsupported", "#D13438" + + max_ver, min_ver = compat_tuple + + if max_ver and min_ver: + max_name = os_data.get_macos_name_by_darwin(max_ver) + min_name = os_data.get_macos_name_by_darwin(min_ver) + + if max_name == min_name: + return "Up to {}".format(max_name), "#0078D4" + else: + return "{} to {}".format(min_name, max_name), "#107C10" + + return "Unknown", "#605E5C" + + def update_display(self): + if not self.contentLayout: + return + + while self.contentLayout.count() > 0: + item = self.contentLayout.takeAt(0) + widget = item.widget() + if widget: + widget.deleteLater() + + if not self.controller.hardware_state.hardware_report: + self._show_placeholder() + return + + report = self.controller.hardware_state.hardware_report + cards_added = 0 + + cards_added += self._add_cpu_card(report) + cards_added += self._add_gpu_card(report) + cards_added += self._add_sound_card(report) + cards_added += self._add_network_card(report) + cards_added += self._add_storage_card(report) + cards_added += self._add_bluetooth_card(report) + cards_added += self._add_biometric_card(report) + cards_added += self._add_sd_card(report) + + if cards_added == 0: + self._show_no_data_label() + + self.contentLayout.addStretch() + self.update_status_banner() + self.scrollWidget.updateGeometry() + self.scrollWidget.update() + self.update() + + def _show_placeholder(self): + self.placeholder_label = BodyLabel("Load hardware report to see compatibility information") + self.placeholder_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.placeholder_label.setStyleSheet("color: #605E5C; padding: 40px;") + self.placeholder_label.setWordWrap(True) + self.contentLayout.addWidget(self.placeholder_label) + self.contentLayout.addStretch() + + def _show_no_data_label(self): + no_data_card = self.ui_utils.custom_card( + card_type="error", + icon=FluentIcon.CLOSE, + title="No compatible hardware information found in the report.", + body="Please ensure the hardware report contains valid device data.", + parent=self.scrollWidget + ) + self.contentLayout.addWidget(no_data_card) + + def _add_compatibility_group(self, card, title, compat): + compat_text, compat_color = self.format_compatibility(compat) + self.ui_utils.add_group_with_indent( + card, + self.ui_utils.get_compatibility_icon(compat), + title, + compat_text, + self.ui_utils.create_info_widget("", compat_color), + indent_level=1 + ) + + def _add_cpu_card(self, report): + if "CPU" not in report: return 0 + cpu_info = report["CPU"] + if not isinstance(cpu_info, dict): return 0 + + cpu_card = GroupHeaderCardWidget(self.scrollWidget) + cpu_card.setTitle("CPU") + + name = cpu_info.get("Processor Name", "Unknown") + self.ui_utils.add_group_with_indent( + cpu_card, + self.ui_utils.colored_icon(FluentIcon.TAG, COLORS["primary"]), + "Processor", + name, + indent_level=0 + ) + + self._add_compatibility_group(cpu_card, "macOS Compatibility", cpu_info.get("Compatibility", (None, None))) + + details = [] + if cpu_info.get("Codename"): + details.append("Codename: {}".format(cpu_info.get("Codename"))) + if cpu_info.get("Core Count"): + details.append("Cores: {}".format(cpu_info.get("Core Count"))) + + if details: + self.ui_utils.add_group_with_indent( + cpu_card, + self.ui_utils.colored_icon(FluentIcon.INFO, COLORS["info"]), + "Details", + " • ".join(details), + indent_level=1 + ) + + self.contentLayout.addWidget(cpu_card) + return 1 + + def _add_gpu_card(self, report): + if "GPU" not in report or not report["GPU"]: return 0 + + gpu_card = GroupHeaderCardWidget(self.scrollWidget) + gpu_card.setTitle("Graphics") + + for idx, (gpu_name, gpu_info) in enumerate(report["GPU"].items()): + device_type = gpu_info.get("Device Type", "Unknown") + self.ui_utils.add_group_with_indent( + gpu_card, + self.ui_utils.colored_icon(FluentIcon.PHOTO, COLORS["primary"]), + gpu_name, + "Type: {}".format(device_type), + indent_level=0 + ) + + self._add_compatibility_group(gpu_card, "macOS Compatibility", gpu_info.get("Compatibility", (None, None))) + + if "OCLP Compatibility" in gpu_info: + oclp_compat = gpu_info.get("OCLP Compatibility") + oclp_text, oclp_color = self.format_compatibility(oclp_compat) + self.ui_utils.add_group_with_indent( + gpu_card, + self.ui_utils.colored_icon(FluentIcon.IOT, COLORS["primary"]), + "OCLP Compatibility", + oclp_text, + self.ui_utils.create_info_widget("Extended support with OpenCore Legacy Patcher", COLORS["text_secondary"]), + indent_level=1 + ) + + if "Monitor" in report: + self._add_monitor_info(gpu_card, gpu_name, gpu_info, report["Monitor"]) + + self.contentLayout.addWidget(gpu_card) + return 1 + + def _add_monitor_info(self, gpu_card, gpu_name, gpu_info, monitors): + connected_monitors = [] + for monitor_name, monitor_info in monitors.items(): + if monitor_info.get("Connected GPU") == gpu_name: + connector = monitor_info.get("Connector Type", "Unknown") + monitor_str = "{} ({})".format(monitor_name, connector) + + manufacturer = gpu_info.get("Manufacturer", "") + raw_device_id = gpu_info.get("Device ID", "") + device_id = raw_device_id[5:] if len(raw_device_id) > 5 else raw_device_id + + if "Intel" in manufacturer and device_id.startswith(("01", "04", "0A", "0C", "0D")): + if connector == "VGA": + monitor_str += " (Unsupported)" + + connected_monitors.append(monitor_str) + + if connected_monitors: + self.ui_utils.add_group_with_indent( + gpu_card, + self.ui_utils.colored_icon(FluentIcon.VIEW, COLORS["info"]), + "Connected Displays", + ", ".join(connected_monitors), + indent_level=1 + ) + + def _add_sound_card(self, report): + if "Sound" not in report or not report["Sound"]: return 0 + + sound_card = GroupHeaderCardWidget(self.scrollWidget) + sound_card.setTitle("Audio") + + for audio_device, audio_props in report["Sound"].items(): + self.ui_utils.add_group_with_indent( + sound_card, + self.ui_utils.colored_icon(FluentIcon.MUSIC, COLORS["primary"]), + audio_device, + "", + indent_level=0 + ) + + self._add_compatibility_group(sound_card, "macOS Compatibility", audio_props.get("Compatibility", (None, None))) + + endpoints = audio_props.get("Audio Endpoints", []) + if endpoints: + self.ui_utils.add_group_with_indent( + sound_card, + self.ui_utils.colored_icon(FluentIcon.HEADPHONE, COLORS["info"]), + "Audio Endpoints", + ", ".join(endpoints), + indent_level=1 + ) + + self.contentLayout.addWidget(sound_card) + return 1 + + def _add_network_card(self, report): + if "Network" not in report or not report["Network"]: return 0 + + network_card = GroupHeaderCardWidget(self.scrollWidget) + network_card.setTitle("Network") + + for device_name, device_props in report["Network"].items(): + self.ui_utils.add_group_with_indent( + network_card, + self.ui_utils.colored_icon(FluentIcon.WIFI, COLORS["primary"]), + device_name, + "", + indent_level=0 + ) + + self._add_compatibility_group(network_card, "macOS Compatibility", device_props.get("Compatibility", (None, None))) + + if "OCLP Compatibility" in device_props: + oclp_compat = device_props.get("OCLP Compatibility") + oclp_text, oclp_color = self.format_compatibility(oclp_compat) + self.ui_utils.add_group_with_indent( + network_card, + self.ui_utils.colored_icon(FluentIcon.IOT, COLORS["primary"]), + "OCLP Compatibility", + oclp_text, + self.ui_utils.create_info_widget("Extended support with OpenCore Legacy Patcher", COLORS["text_secondary"]), + indent_level=1 + ) + + self._add_continuity_info(network_card, device_props) + + self.contentLayout.addWidget(network_card) + return 1 + + def _add_continuity_info(self, network_card, device_props): + device_id = device_props.get("Device ID", "") + if not device_id: return + + continuity_info = "" + continuity_color = COLORS["text_secondary"] + + if device_id in pci_data.BroadcomWiFiIDs: + continuity_info = "Full support (AirDrop, Handoff, Universal Clipboard, Instant Hotspot, etc.)" + continuity_color = COLORS["success"] + elif device_id in pci_data.IntelWiFiIDs: + continuity_info = "Partial (Handoff and Universal Clipboard with AirportItlwm) - AirDrop, Universal Clipboard, Instant Hotspot,... not available" + continuity_color = COLORS["warning"] + elif device_id in pci_data.AtherosWiFiIDs: + continuity_info = "Limited support (No Continuity features available). Atheros cards are not recommended for macOS." + continuity_color = COLORS["error"] + + if continuity_info: + self.ui_utils.add_group_with_indent( + network_card, + self.ui_utils.colored_icon(FluentIcon.SYNC, continuity_color), + "Continuity Features", + continuity_info, + self.ui_utils.create_info_widget("", continuity_color), + indent_level=1 + ) + + def _add_storage_card(self, report): + if "Storage Controllers" not in report or not report["Storage Controllers"]: return 0 + + storage_card = GroupHeaderCardWidget(self.scrollWidget) + storage_card.setTitle("Storage") + + for controller_name, controller_props in report["Storage Controllers"].items(): + self.ui_utils.add_group_with_indent( + storage_card, + self.ui_utils.colored_icon(FluentIcon.FOLDER, COLORS["primary"]), + controller_name, + "", + indent_level=0 + ) + + self._add_compatibility_group(storage_card, "macOS Compatibility", controller_props.get("Compatibility", (None, None))) + + disk_drives = controller_props.get("Disk Drives", []) + if disk_drives: + self.ui_utils.add_group_with_indent( + storage_card, + self.ui_utils.colored_icon(FluentIcon.FOLDER, COLORS["info"]), + "Disk Drives", + ", ".join(disk_drives), + indent_level=1 + ) + + self.contentLayout.addWidget(storage_card) + return 1 + + def _add_bluetooth_card(self, report): + if "Bluetooth" not in report or not report["Bluetooth"]: return 0 + + bluetooth_card = GroupHeaderCardWidget(self.scrollWidget) + bluetooth_card.setTitle("Bluetooth") + + for bluetooth_name, bluetooth_props in report["Bluetooth"].items(): + self.ui_utils.add_group_with_indent( + bluetooth_card, + self.ui_utils.colored_icon(FluentIcon.BLUETOOTH, COLORS["primary"]), + bluetooth_name, + "", + indent_level=0 + ) + + self._add_compatibility_group(bluetooth_card, "macOS Compatibility", bluetooth_props.get("Compatibility", (None, None))) + + self.contentLayout.addWidget(bluetooth_card) + return 1 + + def _add_biometric_card(self, report): + if "Biometric" not in report or not report["Biometric"]: return 0 + bio_card = GroupHeaderCardWidget(self.scrollWidget) + bio_card.setTitle("Biometric") + + self.ui_utils.add_group_with_indent( + bio_card, + self.ui_utils.colored_icon(FluentIcon.CLOSE, COLORS["warning"]), + "Hardware Limitation", + "Biometric authentication in macOS requires Apple T2 Chip, which is not available for Hackintosh systems.", + self.ui_utils.create_info_widget("", COLORS["warning"]), + indent_level=0 + ) + + for bio_device, bio_props in report["Biometric"].items(): + self.ui_utils.add_group_with_indent( + bio_card, + self.ui_utils.colored_icon(FluentIcon.FINGERPRINT, COLORS["error"]), + bio_device, + "Unsupported", + indent_level=0 + ) + + self.contentLayout.addWidget(bio_card) + return 1 + + def _add_sd_card(self, report): + if "SD Controller" not in report or not report["SD Controller"]: return 0 + + sd_card = GroupHeaderCardWidget(self.scrollWidget) + sd_card.setTitle("SD Controller") + + for controller_name, controller_props in report["SD Controller"].items(): + self.ui_utils.add_group_with_indent( + sd_card, + self.ui_utils.colored_icon(FluentIcon.SAVE, COLORS["primary"]), + controller_name, + "", + indent_level=0 + ) + + self._add_compatibility_group(sd_card, "macOS Compatibility", controller_props.get("Compatibility", (None, None))) + + self.contentLayout.addWidget(sd_card) + return 1 + + def refresh(self): + self.update_display() \ No newline at end of file diff --git a/Scripts/pages/configuration_page.py b/Scripts/pages/configuration_page.py new file mode 100644 index 00000000..604820ab --- /dev/null +++ b/Scripts/pages/configuration_page.py @@ -0,0 +1,293 @@ +import os + +from PyQt6.QtWidgets import QWidget, QVBoxLayout +from PyQt6.QtCore import Qt +from qfluentwidgets import ( + ScrollArea, SubtitleLabel, BodyLabel, FluentIcon, + PushSettingCard, ExpandGroupSettingCard, + SettingCard, PushButton +) + +from Scripts.custom_dialogs import show_macos_version_dialog +from Scripts.styles import SPACING, COLORS +from Scripts import ui_utils + + +class macOSCard(SettingCard): + def __init__(self, controller, on_select_version, parent=None): + super().__init__( + FluentIcon.GLOBE, + "macOS Version", + "Target operating system version", + parent + ) + self.controller = controller + + self.versionLabel = BodyLabel(self.controller.macos_state.selected_version_name) + self.versionLabel.setStyleSheet("color: {}; margin-right: 10px;".format(COLORS["text_secondary"])) + + self.selectVersionBtn = PushButton("Select Version") + self.selectVersionBtn.clicked.connect(on_select_version) + self.selectVersionBtn.setFixedWidth(150) + + self.hBoxLayout.addWidget(self.versionLabel) + self.hBoxLayout.addWidget(self.selectVersionBtn) + self.hBoxLayout.addSpacing(16) + + def update_version(self): + self.versionLabel.setText(self.controller.macos_state.selected_version_name) + +class AudioLayoutCard(SettingCard): + def __init__(self, controller, on_select_layout, parent=None): + super().__init__( + FluentIcon.MUSIC, + "Audio Layout ID", + "Select layout ID for your audio codec", + parent + ) + self.controller = controller + + layout_text = str(self.controller.hardware_state.audio_layout_id) if self.controller.hardware_state.audio_layout_id is not None else "Not configured" + self.layoutLabel = BodyLabel(layout_text) + self.layoutLabel.setStyleSheet("color: {}; margin-right: 10px;".format(COLORS["text_secondary"])) + + self.selectLayoutBtn = PushButton("Configure Layout") + self.selectLayoutBtn.clicked.connect(on_select_layout) + self.selectLayoutBtn.setFixedWidth(150) + + self.hBoxLayout.addWidget(self.layoutLabel) + self.hBoxLayout.addWidget(self.selectLayoutBtn) + self.hBoxLayout.addSpacing(16) + + self.setVisible(False) + + def update_layout(self): + layout_text = str(self.controller.hardware_state.audio_layout_id) if self.controller.hardware_state.audio_layout_id is not None else "Not configured" + self.layoutLabel.setText(layout_text) + +class SMBIOSModelCard(SettingCard): + def __init__(self, controller, on_select_model, parent=None): + super().__init__( + FluentIcon.TAG, + "SMBIOS Model", + "Select Mac model identifier for your system", + parent + ) + self.controller = controller + + model_text = self.controller.smbios_state.model_name if self.controller.smbios_state.model_name != "Not selected" else "Not configured" + self.modelLabel = BodyLabel(model_text) + self.modelLabel.setStyleSheet("color: {}; margin-right: 10px;".format(COLORS["text_secondary"])) + + self.selectModelBtn = PushButton("Configure Model") + self.selectModelBtn.clicked.connect(on_select_model) + self.selectModelBtn.setFixedWidth(150) + + self.hBoxLayout.addWidget(self.modelLabel) + self.hBoxLayout.addWidget(self.selectModelBtn) + self.hBoxLayout.addSpacing(16) + + def update_model(self): + model_text = self.controller.smbios_state.model_name if self.controller.smbios_state.model_name != "Not selected" else "Not configured" + self.modelLabel.setText(model_text) + +class ConfigurationPage(ScrollArea): + def __init__(self, parent, ui_utils_instance=None): + super().__init__(parent) + self.setObjectName("configurationPage") + self.controller = parent + self.settings = self.controller.backend.settings + self.scrollWidget = QWidget() + self.expandLayout = QVBoxLayout(self.scrollWidget) + self.ui_utils = ui_utils_instance if ui_utils_instance else ui_utils.UIUtils() + + self.setWidget(self.scrollWidget) + self.setWidgetResizable(True) + self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) + self.enableTransparentBackground() + + self.status_card = None + + self._init_ui() + + def _init_ui(self): + self.expandLayout.setContentsMargins(SPACING["xxlarge"], SPACING["xlarge"], SPACING["xxlarge"], SPACING["xlarge"]) + self.expandLayout.setSpacing(SPACING["large"]) + + self.expandLayout.addWidget(self.ui_utils.create_step_indicator(3)) + + header_container = QWidget() + header_layout = QVBoxLayout(header_container) + header_layout.setContentsMargins(0, 0, 0, 0) + header_layout.setSpacing(SPACING["tiny"]) + + title_label = SubtitleLabel("Configuration") + header_layout.addWidget(title_label) + + subtitle_label = BodyLabel("Configure your OpenCore EFI settings") + subtitle_label.setStyleSheet("color: {};".format(COLORS["text_secondary"])) + header_layout.addWidget(subtitle_label) + + self.expandLayout.addWidget(header_container) + self.expandLayout.addSpacing(SPACING["large"]) + + self.status_start_index = self.expandLayout.count() + self._update_status_card() + + self.macos_card = macOSCard(self.controller, self.select_macos_version, self.scrollWidget) + self.expandLayout.addWidget(self.macos_card) + + self.acpi_card = PushSettingCard( + "Configure Patches", + FluentIcon.DEVELOPER_TOOLS, + "ACPI Patches", + "Customize system ACPI table modifications for hardware compatibility", + self.scrollWidget + ) + self.acpi_card.clicked.connect(self.customize_acpi_patches) + self.expandLayout.addWidget(self.acpi_card) + + self.kexts_card = PushSettingCard( + "Manage Kexts", + FluentIcon.CODE, + "Kernel Extensions", + "Configure kexts required for your hardware", + self.scrollWidget + ) + self.kexts_card.clicked.connect(self.customize_kexts) + self.expandLayout.addWidget(self.kexts_card) + + self.audio_layout_card = None + self.audio_layout_card_index = None + self.audio_layout_card = AudioLayoutCard(self.controller, self.customize_audio_layout, self.scrollWidget) + self.expandLayout.addWidget(self.audio_layout_card) + + self.smbios_card = SMBIOSModelCard(self.controller, self.customize_smbios_model, self.scrollWidget) + self.expandLayout.addWidget(self.smbios_card) + + self.expandLayout.addStretch() + + def _update_status_card(self): + if self.status_card is not None: + self.expandLayout.removeWidget(self.status_card) + self.status_card.deleteLater() + self.status_card = None + + disabled_devices = self.controller.hardware_state.disabled_devices or {} + + status_text = "" + status_color = COLORS["text_secondary"] + bg_color = COLORS["bg_card"] + icon = FluentIcon.INFO + + if disabled_devices: + status_text = "Hardware components excluded from configuration" + status_color = COLORS["text_secondary"] + bg_color = COLORS["warning_bg"] + elif not self.controller.hardware_state.hardware_report: + status_text = "Please select hardware report first" + elif not self.controller.macos_state.darwin_version: + status_text = "Please select target macOS version first" + else: + status_text = "All hardware components are compatible and enabled" + status_color = COLORS["success"] + bg_color = COLORS["success_bg"] + icon = FluentIcon.ACCEPT + + self.status_card = ExpandGroupSettingCard( + icon, + "Compatibility Status", + status_text, + self.scrollWidget + ) + + if disabled_devices: + for device_name, device_info in disabled_devices.items(): + self.ui_utils.add_group_with_indent( + self.status_card, + FluentIcon.CLOSE, + device_name, + "Incompatible" if device_info.get("Compatibility") == (None, None) else "Disabled", + ) + else: + pass + + self.expandLayout.insertWidget(self.status_start_index, self.status_card) + + def select_macos_version(self): + if not self.controller.validate_prerequisites(require_darwin_version=False, require_customized_hardware=False): + return + + selected_version = show_macos_version_dialog( + self.controller.macos_state.native_version, + self.controller.macos_state.ocl_patched_version, + self.controller.macos_state.suggested_version + ) + + if selected_version: + self.controller.apply_macos_version(selected_version) + self.controller.update_status("macOS version updated to {}".format(self.controller.macos_state.selected_version_name), "success") + if hasattr(self, "macos_card"): + self.macos_card.update_version() + + def customize_acpi_patches(self): + if not self.controller.validate_prerequisites(): + return + + self.controller.backend.ac.customize_patch_selection() + self.controller.update_status("ACPI patches configuration updated successfully", "success") + + def customize_kexts(self): + if not self.controller.validate_prerequisites(): + return + + self.controller.backend.k.kext_configuration_menu(self.controller.macos_state.darwin_version) + self.controller.update_status("Kext configuration updated successfully", "success") + + def customize_audio_layout(self): + if not self.controller.validate_prerequisites(): + return + + audio_layout_id, audio_controller_properties = self.controller.backend.k._select_audio_codec_layout( + self.controller.hardware_state.hardware_report, + default_layout_id=self.controller.hardware_state.audio_layout_id + ) + + if audio_layout_id is not None: + self.controller.hardware_state.audio_layout_id = audio_layout_id + self.controller.hardware_state.audio_controller_properties = audio_controller_properties + self._update_audio_layout_card_visibility() + self.controller.update_status("Audio layout updated to {}".format(audio_layout_id), "success") + + def customize_smbios_model(self): + if not self.controller.validate_prerequisites(): + return + + current_model = self.controller.smbios_state.model_name + selected_model = self.controller.backend.s.customize_smbios_model(self.controller.hardware_state.customized_hardware, current_model, self.controller.macos_state.darwin_version, self.controller.window()) + + if selected_model and selected_model != current_model: + self.controller.smbios_state.model_name = selected_model + self.controller.backend.s.smbios_specific_options(self.controller.hardware_state.customized_hardware, selected_model, self.controller.macos_state.darwin_version, self.controller.backend.ac.patches, self.controller.backend.k) + + if hasattr(self, "smbios_card"): + self.smbios_card.update_model() + self.controller.update_status("SMBIOS model updated to {}".format(selected_model), "success") + + def _update_audio_layout_card_visibility(self): + if self.controller.hardware_state.audio_layout_id is not None: + self.audio_layout_card.setVisible(True) + self.audio_layout_card.update_layout() + else: + self.audio_layout_card.setVisible(False) + + def update_display(self): + self._update_status_card() + if hasattr(self, "macos_card"): + self.macos_card.update_version() + self._update_audio_layout_card_visibility() + if hasattr(self, "smbios_card"): + self.smbios_card.update_model() + + def refresh(self): + self.update_display() \ No newline at end of file diff --git a/Scripts/pages/home_page.py b/Scripts/pages/home_page.py new file mode 100644 index 00000000..ce8eb190 --- /dev/null +++ b/Scripts/pages/home_page.py @@ -0,0 +1,168 @@ +from PyQt6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QFrame +from PyQt6.QtCore import Qt +from qfluentwidgets import SubtitleLabel, BodyLabel, CardWidget, StrongBodyLabel, FluentIcon, ScrollArea + +from Scripts.styles import COLORS, SPACING +from Scripts import ui_utils + + +class HomePage(ScrollArea): + def __init__(self, parent, ui_utils_instance=None): + super().__init__(parent) + self.setObjectName("homePage") + self.controller = parent + self.scrollWidget = QWidget() + self.expandLayout = QVBoxLayout(self.scrollWidget) + self.ui_utils = ui_utils_instance if ui_utils_instance else ui_utils.UIUtils() + + self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) + self.setWidget(self.scrollWidget) + self.setWidgetResizable(True) + self.enableTransparentBackground() + + self.scrollWidget.setStyleSheet("QWidget { background: transparent; }") + + self._init_ui() + + def _init_ui(self): + self.expandLayout.setContentsMargins(SPACING["xxlarge"], SPACING["xlarge"], SPACING["xxlarge"], SPACING["xlarge"]) + self.expandLayout.setSpacing(SPACING["large"]) + + self.expandLayout.addWidget(self._create_title_label()) + + self.expandLayout.addWidget(self._create_hero_section()) + + self.expandLayout.addWidget(self._create_note_card()) + + self.expandLayout.addWidget(self._create_warning_card()) + + self.expandLayout.addWidget(self._create_guide_card()) + + self.expandLayout.addStretch() + + def _create_title_label(self): + title_label = SubtitleLabel("Welcome to OpCore Simplify") + title_label.setStyleSheet("font-size: 24px; font-weight: bold;") + return title_label + + def _create_hero_section(self): + hero_card = CardWidget() + + hero_layout = QHBoxLayout(hero_card) + hero_layout.setContentsMargins(SPACING["large"], SPACING["large"], SPACING["large"], SPACING["large"]) + hero_layout.setSpacing(SPACING["large"]) + + hero_text = QVBoxLayout() + hero_text.setSpacing(SPACING["medium"]) + + hero_title = StrongBodyLabel("Introduction") + hero_title.setStyleSheet("font-size: 18px; color: {};".format(COLORS["primary"])) + hero_text.addWidget(hero_title) + + hero_body = BodyLabel( + "A specialized tool that streamlines OpenCore EFI creation by automating the essential setup process and providing standardized configurations.
" + "Designed to reduce manual effort while ensuring accuracy in your Hackintosh journey." + ) + hero_body.setWordWrap(True) + hero_body.setStyleSheet("line-height: 1.6; font-size: 14px;") + hero_text.addWidget(hero_body) + + hero_layout.addLayout(hero_text, 2) + + robot_icon = self.ui_utils.build_icon_label(FluentIcon.ROBOT, COLORS["primary"], size=64) + hero_layout.addWidget(robot_icon, 1, Qt.AlignmentFlag.AlignVCenter) + + return hero_card + + def _create_note_card(self): + return self.ui_utils.custom_card( + card_type="note", + title="OpenCore Legacy Patcher 3.0.0 - Now Supports macOS Tahoe 26!", + body=( + "The long awaited version 3.0.0 of OpenCore Legacy Patcher is here, bringing initial support for macOS Tahoe 26 to the community!

" + "Please Note:
" + "- Only OpenCore-Patcher 3.0.0 from the lzhoang2801/OpenCore-Legacy-Patcher repository provides support for macOS Tahoe 26 with early patches.
" + "- Official Dortania releases or older patches will NOT work with macOS Tahoe 26." + ) + ) + + def _create_warning_card(self): + return self.ui_utils.custom_card( + card_type="warning", + title="WARNING", + body=( + "While OpCore Simplify significantly reduces setup time, the Hackintosh journey still requires:

" + "- Understanding basic concepts from the Dortania Guide
" + "- Testing and troubleshooting during the installation process.
" + "- Patience and persistence in resolving any issues that arise.

" + "Our tool does not guarantee a successful installation in the first attempt, but it should help you get started." + ) + ) + + def _create_guide_card(self): + guide_card = CardWidget() + guide_layout = QVBoxLayout(guide_card) + guide_layout.setContentsMargins(SPACING["large"], SPACING["large"], SPACING["large"], SPACING["large"]) + guide_layout.setSpacing(SPACING["medium"]) + + guide_title = StrongBodyLabel("Getting Started") + guide_title.setStyleSheet("font-size: 18px;") + guide_layout.addWidget(guide_title) + + step_items = [ + (FluentIcon.FOLDER_ADD, "1. Select Hardware Report", "Select hardware report of target system you want to build EFI for."), + (FluentIcon.CHECKBOX, "2. Check Compatibility", "Review hardware compatibility with macOS."), + (FluentIcon.EDIT, "3. Configure Settings", "Customize ACPI patches, kexts, and config for your OpenCore EFI."), + (FluentIcon.DEVELOPER_TOOLS, "4. Build EFI", "Generate your OpenCore EFI."), + ] + + for idx, (icon, title, desc) in enumerate(step_items): + guide_layout.addWidget(self._create_guide_row(icon, title, desc)) + + if idx < len(step_items) - 1: + guide_layout.addWidget(self._create_divider()) + + return guide_card + + def _create_guide_row(self, icon, title, desc): + row = QWidget() + row_layout = QHBoxLayout(row) + row_layout.setContentsMargins(0, 0, 0, 0) + row_layout.setSpacing(SPACING["medium"]) + + icon_container = QWidget() + icon_container.setFixedWidth(40) + icon_layout = QVBoxLayout(icon_container) + icon_layout.setContentsMargins(0, 0, 0, 0) + icon_layout.setAlignment(Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignHCenter) + + row_icon = self.ui_utils.build_icon_label(icon, COLORS["primary"], size=24) + icon_layout.addWidget(row_icon) + + row_layout.addWidget(icon_container) + + text_col = QVBoxLayout() + text_col.setSpacing(SPACING["tiny"]) + + title_label = StrongBodyLabel(title) + title_label.setStyleSheet("font-size: 14px;") + + desc_label = BodyLabel(desc) + desc_label.setWordWrap(True) + + desc_label.setStyleSheet("color: {}; line-height: 1.4;".format(COLORS["text_secondary"])) + + text_col.addWidget(title_label) + text_col.addWidget(desc_label) + row_layout.addLayout(text_col) + + return row + + def _create_divider(self): + divider = QFrame() + divider.setFrameShape(QFrame.Shape.HLine) + divider.setStyleSheet("color: {};".format(COLORS["border_light"])) + return divider + + def refresh(self): + pass \ No newline at end of file diff --git a/Scripts/pages/select_hardware_report_page.py b/Scripts/pages/select_hardware_report_page.py new file mode 100644 index 00000000..61a5458f --- /dev/null +++ b/Scripts/pages/select_hardware_report_page.py @@ -0,0 +1,485 @@ +import os +import threading + +from PyQt6.QtCore import pyqtSignal +from PyQt6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QFileDialog, QLabel +from qfluentwidgets import ( + PushButton, SubtitleLabel, BodyLabel, CardWidget, FluentIcon, + StrongBodyLabel, PrimaryPushButton, ProgressBar, + IconWidget, ExpandGroupSettingCard +) + +from Scripts.datasets import os_data +from Scripts.custom_dialogs import show_info, show_confirmation +from Scripts.state import HardwareReportState, macOSVersionState, SMBIOSState +from Scripts.styles import SPACING, COLORS +from Scripts import ui_utils + +class ReportDetailsGroup(ExpandGroupSettingCard): + def __init__(self, parent=None): + super().__init__( + FluentIcon.INFO, + "Hardware Report Details", + "View selected report paths and validation status", + parent + ) + + self.reportIcon = IconWidget(FluentIcon.INFO) + self.reportIcon.setFixedSize(16, 16) + self.reportIcon.setVisible(False) + + self.acpiIcon = IconWidget(FluentIcon.INFO) + self.acpiIcon.setFixedSize(16, 16) + self.acpiIcon.setVisible(False) + + self.viewLayout.setContentsMargins(0, 0, 0, 0) + self.viewLayout.setSpacing(0) + + self.reportCard = self.addGroup( + FluentIcon.DOCUMENT, + "Report Path", + "Not selected", + self.reportIcon + ) + + self.acpiCard = self.addGroup( + FluentIcon.FOLDER, + "ACPI Directory", + "Not selected", + self.acpiIcon + ) + + self.reportCard.contentLabel.setStyleSheet("color: {};".format(COLORS["text_secondary"])) + self.acpiCard.contentLabel.setStyleSheet("color: {};".format(COLORS["text_secondary"])) + + def update_status(self, section, path, status_type, message): + card = self.reportCard if section == "report" else self.acpiCard + icon_widget = self.reportIcon if section == "report" else self.acpiIcon + + if path and path != "Not selected": + path = os.path.normpath(path) + + card.setContent(path) + card.setToolTip(message if message else path) + + icon = FluentIcon.INFO + color = COLORS["text_secondary"] + + if status_type == "success": + color = COLORS["text_primary"] + icon = FluentIcon.ACCEPT + elif status_type == "error": + color = COLORS["error"] + icon = FluentIcon.CANCEL + elif status_type == "warning": + color = COLORS["warning"] + icon = FluentIcon.INFO + + card.contentLabel.setStyleSheet("color: {};".format(color)) + icon_widget.setIcon(icon) + icon_widget.setVisible(True) + +class SelectHardwareReportPage(QWidget): + export_finished_signal = pyqtSignal(bool, str, str, str) + load_report_progress_signal = pyqtSignal(str, str, int) + load_report_finished_signal = pyqtSignal(bool, str, str, str) + report_validated_signal = pyqtSignal(str, str) + compatibility_checked_signal = pyqtSignal() + + def __init__(self, parent, ui_utils_instance=None): + super().__init__(parent) + self.setObjectName("SelectHardwareReport") + self.controller = parent + self.ui_utils = ui_utils_instance if ui_utils_instance else ui_utils.UIUtils() + self._connect_signals() + self._init_ui() + + def _connect_signals(self): + self.export_finished_signal.connect(self._handle_export_finished) + self.load_report_progress_signal.connect(self._handle_load_report_progress) + self.load_report_finished_signal.connect(self._handle_load_report_finished) + self.report_validated_signal.connect(self._handle_report_validated) + self.compatibility_checked_signal.connect(self._handle_compatibility_checked) + + def _init_ui(self): + self.main_layout = QVBoxLayout(self) + self.main_layout.setContentsMargins(SPACING["xxlarge"], SPACING["xlarge"], SPACING["xxlarge"], SPACING["xlarge"]) + self.main_layout.setSpacing(SPACING["large"]) + + self.main_layout.addWidget(self.ui_utils.create_step_indicator(1)) + + header_layout = QVBoxLayout() + header_layout.setSpacing(SPACING["small"]) + title = SubtitleLabel("Select Hardware Report") + subtitle = BodyLabel("Select hardware report of target system you want to build EFI for") + subtitle.setStyleSheet("color: {};".format(COLORS["text_secondary"])) + header_layout.addWidget(title) + header_layout.addWidget(subtitle) + self.main_layout.addLayout(header_layout) + + self.main_layout.addSpacing(SPACING["medium"]) + + self.create_instructions_card() + + self.create_action_card() + + self.create_report_details_group() + + self.main_layout.addStretch() + + def create_instructions_card(self): + card = self.ui_utils.custom_card( + card_type="note", + title="Quick Guide", + body=( + "Windows Users: Click Export Hardware Report button to generate hardware report for current system. Alternatively, you can manually generate hardware report using Hardware Sniffer tool.
" + "Linux/macOS Users: Please transfer a report generated on Windows. Native generation is not supported." + ) + ) + self.main_layout.addWidget(card) + + def create_action_card(self): + self.action_card = CardWidget() + layout = QVBoxLayout(self.action_card) + layout.setContentsMargins(SPACING["large"], SPACING["large"], SPACING["large"], SPACING["large"]) + layout.setSpacing(SPACING["medium"]) + + title = StrongBodyLabel("Select Methods") + layout.addWidget(title) + + btn_layout = QHBoxLayout() + btn_layout.setSpacing(SPACING["medium"]) + + self.select_btn = PrimaryPushButton(FluentIcon.FOLDER_ADD, "Select Hardware Report") + self.select_btn.clicked.connect(self.select_hardware_report) + btn_layout.addWidget(self.select_btn) + + if os.name == "nt": + self.export_btn = PushButton(FluentIcon.DOWNLOAD, "Export Hardware Report") + self.export_btn.clicked.connect(self.export_hardware_report) + btn_layout.addWidget(self.export_btn) + + layout.addLayout(btn_layout) + + self.progress_container = QWidget() + progress_layout = QVBoxLayout(self.progress_container) + progress_layout.setContentsMargins(0, SPACING["small"], 0, 0) + progress_layout.setSpacing(SPACING["medium"]) + + status_row = QHBoxLayout() + status_row.setSpacing(SPACING["medium"]) + + self.status_icon_label = QLabel() + self.status_icon_label.setFixedSize(28, 28) + status_row.addWidget(self.status_icon_label) + + self.progress_label = StrongBodyLabel("Ready") + self.progress_label.setStyleSheet("color: {}; font-size: 15px; font-weight: 600;".format(COLORS["text_secondary"])) + status_row.addWidget(self.progress_label) + status_row.addStretch() + + progress_layout.addLayout(status_row) + + self.progress_bar = ProgressBar() + self.progress_bar.setValue(0) + self.progress_bar.setFixedHeight(10) + self.progress_bar.setTextVisible(True) + progress_layout.addWidget(self.progress_bar) + + self.progress_container.setVisible(False) + layout.addWidget(self.progress_container) + + self.progress_helper = ui_utils.ProgressStatusHelper( + self.status_icon_label, + self.progress_label, + self.progress_bar, + self.progress_container + ) + + self.main_layout.addWidget(self.action_card) + + def create_report_details_group(self): + self.report_group = ReportDetailsGroup(self) + self.main_layout.addWidget(self.report_group) + + def select_report_file(self): + report_path, _ = QFileDialog.getOpenFileName( + self, "Select Hardware Report", "", "JSON Files (*.json)" + ) + return report_path if report_path else None + + def select_acpi_folder(self): + acpi_dir = QFileDialog.getExistingDirectory(self, "Select ACPI Folder", "") + return acpi_dir if acpi_dir else None + + def select_hardware_report(self): + report_path = self.select_report_file() + if not report_path: + return + + report_dir = os.path.dirname(report_path) + potential_acpi = os.path.join(report_dir, "ACPI") + + acpi_dir = None + if os.path.isdir(potential_acpi): + if show_confirmation("ACPI Folder Detected", "Found an ACPI folder at: {}\n\nDo you want to use this ACPI folder?".format(potential_acpi)): + acpi_dir = potential_acpi + + if not acpi_dir: + acpi_dir = self.select_acpi_folder() + + if not acpi_dir: + return + + self.load_hardware_report(report_path, acpi_dir) + + def set_detail_status(self, section, path, status_type, message): + self.report_group.update_status(section, path, status_type, message) + + def suggest_macos_version(self): + if not self.controller.hardware_state.hardware_report or not self.controller.macos_state.native_version: + return None + + hardware_report = self.controller.hardware_state.hardware_report + native_macos_version = self.controller.macos_state.native_version + + suggested_macos_version = native_macos_version[1] + + for device_type in ("GPU", "Network", "Bluetooth", "SD Controller"): + if device_type in hardware_report: + for device_name, device_props in hardware_report[device_type].items(): + if device_props.get("Compatibility", (None, None)) != (None, None): + if device_type == "GPU" and device_props.get("Device Type") == "Integrated GPU": + device_id = device_props.get("Device ID", " " * 8)[5:] + + if device_props.get("Manufacturer") == "AMD" or device_id.startswith(("59", "87C0")): + suggested_macos_version = "22.99.99" + elif device_id.startswith(("09", "19")): + suggested_macos_version = "21.99.99" + + if self.controller.backend.u.parse_darwin_version(suggested_macos_version) > self.controller.backend.u.parse_darwin_version(device_props.get("Compatibility")[0]): + suggested_macos_version = device_props.get("Compatibility")[0] + + while True: + if "Beta" in os_data.get_macos_name_by_darwin(suggested_macos_version): + suggested_macos_version = "{}{}".format( + int(suggested_macos_version[:2]) - 1, suggested_macos_version[2:]) + else: + break + + self.controller.macos_state.suggested_version = suggested_macos_version + + def load_hardware_report(self, report_path, acpi_dir, from_export=False): + self.controller.hardware_state = HardwareReportState(report_path=report_path, acpi_dir=acpi_dir) + self.controller.macos_state = macOSVersionState() + self.controller.smbios_state = SMBIOSState() + self.controller.backend.ac.acpi.acpi_tables = {} + self.controller.backend.ac.acpi.dsdt = None + + self.controller.compatibilityPage.update_display() + self.controller.configurationPage.update_display() + + if not from_export: + self.progress_container.setVisible(True) + self.select_btn.setEnabled(False) + if hasattr(self, "export_btn"): + self.export_btn.setEnabled(False) + + progress_offset = 40 if from_export else 0 + self.progress_helper.update("loading", "Validating report...", progress_offset) + self.report_group.setExpand(True) + + def load_thread(): + try: + progress_scale = 0.5 if from_export else 1.0 + + def get_progress(base_progress): + return progress_offset + int(base_progress * progress_scale) + + self.load_report_progress_signal.emit("loading", "Validating report...", get_progress(10)) + + is_valid, errors, warnings, validated_data = self.controller.backend.v.validate_report(report_path) + + if not is_valid or errors: + error_msg = "Report Errors:\n" + "\n".join(errors) + self.load_report_finished_signal.emit(False, "validation_error", report_path, acpi_dir) + return + + self.load_report_progress_signal.emit("loading", "Validating report...", get_progress(30)) + + self.report_validated_signal.emit(report_path, "Hardware report validated successfully.") + + self.load_report_progress_signal.emit("loading", "Checking compatibility...", get_progress(35)) + + self.controller.hardware_state.hardware_report = validated_data + + self.controller.hardware_state.hardware_report, self.controller.macos_state.native_version, self.controller.macos_state.ocl_patched_version, self.controller.hardware_state.compatibility_error = self.controller.backend.c.check_compatibility(validated_data) + + self.load_report_progress_signal.emit("loading", "Checking compatibility...", get_progress(55)) + + self.compatibility_checked_signal.emit() + + if self.controller.hardware_state.compatibility_error: + error_msg = self.controller.hardware_state.compatibility_error + if isinstance(error_msg, list): + error_msg = "\n".join(error_msg) + self.load_report_finished_signal.emit(False, "compatibility_error", report_path, acpi_dir) + return + + self.load_report_progress_signal.emit("loading", "Loading ACPI tables...", get_progress(60)) + + self.controller.backend.ac.read_acpi_tables(acpi_dir) + + self.load_report_progress_signal.emit("loading", "Loading ACPI tables...", get_progress(90)) + + if not self.controller.backend.ac._ensure_dsdt(): + self.load_report_finished_signal.emit(False, "acpi_error", report_path, acpi_dir) + return + + self.load_report_finished_signal.emit(True, "success", report_path, acpi_dir) + + except Exception as e: + self.load_report_finished_signal.emit(False, "Exception: {}".format(e), report_path, acpi_dir) + + thread = threading.Thread(target=load_thread, daemon=True) + thread.start() + + def _handle_load_report_progress(self, status, message, progress): + self.progress_helper.update(status, message, progress) + + def _handle_report_validated(self, report_path, message): + self.set_detail_status("report", report_path, "success", message) + + def _handle_compatibility_checked(self): + self.controller.compatibilityPage.update_display() + + def _handle_load_report_finished(self, success, error_type, report_path, acpi_dir): + self.select_btn.setEnabled(True) + if hasattr(self, "export_btn"): + self.export_btn.setEnabled(True) + + if success: + count = len(self.controller.backend.ac.acpi.acpi_tables) + self.set_detail_status("acpi", acpi_dir, "success", "ACPI Tables loaded: {} tables found.".format(count)) + + self.progress_helper.update("success", "Hardware report loaded successfully", 100) + + self.controller.update_status("Hardware report loaded successfully", "success") + self.suggest_macos_version() + self.controller.configurationPage.update_display() + else: + if error_type == "validation_error": + is_valid, errors, warnings, validated_data = self.controller.backend.v.validate_report(report_path) + msg = "Report Errors:\n" + "\n".join(errors) + self.set_detail_status("report", report_path, "error", msg) + self.progress_helper.update("error", "Report validation failed", None) + show_info("Report Validation Failed", "The hardware report has errors:\n{}\n\nPlease select a valid report file.".format("\n".join(errors))) + elif error_type == "compatibility_error": + error_msg = self.controller.hardware_state.compatibility_error + if isinstance(error_msg, list): + error_msg = "\n".join(error_msg) + compat_text = "\nCompatibility Error:\n{}".format(error_msg) + self.set_detail_status("report", report_path, "error", compat_text) + show_info("Incompatible Hardware", "Your hardware is not compatible with macOS:\n\n" + error_msg) + elif error_type == "acpi_error": + self.set_detail_status("acpi", acpi_dir, "error", "No ACPI tables found in selected folder.") + self.progress_helper.update("error", "No ACPI tables found", None) + show_info("No ACPI tables", "No ACPI tables found in ACPI folder.") + else: + self.progress_helper.update("error", "Error: {}".format(error_type), None) + self.controller.update_status("Failed to load hardware report: {}".format(error_type), "error") + + def export_hardware_report(self): + self.progress_container.setVisible(True) + self.select_btn.setEnabled(False) + if hasattr(self, "export_btn"): + self.export_btn.setEnabled(False) + + self.progress_helper.update("loading", "Gathering Hardware Sniffer...", 10) + + current_dir = os.path.dirname(os.path.realpath(__file__)) + main_dir = os.path.dirname(os.path.dirname(current_dir)) + report_dir = os.path.join(main_dir, "SysReport") + + def export_thread(): + try: + hardware_sniffer = self.controller.backend.o.gather_hardware_sniffer() + + if not hardware_sniffer: + self.export_finished_signal.emit(False, "Hardware Sniffer not found", "", "") + return + + self.export_finished_signal.emit(True, "gathering_complete", hardware_sniffer, report_dir) + except Exception as e: + self.export_finished_signal.emit(False, "Exception gathering sniffer: {}".format(e), "", "") + + thread = threading.Thread(target=export_thread, daemon=True) + thread.start() + + def _handle_export_finished(self, success, message, hardware_sniffer_or_error, report_dir): + if not success: + self.progress_container.setVisible(False) + self.select_btn.setEnabled(True) + if hasattr(self, "export_btn"): + self.export_btn.setEnabled(True) + self.progress_helper.update("error", "Export failed", 0) + self.controller.update_status(hardware_sniffer_or_error, "error") + return + + if message == "gathering_complete": + self.progress_helper.update("loading", "Exporting hardware report...", 50) + + def run_export_thread(): + try: + output = self.controller.backend.r.run({ + "args": [hardware_sniffer_or_error, "-e", "-o", report_dir] + }) + + success = output[-1] == 0 + error_message = "" + report_path = "" + acpi_dir = "" + + if success: + report_path = os.path.join(report_dir, "Report.json") + acpi_dir = os.path.join(report_dir, "ACPI") + error_message = "Export successful" + else: + error_code = output[-1] + if error_code == 3: error_message = "Error collecting hardware." + elif error_code == 4: error_message = "Error generating hardware report." + elif error_code == 5: error_message = "Error dumping ACPI tables." + else: error_message = "Unknown error." + + paths = "{}|||{}".format(report_path, acpi_dir) if report_path and acpi_dir else "" + self.export_finished_signal.emit(success, "export_complete", error_message, paths) + except Exception as e: + self.export_finished_signal.emit(False, "export_complete", "Exception: {}".format(e), "") + + thread = threading.Thread(target=run_export_thread, daemon=True) + thread.start() + return + + if message == "export_complete": + self.progress_container.setVisible(False) + self.select_btn.setEnabled(True) + if hasattr(self, "export_btn"): + self.export_btn.setEnabled(True) + + self.controller.backend.u.log_message("[EXPORT] Export at: {}".format(report_dir), level="INFO") + + if success: + if report_dir and "|||" in report_dir: + report_path, acpi_dir = report_dir.split("|||", 1) + else: + report_path = "" + acpi_dir = "" + + if report_path and acpi_dir: + self.load_hardware_report(report_path, acpi_dir, from_export=True) + else: + self.progress_helper.update("error", "Export completed but paths are invalid", None) + self.controller.update_status("Export completed but paths are invalid", "error") + else: + self.progress_helper.update("error", "Export failed: {}".format(hardware_sniffer_or_error), None) + self.controller.update_status("Export failed: {}".format(hardware_sniffer_or_error), "error") \ No newline at end of file diff --git a/Scripts/pages/settings_page.py b/Scripts/pages/settings_page.py new file mode 100644 index 00000000..5bb611b7 --- /dev/null +++ b/Scripts/pages/settings_page.py @@ -0,0 +1,271 @@ +import os + +from PyQt6.QtWidgets import ( + QWidget, QVBoxLayout, QHBoxLayout, QFileDialog +) +from PyQt6.QtCore import Qt +from qfluentwidgets import ( + ScrollArea, BodyLabel, PushButton, LineEdit, FluentIcon, + SettingCardGroup, SwitchSettingCard, ComboBoxSettingCard, + PushSettingCard, SpinBox, + OptionsConfigItem, OptionsValidator, HyperlinkCard, + StrongBodyLabel, CaptionLabel, SettingCard, SubtitleLabel, + setTheme, Theme +) + +from Scripts.custom_dialogs import show_confirmation +from Scripts.styles import COLORS, SPACING + + +class SettingsPage(ScrollArea): + def __init__(self, parent): + super().__init__(parent) + self.setObjectName("settingsPage") + self.controller = parent + self.scrollWidget = QWidget() + self.expandLayout = QVBoxLayout(self.scrollWidget) + self.settings = self.controller.backend.settings + + self.setWidget(self.scrollWidget) + self.setWidgetResizable(True) + self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) + self.enableTransparentBackground() + + self._init_ui() + + def _init_ui(self): + self.expandLayout.setContentsMargins(SPACING["xxlarge"], SPACING["xlarge"], SPACING["xxlarge"], SPACING["xlarge"]) + self.expandLayout.setSpacing(SPACING["large"]) + + header_container = QWidget() + header_layout = QVBoxLayout(header_container) + header_layout.setContentsMargins(0, 0, 0, 0) + header_layout.setSpacing(SPACING["tiny"]) + + title_label = SubtitleLabel("Settings") + header_layout.addWidget(title_label) + + subtitle_label = BodyLabel("Configure OpCore Simplify preferences") + subtitle_label.setStyleSheet("color: {};".format(COLORS["text_secondary"])) + header_layout.addWidget(subtitle_label) + + self.expandLayout.addWidget(header_container) + self.expandLayout.addSpacing(SPACING["medium"]) + + self.build_output_group = self.create_build_output_group() + self.expandLayout.addWidget(self.build_output_group) + + self.macos_group = self.create_macos_version_group() + self.expandLayout.addWidget(self.macos_group) + + #self.appearance_group = self.create_appearance_group() + #self.expandLayout.addWidget(self.appearance_group) + + self.update_group = self.create_update_settings_group() + self.expandLayout.addWidget(self.update_group) + + self.advanced_group = self.create_advanced_group() + self.expandLayout.addWidget(self.advanced_group) + + self.help_group = self.create_help_group() + self.expandLayout.addWidget(self.help_group) + + self.bottom_widget = QWidget() + bottom_layout = QHBoxLayout(self.bottom_widget) + bottom_layout.setContentsMargins(0, SPACING["large"], 0, SPACING["large"]) + bottom_layout.setSpacing(SPACING["medium"]) + bottom_layout.addStretch() + + reset_btn = PushButton("Reset All to Defaults", self.bottom_widget) + reset_btn.setIcon(FluentIcon.CANCEL) + reset_btn.clicked.connect(self.reset_to_defaults) + bottom_layout.addWidget(reset_btn) + + self.expandLayout.addWidget(self.bottom_widget) + + for card in self.findChildren(SettingCard): + card.setIconSize(18, 18) + + def _update_widget_value(self, widget, value): + if widget is None: + return + + if isinstance(widget, SwitchSettingCard): + widget.switchButton.setChecked(value) + elif isinstance(widget, (ComboBoxSettingCard, OptionsConfigItem)): + widget.setValue(value) + elif isinstance(widget, SpinBox): + widget.setValue(value) + elif isinstance(widget, LineEdit): + widget.setText(value) + elif isinstance(widget, PushSettingCard): + widget.setContent(value or "Use temporary directory (default)") + + def create_build_output_group(self): + group = SettingCardGroup("Build Output", self.scrollWidget) + + self.output_dir_card = PushSettingCard( + "Browse", + FluentIcon.FOLDER, + "Output Directory", + self.settings.get("build_output_directory") or "Use temporary directory (default)", + group + ) + self.output_dir_card.setObjectName("build_output_directory") + self.output_dir_card.clicked.connect(self.browse_output_directory) + group.addSettingCard(self.output_dir_card) + + return group + + def create_macos_version_group(self): + group = SettingCardGroup("macOS Version", self.scrollWidget) + + self.include_beta_card = SwitchSettingCard( + FluentIcon.UPDATE, + "Include beta version", + "Show major beta macOS versions in version selection menus. Enable to test new macOS releases.", + configItem=None, + parent=group + ) + self.include_beta_card.setObjectName("include_beta_versions") + self.include_beta_card.switchButton.setChecked(self.settings.get_include_beta_versions()) + self.include_beta_card.switchButton.checkedChanged.connect(lambda checked: self.settings.set("include_beta_versions", checked)) + group.addSettingCard(self.include_beta_card) + + return group + + def create_appearance_group(self): + group = SettingCardGroup("Appearance", self.scrollWidget) + + theme_values = [ + "Light", + #"Dark", + ] + theme_value = self.settings.get_theme() + if theme_value not in theme_values: + theme_value = "Light" + + self.theme_config = OptionsConfigItem( + "Appearance", + "Theme", + theme_value, + OptionsValidator(theme_values) + ) + + def on_theme_changed(value): + self.settings.set("theme", value) + if value == "Dark": + setTheme(Theme.DARK) + else: + setTheme(Theme.LIGHT) + + self.theme_config.valueChanged.connect(on_theme_changed) + + self.theme_card = ComboBoxSettingCard( + self.theme_config, + FluentIcon.BRUSH, + "Theme", + "Selects the application color theme.", + theme_values, + group + ) + self.theme_card.setObjectName("theme") + group.addSettingCard(self.theme_card) + + return group + + def create_update_settings_group(self): + group = SettingCardGroup("Updates & Downloads", self.scrollWidget) + + self.auto_update_card = SwitchSettingCard( + FluentIcon.UPDATE, + "Check for updates on startup", + "Automatically checks for new OpCore Simplify updates when the application launches to keep you up to date", + configItem=None, + parent=group + ) + self.auto_update_card.setObjectName("auto_update_check") + self.auto_update_card.switchButton.setChecked(self.settings.get_auto_update_check()) + self.auto_update_card.switchButton.checkedChanged.connect(lambda checked: self.settings.set("auto_update_check", checked)) + group.addSettingCard(self.auto_update_card) + + return group + + def create_advanced_group(self): + group = SettingCardGroup("Advanced Settings", self.scrollWidget) + + self.debug_logging_card = SwitchSettingCard( + FluentIcon.DEVELOPER_TOOLS, + "Enable debug logging", + "Enables detailed debug logging throughout the application for advanced troubleshooting and diagnostics", + configItem=None, + parent=group + ) + self.debug_logging_card.setObjectName("enable_debug_logging") + self.debug_logging_card.switchButton.setChecked(self.settings.get_enable_debug_logging()) + self.debug_logging_card.switchButton.checkedChanged.connect(lambda checked: self.settings.set("enable_debug_logging", checked)) + group.addSettingCard(self.debug_logging_card) + + return group + + def create_help_group(self): + group = SettingCardGroup("Help & Documentation", self.scrollWidget) + + self.opencore_docs_card = HyperlinkCard( + "https://dortania.github.io/OpenCore-Install-Guide/", + "OpenCore Install Guide", + FluentIcon.BOOK_SHELF, + "OpenCore Documentation", + "Complete guide for installing macOS with OpenCore", + group + ) + group.addSettingCard(self.opencore_docs_card) + + self.troubleshoot_card = HyperlinkCard( + "https://dortania.github.io/OpenCore-Install-Guide/troubleshooting/troubleshooting.html", + "Troubleshooting", + FluentIcon.HELP, + "Troubleshooting Guide", + "Solutions to common OpenCore installation issues", + group + ) + group.addSettingCard(self.troubleshoot_card) + + self.github_card = HyperlinkCard( + "https://github.com/lzhoang2801/OpCore-Simplify", + "View on GitHub", + FluentIcon.GITHUB, + "OpCore-Simplify Repository", + "Report issues, contribute, or view the source code", + group + ) + group.addSettingCard(self.github_card) + + return group + + def browse_output_directory(self): + folder = QFileDialog.getExistingDirectory( + self, + "Select Build Output Directory", + os.path.expanduser("~") + ) + + if folder: + self.settings.set("build_output_directory", folder) + self.output_dir_card.setContent(folder) + self.controller.update_status("Output directory updated successfully", "success") + + def reset_to_defaults(self): + result = show_confirmation("Reset Settings", "Are you sure you want to reset all settings to their default values?") + + if result: + self.settings.settings = self.settings.defaults.copy() + self.settings.save_settings() + + for widget in self.findChildren(QWidget): + key = widget.objectName() + if key and key in self.settings.defaults: + default_value = self.settings.defaults.get(key) + self._update_widget_value(widget, default_value) + + self.controller.update_status("All settings reset to defaults", "success") \ No newline at end of file diff --git a/Scripts/report_validator.py b/Scripts/report_validator.py index 7fc602ff..66167dba 100644 --- a/Scripts/report_validator.py +++ b/Scripts/report_validator.py @@ -4,10 +4,10 @@ import re class ReportValidator: - def __init__(self): + def __init__(self, utils_instance=None): self.errors = [] self.warnings = [] - self.u = utils.Utils() + self.u = utils_instance if utils_instance else utils.Utils() self.PATTERNS = { "not_empty": r".+", @@ -244,17 +244,17 @@ def _validate_node(self, data, rule, path): if expected_type: if not isinstance(data, expected_type): type_name = expected_type.__name__ if hasattr(expected_type, "__name__") else str(expected_type) - self.errors.append(f"{path}: Expected type {type_name}, got {type(data).__name__}") + self.errors.append("{}: Expected type {}, got {}".format(path, type_name, type(data).__name__)) return None if isinstance(data, str): pattern = rule.get("pattern") if pattern is not None: if not re.match(pattern, data): - self.errors.append(f"{path}: Value '{data}' does not match pattern '{pattern}'") + self.errors.append("{}: Value '{}' does not match pattern '{}'".format(path, data, pattern)) return None elif not re.match(self.PATTERNS["not_empty"], data): - self.errors.append(f"{path}: Value '{data}' does not match pattern '{self.PATTERNS['not_empty']}'") + self.errors.append("{}: Value '{}' does not match pattern '{}'".format(path, data, self.PATTERNS["not_empty"])) return None cleaned_data = data @@ -265,53 +265,30 @@ def _validate_node(self, data, rule, path): for key, value in data.items(): if key in schema_keys: - cleaned_val = self._validate_node(value, schema_keys[key], f"{path}.{key}") + cleaned_val = self._validate_node(value, schema_keys[key], "{}.".format(path, key)) if cleaned_val is not None: cleaned_data[key] = cleaned_val elif "values_rule" in rule: - cleaned_val = self._validate_node(value, rule["values_rule"], f"{path}.{key}") + cleaned_val = self._validate_node(value, rule["values_rule"], "{}.".format(path, key)) if cleaned_val is not None: cleaned_data[key] = cleaned_val else: if schema_keys: - self.warnings.append(f"{path}: Unknown key '{key}'") + self.warnings.append("{}: Unknown key '{}'".format(path, key)) for key, key_rule in schema_keys.items(): if key_rule.get("required", True) and key not in cleaned_data: - self.errors.append(f"{path}: Missing required key '{key}'") + self.errors.append("{}: Missing required key '{}'".format(path, key)) elif isinstance(data, list): item_rule = rule.get("item_rule") if item_rule: cleaned_data = [] for i, item in enumerate(data): - cleaned_val = self._validate_node(item, item_rule, f"{path}[{i}]") + cleaned_val = self._validate_node(item, item_rule, "{}[{}]".format(path, i)) if cleaned_val is not None: cleaned_data.append(cleaned_val) else: cleaned_data = list(data) - return cleaned_data - - def show_validation_report(self, report_path, is_valid, errors, warnings): - self.u.head("Validation Report") - print("") - print("Validation report for: {}".format(report_path)) - print("") - - if is_valid: - print("Hardware report is valid!") - else: - print("Hardware report is not valid! Please check the errors and warnings below.") - - if errors: - print("") - print("\033[31mErrors ({}):\033[0m".format(len(errors))) - for i, error in enumerate(errors, 1): - print(" {}. {}".format(i, error)) - - if warnings: - print("") - print("\033[33mWarnings ({}):\033[0m".format(len(warnings))) - for i, warning in enumerate(warnings, 1): - print(" {}. {}".format(i, warning)) \ No newline at end of file + return cleaned_data \ No newline at end of file diff --git a/Scripts/resource_fetcher.py b/Scripts/resource_fetcher.py index efd4220d..adb0de0f 100644 --- a/Scripts/resource_fetcher.py +++ b/Scripts/resource_fetcher.py @@ -20,14 +20,14 @@ MAX_ATTEMPTS = 3 class ResourceFetcher: - def __init__(self, headers=None): + def __init__(self, utils_instance=None, integrity_checker_instance=None, headers=None): self.request_headers = headers or { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36" } + self.utils = utils_instance if utils_instance else utils.Utils() self.buffer_size = 16 * 1024 self.ssl_context = self.create_ssl_context() - self.integrity_checker = integrity_checker.IntegrityChecker() - self.utils = utils.Utils() + self.integrity_checker = integrity_checker_instance if integrity_checker_instance else integrity_checker.IntegrityChecker() def create_ssl_context(self): try: @@ -36,9 +36,10 @@ def create_ssl_context(self): import certifi cafile = certifi.where() ssl_context = ssl.create_default_context(cafile=cafile) + self.utils.log_message("[RESOURCE FETCHER] Created SSL context", level="INFO") except Exception as e: - print("Failed to create SSL context: {}".format(e)) ssl_context = ssl._create_unverified_context() + self.utils.log_message("[RESOURCE FETCHER] Created unverified SSL context", level="INFO") return ssl_context def _make_request(self, resource_url, timeout=10): @@ -48,13 +49,13 @@ def _make_request(self, resource_url, timeout=10): return urlopen(Request(resource_url, headers=headers), timeout=timeout, context=self.ssl_context) except socket.timeout as e: - print("Timeout error: {}".format(e)) + self.utils.log_message("[RESOURCE FETCHER] Timeout error: {}".format(e), level="ERROR", to_build_log=True) except ssl.SSLError as e: - print("SSL error: {}".format(e)) + self.utils.log_message("[RESOURCE FETCHER] SSL error: {}".format(e), level="ERROR", to_build_log=True) except (URLError, socket.gaierror) as e: - print("Connection error: {}".format(e)) + self.utils.log_message("[RESOURCE FETCHER] Connection error: {}".format(e), level="ERROR", to_build_log=True) except Exception as e: - print("Request failed: {}".format(e)) + self.utils.log_message("[RESOURCE FETCHER] Request failed: {}".format(e), level="ERROR", to_build_log=True) return None @@ -62,12 +63,14 @@ def fetch_and_parse_content(self, resource_url, content_type=None): attempt = 0 response = None - while attempt < 3: + self.utils.log_message("[RESOURCE FETCHER] Fetching and parsing content from {}".format(resource_url), level="INFO") + + while attempt < MAX_ATTEMPTS: response = self._make_request(resource_url) if not response: attempt += 1 - print("Failed to fetch content from {}. Retrying...".format(resource_url)) + self.utils.log_message("[RESOURCE FETCHER] Failed to fetch content from {}. Retrying...".format(resource_url), level="WARNING", to_build_log=True) continue if response.getcode() == 200: @@ -76,7 +79,7 @@ def fetch_and_parse_content(self, resource_url, content_type=None): attempt += 1 if not response: - print("Failed to fetch content from {}".format(resource_url)) + self.utils.log_message("[RESOURCE FETCHER] Failed to fetch content from {}".format(resource_url), level="ERROR", to_build_log=True) return None content = response.read() @@ -85,12 +88,12 @@ def fetch_and_parse_content(self, resource_url, content_type=None): try: content = gzip.decompress(content) except Exception as e: - print("Failed to decompress gzip content: {}".format(e)) + self.utils.log_message("[RESOURCE FETCHER] Failed to decompress gzip content: {}".format(e), level="ERROR", to_build_log=True) elif response.info().get("Content-Encoding") == "deflate": try: content = zlib.decompress(content) except Exception as e: - print("Failed to decompress deflate content: {}".format(e)) + self.utils.log_message("[RESOURCE FETCHER] Failed to decompress deflate content: {}".format(e), level="ERROR", to_build_log=True) try: if content_type == "json": @@ -100,7 +103,7 @@ def fetch_and_parse_content(self, resource_url, content_type=None): else: return content.decode("utf-8") except Exception as e: - print("Error parsing content as {}: {}".format(content_type, e)) + self.utils.log_message("[RESOURCE FETCHER] Error parsing content as {}: {}".format(content_type, e), level="ERROR", to_build_log=True) return None @@ -150,20 +153,19 @@ def _download_with_progress(self, response, local_file): else: progress = "{} {:.1f}MB downloaded".format(speed_str, bytes_downloaded/(1024*1024)) - print(" " * 80, end="\r") - print(progress, end="\r") - - print() + self.utils.log_message("[RESOURCE FETCHER] Download progress: {}".format(progress), level="INFO", to_build_log=True) def download_and_save_file(self, resource_url, destination_path, sha256_hash=None): attempt = 0 + self.utils.log_message("[RESOURCE FETCHER] Downloading and saving file from {} to {}".format(resource_url, destination_path), level="INFO") + while attempt < MAX_ATTEMPTS: attempt += 1 response = self._make_request(resource_url) if not response: - print("Failed to fetch content from {}. Retrying...".format(resource_url)) + self.utils.log_message("[RESOURCE FETCHER] Failed to fetch content from {}. Retrying...".format(resource_url), level="WARNING", to_build_log=True) continue with open(destination_path, "wb") as local_file: @@ -171,24 +173,24 @@ def download_and_save_file(self, resource_url, destination_path, sha256_hash=Non if os.path.exists(destination_path) and os.path.getsize(destination_path) > 0: if sha256_hash: - print("Verifying SHA256 checksum...") + self.utils.log_message("[RESOURCE FETCHER] Verifying SHA256 checksum...", level="INFO", to_build_log=True) downloaded_hash = self.integrity_checker.get_sha256(destination_path) if downloaded_hash.lower() == sha256_hash.lower(): - print("Checksum verified successfully.") + self.utils.log_message("[RESOURCE FETCHER] Checksum verified successfully.", level="INFO", to_build_log=True) return True else: - print("Checksum mismatch! Removing file and retrying download...") + self.utils.log_message("[RESOURCE FETCHER] Checksum mismatch! Removing file and retrying download...", level="WARNING", to_build_log=True) os.remove(destination_path) continue else: - print("No SHA256 hash provided. Downloading file without verification.") + self.utils.log_message("[RESOURCE FETCHER] No SHA256 hash provided. Downloading file without verification.", level="INFO", to_build_log=True) return True if os.path.exists(destination_path): os.remove(destination_path) if attempt < MAX_ATTEMPTS: - print("Download failed for {}. Retrying...".format(resource_url)) + self.utils.log_message("[RESOURCE FETCHER] Download failed for {}. Retrying...".format(resource_url), level="WARNING", to_build_log=True) - print("Failed to download {} after {} attempts.".format(resource_url, MAX_ATTEMPTS)) + self.utils.log_message("[RESOURCE FETCHER] Failed to download {} after {} attempts.".format(resource_url, MAX_ATTEMPTS), level="ERROR", to_build_log=True) return False \ No newline at end of file diff --git a/Scripts/settings.py b/Scripts/settings.py new file mode 100644 index 00000000..09cfbb86 --- /dev/null +++ b/Scripts/settings.py @@ -0,0 +1,53 @@ +import os +from Scripts import utils + + +class Settings: + def __init__(self, utils_instance=None): + self.u = utils_instance if utils_instance else utils.Utils() + self.defaults = { + "build_output_directory": "", + "include_beta_versions": False, + "theme": "Light", + "auto_update_check": True, + "enable_debug_logging": False, + } + + self.settings_file = self._get_settings_file_path() + self.settings = self.load_settings() + + def _get_settings_file_path(self): + script_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + return os.path.join(script_dir, "settings.json") + + def load_settings(self): + try: + loaded_settings = self.u.read_file(self.settings_file) + + if loaded_settings is not None: + return loaded_settings + except Exception as e: + print("Error loading settings: {}".format(e)) + + return self.defaults.copy() + + def save_settings(self): + try: + self.u.write_file(self.settings_file, self.settings) + except Exception as e: + print("Error saving settings: {}".format(e)) + + def get(self, key, default=None): + return self.settings.get(key, self.defaults.get(key, default)) + + def set(self, key, value): + self.settings[key] = value + self.save_settings() + + def __getattr__(self, name): + if name.startswith("get_"): + key = name[4:] + if key in self.defaults: + return lambda: self.get(key) + + raise AttributeError("\"{}\" object has no attribute \"{}\"".format(type(self).__name__, name)) \ No newline at end of file diff --git a/Scripts/smbios.py b/Scripts/smbios.py index f52b09e7..c35cc6fa 100644 --- a/Scripts/smbios.py +++ b/Scripts/smbios.py @@ -1,9 +1,11 @@ from Scripts.datasets.mac_model_data import mac_devices from Scripts.datasets import kext_data from Scripts.datasets import os_data +from Scripts.custom_dialogs import show_smbios_selection_dialog from Scripts import gathering_files from Scripts import run from Scripts import utils +from Scripts import settings import os import uuid import random @@ -12,10 +14,11 @@ os_name = platform.system() class SMBIOS: - def __init__(self): - self.g = gathering_files.gatheringFiles() - self.run = run.Run().run - self.utils = utils.Utils() + def __init__(self, gathering_files_instance=None, run_instance=None, utils_instance=None, settings_instance=None): + self.g = gathering_files_instance if gathering_files_instance else gathering_files.gatheringFiles() + self.run = run_instance.run if run_instance else run.Run().run + self.utils = utils_instance if utils_instance else utils.Utils() + self.settings = settings_instance if settings_instance else settings.Settings() self.script_dir = os.path.dirname(os.path.realpath(__file__)) def check_macserial(self, retry_count=0): @@ -28,6 +31,7 @@ def check_macserial(self, retry_count=0): elif os_name == "Darwin": macserial_binary = ["macserial"] else: + self.utils.log_message("[SMBIOS] Unknown OS for macserial", level="ERROR") raise Exception("Unknown OS for macserial") for binary in macserial_binary: @@ -36,6 +40,7 @@ def check_macserial(self, retry_count=0): return macserial_path if retry_count >= max_retries: + self.utils.log_message("[SMBIOS] Failed to find macserial after {} attempts".format(max_retries), level="ERROR") raise Exception("Failed to find macserial after {} attempts".format(max_retries)) download_history = self.utils.read_file(self.g.download_history_file) @@ -68,13 +73,21 @@ def generate_smbios(self, smbios_model): else: serial = output[0].splitlines()[0].split(" | ") - return { + smbios_info = { "MLB": "A" + "0"*15 + "Z" if not serial else serial[-1], "ROM": random_mac_address, "SystemProductName": smbios_model, "SystemSerialNumber": "A" + "0"*10 + "9" if not serial else serial[0], "SystemUUID": str(uuid.uuid4()).upper(), } + + self.utils.log_message("[SMBIOS] Generated SMBIOS info: MLB: {}..., ROM: {}..., SystemProductName: {}, SystemSerialNumber: {}..., SystemUUID: {}...".format( + smbios_info["MLB"][:5], + smbios_info["ROM"][:5], + smbios_info["SystemProductName"], + smbios_info["SystemSerialNumber"][:5], + smbios_info["SystemUUID"].split("-")[0]), level="INFO") + return smbios_info def smbios_specific_options(self, hardware_report, smbios_model, macos_version, acpi_patches, kext_maestro): for patch in acpi_patches: @@ -160,79 +173,45 @@ def select_smbios_model(self, hardware_report, macos_version): elif "Ice Lake" in codename: smbios_model = "MacBookAir9,1" + self.utils.log_message("[SMBIOS] Suggested SMBIOS model: {}".format(smbios_model), level="INFO") return smbios_model - - def customize_smbios_model(self, hardware_report, selected_smbios_model, macos_version): - current_category = None + + def customize_smbios_model(self, hardware_report, selected_smbios_model, macos_version, parent=None): default_smbios_model = self.select_smbios_model(hardware_report, macos_version) - show_all_models = False is_laptop = "Laptop" == hardware_report.get("Motherboard").get("Platform") - - while True: - incompatible_models_by_index = [] - contents = [] - contents.append("") - if show_all_models: - contents.append("List of available SMBIOS:") - else: - contents.append("List of compatible SMBIOS:") - for index, device in enumerate(mac_devices, start=1): - isSupported = self.utils.parse_darwin_version(device.initial_support) <= self.utils.parse_darwin_version(macos_version) <= self.utils.parse_darwin_version(device.last_supported_version) - if device.name not in (default_smbios_model, selected_smbios_model) and not show_all_models and (not isSupported or (is_laptop and not device.name.startswith("MacBook")) or (not is_laptop and device.name.startswith("MacBook"))): - incompatible_models_by_index.append(index - 1) - continue - - category = "" - for char in device.name: - if char.isdigit(): - break - category += char - if category != current_category: - current_category = category - category_header = "Category: {}".format(current_category if current_category else "Uncategorized") - contents.append(f"\n{category_header}\n" + "=" * len(category_header)) - checkbox = "[*]" if device.name == selected_smbios_model else "[ ]" + macos_name = os_data.get_macos_name_by_darwin(macos_version) + + items = [] + for index, device in enumerate(mac_devices): + is_supported = self.utils.parse_darwin_version(device.initial_support) <= self.utils.parse_darwin_version(macos_version) <= self.utils.parse_darwin_version(device.last_supported_version) + + platform_match = True + if is_laptop and not device.name.startswith("MacBook"): + platform_match = False + elif not is_laptop and device.name.startswith("MacBook"): + platform_match = False - line = "{} {:2}. {:15} - {:10} {:20}{}".format(checkbox, index, device.name, device.cpu, "({})".format(device.cpu_generation), "" if not device.discrete_gpu else " - {}".format(device.discrete_gpu)) - if device.name == selected_smbios_model: - line = "\033[1;32m{}\033[0m".format(line) - elif not isSupported: - line = "\033[90m{}\033[0m".format(line) - contents.append(line) - contents.append("") - contents.append("\033[1;93mNote:\033[0m") - contents.append("- Lines in gray indicate mac models that are not officially supported by {}.".format(os_data.get_macos_name_by_darwin(macos_version))) - contents.append("") - if not show_all_models: - contents.append("A. Show all models") - else: - contents.append("C. Show compatible models only") - if selected_smbios_model != default_smbios_model: - contents.append("R. Restore default SMBIOS model ({})".format(default_smbios_model)) - contents.append("") - contents.append("B. Back") - contents.append("Q. Quit") - contents.append("") - content = "\n".join(contents) - - self.utils.adjust_window_size(content) - self.utils.head("Customize SMBIOS Model", resize=False) - print(content) - option = self.utils.request_input("Select your option: ") - if option.lower() == "q": - self.utils.exit_program() - if option.lower() == "b": - return selected_smbios_model - if option.lower() == "r" and selected_smbios_model != default_smbios_model: - return default_smbios_model - if option.lower() in ("a", "c"): - show_all_models = not show_all_models - continue - - if option.strip().isdigit(): - index = int(option) - 1 - if index >= 0 and index < len(mac_devices): - if not show_all_models and index in incompatible_models_by_index: - continue - - selected_smbios_model = mac_devices[index].name \ No newline at end of file + is_compatible = is_supported and platform_match + + category = "" + for char in device.name: + if char.isdigit(): + break + category += char + + gpu_str = "" if not device.discrete_gpu else " - {}".format(device.discrete_gpu) + label = "{} - {} ({}){}".format(device.name, device.cpu, device.cpu_generation, gpu_str) + + items.append({ + 'name': device.name, + 'label': label, + 'category': category, + 'is_supported': is_supported, + 'is_compatible': is_compatible + }) + + content = "Lines in gray indicate mac models that are not officially supported by {}.".format(macos_name) + + result = show_smbios_selection_dialog("Customize SMBIOS Model", content, items, selected_smbios_model, default_smbios_model) + + return result if result else selected_smbios_model \ No newline at end of file diff --git a/Scripts/state.py b/Scripts/state.py new file mode 100644 index 00000000..e3f018ae --- /dev/null +++ b/Scripts/state.py @@ -0,0 +1,36 @@ +from dataclasses import dataclass, field +from typing import Optional, Dict, List, Any + + +@dataclass +class HardwareReportState: + report_path: str = "Not selected" + acpi_dir: str = "Not selected" + hardware_report: Optional[Dict[str, Any]] = None + compatibility_error: Optional[str] = None + customized_hardware: Optional[Dict[str, Any]] = None + disabled_devices: Optional[Dict[str, str]] = None + audio_layout_id: Optional[int] = None + audio_controller_properties: Optional[Dict[str, Any]] = None + + +@dataclass +class macOSVersionState: + suggested_version: Optional[str] = None + selected_version_name: str = "Not selected" + darwin_version: str = "" + native_version: Optional[tuple] = None + ocl_patched_version: Optional[tuple] = None + needs_oclp: bool = False + + +@dataclass +class SMBIOSState: + model_name: str = "Not selected" + + +@dataclass +class BuildState: + in_progress: bool = False + successful: bool = False + log_messages: List[str] = field(default_factory=list) \ No newline at end of file diff --git a/Scripts/styles.py b/Scripts/styles.py new file mode 100644 index 00000000..9f5c5b23 --- /dev/null +++ b/Scripts/styles.py @@ -0,0 +1,68 @@ +from typing import Final + + +COLORS: Final[dict[str, str]] = { + "primary": "#0078D4", + "primary_dark": "#005A9E", + "primary_light": "#4CC2FF", + "primary_hover": "#106EBE", + + "bg_main": "#FFFFFF", + "bg_secondary": "#F3F3F3", + "bg_sidebar": "#F7F7F7", + "bg_hover": "#E8E8E8", + "bg_selected": "#0078D4", + "bg_card": "#FAFAFA", + + "text_primary": "#000000", + "text_secondary": "#605E5C", + "text_tertiary": "#8A8886", + "text_sidebar": "#201F1E", + "text_sidebar_selected": "#FFFFFF", + + "success": "#107C10", + "warning": "#FF8C00", + "error": "#E81123", + "info": "#0078D4", + + "note_bg": "#E3F2FD", + "note_border": "#2196F3", + "note_text": "#1565C0", + "warning_bg": "#FFF3E0", + "warning_border": "#FF9800", + "warning_text": "#F57C00", + "success_bg": "#F3FAF3", + + "border": "#D1D1D1", + "border_light": "#EDEBE9", + "border_focus": "#0078D4", +} + +SPACING: Final[dict[str, int]] = { + "tiny": 4, + "small": 8, + "medium": 12, + "large": 16, + "xlarge": 20, + "xxlarge": 24, + "xxxlarge": 32, +} + +SIZES: Final[dict[str, int]] = { + "sidebar_width": 220, + "sidebar_item_height": 40, + "button_height": 32, + "button_padding_x": 16, + "button_padding_y": 6, + "input_height": 32, + "icon_size": 16, +} + +RADIUS: Final[dict[str, int]] = { + "small": 4, + "medium": 6, + "large": 8, + "xlarge": 10, + "button": 4, + "card": 8, +} \ No newline at end of file diff --git a/Scripts/ui_utils.py b/Scripts/ui_utils.py new file mode 100644 index 00000000..6642f1fd --- /dev/null +++ b/Scripts/ui_utils.py @@ -0,0 +1,183 @@ +from typing import Optional, Tuple, TYPE_CHECKING + +from PyQt6.QtWidgets import QWidget, QLabel, QHBoxLayout, QVBoxLayout +from PyQt6.QtCore import Qt +from PyQt6.QtGui import QColor +from qfluentwidgets import FluentIcon, BodyLabel, CardWidget, StrongBodyLabel + +from .styles import SPACING, COLORS, RADIUS + +if TYPE_CHECKING: + from qfluentwidgets import GroupHeaderCardWidget, CardGroupWidget + +class ProgressStatusHelper: + def __init__(self, status_icon_label, progress_label, progress_bar, progress_container): + self.status_icon_label = status_icon_label + self.progress_label = progress_label + self.progress_bar = progress_bar + self.progress_container = progress_container + + def update(self, status, message, progress=None): + icon_size = 28 + icon_map = { + "loading": (FluentIcon.SYNC, COLORS["primary"]), + "success": (FluentIcon.COMPLETED, COLORS["success"]), + "error": (FluentIcon.CLOSE, COLORS["error"]), + "warning": (FluentIcon.INFO, COLORS["warning"]), + } + + if status in icon_map: + icon, color = icon_map[status] + pixmap = icon.icon(color=color).pixmap(icon_size, icon_size) + self.status_icon_label.setPixmap(pixmap) + + self.progress_label.setText(message) + if status == "success": + self.progress_label.setStyleSheet("color: {}; font-size: 15px; font-weight: 600;".format(COLORS["success"])) + elif status == "error": + self.progress_label.setStyleSheet("color: {}; font-size: 15px; font-weight: 600;".format(COLORS["error"])) + elif status == "warning": + self.progress_label.setStyleSheet("color: {}; font-size: 15px; font-weight: 600;".format(COLORS["warning"])) + else: + self.progress_label.setStyleSheet("color: {}; font-size: 15px; font-weight: 600;".format(COLORS["primary"])) + + if progress is not None: + self.progress_bar.setRange(0, 100) + self.progress_bar.setValue(progress) + else: + self.progress_bar.setRange(0, 0) + + self.progress_container.setVisible(True) + +class UIUtils: + def __init__(self): + pass + + def build_icon_label(self, icon: FluentIcon, color: str, size: int = 32) -> QLabel: + label = QLabel() + label.setPixmap(icon.icon(color=color).pixmap(size, size)) + label.setAlignment(Qt.AlignmentFlag.AlignCenter) + label.setFixedSize(size + 12, size + 12) + return label + + def create_info_widget(self, text: str, color: Optional[str] = None) -> QWidget: + if not text: + return QWidget() + + label = BodyLabel(text) + label.setWordWrap(True) + if color: + label.setStyleSheet("color: {};".format(color)) + return label + + def colored_icon(self, icon: FluentIcon, color_hex: str) -> FluentIcon: + if not icon or not color_hex: + return icon + + tint = QColor(color_hex) + return icon.colored(tint, tint) + + def get_compatibility_icon(self, compat_tuple: Optional[Tuple[Optional[str], Optional[str]]]) -> FluentIcon: + if not compat_tuple or compat_tuple == (None, None): + return self.colored_icon(FluentIcon.CLOSE, COLORS["error"]) + return self.colored_icon(FluentIcon.ACCEPT, COLORS["success"]) + + def add_group_with_indent(self, card: "GroupHeaderCardWidget", icon: FluentIcon, title: str, content: str, widget: Optional[QWidget] = None, indent_level: int = 0) -> "CardGroupWidget": + if widget is None: + widget = QWidget() + + group = card.addGroup(icon, title, content, widget) + + if indent_level > 0: + base_margin = 24 + indent = 20 * indent_level + group.hBoxLayout.setContentsMargins(base_margin + indent, 10, 24, 10) + + return group + + def create_step_indicator(self, step_number: int, total_steps: int = 4, color: str = "#0078D4") -> BodyLabel: + label = BodyLabel("STEP {} OF {}".format(step_number, total_steps)) + label.setStyleSheet("color: {}; font-weight: bold;".format(color)) + return label + + def create_vertical_spacer(self, spacing: int = SPACING["medium"]) -> QWidget: + spacer = QWidget() + spacer.setFixedHeight(spacing) + return spacer + + def custom_card(self, card_type: str = "note", icon: Optional[FluentIcon] = None, title: str = "", body: str = "", custom_widget: Optional[QWidget] = None, parent: Optional[QWidget] = None) -> CardWidget: + card_styles = { + "note": { + "bg": COLORS["note_bg"], + "text": COLORS["note_text"], + "border": "rgba(21, 101, 192, 0.2)", + "default_icon": FluentIcon.INFO + }, + "warning": { + "bg": COLORS["warning_bg"], + "text": COLORS["warning_text"], + "border": "rgba(245, 124, 0, 0.25)", + "default_icon": FluentIcon.MEGAPHONE + }, + "success": { + "bg": COLORS["success_bg"], + "text": COLORS["success"], + "border": "rgba(16, 124, 16, 0.2)", + "default_icon": FluentIcon.COMPLETED + }, + "error": { + "bg": "#FFEBEE", + "text": COLORS["error"], + "border": "rgba(232, 17, 35, 0.25)", + "default_icon": FluentIcon.CLOSE + }, + "info": { + "bg": COLORS["note_bg"], + "text": COLORS["info"], + "border": "rgba(0, 120, 212, 0.2)", + "default_icon": FluentIcon.INFO + } + } + + style = card_styles.get(card_type, card_styles["note"]) + + if icon is None: + icon = style["default_icon"] + + card = CardWidget(parent) + card.setStyleSheet(f""" + CardWidget {{ + background-color: {style["bg"]}; + border: 1px solid {style["border"]}; + border-radius: {RADIUS["card"]}px; + }} + """) + + main_layout = QHBoxLayout(card) + main_layout.setContentsMargins(SPACING["large"], SPACING["large"], SPACING["large"], SPACING["large"]) + main_layout.setSpacing(SPACING["large"]) + + icon_label = self.build_icon_label(icon, style["text"], size=40) + main_layout.addWidget(icon_label, 0, Qt.AlignmentFlag.AlignVCenter) + + text_layout = QVBoxLayout() + text_layout.setSpacing(SPACING["small"]) + + if title: + title_label = StrongBodyLabel(title) + title_label.setStyleSheet("color: {}; font-size: 16px;".format(style["text"])) + text_layout.addWidget(title_label) + + if body: + body_label = BodyLabel(body) + body_label.setWordWrap(True) + body_label.setOpenExternalLinks(True) + body_label.setStyleSheet("color: #424242; line-height: 1.6;") + text_layout.addWidget(body_label) + + if custom_widget: + text_layout.addWidget(custom_widget) + + main_layout.addLayout(text_layout) + + return card \ No newline at end of file diff --git a/Scripts/utils.py b/Scripts/utils.py index fc9f6ff7..433be71f 100644 --- a/Scripts/utils.py +++ b/Scripts/utils.py @@ -1,18 +1,38 @@ import os -import sys import json import plistlib import shutil import re import binascii import subprocess -import pathlib import zipfile import tempfile +import traceback +import contextlib +import logging class Utils: - def __init__(self, script_name = "OpCore Simplify"): - self.script_name = script_name + def __init__(self): + self.gui_handler = None + self.logger = logging.getLogger("OpCoreSimplify") + + @contextlib.contextmanager + def safe_block(self, task_name="Operation", suppress_error=True): + try: + yield + except Exception as e: + error_details = "".join(traceback.format_exc()) + self.log_message("Error during '{}': {}\n{}".format(task_name, str(e), error_details), level="ERROR") + if not suppress_error: + raise + + def log_message(self, message, level="INFO", to_build_log=False): + log_level = getattr(logging, level.upper(), logging.INFO) + + extra = {'to_build_log': to_build_log} + + self.logger.log(log_level, message, extra=extra) + return True def clean_temporary_dir(self): temporary_dir = tempfile.gettempdir() @@ -26,6 +46,7 @@ def clean_temporary_dir(self): try: shutil.rmtree(os.path.join(temporary_dir, file)) except Exception as e: + self.log_message("[UTILS] Failed to remove temp directory {}: {}".format(file, e), "Error") pass def get_temporary_dir(self): @@ -127,23 +148,6 @@ def extract_zip_file(self, zip_path, extraction_directory=None): def contains_any(self, data, search_item, start=0, end=None): return next((item for item in data[start:end] if item.lower() in search_item.lower()), None) - - def normalize_path(self, path): - path = re.sub(r'^[\'"]+|[\'"]+$', '', path) - - path = path.strip() - - path = os.path.expanduser(path) - - if os.name == 'nt': - path = path.replace('\\', '/') - path = re.sub(r'/+', '/', path) - else: - path = path.replace('\\', '') - - path = os.path.normpath(path) - - return str(pathlib.Path(path).resolve()) def parse_darwin_version(self, darwin_version): major, minor, patch = map(int, darwin_version.split('.')) @@ -156,78 +160,4 @@ def open_folder(self, folder_path): else: subprocess.run(['xdg-open', folder_path]) elif os.name == 'nt': - os.startfile(folder_path) - - def request_input(self, prompt="Press Enter to continue..."): - if sys.version_info[0] < 3: - user_response = raw_input(prompt) - else: - user_response = input(prompt) - - if not isinstance(user_response, str): - user_response = str(user_response) - - return user_response - - def progress_bar(self, title, steps, current_step_index, done=False): - self.head(title) - print("") - if done: - for step in steps: - print(" [\033[92m✓\033[0m] {}".format(step)) - else: - for i, step in enumerate(steps): - if i < current_step_index: - print(" [\033[92m✓\033[0m] {}".format(step)) - elif i == current_step_index: - print(" [\033[1;93m>\033[0m] {}...".format(step)) - else: - print(" [ ] {}".format(step)) - print("") - - def head(self, text = None, width = 68, resize=True): - if resize: - self.adjust_window_size() - os.system('cls' if os.name=='nt' else 'clear') - if text == None: - text = self.script_name - separator = "═" * (width - 2) - title = " {} ".format(text) - if len(title) > width - 2: - title = title[:width-4] + "..." - title = title.center(width - 2) - - print("╔{}╗\n║{}║\n╚{}╝".format(separator, title, separator)) - - def adjust_window_size(self, content=""): - lines = content.splitlines() - rows = len(lines) - cols = max(len(line) for line in lines) if lines else 0 - print('\033[8;{};{}t'.format(max(rows+6, 30), max(cols+2, 100))) - - def exit_program(self): - self.head() - width = 68 - print("") - print("For more information, to report errors, or to contribute to the product:".center(width)) - print("") - - separator = "─" * (width - 4) - print(f" ┌{separator}┐ ") - - contacts = { - "Facebook": "https://www.facebook.com/macforce2601", - "Telegram": "https://t.me/lzhoang2601", - "GitHub": "https://github.com/lzhoang2801/OpCore-Simplify" - } - - for platform, link in contacts.items(): - line = f" * {platform}: {link}" - print(f" │{line.ljust(width - 4)}│ ") - - print(f" └{separator}┘ ") - print("") - print("Thank you for using our program!".center(width)) - print("") - self.request_input("Press Enter to exit.".center(width)) - sys.exit(0) \ No newline at end of file + os.startfile(folder_path) \ No newline at end of file diff --git a/Scripts/value_formatters.py b/Scripts/value_formatters.py new file mode 100644 index 00000000..4400a8dd --- /dev/null +++ b/Scripts/value_formatters.py @@ -0,0 +1,29 @@ +def format_value(value): + if value is None: + return "None" + elif isinstance(value, bool): + return "True" if value else "False" + elif isinstance(value, (bytes, bytearray)): + return value.hex().upper() + elif isinstance(value, str): + return value + + return str(value) + +def get_value_type(value): + if value is None: + return None + elif isinstance(value, dict): + return "Dictionary" + elif isinstance(value, list): + return "Array" + elif isinstance(value, (bytes, bytearray)): + return "Data" + elif isinstance(value, bool): + return "Boolean" + elif isinstance(value, (int, float)): + return "Number" + elif isinstance(value, str): + return "String" + + return "String" \ No newline at end of file diff --git a/Scripts/widgets/config_editor.py b/Scripts/widgets/config_editor.py new file mode 100644 index 00000000..42b628d4 --- /dev/null +++ b/Scripts/widgets/config_editor.py @@ -0,0 +1,310 @@ +from PyQt6.QtWidgets import QWidget, QVBoxLayout, QTreeWidgetItem, QHeaderView, QAbstractItemView +from PyQt6.QtCore import Qt, pyqtSignal, QTimer +from PyQt6.QtGui import QBrush, QColor + +from qfluentwidgets import CardWidget, TreeWidget, BodyLabel, StrongBodyLabel + +from Scripts.datasets.config_tooltips import get_tooltip +from Scripts.value_formatters import format_value, get_value_type +from Scripts.styles import SPACING, COLORS, RADIUS + + +class ConfigEditor(QWidget): + config_changed = pyqtSignal(dict) + + def __init__(self, parent=None): + super().__init__(parent) + self.setObjectName("configEditor") + + self.original_config = None + self.modified_config = None + self.context = {} + + self.mainLayout = QVBoxLayout(self) + + self._init_ui() + + def _init_ui(self): + self.mainLayout.setContentsMargins(0, 0, 0, 0) + self.mainLayout.setSpacing(0) + + card = CardWidget() + card.setBorderRadius(RADIUS["card"]) + card_layout = QVBoxLayout(card) + card_layout.setContentsMargins(SPACING["large"], SPACING["large"], SPACING["large"], SPACING["large"]) + card_layout.setSpacing(SPACING["medium"]) + + title = StrongBodyLabel("Config Editor") + card_layout.addWidget(title) + + description = BodyLabel("View differences between original and modified config.plist") + description.setStyleSheet("color: {}; font-size: 13px;".format(COLORS["text_secondary"])) + card_layout.addWidget(description) + + self.tree = TreeWidget() + self.tree.setHeaderLabels(["Key", "", "Original", "Modified"]) + self.tree.setColumnCount(4) + self.tree.setRootIsDecorated(True) + self.tree.setItemsExpandable(True) + self.tree.setExpandsOnDoubleClick(False) + self.tree.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers) + self.tree.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection) + self.tree.itemExpanded.connect(self._update_tree_height) + self.tree.itemCollapsed.connect(self._update_tree_height) + + header = self.tree.header() + header.setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents) + header.setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents) + header.setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch) + header.setSectionResizeMode(3, QHeaderView.ResizeMode.Stretch) + + self.tree.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) + + card_layout.addWidget(self.tree) + self.mainLayout.addWidget(card) + + def load_configs(self, original, modified, context=None): + self.original_config = original + self.modified_config = modified + self.context = context or {} + + self.tree.clear() + + self._render_config(self.original_config, self.modified_config, []) + + QTimer.singleShot(0, self._update_tree_height) + + def _get_all_keys_from_both_configs(self, original, modified): + original_keys = set(original.keys()) if isinstance(original, dict) else set() + modified_keys = set(modified.keys()) if isinstance(modified, dict) else set() + return sorted(original_keys | modified_keys) + + def _determine_change_type(self, original_value, modified_value, key_in_original, key_in_modified): + if not key_in_original: + return "added" + elif not key_in_modified: + return "removed" + elif original_value != modified_value: + return "modified" + return None + + def _get_effective_value(self, original_value, modified_value): + return modified_value if modified_value is not None else original_value + + def _get_safe_value(self, value, default = None): + return value if value is not None else default + + def _set_value_columns(self, item, original_value, modified_value, change_type, is_dict=False, is_array=False): + if original_value is not None: + if is_dict: + item.setText(2, "".format(len(original_value))) + elif is_array: + item.setText(2, "".format(len(original_value))) + else: + item.setText(2, format_value(original_value)) + item.setData(2, Qt.ItemDataRole.UserRole, get_value_type(original_value)) + else: + item.setText(2, "") + + if change_type is not None and modified_value is not None: + if is_dict: + item.setText(3, "".format(len(modified_value))) + elif is_array: + item.setText(3, "".format(len(modified_value))) + else: + item.setText(3, format_value(modified_value)) + item.setData(3, Qt.ItemDataRole.UserRole, get_value_type(modified_value)) + else: + item.setText(3, "") + + def _build_path_string(self, path_parts): + if not path_parts: + return "" + return ".".join(path_parts) + + def _build_array_path(self, path_parts, index): + return path_parts + [f"[{index}]"] + + def _render_config(self, original, modified, path_parts, parent_item=None): + if parent_item is None: + parent_item = self.tree.invisibleRootItem() + + all_keys = self._get_all_keys_from_both_configs(original, modified) + + for key in all_keys: + current_path_parts = path_parts + [key] + current_path = self._build_path_string(current_path_parts) + + original_value = original.get(key) if isinstance(original, dict) else None + modified_value = modified.get(key) if isinstance(modified, dict) else None + + key_in_original = key in original if isinstance(original, dict) else False + key_in_modified = key in modified if isinstance(modified, dict) else False + + change_type = self._determine_change_type( + original_value, modified_value, key_in_original, key_in_modified + ) + effective_value = self._get_effective_value(original_value, modified_value) + + if isinstance(effective_value, list): + self._render_array( + original_value if isinstance(original_value, list) else [], + modified_value if isinstance(modified_value, list) else [], + current_path_parts, + parent_item + ) + continue + + item = QTreeWidgetItem(parent_item) + item.setText(0, key) + item.setData(0, Qt.ItemDataRole.UserRole, current_path) + self._apply_highlighting(item, change_type) + self._setup_tooltip(item, current_path, modified_value, original_value) + + if isinstance(effective_value, dict): + item.setData(3, Qt.ItemDataRole.UserRole, "dict") + self._set_value_columns(item, original_value, modified_value, change_type, is_dict=True) + + self._render_config( + self._get_safe_value(original_value, {}), + self._get_safe_value(modified_value, {}), + current_path_parts, + item + ) + else: + self._set_value_columns(item, original_value, modified_value, change_type) + + def _render_array(self, original_array, modified_array, path_parts, parent_item): + change_type = self._determine_change_type( + original_array, modified_array, + original_array is not None, modified_array is not None + ) + + effective_original = self._get_safe_value(original_array, []) + effective_modified = self._get_safe_value(modified_array, []) + + path_string = self._build_path_string(path_parts) + array_key = path_parts[-1] if path_parts else "array" + + item = QTreeWidgetItem(parent_item) + item.setText(0, array_key) + item.setData(0, Qt.ItemDataRole.UserRole, path_string) + item.setData(3, Qt.ItemDataRole.UserRole, "array") + + self._apply_highlighting(item, change_type) + self._setup_tooltip(item, path_string, modified_array, original_array) + self._set_value_columns(item, original_array, modified_array, change_type, is_array=True) + + original_len = len(effective_original) + modified_len = len(effective_modified) + max_len = max(original_len, modified_len) + + for i in range(max_len): + original_element = effective_original[i] if i < original_len else None + modified_element = effective_modified[i] if i < modified_len else None + + element_change_type = self._determine_change_type( + original_element, modified_element, + original_element is not None, modified_element is not None + ) + + effective_element = self._get_effective_value(original_element, modified_element) + + if effective_element is None: + continue + + element_path_parts = self._build_array_path(path_parts, i) + element_path = self._build_path_string(element_path_parts) + + element_item = QTreeWidgetItem(item) + element_item.setText(0, "[{}]".format(i)) + element_item.setData(0, Qt.ItemDataRole.UserRole, element_path) + + self._apply_highlighting(element_item, element_change_type) + self._setup_tooltip(element_item, element_path, modified_element, original_element) + + if isinstance(effective_element, dict): + element_item.setData(3, Qt.ItemDataRole.UserRole, "dict") + self._set_value_columns(element_item, original_element, modified_element, element_change_type, is_dict=True) + + self._render_config( + self._get_safe_value(original_element, {}), + self._get_safe_value(modified_element, {}), + element_path_parts, + element_item + ) + elif isinstance(effective_element, list): + element_item.setData(3, Qt.ItemDataRole.UserRole, "array") + self._set_value_columns(element_item, original_element, modified_element, element_change_type, is_array=True) + + self._render_array( + self._get_safe_value(original_element, []), + self._get_safe_value(modified_element, []), + element_path_parts, + element_item + ) + else: + self._set_value_columns(element_item, original_element, modified_element, element_change_type) + + def _apply_highlighting(self, item, change_type=None): + if change_type == "added": + color = "#E3F2FD" + status_text = "A" + elif change_type == "removed": + color = "#FFEBEE" + status_text = "R" + elif change_type == "modified": + color = "#FFF9C4" + status_text = "M" + else: + color = None + status_text = "" + + item.setText(1, status_text) + + if color: + brush = QBrush(QColor(color)) + else: + brush = QBrush() + + for col in range(4): + item.setBackground(col, brush) + + def _setup_tooltip(self, item, key_path, value, original_value=None): + tooltip_text = get_tooltip(key_path, value, original_value, self.context) + item.setToolTip(0, tooltip_text) + + def _calculate_tree_height(self): + if self.tree.topLevelItemCount() == 0: + return self.tree.header().height() if self.tree.header().isVisible() else 0 + + header_height = self.tree.header().height() if self.tree.header().isVisible() else 0 + + first_item = self.tree.topLevelItem(0) + row_height = 24 + if first_item: + rect = self.tree.visualItemRect(first_item) + if rect.height() > 0: + row_height = rect.height() + else: + font_metrics = self.tree.fontMetrics() + row_height = font_metrics.height() + 6 + + def count_visible_rows(item): + count = 1 + if item.isExpanded(): + for i in range(item.childCount()): + count += count_visible_rows(item.child(i)) + return count + + total_rows = 0 + for i in range(self.tree.topLevelItemCount()): + total_rows += count_visible_rows(self.tree.topLevelItem(i)) + + padding = 10 + return header_height + (total_rows * row_height) + padding + + def _update_tree_height(self): + height = self._calculate_tree_height() + if height > 0: + self.tree.setFixedHeight(height) \ No newline at end of file diff --git a/Scripts/wifi_profile_extractor.py b/Scripts/wifi_profile_extractor.py index 82b9726b..d7a734ba 100644 --- a/Scripts/wifi_profile_extractor.py +++ b/Scripts/wifi_profile_extractor.py @@ -1,14 +1,15 @@ from Scripts import run from Scripts import utils +from Scripts.custom_dialogs import ask_network_count, show_info, show_confirmation import platform import json os_name = platform.system() class WifiProfileExtractor: - def __init__(self): - self.run = run.Run().run - self.utils = utils.Utils() + def __init__(self, run_instance=None, utils_instance=None): + self.run = run_instance.run if run_instance else run.Run().run + self.utils = utils_instance if utils_instance else utils.Utils() def get_authentication_type(self, authentication_type): authentication_type = authentication_type.lower() @@ -27,14 +28,16 @@ def get_authentication_type(self, authentication_type): return None def validate_wifi_password(self, authentication_type=None, password=None): - print("Validating password with authentication type: {}".format(authentication_type)) - if password is None: + self.utils.log_message("[WIFI PROFILE EXTRACTOR] Password is not found", level="INFO") return None if authentication_type is None: + self.utils.log_message("[WIFI PROFILE EXTRACTOR] Authentication type is not found", level="INFO") return password + self.utils.log_message("[WIFI PROFILE EXTRACTOR] Validating password for \"{}\" with {} authentication type".format(password, authentication_type), level="INFO") + if authentication_type == "open": return "" @@ -103,30 +106,14 @@ def get_wifi_password_linux(self, ssid): return self.validate_wifi_password(authentication_type, password) def ask_network_count(self, total_networks): - self.utils.head("WiFi Network Retrieval") - print("") - print("Found {} WiFi networks on this device.".format(total_networks)) - print("") - print("How many networks would you like to process?") - print(" 1-{} - Specific number (default: 5)".format(total_networks)) - print(" A - All available networks") - print("") - - num_choice = self.utils.request_input("Enter your choice: ").strip().lower() or "5" + if self.utils.gui_handler: + result = ask_network_count(total_networks) + if result == 'a': + return total_networks + return int(result) - if num_choice == "a": - print("Will process all available networks.") - return total_networks - - try: - max_networks = min(int(num_choice), total_networks) - print("Will process up to {} networks.".format(max_networks)) - return max_networks - except: - max_networks = min(5, total_networks) - print("Invalid choice. Will process up to {} networks.".format(max_networks)) - return max_networks - + return 5 + def process_networks(self, ssid_list, max_networks, get_password_func): networks = [] processed_count = 0 @@ -137,39 +124,35 @@ def process_networks(self, ssid_list, max_networks, get_password_func): ssid = ssid_list[processed_count] try: - print("") - print("Processing {}/{}: {}".format(processed_count + 1, len(ssid_list), ssid)) - if os_name == "Darwin": - print("Please enter your administrator name and password or click 'Deny' to skip this network.") - + self.utils.log_message("[WIFI PROFILE EXTRACTOR] Retrieving password for \"{}\" ({} of {})".format(ssid, processed_count + 1, len(ssid_list)), level="INFO", to_build_log=True) password = get_password_func(ssid) if password is not None: if (ssid, password) not in networks: consecutive_failures = 0 networks.append((ssid, password)) - print("Successfully retrieved password.") + self.utils.log_message("[WIFI PROFILE EXTRACTOR] Successfully retrieved password for \"{}\"".format(ssid), level="INFO", to_build_log=True) if len(networks) == max_networks: break else: + self.utils.log_message("[WIFI PROFILE EXTRACTOR] Could not retrieve password for \"{}\"".format(ssid), level="INFO", to_build_log=True) consecutive_failures += 1 if os_name == "Darwin" else 0 - print("Could not retrieve password for this network.") if consecutive_failures >= max_consecutive_failures: - continue_input = self.utils.request_input("\nUnable to retrieve passwords. Continue trying? (Yes/no): ").strip().lower() or "yes" + result = show_confirmation("WiFi Profile Extractor", "Unable to retrieve passwords. Continue trying?") - if continue_input != "yes": + if not result: break consecutive_failures = 0 except Exception as e: consecutive_failures += 1 if os_name == "Darwin" else 0 - print("Error processing network '{}': {}".format(ssid, str(e))) + self.utils.log_message("[WIFI PROFILE EXTRACTOR] Error processing network \"{}\": {}".format(ssid, str(e)), level="ERROR", to_build_log=True) if consecutive_failures >= max_consecutive_failures: - continue_input = self.utils.request_input("\nUnable to retrieve passwords. Continue trying? (Yes/no): ").strip().lower() or "yes" - - if continue_input != "yes": + result = show_confirmation("WiFi Profile Extractor", "Unable to retrieve passwords. Continue trying?") + + if not result: break consecutive_failures = 0 @@ -177,12 +160,11 @@ def process_networks(self, ssid_list, max_networks, get_password_func): processed_count += 1 if processed_count >= max_networks and len(networks) < max_networks and processed_count < len(ssid_list): - continue_input = self.utils.request_input("\nOnly retrieved {}/{} networks. Try more to reach your target? (Yes/no): ".format(len(networks), max_networks)).strip().lower() or "yes" - - if continue_input != "yes": - break - consecutive_failures = 0 + result = show_confirmation("WiFi Profile Extractor", "Only retrieved {}/{} networks. Try more to reach your target?".format(len(networks), max_networks)) + + if not result: + break return networks @@ -201,10 +183,12 @@ def get_preferred_networks_macos(self, interface): max_networks = self.ask_network_count(len(ssid_list)) - self.utils.head("Administrator Authentication Required") - print("") - print("To retrieve WiFi passwords from the Keychain, macOS will prompt") - print("you for administrator credentials for each WiFi network.") + if self.utils.gui_handler: + content = ( + "To retrieve WiFi passwords from the Keychain, macOS will prompt
" + "you for administrator credentials for each WiFi network." + ) + show_info("Administrator Authentication Required", content) return self.process_networks(ssid_list, max_networks, self.get_wifi_password_macos) @@ -232,9 +216,7 @@ def get_preferred_networks_windows(self): max_networks = len(ssid_list) - self.utils.head("WiFi Profile Extractor") - print("") - print("Retrieving passwords for {} network(s)...".format(len(ssid_list))) + self.utils.log_message("[WIFI PROFILE EXTRACTOR] Retrieving passwords for {} network(s)".format(len(ssid_list)), level="INFO", to_build_log=True) return self.process_networks(ssid_list, max_networks, self.get_wifi_password_windows) @@ -253,9 +235,7 @@ def get_preferred_networks_linux(self): max_networks = len(ssid_list) - self.utils.head("WiFi Profile Extractor") - print("") - print("Retrieving passwords for {} network(s)...".format(len(ssid_list))) + self.utils.log_message("[WIFI PROFILE EXTRACTOR] Retrieving passwords for {} network(s)".format(len(ssid_list)), level="INFO", to_build_log=True) return self.process_networks(ssid_list, max_networks, self.get_wifi_password_linux) @@ -286,31 +266,21 @@ def get_wifi_interfaces(self): return interfaces def get_profiles(self): - os_name = platform.system() - - self.utils.head("WiFi Profile Extractor") - print("") - print("\033[1;93mNote:\033[0m") - print("- When using itlwm kext, WiFi appears as Ethernet in macOS") - print("- You'll need Heliport app to manage WiFi connections in macOS") - print("- This step will enable auto WiFi connections at boot time") - print(" and is useful for users installing macOS via Recovery OS") - print("") + content = ( + "Note:
" + "
    " + "
  • When using itlwm kext, WiFi appears as Ethernet in macOS
  • " + "
  • You'll need Heliport app to manage WiFi connections in macOS
  • " + "
  • This step will enable auto WiFi connections at boot time
    " + "and is useful for users installing macOS via Recovery OS
  • " + "

" + "Would you like to scan for WiFi profiles?" + ) + if not show_confirmation("WiFi Profile Extractor", content): + return [] - while True: - user_input = self.utils.request_input("Would you like to scan for WiFi profiles? (Yes/no): ").strip().lower() - - if user_input == "yes": - break - elif user_input == "no": - return [] - else: - print("\033[91mInvalid selection, please try again.\033[0m\n\n") - profiles = [] - self.utils.head("Detecting WiFi Profiles") - print("") - print("Scanning for WiFi profiles...") + self.utils.log_message("[WIFI PROFILE EXTRACTOR] Detecting WiFi Profiles", level="INFO", to_build_log=True) if os_name == "Windows": profiles = self.get_preferred_networks_windows() @@ -321,31 +291,17 @@ def get_profiles(self): if wifi_interfaces: for interface in wifi_interfaces: - print("Checking interface: {}".format(interface)) + self.utils.log_message("[WIFI PROFILE EXTRACTOR] Checking interface: {}".format(interface), level="INFO", to_build_log=True) interface_profiles = self.get_preferred_networks_macos(interface) if interface_profiles: profiles = interface_profiles break else: - print("No WiFi interfaces detected.") + self.utils.log_message("[WIFI PROFILE EXTRACTOR] No WiFi interfaces detected.", level="INFO", to_build_log=True) if not profiles: - self.utils.head("WiFi Profile Extractor") - print("") - print("No WiFi profiles with saved passwords were found.") - self.utils.request_input() + self.utils.log_message("[WIFI PROFILE EXTRACTOR] No WiFi profiles with saved passwords were found.", level="INFO", to_build_log=True) - self.utils.head("WiFi Profile Extractor") - print("") - print("Found the following WiFi profiles with saved passwords:") - print("") - print("Index SSID Password") - print("-------------------------------------------------------") - for index, (ssid, password) in enumerate(profiles, start=1): - print("{:<6} {:<32} {:<8}".format(index, ssid[:31] + "..." if len(ssid) > 31 else ssid, password[:12] + "..." if len(password) > 12 else password)) - print("") - print("Successfully applied {} WiFi profiles.".format(len(profiles))) - print("") - - self.utils.request_input() + self.utils.log_message("[WIFI PROFILE EXTRACTOR] Successfully applied {} WiFi profiles".format(len(profiles)), level="INFO", to_build_log=True) + return profiles \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..f7e91d60 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +certifi +PyQt6 +pyqt6-sip +PyQt6-Fluent-Widgets \ No newline at end of file diff --git a/updater.py b/updater.py index 32ff2b43..60afae3d 100644 --- a/updater.py +++ b/updater.py @@ -1,201 +1,260 @@ +import os +import tempfile +import shutil +import sys + +from PyQt6.QtCore import QThread, pyqtSignal + from Scripts import resource_fetcher from Scripts import github from Scripts import run from Scripts import utils -import os -import tempfile -import shutil +from Scripts import integrity_checker +from Scripts.custom_dialogs import show_update_dialog, show_info, show_confirmation + +class UpdateCheckerThread(QThread): + update_available = pyqtSignal(dict) + check_failed = pyqtSignal(str) + no_update = pyqtSignal() + + def __init__(self, updater_instance): + super().__init__() + self.updater = updater_instance + + def run(self): + try: + remote_manifest = self.updater.get_remote_manifest() + if not remote_manifest: + self.check_failed.emit("Could not fetch update information from GitHub.\n\nPlease check your internet connection and try again later.") + return + + local_manifest = self.updater.get_local_manifest() + if not local_manifest: + self.check_failed.emit("Could not generate local manifest.\n\nPlease try again later.") + return + + files_to_update = self.updater.compare_manifests(local_manifest, remote_manifest) + if not files_to_update: + self.no_update.emit() + else: + self.update_available.emit(files_to_update) + except Exception as e: + self.check_failed.emit("An error occurred during update check:\n\n{}".format(str(e))) class Updater: - def __init__(self): - self.github = github.Github() - self.fetcher = resource_fetcher.ResourceFetcher() - self.run = run.Run().run - self.utils = utils.Utils() - self.sha_version = os.path.join(os.path.dirname(os.path.realpath(__file__)), "sha_version.txt") + def __init__(self, utils_instance=None, github_instance=None, resource_fetcher_instance=None, run_instance=None, integrity_checker_instance=None): + self.utils = utils_instance if utils_instance else utils.Utils() + self.github = github_instance if github_instance else github.Github(utils_instance=self.utils) + self.fetcher = resource_fetcher_instance if resource_fetcher_instance else resource_fetcher.ResourceFetcher(utils_instance=self.utils) + self.run = run_instance.run if run_instance else run.Run().run + self.integrity_checker = integrity_checker_instance if integrity_checker_instance else integrity_checker.IntegrityChecker(utils_instance=self.utils) + self.remote_manifest_url = "https://nightly.link/lzhoang2801/OpCore-Simplify/workflows/generate-manifest/main/manifest.json.zip" self.download_repo_url = "https://github.com/lzhoang2801/OpCore-Simplify/archive/refs/heads/main.zip" self.temporary_dir = tempfile.mkdtemp() - self.current_step = 0 + self.root_dir = os.path.dirname(os.path.realpath(__file__)) - def get_current_sha_version(self): - print("Checking current version...") + def get_remote_manifest(self, dialog=None): + if dialog: + dialog.update_progress(10, "Fetching remote manifest...") + try: - current_sha_version = self.utils.read_file(self.sha_version) - - if not current_sha_version: - print("SHA version information is missing.") - return "missing_sha_version" + temp_manifest_zip_path = os.path.join(self.temporary_dir, "remote_manifest.json.zip") + success = self.fetcher.download_and_save_file(self.remote_manifest_url, temp_manifest_zip_path) + + if not success or not os.path.exists(temp_manifest_zip_path): + return None - return current_sha_version.decode() + self.utils.extract_zip_file(temp_manifest_zip_path, self.temporary_dir) + + remote_manifest_path = os.path.join(self.temporary_dir, "manifest.json") + manifest_data = self.utils.read_file(remote_manifest_path) + + if dialog: + dialog.update_progress(20, "Manifest downloaded successfully") + + return manifest_data except Exception as e: - print("Error reading current SHA version: {}".format(str(e))) - return "error_reading_sha_version" - - def get_latest_sha_version(self): - print("Fetching latest version from GitHub...") + self.utils.log_message("[UPDATER] Error fetching remote manifest: {}".format(str(e)), level="ERROR") + return None + + def get_local_manifest(self, dialog=None): + if dialog: + dialog.update_progress(40, "Generating local manifest...") + try: - commits = self.github.get_commits("lzhoang2801", "OpCore-Simplify") - return commits["commitGroups"][0]["commits"][0]["oid"] + manifest_data = self.integrity_checker.generate_folder_manifest(self.root_dir, save_manifest=False) + + if dialog: + dialog.update_progress(50, "Local manifest generated") + + return manifest_data except Exception as e: - print("Error fetching latest SHA version: {}".format(str(e))) + self.utils.log_message("[UPDATER] Error generating local manifest: {}".format(str(e)), level="ERROR") + return None + + def compare_manifests(self, local_manifest, remote_manifest): + if not local_manifest or not remote_manifest: + return None + + files_to_update = { + "modified": [], + "missing": [], + "new": [] + } + + local_files = set(local_manifest.keys()) + remote_files = set(remote_manifest.keys()) + + for file_path in local_files & remote_files: + if local_manifest[file_path] != remote_manifest[file_path]: + files_to_update["modified"].append(file_path) + + files_to_update["missing"] = list(remote_files - local_files) + + files_to_update["new"] = list(local_files - remote_files) + + total_changes = len(files_to_update["modified"]) + len(files_to_update["missing"]) + + return files_to_update if total_changes > 0 else None + + def download_update(self, dialog=None): + if dialog: + dialog.update_progress(60, "Creating temporary directory...") - return None - - def download_update(self): - self.current_step += 1 - print("") - print("Step {}: Creating temporary directory...".format(self.current_step)) try: self.utils.create_folder(self.temporary_dir) - print(" Temporary directory created.") - self.current_step += 1 - print("Step {}: Downloading update package...".format(self.current_step)) - print(" ", end="") - file_path = os.path.join(self.temporary_dir, os.path.basename(self.download_repo_url)) - self.fetcher.download_and_save_file(self.download_repo_url, file_path) + if dialog: + dialog.update_progress(65, "Downloading update package...") - if os.path.exists(file_path) and os.path.getsize(file_path) > 0: - print(" Update package downloaded ({:.1f} KB)".format(os.path.getsize(file_path)/1024)) - - self.current_step += 1 - print("Step {}: Extracting files...".format(self.current_step)) - self.utils.extract_zip_file(file_path) - print(" Files extracted successfully") - return True - else: - print(" Download failed or file is empty") + file_path = os.path.join(self.temporary_dir, "update.zip") + success = self.fetcher.download_and_save_file(self.download_repo_url, file_path) + + if not success or not os.path.exists(file_path) or os.path.getsize(file_path) == 0: return False + + if dialog: + dialog.update_progress(75, "Extracting files...") + + self.utils.extract_zip_file(file_path, self.temporary_dir) + + if dialog: + dialog.update_progress(80, "Files extracted successfully") + + return True except Exception as e: - print(" Error during download/extraction: {}".format(str(e))) + self.utils.log_message("[UPDATER] Error during download/extraction: {}".format(str(e)), level="ERROR") return False - - def update_files(self): - self.current_step += 1 - print("Step {}: Updating files...".format(self.current_step)) + + def update_files(self, files_to_update, dialog=None): + if not files_to_update: + return True + try: target_dir = os.path.join(self.temporary_dir, "OpCore-Simplify-main") + if not os.path.exists(target_dir): - target_dir = os.path.join(self.temporary_dir, "main", "OpCore-Simplify-main") - - if not os.path.exists(target_dir): - print(" Could not locate extracted files directory") + self.utils.log_message("[UPDATER] Target directory not found: {}".format(target_dir), level="ERROR") return False - - file_paths = self.utils.find_matching_paths(target_dir, type_filter="file") - total_files = len(file_paths) - print(" Found {} files to update".format(total_files)) + all_files = files_to_update["modified"] + files_to_update["missing"] + total_files = len(all_files) + + if dialog: + dialog.update_progress(85, "Updating {} files...".format(total_files)) updated_count = 0 - for index, (path, type) in enumerate(file_paths, start=1): - source = os.path.join(target_dir, path) - destination = source.replace(target_dir, os.path.dirname(os.path.realpath(__file__))) + for index, relative_path in enumerate(all_files, start=1): + source = os.path.join(target_dir, relative_path) + + if not os.path.exists(source): + self.utils.log_message("[UPDATER] Source file not found: {}".format(source), level="ERROR") + continue + + destination = os.path.join(self.root_dir, relative_path) self.utils.create_folder(os.path.dirname(destination)) - print(" Updating [{}/{}]: {}".format(index, total_files, os.path.basename(path)), end="\r") + self.utils.log_message("[UPDATER] Updating [{}/{}]: {}".format(index, total_files, os.path.basename(relative_path)), level="INFO") + if dialog: + progress = 85 + int((index / total_files) * 10) + dialog.update_progress(progress, "Updating [{}/{}]: {}".format(index, total_files, os.path.basename(relative_path))) try: shutil.move(source, destination) updated_count += 1 - if ".command" in os.path.splitext(path)[-1] and os.name != "nt": + if ".command" in os.path.splitext(relative_path)[-1] and os.name != "nt": self.run({ "args": ["chmod", "+x", destination] }) except Exception as e: - print(" Failed to update {}: {}".format(path, str(e))) + self.utils.log_message("[UPDATER] Failed to update {}: {}".format(relative_path, str(e)), level="ERROR") - print("") - print(" Successfully updated {}/{} files".format(updated_count, total_files)) + if dialog: + dialog.update_progress(95, "Successfully updated {}/{} files".format(updated_count, total_files)) - self.current_step += 1 - print("Step {}: Cleaning up temporary files...".format(self.current_step)) - shutil.rmtree(self.temporary_dir) - print(" Cleanup complete") + if os.path.exists(self.temporary_dir): + shutil.rmtree(self.temporary_dir) + + if dialog: + dialog.update_progress(100, "Update completed!") return True except Exception as e: - print(" Error during file update: {}".format(str(e))) - return False - - def save_latest_sha_version(self, latest_sha): - try: - self.utils.write_file(self.sha_version, latest_sha.encode()) - self.current_step += 1 - print("Step {}: Version information updated.".format(self.current_step)) - return True - except Exception as e: - print("Failed to save version information: {}".format(str(e))) + self.utils.log_message("[UPDATER] Error during file update: {}".format(str(e)), level="ERROR") return False - - def run_update(self): - self.utils.head("Check for Updates") - print("") - - current_sha_version = self.get_current_sha_version() - latest_sha_version = self.get_latest_sha_version() + + def run_update(self): + checker_thread = UpdateCheckerThread(self) - print("") - - if latest_sha_version is None: - print("Could not verify the latest version from GitHub.") - print("Current script SHA version: {}".format(current_sha_version)) - print("Please check your internet connection and try again later.") - print("") - - while True: - user_input = self.utils.request_input("Do you want to skip the update process? (yes/No): ").strip().lower() - if user_input == "yes": - print("") - print("Update process skipped.") - return False - elif user_input == "no": - print("") - print("Continuing with update using default version check...") - latest_sha_version = "update_forced_by_user" - break - else: - print("\033[91mInvalid selection, please try again.\033[0m\n\n") - else: - print("Current script SHA version: {}".format(current_sha_version)) - print("Latest script SHA version: {}".format(latest_sha_version)) - - print("") - - if latest_sha_version != current_sha_version: - print("Update available!") - print("Updating from version {} to {}".format(current_sha_version, latest_sha_version)) - print("") - print("Starting update process...") - - if not self.download_update(): - print("") - print(" Update failed: Could not download or extract update package") - - if os.path.exists(self.temporary_dir): - self.current_step += 1 - print("Step {}: Cleaning up temporary files...".format(self.current_step)) - shutil.rmtree(self.temporary_dir) - print(" Cleanup complete") - + def on_update_available(files_to_update): + checker_thread.quit() + checker_thread.wait() + + if not show_confirmation("An update is available!", "Would you like to update now?", yes_text="Update", no_text="Later"): return False + + dialog = show_update_dialog("Updating", "Starting update process...") + dialog.show() + + try: + if not self.download_update(dialog): + dialog.close() + show_info("Update Failed", "Could not download or extract update package.\n\nPlease check your internet connection and try again.") + return - if not self.update_files(): - print("") - print(" Update failed: Could not update files") - return False + if not self.update_files(files_to_update, dialog): + dialog.close() + show_info("Update Failed", "Could not update files.\n\nPlease try again later.") + return - if not self.save_latest_sha_version(latest_sha_version): - print("") - print(" Update completed but version information could not be saved") - - print("") - print("Update completed successfully!") - print("") - print("The program needs to restart to complete the update process.") - return True - else: - print("You are already using the latest version") - return False \ No newline at end of file + dialog.close() + show_info("Update Complete", "Update completed successfully!\n\nThe program needs to restart to complete the update process.") + + os.execv(sys.executable, ["python3"] + sys.argv) + except Exception as e: + dialog.close() + self.utils.log_message("[UPDATER] Error during update: {}".format(str(e)), level="ERROR") + show_info("Update Error", "An error occurred during the update process:\n\n{}".format(str(e))) + finally: + if os.path.exists(self.temporary_dir): + try: + shutil.rmtree(self.temporary_dir) + except: + pass + + def on_check_failed(error_message): + checker_thread.quit() + checker_thread.wait() + show_info("Update Check Failed", error_message) + + def on_no_update(): + checker_thread.quit() + checker_thread.wait() + + checker_thread.update_available.connect(on_update_available) + checker_thread.check_failed.connect(on_check_failed) + checker_thread.no_update.connect(on_no_update) + + checker_thread.start() \ No newline at end of file