Compare commits

..

27 Commits
0.12 ... main

Author SHA1 Message Date
github-actions[bot]
32e4783218 Update version.json with commit count
Some checks failed
Bleeding Update version / update-version (push) Has been cancelled
Version Change Action / version-release (push) Has been cancelled
2025-04-07 21:05:29 +00:00
Nix
502e64df83
Update version.json 2025-04-07 18:05:20 -03:00
github-actions[bot]
1cfb6ffcb6 Update version.json with commit count 2025-04-07 20:53:17 +00:00
Nix
77291ad89e
Update version.json 2025-04-07 17:53:08 -03:00
github-actions[bot]
3baf6e0b1d Update version.json with commit count 2025-04-07 20:52:52 +00:00
Nix
0768897706
stoped parsing commands (#12)
* Add files via upload

* auth done even better!
2025-04-07 17:52:43 -03:00
github-actions[bot]
ba8072c669 Update version.json with commit count
Some checks failed
Bleeding Update version / update-version (push) Has been cancelled
Version Change Action / version-release (push) Has been cancelled
2025-03-03 04:12:50 +00:00
Nix
785e9be9f9
Update version.json 2025-03-03 01:12:37 -03:00
github-actions[bot]
0cbd000be4 Update version.json with commit count 2025-03-03 04:11:40 +00:00
Nixietab
52b635285e Moved the health checks to a OOP method 2025-03-03 01:10:53 -03:00
github-actions[bot]
67a16c008a Update version.json with commit count
Some checks failed
Bleeding Update version / update-version (push) Has been cancelled
Version Change Action / version-release (push) Has been cancelled
2025-03-01 08:34:21 +00:00
Nix
a4bd707461
Delete healtcheck.py 2025-03-01 05:34:12 -03:00
github-actions[bot]
1b27fffc96 Update version.json with commit count 2025-03-01 08:34:00 +00:00
Nix
fade5f86b7
Update picodulce.py 2025-03-01 05:33:51 -03:00
github-actions[bot]
823b438840 Update version.json with commit count 2025-03-01 08:33:36 +00:00
Nix
9a8c3f44d0
Update version.json 2025-03-01 05:33:25 -03:00
github-actions[bot]
6b65fb0d1e Update version.json with commit count 2025-03-01 08:33:18 +00:00
Nix
8247009d60
Delete locales directory 2025-03-01 05:33:07 -03:00
github-actions[bot]
e5c395d031 Update version.json with commit count 2025-03-01 08:06:58 +00:00
Nix
263e6eae07
Added rudimentary translations (#11)
* Update picodulce.py

* Update picodulce.py

* Update picodulce.py

* Update picodulce.py

* Create healtcheck.py

* Update picodulce.py

* Update version.json

* Create locales-go-here

* Add files via upload

* Update version.json

* Update picodulce.py

* Update healtcheck.py

* Update picodulce.py

* Added more locales

* Update version.json

* Update picodulce.py
2025-03-01 05:06:48 -03:00
github-actions[bot]
ec99488326 Update version.json with commit count
Some checks failed
Bleeding Update version / update-version (push) Has been cancelled
Version Change Action / version-release (push) Has been cancelled
2025-02-23 04:26:44 +00:00
Nix
61cd427beb
Delete locales directory 2025-02-23 01:26:35 -03:00
github-actions[bot]
cb2f5b52b3 Update version.json with commit count 2025-02-23 04:26:29 +00:00
Nix
ba40354a5d
Create bogosbinted.json 2025-02-23 01:26:19 -03:00
github-actions[bot]
0c151b058e Update version.json with commit count
Some checks failed
Bleeding Update version / update-version (push) Has been cancelled
Version Change Action / version-release (push) Has been cancelled
2025-02-12 19:13:11 +00:00
Nix
fc7f47d273
put the loading theme background in a separate function 2025-02-12 16:12:59 -03:00
github-actions[bot]
4f4ff35ee5 Update version.json with commit count
Some checks failed
Bleeding Update version / update-version (push) Has been cancelled
Version Change Action / version-release (push) Has been cancelled
2025-02-11 11:52:38 +00:00
5 changed files with 411 additions and 397 deletions

View File

@ -1,43 +1,19 @@
import sys
import subprocess
import re
import colorama
import requests
from PyQt5.QtWidgets import (QApplication, QDialog, QLabel, QVBoxLayout,
QPushButton, QLineEdit, QMessageBox)
from PyQt5.QtCore import QThread, pyqtSignal, Qt, QUrl, QObject
from PyQt5.QtCore import QThread, pyqtSignal, Qt, QUrl, QObject, QTimer
from PyQt5.QtGui import QDesktopServices
from picomc.logging import logger
class AuthenticationParser:
@staticmethod
def clean_ansi(text):
ansi_clean = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
printable_clean = re.compile(r'[^\x20-\x7E\n]')
text = ansi_clean.sub('', text)
text = printable_clean.sub('', text)
return text.strip()
@staticmethod
def is_auth_error(output):
cleaned_output = AuthenticationParser.clean_ansi(output)
return "AADSTS70016" in cleaned_output and "not yet been authorized" in cleaned_output
@staticmethod
def parse_auth_output(output):
cleaned_output = AuthenticationParser.clean_ansi(output)
if AuthenticationParser.is_auth_error(cleaned_output):
return None
pattern = r"https://[^\s]+"
code_pattern = r"code\s+([A-Z0-9]+)"
url_match = re.search(pattern, cleaned_output)
code_match = re.search(code_pattern, cleaned_output, re.IGNORECASE)
if url_match and code_match:
return {
'url': url_match.group(0),
'code': code_match.group(1)
}
return None
# Constants
URL_DEVICE_AUTH = "https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode"
URL_TOKEN = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token"
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):
@ -106,66 +82,81 @@ class AuthenticationThread(QThread):
error_occurred = pyqtSignal(str)
auth_error_detected = pyqtSignal(str)
finished = pyqtSignal()
access_token_received = pyqtSignal(str, str)
def __init__(self, account):
super().__init__()
self.account = account
self.process = None
self.device_code = None
self.is_running = True
self.current_output = ""
self.waiting_for_auth = False
def run(self):
try:
command = f'picomc account authenticate {self.account}'
self.process = subprocess.Popen(
command,
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
stdin=subprocess.PIPE,
text=True,
bufsize=1,
universal_newlines=True
)
self.current_output = ""
while self.is_running and self.process.poll() is None:
line = self.process.stdout.readline()
if line:
self.current_output += line
if not self.waiting_for_auth:
parsed_data = AuthenticationParser.parse_auth_output(self.current_output)
if parsed_data:
self.auth_data_received.emit(parsed_data)
self.waiting_for_auth = True
self.current_output = ""
elif AuthenticationParser.is_auth_error(self.current_output):
self.auth_error_detected.emit(self.current_output)
self.waiting_for_auth = False
self.current_output = ""
self.process.wait()
self.finished.emit()
self.authenticate(self.account)
except Exception as e:
self.error_occurred.emit(str(e))
self.finished.emit()
def authenticate(self, account):
try:
data = {"client_id": CLIENT_ID, "scope": SCOPE}
# Request device code
resp = requests.post(URL_DEVICE_AUTH, data)
resp.raise_for_status()
j = resp.json()
self.device_code = j["device_code"]
user_code = j["user_code"]
link = j["verification_uri"]
# Format message with colorama
msg = j["message"]
msg = msg.replace(
user_code, colorama.Fore.RED + user_code + colorama.Fore.RESET
).replace(link, colorama.Style.BRIGHT + link + colorama.Style.NORMAL)
# Emit auth data received signal
self.auth_data_received.emit({'url': link, 'code': user_code})
except requests.exceptions.RequestException as e:
logger.error(f"Request failed: {e}")
self.error_occurred.emit(str(e))
self.finished.emit()
def poll_for_token(self):
try:
data = {"code": self.device_code, "grant_type": GRANT_TYPE, "client_id": CLIENT_ID}
resp = requests.post(URL_TOKEN, data)
if resp.status_code == 400:
j = resp.json()
logger.debug(j)
if j["error"] == "authorization_pending":
logger.warning(j["error_description"])
self.auth_error_detected.emit(j["error_description"])
return
else:
raise Exception(j["error_description"])
resp.raise_for_status()
j = resp.json()
access_token = j["access_token"]
refresh_token = j["refresh_token"]
logger.debug("OAuth device code flow successful")
self.access_token_received.emit(access_token, refresh_token)
self.finished.emit()
except requests.exceptions.RequestException as e:
logger.error(f"Request failed: {e}")
self.error_occurred.emit(str(e))
self.finished.emit()
def send_enter(self):
if self.process and self.process.poll() is None:
self.process.stdin.write("\n")
self.process.stdin.flush()
self.poll_for_token()
def stop(self):
self.is_running = False
if self.process:
self.process.terminate()
class MinecraftAuthenticator(QObject): # Changed to inherit from QObject
auth_finished = pyqtSignal(bool) # Add signal for completion
class MinecraftAuthenticator(QObject):
auth_finished = pyqtSignal(bool)
def __init__(self, parent=None):
super().__init__(parent)
@ -175,15 +166,12 @@ class MinecraftAuthenticator(QObject): # Changed to inherit from QObject
self.success = False
def authenticate(self, username):
"""
Start the authentication process for the given username
Returns immediately, authentication result will be emitted via auth_finished signal
"""
self.success = False
self.auth_thread = AuthenticationThread(username)
self.auth_thread.auth_data_received.connect(self.show_auth_dialog)
self.auth_thread.auth_error_detected.connect(self.handle_auth_error)
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()
@ -217,6 +205,11 @@ class MinecraftAuthenticator(QObject): # Changed to inherit from QObject
self.success = False
self.auth_finished.emit(False)
def on_access_token_received(self, access_token, refresh_token):
QMessageBox.information(None, "Success", "Authentication successful!")
self.success = True
self.auth_finished.emit(True)
def on_authentication_finished(self):
if self.auth_dialog is not None:
self.auth_dialog.close()
@ -226,8 +219,8 @@ class MinecraftAuthenticator(QObject): # Changed to inherit from QObject
self.auth_thread.stop()
self.auth_thread = None
self.success = True
self.auth_finished.emit(True)
if not self.success:
self.auth_finished.emit(False)
def cleanup(self):
if self.auth_dialog is not None:
@ -240,5 +233,7 @@ class MinecraftAuthenticator(QObject): # Changed to inherit from QObject
# Example usage
if __name__ == '__main__':
app = QApplication(sys.argv)
authenticator = MinecraftAuthenticator()
authenticator.authenticate("TestUser")
sys.exit(app.exec_())

119
healthcheck.py Normal file
View File

@ -0,0 +1,119 @@
import os
import json
import requests
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": "",
"IsFirstLaunch": True,
"Instance": "default",
"Theme": "Dark.json",
"ThemeBackground": True,
"ThemeRepository": "https://raw.githubusercontent.com/nixietab/picodulce-themes/main/repo.json",
"Locale": "en"
}
# Step 1: Check if the file exists; if not, create it with default values
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
# Step 2: Try loading the config file, handle invalid JSON
try:
with open(config_path, "r") as config_file:
self.config = json.load(config_file)
except (json.JSONDecodeError, ValueError):
# File is corrupted, overwrite it with default configuration
with open(config_path, "w") as config_file:
json.dump(default_config, config_file, indent=4)
self.config = default_config
return
# Step 3: Check for missing keys and add defaults if necessary
updated = False
for key, value in default_config.items():
if key not in self.config: # Field is missing
self.config[key] = value
updated = True
# Step 4: Save the repaired config back to the file
if updated:
with open(config_path, "w") as config_file:
json.dump(self.config, config_file, indent=4)
def themes_integrity(self):
# Define folder and file paths
themes_folder = "themes"
dark_theme_file = os.path.join(themes_folder, "Dark.json")
native_theme_file = os.path.join(themes_folder, "Native.json")
# Define the default content for Dark.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": ""
}
# Define the default content for Native.json
native_theme_content = {
"manifest": {
"name": "Native",
"description": "The native looks of your OS",
"author": "Your Qt Style",
"license": "Any"
},
"palette": {}
}
# Step 1: Ensure the themes folder exists
if not os.path.exists(themes_folder):
print(f"Creating folder: {themes_folder}")
os.makedirs(themes_folder)
# Step 2: Ensure Dark.json exists
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.")
# Step 3: Ensure Native.json exists
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.")
# Check if both files exist and print OK message
if os.path.isfile(dark_theme_file) and os.path.isfile(native_theme_file):
print("Theme Integrity OK")

