mirror of
				https://github.com/nixietab/picodulce.git
				synced 2025-10-30 21:15:11 +00:00 
			
		
		
		
	Compare commits
	
		
			109 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 2ca02b378c | ||
|   | 3c9634bd24 | ||
|   | 13a167f7fa | ||
|   | 42dfaf8904 | ||
|   | 078518e5ad | ||
|   | 7f0108221b | ||
|   | 16936e3a0d | ||
|   | 8f24903fb3 | ||
| ![github-actions[bot]](/assets/img/avatar_default.png)  | 408afef6f1 | ||
|   | 2b950c3c2d | ||
| ![github-actions[bot]](/assets/img/avatar_default.png)  | 12b8b67bbd | ||
|   | aceb899ea6 | ||
| ![github-actions[bot]](/assets/img/avatar_default.png)  | 5e27a78ded | ||
|   | aaf5ccbc90 | ||
| ![github-actions[bot]](/assets/img/avatar_default.png)  | aaf73533c1 | ||
|   | 8a6246cbbb | ||
| ![github-actions[bot]](/assets/img/avatar_default.png)  | 723cd1dd56 | ||
|   | 1e30d2a164 | ||
| ![github-actions[bot]](/assets/img/avatar_default.png)  | 9523f1ab0c | ||
|   | 103927328a | ||
| ![github-actions[bot]](/assets/img/avatar_default.png)  | faefc09aad | ||
|   | dfc2dac3f5 | ||
| ![github-actions[bot]](/assets/img/avatar_default.png)  | 7942c09082 | ||
|   | d85cfe2de6 | ||
| ![github-actions[bot]](/assets/img/avatar_default.png)  | d145fb2df1 | ||
|   | dedc59d09c | ||
| ![github-actions[bot]](/assets/img/avatar_default.png)  | 32e4783218 | ||
|   | 502e64df83 | ||
| ![github-actions[bot]](/assets/img/avatar_default.png)  | 1cfb6ffcb6 | ||
|   | 77291ad89e | ||
| ![github-actions[bot]](/assets/img/avatar_default.png)  | 3baf6e0b1d | ||
|   | 0768897706 | ||
| ![github-actions[bot]](/assets/img/avatar_default.png)  | ba8072c669 | ||
|   | 785e9be9f9 | ||
| ![github-actions[bot]](/assets/img/avatar_default.png)  | 0cbd000be4 | ||
|   | 52b635285e | ||
| ![github-actions[bot]](/assets/img/avatar_default.png)  | 67a16c008a | ||
|   | a4bd707461 | ||
| ![github-actions[bot]](/assets/img/avatar_default.png)  | 1b27fffc96 | ||
|   | fade5f86b7 | ||
| ![github-actions[bot]](/assets/img/avatar_default.png)  | 823b438840 | ||
|   | 9a8c3f44d0 | ||
| ![github-actions[bot]](/assets/img/avatar_default.png)  | 6b65fb0d1e | ||
|   | 8247009d60 | ||
| ![github-actions[bot]](/assets/img/avatar_default.png)  | e5c395d031 | ||
|   | 263e6eae07 | ||
| ![github-actions[bot]](/assets/img/avatar_default.png)  | ec99488326 | ||
|   | 61cd427beb | ||
| ![github-actions[bot]](/assets/img/avatar_default.png)  | cb2f5b52b3 | ||
|   | ba40354a5d | ||
| ![github-actions[bot]](/assets/img/avatar_default.png)  | 0c151b058e | ||
|   | fc7f47d273 | ||
| ![github-actions[bot]](/assets/img/avatar_default.png)  | 4f4ff35ee5 | ||
|   | 8b9827b422 | ||
| ![github-actions[bot]](/assets/img/avatar_default.png)  | 892cbc4d07 | ||
|   | f61f15fe7e | ||
| ![github-actions[bot]](/assets/img/avatar_default.png)  | d077a922c0 | ||
|   | 9b70503d26 | ||
| ![github-actions[bot]](/assets/img/avatar_default.png)  | ae9f25a7a8 | ||
|   | 00ed5f97b9 | ||
| ![github-actions[bot]](/assets/img/avatar_default.png)  | 5dbbfd5d87 | ||
|   | 37a1c5b0df | ||
| ![github-actions[bot]](/assets/img/avatar_default.png)  | f2a1989993 | ||
|   | 3d40ce7df3 | ||
| ![github-actions[bot]](/assets/img/avatar_default.png)  | 36ff8896ef | ||
|   | 5f59acf0b4 | ||
| ![github-actions[bot]](/assets/img/avatar_default.png)  | c48a193d9a | ||
|   | 47a843c669 | ||
| ![github-actions[bot]](/assets/img/avatar_default.png)  | 52be28bb6c | ||
| ![github-actions[bot]](/assets/img/avatar_default.png)  | 6522b70066 | ||
|   | 8d486a9af2 | ||
| ![github-actions[bot]](/assets/img/avatar_default.png)  | 97393e4ae7 | ||
|   | e35120bb36 | ||
| ![github-actions[bot]](/assets/img/avatar_default.png)  | f2cfb3ceb3 | ||
|   | 15246cd535 | ||
|   | 3edcd10c12 | ||
|   | 874e513b47 | ||
|   | a10318e00d | ||
|   | 7608b647fe | ||
|   | 60d16326b0 | ||
|   | 0d300f0435 | ||
|   | 3123ed30cf | ||
|   | db41858aae | ||
|   | 8c0a794202 | ||
|   | 514f6427ab | ||
|   | 9920636b9c | ||
|   | 5182e42f81 | ||
|   | d2f3aa6a49 | ||
|   | 8167462838 | ||
|   | 51dc126c8a | ||
|   | 5202f8fb25 | ||
|   | 46d053d952 | ||
|   | 82f44105b5 | ||
|   | fd2e57ffb3 | ||
|   | 372075132f | ||
|   | 9c66a3eeb8 | ||
|   | 27c75d1ef2 | ||
|   | 18692c3abc | ||
|   | c4f662235e | ||
|   | bf30a6ecd6 | ||
|   | f4b0b39090 | ||
|   | e02ca8ab76 | ||
|   | b55ac71db1 | ||
|   | f4eac0f412 | ||
|   | dbca6721be | ||
|   | b0ec76ebc3 | ||
|   | 0028de0d69 | ||
|   | 17f5f2a8d0 | ||
|   | a19b8a1545 | 
							
								
								
									
										69
									
								
								.github/workflows/Build.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								.github/workflows/Build.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,69 @@ | ||||
| name: Version Change Action | ||||
| 
 | ||||
