diff --git a/.devcontainer.json b/.devcontainer.json
new file mode 100644
index 0000000..3043ccd
--- /dev/null
+++ b/.devcontainer.json
@@ -0,0 +1,12 @@
+{
+ "customizations": {
+ "vscode": {
+ "extensions": [
+ "mkhl.direnv"
+ ]
+ }
+ },
+ "image": "ghcr.io/cachix/devenv/devcontainer:latest",
+ "overrideCommand": false,
+ "updateContentCommand": "devenv test"
+}
diff --git a/.envrc b/.envrc
new file mode 100644
index 0000000..c420855
--- /dev/null
+++ b/.envrc
@@ -0,0 +1,10 @@
+if ! has nix_direnv_version || ! nix_direnv_version 2.2.1; then
+ source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/2.2.1/direnvrc" "sha256-zelF0vLbEl5uaqrfIzbgNzJWGmLzCmYAkInj/LNxvKs="
+fi
+
+watch_file flake.nix
+watch_file flake.lock
+if ! use flake . --impure
+then
+ echo "devenv could not be built. The devenv environment was not loaded. Make the necessary changes to devenv.nix and hit enter to try again." >&2
+fi
diff --git a/.gitignore b/.gitignore
index 7418e95..b2cb3c4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,3 +8,17 @@ wemod.conf
wemod.log
winetricks
pip.pyz
+# Devenv
+.devenv*
+devenv.local.nix
+
+# direnv
+.direnv
+
+# pre-commit
+.pre-commit-config.yaml
+
+/dist
+
+/.pdm-build
+/.pdm-python
diff --git a/readme.md b/README.md
similarity index 59%
rename from readme.md
rename to README.md
index 516ff89..bbc1c91 100644
--- a/readme.md
+++ b/README.md
@@ -1,12 +1,12 @@
-# WeMod Launcher (Wemod for Linux)
+# Wand Launcher (Wand for Linux)
-**The WeMod Launcher is currently on version 1.535.**
+**The Wand Launcher is currently on version 1.535.**
## DISCLAIMER
-This project is *NOT* affiliated with, funded by, or paid by WeMod.
+This project is *NOT* affiliated with, funded by, or paid by Wand.
The work done here is purely from the contributors who donate their time and effort.
-WeMod (the company) makes WeMod (the mod tool).
-We (`wemod-launcher`) enable you to run it on Linux (and by extension, the Steam Deck).
+Wand (the company) makes Wand (the mod tool).
+We (`wand-launcher`) enable you to run it on Linux (and by extension, the Steam Deck).
## Support & Contributions
@@ -36,9 +36,9 @@ If this tool helps you, please consider one or more of the following:
[GitHub](https://github.com/JohnHamwi)
For more help or to contribute:
-* Suggest improvements via [GitHub Discussions](https://github.com/DeckCheatz/wemod-launcher/discussions)
-* See the [Wiki Suggestions](https://github.com/DeckCheatz/wemod-launcher/wiki/Suggestions) and [Changes](https://github.com/DeckCheatz/wemod-launcher/wiki/Changes)
-* Report / Help solve issues or bugs by [filing / answering an Issue](https://github.com/DeckCheatz/wemod-launcher/issues)
+* Suggest improvements via [GitHub Discussions](https://github.com/DeckCheatz/wand-launcher/discussions)
+* See the [Wiki Suggestions](https://github.com/DeckCheatz/wand-launcher/wiki/Suggestions) and [Changes](https://github.com/DeckCheatz/wand-launcher/wiki/Changes)
+* Report / Help solve issues or bugs by [filing / answering an Issue](https://github.com/DeckCheatz/wand-launcher/issues)
## Rework Project (Led by shymega)
@@ -51,20 +51,20 @@ We are calling for contributors to assist with:
* **Wiki writers & documenters**: Help write guides and technical documentation for the rework.
Want to get involved?
-* [Join the Discussion on GitHub](https://github.com/DeckCheatz/wemod-launcher/discussions)
-* [Track the Rework Progress](https://github.com/DeckCheatz/wemod-launcher/pull/180))
+* [Join the Discussion on GitHub](https://github.com/DeckCheatz/wand-launcher/discussions)
+* [Track the Rework Progress](https://github.com/DeckCheatz/wand-launcher/pull/180))
* Or reach out to shymega directly via GitHub.
## Quick Guide
- **This guide only includes the most relevant info and might not be enough to run WeMod;**
-in which case, check out the [Full Guide](https://github.com/DeckCheatz/wemod-launcher/wiki/Full-Guide) **OR** the [video tutorial by Marvin1099](https://youtu.be/5UlVCZvIl1E).
+ **This guide only includes the most relevant info and might not be enough to run Wand;**
+in which case, check out the [Full Guide](https://github.com/DeckCheatz/wand-launcher/wiki/Full-Guide) **OR** the [video tutorial by Marvin1099](https://youtu.be/5UlVCZvIl1E).
- **Optional:** If you have access to another PC and wish to control the Steam Deck remotely,
consider using **[RustDesk](https://github.com/rustdesk/rustdesk/releases/latest)** for easier setup (the `.flatpak` is easiest).
-- **Info:** [License Change](https://github.com/DeckCheatz/wemod-launcher/discussions/131)
+- **Info:** [License Change](https://github.com/DeckCheatz/wand-launcher/discussions/131)
-- **Info:** Games no longer seem to be detected by **Wemod**. If anyone has info on how **Wemod** finds games please make a new issue. If you have no idea please **don't** make a bug report. We cant fix this if we don't know how. You will have to add the game manualy right now.
+- **Info:** Games no longer seem to be detected by **Wand**. If anyone has info on how **Wand** finds games please make a new issue. If you have no idea please **don't** make a bug report. We cant fix this if we don't know how. You will have to add the game manualy right now.
1. Python `python-venv` (or `python3-venv` or `venv` or `virtualenv`; use first one found)
and `Tk` need to be installed.
@@ -73,40 +73,40 @@ consider using **[RustDesk](https://github.com/rustdesk/rustdesk/releases/latest
- Ubuntu/Debian: `sudo apt install python3-tk`
- Arch Linux: `sudo pacman -S tk`.
- Fedora: `sudo dnf install python3-tkinter`
-3. Install GE-Proton, which is necessary to run the game and WeMod with. Using Valve's own Proton seems to work, but using GE-Proton is recommended:
+3. Install GE-Proton, which is necessary to run the game and Wand with. Using Valve's own Proton seems to work, but using GE-Proton is recommended:
1. Search for and install `ProtonUp-QT` via your distro's software center. If using Flatpak, command is: `flatpak install net.davidotek.pupgui2`.
2. Download the latest GE-Proton in `ProtonUp-QT`
4. Restart Steam/SteamOS.
5. In a terminal session (Konsole if using KDE Plasma):
- 1. Change directory to a location of your choosing, then run `git clone https://github.com/DeckCheatz/wemod-launcher`.
- Make note of the directory obtained with `readlink -f wemod-launcher` (which will be labeled `{path/to/wemod-launcher}` for the rest of this guide).
- 2. Run `chmod -R ug+x wemod-launcher`.
- **NOTE:** To use this tool with the Flatpak version of Steam (not recomended), continue [here](https://github.com/DeckCheatz/wemod-launcher/wiki/Steam-Flatpak-Usage).
-6. In your Steam Library, open the game settings with which to run WeMod with. Make sure you ran the game once before doing this!
+ 1. Change directory to a location of your choosing, then run `git clone https://github.com/DeckCheatz/wand-launcher`.
+ Make note of the directory obtained with `readlink -f wand-launcher` (which will be labeled `{path/to/wand-launcher}` for the rest of this guide).
+ 2. Run `chmod -R ug+x wand-launcher`.
+ **NOTE:** To use this tool with the Flatpak version of Steam (not recomended), continue [here](https://github.com/DeckCheatz/wand-launcher/wiki/Steam-Flatpak-Usage).
+6. In your Steam Library, open the game settings with which to run Wand with. Make sure you ran the game once before doing this!
1. In the `Compatibility` tab, change the Proton version to the one picked in Step 2, or otherwise to the latest numbered Proton (e.g. Proton-9.0).
- 2. Under `Launch Options`, input `{path/to/wemod-launcher}/wemod %command%`.
+ 2. Under `Launch Options`, input `{path/to/wand-launcher}/wand %command%`.
7. Start the game.
8. Select "no" to the "copy prefix question" if it appears and says `might work`.
If it mentions `likely works` (or better) go to step 9 (accept all).
9. Select download.
10. Select Yes/Ok until no more windows appear.
All rundll32.exe errors can safely be ignored (by clicking `no`).
- WeMod should start with the game.
-11. (Only done once): Login to your WeMod account.
-12. Select the game you're running from the library, then click the Play to start the WeMod engine.
-13. You may now set or switch mods. Closing the WeMod window will keep it running in the background.
+ Wand should start with the game.
+11. (Only done once): Login to your Wand account.
+12. Select the game you're running from the library, then click the Play to start the Wand engine.
+13. You may now set or switch mods. Closing the Wand window will keep it running in the background.
-wemod-launcher will automatically update if you installed it using step 5.
-**But**: This will only work if you have [launcher version 1.092 or older](https://github.com/DeckCheatz/wemod-launcher/wiki/The-Self-Update).
+wand-launcher will automatically update if you installed it using step 5.
+**But**: This will only work if you have [launcher version 1.092 or older](https://github.com/DeckCheatz/wand-launcher/wiki/The-Self-Update).
-**Optionally**: Check out tutorials on how to use specific [WeMod Laucher features](https://github.com/DeckCheatz/wemod-launcher/wiki/Launcher-Tutorials)
-**Like**: Check how to [Use External Launchers](https://github.com/DeckCheatz/wemod-launcher/wiki/Using-External-Launchers) (Use The WeMod Launcher outside of Steam)
-**OR**: Check out how to [Edit The Config](https://github.com/DeckCheatz/wemod-launcher/wiki/Config-Usage)
+**Optionally**: Check out tutorials on how to use specific [Wand Laucher features](https://github.com/DeckCheatz/wand-launcher/wiki/Launcher-Tutorials)
+**Like**: Check how to [Use External Launchers](https://github.com/DeckCheatz/wand-launcher/wiki/Using-External-Launchers) (Use The Wand Launcher outside of Steam)
+**OR**: Check out how to [Edit The Config](https://github.com/DeckCheatz/wand-launcher/wiki/Config-Usage)
## Common Issues
### Dot Net Error
-* If you see a .net error in Wemod that means your prefix is messed up.
+* If you see a .net error in Wand that means your prefix is messed up.
* After you close the game the troubleshooter should come up (close ingame not from steam).
1. There select "delete prefix" or something like that.
2. Rerun the game, and when it asks you to use a already installed prefix, with some version, say no.
@@ -116,16 +116,16 @@ wemod-launcher will automatically update if you installed it using step 5.
##  Additional
- **Video Tutorial:** [WeMod-launcher Setup Tutorial by Marvin1099](https://youtu.be/5UlVCZvIl1E)
- **Guide was written by Trippin and updated by Marvin1099.**
- **WeMod Linux is developed by DaniAsh551 with recent support by Marvin1099.**
- **If you find this guide to be helpful, we encourage you to star the project.**
+ **Video Tutorial:** [Wand-launcher Setup Tutorial by Marvin1099](https://youtu.be/5UlVCZvIl1E)
+ **Guide was written by Trippin and updated by Marvin1099.**
+ **Wand Linux is developed by DaniAsh551 with recent support by Marvin1099.**
+ **If you find this guide to be helpful, we encourage you to star the project.**
-
+
-
-
-
+
+
+
diff --git a/default.nix b/default.nix
new file mode 100644
index 0000000..3824d73
--- /dev/null
+++ b/default.nix
@@ -0,0 +1,17 @@
+# SPDX-FileCopyrightText: 2024-2025 The DeckCheatz Developers
+#
+# SPDX-License-Identifier: AGPL-3.0-only
+(
+ import
+ (
+ let
+ lock = builtins.fromJSON (builtins.readFile ./flake.lock);
+ in
+ fetchTarball {
+ url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz";
+ sha256 = lock.nodes.flake-compat.locked.narHash;
+ }
+ )
+ {src = ./.;}
+)
+.defaultNix
diff --git a/flake.lock b/flake.lock
new file mode 100644
index 0000000..04935ac
--- /dev/null
+++ b/flake.lock
@@ -0,0 +1,276 @@
+{
+ "nodes": {
+ "cachix": {
+ "inputs": {
+ "devenv": [
+ "devenv"
+ ],
+ "flake-compat": [
+ "devenv",
+ "flake-compat"
+ ],
+ "git-hooks": [
+ "devenv",
+ "git-hooks"
+ ],
+ "nixpkgs": [
+ "devenv",
+ "nixpkgs"
+ ]
+ },
+ "locked": {
+ "lastModified": 1760971495,
+ "narHash": "sha256-IwnNtbNVrlZIHh7h4Wz6VP0Furxg9Hh0ycighvL5cZc=",
+ "owner": "cachix",
+ "repo": "cachix",
+ "rev": "c5bfd933d1033672f51a863c47303fc0e093c2d2",
+ "type": "github"
+ },
+ "original": {
+ "owner": "cachix",
+ "ref": "latest",
+ "repo": "cachix",
+ "type": "github"
+ }
+ },
+ "devenv": {
+ "inputs": {
+ "cachix": "cachix",
+ "flake-compat": "flake-compat",
+ "flake-parts": "flake-parts",
+ "git-hooks": "git-hooks",
+ "nix": "nix",
+ "nixpkgs": "nixpkgs"
+ },
+ "locked": {
+ "lastModified": 1766676776,
+ "narHash": "sha256-2bmlDZ7eFr1gO4lMKE9aw1RRUoR+9MDMAHbuVugIoi4=",
+ "owner": "cachix",
+ "repo": "devenv",
+ "rev": "85b34019389c192e10e3508745c15b98060216f5",
+ "type": "github"
+ },
+ "original": {
+ "owner": "cachix",
+ "repo": "devenv",
+ "type": "github"
+ }
+ },
+ "flake-compat": {
+ "flake": false,
+ "locked": {
+ "lastModified": 1761588595,
+ "narHash": "sha256-XKUZz9zewJNUj46b4AJdiRZJAvSZ0Dqj2BNfXvFlJC4=",
+ "owner": "edolstra",
+ "repo": "flake-compat",
+ "rev": "f387cd2afec9419c8ee37694406ca490c3f34ee5",
+ "type": "github"
+ },
+ "original": {
+ "owner": "edolstra",
+ "repo": "flake-compat",
+ "type": "github"
+ }
+ },
+ "flake-compat_2": {
+ "flake": false,
+ "locked": {
+ "lastModified": 1766661267,
+ "narHash": "sha256-QN1r/zNqvXHwWqlRAnRtFf4CQwIOJx58PtdExIzAw94=",
+ "owner": "edolstra",
+ "repo": "flake-compat",
+ "rev": "f275e157c50c3a9a682b4c9b4aa4db7a4cd3b5f2",
+ "type": "github"
+ },
+ "original": {
+ "owner": "edolstra",
+ "repo": "flake-compat",
+ "type": "github"
+ }
+ },
+ "flake-parts": {
+ "inputs": {
+ "nixpkgs-lib": [
+ "devenv",
+ "nixpkgs"
+ ]
+ },
+ "locked": {
+ "lastModified": 1760948891,
+ "narHash": "sha256-TmWcdiUUaWk8J4lpjzu4gCGxWY6/Ok7mOK4fIFfBuU4=",
+ "owner": "hercules-ci",
+ "repo": "flake-parts",
+ "rev": "864599284fc7c0ba6357ed89ed5e2cd5040f0c04",
+ "type": "github"
+ },
+ "original": {
+ "owner": "hercules-ci",
+ "repo": "flake-parts",
+ "type": "github"
+ }
+ },
+ "flake-utils": {
+ "inputs": {
+ "systems": "systems"
+ },
+ "locked": {
+ "lastModified": 1731533236,
+ "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
+ "type": "github"
+ },
+ "original": {
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "type": "github"
+ }
+ },
+ "git-hooks": {
+ "inputs": {
+ "flake-compat": [
+ "devenv",
+ "flake-compat"
+ ],
+ "gitignore": "gitignore",
+ "nixpkgs": [
+ "devenv",
+ "nixpkgs"
+ ]
+ },
+ "locked": {
+ "lastModified": 1760663237,
+ "narHash": "sha256-BflA6U4AM1bzuRMR8QqzPXqh8sWVCNDzOdsxXEguJIc=",
+ "owner": "cachix",
+ "repo": "git-hooks.nix",
+ "rev": "ca5b894d3e3e151ffc1db040b6ce4dcc75d31c37",
+ "type": "github"
+ },
+ "original": {
+ "owner": "cachix",
+ "repo": "git-hooks.nix",
+ "type": "github"
+ }
+ },
+ "gitignore": {
+ "inputs": {
+ "nixpkgs": [
+ "devenv",
+ "git-hooks",
+ "nixpkgs"
+ ]
+ },
+ "locked": {
+ "lastModified": 1709087332,
+ "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
+ "owner": "hercules-ci",
+ "repo": "gitignore.nix",
+ "rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
+ "type": "github"
+ },
+ "original": {
+ "owner": "hercules-ci",
+ "repo": "gitignore.nix",
+ "type": "github"
+ }
+ },
+ "nix": {
+ "inputs": {
+ "flake-compat": [
+ "devenv",
+ "flake-compat"
+ ],
+ "flake-parts": [
+ "devenv",
+ "flake-parts"
+ ],
+ "git-hooks-nix": [
+ "devenv",
+ "git-hooks"
+ ],
+ "nixpkgs": [
+ "devenv",
+ "nixpkgs"
+ ],
+ "nixpkgs-23-11": [
+ "devenv"
+ ],
+ "nixpkgs-regression": [
+ "devenv"
+ ]
+ },
+ "locked": {
+ "lastModified": 1761648602,
+ "narHash": "sha256-H97KSB/luq/aGobKRuHahOvT1r7C03BgB6D5HBZsbN8=",
+ "owner": "cachix",
+ "repo": "nix",
+ "rev": "3e5644da6830ef65f0a2f7ec22830c46285bfff6",
+ "type": "github"
+ },
+ "original": {
+ "owner": "cachix",
+ "ref": "devenv-2.30.6",
+ "repo": "nix",
+ "type": "github"
+ }
+ },
+ "nixpkgs": {
+ "locked": {
+ "lastModified": 1761313199,
+ "narHash": "sha256-wCIACXbNtXAlwvQUo1Ed++loFALPjYUA3dpcUJiXO44=",
+ "owner": "cachix",
+ "repo": "devenv-nixpkgs",
+ "rev": "d1c30452ebecfc55185ae6d1c983c09da0c274ff",
+ "type": "github"
+ },
+ "original": {
+ "owner": "cachix",
+ "ref": "rolling",
+ "repo": "devenv-nixpkgs",
+ "type": "github"
+ }
+ },
+ "nixpkgs_2": {
+ "locked": {
+ "lastModified": 1766651565,
+ "narHash": "sha256-QEhk0eXgyIqTpJ/ehZKg9IKS7EtlWxF3N7DXy42zPfU=",
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "3e2499d5539c16d0d173ba53552a4ff8547f4539",
+ "type": "github"
+ },
+ "original": {
+ "owner": "NixOS",
+ "ref": "nixos-unstable",
+ "repo": "nixpkgs",
+ "type": "github"
+ }
+ },
+ "root": {
+ "inputs": {
+ "devenv": "devenv",
+ "flake-compat": "flake-compat_2",
+ "flake-utils": "flake-utils",
+ "nixpkgs": "nixpkgs_2"
+ }
+ },
+ "systems": {
+ "locked": {
+ "lastModified": 1681028828,
+ "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+ "owner": "nix-systems",
+ "repo": "default",
+ "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nix-systems",
+ "repo": "default",
+ "type": "github"
+ }
+ }
+ },
+ "root": "root",
+ "version": 7
+}
diff --git a/flake.nix b/flake.nix
new file mode 100644
index 0000000..d7b56b6
--- /dev/null
+++ b/flake.nix
@@ -0,0 +1,316 @@
+{
+ description = "wand_launcher Flake";
+
+ inputs = {
+ flake-utils.url = "github:numtide/flake-utils";
+ nixpkgs.url = "github:NixOS/nixpkgs?ref=nixos-unstable";
+ flake-compat = {
+ url = "github:edolstra/flake-compat";
+ flake = false;
+ };
+ devenv.url = "github:cachix/devenv";
+ };
+
+ nixConfig = {
+ extra-trusted-public-keys = "devenv.cachix.org-1:w1cLUi8dv3hnoSPGAuibQv+f9TZLr6cv/Hm9XgU50cw=";
+ extra-substituters = "https://devenv.cachix.org";
+ };
+
+ outputs = inputs: let
+ inherit (inputs) self;
+ in
+ inputs.flake-utils.lib.eachDefaultSystem
+ (
+ system: let
+ pkgs = import inputs.nixpkgs {
+ inherit system;
+ };
+ inherit (pkgs) lib;
+ in {
+ packages = {
+ wand-launcher = pkgs.python3Packages.buildPythonApplication {
+ name = "wand-launcher";
+ version = "unstable";
+ pyproject = true;
+
+ src = lib.cleanSource self;
+
+ build-system = with pkgs.python3Packages; [
+ pdm-backend
+ ];
+
+ buildInputs = with pkgs.python3Packages; [
+ setuptools
+ pyinstaller
+ ];
+
+ nativeBuildInputs = with pkgs; [
+ makeWrapper
+ ];
+
+ propagatedBuildInputs = with pkgs.python3Packages; [
+ # Qt6 GUI dependencies - pin to avoid version conflicts
+ pyqt6
+ # Core dependencies
+ requests
+ pyxdg
+ click
+ ];
+ # Set Qt plugin paths and library paths for proper Qt operation
+ makeWrapperArgs = [
+ "--set QT_QPA_PLATFORM_PLUGIN_PATH ${pkgs.qt6.qtbase}/lib/qt-6/plugins/platforms"
+ "--set QT_PLUGIN_PATH ${pkgs.qt6.qtbase}/lib/qt-6/plugins"
+ "--prefix LD_LIBRARY_PATH : ${pkgs.lib.makeLibraryPath (with pkgs; [qt6.qtbase qt6.qtwayland])}"
+ ];
+ doCheck = false;
+ pythonImportsCheck = ["wand_launcher"];
+ meta = {
+ description = "Tool to launch WeMod with games on Steam Deck/Linux";
+ homepage = "https://github.com/DeckCheatz/wand-launcher";
+ license = lib.licenses.mit;
+ mainProgram = "wand-launcher";
+ maintainers = with lib.maintainers; [shymega];
+ platforms = lib.platforms.linux;
+ };
+ };
+
+ # Real AppImage build - creates a proper .AppImage file
+ wand-launcher-appimage = let
+ wrapped = self.packages.${pkgs.stdenv.hostPlatform.system}.wand-launcher;
+
+ # Get AppImage runtime from a known, deterministic source
+ appimage-runtime = pkgs.fetchurl {
+ url = "https://github.com/AppImage/AppImageKit/releases/download/continuous/runtime-x86_64";
+ sha256 = "sha256-q01r9vJbvLD6kkkelQBz/UqeTuoa8PugzJ5TtmtYExI="; # Current hash
+ executable = true;
+ };
+
+ # Create AppDir structure
+ appDir = with pkgs;
+ pkgs.runCommand "wand-launcher-appdir" {
+ nativeBuildInputs = with pkgs; [
+ imagemagick
+ coreutils
+ findutils
+ bash
+ desktop-file-utils
+ ];
+ } ''
+ set -e
+ echo "Creating AppDir structure for AppImage..."
+
+ # Create AppDir structure
+ mkdir -p $out/usr/bin
+ mkdir -p $out/usr/lib
+ mkdir -p $out/usr/share/applications
+ mkdir -p $out/usr/share/icons/hicolor/256x256/apps
+
+ # Copy the main executable
+ echo "Copying main executable..."
+ cp ${lib.getExe wrapped} $out/usr/bin/
+ chmod +x $out/usr/bin/wand-launcher
+
+ # Copy Python and its dependencies
+ echo "Copying Python runtime and dependencies..."
+ cp -L ${python3}/bin/python* $out/usr/bin/ || true
+
+ # Copy the Python site-packages from our wrapped application
+ if [ -d "${wrapped}/lib" ]; then
+ cp -r ${wrapped}/lib/* $out/usr/lib/ || true
+ fi
+
+ # Copy Qt6 plugins directory
+ echo "Copying Qt6 plugins..."
+ mkdir -p $out/usr/lib/qt6
+ if [ -d "${qt6.qtbase}/lib/qt-6/plugins" ]; then
+ cp -r "${qt6.qtbase}/lib/qt-6/plugins" $out/usr/lib/qt6/
+ fi
+
+ # Copy essential Qt6 libraries
+ echo "Copying Qt6 libraries..."
+ mkdir -p $out/usr/lib
+ for lib in libQt6Core.so.6 libQt6Gui.so.6 libQt6Widgets.so.6; do
+ if [ -f "${qt6.qtbase}/lib/$lib" ]; then
+ cp -L "${qt6.qtbase}/lib/$lib" $out/usr/lib/ || echo "Warning: Failed to copy $lib"
+ fi
+ done
+
+ # Copy Wayland support if available
+ if [ -f "${qt6.qtwayland}/lib/libQt6WaylandClient.so.6" ]; then
+ cp -L "${qt6.qtwayland}/lib/libQt6WaylandClient.so.6" $out/usr/lib/ || true
+ fi
+
+ # Copy other essential libraries
+ echo "Copying other essential libraries..."
+ for lib in "${lib.makeLibraryPath [qt6.qtbase qt6.qtwayland]}"; do
+ if [ -d "$lib" ]; then
+ cp -L $lib/*.so* $out/usr/lib/ 2>/dev/null || true
+ fi
+ done
+
+ # Create desktop entry (required by AppImage spec)
+ echo "Creating desktop entry..."
+ cat > $out/wand-launcher.desktop << 'EOF'
+ [Desktop Entry]
+ Type=Application
+ Name=WeMod Launcher
+ Comment=Tool to launch WeMod with games on Steam Deck/Linux
+ Exec=wand-launcher
+ Icon=wand-launcher
+ Categories=Game;Utility;
+ Keywords=gaming;steam-deck;wine;cheats;wand;
+ StartupNotify=true
+ EOF
+
+ # Validate desktop file
+ desktop-file-validate $out/wand-launcher.desktop || echo "Warning: Desktop file validation failed"
+
+ # Copy to standard location (AppImage spec requirement)
+ cp $out/wand-launcher.desktop $out/usr/share/applications/
+
+ # Create icon (required by AppImage spec)
+ echo "Creating icon..."
+ convert -size 256x256 xc:"#4A90E2" \
+ -pointsize 48 -fill white -gravity center \
+ -annotate +0+0 "WM" \
+ $out/wand-launcher.png || echo "Warning: Icon creation failed"
+
+ # Copy icon to proper location
+ if [ -f "$out/wand-launcher.png" ]; then
+ cp $out/wand-launcher.png $out/usr/share/icons/hicolor/256x256/apps/
+ fi
+
+ # Create AppRun script (required by AppImage spec)
+ echo "Creating AppRun script..."
+ cat > $out/AppRun << 'APPRUN_EOF'
+ #!/bin/bash
+ HERE="$(dirname "$(readlink -f "$0")")"
+ export QT_QPA_PLATFORM_PLUGIN_PATH="$HERE/usr/lib/qt6/plugins/platforms"
+ export QT_PLUGIN_PATH="$HERE/usr/lib/qt6/plugins"
+ export LD_LIBRARY_PATH="$HERE/usr/lib:$LD_LIBRARY_PATH"
+ export PYTHONPATH="$HERE/usr/lib:$PYTHONPATH"
+ exec "$HERE/usr/bin/wand-launcher" "$@"
+ APPRUN_EOF
+
+ chmod +x $out/AppRun
+ '';
+ in
+ # Create a real AppImage using deterministic tools
+ pkgs.runCommand "wand-launcher.AppImage" {
+ nativeBuildInputs = with pkgs; [
+ squashfsTools
+ file
+ coreutils
+ ];
+ } ''
+ set -e
+ echo "Creating AppImage from AppDir..."
+
+ # Copy the AppDir to a writable location
+ cp -r ${appDir} ./AppDir
+ chmod -R u+w ./AppDir
+
+ # Ensure AppRun is at the root of AppDir
+ if [ ! -f ./AppDir/AppRun ]; then
+ echo "Error: AppRun not found!"
+ exit 1
+ fi
+
+ # Create squashfs filesystem from AppDir
+ echo "Creating squashfs filesystem..."
+ mksquashfs ./AppDir filesystem.squashfs -root-owned -noappend -no-exports -comp gzip
+
+ # Combine runtime and squashfs to create AppImage
+ echo "Combining runtime and squashfs..."
+ cat ${appimage-runtime} filesystem.squashfs > $out
+ chmod +x $out
+
+ # Verify the AppImage
+ echo "AppImage created successfully!"
+
+ # Test that it's executable
+ if [ -x "$out" ]; then
+ echo "AppImage is executable ✓"
+ else
+ echo "Error: AppImage is not executable!"
+ exit 1
+ fi
+ '';
+
+ default = pkgs.stdenv.mkDerivation {
+ name = "wand-launcher";
+
+ outputs = [
+ "out"
+ ];
+
+ src = self;
+
+ dontBuild = true;
+
+ installPhase = let
+ wrapped = self.packages.${pkgs.stdenv.hostPlatform.system}.wand-launcher-appimage;
+ in ''
+ runHook preInstall
+
+ cp ${wrapped} wand-launcher
+
+ install -Dt $out -m755 wand-launcher
+
+ runHook postInstall
+ '';
+ };
+ devenv-up = self.devShells.${pkgs.stdenv.hostPlatform.system}.default.config.procfileScript;
+ devenv-test = self.devShells.${pkgs.stdenv.hostPlatform.system}.default.config.test;
+ };
+
+ devShells.default = inputs.devenv.lib.mkShell {
+ inherit inputs pkgs;
+ modules = [
+ ({
+ pkgs,
+ inputs,
+ ...
+ }: {
+ packages = with pkgs;
+ [
+ git
+ pdm
+ pyright
+ ]
+ ++ (with pkgs.python3Packages; [
+ requests
+ pyxdg
+ setuptools
+ pyinstaller
+ tkinter
+ pyqt6
+ click
+ ]);
+
+ languages = {
+ nix.enable = true;
+ python = {
+ enable = true;
+ package = pkgs.python3;
+ };
+ shell.enable = true;
+ };
+
+ difftastic.enable = true;
+ devcontainer.enable = true;
+ })
+ ];
+ };
+ }
+ )
+ // {
+ overlays.default = final: prev: {
+ inherit
+ (self.packages.${final.system})
+ wand-launcher
+ wand-launcher-appimage
+ ;
+ };
+ };
+}
diff --git a/pdm.lock b/pdm.lock
new file mode 100644
index 0000000..adbe830
--- /dev/null
+++ b/pdm.lock
@@ -0,0 +1,248 @@
+# This file is @generated by PDM.
+# It is not intended for manual editing.
+
+[metadata]
+groups = ["default"]
+strategy = ["inherit_metadata"]
+lock_version = "4.5.0"
+content_hash = "sha256:a7ebdffdd8052b5027f405deef19c7bb0733c216ebd2a4b8a041669259a10c7f"
+
+[[metadata.targets]]
+requires_python = ">=3.11"
+
+[[package]]
+name = "altgraph"
+version = "0.17.4"
+summary = "Python graph (network) package"
+groups = ["default"]
+files = [
+ {file = "altgraph-0.17.4-py2.py3-none-any.whl", hash = "sha256:642743b4750de17e655e6711601b077bc6598dbfa3ba5fa2b2a35ce12b508dff"},
+ {file = "altgraph-0.17.4.tar.gz", hash = "sha256:1b5afbb98f6c4dcadb2e2ae6ab9fa994bbb8c1d75f4fa96d340f9437ae454406"},
+]
+
+[[package]]
+name = "certifi"
+version = "2024.8.30"
+requires_python = ">=3.6"
+summary = "Python package for providing Mozilla's CA Bundle."
+groups = ["default"]
+files = [
+ {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"},
+ {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"},
+]
+
+[[package]]
+name = "charset-normalizer"
+version = "3.4.0"
+requires_python = ">=3.7.0"
+summary = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
+groups = ["default"]
+files = [
+ {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c"},
+ {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944"},
+ {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee"},
+ {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c"},
+ {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6"},
+ {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea"},
+ {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc"},
+ {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5"},
+ {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594"},
+ {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c"},
+ {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365"},
+ {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129"},
+ {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236"},
+ {file = "charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99"},
+ {file = "charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27"},
+ {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6"},
+ {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf"},
+ {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db"},
+ {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1"},
+ {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03"},
+ {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284"},
+ {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15"},
+ {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8"},
+ {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2"},
+ {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719"},
+ {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631"},
+ {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b"},
+ {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565"},
+ {file = "charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7"},
+ {file = "charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9"},
+ {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114"},
+ {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed"},
+ {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250"},
+ {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920"},
+ {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64"},
+ {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23"},
+ {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc"},
+ {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d"},
+ {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88"},
+ {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90"},
+ {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b"},
+ {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d"},
+ {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482"},
+ {file = "charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67"},
+ {file = "charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b"},
+ {file = "charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079"},
+ {file = "charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e"},
+]
+
+[[package]]
+name = "freesimplegui"
+version = "5.1.1"
+summary = "The free-forever Python GUI framework."
+groups = ["default"]
+files = [
+ {file = "FreeSimpleGUI-5.1.1-py3-none-any.whl", hash = "sha256:d7629d5c94b55264d119bd2a89f52667d863ea7914d808e619aea29922ff842e"},
+ {file = "freesimplegui-5.1.1.tar.gz", hash = "sha256:2f0946c7ac221c997929181cbe7526e342fff5fc291a26d1d726287a5dd964fb"},
+]
+
+[[package]]
+name = "idna"
+version = "3.10"
+requires_python = ">=3.6"
+summary = "Internationalized Domain Names in Applications (IDNA)"
+groups = ["default"]
+files = [
+ {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"},
+ {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"},
+]
+
+[[package]]
+name = "macholib"
+version = "1.16.3"
+summary = "Mach-O header analysis and editing"
+groups = ["default"]
+marker = "sys_platform == \"darwin\""
+dependencies = [
+ "altgraph>=0.17",
+]
+files = [
+ {file = "macholib-1.16.3-py2.py3-none-any.whl", hash = "sha256:0e315d7583d38b8c77e815b1ecbdbf504a8258d8b3e17b61165c6feb60d18f2c"},
+ {file = "macholib-1.16.3.tar.gz", hash = "sha256:07ae9e15e8e4cd9a788013d81f5908b3609aa76f9b1421bae9c4d7606ec86a30"},
+]
+
+[[package]]
+name = "packaging"
+version = "24.2"
+requires_python = ">=3.8"
+summary = "Core utilities for Python packages"
+groups = ["default"]
+files = [
+ {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"},
+ {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"},
+]
+
+[[package]]
+name = "pefile"
+version = "2024.8.26"
+requires_python = ">=3.6.0"
+summary = "Python PE parsing module"
+groups = ["default"]
+marker = "sys_platform == \"win32\""
+files = [
+ {file = "pefile-2024.8.26-py3-none-any.whl", hash = "sha256:76f8b485dcd3b1bb8166f1128d395fa3d87af26360c2358fb75b80019b957c6f"},
+ {file = "pefile-2024.8.26.tar.gz", hash = "sha256:3ff6c5d8b43e8c37bb6e6dd5085658d658a7a0bdcd20b6a07b1fcfc1c4e9d632"},
+]
+
+[[package]]
+name = "pyinstaller"
+version = "4.5.1"
+requires_python = ">=3.6"
+summary = "PyInstaller bundles a Python application and all its dependencies into a single package."
+groups = ["default"]
+dependencies = [
+ "altgraph",
+ "importlib-metadata; python_version < \"3.8\"",
+ "macholib>=1.8; sys_platform == \"darwin\"",
+ "pefile>=2017.8.1; sys_platform == \"win32\"",
+ "pyinstaller-hooks-contrib>=2020.6",
+ "pywin32-ctypes>=0.2.0; sys_platform == \"win32\"",
+ "setuptools",
+]
+files = [
+ {file = "pyinstaller-4.5.1-py3-none-macosx_10_13_universal2.whl", hash = "sha256:ecc2baadeeefd2b6fbf39d13c65d4aa603afdda1c6aaaebc4577ba72893fee9e"},
+ {file = "pyinstaller-4.5.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:4d848cd782ee0893d7ad9fe2bfe535206a79f0b6760cecc5f2add831258b9322"},
+ {file = "pyinstaller-4.5.1-py3-none-manylinux2014_i686.whl", hash = "sha256:8f747b190e6ad30e2d2fd5da9a64636f61aac8c038c0b7f685efa92c782ea14f"},
+ {file = "pyinstaller-4.5.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:c587da8f521a7ce1b9efb4e3d0117cd63c92dc6cedff24590aeef89372f53012"},
+ {file = "pyinstaller-4.5.1-py3-none-win32.whl", hash = "sha256:fed9f5e4802769a416a8f2ca171c6be961d1861cc05a0b71d20dfe05423137e9"},
+ {file = "pyinstaller-4.5.1-py3-none-win_amd64.whl", hash = "sha256:aae456205c68355f9597411090576bb31b614e53976b4c102d072bbe5db8392a"},
+ {file = "pyinstaller-4.5.1.tar.gz", hash = "sha256:30733baaf8971902286a0ddf77e5499ac5f7bf8e7c39163e83d4f8c696ef265e"},
+]
+
+[[package]]
+name = "pyinstaller-hooks-contrib"
+version = "2024.10"
+requires_python = ">=3.8"
+summary = "Community maintained hooks for PyInstaller"
+groups = ["default"]
+dependencies = [
+ "importlib-metadata>=4.6; python_version < \"3.10\"",
+ "packaging>=22.0",
+ "setuptools>=42.0.0",
+]
+files = [
+ {file = "pyinstaller_hooks_contrib-2024.10-py3-none-any.whl", hash = "sha256:ad47db0e153683b4151e10d231cb91f2d93c85079e78d76d9e0f57ac6c8a5e10"},
+ {file = "pyinstaller_hooks_contrib-2024.10.tar.gz", hash = "sha256:8a46655e5c5b0186b5e527399118a9b342f10513eb1425c483fa4f6d02e8800c"},
+]
+
+[[package]]
+name = "pywin32-ctypes"
+version = "0.2.3"
+requires_python = ">=3.6"
+summary = "A (partial) reimplementation of pywin32 using ctypes/cffi"
+groups = ["default"]
+marker = "sys_platform == \"win32\""
+files = [
+ {file = "pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755"},
+ {file = "pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8"},
+]
+
+[[package]]
+name = "pyxdg"
+version = "0.28"
+summary = "PyXDG contains implementations of freedesktop.org standards in python."
+groups = ["default"]
+files = [
+ {file = "pyxdg-0.28-py2.py3-none-any.whl", hash = "sha256:bdaf595999a0178ecea4052b7f4195569c1ff4d344567bccdc12dfdf02d545ab"},
+ {file = "pyxdg-0.28.tar.gz", hash = "sha256:3267bb3074e934df202af2ee0868575484108581e6f3cb006af1da35395e88b4"},
+]
+
+[[package]]
+name = "requests"
+version = "2.32.3"
+requires_python = ">=3.8"
+summary = "Python HTTP for Humans."
+groups = ["default"]
+dependencies = [
+ "certifi>=2017.4.17",
+ "charset-normalizer<4,>=2",
+ "idna<4,>=2.5",
+ "urllib3<3,>=1.21.1",
+]
+files = [
+ {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"},
+ {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"},
+]
+
+[[package]]
+name = "setuptools"
+version = "75.5.0"
+requires_python = ">=3.9"
+summary = "Easily download, build, install, upgrade, and uninstall Python packages"
+groups = ["default"]
+files = [
+ {file = "setuptools-75.5.0-py3-none-any.whl", hash = "sha256:87cb777c3b96d638ca02031192d40390e0ad97737e27b6b4fa831bea86f2f829"},
+ {file = "setuptools-75.5.0.tar.gz", hash = "sha256:5c4ccb41111392671f02bb5f8436dfc5a9a7185e80500531b133f5775c4163ef"},
+]
+
+[[package]]
+name = "urllib3"
+version = "2.2.3"
+requires_python = ">=3.8"
+summary = "HTTP library with thread-safe connection pooling, file post, and more."
+groups = ["default"]
+files = [
+ {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"},
+ {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"},
+]
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..56b3e47
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,133 @@
+[project]
+name = "wand-launcher"
+version = "1.503"
+description = "Tool made to launch the popular Game Trainer / Cheat tool: Wand along with your game (made for steam-runtime version in Linux)."
+authors = [
+ {name = "The DeckCheatz Team", email = "deckcheatz@noreply.github.com"},
+]
+dependencies = [
+ "requests>=2.31.0",
+ "pyxdg>=0.28",
+ "setuptools>=75.1.0.post0",
+ "click>=8.0.0",
+]
+requires-python = ">=3.11"
+readme = "README.md"
+license = {text = "MIT"}
+keywords = ["gaming", "steam-deck", "wine", "cheats", "wand", "launcher"]
+classifiers = [
+ "Development Status :: 4 - Beta",
+ "Intended Audience :: End Users/Desktop",
+ "License :: OSI Approved :: MIT License",
+ "Operating System :: POSIX :: Linux",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+ "Topic :: Games/Entertainment",
+ "Topic :: System :: Emulators",
+]
+
+[build-system]
+requires = ["pdm-backend"]
+build-backend = "pdm.backend"
+
+[tool.pdm.scripts]
+# Development scripts
+dev-install = "pip install -e .[dev,qt,build]"
+test = "pytest tests/"
+lint = "flake8 src/"
+format = "black src/ tests/"
+type-check = "mypy src/"
+
+# Build scripts
+build-qt = "pyinstaller wand-launcher.spec"
+package-exe = {call = "wand_launcher.pyinstaller:install"}
+clean = "rm -rf build/ dist/ *.egg-info/"
+
+# Combined workflows
+check = {composite = ["format", "lint", "type-check", "test"]}
+build-all = {composite = ["clean", "build-qt"]}
+
+[project.scripts]
+wand-launcher = "wand_launcher.cli:main"
+
+[tool.pdm]
+distribution = true
+
+[project.optional-dependencies]
+dev = [
+ "pytest>=7.0.0",
+ "black>=22.0.0",
+ "flake8>=4.0.0",
+ "mypy>=0.950",
+]
+build = [
+ "pyinstaller>=5.0.0",
+ "setuptools>=75.1.0.post0",
+]
+qt = [
+ "PyQt6>=6.4.0,<6.10.0",
+ "PyQt6-Qt6>=6.4.0,<6.10.0",
+ "PyQt6-sip>=13.4.0,<14.0.0",
+]
+
+[project.urls]
+Homepage = "https://github.com/DeckCheatz/wand-launcher"
+Repository = "https://github.com/DeckCheatz/wand-launcher.git"
+Issues = "https://github.com/DeckCheatz/wand-launcher/issues"
+Documentation = "https://github.com/DeckCheatz/wand-launcher/blob/main/README.md"
+
+[tool.black]
+line-length = 88
+target-version = ['py311']
+include = '\.pyi?$'
+extend-exclude = '''
+/(
+ __pycache__
+ | \.git
+ | \.mypy_cache
+ | \.pytest_cache
+ | \.venv
+ | wand_venv
+ | build
+ | dist
+)/
+'''
+
+[tool.mypy]
+python_version = "3.11"
+warn_return_any = true
+warn_unused_configs = true
+disallow_untyped_defs = true
+disallow_incomplete_defs = true
+check_untyped_defs = true
+disallow_untyped_decorators = true
+no_implicit_optional = true
+warn_redundant_casts = true
+warn_unused_ignores = true
+warn_no_return = true
+warn_unreachable = true
+strict_equality = true
+
+[tool.pytest.ini_options]
+minversion = "7.0"
+addopts = "-ra -q --strict-markers"
+testpaths = [
+ "tests",
+]
+pythonpath = [
+ "src",
+]
+
+[tool.flake8]
+max-line-length = 88
+extend-ignore = ["E203", "W503"]
+exclude = [
+ ".git",
+ "__pycache__",
+ "build",
+ "dist",
+ "*.egg-info",
+ ".venv",
+ "wand_venv",
+]
diff --git a/requirements.txt b/requirements.txt
deleted file mode 100644
index 8c9043c..0000000
--- a/requirements.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-FreeSimpleGUI
-requests
diff --git a/shell.nix b/shell.nix
new file mode 100644
index 0000000..6070f33
--- /dev/null
+++ b/shell.nix
@@ -0,0 +1,17 @@
+# SPDX-FileCopyrightText: 2024-2025 The DeckCheatz Developers
+#
+# SPDX-License-Identifier: AGPL-3.0-only
+(
+ import
+ (
+ let
+ lock = builtins.fromJSON (builtins.readFile ./flake.lock);
+ in
+ fetchTarball {
+ url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz";
+ sha256 = lock.nodes.flake-compat.locked.narHash;
+ }
+ )
+ {src = ./.;}
+)
+.shellNix
diff --git a/src/wemod_launcher/__init__.py b/src/wemod_launcher/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/wemod_launcher/cli.py b/src/wemod_launcher/cli.py
new file mode 100644
index 0000000..466c0e4
--- /dev/null
+++ b/src/wemod_launcher/cli.py
@@ -0,0 +1,75 @@
+import click
+import sys
+from wemod_launcher.utils.logger import LoggingHandler
+from wemod_launcher.utils.configuration import Configuration
+from wemod_launcher.utils.consts import Consts
+
+log = LoggingHandler(__name__).get_logger()
+cfg = Configuration()
+consts = Consts()
+
+
+@click.command()
+@click.option("--version", is_flag=True, help="Show version information")
+@click.option("--no-gui", is_flag=True, help="Run in command-line mode only")
+@click.option("--debug", is_flag=True, help="Enable debug logging")
+def main(version, no_gui, debug):
+ """WeMod Launcher - Tool to launch WeMod with games on Steam Deck/Linux.
+
+ This tool provides a GUI interface for launching WeMod with games.
+ Run without arguments to start the graphical interface.
+ """
+ if debug:
+ log.setLevel("DEBUG")
+ log.debug("Debug logging enabled")
+
+ if version:
+ click.echo("WeMod Launcher v1.503")
+ click.echo("Qt6-based GUI for Steam Deck compatibility")
+ return
+
+ if no_gui:
+ click.echo("Command-line mode not yet implemented")
+ click.echo("Please run without --no-gui to use the graphical interface")
+ return
+
+ log.info("Welcome to WeMod Launcher!")
+
+ # Only import and use Qt components when needed for GUI
+ try:
+ # Check if we have a display available
+ import os
+
+ if not os.environ.get("DISPLAY") and not os.environ.get("WAYLAND_DISPLAY"):
+ if os.environ.get("XDG_SESSION_TYPE") != "tty":
+ log.warning("No display detected, setting Qt platform to offscreen")
+ os.environ["QT_QPA_PLATFORM"] = "offscreen"
+
+ from wemod_launcher.gfx.welcome_screen import WelcomeScreenGfx
+
+ WelcomeScreenGfx().run()
+ from wemod_launcher.gfx.download_popup import DownloadPopupGfx
+
+ dl = DownloadPopupGfx(
+ "dotnet48.exe",
+ "https://download.visualstudio.microsoft.com/download/pr/2d6bb6b2-226a-4baa-bdec-798822606ff1/8494001c276a4b96804cde7829c04d7f/ndp48-x86-x64-allos-enu.exe",
+ )
+ result = dl.run()
+ log.info(f"Download result: {result}")
+
+ except ImportError as e:
+ log.error(f"Failed to import Qt components: {e}")
+ click.echo("Error: Qt6 components not available. Please install PyQt6.")
+ sys.exit(1)
+ except Exception as e:
+ log.error(f"Failed to start GUI: {e}")
+ log.debug(f"Exception details: {type(e).__name__}: {e}")
+ click.echo(f"Error: Failed to start graphical interface: {e}")
+ click.echo(
+ "Try setting QT_QPA_PLATFORM=offscreen or ensure X11/Wayland is available"
+ )
+ sys.exit(1)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/constutils.py b/src/wemod_launcher/const_utils.py
similarity index 85%
rename from constutils.py
rename to src/wemod_launcher/const_utils.py
index ef95ef7..4f03047 100755
--- a/constutils.py
+++ b/src/wemod_launcher/const_utils.py
@@ -10,7 +10,7 @@
from urllib import request
# Import consts
-from consts import (
+from wemod_launcher.consts import (
STEAM_COMPAT_FOLDER,
BASE_STEAM_COMPAT,
SCAN_FOLDER,
@@ -18,14 +18,14 @@
INIT_FILE,
)
-from coreutils import (
+from wemod_launcher.core_utils import (
exit_with_message,
get_user_input,
popup_options,
show_message,
)
-from corenodep import (
+from wemod_launcher.core_nodeps import (
load_conf_setting,
save_conf_setting,
parse_version,
@@ -38,11 +38,11 @@
Optional,
)
-from coreutils import (
+from wemod_launcher.core_utils import (
log,
)
-from mainutils import (
+from wemod_launcher.main_utils import (
popup_execute,
)
@@ -120,12 +120,12 @@ def ensure_wine(verstr: Optional[str] = None) -> str:
else:
exit_with_message(
"Missing Prefix",
- "Error: wine prefix is missing.\nMake sure you run the game without wemod-launcher once",
+ "Error: wine prefix is missing.\nMake sure you run the game without wand-launcher once",
ask_for_log=True,
)
-# Scan the steam compat folder for WeMod installed prefixes
+# Scan the steam compat folder for Wand installed prefixes
def scanfolderforversions(
current_version_parts: List[Union[int, None]] = [None, None]
) -> List[Union[Optional[List[int]], Optional[str]]]:
@@ -139,12 +139,12 @@ def scanfolderforversions(
# For all folders in steam compat
for folder in os.listdir(SCAN_FOLDER):
- # Get the version file, folder path and check if WeMod is installed
+ # Get the version file, folder path and check if Wand is installed
folder_path = os.path.join(SCAN_FOLDER, folder)
version_file = os.path.join(folder_path, "version")
if os.path.isdir(folder_path) and os.path.exists(
- os.path.join(folder_path, "pfx", ".wemod_installer")
+ os.path.join(folder_path, "pfx", ".wand_installer")
):
folder_version_str = read_file(version_file)
folder_version_parts = parse_version(folder_version_str)
@@ -173,8 +173,7 @@ def scanfolderforversions(
priority == 2
and (
not closest_version_folder
- or folder_version_parts[1]
- > closest_version_number[1]
+ or folder_version_parts[1] > closest_version_number[1]
)
):
priority = 2
@@ -189,8 +188,7 @@ def scanfolderforversions(
priority == 3
and (
not closest_version_folder
- or folder_version_parts[1]
- < closest_version_number[1]
+ or folder_version_parts[1] < closest_version_number[1]
)
):
priority = 3
@@ -202,13 +200,10 @@ def scanfolderforversions(
priority == 4
and (
not closest_version_folder
- or folder_version_parts[0]
- > closest_version_number[0]
+ or folder_version_parts[0] > closest_version_number[0]
or (
- folder_version_parts[0]
- == closest_version_number[0]
- and folder_version_parts[1]
- > closest_version_number[1]
+ folder_version_parts[0] == closest_version_number[0]
+ and folder_version_parts[1] > closest_version_number[1]
)
)
):
@@ -221,13 +216,10 @@ def scanfolderforversions(
priority == 5
and (
not closest_version_folder
- or folder_version_parts[0]
- < closest_version_number[0]
+ or folder_version_parts[0] < closest_version_number[0]
or (
- folder_version_parts[0]
- == closest_version_number[0]
- and folder_version_parts[1]
- < closest_version_number[1]
+ folder_version_parts[0] == closest_version_number[0]
+ and folder_version_parts[1] < closest_version_number[1]
)
)
):
@@ -255,24 +247,22 @@ def scanfolderforversions(
prefix_path_seven = folder_path
if prefix_path_seven:
- from mainutils import copy_folder_with_progress
+ from wemod_launcher.main_utils import copy_folder_with_progress
prefixesfolder = os.path.join(SCAN_FOLDER, "prefix")
os.makedirs(prefixesfolder, exists_ok=True)
protonconfminor = load_conf_setting("ProtonMinorSeven")
- prefixesfile = os.path.join(
- prefixesfolder, f"Proton7.{protonconfminor}.zip"
- )
+ prefixesfile = os.path.join(prefixesfolder, f"Proton7.{protonconfminor}.zip")
# ask the user to upload the prefix if they have one
prresp = show_message(
- f"In your scan folder, the online missing prefix version with GE-Proton 7 (.{protonconfminor}) was found.\nPlease, be so kind and click yes to zip the prefix\nand that upload it to something like https://www.sendgb.com/; \nlastly, paste the link in a WeMod-Launcher issue on GitHub",
+ f"In your scan folder, the online missing prefix version with GE-Proton 7 (.{protonconfminor}) was found.\nPlease, be so kind and click yes to zip the prefix\nand that upload it to something like https://www.sendgb.com/; \nlastly, paste the link in a Wand-Launcher issue on GitHub",
"GE-Proton7 found",
60,
True,
)
if prresp == "Yes":
sevenpfx = os.path.join(prefix_path_seven, "pfx")
- seveninit = os.path.join(sevenpfx, ".wemod_installer")
+ seveninit = os.path.join(sevenpfx, ".wand_installer")
initcont = read_file(seveninit)
with open(seveninit, "w") as init:
@@ -317,9 +307,7 @@ def winetricks(command: str, proton_bin: str) -> int:
winetricks_sh,
)
log(f"setting exec permissions on '{winetricks_sh}'")
- process = subprocess.Popen(
- f"sh -c 'chmod +x {winetricks_sh}'", shell=True
- )
+ process = subprocess.Popen(f"sh -c 'chmod +x {winetricks_sh}'", shell=True)
exit_code = process.wait()
if exit_code != 0:
@@ -361,7 +349,7 @@ def troubleshooter() -> None:
log("Troubleshooter start loop")
ret = popup_options(
"Troubleshooter",
- "Did WeMod work as expected?\nIf not, troubleshoot common problems with WeMod.\nDeleting the game prefix usually helps.\nDeleting Wemod.exe helps if wemod updates their program.\nTo use the Troubleshooter after it was disabled,\nyou can add TROUBLESHOOT=true in front of the launch command",
+ "Did Wand work as expected?\nIf not, troubleshoot common problems with Wand.\nDeleting the game prefix usually helps.\nDeleting Wand.exe helps if wand updates their program.\nTo use the Troubleshooter after it was disabled,\nyou can add TROUBLESHOOT=true in front of the launch command",
[
[
"Disable troubleshooter globally",
@@ -371,8 +359,8 @@ def troubleshooter() -> None:
"Enable troubleshooter globally",
"Enable troubleshooter for this game",
],
- ["Delete game prefix", "Delete WeMod.exe"],
- ["Close wemod-launcher"],
+ ["Delete game prefix", "Delete Wand.exe"],
+ ["Close wand-launcher"],
],
120,
)
@@ -387,9 +375,9 @@ def troubleshooter() -> None:
elif ret == "Enable troubleshooter for this game":
with open(INIT_FILE, "w") as init:
init.write("true")
- elif ret == "Delete WeMod.exe":
+ elif ret == "Delete Wand.exe":
try:
- os.remove(os.path.join(SCRIPT_PATH, "wemod_bin", "WeMod.exe"))
+ os.remove(os.path.join(SCRIPT_PATH, "wand_bin", "Wand.exe"))
except Exception as e:
pass
elif ret == "Delete game prefix":
@@ -397,6 +385,6 @@ def troubleshooter() -> None:
shutil.rmtree(BASE_STEAM_COMPAT)
except Exception as e:
pass
- elif not ret or ret == "Close wemod-launcher":
+ elif not ret or ret == "Close wand-launcher":
runtro = False
log("Closing troubleshooter as requested")
diff --git a/consts.py b/src/wemod_launcher/consts.py
similarity index 66%
rename from consts.py
rename to src/wemod_launcher/consts.py
index 3ebe400..60fbfd2 100755
--- a/consts.py
+++ b/src/wemod_launcher/consts.py
@@ -3,13 +3,19 @@
import os
import sys
+from pathlib import Path
+from wemod_launcher.utils.configuration import Configuration
+from wemod_launcher.utils.consts import Consts
+from wemod_launcher.pfx.wine_utils import WineUtils
-from corenodep import (
+cfg: Configuration = Configuration()
+
+from wemod_launcher.core_nodeps import (
load_conf_setting,
winpath,
)
-from coreutils import (
+from wemod_launcher.core_utils import (
exit_with_message,
log,
)
@@ -22,7 +28,7 @@
def getbatcmd():
- batf = os.path.join(SCRIPT_PATH, "wemod.bat")
+ batf = os.path.join(SCRIPT_PATH, "wand.bat")
if not os.path.isfile(batf):
try:
import urllib.request
@@ -35,7 +41,7 @@ def getbatcmd():
repo_name = load_conf_setting("RepoName")
if not repo_name:
- repo_name = "wemod-launcher"
+ repo_name = "wand-launcher"
# save_conf_setting("RepoName", repo_name)
log("RepoName not set in config. Using: " + repo_name)
@@ -49,7 +55,7 @@ def getbatcmd():
repo_concat = repo_user + "/" + repo_name
- url = f"https://raw.githubusercontent.com/{repo_concat}/refs/heads/main/wemod.bat"
+ url = f"https://raw.githubusercontent.com/{repo_concat}/refs/heads/main/wand.bat"
urllib.request.urlretrieve(url, batf)
except Exception as e:
@@ -57,7 +63,7 @@ def getbatcmd():
if not os.path.isfile(batf):
exit_with_message(
"Missing bat",
- "The 'wemod.bat' file is missing and could not be downloaded. Exiting",
+ "The 'wand.bat' file is missing and could not be downloaded. Exiting",
)
return ["start", winpath(batf)]
@@ -66,23 +72,33 @@ def getbatcmd():
BAT_COMMAND = getbatcmd()
-# Function to grab the Steam Compat Data Path
+# Function to grab the Steam Compat Data path
def get_compat() -> str:
ccompat = load_conf_setting("SteamCompatDataPath")
wcompat = load_conf_setting("WinePrefixPath")
if not wcompat and os.getenv("WINE_PREFIX_PATH"):
- os.environ["WINEPREFIX"] = os.getenv("WINE_PREFIX_PATH")
- ecompat = os.getenv("STEAM_COMPAT_DATA_PATH")
+ os.environ["WINEPREFIX"] = os.getenv(
+ "WINE_PREFIX_PATH", ""
+ ) # TODO: Either use try/except here, or a sane default.
+ ecompat = os.getenv(
+ "STEAM_COMPAT_DATA_PATH", ""
+ ) # TODO: Either use try/except here, or a sane default.
nogame = False
# STEAM_COMPAT_DATA_PATH not set
if not ecompat:
if os.getenv("WINEPREFIX") or wcompat:
ecompat = wcompat
if not ecompat:
- ecompat = os.getenv("WINEPREFIX")
+ ecompat = os.getenv(
+ "WINEPREFIX", ""
+ ) # TODO: Either use try/except here, or a sane default.
nogame = True
- wine = os.getenv("WINE")
- tools = os.getenv("STEAM_COMPAT_TOOL_PATHS")
+ wine = os.getenv(
+ "WINE", ""
+ ) # TODO: Either use try/except here, or a sane default.
+ tools = os.getenv(
+ "STEAM_COMPAT_TOOL_PATHS", ""
+ ) # TODO: Either use try/except here, or a sane default.
# if tools set and wine not in compat tools
if (
tools
@@ -123,21 +139,36 @@ def get_compat() -> str:
return ecompat
-BASE_STEAM_COMPAT = get_compat()
-STEAM_COMPAT_FOLDER = os.path.dirname(BASE_STEAM_COMPAT)
+def get_scan_folder() -> str:
+ scan_folder: str
+ try:
+ scan_folder = os.getenv("SCANFOLDER") or ""
+ except KeyError:
+ scan_folder = ""
+ pass
+
+ if not Path(scan_folder).exists():
+ scan_folder = load_conf_setting("ScanFolder") or ""
+
+ if not Path(scan_folder).exists():
+ scan_folder = STEAM_COMPAT_FOLDER
+
+ return scan_folder
-def get_scan_folder():
- wscanfolder = os.getenv("SCANFOLDER")
- cscanfolder = load_conf_setting("ScanFolder")
- if not wscanfolder:
- wscanfolder = cscanfolder
- if not wscanfolder:
- wscanfolder = STEAM_COMPAT_FOLDER
- return wscanfolder
+CONSTS = Consts()
+WINE_UTILS = WineUtils()
+SCRIPT_IMP_FILE = str(CONSTS.SCRIPT_PATH)
+SCRIPT_PATH = str(CONSTS.SCRIPT_RUNTIME_DIR)
+BAT_COMMAND = [
+ "start",
+ WINE_UTILS.native_path(os.path.join(SCRIPT_IMP_FILE, "wemod.bat")),
+]
+BASE_STEAM_COMPAT = get_scan_folder()
+STEAM_COMPAT_FOLDER = str(CONSTS.STEAM_COMPAT_DATA_DIR)
SCAN_FOLDER = get_scan_folder()
WINETRICKS = os.path.join(SCRIPT_PATH, "winetricks")
WINEPREFIX = os.path.join(BASE_STEAM_COMPAT, "pfx")
-INIT_FILE = os.path.join(WINEPREFIX, ".wemod_installer")
+INIT_FILE = os.path.join(WINEPREFIX, ".wand_installer")
diff --git a/corenodep.py b/src/wemod_launcher/core_nodeps.py
similarity index 93%
rename from corenodep.py
rename to src/wemod_launcher/core_nodeps.py
index 9ea9d15..a39f4c1 100755
--- a/corenodep.py
+++ b/src/wemod_launcher/core_nodeps.py
@@ -17,7 +17,7 @@
Union,
)
-CONFIG_PATH = os.path.join(SCRIPT_PATH, "wemod.conf")
+CONFIG_PATH = os.path.join(SCRIPT_PATH, "wand.conf")
DEF_SECTION = "Settings"
CONFIG = configparser.ConfigParser()
CONFIG.optionxform = str
@@ -36,7 +36,7 @@ def check_dependencies(requirements_file: str) -> bool:
try:
importlib.import_module(package)
except ImportError:
- from coreutils import log
+ from wemod_launcher.core_utils import log
log(f"Package '{package}' is missing")
ret = False
@@ -44,9 +44,7 @@ def check_dependencies(requirements_file: str) -> bool:
# Read a setting of the config file
-def load_conf_setting(
- setting: str, section: str = DEF_SECTION
-) -> Optional[str]:
+def load_conf_setting(setting: str, section: str = DEF_SECTION) -> Optional[str]:
if section in CONFIG and setting in CONFIG[section]:
return CONFIG[section][setting]
return None
@@ -56,7 +54,7 @@ def load_conf_setting(
def save_conf_setting(
setting: str, value: Optional[str] = None, section: str = DEF_SECTION
) -> None:
- from coreutils import log
+ from wemod_launcher.core_utils import log
if not isinstance(section, str):
log("Error adding the given section: Not a string")
@@ -133,9 +131,7 @@ def winpath(path: str, dobble: bool = True, addfront: str = "Z:") -> str:
return addfront + path.replace(os.sep, "\\")
-def split_list_by_delimiter(
- input_list: List[str], delimiter: str
-) -> List[List[str]]:
+def split_list_by_delimiter(input_list: List[str], delimiter: str) -> List[List[str]]:
result = []
current_sublist = []
for item in input_list:
diff --git a/coreutils.py b/src/wemod_launcher/core_utils.py
similarity index 82%
rename from coreutils.py
rename to src/wemod_launcher/core_utils.py
index 4cb5fd7..31f9571 100755
--- a/coreutils.py
+++ b/src/wemod_launcher/core_utils.py
@@ -15,7 +15,7 @@
Any,
)
-from corenodep import (
+from wemod_launcher.core_nodeps import (
join_lists_with_delimiter,
load_conf_setting,
save_conf_setting,
@@ -31,30 +31,30 @@
# Function for logging messages
def log(message: Optional[str] = None, open_log: bool = False) -> None:
- oswemodlog = os.getenv("WEMOD_LOG")
- wemodlog = oswemodlog
- cowemodlog = load_conf_setting("WeModLog")
- if not wemodlog:
- wemodlog = cowemodlog
- if wemodlog != "":
+ oswandlog = os.getenv("WAND_LOG")
+ wandlog = oswandlog
+ cowandlog = load_conf_setting("WandLog")
+ if not wandlog:
+ wandlog = cowandlog
+ if wandlog != "":
try:
- if not wemodlog:
- raise Exception("WeModLog unset")
- elif os.path.isabs(wemodlog):
- os.makedirs(os.path.dirname(wemodlog), exist_ok=True)
+ if not wandlog:
+ raise Exception("WandLog unset")
+ elif os.path.isabs(wandlog):
+ os.makedirs(os.path.dirname(wandlog), exist_ok=True)
else:
os.makedirs(
os.path.dirname(
- os.path.abspath(os.path.join(SCRIPT_PATH, wemodlog))
+ os.path.abspath(os.path.join(SCRIPT_PATH, wandlog))
),
exist_ok=True,
)
except:
- wemodlog = "wemod.log"
- if not oswemodlog: # Only save if not a environment var
- save_conf_setting("WeModLog", wemodlog)
+ wandlog = "wand.log"
+ if not oswandlog: # Only save if not a environment var
+ save_conf_setting("WandLog", wandlog)
- new_message = f"WeModLog path was not given or invalid using path '{wemodlog}'\nIf you don't want to generate a log file, use WEMOD_LOG='' or set the config to WeModLog=''"
+ new_message = f"WandLog path was not given or invalid using path '{wandlog}'\nIf you don't want to generate a log file, use WAND_LOG='' or set the config to WandLog=''"
if message == None:
message = new_message
else:
@@ -63,13 +63,13 @@ def log(message: Optional[str] = None, open_log: bool = False) -> None:
message = str(message)
if message and message[-1] != "\n":
message += "\n"
- if not os.path.isabs(wemodlog):
- wemodlog = os.path.abspath(os.path.join(SCRIPT_PATH, wemodlog))
- with open(wemodlog, "a") as f:
+ if not os.path.isabs(wandlog):
+ wandlog = os.path.abspath(os.path.join(SCRIPT_PATH, wandlog))
+ with open(wandlog, "a") as f:
if message != None:
f.write(message)
if open_log:
- os.system(f"xdg-open '{wemodlog}'")
+ os.system(f"xdg-open '{wandlog}'")
# Function to display a message
@@ -119,9 +119,7 @@ def exit_with_message(
ask_for_log: bool = False,
) -> None:
if ask_for_log:
- exit_message += (
- "\nDo you want to open the log for more info on the exit error?"
- )
+ exit_message += "\nDo you want to open the log for more info on the exit error?"
ret = show_message(
exit_message,
title,
@@ -153,9 +151,7 @@ def pip(command: str, venv_path: Optional[str] = None) -> int:
venv_path = os.path.abspath(os.path.join(SCRIPT_PATH, venv_path))
pos_pip = None
if venv_path:
- python_executable = os.path.join(
- venv_path, os.path.basename(sys.executable)
- )
+ python_executable = os.path.join(venv_path, os.path.basename(sys.executable))
pos_pip = os.path.join(venv_path, "bin", "pip")
if not os.path.isfile(pos_pip):
pos_pip = None
@@ -200,10 +196,10 @@ def pip(command: str, venv_path: Optional[str] = None) -> int:
return 99
else:
show_message(
- "The pip inside the virtual environment reported an error.\nThis may require the deletion of the virtual environment folder;\nby default, the folder is named named wemod_venv\nand is located inside the wemod-launcher folder"
+ "The pip inside the virtual environment reported an error.\nThis may require the deletion of the virtual environment folder;\nby default, the folder is named named wand_venv\nand is located inside the wand-launcher folder"
)
log(
- f"A pip error occurred.\nThis may require the deletion of the virtual environment folder;\nby default, the folder is named named wemod_venv\nand is located inside the wemod-launcher folder.\nError message:\n\t{stdout}\n\t{stderr}"
+ f"A pip error occurred.\nThis may require the deletion of the virtual environment folder;\nby default, the folder is named named wand_venv\nand is located inside the wand-launcher folder.\nError message:\n\t{stdout}\n\t{stderr}"
)
# Try to use the built-in pip
@@ -269,9 +265,7 @@ def pip(command: str, venv_path: Optional[str] = None) -> int:
return process.returncode
-def monitor_file(
- ttfile: str, tout: int, responsefile: str, bout: Optional[int] = 60
-):
+def monitor_file(ttfile: str, tout: int, responsefile: str, bout: Optional[int] = 60):
import time
cout = os.getenv("WAIT_ON_GAMECLOSE")
@@ -299,7 +293,7 @@ def monitor_file(
log("Finished early game close detention")
else:
log(
- "The game ran long enough, wemod is now allowed to close on game exit, therefore early game close detention is finished"
+ "The game ran long enough, wand is now allowed to close on game exit, therefore early game close detention is finished"
)
@@ -310,33 +304,31 @@ def bat_respond(responsefile: str, bout: Optional[int]) -> Optional[bool]:
if bout != None:
batresp = show_message(
returnmessage
- + f'\nYou can still use wemod by clicking "Yes",\nthis will keep wemod open in the backround\nIf you want to close WeMod click "No"\nWeMod will automaticly close in {bout} seconds, if nothing is done',
+ + f'\nYou can still use wand by clicking "Yes",\nthis will keep wand open in the backround\nIf you want to close Wand click "No"\nWand will automaticly close in {bout} seconds, if nothing is done',
"BAT Warning",
bout,
True,
)
log(
- f"The user selected {batresp} after being asked to wait longer for WeMod"
+ f"The user selected {batresp} after being asked to wait longer for Wand"
)
if bout == None or batresp == "Yes":
show_message(
returnmessage
- + '\nClick "OK" ONLY if you are ready to close WeMod\nTo KEEP it open, just minimize THIS message box.',
+ + '\nClick "OK" ONLY if you are ready to close Wand\nTo KEEP it open, just minimize THIS message box.',
"BAT Warning",
None,
False,
)
- log("The user accepted to close WeMod")
+ log("The user accepted to close Wand")
os.remove(responsefile)
return True
return None
# Function to handle caching of files
-def cache(
- file_path: str, default: Callable[[str], None], simple: bool = False
-) -> str:
- CACHE = os.path.join(SCRIPT_PATH, ".cache")
+def cache(file_path: str, default: Callable[[str], None], simple: bool = False) -> str:
+ CACHE = "/tmp/wemod-launcher/.cache"
if not os.path.isdir(CACHE):
log("Cache dir not found. Creating...")
os.mkdir(CACHE)
@@ -365,9 +357,7 @@ def popup_options(
import FreeSimpleGUI as sg
# Define the layout based on provided options
- buttons_layout = [
- [sg.Button(option) for option in row] for row in options
- ]
+ buttons_layout = [[sg.Button(option) for option in row] for row in options]
layout = [[sg.Text(message)]] + buttons_layout
close = True
@@ -443,7 +433,7 @@ def get_user_input(
def script_manager() -> None:
- script_name = "wemod-launcher"
+ script_name = "wand-launcher"
script_version = "1.535"
last_name = load_conf_setting("ScriptName")
last_version = load_conf_setting("Version")
@@ -455,17 +445,13 @@ def script_manager() -> None:
if last_version:
try:
if float(last_version) < float(script_version):
- log(
- f"Config on version {last_version} updating to {script_version}"
- )
+ log(f"Config on version {last_version} updating to {script_version}")
elif float(last_version) > float(script_version):
log(
f"Warning: config on version {last_version}; downgrading to {script_version}"
)
except Exception as e:
- log(
- f"Warning: config error '{e}'; changing version to {script_version}"
- )
+ log(f"Warning: config error '{e}'; changing version to {script_version}")
else:
log("Adding script version to config")
@@ -473,6 +459,6 @@ def script_manager() -> None:
save_conf_setting("Version", script_version)
log(f"The script {script_name} is running on version {script_version}")
print(
- f"The WeMod script {script_name} is running on version {script_version}"
+ f"The Wand script {script_name} is running on version {script_version}"
)
return
diff --git a/src/wemod_launcher/gfx/__init__.py b/src/wemod_launcher/gfx/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/wemod_launcher/gfx/download_popup.py b/src/wemod_launcher/gfx/download_popup.py
new file mode 100644
index 0000000..ee413da
--- /dev/null
+++ b/src/wemod_launcher/gfx/download_popup.py
@@ -0,0 +1,325 @@
+from xdg.BaseDirectory import save_cache_path
+import requests
+import os
+import threading
+from pathlib import Path
+import signal
+import json
+import sys
+from PyQt6.QtWidgets import (
+ QApplication,
+ QDialog,
+ QVBoxLayout,
+ QHBoxLayout,
+ QLabel,
+ QPushButton,
+ QProgressBar,
+ QMessageBox,
+)
+from PyQt6.QtCore import Qt, QThread, pyqtSignal, QTimer
+from PyQt6.QtGui import QCloseEvent
+
+# Assuming utils.logger is available and correctly configured
+from ..utils.logger import LoggingHandler
+
+
+class DownloadCancelledByUserException(Exception):
+ """Custom exception raised when the user explicitly cancels the download process.
+ This is primarily for internal thread communication, not for the run() method's final return.
+ """
+
+ pass
+
+
+class DownloadPopupGfx(object):
+ def __init__(self, filename: str, dl_uri: str):
+ self.__logger = LoggingHandler(__name__).get_logger()
+
+ self.__filename = Path(save_cache_path("wemod_launcher")) / f"assets/{filename}"
+ self.__timestamp_file = self.__filename.with_suffix(".timestamp")
+ self.__dl_uri = dl_uri
+
+ if not self.__filename.parent.exists():
+ self.__filename.parent.mkdir(parents=True, exist_ok=True)
+
+ self.__logger.debug("DownloadPopupGfx initialized")
+
+ def __get_cached_timestamp(self) -> str | None:
+ """Reads the cached Last-Modified timestamp."""
+ if self.__timestamp_file.exists():
+ try:
+ with open(self.__timestamp_file, "r") as f:
+ data = json.load(f)
+ return data.get("last_modified")
+ except json.JSONDecodeError:
+ self.__logger.warning(
+ f"Could not decode JSON from {self.__timestamp_file}"
+ )
+ return None
+ return None
+
+ def __save_cached_timestamp(self, timestamp: str):
+ """Saves the Last-Modified timestamp to a cache file."""
+ with open(self.__timestamp_file, "w") as f:
+ json.dump({"last_modified": timestamp}, f)
+
+ def run(self) -> Path | str:
+ """Display the download dialog and handle the download process."""
+ try:
+ # Create QApplication if it doesn't exist
+ app = QApplication.instance()
+ if app is None:
+ self.__logger.debug("Creating new QApplication instance")
+ app = QApplication(sys.argv)
+
+ # Set application properties
+ app.setApplicationName("WeMod Launcher")
+ app.setApplicationVersion("1.503")
+
+ # Check if we have a valid display
+ if not app.primaryScreen():
+ self.__logger.error("No display/screen available for Qt application")
+ return "No display available - cannot show GUI"
+
+ # Show dialog
+ self.__logger.debug("Creating download dialog")
+ dialog = DownloadDialog(self.__filename.name)
+ dialog.start_download(self)
+
+ self.__logger.debug("Executing dialog")
+ result = dialog.exec()
+
+ if result == QDialog.DialogCode.Accepted:
+ self.__logger.info(
+ f"Download completed successfully: {self.__filename}"
+ )
+ return self.__filename
+ elif result == QDialog.DialogCode.Rejected:
+ self.__logger.info("Download was cancelled by user")
+ return "Download cancelled by user"
+ else:
+ self.__logger.warning("Download dialog closed with unknown result")
+ return "Download cancelled or failed"
+
+ except Exception as e:
+ self.__logger.error(f"Error in Qt GUI: {e}")
+ return f"GUI Error: {e}"
+
+ def __download_with_signals(self, worker):
+ """Download method that emits signals for Qt worker thread."""
+ try:
+ worker.status_updated.emit("Checking for cached file...")
+
+ # Check if file exists and get timestamp
+ local_timestamp = self.__get_cached_timestamp()
+ headers = {}
+ if local_timestamp:
+ headers["If-Modified-Since"] = local_timestamp
+
+ worker.status_updated.emit("Contacting server...")
+ response = requests.head(self.__dl_uri, headers=headers)
+
+ if response.status_code == 304:
+ # File hasn't changed
+ worker.status_updated.emit("File is up-to-date!")
+ worker.download_finished.emit(
+ True, f"File '{self.__filename.name}' is already up-to-date!"
+ )
+ return
+
+ # Download the file
+ worker.status_updated.emit("Starting download...")
+ response = requests.get(self.__dl_uri, stream=True, headers=headers)
+ response.raise_for_status()
+
+ total_size = int(response.headers.get("content-length", 0))
+ downloaded = 0
+
+ with open(self.__filename, "wb") as f:
+ for chunk in response.iter_content(chunk_size=8192):
+ if worker.cancelled:
+ worker.status_updated.emit("Download cancelled")
+ worker.download_finished.emit(
+ False, "Download cancelled by user"
+ )
+ return
+
+ if chunk:
+ f.write(chunk)
+ downloaded += len(chunk)
+
+ if total_size > 0:
+ progress = int((downloaded / total_size) * 100)
+ worker.progress_updated.emit(progress)
+ worker.status_updated.emit(
+ f"Downloaded {downloaded:,} / {total_size:,} bytes"
+ )
+
+ # Save timestamp
+ last_modified = response.headers.get("Last-Modified")
+ if last_modified:
+ self.__save_cached_timestamp(last_modified)
+
+ worker.status_updated.emit("Download completed!")
+ worker.download_finished.emit(True, "Download completed successfully!")
+
+ except requests.exceptions.RequestException as e:
+ worker.status_updated.emit(f"Network error: {e}")
+ worker.download_finished.emit(False, f"Network error: {e}")
+ except Exception as e:
+ worker.status_updated.emit(f"Error: {e}")
+ worker.download_finished.emit(False, f"Error: {e}")
+
+
+class DownloadWorker(QThread):
+ """Worker thread for downloading files with progress updates."""
+
+ progress_updated = pyqtSignal(int) # Progress percentage
+ status_updated = pyqtSignal(str) # Status message
+ download_finished = pyqtSignal(bool, str) # Success, message
+
+ def __init__(self, download_popup, parent=None):
+ super().__init__(parent)
+ self.download_popup = download_popup
+ self.cancelled = False
+
+ def cancel(self):
+ """Cancel the download."""
+ self.cancelled = True
+
+ def run(self):
+ """Run the download in the background thread."""
+ try:
+ self.download_popup._DownloadPopupGfx__download_with_signals(self)
+ except Exception as e:
+ self.download_finished.emit(False, str(e))
+
+
+class DownloadDialog(QDialog):
+ """Qt-based download progress dialog."""
+
+ def __init__(self, filename: str, parent=None):
+ super().__init__(parent)
+ self.setWindowTitle("Asset Downloader")
+ self.setModal(True)
+ self.setFixedSize(500, 200)
+
+ # Center the dialog on screen
+ self.move(
+ QApplication.primaryScreen().geometry().center() - self.rect().center()
+ )
+
+ self.filename = filename
+ self.download_worker = None
+ self.setup_ui()
+
+ def setup_ui(self):
+ """Set up the UI elements."""
+ layout = QVBoxLayout()
+ layout.setSpacing(15)
+ layout.setContentsMargins(20, 20, 20, 20)
+
+ # Filename label
+ filename_layout = QHBoxLayout()
+ filename_layout.addWidget(QLabel("Downloading asset:"))
+ self.filename_label = QLabel(self.filename)
+ self.filename_label.setStyleSheet("font-weight: bold;")
+ filename_layout.addWidget(self.filename_label)
+ filename_layout.addStretch()
+ layout.addLayout(filename_layout)
+
+ # Progress bar
+ self.progress_bar = QProgressBar()
+ self.progress_bar.setRange(0, 100)
+ self.progress_bar.setValue(0)
+ layout.addWidget(self.progress_bar)
+
+ # Status label
+ status_layout = QHBoxLayout()
+ status_layout.addWidget(QLabel("Status:"))
+ self.status_label = QLabel("")
+ status_layout.addWidget(self.status_label)
+ status_layout.addStretch()
+ layout.addLayout(status_layout)
+
+ # Buttons
+ button_layout = QHBoxLayout()
+ button_layout.addStretch()
+
+ self.cancel_button = QPushButton("Cancel")
+ self.cancel_button.clicked.connect(self.cancel_download)
+
+ self.retry_button = QPushButton("Retry")
+ self.retry_button.clicked.connect(self.retry_download)
+ self.retry_button.setVisible(False)
+
+ button_layout.addWidget(self.cancel_button)
+ button_layout.addWidget(self.retry_button)
+
+ layout.addLayout(button_layout)
+ self.setLayout(layout)
+
+ def start_download(self, download_popup):
+ """Start the download process."""
+ self.download_worker = DownloadWorker(download_popup, self)
+ self.download_worker.progress_updated.connect(self.update_progress)
+ self.download_worker.status_updated.connect(self.update_status)
+ self.download_worker.download_finished.connect(self.download_completed)
+ self.download_worker.start()
+
+ def update_progress(self, percentage):
+ """Update the progress bar."""
+ self.progress_bar.setValue(percentage)
+
+ def update_status(self, message):
+ """Update the status label."""
+ self.status_label.setText(message)
+
+ def cancel_download(self):
+ """Cancel the current download."""
+ if self.cancel_button.text() == "Close":
+ # Dialog is showing error state, just close it
+ self.reject()
+ return
+
+ if self.download_worker and self.download_worker.isRunning():
+ self.download_worker.cancel()
+ self.update_status("Cancelling download...")
+ self.cancel_button.setEnabled(False)
+
+ # Wait briefly for the worker to cancel, then close dialog
+ QTimer.singleShot(500, self.reject) # Close dialog after 500ms
+ else:
+ # No download running, just close the dialog
+ self.reject()
+
+ def retry_download(self):
+ """Retry the download."""
+ self.retry_button.setVisible(False)
+ self.cancel_button.setEnabled(True)
+ self.progress_bar.setValue(0)
+ # The download_popup will restart the download
+
+ def download_completed(self, success, message):
+ """Handle download completion."""
+ if success:
+ self.update_status("Download completed successfully!")
+ QTimer.singleShot(1000, self.accept) # Close after 1 second
+ else:
+ # Check if this was a user cancellation
+ if "cancelled" in message.lower():
+ self.update_status("Download cancelled by user")
+ QTimer.singleShot(500, self.reject) # Close dialog with rejection
+ else:
+ # This was an error, show retry option
+ self.update_status(f"Download failed: {message}")
+ self.cancel_button.setText("Close")
+ self.cancel_button.setEnabled(True)
+ self.retry_button.setVisible(True)
+
+ def closeEvent(self, event: QCloseEvent):
+ """Handle window close event."""
+ if self.download_worker and self.download_worker.isRunning():
+ self.cancel_download()
+ self.download_worker.wait(3000) # Wait up to 3 seconds
+ event.accept()
diff --git a/src/wemod_launcher/gfx/welcome_screen.py b/src/wemod_launcher/gfx/welcome_screen.py
new file mode 100644
index 0000000..a8978f3
--- /dev/null
+++ b/src/wemod_launcher/gfx/welcome_screen.py
@@ -0,0 +1,215 @@
+from xdg.BaseDirectory import save_cache_path
+import requests
+from pathlib import Path
+import json
+import threading
+import signal
+import sys
+from PyQt6.QtWidgets import (
+ QApplication,
+ QDialog,
+ QVBoxLayout,
+ QHBoxLayout,
+ QLabel,
+ QPushButton,
+ QMessageBox,
+)
+from PyQt6.QtCore import Qt, QThread, pyqtSignal
+from PyQt6.QtGui import QPixmap, QIcon
+
+# Assuming utils.logger is available and correctly configured
+from ..utils.logger import LoggingHandler
+
+
+class WelcomeScreenDialog(QDialog):
+ """Qt-based welcome screen dialog."""
+
+ WINDOW_TITLE = "WeMod Launcher Setup"
+ WELCOME_MESSAGE = "Welcome to the WeMod Installer!\nPress OK to start the setup."
+
+ def __init__(self, logo_pixmap=None, parent=None):
+ super().__init__(parent)
+ self.setWindowTitle(self.WINDOW_TITLE)
+ self.setModal(True)
+ self.setFixedSize(400, 300)
+
+ # Center the dialog on screen
+ self.move(
+ QApplication.primaryScreen().geometry().center() - self.rect().center()
+ )
+
+ self.setup_ui(logo_pixmap)
+
+ def setup_ui(self, logo_pixmap):
+ """Set up the UI elements."""
+ layout = QVBoxLayout()
+ layout.setSpacing(20)
+ layout.setContentsMargins(30, 30, 30, 30)
+
+ # Logo
+ if logo_pixmap and not logo_pixmap.isNull():
+ logo_label = QLabel()
+ # Scale logo to reasonable size while maintaining aspect ratio
+ scaled_pixmap = logo_pixmap.scaled(
+ 64,
+ 64,
+ Qt.AspectRatioMode.KeepAspectRatio,
+ Qt.TransformationMode.SmoothTransformation,
+ )
+ logo_label.setPixmap(scaled_pixmap)
+ logo_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
+ layout.addWidget(logo_label)
+
+ # Welcome message
+ message_label = QLabel(self.WELCOME_MESSAGE)
+ message_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
+ message_label.setStyleSheet("font-size: 14px; margin: 20px 0px;")
+ layout.addWidget(message_label)
+
+ # Buttons
+ button_layout = QHBoxLayout()
+
+ self.ok_button = QPushButton("OK")
+ self.ok_button.setDefault(True)
+ self.ok_button.clicked.connect(self.accept)
+
+ self.cancel_button = QPushButton("Cancel")
+ self.cancel_button.clicked.connect(self.reject)
+
+ button_layout.addWidget(self.cancel_button)
+ button_layout.addWidget(self.ok_button)
+
+ layout.addLayout(button_layout)
+ self.setLayout(layout)
+
+
+class WelcomeScreenGfx(object):
+ WEMOD_LOGO_URL: str = (
+ "https://www.wemod.com/static/images/device-icons/favicon-192-ce0bc030f3.png"
+ )
+
+ def __init__(self):
+ self.__logger = LoggingHandler(__name__).get_logger()
+
+ self.__cache_path = (
+ Path(save_cache_path("wemod_launcher")) / "assets/wemod_logo.png"
+ )
+ self.__timestamp_file = self.__cache_path.with_suffix(".timestamp")
+
+ self.__logo_raw = None
+ # Don't prepare logo until run() is called to avoid Qt issues
+
+ self.__logger.debug("WelcomeScreenGfx initialized")
+
+ def __get_cached_timestamp(self) -> str | None:
+ """Reads the cached Last-Modified timestamp."""
+ if self.__timestamp_file.exists():
+ try:
+ with open(self.__timestamp_file, "r") as f:
+ data = json.load(f)
+ return data.get("last_modified")
+ except json.JSONDecodeError:
+ self.__logger.warning("Invalid timestamp file, will re-download")
+ return None
+
+ def __save_cached_timestamp(self, timestamp: str):
+ """Saves the Last-Modified timestamp to a cache file."""
+ with open(self.__timestamp_file, "w") as f:
+ json.dump({"last_modified": timestamp}, f)
+
+ def __prepare_logo(self):
+ """Prepare the WeMod logo for display."""
+ if self.__cache_path.exists():
+ self.__logger.debug("Loading logo from cache")
+ with open(str(self.__cache_path), "rb") as f:
+ self.__logo_raw = f.read()
+
+ # Check if we need to update the cached logo
+ local_timestamp = self.__get_cached_timestamp()
+ if local_timestamp:
+ try:
+ headers = {"If-Modified-Since": local_timestamp}
+ response = requests.head(self.WEMOD_LOGO_URL, headers=headers)
+ if response.status_code == 304:
+ self.__logger.debug("Logo is up to date")
+ return
+ elif response.status_code == 200:
+ self.__logger.debug(
+ "Logo has been updated, downloading new version"
+ )
+ self.__download_and_cache_logo()
+ except requests.exceptions.RequestException as e:
+ self.__logger.warning(f"Failed to check logo freshness: {e}")
+ # Continue with cached version
+ else:
+ self.__download_and_cache_logo()
+
+ def __download_and_cache_logo(self):
+ """Download and cache the logo with timestamp tracking."""
+ self.__cache_path.parent.mkdir(parents=True, exist_ok=True)
+ self.__logger.debug("Downloading logo to cache, and memory.")
+ try:
+ response = requests.get(self.WEMOD_LOGO_URL, stream=False)
+ response.raise_for_status()
+
+ logo_raw = response.content
+ with open(str(self.__cache_path), "wb") as f:
+ f.write(logo_raw)
+ with open(str(self.__cache_path), "rb") as f:
+ self.__logo_raw = f.read()
+
+ # Save timestamp if available
+ last_modified = response.headers.get("Last-Modified")
+ if last_modified:
+ self.__save_cached_timestamp(last_modified)
+
+ except requests.exceptions.RequestException as e:
+ self.__logger.error(f"Failed to download logo: {e}")
+ self.__logo_raw = None
+
+ def run(self) -> bool:
+ """Display the welcome screen and return user's choice."""
+ # Prepare logo now that Qt is about to be initialized
+ self.__prepare_logo()
+
+ # Ensure proper Qt initialization
+ import os
+
+ # Set Qt platform plugin path if not set
+ if "QT_QPA_PLATFORM_PLUGIN_PATH" not in os.environ:
+ # Try to find Qt plugins in common locations
+ possible_paths = [
+ "/usr/lib/qt6/plugins/platforms",
+ "/usr/lib/x86_64-linux-gnu/qt6/plugins/platforms",
+ "/usr/lib64/qt6/plugins/platforms",
+ ]
+ for path in possible_paths:
+ if os.path.exists(path):
+ os.environ["QT_QPA_PLATFORM_PLUGIN_PATH"] = os.path.dirname(path)
+ break
+
+ # Set a fallback platform if no display is available
+ if "DISPLAY" not in os.environ and "WAYLAND_DISPLAY" not in os.environ:
+ os.environ["QT_QPA_PLATFORM"] = "offscreen"
+
+ # Create QApplication if it doesn't exist
+ app = QApplication.instance()
+ if app is None:
+ # Use sys.argv for proper argument handling
+ app = QApplication(sys.argv)
+ # Set application properties
+ app.setApplicationName("WeMod Launcher")
+ app.setApplicationVersion("1.503")
+ app.setOrganizationName("DeckCheatz")
+
+ # Create logo pixmap if available
+ logo_pixmap = None
+ if self.__logo_raw:
+ logo_pixmap = QPixmap()
+ logo_pixmap.loadFromData(self.__logo_raw)
+
+ # Show dialog and get result
+ dialog = WelcomeScreenDialog(logo_pixmap)
+ result = dialog.exec()
+
+ return result == QDialog.DialogCode.Accepted
diff --git a/mainutils.py b/src/wemod_launcher/main_utils.py
similarity index 95%
rename from mainutils.py
rename to src/wemod_launcher/main_utils.py
index de1e8e7..0fdcfba 100755
--- a/mainutils.py
+++ b/src/wemod_launcher/main_utils.py
@@ -15,11 +15,11 @@
)
-from corenodep import (
+from wemod_launcher.core_nodeps import (
parse_version,
)
-from coreutils import (
+from wemod_launcher.core_utils import (
exit_with_message,
save_conf_setting,
load_conf_setting,
@@ -75,9 +75,7 @@ def find_closest_compatible_release(
# Exact match
closest_release = release
closest_version = release_version_parts
- closest_release_url = release["assets"][0][
- "browser_download_url"
- ]
+ closest_release_url = release["assets"][0]["browser_download_url"]
break
elif (
release_version_parts[0] == current_version_parts[0]
@@ -124,8 +122,7 @@ def find_closest_compatible_release(
or release_version_parts[0] > closest_version[0]
or (
release_version_parts[0] == closest_version[0]
- and release_version_parts[1]
- > closest_version[1]
+ and release_version_parts[1] > closest_version[1]
)
)
):
@@ -144,8 +141,7 @@ def find_closest_compatible_release(
or release_version_parts[0] < closest_version[0]
or (
release_version_parts[0] == closest_version[0]
- and release_version_parts[1]
- < closest_version[1]
+ and release_version_parts[1] < closest_version[1]
)
)
):
@@ -237,7 +233,7 @@ def popup_download(title: str, link: str, file_name: str) -> str:
status = [0, 0]
- cache = os.path.join(SCRIPT_PATH, ".cache")
+ cache = "/tmp/wemod-launcher/.cache"
if not os.path.isdir(cache):
os.makedirs(cache)
@@ -289,9 +285,7 @@ def update_log(status: list[int], dl: int, total: int) -> None:
def get_dotnet48() -> str:
# Newer if you like to test: "https://download.visualstudio.microsoft.com/download/pr/2d6bb6b2-226a-4baa-bdec-798822606ff1/8494001c276a4b96804cde7829c04d7f/ndp48-x86-x64-allos-enu.exe"
LINK = "https://download.visualstudio.microsoft.com/download/pr/7afca223-55d2-470a-8edc-6a1739ae3252/abd170b4b0ec15ad0222a809b761a036/ndp48-x86-x64-allos-enu."
- cache_func = lambda FILE: popup_download(
- "Downloading dotnet48", LINK, FILE
- )
+ cache_func = lambda FILE: popup_download("Downloading dotnet48", LINK, FILE)
dotnet48 = cache("ndp48-x86-x64-allos-enu.exe", cache_func)
return dotnet48
@@ -428,9 +422,7 @@ def copy_files() -> None:
files = traverse_folders(source)
copy = []
for f in files:
- rfile = os.path.relpath(
- f, source
- ) # get file path relative to source
+ rfile = os.path.relpath(f, source) # get file path relative to source
use = True # by default, use the file
# Check if the file is in one of the dirs to ignore
@@ -524,9 +516,9 @@ def unpack_files() -> None:
for i, file in enumerate(files):
full_file = os.path.join(dest_path, file)
try: # try to create folder if missing
- if len(
+ if len(os.path.dirname(full_file)) > 0 and not os.path.isdir(
os.path.dirname(full_file)
- ) > 0 and not os.path.isdir(os.path.dirname(full_file)):
+ ):
os.makedirs(os.path.dirname(full_file), exist_ok=True)
except Exception as e:
log(
@@ -543,9 +535,7 @@ def unpack_files() -> None:
try:
zip_ref.extract(file, dest_path)
except Exception as e:
- log(
- f"Failed to extract '{file}' to '{dest_path}':\n\t{e}"
- )
+ log(f"Failed to extract '{file}' to '{dest_path}':\n\t{e}")
update_progress(i + 1, total_files)
@@ -620,7 +610,7 @@ def flatpakrunner():
import subprocess
import time
- cachedir = os.path.join(SCRIPT_PATH, ".cache")
+ cachedir = "/tmp/wemod-launcher/.cache"
os.makedirs(cachedir, exist_ok=True)
flatpakrunfile = os.path.join(cachedir, "insideflatpak.tmp")
@@ -632,9 +622,7 @@ def flatpakrunner():
save_conf_setting("FlatpakRunning", "new")
time.sleep(2)
- if load_conf_setting("FlatpakRunning") != "true" and os.path.isfile(
- flatpakrunfile
- ):
+ if load_conf_setting("FlatpakRunning") != "true" and os.path.isfile(flatpakrunfile):
os.remove(flatpakrunfile)
while not os.path.isfile(flatpakrunfile):
diff --git a/src/wemod_launcher/pfx/__init__.py b/src/wemod_launcher/pfx/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/wemod_launcher/pfx/wine_utils.py b/src/wemod_launcher/pfx/wine_utils.py
new file mode 100644
index 0000000..d87ac7a
--- /dev/null
+++ b/src/wemod_launcher/pfx/wine_utils.py
@@ -0,0 +1,20 @@
+import re
+from pathlib import PureWindowsPath, PurePosixPath, PurePath
+
+
+class WineUtils:
+ @staticmethod
+ def native_path(path: str) -> PureWindowsPath | PurePosixPath:
+ # Match any Windows drive letter pattern like C:\ or D:/ etc.
+ if re.match(r"^[a-zA-Z]:[\\/]", path):
+ return PureWindowsPath(path)
+
+ return PurePosixPath(path)
+
+ @staticmethod
+ def is_windows_path(path: PurePath) -> bool:
+ return isinstance(path, PureWindowsPath)
+
+ @staticmethod
+ def is_posix_path(path: PurePath) -> bool:
+ return isinstance(path, PurePosixPath)
diff --git a/setup.py b/src/wemod_launcher/prepare.py
similarity index 85%
rename from setup.py
rename to src/wemod_launcher/prepare.py
index d58fefb..242544e 100755
--- a/setup.py
+++ b/src/wemod_launcher/prepare.py
@@ -7,23 +7,23 @@
import shutil
import subprocess
-from coreutils import (
+from wemod_launcher.core_utils import (
log,
pip,
exit_with_message,
)
-from corenodep import (
+from wemod_launcher.core_nodeps import (
load_conf_setting,
save_conf_setting,
check_dependencies,
)
-from coreutils import (
+from wemod_launcher.core_utils import (
show_message,
)
-from mainutils import (
+from wemod_launcher.main_utils import (
download_progress,
is_flatpak,
)
@@ -45,23 +45,23 @@ def welcome() -> bool:
import FreeSimpleGUI as sg
import requests
- wemod_logo = requests.get(
- "https://www.wemod.com/static/images/device-icons/favicon-192-ce0bc030f3.png",
+ wand_logo = requests.get(
+ "https://www.wand.com/static/images/device-icons/favicon-192-ce0bc030f3.png",
stream=False,
)
sg.theme("systemdefault")
ret = sg.popup_ok_cancel(
- "Welcome to WeMod Installer!\nPress OK to start the setup.",
- title="WeMod Launcher Setup",
- image=wemod_logo.content,
- icon=wemod_logo.content,
+ "Welcome to Wand Installer!\nPress OK to start the setup.",
+ title="Wand Launcher Setup",
+ image=wand_logo.content,
+ icon=wand_logo.content,
)
return ret == "OK"
-def download_wemod(temp_dir: str) -> str:
+def download_wand(temp_dir: str) -> str:
import FreeSimpleGUI as sg
sg.theme("systemdefault")
@@ -72,24 +72,22 @@ def download_wemod(temp_dir: str) -> str:
text = sg.Text("0%")
# text = sg.Multiline(str.join("", log), key="-LOG-", autoscroll=True, size=(50,50), disabled=True)
layout = [[progress], [text]]
- window = sg.Window("Downloading WeMod", layout, finalize=True)
+ window = sg.Window("Downloading Wand", layout, finalize=True)
def update_log(status: list[int], dl: int, total: int) -> None:
status.clear()
status.append(dl)
status.append(total)
- setup_file = os.path.join(temp_dir, "wemod_setup.exe")
+ setup_file = os.path.join(temp_dir, "wand_setup.exe")
def download_func():
return download_progress(
- get_wemod_exe_url(),
+ "https://storage-cdn.wemod.com/app/releases/stable/Wand-12.6.0-full.nupkg",
setup_file,
lambda dl, total: update_log(status, dl, total),
)
- # download_func = lambda: download_progress("http://localhost:8000/WeMod-8.3.15.exe", setup_file, lambda dl,total: update_log(status, dl, total))
-
window.perform_long_operation(download_func, "-DL COMPLETE-")
while True: # Event Loop
@@ -112,19 +110,19 @@ def download_func():
return setup_file
-def get_wemod_exe_url():
+def get_wand_exe_url():
import requests
SCOOP_METADATA_URL = (
"https://raw.githubusercontent.com/"
"Calinou/scoop-games/refs/heads/master/bucket/"
- "wemod.json"
+ "wand.json"
)
raw = requests.get(SCOOP_METADATA_URL).json()
if not raw["architecture"]["64bit"]["url"]:
exit_with_message(
- "Unable to find WeMod EXE URL from Scoop",
+ "Unable to find Wand EXE URL from Scoop",
"Please raise on GitHub!",
timeout=120,
)
@@ -132,7 +130,7 @@ def get_wemod_exe_url():
return raw["architecture"]["64bit"]["url"]
-def unpack_wemod(
+def unpack_wand(
setup_file: str, temp_dir: str, install_location: str
) -> bool:
try:
@@ -148,7 +146,7 @@ def unpack_wemod(
)
)
- tmp_net = tempfile.mkdtemp(prefix="wemod-net")
+ tmp_net = tempfile.mkdtemp(prefix="wand-net")
archive.extractall(tmp_net, net)
shutil.move(os.path.join(tmp_net, net[0].filename), install_location)
@@ -157,12 +155,12 @@ def unpack_wemod(
return True
except Exception as e:
- log(f"Failed to unpack WeMod: {e}")
+ log(f"Failed to unpack Wand: {e}")
return False
def mk_venv() -> Optional[str]:
- venv_path = load_conf_setting("VirtualEnvironment") or "wemod_venv"
+ venv_path = load_conf_setting("VirtualEnvironment") or "wand_venv"
try:
if os.path.isabs(venv_path):
subprocess.run(
@@ -200,7 +198,7 @@ def tk_check() -> None:
def venv_manager() -> List[Optional[str]]:
requirements_txt = os.path.join(SCRIPT_PATH, "requirements.txt")
- tk_check()
+ # tk_check()
if not check_dependencies(requirements_txt):
pip_install = f"install -r '{requirements_txt}'"
return_code = pip(pip_install)
@@ -259,7 +257,7 @@ def self_update(path: List[Optional[str]]) -> List[Optional[str]]:
if not upd:
upd = load_conf_setting("SelfUpdate")
- infinite = os.getenv("WeModInfProtect", "1")
+ infinite = os.getenv("WandInfProtect", "1")
if int(infinite) > 3:
return path
elif int(infinite) > 2:
@@ -326,7 +324,7 @@ def self_update(path: List[Optional[str]]) -> List[Optional[str]]:
# Set executable permissions (replace with specific file names if needed)
subprocess.run(
- flatpak_cmd + ["chmod", "-R", "ug+x", "*.py", "wemod{,.bat}"],
+ flatpak_cmd + ["chmod", "-R", "ug+x", "*.py", "wand{,.bat}"],
text=True,
)
@@ -342,9 +340,8 @@ def self_update(path: List[Optional[str]]) -> List[Optional[str]]:
return path
-def check_flatpak(flatpak_cmd: Optional[List[str]]) -> List[str]:
- flatpak_start = []
- if is_flatpak() and not os.getenv("FROM_FLATPAK"):
+def check_flatpak():
+ if "FLATPAK_ID" in os.environ or os.path.exists("/.flatpak-info"):
flatpak_start = [
"flatpak-spawn",
"--host",
@@ -358,10 +355,10 @@ def check_flatpak(flatpak_cmd: Optional[List[str]]) -> List[str]:
"WINE",
"SCANFOLDER",
"TROUBLESHOOT",
- "WEMOD_LOG",
+ "WAND_LOG",
"WAIT_ON_GAMECLOSE",
"SELF_UPDATE",
- "FORCE_UPDATE_WEMOD",
+ "FORCE_UPDATE_WAND",
"REPO_STRING",
"GAME_FRONT",
"NO_EXE",
@@ -369,11 +366,11 @@ def check_flatpak(flatpak_cmd: Optional[List[str]]) -> List[str]:
for env in envlist:
if env in os.environ:
flatpak_start.append(f"--env={env}={os.environ[env]}")
- infpr = os.getenv("WeModInfProtect", "1")
+ infpr = os.getenv("WandInfProtect", "1")
infpr = str(int(infpr) + 1)
flatpak_start.append("--env=FROM_FLATPAK=true")
- flatpak_start.append(f"--env=WeModInfProtect={infpr}")
+ flatpak_start.append(f"--env=WandInfProtect={infpr}")
flatpak_start.append("--") # Isolate command from command args
if flatpak_cmd: # if venv is set use it
@@ -392,10 +389,10 @@ def setup_main() -> None:
print("Installation cancelled by user")
return
- install_location = os.path.join(SCRIPT_PATH, "wemod_bin")
+ install_location = os.path.join(SCRIPT_PATH, "wand_bin")
winetricks = os.path.join(SCRIPT_PATH, "winetricks")
- if os.getenv("FORCE_UPDATE_WEMOD", "0") == "1" or not os.path.isfile(
+ if os.getenv("FORCE_UPDATE_WAND", "0") == "1" or not os.path.isfile(
winetricks
):
if os.path.isfile(winetricks):
@@ -420,42 +417,42 @@ def setup_main() -> None:
if (
not os.path.isdir(install_location)
- or not os.path.isfile(os.path.join(install_location, "WeMod.exe"))
- or os.getenv("FORCE_UPDATE_WEMOD", "0") == "1"
+ or not os.path.isfile(os.path.join(install_location, "Wand.exe"))
+ or os.getenv("FORCE_UPDATE_WAND", "0") == "1"
):
if os.path.isdir(install_location):
shutil.rmtree(install_location, ignore_errors=True)
- temp_dir = tempfile.mkdtemp(prefix="wemod-launcher-")
- setup_file = download_wemod(temp_dir)
- unpacked = unpack_wemod(setup_file, temp_dir, install_location)
+ temp_dir = tempfile.mkdtemp(prefix="wand-launcher-")
+ setup_file = download_wand(temp_dir)
+ unpacked = unpack_wand(setup_file, temp_dir, install_location)
show_message(
'Setup completed successfully.\nMake sure the "LAUNCH OPTIONS" of the game say \''
- + str(os.path.join(SCRIPT_PATH, "wemod"))
+ + str(os.path.join(SCRIPT_PATH, "wand"))
+ " %command%'",
- title="WeMod Downloaded",
+ title="Wand Downloaded",
timeout=5,
)
if not unpacked:
exit_with_message(
"Failed Unpack",
- "Failed to unpack WeMod, exiting",
+ "Failed to unpack Wand, exiting",
1,
timeout=10,
ask_for_log=True,
)
-def run_wemod() -> None:
+def run_wand() -> None:
if getattr(sys, "frozen", False):
exit_with_message(
"Invalid compile",
- "The script was compiled with 'setup.py' as the start file.\nThis is incorrect the start-file is 'wemod', please recompile",
+ "The script was compiled with 'setup.py' as the start file.\nThis is incorrect the start-file is 'wand', please recompile",
)
else:
- script_file = os.path.join(SCRIPT_PATH, "wemod")
+ script_file = os.path.join(SCRIPT_PATH, "wand")
command = [sys.executable, script_file] + sys.argv[1:]
# Execute the main script so the venv gets created
@@ -469,4 +466,4 @@ def run_wemod() -> None:
if __name__ == "__main__":
- run_wemod()
+ run_wand()
diff --git a/src/wemod_launcher/pyinstaller.py b/src/wemod_launcher/pyinstaller.py
new file mode 100644
index 0000000..725b1ed
--- /dev/null
+++ b/src/wemod_launcher/pyinstaller.py
@@ -0,0 +1,20 @@
+import subprocess
+from pathlib import Path
+
+HERE = Path(__file__).parent.absolute()
+PATH_TO_MAIN = str(HERE / "cli.py")
+
+
+def install():
+ arglist = [
+ "poetry",
+ "run",
+ "pyinstaller",
+ "--onefile",
+ "--windowed",
+ "--clean",
+ "--hidden-import tkinter",
+ "--collect-all tkinter",
+ PATH_TO_MAIN,
+ ]
+ subprocess.run(" ".join(arglist), stdout=subprocess.PIPE, shell=True)
diff --git a/src/wemod_launcher/utils/__init__.py b/src/wemod_launcher/utils/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/wemod_launcher/utils/configuration.py b/src/wemod_launcher/utils/configuration.py
new file mode 100644
index 0000000..15fedd6
--- /dev/null
+++ b/src/wemod_launcher/utils/configuration.py
@@ -0,0 +1,39 @@
+import logging
+from typing import Any, Optional
+from tomllib import load
+from pathlib import Path
+from xdg.BaseDirectory import save_config_path
+from wemod_launcher.utils.logger import LoggingHandler
+
+
+class Configuration(object):
+ def __init__(self, cfg_path: str = save_config_path("wemod-launcher")):
+ # Initialize logger for Config.
+ self.__log = LoggingHandler(module_name=__name__).get_logger()
+
+ path = Path(cfg_path)
+ if not path.exists():
+ logging.debug("Configuration path does not exist, creating.")
+ path.mkdir()
+
+ cfg_file = path / "config.toml"
+
+ if not cfg_file.exists():
+ # TODO: Add support to write dummy config.
+ cfg_file.write_text("### WIP ###")
+
+ self.__cfg_path = cfg_file
+
+ self.__config = load(open(str(cfg_file), "rb"))
+
+ def get_key(self, keys: list[str]) -> Optional[Any]:
+ try:
+ config = self.__config
+ for key in keys:
+ config = config[key]
+
+ return config
+ except KeyError:
+ self.__log.debug("Unable to get configuration entry, returning None")
+ self.__log.debug(f"Key: {keys}, config path: {self.__cfg_path}")
+ return None
diff --git a/src/wemod_launcher/utils/consts.py b/src/wemod_launcher/utils/consts.py
new file mode 100644
index 0000000..3a279bb
--- /dev/null
+++ b/src/wemod_launcher/utils/consts.py
@@ -0,0 +1,16 @@
+import os
+from wemod_launcher.utils.configuration import Configuration
+from pathlib import Path
+
+
+class Consts:
+ CFG = Configuration()
+ SCRIPT_RUNTIME_DIR: Path = Path(os.path.dirname(os.path.realpath(__file__)))
+ SCRIPT_PATH: Path = Path(os.path.realpath(__file__))
+ STEAM_COMPAT_DATA_DIR: Path = Path(
+ os.getenv(
+ "STEAM_COMPAT_DATA_PATH",
+ CFG.get_key(["steam", "compat_data_dir"]),
+ )
+ or Path.home() / ".steam/steam/steamapps/compatdata"
+ ).expanduser()
diff --git a/src/wemod_launcher/utils/logger.py b/src/wemod_launcher/utils/logger.py
new file mode 100644
index 0000000..7d0abdc
--- /dev/null
+++ b/src/wemod_launcher/utils/logger.py
@@ -0,0 +1,77 @@
+import os
+from xdg.BaseDirectory import save_data_path
+from pathlib import Path
+import logging
+from os import environ, getenv
+
+
+class LoggingHandler(object):
+ def __init__(
+ self,
+ module_name: str,
+ level: int = 0,
+ log_dir: str = save_data_path("wemod-launcher"),
+ ):
+ if not module_name or module_name.strip() == "":
+ print("Module name is required!")
+ print("This IS a bug, contact upstream devs.")
+ try:
+ if (
+ getenv("WEMOD_LAUNCHER_DEV_MODE", "false").lower() in ("true", "1", "t")
+ and level == 0
+ ):
+ level = logging.DEBUG
+ elif "WEMOD_LAUNCHER_LOG_LEVEL" in environ and level == 0:
+ env_log_level = environ["WEMOD_LAUNCHER_LOG_LEVEL"]
+ match env_log_level:
+ case "DEBUG":
+ level = logging.DEBUG
+ case "INFO":
+ level = logging.INFO
+ case "WARNING":
+ level = logging.WARNING
+ case "ERROR":
+ level = logging.ERROR
+ case "CRITICAL":
+ level = logging.CRITICAL
+ case _:
+ level = logging.INFO
+ else:
+ level = logging.INFO
+ except KeyError:
+ level = logging.INFO
+ pass
+
+ log_base = Path(log_dir)
+ if not log_base.exists():
+ log_base.mkdir()
+
+ logFormatter = logging.Formatter(
+ "%(asctime)s [%(threadName)-12.12s] [%(levelname)-5.5s] %(name)s: %(message)s"
+ )
+ root_logger = logging.getLogger(module_name)
+
+ file_handler = logging.FileHandler(
+ os.getenv(
+ "WEMOD_LAUNCHER_LOG_FILE",
+ str(log_base / "wemod-launcher.log"),
+ )
+ )
+ file_handler.setLevel(level)
+ file_handler.setFormatter(logFormatter)
+
+ console_handler = logging.StreamHandler()
+ console_handler.setLevel(level)
+ console_handler.setFormatter(logFormatter)
+
+ root_logger.setLevel(level)
+ root_logger.addHandler(file_handler)
+ root_logger.addHandler(console_handler)
+
+ self.__logger = root_logger
+ root_logger.debug(
+ f"Logger for module ({module_name}) initialized with log level ({level})"
+ )
+
+ def get_logger(self) -> logging.Logger:
+ return self.__logger
diff --git a/wemod b/src/wemod_launcher/wemod
similarity index 82%
rename from wemod
rename to src/wemod_launcher/wemod
index 20f0955..dce0dbb 100755
--- a/wemod
+++ b/src/wemod_launcher/wemod
@@ -11,7 +11,7 @@ import subprocess
from typing import Optional
# Import core utils without download dependencies
-from corenodep import (
+from core_nodeps import (
join_lists_with_delimiter,
split_list_by_delimiter,
load_conf_setting,
@@ -22,7 +22,7 @@ from corenodep import (
)
# Import core utils
-from coreutils import (
+from core_utils import (
exit_with_message,
script_manager,
popup_options,
@@ -33,7 +33,7 @@ from coreutils import (
)
# Import main utils
-from mainutils import (
+from main_utils import (
find_closest_compatible_release,
unpack_zip_with_progress,
get_github_releases,
@@ -44,8 +44,8 @@ from mainutils import (
deref,
)
-# Import from setup
-from setup import (
+# Import from prepare
+from prepare import (
check_flatpak,
venv_manager,
self_update,
@@ -74,13 +74,13 @@ if __name__ == "__main__":
# On venv path restart the script
if 0 < len(if_flatpak_list):
# Use environment variable to protect the script from re-running forever
- inf_protect = os.getenv("WeModInfProtect", "1")
+ inf_protect = os.getenv("WandInfProtect", "1")
if int(inf_protect) > 4:
exit_with_message(
"Infinite rerun",
- "Infinite script reruns were detected\nThe script was stopped\nCreate an issue on the wemod_laucher GitHub\nand attach this file",
+ "Infinite script reruns were detected\nThe script was stopped\nCreate an issue on the wand_laucher GitHub\nand attach this file",
)
- os.environ["WeModInfProtect"] = str(int(inf_protect) + 1)
+ os.environ["WandInfProtect"] = str(int(inf_protect) + 1)
# if we are in a flatpak we wait for a command to be passed back into the script in a thread
if len(if_flatpak_list) > 1:
@@ -112,8 +112,8 @@ if __name__ == "__main__":
# Import utils that need constants
-from constutils import (
- scanfolderforversions,
+from const_utils import (
+ scan_compat_for_versions,
troubleshooter,
ensure_wine,
winetricks,
@@ -131,8 +131,8 @@ from consts import (
)
-# Symlink WeMod data to make all WeMod prefixes use the same WeMod data
-def syncwemod(
+# Symlink Wand data to make all Wand prefixes use the same Wand data
+def syncwand(
folder: Optional[str] = None,
) -> None:
response = None
@@ -153,19 +153,17 @@ def syncwemod(
save_conf_setting("PackagePrefix", package_prefix)
package_prefix = "true"
if package_prefix and folder == None and package_prefix.lower() == "true":
- from mainutils import copy_folder_with_progress
+ from main_utils import copy_folder_with_progress
log(
"Prefix packaging was requested with PACKAGEPREFIX=true in front of the command"
)
- current_proton_version = read_file(
- os.path.join(BASE_STEAM_COMPAT, "version")
- )
+ current_proton_version = read_file(os.path.join(BASE_STEAM_COMPAT, "version"))
if not current_proton_version:
log(f"Version is not set for {BASE_STEAM_COMPAT}, Error")
exit_with_message(
"Prefix Version unknown",
- "The prefix version is unknown. Please make sure the prefix works with WeMod before trying to zip it",
+ "The prefix version is unknown. Please make sure the prefix works with Wand before trying to zip it",
timeout=20,
)
cut_proton_version = parse_version(current_proton_version)
@@ -180,10 +178,10 @@ def syncwemod(
)
if not os.path.isfile(INIT_FILE):
- log(f"WeMod is not installed in {BASE_STEAM_COMPAT}, error")
+ log(f"Wand is not installed in {BASE_STEAM_COMPAT}, error")
exit_with_message(
- "WeMod not installed",
- "WeMod is not installed in the active prefix, exiting",
+ "Wand not installed",
+ "Wand is not installed in the active prefix, exiting",
timeout=20,
)
@@ -203,9 +201,7 @@ def syncwemod(
os.remove(WINEPREFIX)
waslink = True
- copy_folder_with_progress(
- BASE_STEAM_COMPAT, destfile, True, [None], [None]
- )
+ copy_folder_with_progress(BASE_STEAM_COMPAT, destfile, True, [None], [None])
if waslink:
try:
@@ -216,9 +212,7 @@ def syncwemod(
with open(INIT_FILE, "w") as init:
init.write(initcont)
- os.system(
- "xdg-open '" + os.path.join(STEAM_COMPAT_FOLDER, "prefixes") + "'"
- )
+ os.system("xdg-open '" + os.path.join(STEAM_COMPAT_FOLDER, "prefixes") + "'")
log("Done creating Prefix zip")
exit_with_message(
"Prefix Packaged",
@@ -229,64 +223,64 @@ def syncwemod(
if folder == None:
folder = BASE_STEAM_COMPAT
- WeModData = os.path.join(SCRIPT_PATH, "wemod_data") # link source
- WeModExtenal = os.path.join(
- folder, "pfx/drive_c/users/steamuser/AppData/Roaming/WeMod"
+ WandData = os.path.join(SCRIPT_PATH, "wand_data") # link source
+ WandExtenal = os.path.join(
+ folder, "pfx/drive_c/users/steamuser/AppData/Roaming/Wand"
) # link dest
log(
- f"Syncing WeMod data from '{WeModExtenal}' to launcher dir '{WeModData}'"
+ f"Syncing Wand data from '{WandExtenal}' to launcher dir '{WandData}'"
)
# Ensure the launcher dir exists
- if not os.path.isdir(WeModData):
- os.makedirs(WeModData)
+ if not os.path.isdir(WandData):
+ os.makedirs(WandData)
- # If WeModExtenal exists but is a broken symlink, or any non-dir — remove it
- if os.path.lexists(WeModExtenal) and not os.path.isdir(WeModExtenal):
- log("Removing broken or invalid WeModExtenal path")
+ # If WandExtenal exists but is a broken symlink, or any non-dir — remove it
+ if os.path.lexists(WandExtenal) and not os.path.isdir(WandExtenal):
+ log("Removing broken or invalid WandExtenal path")
try:
- os.remove(WeModExtenal)
+ os.remove(WandExtenal)
except Exception as e:
log(f"Failed to remove existing path: {e}")
# Create the external folder if it's still missing
- if not os.path.exists(WeModExtenal):
- os.makedirs(WeModExtenal)
+ if not os.path.exists(WandExtenal):
+ os.makedirs(WandExtenal)
- # If WeModExtenal is a real directory (not a symlink)
- if os.path.isdir(WeModExtenal) and not os.path.islink(WeModExtenal):
- wemod_data_not_empty = len(os.listdir(WeModData)) > 0
- external_data_not_empty = len(os.listdir(WeModExtenal)) > 0
+ # If WandExtenal is a real directory (not a symlink)
+ if os.path.isdir(WandExtenal) and not os.path.islink(WandExtenal):
+ wand_data_not_empty = len(os.listdir(WandData)) > 0
+ external_data_not_empty = len(os.listdir(WandExtenal)) > 0
- if wemod_data_not_empty and external_data_not_empty:
+ if wand_data_not_empty and external_data_not_empty:
response = show_message(
- "Warning: WeMod might have been installed previously.\n"
- "Use WeMod Launcher dir account (Yes) or\n"
- "Use WeMod prefix/game dir account (No)",
+ "Warning: Wand might have been installed previously.\n"
+ "Use Wand Launcher dir account (Yes) or\n"
+ "Use Wand prefix/game dir account (No)",
title="Multiple accounts found",
yesno=True,
)
# Overwrite launcher dir if user said No, or if it's empty
if (
- not wemod_data_not_empty or response == "No"
+ not wand_data_not_empty or response == "No"
) and external_data_not_empty:
- log("The local WeMod data was requested to be overwritten")
- shutil.rmtree(WeModData)
- shutil.copytree(WeModExtenal, WeModData)
+ log("The local Wand data was requested to be overwritten")
+ shutil.rmtree(WandData)
+ shutil.copytree(WandExtenal, WandData)
# Now that we’ve synced, remove the external folder
- shutil.rmtree(WeModExtenal)
+ shutil.rmtree(WandExtenal)
# Now create the symlink if nothing is there
- if not os.path.exists(WeModExtenal):
- os.symlink(WeModData, WeModExtenal)
- log("Linked WeMod data to game prefix")
+ if not os.path.exists(WandExtenal):
+ os.symlink(WandData, WandExtenal)
+ log("Linked Wand data to game prefix")
# Ensure main setup is done
if not os.path.exists(
- os.path.join(SCRIPT_PATH, "wemod_bin", "WeMod.exe")
+ os.path.join(SCRIPT_PATH, "wand_bin", "Wand.exe")
):
setup_main()
@@ -309,9 +303,7 @@ def init(proton: str, iswine: bool = False) -> None:
text=True,
)
except Exception as e:
- log(
- f"Error grabbing the external wine version using file:\n\t{e}"
- )
+ log(f"Error grabbing the external wine version using file:\n\t{e}")
prefix_version_file = ensure_wine()
else:
prefix_version_file = ensure_wine(str(wver.stdout))
@@ -327,14 +319,14 @@ def init(proton: str, iswine: bool = False) -> None:
ask_for_log=True,
)
- # If WeMod is not installed try to copy a working prefix to the current one
+ # If Wand is not installed try to copy a working prefix to the current one
log(f"Looking for init file '{INIT_FILE}'")
if not os.path.exists(INIT_FILE):
log(
f"Looking for compatible wine prefixes in '{STEAM_COMPAT_FOLDER}' with Proton version '{current_version_parts[0]}.{current_version_parts[1]}'"
)
- # Get closest version that has WeMod installed
+ # Get closest version that has Wand installed
closest_version, closest_prefix_folder = scanfolderforversions(
current_version_parts
)
@@ -345,7 +337,7 @@ def init(proton: str, iswine: bool = False) -> None:
f"Found '{cut_version[0]}.{cut_version[1]}' on '{current_version_parts[0]}.{current_version_parts[1]}'"
)
- from mainutils import copy_folder_with_progress
+ from main_utils import copy_folder_with_progress
response = "No"
if (
@@ -354,8 +346,8 @@ def init(proton: str, iswine: bool = False) -> None:
and closest_version == current_version_parts
):
response = show_message(
- f"The Proton version {current_version_parts[0]}.{current_version_parts[1]} doesn't have WeMod installed. Would you like to use the perfectly matched Proton version {cut_version[0]}.{cut_version[1]} that has WeMod installed, which is very likely going to work?",
- title="Very likely compatible WeMod version detected",
+ f"The Proton version {current_version_parts[0]}.{current_version_parts[1]} doesn't have Wand installed. Would you like to use the perfectly matched Proton version {cut_version[0]}.{cut_version[1]} that has Wand installed, which is very likely going to work?",
+ title="Very likely compatible Wand version detected",
yesno=True,
)
if response == None:
@@ -365,10 +357,9 @@ def init(proton: str, iswine: bool = False) -> None:
and current_version_parts
and closest_version[0] == current_version_parts[0]
):
-
response = show_message(
- f"The Proton version {current_version_parts[0]}.{current_version_parts[1]} doesn't have WeMod installed. Would you like to use the closest Proton version {cut_version[0]}.{cut_version[1]} that has WeMod installed, which is likely going to work?",
- title="Likely compatible WeMod version detected",
+ f"The Proton version {current_version_parts[0]}.{current_version_parts[1]} doesn't have Wand installed. Would you like to use the closest Proton version {cut_version[0]}.{cut_version[1]} that has Wand installed, which is likely going to work?",
+ title="Likely compatible Wand version detected",
yesno=True,
)
if response == None:
@@ -379,20 +370,18 @@ def init(proton: str, iswine: bool = False) -> None:
and closest_version[0] != current_version_parts[0]
):
response = show_message(
- f"The Proton version {current_version_parts[0]}.{current_version_parts[1]} doesn't have WeMod installed. Would you like to attempt to use the closest Proton version {cut_version[0]}.{cut_version[1]} that has WeMod installed, which may result in some issues?",
- title="Maybe compatible WeMod version detected",
+ f"The Proton version {current_version_parts[0]}.{current_version_parts[1]} doesn't have Wand installed. Would you like to attempt to use the closest Proton version {cut_version[0]}.{cut_version[1]} that has Wand installed, which may result in some issues?",
+ title="Maybe compatible Wand version detected",
yesno=True,
)
else:
- log(
- "No compatible Proton version found in the compatibility folder."
- )
+ log("No compatible Proton version found in the compatibility folder.")
if response == "Yes":
# Copy the closest version's prefix to the game prefix
log(f"Copying {closest_prefix_folder} to {BASE_STEAM_COMPAT}")
- syncwemod(
+ syncwand(
closest_prefix_folder
- ) # Sync WeMod data in closest version
+ ) # Sync Wand data in closest version
copy_folder_with_progress(
closest_prefix_folder,
@@ -401,7 +390,7 @@ def init(proton: str, iswine: bool = False) -> None:
[None],
[None],
)
- syncwemod() # Sync WeMod data
+ syncwand() # Sync Wand data
log(
f"Copied Proton version {cut_version[0]}.{cut_version[1]} prefix to game prefix that was on version {current_version_parts[0]}.{current_version_parts[1]}"
)
@@ -415,7 +404,7 @@ def init(proton: str, iswine: bool = False) -> None:
# Check for the initialization file in the wine prefix
log(f"Looking once more for the init file")
if os.path.exists(INIT_FILE):
- syncwemod() # Sync WeMod data and prefix packaging
+ syncwand() # Sync Wand data and prefix packaging
log("Found init file. Continuing launch...")
return
@@ -438,7 +427,7 @@ def init(proton: str, iswine: bool = False) -> None:
build_prefix(proton_dir)
else:
download_prefix(proton_dir)
- syncwemod() # Sync WeMod data
+ syncwand() # Sync Wand data
# Function to download and unpack a pre-configured wine prefix
@@ -448,7 +437,7 @@ def download_prefix(proton_dir: str) -> None:
log(WINEPREFIX)
exit_with_message(
"First Launch",
- "First Launch Detected: Please run the game once without WeMod first. Error.",
+ "First Launch Detected: Please run the game once without Wand first. Error.",
ask_for_log=True,
)
@@ -464,7 +453,7 @@ def download_prefix(proton_dir: str) -> None:
log("RepoUser not set in config using: " + repo_user)
repo_name = load_conf_setting("RepoName")
- if repo_name and repo_name.lower() == "wemod-launcher".lower():
+ if repo_name and repo_name.lower() == "wand-launcher".lower():
repo_name = "BuiltPrefixes-dev"
save_conf_setting("RepoName", repo_name)
log("Updated RepoName in config to: " + repo_name)
@@ -484,9 +473,7 @@ def download_prefix(proton_dir: str) -> None:
repo_concat = repo_user + "/" + repo_name
- current_proton_version = read_file(
- os.path.join(BASE_STEAM_COMPAT, "version")
- )
+ current_proton_version = read_file(os.path.join(BASE_STEAM_COMPAT, "version"))
current_version_parts = parse_version(current_proton_version)
closest_version = None
@@ -496,7 +483,7 @@ def download_prefix(proton_dir: str) -> None:
releases, current_version_parts
)
file_name = (
- f"wemod_prefix{closest_version[0]}.{closest_version[1]}.zip"
+ f"wand_prefix{closest_version[0]}.{closest_version[1]}.zip"
)
if (
@@ -527,7 +514,7 @@ def download_prefix(proton_dir: str) -> None:
log(f"No version to download found on repo '{repo_concat}'")
exit_with_message(
"No downloads available",
- f"Error: Nothing to download on repo '{repo_concat}',\nTo fix this, you can try to delete wemod.conf",
+ f"Error: Nothing to download on repo '{repo_concat}',\nTo fix this, you can try to delete wand.conf",
ask_for_log=True,
)
if response == "No":
@@ -556,9 +543,9 @@ def download_prefix(proton_dir: str) -> None:
if os.path.isfile(prefix_path):
os.remove(prefix_path)
- syncwemod()
+ syncwand()
if not os.path.isfile(
- os.path.join(SCRIPT_PATH, "wemod_bin", "WeMod.exe")
+ os.path.join(SCRIPT_PATH, "wand_bin", "Wand.exe")
):
setup_main()
@@ -569,13 +556,7 @@ def build_prefix(proton_dir: str) -> None:
import FreeSimpleGUI as sg
# Set environment path
- path = (
- os.path.join(SCRIPT_PATH, "bin")
- + ":"
- + proton_dir
- + ":"
- + os.getenv("PATH")
- )
+ path = os.path.join(SCRIPT_PATH, "bin") + ":" + proton_dir + ":" + os.getenv("PATH")
# deref
winfolder = os.path.join(WINEPREFIX, "drive_c", "windows")
@@ -587,8 +568,8 @@ def build_prefix(proton_dir: str) -> None:
# Choose method to install dotnet48
dotnet48_method = popup_options(
"dotnet48",
- "Would you like to install dotnet48 with winetricks (default for GE-Proton8 or above)\nor with wemod-launcher (ONLY USE FOR GE-Proton7)\nWARNING: The WeMod Launcher option isn't working well, you can try using it anyway (ONLY ON GE-Proton7)",
- [["winetricks", "wemod-launcher"]],
+ "Would you like to install dotnet48 with winetricks (default for GE-Proton8 or above)\nor with wand-launcher (ONLY USE FOR GE-Proton7)\nWARNING: The Wand Launcher option isn't working well, you can try using it anyway (ONLY ON GE-Proton7)",
+ [["winetricks", "wand-launcher"]],
)
# Add dependencies to the list
@@ -608,23 +589,17 @@ def build_prefix(proton_dir: str) -> None:
dep_i = dep_i + 1
response = winetricks(deps[dep_i], path)
- # Install dotnet48 using wemod-launcher if selected
- if dotnet48_method and dotnet48_method == "wemod-launcher":
+ # Install dotnet48 using wand-launcher if selected
+ if dotnet48_method and dotnet48_method == "wand-launcher":
log("Installing dotnet48...")
dotnet48 = get_dotnet48()
wine("winecfg -v win7", path)
dotnet48_result = wine(dotnet48, path)
- if (
- dotnet48_result != 0
- and dotnet48_result != 194
- and dotnet48_result != -15
- ):
+ if dotnet48_result != 0 and dotnet48_result != 194 and dotnet48_result != -15:
exit_with_message(
"dotnet48 install error",
- "dotnet48 installation exited with code '{}'".format(
- dotnet48_result
- ),
+ "dotnet48 installation exited with code '{}'".format(dotnet48_result),
ask_for_log=True,
)
@@ -735,7 +710,7 @@ def run(skip_init: bool = False) -> str:
log(f"Error, the game executable '{GAME_EXE}' is missing")
exit_with_message(
"Game Missing",
- f"The game executable '{GAME_EXE}' is missing.\nMake sure the game runs without WeMod.\nIf not, use 'Verify Files' in Steam or ensure the game is installed correctly.",
+ f"The game executable '{GAME_EXE}' is missing.\nMake sure the game runs without Wand.\nIf not, use 'Verify Files' in Steam or ensure the game is installed correctly.",
ask_for_log=True,
)
@@ -848,9 +823,7 @@ def run(skip_init: bool = False) -> str:
warn = fwf.read()
os.remove(warnfile)
if warn:
- log(
- f"Error while loading the wine server waiter:\n\t{warn}"
- )
+ log(f"Error while loading the wine server waiter:\n\t{warn}")
log("Finished flatpak mode code")
else:
@@ -879,9 +852,7 @@ def run(skip_init: bool = False) -> str:
try:
if os.path.isdir(os.path.join(WINEPREFIX, "drive_c")):
os.environ["WINEPREFIX"] = WINEPREFIX
- elif os.path.isdir(
- os.path.join(BASE_STEAM_COMPAT, "drive_c")
- ):
+ elif os.path.isdir(os.path.join(BASE_STEAM_COMPAT, "drive_c")):
os.environ["WINEPREFIX"] = BASE_STEAM_COMPAT
else:
os.environ["WINEPREFIX"] = WINEPREFIX
@@ -923,9 +894,7 @@ if __name__ == "__main__":
RESPONCE = run()
except Exception as e:
RESPONCE = "ERR:\n" + str(e)
- logy = show_message(
- "Error occurred. Open the log?", "Error occurred", 30, True
- )
+ logy = show_message("Error occurred. Open the log?", "Error occurred", 30, True)
# Log final response or error
log(str(RESPONCE))
diff --git a/src/wemod_launcher/wemod.py b/src/wemod_launcher/wemod.py
new file mode 120000
index 0000000..cc5af10
--- /dev/null
+++ b/src/wemod_launcher/wemod.py
@@ -0,0 +1 @@
+wemod
\ No newline at end of file
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/wemod-launcher.spec b/wemod-launcher.spec
new file mode 100644
index 0000000..09b121b
--- /dev/null
+++ b/wemod-launcher.spec
@@ -0,0 +1,88 @@
+# -*- mode: python ; coding: utf-8 -*-
+
+import sys
+from pathlib import Path
+
+# Add source directory to path
+src_path = str(Path(__file__).parent / "src")
+sys.path.insert(0, src_path)
+
+block_cipher = None
+
+a = Analysis(
+ ['src/wemod_launcher/wemod.py'],
+ pathex=[src_path],
+ binaries=[],
+ datas=[],
+ hiddenimports=[
+ # Qt6 modules - required for PyQt6
+ 'PyQt6.QtCore',
+ 'PyQt6.QtGui',
+ 'PyQt6.QtWidgets',
+ 'PyQt6.sip',
+ # CLI framework
+ 'click',
+ 'click.core',
+ 'click.decorators',
+ # Application modules
+ 'wemod_launcher.cli',
+ 'wemod_launcher.gfx.welcome_screen',
+ 'wemod_launcher.gfx.download_popup',
+ 'wemod_launcher.utils.logger',
+ 'wemod_launcher.utils.configuration',
+ 'wemod_launcher.utils.consts',
+ 'wemod_launcher.pfx.wine_utils',
+ # Standard library modules that might not be auto-detected
+ 'json',
+ 'threading',
+ 'pathlib',
+ 'signal',
+ 'sys',
+ ],
+ hookspath=[],
+ hooksconfig={},
+ runtime_hooks=[],
+ excludes=[
+ 'FreeSimpleGUI', # Exclude old GUI library
+ 'tkinter', # Often included by default but not needed
+ 'matplotlib', # Heavy dependency, exclude if not needed
+ 'numpy', # Heavy dependency, exclude if not needed
+ ],
+ win_no_prefer_redirects=False,
+ win_private_assemblies=False,
+ cipher=block_cipher,
+ noarchive=False,
+)
+
+pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
+
+exe = EXE(
+ pyz,
+ a.scripts,
+ a.binaries,
+ a.zipfiles,
+ a.datas,
+ [],
+ name='wemod-launcher',
+ debug=False,
+ bootloader_ignore_signals=False,
+ strip=False,
+ upx=True,
+ upx_exclude=[],
+ runtime_tmpdir=None,
+ console=False, # Set to False for GUI app on Windows/macOS
+ disable_windowed_traceback=False,
+ argv_emulation=False,
+ target_arch=None,
+ codesign_identity=None,
+ entitlements_file=None,
+ icon=None, # Add icon path if you have one
+)
+
+# For Linux AppImage support
+app = BUNDLE(
+ exe,
+ name='wemod-launcher.app',
+ icon=None,
+ bundle_identifier='org.uk.shymega.deckCheatz.wemod-launcher',
+)
diff --git a/wemod.bat b/wemod.bat
index 9f01824..dd601b1 100644
--- a/wemod.bat
+++ b/wemod.bat
@@ -1,16 +1,16 @@
@echo off
-@title Wemod Launcher
+@title Wand Launcher
SET mypath=%~dp0
-SET wemodname=WeMod.exe
-SET wemodpath=%mypath:~0,-1%\wemod_bin\%wemodname%
+SET wandname=Wand.exe
+SET wandpath=%mypath:~0,-1%\wand_bin\%wandname%
SET temptime=%mypath:~0,-1%\.cache\early.tmp
SET returnfile=%mypath:~0,-1%\.cache\return.tmp
-echo Hello from the WeMod Launcher, the WeMod bat was started successfully.
+echo Hello from the Wand Launcher, the Wand bat was started successfully.
echo.
-echo WEMOD EXE:
-echo "%wemodpath%"
+echo WAND EXE:
+echo "%wandpath%"
echo.
echo PWD:
echo "%cd%"
@@ -21,56 +21,56 @@ echo %*
echo.
echo.
-REM Start WeMod.exe and get its PID
-echo Starting WeMod by using %wemodname%.
-start "" %wemodpath%
+REM Start Wand.exe and get its PID
+echo Starting Wand by using %wandname%.
+start "" %wandpath%
-echo Checking for running WeMod pid
+echo Checking for running Wand pid
set retry_count=0
:retry_pid
-set wemodPID=
+set wandPID=
set /a retry_count+=1
-REM Get the wemod pid over Proton
-if not defined wemodPID (
- for /F "TOKENS=1,2,*" %%a in ('C:/windows/system32/tasklist /FI "IMAGENAME eq %wemodname%"') do (
+REM Get the wand pid over Proton
+if not defined wandPID (
+ for /F "TOKENS=1,2,*" %%a in ('C:/windows/system32/tasklist /FI "IMAGENAME eq %wandname%"') do (
set void=%%a
- set wemodPID=%%b
+ set wandPID=%%b
)
)
-REM Retry to get the wemod pid over Proton
-if not defined wemodPID (
- for /F "TOKENS=1,2,*" %%a in ('C:/windows/system32/tasklist /FI "IMAGENAME eq %wemodname%"') do (
+REM Retry to get the wand pid over Proton
+if not defined wandPID (
+ for /F "TOKENS=1,2,*" %%a in ('C:/windows/system32/tasklist /FI "IMAGENAME eq %wandname%"') do (
set void=%%a
- set wemodPID=%%b
+ set wandPID=%%b
)
)
-REM If still not set get wemod pid over wine
-if not defined wemodPID (
- for /F "TOKENS=2 delims=," %%d in ('C:/windows/system32/tasklist /FI "IMAGENAME eq %wemodname%"') do (
- set wemodPID=%%d
+REM If still not set get wand pid over wine
+if not defined wandPID (
+ for /F "TOKENS=2 delims=," %%d in ('C:/windows/system32/tasklist /FI "IMAGENAME eq %wandname%"') do (
+ set wandPID=%%d
)
)
-REM And If still not set retry getting the wemod pid over wine
-if not defined wemodPID (
- for /F "TOKENS=2 delims=," %%d in ('C:/windows/system32/tasklist /FI "IMAGENAME eq %wemodname%"') do (
- set wemodPID=%%d
+REM And If still not set retry getting the wand pid over wine
+if not defined wandPID (
+ for /F "TOKENS=2 delims=," %%d in ('C:/windows/system32/tasklist /FI "IMAGENAME eq %wandname%"') do (
+ set wandPID=%%d
)
)
-if not defined wemodPID (
- echo Attempting to find WeMod PID (attempt %retry_count% of 3)...
+if not defined wandPID (
+ echo Attempting to find Wand PID (attempt %retry_count% of 3)...
if %retry_count% leq 3 (
@ping localhost -n 1 > NUL 2>&1
goto retry_pid
)
- echo Failed to find WeMod PID after multiple attempts. Continuing anyway.
+ echo Failed to find Wand PID after multiple attempts. Continuing anyway.
)
-echo WeMod found with pid %wemodPID%
+echo Wand found with pid %wandPID%
echo.
-echo WeMod found with pid %wemodPID%
+echo Wand found with pid %wandPID%
echo.
REM Start the game and wait for exit
@@ -80,14 +80,14 @@ start /wait "" %*
echo.
echo The game was closed
-if defined wemodPID (
+if defined wandPID (
if exist %temptime% (
del %temptime%
echo Game closed to fast, Game detection may have failed. > %returnfile%
- echo Keep in mind the wemod-launcher usualy can`t detect game launchers. >> %returnfile%
+ echo Keep in mind the wand-launcher usualy can`t detect game launchers. >> %returnfile%
echo THIS IS NOT A BUG, its not possible with the current project structure. >> %returnfile%
echo Only open a Issue if the game (launcher) did not start, >> %returnfile%
- echo or if the game crashed, or if wemod won't closes unexpectedly. >> %returnfile%
+ echo or if the game crashed, or if wand won't closes unexpectedly. >> %returnfile%
echo.
echo Game closed to fast, Game detection may have failed, sending problem to python script and waiting for awnser
:WaitUser
@@ -96,11 +96,11 @@ if defined wemodPID (
goto WaitUser
)
)
- echo Closing WeMod
- C:/windows/system32/taskkill.exe /PID %wemodPID% /F 2>NUL
- C:/windows/system32/taskkill.exe /PID %wemodPID% /F 2>NUL
+ echo Closing Wand
+ C:/windows/system32/taskkill.exe /PID %wandPID% /F 2>NUL
+ C:/windows/system32/taskkill.exe /PID %wandPID% /F 2>NUL
echo.
- echo Killed %wemodname% with pid %wemodPID%
+ echo Killed %wandname% with pid %wandPID%
)
echo.