30
modulecli.py Normal file
View File

@ -0,0 +1,30 @@
import click
from picomc.cli.main import picomc_cli
from io import StringIO
import sys
def run_command(command="picomc"):
# 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:
picomc_cli.main(args=command.split())
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()
if not output:
return f"Error: No output from command. Stderr: {error}"
return output

View File

@ -10,22 +10,30 @@ import requests
import json
import os
import time
from authser import MinecraftAuthenticator
from healthcheck import HealthCheck
import modulecli
from PyQt5.QtWidgets import QApplication, QComboBox, QWidget, QInputDialog, QVBoxLayout, QListWidget, QPushButton, QMessageBox, QDialog, QHBoxLayout, QLabel, QLineEdit, QCheckBox, QTabWidget, QFrame, QSpacerItem, QSizePolicy, QMainWindow, QGridLayout, QTextEdit, QListWidget, QListWidgetItem, QMenu
from PyQt5.QtGui import QFont, QIcon, QColor, QPalette, QMovie, QPixmap, QDesktopServices, QBrush
from PyQt5.QtCore import Qt, QObject, pyqtSignal, QThread, QUrl, QMetaObject, Q_ARG, QByteArray, QSize
from datetime import datetime
logging.basicConfig(level=logging.ERROR, format='%(asctime)s - %(levelname)s - %(message)s')
logging.basicConfig(level=logging.ERROR, format='%(levelname)s - %(message)s')
class PicomcVersionSelector(QWidget):
def __init__(self):
self.current_state = "menu"
self.open_dialogs = []
self.check_config_file()
self.themes_integrity()
themes_folder = "themes"
# Set up and use the health_check module
health_checker = HealthCheck()
health_checker.themes_integrity()
health_checker.check_config_file()
self.config = health_checker.config
themes_folder = "themes"
theme_file = self.config.get("Theme", "Dark.json")
# Ensure the theme file exists in the themes directory
@ -115,82 +123,17 @@ class PicomcVersionSelector(QWidget):
else:
print("Theme dosn't seem to have a stylesheet")
def themes_integrity(self):
# Define folder and file paths
themes_folder = "themes"
dark_theme_file = os.path.join(themes_folder, "Dark.json")
native_theme_file = os.path.join(themes_folder, "Native.json")
# Define the default content for Dark.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": ""
}
# Define the default content for Native.json
native_theme_content = {
"manifest": {
"name": "Native",
"description": "The native looks of your OS",
"author": "Your Qt Style",
"license": "Any"
},
"palette": {}
}
# Step 1: Ensure the themes folder exists
if not os.path.exists(themes_folder):
print(f"Creating folder: {themes_folder}")
os.makedirs(themes_folder)
# Step 2: Ensure Dark.json exists
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.")
# Step 3: Ensure Native.json exists
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.")
# Check if both files exist and print OK message
if os.path.isfile(dark_theme_file) and os.path.isfile(native_theme_file):
print("Theme Integrity OK")
def FirstLaunch(self):
try:
self.config_path = "config.json"
print("Running picomc instance create default command...")
# Run the command using subprocess
result = subprocess.run(["picomc", "instance", "create", "default"], check=True, capture_output=True, text=True)
# Run the command using modulecli
command = "instance create default"
result = modulecli.run_command(command)
# Print the output of the command
print("Command output:", result.stdout)
print("Command output:", result)
# Change the value of IsFirstLaunch to False
self.config["IsFirstLaunch"] = False
@ -201,35 +144,26 @@ class PicomcVersionSelector(QWidget):
json.dump(self.config, f, indent=4)
print("Configuration saved to", self.config_path)
except subprocess.CalledProcessError as e:
except Exception as e:
print("An error occurred while creating the instance.")
print("Error output:", e.stderr)
print("Error output:", str(e))
def resize_event(self, event):
if hasattr(self, 'movie_label'):
self.movie_label.setGeometry(0, 0, 400, 320)
event.accept() # Accept the resize event
def load_theme_background(self):
"""Load and set the theme background image from base64 data in the theme configuration."""
if not self.config.get("ThemeBackground", False): # Default to False if ThemeBackground is missing
return
def init_ui(self):
self.setWindowTitle('PicoDulce Launcher') # Change window title
current_date = datetime.now()
if (current_date.month == 12 and current_date.day >= 8) or (current_date.month == 1 and current_date.day <= 1):
self.setWindowIcon(QIcon('holiday.ico')) # Set holiday icon
else:
self.setWindowIcon(QIcon('launcher_icon.ico')) # Set regular icon
self.setGeometry(100, 100, 400, 250)
# Set application style and theme
QApplication.setStyle("Fusion")
with open("config.json", "r") as config_file:
config = json.load(config_file)
if self.config.get("ThemeBackground", False): # Default to False if ThemeBackground is missing
# Get the base64 string for the background image from the theme file
theme_background_base64 = self.theme.get("background_image_base64", "")
if theme_background_base64:
if not theme_background_base64:
print("No background GIF base64 string found in the theme file.")
return
try:
# Decode the base64 string to get the binary data
background_image_data = QByteArray.fromBase64(theme_background_base64.encode())
@ -262,8 +196,24 @@ class PicomcVersionSelector(QWidget):
print("Error: Failed to load background GIF from base64 string.")
except Exception as e:
print(f"Error: Failed to decode and set background GIF. {e}")
def init_ui(self):
self.setWindowTitle('PicoDulce Launcher') # Change window title
current_date = datetime.now()
if (current_date.month == 12 and current_date.day >= 8) or (current_date.month == 1 and current_date.day <= 1):
self.setWindowIcon(QIcon('holiday.ico')) # Set holiday icon
else:
print("No background GIF base64 string found in the theme file.")
self.setWindowIcon(QIcon('launcher_icon.ico')) # Set regular icon
self.setGeometry(100, 100, 400, 250)
# Set application style and theme
QApplication.setStyle("Fusion")
with open("config.json", "r") as config_file:
config = json.load(config_file)
# Load theme background
self.load_theme_background()
# Create title label
title_label = QLabel('PicoDulce Launcher') # Change label text
@ -343,50 +293,6 @@ class PicomcVersionSelector(QWidget):
else:
super().keyPressEvent(event)
def check_config_file(self):
config_path = "config.json"
default_config = {
"IsRCPenabled": False,
"CheckUpdate": False,
"IsBleeding": False,
"LastPlayed": "",
"IsFirstLaunch": True,
"Instance": "default",
"Theme": "Dark.json",
"ThemeBackground": True,
"ThemeRepository": "https://raw.githubusercontent.com/nixietab/picodulce-themes/main/repo.json"
}
# Step 1: Check if the file exists; if not, create it with default values
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
# Step 2: Try loading the config file, handle invalid JSON
try:
with open(config_path, "r") as config_file:
self.config = json.load(config_file)
except (json.JSONDecodeError, ValueError):
# File is corrupted, overwrite it with default configuration
with open(config_path, "w") as config_file:
json.dump(default_config, config_file, indent=4)
self.config = default_config
return
# Step 3: Check for missing keys and add defaults if necessary
updated = False
for key, value in default_config.items():
if key not in self.config: # Field is missing
self.config[key] = value
updated = True
# Step 4: Save the repaired config back to the file
if updated:
with open(config_path, "w") as config_file:
json.dump(self.config, config_file, indent=4)
def open_settings_dialog(self):
dialog = QDialog(self)
dialog.setWindowTitle('Settings')
@ -491,8 +397,8 @@ class PicomcVersionSelector(QWidget):
discord_rcp_checkbox.isChecked(),
check_updates_checkbox.isChecked(),
theme_background_checkbox.isChecked(),
self.selected_theme, # Pass the selected theme here
bleeding_edge_checkbox.isChecked() # Pass the bleeding edge setting here
self.selected_theme,
bleeding_edge_checkbox.isChecked()
)
)
@ -756,20 +662,6 @@ class PicomcVersionSelector(QWidget):
)
self.__init__()
def get_palette(self, palette_type):
"""Retrieve the corresponding palette based on the palette type."""
palettes = {
"Dark": self.create_dark_palette,
"Obsidian": self.create_obsidian_palette,
"Redstone": self.create_redstone_palette,
"Alpha": self.create_alpha_palette,
"Strawberry": self.create_strawberry_palette,
"Native": self.create_native_palette,
"Christmas": self.create_christmas_palette,
}
# Default to dark palette if the type is not specified or invalid
return palettes.get(palette_type, self.create_dark_palette)()
def get_system_info(self):
# Get system information
java_version = subprocess.getoutput("java -version 2>&1 | head -n 1")
@ -809,16 +701,16 @@ class PicomcVersionSelector(QWidget):
def open_game_directory(self):
try:
# Run the command and capture the output
result = subprocess.run(['picomc', 'instance', 'dir'], capture_output=True, text=True, check=True)
game_directory = result.stdout.strip()
# Run the command using modulecli
command = "instance dir"
result = modulecli.run_command(command)
game_directory = result.strip()
# Open the directory in the system's file explorer
QDesktopServices.openUrl(QUrl.fromLocalFile(game_directory))
except subprocess.CalledProcessError as e:
except Exception as e:
print(f"Error running picomc command: {e}")
def populate_installed_versions(self):
config_path = "config.json"
@ -839,20 +731,17 @@ class PicomcVersionSelector(QWidget):
# Run the command and capture the output
try:
process = subprocess.Popen(['picomc', 'version', 'list'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
output, error = process.communicate()
command = "version list"
output = modulecli.run_command(command)
if process.returncode != 0:
raise subprocess.CalledProcessError(process.returncode, process.args, output=output, stderr=error)
except FileNotFoundError:
logging.error("'picomc' command not found. Please ensure it's installed and in your PATH.")
return
except subprocess.CalledProcessError as e:
logging.error("Error running 'picomc': %s", e.stderr)
if not output:
raise Exception("Failed to get output from modulecli")
except Exception as e:
logging.error("Error running 'picomc': %s", e)
return
# Parse the output and replace '[local]' with a space
versions = [version.replace('[local]', ' ').strip() for version in output.splitlines()]
versions = [version.replace('[local]', ' ').strip() for version in output.splitlines() if version.strip()]
# Get the last played version from the config
last_played = self.config.get("LastPlayed", "")
@ -869,28 +758,22 @@ class PicomcVersionSelector(QWidget):
def populate_installed_versions_normal_order(self):
# Run the 'picomc instance create default' command at the start
try:
process = subprocess.Popen(['picomc', 'instance', 'create', 'default'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
output, error = process.communicate()
if process.returncode != 0:
raise subprocess.CalledProcessError(process.returncode, process.args, error)
except FileNotFoundError:
logging.error("'picomc' command not found. Please make sure it's installed and in your PATH.")
return
except subprocess.CalledProcessError as e:
logging.error("Error creating default instance: %s", e.stderr)
command = "instance create default"
output = modulecli.run_command(command)
if not output:
raise Exception("Failed to get output from modulecli for 'instance create default'")
except Exception as e:
logging.error("Error creating default instance: %s", str(e))
return
# Run the 'picomc version list' command and get the output
try:
process = subprocess.Popen(['picomc', 'version', 'list'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
output, error = process.communicate()
if process.returncode != 0:
raise subprocess.CalledProcessError(process.returncode, process.args, error)
except FileNotFoundError:
logging.error("'picomc' command not found. Please make sure it's installed and in your PATH.")
return
except subprocess.CalledProcessError as e:
logging.error("Error: %s", e.stderr)
command = "version list"
output = modulecli.run_command(command)
if not output:
raise Exception("Failed to get output from modulecli for 'version list'")
except Exception as e:
logging.error("Error: %s", str(e))
return
# Parse the output and replace '[local]' with a space
@ -917,7 +800,7 @@ class PicomcVersionSelector(QWidget):
# Check if there are any accounts
try:
account_list_output = subprocess.check_output(["picomc", "account", "list"]).decode("utf-8").strip()
account_list_output = modulecli.run_command("account list").strip()
if not account_list_output:
QMessageBox.warning(self, "No Account Available", "Please create an account first.")
return
@ -926,18 +809,25 @@ class PicomcVersionSelector(QWidget):
if '*' not in account_list_output:
QMessageBox.warning(self, "No Account Selected", "Please select an account.")
return
except subprocess.CalledProcessError as e:
except Exception as e:
error_message = f"Error fetching accounts: {str(e)}"
logging.error(error_message)
QMessageBox.critical(self, "Error", error_message)
return
selected_instance = self.installed_version_combo.currentText()
logging.info(f"Selected instance: {selected_instance}")
logging.info(f"Selected instance from dropdown: {selected_instance}")
# Verify the selected instance value before starting the game
if not selected_instance:
logging.error("No instance selected.")
QMessageBox.warning(self, "No Instance Selected", "Please select an instance.")
return
play_thread = threading.Thread(target=self.run_game, args=(selected_instance,))
play_thread.start()
def run_game(self, selected_instance):
try:
# Set current_state to the selected instance
@ -952,10 +842,14 @@ class PicomcVersionSelector(QWidget):
update_thread = threading.Thread(target=self.update_last_played, args=(selected_instance,))
update_thread.start()
# Run the game subprocess with the instance_value from config.json
subprocess.run(['picomc', 'instance', 'launch', '--version-override', selected_instance, instance_value], check=True)
# Run the game using the modulecli module
command = f"instance launch --version-override {selected_instance} {instance_value}"
output = modulecli.run_command(command)
except subprocess.CalledProcessError as e:
if not output:
raise Exception("Failed to get output from modulecli")
except Exception as e:
error_message = f"Error playing {selected_instance}: {e}"
logging.error(error_message)
# Use QMetaObject.invokeMethod to call showError safely
@ -967,6 +861,7 @@ class PicomcVersionSelector(QWidget):
# Reset current_state to "menu" after the game closes
self.current_state = "menu"
def update_last_played(self, selected_instance):
config_path = "config.json"
self.config["LastPlayed"] = selected_instance
@ -1064,16 +959,16 @@ class PicomcVersionSelector(QWidget):
return
try:
command = ['picomc', 'account', 'create', username]
command = f"account create {username}"
if is_microsoft:
command.append('--ms')
command += " --ms"
subprocess.run(command, check=True)
modulecli.run_command(command)
QMessageBox.information(dialog, "Success", f"Account '{username}' created successfully!")
self.populate_accounts_for_all_dialogs()
dialog.accept()
except subprocess.CalledProcessError as e:
error_message = f"Error creating account: {e.stderr.decode()}"
except Exception as e:
error_message = f"Error creating account: {str(e)}"
logging.error(error_message)
QMessageBox.critical(dialog, "Error", error_message)
@ -1126,22 +1021,20 @@ class PicomcVersionSelector(QWidget):
confirm_dialog = QMessageBox.question(dialog, "Confirm Removal", confirm_message, QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
if confirm_dialog == QMessageBox.Yes:
try:
subprocess.run(['picomc', 'account', 'remove', username], check=True)
command = f"account remove {username}"
modulecli.run_command(command)
QMessageBox.information(dialog, "Success", f"Account '{username}' removed successfully!")
self.populate_accounts_for_all_dialogs()
except subprocess.CalledProcessError as e:
error_message = f"Error removing account: {e.stderr.decode()}"
except Exception as e:
error_message = f"Error removing account: {str(e)}"
logging.error(error_message)
QMessageBox.critical(dialog, "Error", error_message)
def populate_accounts(self, account_combo):
# Populate the account dropdown
try:
process = subprocess.Popen(['picomc', 'account', 'list'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
output, error = process.communicate()
if process.returncode != 0:
raise subprocess.CalledProcessError(process.returncode, process.args, error)
command = "account list"
output = modulecli.run_command(command)
# Process accounts, keeping the one with "*" at the top
accounts = output.splitlines()
@ -1166,10 +1059,8 @@ class PicomcVersionSelector(QWidget):
for account in normal_accounts:
account_combo.addItem(account)
except FileNotFoundError:
logging.error("'picomc' command not found. Please make sure it's installed and in your PATH.")
except subprocess.CalledProcessError as e:
logging.error(f"Error: {e.stderr}")
except Exception as e:
logging.error(f"Error: {str(e)}")
def populate_accounts_for_all_dialogs(self):
# Update account dropdowns in all open dialogs
@ -1186,14 +1077,16 @@ class PicomcVersionSelector(QWidget):
return
try:
subprocess.run(['picomc', 'account', 'setdefault', account_name], check=True)
command = f"account setdefault {account_name}"
modulecli.run_command(command)
QMessageBox.information(self, "Success", f"Account '{account_name}' set as default!")
self.populate_accounts_for_all_dialogs()
except subprocess.CalledProcessError as e:
error_message = f"Error setting default account '{account_name}': {e.stderr.decode()}"
except Exception as e:
error_message = f"Error setting default account '{account_name}': {str(e)}"
logging.error(error_message)
QMessageBox.critical(self, "Error", error_message)
def show_about_dialog(self):
# Load the version number from version.json
try:
@ -1423,10 +1316,11 @@ class DownloadThread(QThread):
def run(self):
try:
subprocess.run(['picomc', 'version', 'prepare', self.version], check=True)
command = f"version prepare {self.version}"
modulecli.run_command(command)
self.completed.emit(True, f"Version {self.version} prepared successfully!")
except subprocess.CalledProcessError as e:
error_message = f"Error preparing {self.version}: {e.stderr.decode()}"
except Exception as e:
error_message = f"Error preparing {self.version}: {str(e)}"
self.completed.emit(False, error_message)
class ModLoaderAndVersionMenu(QDialog):
@ -1443,11 +1337,11 @@ class ModLoaderAndVersionMenu(QDialog):
# Create tabs
install_mod_tab = QWidget()
download_version_tab = QWidget()
instances_tab = QWidget() # New tab for instances
instances_tab = QWidget()
tab_widget.addTab(download_version_tab, "Download Version")
tab_widget.addTab(install_mod_tab, "Install Mod Loader")
tab_widget.addTab(instances_tab, "Instances") # Add the new tab
tab_widget.addTab(instances_tab, "Instances")
# Add content to "Install Mod Loader" tab
self.setup_install_mod_loader_tab(install_mod_tab)
@ -1499,11 +1393,8 @@ class ModLoaderAndVersionMenu(QDialog):
if instance_name:
try:
# Run the "picomc instance create" command
process = subprocess.Popen(['picomc', 'instance', 'create', instance_name], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
output, error = process.communicate()
if process.returncode != 0:
raise subprocess.CalledProcessError(process.returncode, process.args, error)
command = f"instance create {instance_name}"
modulecli.run_command(command)
# Notify the user that the instance was created
QMessageBox.information(self, "Instance Created", f"Instance '{instance_name}' has been created successfully.")
@ -1514,11 +1405,9 @@ class ModLoaderAndVersionMenu(QDialog):
# Optionally select the newly created instance
self.on_instance_selected(self.instances_list_widget.item(self.instances_list_widget.count() - 1))
except FileNotFoundError:
logging.error("'picomc' command not found. Please make sure it's installed and in your PATH.")
except subprocess.CalledProcessError as e:
logging.error("Error creating instance: %s", e.stderr)
QMessageBox.critical(self, "Error", f"Failed to create instance: {e.stderr}")
except Exception as e:
logging.error("Error creating instance: %s", str(e))
QMessageBox.critical(self, "Error", f"Failed to create instance: {str(e)}")
else:
QMessageBox.warning(self, "Invalid Input", "Please enter a valid instance name.")
@ -1529,14 +1418,8 @@ class ModLoaderAndVersionMenu(QDialog):
try:
# Run the "picomc instance rename" command
process = subprocess.Popen(
['picomc', 'instance', 'rename', old_instance_name, new_instance_name],
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
)
output, error = process.communicate()
if process.returncode != 0:
raise subprocess.CalledProcessError(process.returncode, process.args, error)
command = f"instance rename {old_instance_name} {new_instance_name}"
modulecli.run_command(command)
QMessageBox.information(self, "Instance Renamed", f"Instance '{old_instance_name}' has been renamed to '{new_instance_name}' successfully.")
@ -1548,11 +1431,9 @@ class ModLoaderAndVersionMenu(QDialog):
if matching_items:
self.instances_list_widget.setCurrentItem(matching_items[0])
except FileNotFoundError:
logging.error("'picomc' command not found. Please make sure it's installed and in your PATH.")
except subprocess.CalledProcessError as e:
logging.error("Error renaming instance: %s", e.stderr)
QMessageBox.critical(self, "Error", f"Failed to rename instance: {e.stderr}")
except Exception as e:
logging.error("Error renaming instance: %s", str(e))
QMessageBox.critical(self, "Error", f"Failed to rename instance: {str(e)}")
def delete_instance(self, instance_name):
if instance_name == "default":
@ -1567,11 +1448,8 @@ class ModLoaderAndVersionMenu(QDialog):
if confirm_delete == QMessageBox.Yes:
try:
# Run the "picomc instance delete" command
process = subprocess.Popen(['picomc', 'instance', 'delete', instance_name], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
output, error = process.communicate()
if process.returncode != 0:
raise subprocess.CalledProcessError(process.returncode, process.args, error)
command = f"instance delete {instance_name}"
modulecli.run_command(command)
# Notify the user that the instance was deleted
QMessageBox.information(self, "Instance Deleted", f"Instance '{instance_name}' has been deleted successfully.")
@ -1579,18 +1457,15 @@ class ModLoaderAndVersionMenu(QDialog):
# Reload the instances list
self.load_instances()
except FileNotFoundError:
logging.error("'picomc' command not found. Please make sure it's installed and in your PATH.")
except subprocess.CalledProcessError as e:
logging.error("Error deleting instance: %s", e.stderr)
QMessageBox.critical(self, "Error", f"Failed to delete instance: {e.stderr}")
except Exception as e:
logging.error("Error deleting instance: %s", str(e))
QMessageBox.critical(self, "Error", f"Failed to delete instance: {str(e)}")
def load_instances(self):
try:
process = subprocess.Popen(['picomc', 'instance', 'list'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
output, error = process.communicate()
if process.returncode != 0:
raise subprocess.CalledProcessError(process.returncode, process.args, error)
# Run the "picomc instance list" command
command = "instance list"
output = modulecli.run_command(command)
# Parse the output and add each instance to the list widget
instances = output.splitlines()
@ -1600,10 +1475,9 @@ class ModLoaderAndVersionMenu(QDialog):
self.instances_list_widget.addItem(item)
self.add_instance_buttons(item, instance)
except FileNotFoundError:
logging.error("'picomc' command not found. Please make sure it's installed and in your PATH.")
except subprocess.CalledProcessError as e:
logging.error("Error fetching instances: %s", e.stderr)
except Exception as e:
logging.error("Error fetching instances: %s", str(e))
def add_instance_buttons(self, list_item, instance_name):
widget = QWidget()
@ -1759,21 +1633,19 @@ class ModLoaderAndVersionMenu(QDialog):
options.append('--beta')
if options:
try:
process = subprocess.Popen(['picomc', 'version', 'list'] + options, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
output, error = process.communicate()
if process.returncode != 0:
raise subprocess.CalledProcessError(process.returncode, process.args, error)
except FileNotFoundError:
logging.error("'picomc' command not found. Please make sure it's installed and in your PATH.")
return
except subprocess.CalledProcessError as e:
logging.error("Error: %s", e.stderr)
command = 'version list ' + ' '.join(options)
output = modulecli.run_command(command)
if "Error" in output:
logging.error(output)
return
# Parse the output and replace '[local]' with a space
versions = output.splitlines()
versions = [version.replace('[local]', ' ').strip() for version in versions]
self.version_combo.addItems(versions)
except Exception as e:
logging.error("Unexpected error: %s", e)
return
# Update the download button state whenever versions are updated
self.update_download_button_state()
@ -1830,15 +1702,10 @@ class ModLoaderAndVersionMenu(QDialog):
def populate_available_releases(self, version_combo, install_forge, install_fabric):
try:
process = subprocess.Popen(['picomc', 'version', 'list', '--release'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
output, error = process.communicate()
if process.returncode != 0:
raise subprocess.CalledProcessError(process.returncode, process.args, error)
except FileNotFoundError:
logging.error("'picomc' command not found. Please make sure it's installed and in your PATH.")
return
except subprocess.CalledProcessError as e:
logging.error("Error: %s", e.stderr)
command = "version list --release"
output = modulecli.run_command(command)
except Exception as e:
logging.error("Error: %s", str(e))
return
if install_fabric:
@ -1868,12 +1735,13 @@ class ModLoaderAndVersionMenu(QDialog):
try:
if mod_loader == 'forge':
subprocess.run(['picomc', 'mod', 'loader', 'forge', 'install', '--game', version], check=True)
command = f"mod loader forge install --game {version}"
elif mod_loader == 'fabric':
subprocess.run(['picomc', 'mod', 'loader', 'fabric', 'install', version], check=True)
command = f"mod loader fabric install {version}"
modulecli.run_command(command)
QMessageBox.information(self, "Success", f"{mod_loader.capitalize()} installed successfully for version {version}!")
except subprocess.CalledProcessError as e:
error_message = f"Error installing {mod_loader} for version {version}: {e.stderr.decode()}"
except Exception as e:
error_message = f"Error installing {mod_loader} for version {version}: {str(e)}"
QMessageBox.critical(self, "Error", error_message)
logging.error(error_message)

View File

@ -1,5 +1,5 @@
{
"version": "0.12",
"version": "0.13",
"links": [
"https://raw.githubusercontent.com/nixietab/picodulce/main/version.json",
"https://raw.githubusercontent.com/nixietab/picodulce/main/picodulce.py",
@ -7,7 +7,9 @@
"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/authser.py"
"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.11.9.1-166"
"versionBleeding": "0.13-194"
}