| on: | ||||
|   push: | ||||
|     paths: | ||||
|       - version.json  # Trigger on changes to version.json | ||||
| 
 | ||||
| jobs: | ||||
|   version-release: | ||||
|     runs-on: windows-latest  # Use Windows 10 runner | ||||
| 
 | ||||
|     if: github.actor != 'github-actions[bot]'  # Only run if the actor is not the GitHub Actions bot | ||||
| 
 | ||||
|     steps: | ||||
|     - name: Checkout repository | ||||
|       uses: actions/checkout@v3 | ||||
| 
 | ||||
|     - name: Set up Python | ||||
|       uses: actions/setup-python@v4 | ||||
|       with: | ||||
|         python-version: '3.x'  # Specify the Python version you need | ||||
| 
 | ||||
|     - name: Install dependencies | ||||
|       run: | | ||||
|         python -m pip install --upgrade pip | ||||
|         pip install pyqt5 requests pywin32 pyinstaller pillow  # Install specific dependencies | ||||
| 
 | ||||
|     - name: Create actions-temp folder | ||||
|       run: mkdir actions-temp  # Create the folder called actions-temp | ||||
| 
 | ||||
|     - name: Download picoBuild.py script | ||||
|       run: curl -L -o actions-temp/picoBuild.py https://raw.githubusercontent.com/nixietab/picodulce-build-script/refs/heads/main/picoBuild.py | ||||
| 
 | ||||
|     - name: Run picoBuild.py script | ||||
|       run: python actions-temp/picoBuild.py | ||||
| 
 | ||||
|     - name: Show directory structure | ||||
|       run: |  | ||||
|         dir actions-temp | ||||
|         dir | ||||
| 
 | ||||
|     - name: Get version and name from version.json | ||||
|       id: version_info | ||||
|       run: | | ||||
|         $versionJson = Get-Content version.json | ConvertFrom-Json | ||||
|         echo "RELEASE_NAME=Release $($versionJson.version)" >> $env:GITHUB_ENV | ||||
|         echo "RELEASE_TAG=$($versionJson.version)" >> $env:GITHUB_ENV | ||||
| 
 | ||||
|     - name: Create GitHub Release | ||||
|       id: create_release | ||||
|       uses: actions/create-release@v1 | ||||
|       with: | ||||
|         tag_name: ${{ env.RELEASE_TAG }} | ||||
|         release_name: ${{ env.RELEASE_NAME }} | ||||
|         body: "This release was created automatically by a GitHub Action." | ||||
|         draft: false | ||||
|         prerelease: false | ||||
|       env: | ||||
|         GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||||
| 
 | ||||
|     - name: Upload Release Asset | ||||
|       uses: actions/upload-release-asset@v1 | ||||
|       with: | ||||
|         upload_url: ${{ steps.create_release.outputs.upload_url }} | ||||
|         asset_path: build/2hsu.exe | ||||
|         asset_name: 2hsu.exe | ||||
|         asset_content_type: application/octet-stream | ||||
|       env: | ||||
|         GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||||
							
								
								
									
										76
									
								
								PKGBUILD
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								PKGBUILD
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,76 @@ | ||||
| pkgname=picodulce | ||||
| pkgver=0.13.5 | ||||
| pkgrel=1 | ||||
| pkgdesc="Launcher for Minecraft based on the zucaro library" | ||||
| arch=('x86_64') | ||||
| OPTIONS=(!strip !docs libtool emptydirs) | ||||
| url="https://github.com/nixietab/picodulce" | ||||
| license=('MIT') | ||||
| depends=('python' 'python-virtualenv' 'xdg-utils') | ||||
| makedepends=('git') | ||||
| source=("git+https://github.com/nixietab/picodulce.git") | ||||
| sha256sums=('SKIP') | ||||
| 
 | ||||
| package() { | ||||
|     cd "$srcdir/$pkgname" | ||||
| 
 | ||||
|     # Create a directory for the application in the user's home directory | ||||
|     install -dm755 "$pkgdir/usr/share/$pkgname" | ||||
| 
 | ||||
|     # Copy all project files to the created directory | ||||
|     cp -r . "$pkgdir/usr/share/$pkgname" | ||||
| 
 | ||||
|     # Create a virtual environment | ||||
|     python -m venv "$pkgdir/usr/share/$pkgname/venv" | ||||
| 
 | ||||
|     # Activate the virtual environment and install dependencies | ||||
|     source "$pkgdir/usr/share/$pkgname/venv/bin/activate" | ||||
|     pip install -r requirements.txt | ||||
| 
 | ||||
|     # Create a run.sh script | ||||
|     install -Dm755 /dev/stdin "$pkgdir/usr/share/$pkgname/run.sh" <<EOF | ||||
| #!/bin/bash | ||||
| 
 | ||||
| if [ ! -d "venv" ]; then | ||||
|   echo "venv folder does not exist. Creating virtual environment..." | ||||
|   python3 -m venv venv | ||||
| 
 | ||||
|   source venv/bin/activate | ||||
| 
 | ||||
|   echo "Installing required packages..." | ||||
|   pip install -r requirements.txt | ||||
| else | ||||
|   source venv/bin/activate | ||||
| fi | ||||
| 
 | ||||
| python picodulce.py | ||||
| EOF | ||||
| 
 | ||||
| 
 | ||||
|     # Make the run.sh script executable | ||||
|     chmod +x "$pkgdir/usr/share/$pkgname/run.sh" | ||||
| 
 | ||||
|     # Create a desktop entry for the application | ||||
|     install -Dm644 /dev/stdin "$pkgdir/usr/share/applications/$pkgname.desktop" <<EOF | ||||
| [Desktop Entry] | ||||
| Name=Picodulce | ||||
| Exec=/usr/share/picodulce/run.sh | ||||
| Icon=/usr/share/picodulce/launcher_icon.ico | ||||
| Terminal=true | ||||
| Type=Application | ||||
| Comment=Picodulce Launcher | ||||
| Categories=Game; | ||||
| EOF | ||||
| 
 | ||||
|     # Ensure the normal user has permission to write to the picodulce folder | ||||
|     chown -R "$USER:$USER" "$pkgdir/usr/share/$pkgname" | ||||
|     chmod -R u+w "$pkgdir/usr/share/$pkgname" | ||||
| 
 | ||||
|     #Install into bin | ||||
|     install -Dm755 /dev/stdin "$pkgdir/usr/bin/picodulce" <<EOF | ||||
| #!/bin/bash | ||||
| cd /usr/share/picodulce/ | ||||
| exec ./run.sh | ||||
| EOF | ||||
| } | ||||
| # vim:set ts=2 sw=2 et: | ||||
							
								
								
									
										29
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										29
									
								
								README.md
									
									
									
									
									
								
							| @ -27,7 +27,7 @@ | ||||
