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.
- 
+ 
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.
- 
+ 
- 
+4. **Verifying hardware compatibility**:
- 
+ 
-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.
- 
+ 
-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.
- 
-
- 
-
- 
-
-6. **USB Mapping**:
- - After building your EFI, follow the steps for mapping USB ports.
+ 
- 
+ 
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?
"
+ ""
+ "- Enter a number (1-{})
"
+ "- Or select \"Process All\"
"
+ "
"
+ ).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 += "| Category | "
+ content += "Device Name | "
+ content += "Device ID | "
+ 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 += "
"
+ 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 ({}):
"
+ ""
+ ).format(target_darwin_version)
+
+ for index, (kext_name, is_lilu_dependent) in enumerate(incompatible_kexts):
+ content += "- {}. {}".format(index + 1, kext_name)
+ if is_lilu_dependent:
+ content += " - Lilu Plugin"
+ content += "
"
+
+ content += (
+ "
"
+ "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