| </p> | ||||
| 
 | ||||
| 
 | ||||
|   Picodulce is a feature-rich launcher for Minecraft, developed using Qt5. It serves as a graphical user interface (GUI) for the picomc project, providing users with a seamless experience in managing and launching game versions. | ||||
|   Picodulce is a feature-rich launcher for Minecraft, developed using Qt5. It serves as a graphical user interface (GUI) for the [zucaro backend](https://github.com/nixietab/zucaro), providing users with a seamless experience in managing and launching game versions. | ||||
| 
 | ||||
| 
 | ||||
|  | ||||
| @ -40,14 +40,35 @@ | ||||
| - **Custom Theme Support**: Create and apply personalized themes with ease. A dedicated repository and guide are [available to help you get started.](https://github.com/nixietab/picodulce-themes) | ||||
| 
 | ||||
| # Installation | ||||
| If you are on windows you may be more interested in a [installer](https://github.com/nixietab/2hsu/releases/download/release/2hsu.exe) | ||||
| 
 | ||||
| ## Windows | ||||
| For Windows systems using the [installer](https://github.com/nixietab/picodulce/releases/latest) is recommended | ||||
| 
 | ||||
| # Linux (Generic) | ||||
| We have a install script, to use it run: | ||||
| 
 | ||||
| ~~~ | ||||
| curl -sSL https://raw.githubusercontent.com/nixietab/picodulce/refs/heads/main/install-universal.sh | bash | ||||
| ~~~ | ||||
| 
 | ||||
| ## Arch Linux | ||||
| The package is available in the [AUR](https://aur.archlinux.org/packages/picodulce) as ```picodulce``` | ||||
| 
 | ||||
| For installing on Arch without using an AUR helper a PKGBUILD is provided | ||||
| ``` | ||||
| git clone https://github.com/nixietab/picodulce.git | ||||
| cd picodulce | ||||
| makepkg -si | ||||
| ``` | ||||
| 
 | ||||
| ## Other OS | ||||
| 
 | ||||
| ### 1. Clone the repository | ||||
| 
 | ||||
| ``` git clone https://github.com/nixietab/picodulce ``` | ||||
| 
 | ||||
| ### 2. (Optional) Set Up a Virtual Environment | ||||
| Setting up a virtual environment is recommended to avoid dependency conflicts. Picodulce relies on the path of the `picomc` project, and using a virtual environment helps prevent errors. | ||||
| Setting up a virtual environment is recommended to avoid dependency conflicts. Picodulce relies on the path of the `zucaro` project, and using a virtual environment helps prevent errors. | ||||
| 
 | ||||
| Create the virtual environment: | ||||
| 
 | ||||
| @ -72,7 +93,7 @@ On the venv run it as a normal python script | ||||
| 
 | ||||
| ```python picodulce.py``` | ||||
| 
 | ||||
| Just make sure you have Java installed for running the actual game | ||||
| Just make sure you have Java installed for running the actual game, or check the "manage java" option inside the launcher settings | ||||
| 
 | ||||
| ### About the name | ||||
| The name "Picodulce" comes from a popular Argentinian candy. This reflects the enjoyable and user-friendly experience that the launcher aims to provide, making game management straightforward and pleasant. | ||||
|  | ||||
							
								
								
									
										366
									
								
								authser.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										366
									
								
								authser.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,366 @@ | ||||
| import sys | ||||
| import json | ||||
| import os | ||||
| import uuid | ||||
| import asyncio | ||||
| import aiohttp | ||||
| from datetime import datetime, timezone | ||||
| from pathlib import Path | ||||
| from PyQt5.QtWidgets import (QApplication, QDialog, QLabel, QVBoxLayout,  | ||||
|                            QPushButton, QLineEdit, QMessageBox) | ||||
| from PyQt5.QtCore import QThread, pyqtSignal, Qt, QUrl, QObject | ||||
| from PyQt5.QtGui import QDesktopServices | ||||
| from zucaro.logging import logger | ||||
| from zucaro.launcher import get_default_root, Launcher | ||||
| 
 | ||||
| # Constants for Microsoft Authentication | ||||
| URL_DEVICE_AUTH = "https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode" | ||||
| URL_TOKEN = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token" | ||||
| URL_XBL = "https://user.auth.xboxlive.com/user/authenticate" | ||||
| URL_XSTS = "https://xsts.auth.xboxlive.com/xsts/authorize" | ||||
| URL_MC = "https://api.minecraftservices.com/authentication/login_with_xbox" | ||||
| URL_PROFILE = "https://api.minecraftservices.com/minecraft/profile" | ||||
| 
 | ||||
| CLIENT_ID = "c52aed44-3b4d-4215-99c5-824033d2bc0f" | ||||
| SCOPE = "XboxLive.signin offline_access" | ||||
| GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code" | ||||
| 
 | ||||
| class AuthDialog(QDialog): | ||||
|     def __init__(self, url, code, parent=None, error_mode=False): | ||||
|         super().__init__(parent) | ||||
|         self.setWindowTitle("Microsoft Authentication") | ||||
|         self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint) | ||||
|         self.setModal(True) | ||||
|         self.setup_ui(url, code, error_mode) | ||||
|          | ||||
|     def setup_ui(self, url, code, error_mode): | ||||
|         layout = QVBoxLayout(self) | ||||
|                  | ||||
|         if error_mode: | ||||
|             error_label = QLabel("Error in Login - Please try again") | ||||
|             error_label.setStyleSheet("QLabel { color: red; font-weight: bold; }") | ||||
|             layout.addWidget(error_label) | ||||
| 
 | ||||
|         instructions = QLabel( | ||||
|             "To authenticate your Microsoft Account:\n\n" | ||||
|             "1. Click 'Open Authentication Page' or visit:\n" | ||||
|             "2. Copy the code below\n" | ||||
|             "3. Paste the code on the Microsoft website\n" | ||||
|             "4. After completing authentication, click 'I've Completed Authentication'" | ||||
|         ) | ||||
|         instructions.setWordWrap(True) | ||||
|         layout.addWidget(instructions) | ||||
| 
 | ||||
|         url_label = QLabel(url) | ||||
|         url_label.setTextInteractionFlags(Qt.TextSelectableByMouse) | ||||
|         url_label.setWordWrap(True) | ||||
|         layout.addWidget(url_label) | ||||
|          | ||||
|         self.code_input = QLineEdit(code) | ||||
|         self.code_input.setReadOnly(True) | ||||
|         self.code_input.setAlignment(Qt.AlignCenter) | ||||
|         self.code_input.setStyleSheet(""" | ||||
|             QLineEdit { | ||||
|                 font-size: 16pt; | ||||
|                 font-weight: bold; | ||||
|                 padding: 5px; | ||||
|             } | ||||
|         """) | ||||
|         layout.addWidget(self.code_input) | ||||
|          | ||||
|         copy_button = QPushButton("Copy Code") | ||||
|         copy_button.clicked.connect(self.copy_code) | ||||
|         layout.addWidget(copy_button) | ||||
|          | ||||
|         open_url_button = QPushButton("Open Authentication Page") | ||||
|         open_url_button.clicked.connect(lambda: self.open_url(url)) | ||||
|         layout.addWidget(open_url_button) | ||||
|          | ||||
|         continue_button = QPushButton("I've Completed Authentication") | ||||
|         continue_button.clicked.connect(self.accept) | ||||
|         layout.addWidget(continue_button) | ||||
| 
 | ||||
|     def copy_code(self): | ||||
|         clipboard = QApplication.clipboard() | ||||
|         clipboard.setText(self.code_input.text()) | ||||
| 
 | ||||
|     def open_url(self, url): | ||||
|         QDesktopServices.openUrl(QUrl(url)) | ||||
| 
 | ||||
| class AuthenticationThread(QThread): | ||||
|     auth_data_received = pyqtSignal(dict) | ||||
|     error_occurred = pyqtSignal(str) | ||||
|     auth_error_detected = pyqtSignal(str) | ||||
|     finished = pyqtSignal() | ||||
|     access_token_received = pyqtSignal(dict) | ||||
|      | ||||
|     def __init__(self, username): | ||||
|         super().__init__() | ||||
|         self.username = username | ||||
|         self.device_code = None | ||||
|         self.is_running = True | ||||
| 
 | ||||
|     async def _ms_oauth(self): | ||||
|         data = {"client_id": CLIENT_ID, "scope": SCOPE} | ||||
|          | ||||
|         async with aiohttp.ClientSession() as session: | ||||
|             async with session.post(URL_DEVICE_AUTH, data=data) as resp: | ||||
|                 if resp.status != 200: | ||||
|                     raise Exception(f"Failed to get device code: {await resp.text()}") | ||||
|                 j = await resp.json() | ||||
|                 self.device_code = j["device_code"] | ||||
|                 self.auth_data_received.emit({ | ||||
|                     'url': j["verification_uri"], | ||||
|                     'code': j["user_code"] | ||||
|                 }) | ||||
| 
 | ||||
|             while self.is_running: | ||||
|                 data = { | ||||
|                     "grant_type": GRANT_TYPE, | ||||
|                     "client_id": CLIENT_ID, | ||||
|                     "device_code": self.device_code | ||||
|                 } | ||||
|                  | ||||
|                 async with session.post(URL_TOKEN, data=data) as resp: | ||||
|                     j = await resp.json() | ||||
|                     if resp.status == 400: | ||||
|                         if j["error"] == "authorization_pending": | ||||
|                             await asyncio.sleep(2) | ||||
|                             continue | ||||
|                         else: | ||||
|                             raise Exception(j["error_description"]) | ||||
|                     elif resp.status != 200: | ||||
|                         raise Exception(f"Token request failed: {j}") | ||||
| 
 | ||||
|                     return j["access_token"], j["refresh_token"] | ||||
| 
 | ||||
|     async def _xbl_auth(self, access_token): | ||||
|         data = { | ||||
|             "Properties": { | ||||
|                 "AuthMethod": "RPS", | ||||
|                 "SiteName": "user.auth.xboxlive.com", | ||||
|                 "RpsTicket": f"d={access_token}" | ||||
|             }, | ||||
|             "RelyingParty": "http://auth.xboxlive.com", | ||||
|             "TokenType": "JWT" | ||||
|         } | ||||
| 
 | ||||
|         async with aiohttp.ClientSession() as session: | ||||
|             async with session.post(URL_XBL, json=data) as resp: | ||||
|                 if resp.status != 200: | ||||
|                     raise Exception(f"XBL auth failed: {await resp.text()}") | ||||
|                 j = await resp.json() | ||||
|                 return j["Token"], j["DisplayClaims"]["xui"][0]["uhs"] | ||||
| 
 | ||||
|     async def _xsts_auth(self, xbl_token): | ||||
|         data = { | ||||
|             "Properties": { | ||||
|                 "SandboxId": "RETAIL", | ||||
|                 "UserTokens": [xbl_token] | ||||
|             }, | ||||
|             "RelyingParty": "rp://api.minecraftservices.com/", | ||||
|             "TokenType": "JWT" | ||||
|         } | ||||
| 
 | ||||
|         async with aiohttp.ClientSession() as session: | ||||
|             async with session.post(URL_XSTS, json=data) as resp: | ||||
|                 if resp.status != 200: | ||||
|                     raise Exception(f"XSTS auth failed: {await resp.text()}") | ||||
|                 j = await resp.json() | ||||
|                 return j["Token"] | ||||
| 
 | ||||
|     async def _mc_auth(self, uhs, xsts_token): | ||||
|         data = { | ||||
|             "identityToken": f"XBL3.0 x={uhs};{xsts_token}" | ||||
|         } | ||||
| 
 | ||||
|         async with aiohttp.ClientSession() as session: | ||||
|             async with session.post(URL_MC, json=data) as resp: | ||||
|                 if resp.status != 200: | ||||
|                     raise Exception(f"MC auth failed: {await resp.text()}") | ||||
|                 j = await resp.json() | ||||
|                 return j["access_token"] | ||||
| 
 | ||||
|     async def _get_profile(self, mc_token): | ||||
|         headers = { | ||||
|             "Authorization": f"Bearer {mc_token}" | ||||
|         } | ||||
| 
 | ||||
|         async with aiohttp.ClientSession() as session: | ||||
|             async with session.get(URL_PROFILE, headers=headers) as resp: | ||||
|                 if resp.status != 200: | ||||
|                     raise Exception(f"Profile request failed: {await resp.text()}") | ||||
|                 return await resp.json() | ||||
| 
 | ||||
|     async def _auth_flow(self): | ||||
|         try: | ||||
|             ms_access_token, refresh_token = await self._ms_oauth() | ||||
|             xbl_token, uhs = await self._xbl_auth(ms_access_token) | ||||
|             xsts_token = await self._xsts_auth(xbl_token) | ||||
|             mc_token = await self._mc_auth(uhs, xsts_token) | ||||
|             profile = await self._get_profile(mc_token) | ||||
| 
 | ||||
|             self.access_token_received.emit({ | ||||
|                 'access_token': mc_token, | ||||
|                 'refresh_token': refresh_token, | ||||
|                 'profile': profile | ||||
|             }) | ||||
| 
 | ||||
|         except Exception as e: | ||||
|             self.error_occurred.emit(str(e)) | ||||
| 
 | ||||
|     def run(self): | ||||
|         try: | ||||
|             loop = asyncio.new_event_loop() | ||||
|             asyncio.set_event_loop(loop) | ||||
|             loop.run_until_complete(self._auth_flow()) | ||||
|         except Exception as e: | ||||
|             self.error_occurred.emit(str(e)) | ||||
|         finally: | ||||
|             self.finished.emit() | ||||
| 
 | ||||
|     def stop(self): | ||||
|         self.is_running = False | ||||
| 
 | ||||
| class MinecraftAuthenticator(QObject): | ||||
|     auth_finished = pyqtSignal(bool) | ||||
| 
 | ||||
|     def __init__(self, parent=None): | ||||
|         super().__init__(parent) | ||||
|         self.auth_thread = None | ||||
|         self.auth_dialog = None | ||||
|         self.success = False | ||||
|         self.username = None | ||||
|          | ||||
|         # Initialize the launcher to get the correct config path | ||||
|         with Launcher.new() as launcher: | ||||
|             self.config_path = launcher.root | ||||
| 
 | ||||
|     def authenticate(self, username): | ||||
|         self.username = username | ||||
|         self.success = False | ||||
| 
 | ||||
|         # Create accounts.json if it doesn't exist | ||||
|         if not self.save_to_accounts_json(): | ||||
|             return | ||||
| 
 | ||||
|         self.auth_thread = AuthenticationThread(username) | ||||
|         self.auth_thread.auth_data_received.connect(self.show_auth_dialog) | ||||
|         self.auth_thread.error_occurred.connect(self.show_error) | ||||
|         self.auth_thread.access_token_received.connect(self.on_access_token_received) | ||||
|         self.auth_thread.finished.connect(self.on_authentication_finished) | ||||
|         self.auth_thread.start() | ||||
| 
 | ||||
|     def show_auth_dialog(self, auth_data): | ||||
|         if self.auth_dialog is not None: | ||||
|             self.auth_dialog.close() | ||||
|              | ||||
|         self.auth_dialog = AuthDialog(auth_data['url'], auth_data['code']) | ||||
|          | ||||
|         result = self.auth_dialog.exec_() | ||||
|          | ||||
|         if result != QDialog.Accepted: | ||||
|             self.auth_thread.stop() | ||||
| 
 | ||||
|     def show_error(self, error_msg): | ||||
|         QMessageBox.critical(None, "Error", error_msg) | ||||
|         self.success = False | ||||
|         self.auth_finished.emit(False) | ||||
| 
 | ||||
|     def save_to_accounts_json(self): | ||||
|         try: | ||||
|             accounts_file = Path(self.config_path) / "accounts.json" | ||||
|              | ||||
|             if accounts_file.exists(): | ||||
|                 with open(accounts_file) as f: | ||||
|                     config = json.load(f) | ||||
|             else: | ||||
|                 config = { | ||||
|                     "default": None, | ||||
|                     "accounts": {}, | ||||
|                     "client_token": str(uuid.uuid4()) | ||||
|                 } | ||||
|                 accounts_file.parent.mkdir(parents=True, exist_ok=True) | ||||
|              | ||||
|             # Only create/update if account doesn't exist | ||||
|             if self.username not in config["accounts"]: | ||||
|                 config["accounts"][self.username] = { | ||||
|                     "uuid": "-", | ||||
|                     "online": True, | ||||
|                     "microsoft": True, | ||||
|                     "gname": "-", | ||||
|                     "access_token": "-", | ||||
|                     "refresh_token": "-", | ||||
|                     "is_authenticated": False | ||||
|                 } | ||||
|                  | ||||
|                 # Set as default if no default exists | ||||
|                 if config["default"] is None: | ||||
|                     config["default"] = self.username | ||||
|                  | ||||
|                 with open(accounts_file, 'w') as f: | ||||
|                     json.dump(config, f, indent=4) | ||||
|                      | ||||
|             return True | ||||
| 
 | ||||
|         except Exception as e: | ||||
|             logger.error(f"Failed to initialize account data: {str(e)}") | ||||
|             QMessageBox.critical(None, "Error", f"Failed to initialize account data: {str(e)}") | ||||
|             return False | ||||
| 
 | ||||
|     def on_access_token_received(self, data): | ||||
|         try: | ||||
|             accounts_file = Path(self.config_path) / "accounts.json" | ||||
|              | ||||
|             with open(accounts_file) as f: | ||||
|                 config = json.load(f) | ||||
|              | ||||
|             if self.username in config["accounts"]: | ||||
|                 config["accounts"][self.username].update({ | ||||
|                     "access_token": data['access_token'], | ||||
|                     "refresh_token": data['refresh_token'], | ||||
|                     "uuid": data['profile']['id'], | ||||
|                     "gname": data['profile']['name'], | ||||
|                     "is_authenticated": True | ||||
|                 }) | ||||
|                  | ||||
|                 with open(accounts_file, 'w') as f: | ||||
|                     json.dump(config, f, indent=4) | ||||
|                  | ||||
|                 self.success = True | ||||
|                 QMessageBox.information(None, "Success",  | ||||
|                                       f"Successfully authenticated account: {self.username}") | ||||
|             else: | ||||
|                 raise Exception("Account not found in configuration") | ||||
|                  | ||||
|         except Exception as e: | ||||
|             logger.error(f"Failed to update account data: {str(e)}") | ||||
|             QMessageBox.critical(None, "Error", f"Failed to update account data: {str(e)}") | ||||
|             self.success = False | ||||
|              | ||||
|         self.auth_finished.emit(self.success) | ||||
| 
 | ||||
|     def on_authentication_finished(self): | ||||
|         if self.auth_dialog is not None: | ||||
|             self.auth_dialog.close() | ||||
|             self.auth_dialog = None | ||||
|              | ||||
|         if self.auth_thread: | ||||
|             self.auth_thread.stop() | ||||
|             self.auth_thread = None | ||||
|              | ||||
|         if not self.success: | ||||
|             self.auth_finished.emit(False) | ||||
| 
 | ||||
|     def cleanup(self): | ||||
|         if self.auth_dialog is not None: | ||||
|             self.auth_dialog.close() | ||||
|             self.auth_dialog = None | ||||
|              | ||||
|         if self.auth_thread and self.auth_thread.isRunning(): | ||||
|             self.auth_thread.stop() | ||||
|             self.auth_thread.wait() | ||||
| 
 | ||||
| def create_authenticator(): | ||||
|     """Factory function to create a new MinecraftAuthenticator instance""" | ||||
|     return MinecraftAuthenticator() | ||||
							
								
								
									
										231
									
								
								healthcheck.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										231
									
								
								healthcheck.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,231 @@ | ||||
| import os | ||||
| import json | ||||
| import shutil | ||||
| import modulecli | ||||
| from PyQt5.QtWidgets import QApplication, QDialog, QVBoxLayout, QLabel, QProgressBar | ||||
| from PyQt5.QtCore import Qt, QThread, pyqtSignal | ||||
| import sys | ||||
| 
 | ||||
| class CopyThread(QThread): | ||||
|     progress_changed = pyqtSignal(int) | ||||
|     finished = pyqtSignal() | ||||
| 
 | ||||
|     def __init__(self, src_dir, dst_dir): | ||||
|         super().__init__() | ||||
|         self.src_dir = src_dir | ||||
|         self.dst_dir = dst_dir | ||||
| 
 | ||||
|     def run(self): | ||||
|         # Gather all files recursively | ||||
|         files_to_copy = [] | ||||
|         for root, dirs, files in os.walk(self.src_dir): | ||||
|             for f in files: | ||||
|                 full_path = os.path.join(root, f) | ||||
|                 relative_path = os.path.relpath(full_path, self.src_dir) | ||||
|                 files_to_copy.append(relative_path) | ||||
| 
 | ||||
|         total_files = len(files_to_copy) | ||||
|         copied_files = 0 | ||||
| 
 | ||||
|         for relative_path in files_to_copy: | ||||
|             src_path = os.path.join(self.src_dir, relative_path) | ||||
|             dst_path = os.path.join(self.dst_dir, relative_path) | ||||
|             dst_folder = os.path.dirname(dst_path) | ||||
| 
 | ||||
|             if not os.path.exists(dst_folder): | ||||
|                 try: | ||||
|                     os.makedirs(dst_folder) | ||||
|                 except PermissionError: | ||||
|                     print(f"Skipping folder {dst_folder} (permission denied)") | ||||
|                     continue | ||||
| 
 | ||||
|             try: | ||||
|                 shutil.copy2(src_path, dst_path) | ||||
|             except PermissionError: | ||||
|                 print(f"Skipping file {dst_path} (permission denied)") | ||||
| 
 | ||||
|             copied_files += 1 | ||||
|             progress_percent = int((copied_files / total_files) * 100) | ||||
|             self.progress_changed.emit(progress_percent) | ||||
| 
 | ||||
|         self.finished.emit() | ||||
| 
 | ||||
| 
 | ||||
| class HealthCheck: | ||||
|     def __init__(self): | ||||
|         self.config = None | ||||
| 
 | ||||
|     def check_config_file(self): | ||||
|         config_path = "config.json" | ||||
|         default_config = { | ||||
|             "IsRCPenabled": False, | ||||
|             "CheckUpdate": False, | ||||
|             "IsBleeding": False, | ||||
|             "LastPlayed": "", | ||||
|             "TotalPlaytime": 0, | ||||
|             "IsFirstLaunch": True, | ||||
|             "Instance": "default", | ||||
|             "Theme": "Dark.json", | ||||
|             "ThemeBackground": True, | ||||
|             "ThemeRepository": "https://raw.githubusercontent.com/nixietab/picodulce-themes/main/repo.json", | ||||
|             "Locale": "en", | ||||
|             "ManageJava": False, | ||||
|             "MaxRAM": "2G", | ||||
|             "JavaPath": "", | ||||
|             "ZucaroCheck": False, | ||||
|         } | ||||
| 
 | ||||
|         if not os.path.exists(config_path): | ||||
|             with open(config_path, "w") as config_file: | ||||
|                 json.dump(default_config, config_file, indent=4) | ||||
|             self.config = default_config | ||||
|             return | ||||
| 
 | ||||
|         try: | ||||
|             with open(config_path, "r") as config_file: | ||||
|                 self.config = json.load(config_file) | ||||
|         except (json.JSONDecodeError, ValueError): | ||||
|             with open(config_path, "w") as config_file: | ||||
|                 json.dump(default_config, config_file, indent=4) | ||||
|             self.config = default_config | ||||
|             return | ||||
| 
 | ||||
|         updated = False | ||||
|         for key, value in default_config.items(): | ||||
|             if key not in self.config: | ||||
|                 self.config[key] = value | ||||
|                 updated = True | ||||
| 
 | ||||
|         if updated: | ||||
|             with open(config_path, "w") as config_file: | ||||
|                 json.dump(self.config, config_file, indent=4) | ||||
| 
 | ||||
|     def get_folder_size(self, folder_path): | ||||
|         total_size = 0 | ||||
|         for dirpath, dirnames, filenames in os.walk(folder_path): | ||||
|             for f in filenames: | ||||
|                 fp = os.path.join(dirpath, f) | ||||
|                 if os.path.isfile(fp): | ||||
|                     total_size += os.path.getsize(fp) | ||||
|         return total_size | ||||
| 
 | ||||
|     def zucaro_health_check(self): | ||||
|         if self.config.get("ZucaroCheck"): | ||||
|             return | ||||
| 
 | ||||
|         output = modulecli.run_command("instance dir").strip() | ||||
|         instance_dir = os.path.abspath(output) | ||||
|         base_dir = os.path.abspath(os.path.join(instance_dir, "..", "..")) | ||||
| 
 | ||||
|         possible_zucaro = [os.path.join(base_dir, "zucaro"), os.path.join(base_dir, ".zucaro")] | ||||
|         possible_picomc = [os.path.join(base_dir, "picomc"), os.path.join(base_dir, ".picomc")] | ||||
| 
 | ||||
|         zucaro_dir = next((d for d in possible_zucaro if os.path.exists(d)), None) | ||||
|         picomc_dir = next((d for d in possible_picomc if os.path.exists(d)), None) | ||||
| 
 | ||||
|         if picomc_dir is None or zucaro_dir is None: | ||||
|             print("Required directories not found. Skipping copy.") | ||||
|             # Mark the check as done so it wont run again | ||||
|             self.config["ZucaroCheck"] = True | ||||
|             with open("config.json", "w") as f: | ||||
|                 json.dump(self.config, f, indent=4) | ||||
|             return | ||||
| 
 | ||||
|         picomc_size = self.get_folder_size(picomc_dir) | ||||
|         zucaro_size = self.get_folder_size(zucaro_dir) | ||||
| 
 | ||||
|         if picomc_size <= zucaro_size: | ||||
|             print("No action needed. Zucaro folder is not smaller than Picomc.") | ||||
|             # Update config so the check is considered done | ||||
|             self.config["ZucaroCheck"] = True | ||||
|             with open("config.json", "w") as f: | ||||
|                 json.dump(self.config, f, indent=4) | ||||
|             return | ||||
| 
 | ||||
|         print(f"Copying Picomc ({picomc_size} bytes) to Zucaro ({zucaro_size} bytes)...") | ||||
| 
 | ||||
|         app = QApplication.instance() or QApplication(sys.argv) | ||||
|         dialog = QDialog() | ||||
|         dialog.setWindowTitle("Working...") | ||||
|         dialog.setWindowModality(Qt.ApplicationModal) | ||||
|         layout = QVBoxLayout() | ||||
|         label = QLabel("Working on stuff, please wait...") | ||||
|         progress = QProgressBar() | ||||
|         progress.setValue(0) | ||||
|         layout.addWidget(label) | ||||
|         layout.addWidget(progress) | ||||
|         dialog.setLayout(layout) | ||||
| 
 | ||||
|         # Setup copy thread | ||||
|         thread = CopyThread(picomc_dir, zucaro_dir) | ||||
|         thread.progress_changed.connect(progress.setValue) | ||||
|         thread.finished.connect(dialog.accept) | ||||
|         thread.start() | ||||
| 
 | ||||
|         dialog.exec_()  # Runs the modal event loop | ||||
| 
 | ||||
|         # Mark as done | ||||
|         self.config["ZucaroCheck"] = True | ||||
|         with open("config.json", "w") as f: | ||||
|             json.dump(self.config, f, indent=4) | ||||
| 
 | ||||
|         print("Copy completed.") | ||||
| 
 | ||||
|     def themes_integrity(self): | ||||
|         themes_folder = "themes" | ||||
|         dark_theme_file = os.path.join(themes_folder, "Dark.json") | ||||
|         native_theme_file = os.path.join(themes_folder, "Native.json") | ||||
| 
 | ||||
|         dark_theme_content = { | ||||
|             "manifest": { | ||||
|                 "name": "Dark", | ||||
|                 "description": "The default picodulce launcher theme", | ||||
|                 "author": "Nixietab", | ||||
|                 "license": "MIT" | ||||
|             }, | ||||
|             "palette": { | ||||
|                 "Window": "#353535", | ||||
|                 "WindowText": "#ffffff", | ||||
|                 "Base": "#191919", | ||||
|                 "AlternateBase": "#353535", | ||||
|                 "ToolTipBase": "#ffffff", | ||||
|                 "ToolTipText": "#ffffff", | ||||
|                 "Text": "#ffffff", | ||||
|                 "Button": "#353535", | ||||
|                 "ButtonText": "#ffffff", | ||||
|                 "BrightText": "#ff0000", | ||||
|                 "Link": "#2a82da", | ||||
|                 "Highlight": "#4bb679", | ||||
|                 "HighlightedText": "#ffffff" | ||||
|             }, | ||||
|             "background_image_base64": "" | ||||
|         } | ||||
| 
 | ||||
|         native_theme_content = { | ||||
|             "manifest": { | ||||
|                 "name": "Native", | ||||
|                 "description": "The native looks of your OS", | ||||
|                 "author": "Your Qt Style", | ||||
|                 "license": "Any" | ||||
|             }, | ||||
|             "palette": {} | ||||
|         } | ||||
| 
 | ||||
|         if not os.path.exists(themes_folder): | ||||
|             print(f"Creating folder: {themes_folder}") | ||||
|             os.makedirs(themes_folder) | ||||
| 
 | ||||
|         if not os.path.isfile(dark_theme_file): | ||||
|             print(f"Creating file: {dark_theme_file}") | ||||
|             with open(dark_theme_file, "w", encoding="utf-8") as file: | ||||
|                 json.dump(dark_theme_content, file, indent=2) | ||||
|             print("Dark.json has been created successfully.") | ||||
| 
 | ||||
|         if not os.path.isfile(native_theme_file): | ||||
|             print(f"Creating file: {native_theme_file}") | ||||
|             with open(native_theme_file, "w", encoding="utf-8") as file: | ||||
|                 json.dump(native_theme_content, file, indent=2) | ||||
|             print("Native.json has been created successfully.") | ||||
| 
 | ||||
|         if os.path.isfile(dark_theme_file) and os.path.isfile(native_theme_file): | ||||
|             print("Theme Integrity OK") | ||||
							
								
								
									
										110
									
								
								install-universal.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								install-universal.sh
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,110 @@ | ||||
| #!/bin/bash | ||||
| 
 | ||||
| set -e | ||||
| 
 | ||||
| PICODULCE_DIR="$HOME/.picodulce" | ||||
| GIT_URL="https://github.com/nixietab/picodulce.git" | ||||
| DESKTOP_FILE="$HOME/.local/share/applications/picodulce.desktop" | ||||
| BIN_FILE="/usr/bin/picodulce" | ||||
| 
 | ||||
| # --- Helper functions --- | ||||
| msg() { | ||||
|     echo -e "\033[1;32m$1\033[0m" | ||||
| } | ||||
| 
 | ||||
| err() { | ||||
|     echo -e "\033[1;31m$1\033[0m" >&2 | ||||
|     exit 1 | ||||
| } | ||||
| 
 | ||||
| pause() { | ||||
|     read -rp "Press Enter to continue..." | ||||
| } | ||||
| 
 | ||||
| # --- Check dependencies --- | ||||
| msg "Checking Python3..." | ||||
| if ! command -v python3 >/dev/null; then | ||||
|     err "Python3 is not installed. Please install it first." | ||||
| fi | ||||
| 
 | ||||
| msg "Checking venv module..." | ||||
| if ! python3 -m venv --help >/dev/null 2>&1; then | ||||
|     err "python3-venv is not available. Please install it." | ||||
| fi | ||||
| 
 | ||||
| # --- Clone repo --- | ||||
| msg "Cloning Picodulce repo..." | ||||
| rm -rf "$PICODULCE_DIR" | ||||
| git clone "$GIT_URL" "$PICODULCE_DIR" | ||||
| 
 | ||||
| # --- Create virtual environment --- | ||||
| cd "$PICODULCE_DIR" | ||||
| msg "Creating virtual environment..." | ||||
| python3 -m venv venv | ||||
| source venv/bin/activate | ||||
| pip install -r requirements.txt | ||||
| 
 | ||||
| # --- Create run.sh --- | ||||
| msg "Creating run.sh..." | ||||
| cat > "$PICODULCE_DIR/run.sh" <<'EOF' | ||||
| #!/bin/bash | ||||
| 
 | ||||
| cd "$(dirname "$0")" | ||||
| 
 | ||||
| if [ ! -d "venv" ]; then | ||||
|   echo "venv folder does not exist. Creating virtual environment..." | ||||
|   python3 -m venv venv | ||||
|   source venv/bin/activate | ||||
|   echo "Installing required packages..." | ||||
|   pip install -r requirements.txt | ||||
| else | ||||
|   source venv/bin/activate | ||||
| fi | ||||
| 
 | ||||
| exec python picodulce.py | ||||
| EOF | ||||
| 
 | ||||
| chmod +x "$PICODULCE_DIR/run.sh" | ||||
| 
 | ||||
| # --- Create .desktop entry --- | ||||
| msg "Creating .desktop entry..." | ||||
| mkdir -p "$(dirname "$DESKTOP_FILE")" | ||||
| 
 | ||||
| cat > "$DESKTOP_FILE" <<EOF | ||||
| [Desktop Entry] | ||||
| Name=Picodulce | ||||
| Exec=$PICODULCE_DIR/run.sh | ||||
| Icon=$PICODULCE_DIR/launcher_icon.ico | ||||
| Terminal=true | ||||
| Type=Application | ||||
| Comment=Picodulce Launcher | ||||
| Categories=Game; | ||||
| EOF | ||||
| 
 | ||||
| # --- Ask if install in /usr/bin --- | ||||
| echo | ||||
| read -rp "Do you want to install the "picodulce" command? it requires sudo. (y/n) " choice | ||||
| if [[ "$choice" =~ ^[Yy]$ ]]; then | ||||
|     if [ "$(id -u)" -ne 0 ]; then | ||||
|         echo "Root permissions required to install into /usr/bin" | ||||
|         sudo bash -c "cat > $BIN_FILE" <<EOF | ||||
| #!/bin/bash | ||||
| cd $PICODULCE_DIR | ||||
| exec ./run.sh | ||||
| EOF | ||||
|         sudo chmod +x "$BIN_FILE" | ||||
|     else | ||||
|         cat > "$BIN_FILE" <<EOF | ||||
| #!/bin/bash | ||||
| cd $PICODULCE_DIR | ||||
| exec ./run.sh | ||||
| EOF | ||||
|         chmod +x "$BIN_FILE" | ||||
|     fi | ||||
|     msg "Installed 'picodulce' command in /usr/bin" | ||||
| fi | ||||
| 
 | ||||
| msg "Installation complete!" | ||||
| echo "You can run Picodulce with:" | ||||
| echo "  $PICODULCE_DIR/run.sh" | ||||
| echo "Or from your applications menu." | ||||
							
								
								
									
										46
									
								
								modulecli.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								modulecli.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,46 @@ | ||||
| from io import StringIO | ||||
| import sys | ||||
| import shlex | ||||
| import gc | ||||
| 
 | ||||
| def run_command(command="zucaro"): | ||||
|     # Remove all zucaro-related modules from sys.modules BEFORE import | ||||
|     modules_to_remove = [mod for mod in sys.modules if mod.startswith('zucaro')] | ||||
|     for mod in modules_to_remove: | ||||
|         del sys.modules[mod] | ||||
|     gc.collect() | ||||
| 
 | ||||
|     # Import zucaro_cli dynamically | ||||
|     from zucaro.cli.main import zucaro_cli | ||||
| 
 | ||||
|     # Redirect stdout and stderr to capture the command output | ||||
|     old_stdout, old_stderr = sys.stdout, sys.stderr | ||||
|     sys.stdout = mystdout = StringIO() | ||||
|     sys.stderr = mystderr = StringIO() | ||||
|      | ||||
|     try: | ||||
|         # Use shlex.split to properly parse the command string | ||||
|         # This will call Click's CLI as if from command line, using args | ||||
|         zucaro_cli.main(args=shlex.split(command)) | ||||
|     except SystemExit as e: | ||||
|         if e.code != 0: | ||||
|             print(f"Command exited with code {e.code}", file=sys.stderr) | ||||
|     except Exception as e: | ||||
|         print(f"Unexpected error: {e}", file=sys.stderr) | ||||
|     finally: | ||||
|         # Restore stdout and stderr | ||||
|         sys.stdout = old_stdout | ||||
|         sys.stderr = old_stderr | ||||
| 
 | ||||
|     output = mystdout.getvalue().strip() | ||||
|     error = mystderr.getvalue().strip() | ||||
| 
 | ||||
|     # Cleanup: remove zucaro-related modules from sys.modules and force garbage collection | ||||
|     modules_to_remove = [mod for mod in sys.modules if mod.startswith('zucaro')] | ||||
|     for mod in modules_to_remove: | ||||
|         del sys.modules[mod] | ||||
|     gc.collect() | ||||
|      | ||||
|     if not output: | ||||
|         return f"Error: No output from command. Stderr: {error}" | ||||
|     return output | ||||
							
								
								
									
										1121
									
								
								picodulce.py
									
									
									
									
									
								
							
							
						
						
									
										1121
									
								
								picodulce.py
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -1,4 +1,6 @@ | ||||
| picomc | ||||
| zucaro | ||||
| PyQt5 | ||||
| requests | ||||
| aiohttp | ||||
| pypresence | ||||
| tqdm | ||||
|  | ||||
							
								
								
									
										10
									
								
								version.json
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								version.json
									
									
									
									
									
								
							| @ -1,11 +1,15 @@ | ||||
| { | ||||
|   "version": "0.11.2", | ||||
|   "version": "0.13.6", | ||||
|   "links": [ | ||||
|     "https://raw.githubusercontent.com/nixietab/picodulce/main/version.json", | ||||
|     "https://raw.githubusercontent.com/nixietab/picodulce/main/picodulce.py", | ||||
|     "https://raw.githubusercontent.com/nixietab/picodulce/main/requirements.txt", | ||||
|     "https://raw.githubusercontent.com/nixietab/picodulce/main/drums.gif", | ||||
|     "https://raw.githubusercontent.com/nixietab/picodulce/main/marroc.py", | ||||
|     "https://raw.githubusercontent.com/nixietab/picodulce/main/holiday.ico" | ||||
|   ] | ||||
|     "https://raw.githubusercontent.com/nixietab/picodulce/main/holiday.ico", | ||||
|     "https://raw.githubusercontent.com/nixietab/picodulce/main/authser.py", | ||||
|     "https://raw.githubusercontent.com/nixietab/picodulce/main/healthcheck.py", | ||||
|     "https://raw.githubusercontent.com/nixietab/picodulce/main/modulecli.py" | ||||
|   ], | ||||
|   "versionBleeding": "0.13.3-212" | ||||
| } | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user