Repository: RareDevs/Rare (language: python)

Issue:
[BUG] [flatpak] Crash On Start 
**Describe the bug**

A clear and concise description of what the bug is.

**To Reproduce**

Steps to reproduce the behavior:
1. install flatpak rare
2. login using browser
3. crash

**Expected behavior**

no crash

**Screenshots**
![image](https://github.com/Dummerle/Rare/assets/33910565/da7c726b-d359-4745-8a21-bac7387cd30f)



Discussion / hints:
I recently encountered this.This is most likely a permission issue with the flatpak being unable to access the system's `PATH`. @body20002 you can use `flatseal` to modify the access permissions for other flatpaks. In case you are able to solve it through flatseal it can help use update the package.
@loathingKernel I'm not sure what to do exactly adding `PATH=$PATH` as environment variable in flatseal doesn't help
I think the problem is rare doesn't package wine other launcher like lutris and heroic package or at least give you a way to install and manage wine and proton through them and since I don't have wine installed in my system I can't launch rare because it checks for wine at the startup

so to fix this issue there are two solutions
either rare doesn't check for wine at the startup or do without crash but notify the user that wine is not found and asks the users to provide the path

or the second solution which I rather not do is install wine in my system

P.S. wine exists only in lutris, heroic and steam and I have them installed as a flatpak apps
With Rare we made the conscious choice to not provide such functionality as downloading wine or proton versions. So far we are of the opinion that we do not want to do the job of a package manager since there are so many already and we do not want to waste the development effort required to do a sub-par job at it.

The check for wine at startup is part of detecting games installed through integrated third party clients, such as Origin, i.e. Star Wars Battlefront etc.

I am under the impression that we do check if wine is installed at startup and skip that step if it isn't but I will have to check again.
I think the issue could be resolved with a tool like `Proton-Up QT` which's available in [flathub](https://flathub.org/apps/net.davidotek.pupgui2), you could make rare create its folder for wine, something like what steam does `$HOME/.var/app/com.valvesoftware.Steam/data/Steam/compatibilitytools.d/`
and asks the user to use the tool to download whatever version of proton or wine they want to use if they don't have wine installed

I'm not sure on whether they support rare or not but it should be a simple pr to add the support
We can probably check for more locations, but we do not want to hand-hold our users. It is up to the user to configure the wine version they want to use. What we should have done in this case is not run those checks if we didn't find the wine binary. We need both `wine` and `winepath` for that check to work and IIRC `proton` doesn't include it.

In that regard, there is also another extension to this issue, if you as the user selects a wine version which requires a specific runtime, such as lutris's wine, we do not necessarily configure the correct environment for that version, as we would also need to take care of things such as `LD_LIBRARY_PATH`. Such configuration also falls on the user.

In your specific case, you are also using a non-standard way to use steam(flatpak). So we not only have to include the regular paths but also the flatpak paths. This can get out of hand quickly as each and every one of these paths has to be checked. It becomes insane quite quickly. 

Fix patch:
diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml
index 1a3ab52ce..3fa9536f8 100644
--- a/.github/workflows/checks.yml
+++ b/.github/workflows/checks.yml
@@ -24,14 +24,12 @@ jobs:
   pylint:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v2
-        with:
-          submodules: true
+      - uses: actions/checkout@v3
       - name: Set up Python
-        uses: actions/setup-python@v2
+        uses: actions/setup-python@v4
         with:
           python-version: '3.9'
-      - name: Install Test Dependencies
+      - name: Install dependencies
         run: |
           python3 -m pip install --upgrade pip
           pip3 install astroid
@@ -45,4 +43,4 @@ jobs:
           pip3 install qstylizer
       - name: Analysis with pylint
         run: |
-          python3 -m pylint -E rare --jobs=3 --disable=E0611,E1123,E1120 --ignore=ui,singleton.py --extension-pkg-whitelist=PyQt5 --generated-members=PyQt5.*
+          python3 -m pylint -E rare --jobs=3 --disable=E0611,E1123,E1120 --ignore=ui,singleton.py --extension-pkg-whitelist=PyQt5 --generated-members=PyQt5.*,orjson.*
diff --git a/.github/workflows/job_appimage.yml b/.github/workflows/job_appimage.yml
new file mode 100644
index 000000000..028e749be
--- /dev/null
+++ b/.github/workflows/job_appimage.yml
@@ -0,0 +1,40 @@
+name: job_appimage
+
+on:
+  workflow_call:
+    inputs:
+      version:
+        required: true
+        type: string
+
+jobs:
+  build:
+    name: Build
+    runs-on: ubuntu-22.04
+    steps:
+      - uses: actions/checkout@v3
+      - name: Install build dependencies
+        run: |
+          sudo apt update
+          sudo apt install python3 python3-pip python3-setuptools patchelf desktop-file-utils libgdk-pixbuf2.0-dev fakeroot strace fuse
+      - name: Install appimage-builder
+        run: |
+          sudo wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage -O /usr/local/bin/appimagetool
+          sudo chmod +x /usr/local/bin/appimagetool
+          sudo pip3 install appimage-builder
+      - name: Build
+        run: |
+          appimage-builder --skip-test
+          mv Rare-*.AppImage Rare.AppImage
+          mv Rare-*.AppImage.zsync Rare.AppImage.zsync
+
+      - name: Upload artifact
+        uses: actions/upload-artifact@v3
+        with:
+          name: Rare-${{ inputs.version }}.AppImage
+          path: Rare.AppImage
+      - name: Upload artifact
+        uses: actions/upload-artifact@v3
+        with:
+          name: Rare-${{ inputs.version }}.AppImage.zsync
+          path: Rare.AppImage.zsync
\ No newline at end of file
diff --git a/.github/workflows/job_cx-freeze-msi.yml b/.github/workflows/job_cx-freeze-msi.yml
new file mode 100644
index 000000000..f281d0058
--- /dev/null
+++ b/.github/workflows/job_cx-freeze-msi.yml
@@ -0,0 +1,37 @@
+name: job_cx-freeze-msi
+
+on:
+  workflow_call:
+    inputs:
+      version:
+        required: true
+        type: string
+
+jobs:
+  build:
+    name: Build
+    runs-on: "windows-latest"
+    steps:
+      - uses: actions/checkout@v3
+      - uses: actions/setup-python@v4
+        with:
+          cache: pip
+          python-version: '3.11'
+          check-latest: true
+          architecture: x64
+      - name: Install Build Dependencies
+        run: pip3 install --upgrade cx_freeze wheel
+      - name: Install Target Dependencies
+        run: |
+          pip3 install -r requirements.txt
+          pip3 install -r requirements-presence.txt
+      - name: Build
+        run: |
+          python freeze.py bdist_msi
+          mv dist/*.msi Rare.msi
+
+      - name: Upload artifact
+        uses: actions/upload-artifact@v3
+        with:
+          name: Rare-${{ inputs.version }}.msi
+          path: Rare.msi
\ No newline at end of file
diff --git a/.github/workflows/job_cx-freeze-zip.yml b/.github/workflows/job_cx-freeze-zip.yml
new file mode 100644
index 000000000..4fa80d6e3
--- /dev/null
+++ b/.github/workflows/job_cx-freeze-zip.yml
@@ -0,0 +1,39 @@
+name: job_cx-freeze-zip
+
+on:
+  workflow_call:
+    inputs:
+      version:
+        required: true
+        type: string
+
+jobs:
+  build:
+    name: Build
+    runs-on: "windows-latest"
+    steps:
+      - uses: actions/checkout@v3
+      - uses: actions/setup-python@v4
+        with:
+          cache: pip
+          python-version: '3.11'
+          check-latest: true
+          architecture: x64
+      - name: Install build dependencies
+        run: pip3 install cx_freeze
+      - name: Install target dependencies
+        run: |
+          pip3 install -r requirements.txt
+          pip3 install -r requirements-presence.txt
+          pip3 install .
+      - name: Build
+        run: cxfreeze -c rare/main.py --target-dir dist --target-name rare --icon rare/resources/images/Rare.ico -OO --base-name Win32GUI
+      - name: Compress
+        run: |
+          python -c "import shutil; shutil.make_archive('Rare-Windows', 'zip', 'dist')"
+
+      - name: Upload artifact
+        uses: actions/upload-artifact@v3
+        with:
+          name: Rare-Windows-${{ inputs.version }}.zip
+          path: Rare-Windows.zip
\ No newline at end of file
diff --git a/.github/workflows/job_macos.yml b/.github/workflows/job_macos.yml
new file mode 100644
index 000000000..0ca75122f
--- /dev/null
+++ b/.github/workflows/job_macos.yml
@@ -0,0 +1,46 @@
+name: job_macos
+
+on:
+  workflow_call:
+    inputs:
+      version:
+        required: true
+        type: string
+
+jobs:
+  build:
+    name: Build
+    runs-on: macos-latest
+    steps:
+      - uses: actions/checkout@v3
+      - uses: actions/setup-python@v4
+        with:
+          cache: pip
+          python-version: '3.11'
+          check-latest: true
+      - name: Install Build Dependencies
+        run: pip install pyinstaller
+      - name: Install Target Dependencies
+        run: |
+          pip install -r requirements.txt
+      - name: Move files
+        run: mv rare/__main__.py __main__.py
+      - name: Build
+        run: >-
+          pyinstaller -F --name Rare
+          --add-data "rare/resources/languages/*:rare/resources/languages"
+          --add-data "rare/resources/images/*:rare/resources/images/"
+          --windowed
+          --icon rare/resources/images/Rare.icns
+          --hidden-import=legendary
+          __main__.py
+      - name: Create dmg
+        run: |
+          git clone https://github.com/create-dmg/create-dmg
+          create-dmg/create-dmg Rare.dmg dist/Rare.App --volname Rare --volicon rare/resources/images/Rare.icns
+
+      - name: Upload artifact
+        uses: actions/upload-artifact@v3
+        with:
+          name: Rare-${{ inputs.version }}.dmg
+          path: Rare.dmg
\ No newline at end of file
diff --git a/.github/workflows/job_nuitka-win.yml b/.github/workflows/job_nuitka-win.yml
new file mode 100644
index 000000000..3cf181427
--- /dev/null
+++ b/.github/workflows/job_nuitka-win.yml
@@ -0,0 +1,70 @@
+name: job_nuitka-win
+
+on:
+  workflow_call:
+    inputs:
+      version:
+        required: true
+        type: string
+
+jobs:
+  build:
+    name: Build
+    runs-on: "windows-latest"
+    steps:
+      - uses: actions/checkout@v3
+      - uses: actions/setup-python@v4
+        with:
+          cache: pip
+          python-version: '3.9'
+          check-latest: true
+          architecture: x64
+      - name: Install build dependencies
+        run: pip3 install nuitka ordered-set
+      - name: Install target dependencies
+        run: |
+          pip3 install -r requirements.txt
+          pip3 install -r requirements-presence.txt
+      - name: Build
+        run: >-
+          python -m nuitka
+          --assume-yes-for-downloads
+          --msvc=latest
+          --lto=yes
+          --jobs=2
+          --static-libpython=no
+          --standalone
+          --enable-plugin=anti-bloat
+          --enable-plugin=pyqt5
+          --show-modules
+          --show-anti-bloat-changes
+          --follow-stdlib
+          --follow-imports
+          --nofollow-import-to="*.tests"
+          --nofollow-import-to="*.distutils"
+          --prefer-source-code
+          --include-package=pypresence
+          --include-package-data=qtawesome
+          --include-data-dir=rare\resources\images=rare\resources\images
+          --include-data-files=rare\resources\languages=rare\resources\languages="*.qm"
+          --windows-icon-from-ico=rare\resources\images\Rare.ico
+          --windows-company-name=Rare
+          --windows-product-name=Rare
+          --windows-file-description=rare.exe
+          --windows-file-version=${{ inputs.version }}
+          --windows-product-version=${{ inputs.version }}
+          --disable-console
+          rare
+      - name: Fix QtNetwork SSL
+        run: |
+          Copy-Item -Path "rare.dist\libcrypto-1_1.dll" -Destination "rare.dist\libcrypto-1_1-x64.dll"
+          Copy-Item -Path "rare.dist\libssl-1_1.dll" -Destination "rare.dist\libssl-1_1-x64.dll"
+      - name: Compress
+        run: |
+          python -c "import shutil; shutil.make_archive('Rare-Windows', 'zip', 'rare.dist')"
+
+      - name: Upload artifact
+        uses: actions/upload-artifact@v3
+        with:
+          name: Rare-Windows-${{ inputs.version }}.zip
+          path: Rare-Windows.zip
\ No newline at end of file
diff --git a/.github/workflows/job_pypi.yml b/.github/workflows/job_pypi.yml
new file mode 100644
index 000000000..eb0f64aff
--- /dev/null
+++ b/.github/workflows/job_pypi.yml
@@ -0,0 +1,30 @@
+name: job_pypi
+
+on:
+  workflow_call:
+    inputs:
+      version:
+        required: true
+        type: string
+
+jobs:
+  build:
+    name: Deploy
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3
+      - name: Set up Python
+        uses: actions/setup-python@v4
+        with:
+          python-version: '3.9'
+      - name: Install dependencies
+        run: |
+          python3 -m pip install --upgrade pip
+          pip3 install setuptools wheel twine
+      - name: Build and publish
+        env:
+          TWINE_USERNAME: __token__
+          TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
+        run: |
+          python setup.py sdist bdist_wheel
+          twine upload dist/*
\ No newline at end of file
diff --git a/.github/workflows/job_release.yml b/.github/workflows/job_release.yml
new file mode 100644
index 000000000..02b46d6c8
--- /dev/null
+++ b/.github/workflows/job_release.yml
@@ -0,0 +1,49 @@
+name: job_release
+
+on:
+  workflow_call:
+    inputs:
+      file1:
+        required: true
+        type: string
+      name1:
+        required: true
+        type: string
+      name2:
+        type: string
+      file2:
+        type: string
+
+jobs:
+  release:
+    name: Upload
+    runs-on: ubuntu-latest
+    steps:
+
+    - name: Download ${{ inputs.name1 }} artifact
+      uses: actions/download-artifact@v3
+      with:
+        name: ${{ inputs.name1 }}
+    - name: Upload ${{ inputs.name1 }} to release
+      uses: svenstaro/upload-release-action@v2
+      with:
+        repo_token: ${{ secrets.GITHUB_TOKEN }}
+        file: ${{ inputs.file1 }}
+        asset_name: ${{ inputs.name1 }}
+        tag: ${{ github.ref }}
+        overwrite: true
+
+    - name: Download ${{ inputs.name2 }} artifact
+      uses: actions/download-artifact@v3
+      if: ${{ inputs.name2 != '' }}
+      with:
+        name: ${{ inputs.name2 }}
+    - name: Upload ${{ inputs.name2 }} to release
+      uses: svenstaro/upload-release-action@v2
+      if: ${{ inputs.name2 != '' }}
+      with:
+        repo_token: ${{ secrets.GITHUB_TOKEN }}
+        file: ${{ inputs.file2 }}
+        asset_name: ${{ inputs.name2 }}
+        tag: ${{ github.ref }}
+        overwrite: true
\ No newline at end of file
diff --git a/.github/workflows/job_ubuntu.yml b/.github/workflows/job_ubuntu.yml
new file mode 100644
index 000000000..a7ec01522
--- /dev/null
+++ b/.github/workflows/job_ubuntu.yml
@@ -0,0 +1,39 @@
+name: job_ubuntu
+
+on:
+  workflow_call:
+    inputs:
+      version:
+        required: true
+        type: string
+
+jobs:
+  build:
+    name: Build
+    runs-on: ubuntu-22.04
+    steps:
+      - name: Install makedeb
+        run: |
+          wget -qO - 'https://proget.makedeb.org/debian-feeds/makedeb.pub' | gpg --dearmor | sudo tee /usr/share/keyrings/makedeb-archive-keyring.gpg 1> /dev/null
+          echo 'deb [signed-by=/usr/share/keyrings/makedeb-archive-keyring.gpg arch=all] https://proget.makedeb.org/ makedeb main' | sudo tee /etc/apt/sources.list.d/makedeb.list
+          sudo apt update
+          sudo apt install makedeb
+      - name: Prepare source directory
+        run: |
+          git clone https://github.com/RareDevs/package-mpr.git build
+          sed '/^pkgver=/d' -i build/PKGBUILD
+          sed '/^source=/d' -i build/PKGBUILD
+          echo "pkgver=${{ inputs.version }}" >> build/PKGBUILD
+          echo "source=(\"git+https://github.com/${{ github.repository }}.git#branch=${{ github.ref_name }}\")" >> build/PKGBUILD
+
+      - name: Run makedeb
+        run: |
+          cd build
+          makedeb -d
+          mv *.deb ../Rare.deb
+
+      - name: Upload artifact
+        uses: actions/upload-artifact@v3
+        with:
+          name: Rare-${{ inputs.version }}.deb
+          path: Rare.deb
\ No newline at end of file
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index c0273f6c4..02951da94 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -3,7 +3,6 @@ name: "Release"
 
 
 on:
-  workflow_dispatch:
   release:
     types: [ published ]
 
@@ -13,254 +12,98 @@ permissions:
 
 
 jobs:
-  pypi-deploy:
-    if: "!github.event.release.prerelease"
-    runs-on: ubuntu-latest
-    steps:
-      - uses: actions/checkout@v3
-      - name: Set up Python
-        uses: actions/setup-python@v4
-        with:
-          python-version: '3.9'
-      - name: Install dependencies
-        run: |
-          python -m pip install --upgrade pip
-          pip install setuptools wheel twine
-      - name: Build and publish
-        env:
-          TWINE_USERNAME: __token__
-          TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
-        run: |
-          python setup.py sdist bdist_wheel
-          twine upload dist/*
 
-  deb-package:
+  describe:
+    name: Version ${{ github.ref_name }}
     runs-on: ubuntu-latest
     steps:
-      - name: Install Makedeb
-        run: |
-          wget -qO - 'https://proget.hunterwittenborn.com/debian-feeds/makedeb.pub' | gpg --dearmor | sudo tee /usr/share/keyrings/makedeb-archive-keyring.gpg &> /dev/null
-          echo 'deb [signed-by=/usr/share/keyrings/makedeb-archive-keyring.gpg arch=all] https://proget.hunterwittenborn.com/ makedeb main' | sudo tee /etc/apt/sources.list.d/makedeb.list
-          sudo apt update
-          sudo apt install makedeb
-      - name: Prepare source directory
-        run: |
-          git clone https://mpr.makedeb.org/rare build
-          sed -i "s/pkgver=.*/pkgver=${{ github.event.release.tag_name }}/g" build/PKGBUILD
+      - run: "true"
 
-      - name: build deb
-        run: |
-          cd build
-          makedeb -d
-          mv *.deb ../Rare.deb
-
-      - name: Upload to Release
-        uses: svenstaro/upload-release-action@v2
-        with:
-          repo_token: ${{ secrets.GITHUB_TOKEN }}
-          file: Rare.deb
-          asset_name: Rare-${{ github.event.release.tag_name }}.deb
-          tag: ${{ github.ref }}
-          overwrite: true
+  pypi:
+    if: "!github.event.release.prerelease"
+    name: PyPI
+    uses: ./.github/workflows/job_pypi.yml
+    with:
+      version: ${{ github.ref_name }}
+
+  ubuntu:
+    name: Ubuntu
+    uses: ./.github/workflows/job_ubuntu.yml
+    with:
+      version: ${{ github.ref_name }}
+  ubuntu-release:
+    needs: ubuntu
+    name: Ubuntu
+    uses: ./.github/workflows/job_release.yml
+    with:
+      file1: Rare.deb
+      name1: Rare-${{ github.ref_name }}.deb
 
   appimage:
-    runs-on: ubuntu-22.04
-    steps:
-      - uses: actions/checkout@v3
-      - name: Install build dependencies
-        run: |
-          sudo apt update
-          sudo apt install python3 python3-pip python3-setuptools patchelf desktop-file-utils libgdk-pixbuf2.0-dev fakeroot strace fuse
-      - name: Install appimage-builder
-        run: |
-          sudo wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage -O /usr/local/bin/appimagetool
-          sudo chmod +x /usr/local/bin/appimagetool
-          sudo pip3 install appimage-builder
-      - name: Build
-        run: |
-          cp rare/__main__.py .
-          appimage-builder --skip-test
-          mv Rare-*.AppImage Rare.AppImage
-          mv Rare-*.AppImage.zsync Rare.AppImage.zsync
-
-      - name: Upload to Release
-        uses: svenstaro/upload-release-action@v2
-        with:
-          repo_token: ${{ secrets.GITHUB_TOKEN }}
-          file: Rare.AppImage
-          asset_name: Rare-${{ github.event.release.tag_name }}.AppImage
-          tag: ${{ github.ref }}
-          overwrite: true
-      - name: Upload to Release (zsync)
-        uses: svenstaro/upload-release-action@v2
-        with:
-          repo_token: ${{ secrets.GITHUB_TOKEN }}
-          file: Rare.AppImage.zsync
-          asset_name: Rare-${{ github.event.release.tag_name }}.AppImage.zsync
-          tag: ${{ github.ref }}
-          overwrite: true
-
-  nuitka:
+    name: AppImage
+    uses: ./.github/workflows/job_appimage.yml
+    with:
+      version: ${{ github.ref_name }}
+  appimage-release:
+    needs: appimage
+    name: AppImage
+    uses: ./.github/workflows/job_release.yml
+    with:
+      file1: Rare.AppImage
+      name1: Rare-${{ github.ref_name }}.AppImage
+      file2: Rare.AppImage.zsync
+      name2: Rare-${{ github.ref_name }}.AppImage.zsync
+
+  nuitka-win:
     if: ${{ false }}
-    runs-on: "windows-latest"
-    steps:
-      - uses: actions/checkout@v3
-      - uses: actions/setup-python@v4
-        with:
-          cache: pip
-          python-version: '3.9'
-          check-latest: true
-          architecture: x64
-      - name: Install build dependencies
-        run: pip3 install nuitka ordered-set
-      - name: Install target dependencies
-        run: |
-          pip3 install -r requirements.txt
-          pip3 install -r requirements-presence.txt
-      - name: Build
-        run: >-
-          python -m nuitka
-          --assume-yes-for-downloads
-          --msvc=latest
-          --lto=yes
-          --jobs=2
-          --static-libpython=no
-          --standalone
-          --enable-plugin=anti-bloat
-          --enable-plugin=pyqt5
-          --show-modules
-          --show-anti-bloat-changes
-          --follow-stdlib
-          --follow-imports
-          --nofollow-import-to="*.tests"
-          --nofollow-import-to="*.distutils"
-          --prefer-source-code
-          --include-package=pypresence
-          --include-package-data=qtawesome
-          --include-data-dir=rare\resources\images=rare\resources\images
-          --include-data-files=rare\resources\languages=rare\resources\languages="*.qm"
-          --windows-icon-from-ico=rare\resources\images\Rare.ico
-          --windows-company-name=Rare
-          --windows-product-name=Rare
-          --windows-file-description=rare.exe
-          --windows-file-version=${{ github.event.release.tag_name }}
-          --windows-product-version=${{ github.event.release.tag_name }}
-          --disable-console
-          rare
-      - name: Fix QtNetwork SSL
-        run: |
-          Copy-Item -Path "rare.dist\libcrypto-1_1.dll" -Destination "rare.dist\libcrypto-1_1-x64.dll"
-          Copy-Item -Path "rare.dist\libssl-1_1.dll" -Destination "rare.dist\libssl-1_1-x64.dll"
-      - name: Compress
-        run: |
-          python -c "import shutil; shutil.make_archive('Rare-Windows', 'zip', 'rare.dist')"
-
-      - name: Upload to Release
-        uses: svenstaro/upload-release-action@v2
-        with:
-          repo_token: ${{ secrets.GITHUB_TOKEN }}
-          file: Rare-Windows.zip
-          asset_name: Rare-Windows-${{ github.event.release.tag_name }}.zip
-          tag: ${{ github.ref }}
-          overwrite: true
-
-  cx_freeze_msi:
-    runs-on: "windows-latest"
-    steps:
-      - uses: actions/checkout@v3
-      - uses: actions/setup-python@v4
-        with:
-          cache: pip
-          python-version: '3.11'
-          check-latest: true
-          architecture: x64
-      - name: Install Build Dependencies
-        run: pip3 install --upgrade cx_freeze wheel
-      - name: Install Target Dependencies
-        run: |
-          pip3 install -r requirements.txt
-          pip3 install -r requirements-presence.txt
-      - name: Build
-        run: |
-          python freeze.py bdist_msi
-          mv dist/*.msi Rare.msi
-
-      - name: Upload to Release
-        uses: svenstaro/upload-release-action@v2
-        with:
-          repo_token: ${{ secrets.GITHUB_TOKEN }}
-          file: Rare.msi
-          asset_name: Rare-${{ github.event.release.tag_name }}.msi
-          tag: ${{ github.ref }}
-          overwrite: true
-
-  cx_freeze_zip:
-    runs-on: "windows-latest"
-    steps:
-      - uses: actions/checkout@v3
-      - uses: actions/setup-python@v4
-        with:
-          cache: pip
-          python-version: '3.11'
-          check-latest: true
-          architecture: x64
-      - name: Install build dependencies
-        run: pip3 install cx_freeze
-      - name: Install target dependencies
-        run: |
-          pip3 install -r requirements.txt
-          pip3 install -r requirements-presence.txt
-          pip3 install .
-      - name: Build
-        run: cxfreeze -c rare/__main__.py --target-dir dist --target-name rare --icon rare/resources/images/Rare.ico -OO --base-name Win32GUI
-      - name: Compress
-        run: |
-          python -c "import shutil; shutil.make_archive('Rare-Windows', 'zip', 'dist')"
-
-      - name: Upload to Release
-        uses: svenstaro/upload-release-action@v2
-        with:
-          repo_token: ${{ secrets.GITHUB_TOKEN }}
-          file: Rare-Windows.zip
-          asset_name: Rare-Windows-${{ github.event.release.tag_name }}.zip
-          tag: ${{ github.ref }}
-          overwrite: true
-
-  mac_os:
-    runs-on: macos-latest
-    steps:
-      - uses: actions/checkout@v3
-      - uses: actions/setup-python@v4
-        with:
-          cache: pip
-          python-version: '3.11'
-          check-latest: true
-      - name: Install Build Dependencies
-        run: pip install pyinstaller
-      - name: Install Target Dependencies
-        run: |
-          pip install -r requirements.txt
-      - name: Move files
-        run: mv rare/__main__.py __main__.py
-      - name: Build
-        run: >-
-          pyinstaller -F --name Rare
-          --add-data "rare/resources/languages/*:rare/resources/languages"
-          --add-data "rare/resources/images/*:rare/resources/images/"
-          --windowed
-          --icon rare/resources/images/Rare.icns
-          --hidden-import=legendary
-          __main__.py
-      - name: Create dmg
-        run: |
-          git clone https://github.com/create-dmg/create-dmg
-          create-dmg/create-dmg Rare.dmg dist/Rare.App --volname Rare --volicon rare/resources/images/Rare.icns
+    name: Nuitka Windows
+    uses: ./.github/workflows/job_nuitka-win.yml
+    with:
+      version: ${{ github.ref_name }}
+  nuitka-win-release:
+    needs: nuitka-win
+    name: Nuitka Windows
+    uses: ./.github/workflows/job_release.yml
+    with:
+      file1: Rare-Windows.zip
+      name1: Rare-Windows-${{ github.ref_name }}.zip
+
+  cx-freeze-msi:
+    name: cx-Freeze msi
+    uses: ./.github/workflows/job_cx-freeze-msi.yml
+    with:
+      version: ${{ github.ref_name }}
+  cx-freeze-msi-release:
+    needs: cx-freeze-msi
+    name: cx-Freeze msi
+    uses: ./.github/workflows/job_release.yml
+    with:
+      file1: Rare.msi
+      name1: Rare-${{ github.ref_name }}.msi
+
+  cx-freeze-zip:
+    name: cx-Freeze zip
+    uses: ./.github/workflows/job_cx-freeze-zip.yml
+    with:
+      version: ${{ github.ref_name }}
+  cx-freeze-zip-release:
+    needs: cx-freeze-zip
+    name: cx-Freeze zip
+    uses: ./.github/workflows/job_release.yml
+    with:
+      file1: Rare-Windows.zip
+      name1: Rare-Windows-${{ github.ref_name }}.zip
+
+  macos:
+    name: MacOS
+    uses: ./.github/workflows/job_macos.yml
+    with:
+      version: ${{ github.ref_name }}
+  macos-release:
+    needs: macos
+    name: MacOS
+    uses: ./.github/workflows/job_release.yml
+    with:
+      file1: Rare.dmg
+      name1: Rare-${{ github.ref_name }}.dmg
 
-      - name: Upload to Release
-        uses: svenstaro/upload-release-action@v2
-        with:
-          repo_token: ${{ secrets.GITHUB_TOKEN }}
-          file: Rare.dmg
-          asset_name: Rare-${{ github.event.release.tag_name }}.dmg
-          tag: ${{ github.ref }}
-          overwrite: true
diff --git a/.github/workflows/snapshot.yml b/.github/workflows/snapshot.yml
new file mode 100644
index 000000000..acb00e011
--- /dev/null
+++ b/.github/workflows/snapshot.yml
@@ -0,0 +1,88 @@
+
+name: "Snapshot"
+
+
+on:
+  workflow_dispatch:
+  pull_request:
+    branches:
+      - main
+    types: [closed]
+
+
+jobs:
+
+  version:
+    name: Describe
+    runs-on: ubuntu-latest
+    outputs:
+      tag_abbrev: ${{ steps.describe.outputs.tag_abbrev }}
+      tag_offset: ${{ steps.describe.outputs.tag_offset }}
+      sha_short: ${{ steps.describe.outputs.sha_short }}
+      full_desc: ${{ steps.describe.outputs.full_desc }}
+      branch: ${{ steps.describe.outputs.branch }}
+    steps:
+      - uses: actions/checkout@v3
+        with:
+          fetch-depth: 0
+      - name: Describe version
+        id: describe
+        shell: bash
+        run: |
+          tag_abbrev=$(git tag --sort=v:refname | grep -oE "(^[0-9]+\.[0-9]+(.[0-9]+)?)$" | tail -1)
+          echo "tag_abbrev=$tag_abbrev" >> $GITHUB_OUTPUT
+          echo "tag_offset=$(git rev-list $tag_abbrev..HEAD --count)" >> $GITHUB_OUTPUT
+          echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
+          echo "full_desc=$(git describe --long --tags)" >> $GITHUB_OUTPUT
+          echo "branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> $GITHUB_OUTPUT
+
+  describe:
+    needs: version
+    name: Version ${{ needs.version.outputs.tag_abbrev }}.${{ needs.version.outputs.tag_offset }}
+    runs-on: ubuntu-latest
+    steps:
+      - run: "true"
+
+  ubuntu:
+    needs: version
+    name: Ubuntu
+    uses: ./.github/workflows/job_ubuntu.yml
+    with:
+      version: ${{ needs.version.outputs.tag_abbrev }}.${{ needs.version.outputs.tag_offset }}
+
+  appimage:
+    needs: version
+    name: AppImage
+    uses: ./.github/workflows/job_appimage.yml
+    with:
+      version: ${{ needs.version.outputs.tag_abbrev }}.${{ needs.version.outputs.tag_offset }}
+
+  nuitka-win:
+    if: ${{ false }}
+    needs: version
+    name: Nuitka Windows
+    uses: ./.github/workflows/job_nuitka-win.yml
+    with:
+      version: ${{ needs.version.outputs.tag_abbrev }}.${{ needs.version.outputs.tag_offset }}
+
+  cx-freeze-msi:
+    needs: version
+    name: cx-Freeze msi
+    uses: ./.github/workflows/job_cx-freeze-msi.yml
+    with:
+      version: ${{ needs.version.outputs.tag_abbrev }}.${{ needs.version.outputs.tag_offset }}
+
+  cx-freeze-zip:
+    needs: version
+    name: cx-Freeze zip
+    uses: ./.github/workflows/job_cx-freeze-zip.yml
+    with:
+      version: ${{ needs.version.outputs.tag_abbrev }}.${{ needs.version.outputs.tag_offset }}
+
+  macos:
+    needs: version
+    name: MacOS
+    uses: ./.github/workflows/job_macos.yml
+    with:
+      version: ${{ needs.version.outputs.tag_abbrev }}.${{ needs.version.outputs.tag_offset }}
+
diff --git a/AppImageBuilder.yml b/AppImageBuilder.yml
index 2c9a4caec..dc20b8e12 100644
--- a/AppImageBuilder.yml
+++ b/AppImageBuilder.yml
@@ -3,17 +3,16 @@ version: 1
 
 script:
   # Remove any previous build
-  - rm -rf AppDir Rare | true
+  - rm -rf AppDir | true
   # Make usr and icons dirs
   - mkdir -p AppDir/usr/src
   - mkdir -p AppDir/usr/share/icons/hicolor/256x256/apps/
   # Copy source files
   - cp -r rare AppDir/usr/src/rare
-  - mv AppDir/usr/src/rare/__main__.py AppDir/usr/src/
   # copy Logo
   - cp AppDir/usr/src/rare/resources/images/Rare.png AppDir/usr/share/icons/hicolor/256x256/apps/
   # Install application dependencies
-  - python3 -m pip install --ignore-installed --prefix=/usr --root=AppDir pypresence qtawesome legendary-gl typing_extensions
+  - python3 -m pip install --ignore-installed --prefix=/usr --root=AppDir pypresence qtawesome legendary-gl orjson
 
 AppDir:
   path: AppDir
@@ -23,7 +22,7 @@ AppDir:
     icon: Rare
     version: 1.10.7
     exec: usr/bin/python3
-    exec_args: $APPDIR/usr/src/__main__.py $@
+    exec_args: $APPDIR/usr/src/rare/main.py $@
   apt:
     arch: amd64
     allow_unauthenticated: true
diff --git a/README.md b/README.md
index 8dec58cb2..face2a34e 100644
--- a/README.md
+++ b/README.md
@@ -45,32 +45,36 @@ Run it via:
 There are some AUR packages available:
 
 - [rare](https://aur.archlinux.org/packages/rare) - for stable releases
-- [rare-git](https://aur.archlinux.org/packages/rare-git) - for the latest features, which are not in a stable release
+- [rare-git](https://aur.archlinux.org/packages/rare-git) - for the latest development version
 
 #### Debian based
 
 - DUR package: [rare](https://mpr.hunterwittenborn.com/packages/rare)
-- .deb file in [releases page](https://github.com/Dummerle/Rare/releases)
+- `.deb` file in [releases page](https://github.com/Dummerle/Rare/releases)
 
 **Note**:
-
-- pypresence is an optional package. You can install it
-  from [DUR](https://mpr.hunterwittenborn.com/packages/python3-pypresence) or with pip.
-- Do not wonder if some icons look strange, because the official python3-qtawesome package is too old. Many icons were
-  replaced.
+- pypresence is an optional package. You can install it from [DUR](https://mpr.hunterwittenborn.com/packages/python3-pypresence) or with pip.
+- Some icons might look strange on Debian based distributions. The official python3-qtawesome package is too old.
 
 
 ### macOS
 
-There is a .dmg file available in [releases page](https://github.com/Dummerle/Rare/releases).
+There is a `.dmg` file available in [releases page](https://github.com/Dummerle/Rare/releases).
 
-**Note**: When you launch it, you will see an error, that the package is from an unknown source. You have to enable it
-manually in `Settings -> Security and Privacy`. Otherwise, Gatekeeper will block Rare from running.
+**Note**: When you launch it, you will see an error, that the package is from an unknown source. You have to enable it  manually in `Settings -> Security and Privacy`. Otherwise, Gatekeeper will block Rare from running.
 
 You can also use `pip`.
 
 ### Windows
 
+There is an `.msi` installer available in [releases page](https://github.com/Dummerle/Rare/releases).
+
+There is also a semi-portable `.zip` archive in [releases page](https://github.com/Dummerle/Rare/releases) that lets you run Rare without installing it.
+
+**Important**: On recent version of Windows you should have MSVC 2015 installed, you can get it from [here](https://learn.microsoft.com/en-US/cpp/windows/latest-supported-vc-redist?view=msvc-170#visual-studio-2015-2017-2019-and-2022)
+
+#### Packages
+
 - Rare is available as a [Winget package](https://github.com/microsoft/winget-pkgs/tree/master/manifests/d/Dummerle/Rare)
 You can install Rare with the following one-liner:
 
@@ -81,10 +85,7 @@ You can install Rare with the following one-liner:
 
     `choco install rare`
 
-- There is a small beta tool for Windows: [Rare Updater](https://github.com/Dummerle/RareUpdater), which installs and updates rare with a single click
-
-
-*NOTE*: On recent Windows you should have MSVC 2015 installed, you can get it from [here](https://learn.microsoft.com/en-US/cpp/windows/latest-supported-vc-redist?view=msvc-170#visual-studio-2015-2017-2019-and-2022)
+- We also have a beta tool for Windows: [Rare Updater](https://github.com/Dummerle/RareUpdater), which installs and updates rare with a single click
 
 ### Packages
 
@@ -95,7 +96,8 @@ file for macOS.
 
 In the [actions](https://github.com/Dummerle/Rare/actions) tab you can find packages for the latest commits.
 
-**Note**: They might be unstable.
+**Note**: They might be unstable and likely broken.
+
 
 ### Installation via pip (platform independent)
 
@@ -105,22 +107,13 @@ Linux, Mac and FreeBSD: execute `rare` in your terminal.
 
 Windows: execute `pythonw -m rare` in cmd
 
-It is possible to create a desktop link, or a start menu link. Execute the command above with `--desktop-shortcut`
-or `--startmenu-shortcut` option, alternatively you can create them in the settings.
+It is possible to create a desktop link, or a start menu link. Execute the command above with `--desktop-shortcut` or `--startmenu-shortcut` option, alternatively you can create them in the settings.
 
 **Note about $PATH**:
 
-On Linux:
-
-`/home/user/.local/bin` must be in your PATH.
-
-On Windows:
-
-`PythonInstallationDirectory\Scripts` must be in your PATH.
-
-On Mac:
-
-`/Users/user/Library/Python/3.x/bin` must be in your PATH.
+* On Linux `/home/user/.local/bin` must be in your PATH.
+* On Windows `PythonInstallationDirectory\Scripts` must be in your PATH.
+* On Mac  `/Users/user/Library/Python/3.x/bin` must be in your PATH.
 
 
 ### Run from source
@@ -128,10 +121,9 @@ On Mac:
 1. Clone the repo: `git clone https://github.com/Dummerle/Rare
 2. Change your working directory to the project folder: `cd Rare`
 3. Run `pip install -r requirements.txt` to install all required dependencies.
-   If you want to be able to use the automatic login and Discord pypresence, run `pip install -r requirements-full.txt`
-   If you are on Arch you can
-   run `sudo pacman --needed -S python-wheel python-setuptools python-pyqt5 python-qtawesome python-requests python-typing_extensions` and `yay -S legendary`
-   If you are on FreeBSD you have to install py39-qt5 from the packages: `sudo pkg install py39-qt5`
+   * If you want to be able to use the automatic login and Discord pypresence, run `pip install -r requirements-full.txt`
+   * If you are on Arch you can run `sudo pacman --needed -S python-wheel python-setuptools python-pyqt5 python-qtawesome python-requests python-orjson` and `yay -S legendary`
+   * If you are on FreeBSD you have to install py39-qt5 from the packages: `sudo pkg install py39-qt5`
 4. Run `python3 -m rare`
 
 ## Contributing
@@ -139,8 +131,7 @@ On Mac:
 There are several options to contribute.
 
 - If you know Python and PyQt, you can implement new features (Some ideas are in the projects tab).
-- You can translate the application in your language: Check our [transifex](https://www.transifex.com/rare-1/rare) page
-  for that.
+- You can translate the application in your language: Check our [transifex](https://www.transifex.com/rare-1/rare) page for that.
 
 More information is available in CONTRIBUTING.md.
 
diff --git a/freeze.py b/freeze.py
index 19f4bf48e..99c1b49a2 100644
--- a/freeze.py
+++ b/freeze.py
@@ -31,9 +31,11 @@
 base = "Win32GUI"
 
 exe = Executable(
-    "rare/__main__.py",
-    base=base, icon="rare/resources/images/Rare.ico",
-    target_name="Rare")
+    "rare/main.py",
+    base=base,
+    icon="rare/resources/images/Rare.ico",
+    target_name=name
+)
 
 setup(
     name=name,
diff --git a/pyproject.toml b/pyproject.toml
index 466ea084f..5d51e960b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -17,7 +17,7 @@ description = "A GUI for Legendary"
 authors = ["Dummerle"]
 license = "GPL3"
 readme = "README.md"
-repository = "https://github.com/Dummerle/Rare"
+repository = "https://github.com/RareDevs/Rare"
 
 [tool.poetry.dependencies]
 python = "^3.9"
@@ -32,10 +32,11 @@ pywebview = [
     { version = "^3.6.3", extras = ["gtk"], platform = "freebsd", optional = true },
 ]
 legendary-gl = "^0.20.34"
+orjson = "^3.8.0"
 typing-extensions = "^4.3.0"
 
 [tool.poetry.scripts]
-start = "rare.__main__:main"
+start = "rare.main:main"
 
 [tool.poetry.dev-dependencies]
 Nuitka = "^1.0.6"
diff --git a/rare/__init__.py b/rare/__init__.py
index bf4033ab7..a1e308867 100644
--- a/rare/__init__.py
+++ b/rare/__init__.py
@@ -1,2 +1,8 @@
 __version__ = "1.10.7"
 __codename__ = "Garlic Crab"
+
+# For PyCharm profiler
+if __name__ == "__main__":
+    import sys
+    from rare.main import main
+    sys.exit(main())
diff --git a/rare/__main__.py b/rare/__main__.py
old mode 100755
new mode 100644
index d60a50c80..6d290a34a
--- a/rare/__main__.py
+++ b/rare/__main__.py
@@ -1,133 +1,4 @@
-#!/usr/bin/env python3
-import multiprocessing
-import os
-import pathlib
-import sys
-from argparse import ArgumentParser
-
-
-def main():
-    # fix cx_freeze
-    multiprocessing.freeze_support()
-
-    # insert legendary for installed via pip/setup.py submodule to path
-    # if not __name__ == "__main__":
-    #     sys.path.insert(0, os.path.join(os.path.dirname(__file__), "legendary"))
-
-    # CLI Options
-    parser = ArgumentParser()
-    parser.add_argument(
-        "-V", "--version", action="store_true", help="Shows version and exits"
-    )
-    parser.add_argument(
-        "-S",
-        "--silent",
-        action="store_true",
-        help="Launch Rare in background. Open it from System Tray Icon",
-    )
-    parser.add_argument("--debug", action="store_true", help="Launch in debug mode")
-    parser.add_argument(
-        "--offline", action="store_true", help="Launch Rare in offline mode"
-    )
-    parser.add_argument(
-        "--test-start", action="store_true", help="Quit immediately after launch"
-    )
-
-    parser.add_argument(
-        "--desktop-shortcut",
-        action="store_true",
-        dest="desktop_shortcut",
-        help="Use this, if there is no link on desktop to start Rare",
-    )
-    parser.add_argument(
-        "--startmenu-shortcut",
-        action="store_true",
-        dest="startmenu_shortcut",
-        help="Use this, if there is no link in start menu to launch Rare",
-    )
-    subparsers = parser.add_subparsers(title="Commands", dest="subparser")
-
-    launch_minimal_parser = subparsers.add_parser("start", aliases=["launch"])
-    launch_minimal_parser.add_argument("app_name", help="AppName of the game to launch",
-                                       metavar="<App Name>", action="store")
-    launch_minimal_parser.add_argument("--dry-run", help="Print arguments and exit", action="store_true")
-    launch_minimal_parser.add_argument("--offline", help="Launch game offline",
-                                       action="store_true")
-    launch_minimal_parser.add_argument('--wine-bin', dest='wine_bin', action='store', metavar='<wine binary>',
-                                       default=os.environ.get('LGDRY_WINE_BINARY', None),
-                                       help='Set WINE binary to use to launch the app')
-    launch_minimal_parser.add_argument('--wine-prefix', dest='wine_pfx', action='store', metavar='<wine pfx path>',
-                                       default=os.environ.get('LGDRY_WINE_PREFIX', None),
-                                       help='Set WINE prefix to use')
-    launch_minimal_parser.add_argument("--ask-sync-saves", help="Ask to sync cloud saves",
-                                       action="store_true")
-    launch_minimal_parser.add_argument("--skip-update-check", help="Do not check for updates",
-                                       action="store_true")
-
-    args = parser.parse_args()
-
-    if args.desktop_shortcut or args.startmenu_shortcut:
-        from rare.utils.paths import create_desktop_link
-
-        if args.desktop_shortcut:
-            create_desktop_link(app_name="rare_shortcut", link_type="desktop")
-
-        if args.startmenu_shortcut:
-            create_desktop_link(app_name="rare_shortcut", link_type="start_menu")
-
-        print("Link created")
-        return
-
-    if args.version:
-        from rare import __version__, code_name
-        print(f"Rare {__version__} Codename: {code_name}")
-        return
-
-    if args.subparser == "start" or args.subparser == "launch":
-        from rare import game_launch_helper as helper
-        helper.start_game(args)
-        return
-
-    from rare.utils import singleton
-
-    try:
-        # this object only allows one instance per machine
-
-        me = singleton.SingleInstance()
-    except singleton.SingleInstanceException:
-        print("Rare is already running")
-        from rare.utils.paths import lock_file
-
-        with open(lock_file(), "w") as file:
-            file.write("show")
-            file.close()
-        return
-
-    from rare.app import start
-
-    start(args)
-
-
 if __name__ == "__main__":
-    # run from source
-    # insert raw legendary submodule
-    # sys.path.insert(
-    #     0, os.path.join(pathlib.Path(__file__).parent.absolute(), "legendary")
-    # )
-
-    # insert source directory
-    if "__compiled__" not in globals():
-        sys.path.insert(0, str(pathlib.Path(__file__).parents[1].absolute()))
-
-    # If we are on Windows, and we are in a "compiled" GUI application form
-    # stdout (and stderr?) will be None. So to avoid `'NoneType' object has no attribute 'write'`
-    # errors, redirect both of them to devnull
-    if os.name == "nt" and (getattr(sys, "frozen", False) or ("__compiled__" in globals())):
-        # Check if stdout and stderr are None before redirecting
-        # This is useful in the case of test builds that enable console
-        if sys.stdout is None:
-            sys.stdout = open(os.devnull, 'w')
-        if sys.stderr is None:
-            sys.stderr = open(os.devnull, 'w')
-
-    main()
+    import sys
+    from rare.main import main
+    sys.exit(main())
diff --git a/rare/app.py b/rare/app.py
deleted file mode 100644
index 9d23b8f97..000000000
--- a/rare/app.py
+++ /dev/null
@@ -1,132 +0,0 @@
-import logging
-import os
-import shutil
-import sys
-import traceback
-from argparse import Namespace
-from datetime import datetime, timezone
-from typing import Optional
-
-import requests.exceptions
-from PyQt5.QtCore import QThreadPool, QTimer, pyqtSlot, Qt
-from PyQt5.QtWidgets import QApplication, QMessageBox
-from requests import HTTPError
-
-from rare.components.dialogs.launch_dialog import LaunchDialog
-from rare.components.main_window import MainWindow
-from rare.shared import RareCore
-from rare.utils import config_helper, paths
-from rare.widgets.rare_app import RareApp, RareAppException
-
-logger = logging.getLogger("Rare")
-
-
-class RareException(RareAppException):
-    def __init__(self, parent=None):
-        super(RareException, self).__init__(parent=parent)
-
-    def _handler(self, exc_type, exc_value, exc_tb) -> bool:
-        if exc_type == HTTPError:
-            try:
-                if RareCore.instance() is not None:
-                    if RareCore.instance().core().login():
-                        return True
-                raise ValueError
-            except Exception as e:
-                logger.fatal(str(e))
-                QMessageBox.warning(None, "Error", self.tr("Failed to login"))
-                QApplication.exit(1)
-        return False
-
-
-class Rare(RareApp):
-    def __init__(self, args: Namespace):
-        log_file = "Rare_{0}.log"
-        super(Rare, self).__init__(args, log_file)
-        self._hook.deleteLater()
-        self._hook = RareException(self)
-        self.rcore = RareCore(args=args)
-        self.args = RareCore.instance().args()
-        self.signals = RareCore.instance().signals()
-        self.core = RareCore.instance().core()
-
-        config_helper.init_config_handler(self.core)
-
-        lang = self.settings.value("language", self.core.language_code, type=str)
-        self.load_translator(lang)
-
-        # set Application name for settings
-        self.main_window: Optional[MainWindow] = None
-        self.launch_dialog: Optional[LaunchDialog] = None
-        self.timer: Optional[QTimer] = None
-
-        # launch app
-        self.launch_dialog = LaunchDialog(parent=None)
-        self.launch_dialog.quit_app.connect(self.launch_dialog.close)
-        self.launch_dialog.quit_app.connect(lambda x: sys.exit(x))
-        self.launch_dialog.start_app.connect(self.start_app)
-        self.launch_dialog.start_app.connect(self.launch_dialog.close)
-
-        self.launch_dialog.login()
-
-    def poke_timer(self):
-        dt_exp = datetime.fromisoformat(self.core.lgd.userdata['expires_at'][:-1]).replace(tzinfo=timezone.utc)
-        dt_now = datetime.utcnow().replace(tzinfo=timezone.utc)
-        td = abs(dt_exp - dt_now)
-        self.timer.start(int(td.total_seconds() - 60) * 1000)
-        logger.info(f"Renewed session expires at {self.core.lgd.userdata['expires_at']}")
-
-    def re_login(self):
-        logger.info("Session expires shortly. Renew session")
-        try:
-            self.core.login()
-        except requests.exceptions.ConnectionError:
-            self.timer.start(3000)  # try again if no connection
-            return
-        self.poke_timer()
-
-    def start_app(self):
-        self.timer = QTimer()
-        self.timer.timeout.connect(self.re_login)
-        self.poke_timer()
-
-        self.main_window = MainWindow()
-        self.main_window.exit_app.connect(self.on_exit_app)
-
-        if not self.args.silent:
-            self.main_window.show()
-
-        if self.args.test_start:
-            self.main_window.close()
-            self.main_window = None
-            self.on_exit_app(0)
-
-    @pyqtSlot()
-    @pyqtSlot(int)
-    def on_exit_app(self, exit_code=0):
-        threadpool = QThreadPool.globalInstance()
-        threadpool.waitForDone()
-        if self.timer is not None:
-            self.timer.stop()
-            self.timer.deleteLater()
-            self.timer = None
-        self.rcore.deleteLater()
-        del self.rcore
-        self.processEvents()
-        shutil.rmtree(paths.tmp_dir())
-        os.makedirs(paths.tmp_dir())
-
-        self.exit(exit_code)
-
-
-def start(args):
-    while True:
-        QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)
-        QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True)
-        app = Rare(args)
-        exit_code = app.exec_()
-        # if not restart
-        # restart app
-        del app
-        if exit_code != -133742:
-            break
diff --git a/rare/components/__init__.py b/rare/components/__init__.py
index e69de29bb..8d0e4fd44 100644
--- a/rare/components/__init__.py
+++ b/rare/components/__init__.py
@@ -0,0 +1,131 @@
+import os
+import shutil
+from argparse import Namespace
+from datetime import datetime, timezone
+from typing import Optional
+
+import requests.exceptions
+from PyQt5.QtCore import QThreadPool, QTimer, pyqtSlot, Qt
+from PyQt5.QtWidgets import QApplication, QMessageBox
+from requests import HTTPError
+
+from rare.components.dialogs.launch_dialog import LaunchDialog
+from rare.components.main_window import MainWindow
+from rare.shared import RareCore
+from rare.utils import config_helper, paths
+from rare.utils.misc import ExitCodes
+from rare.widgets.rare_app import RareApp, RareAppException
+
+
+class RareException(RareAppException):
+    def __init__(self, parent=None):
+        super(RareException, self).__init__(parent=parent)
+
+    def _handler(self, exc_type, exc_value, exc_tb) -> bool:
+        if exc_type == HTTPError:
+            try:
+                if RareCore.instance() is not None:
+                    if RareCore.instance().core().login():
+                        return True
+                raise ValueError
+            except Exception as e:
+                self.logger.fatal(str(e))
+                QMessageBox.warning(None, "Error", self.tr("Failed to login"))
+                QApplication.exit(1)
+        return False
+
+
+class Rare(RareApp):
+    def __init__(self, args: Namespace):
+        super(Rare, self).__init__(args, f"{type(self).__name__}_{{0}}.log")
+        self._hook.deleteLater()
+        self._hook = RareException(self)
+        self.rcore = RareCore(args=args)
+        self.args = RareCore.instance().args()
+        self.signals = RareCore.instance().signals()
+        self.core = RareCore.instance().core()
+
+        config_helper.init_config_handler(self.core)
+
+        lang = self.settings.value("language", self.core.language_code, type=str)
+        self.load_translator(lang)
+
+        # set Application name for settings
+        self.main_window: Optional[MainWindow] = None
+        self.launch_dialog: Optional[LaunchDialog] = None
+        self.timer: Optional[QTimer] = None
+
+        # This launches the application after it has been instantiated.
+        # The timer's signal will be serviced once we call `exec()` on the application
+        QTimer.singleShot(0, self.launch_app)
+
+    def poke_timer(self):
+        dt_exp = datetime.fromisoformat(self.core.lgd.userdata['expires_at'][:-1]).replace(tzinfo=timezone.utc)
+        dt_now = datetime.utcnow().replace(tzinfo=timezone.utc)
+        td = abs(dt_exp - dt_now)
+        self.timer.start(int(td.total_seconds() - 60) * 1000)
+        self.logger.info(f"Renewed session expires at {self.core.lgd.userdata['expires_at']}")
+
+    def re_login(self):
+        self.logger.info("Session expires shortly. Renew session")
+        try:
+            self.core.login()
+        except requests.exceptions.ConnectionError:
+            self.timer.start(3000)  # try again if no connection
+            return
+        self.poke_timer()
+
+    @pyqtSlot()
+    def launch_app(self):
+        self.launch_dialog = LaunchDialog(parent=None)
+        self.launch_dialog.exit_app.connect(self.launch_dialog.close)
+        self.launch_dialog.exit_app.connect(self.__on_exit_app)
+        self.launch_dialog.start_app.connect(self.start_app)
+        self.launch_dialog.start_app.connect(self.launch_dialog.close)
+        self.launch_dialog.login()
+
+    @pyqtSlot()
+    def start_app(self):
+        self.timer = QTimer()
+        self.timer.timeout.connect(self.re_login)
+        self.poke_timer()
+
+        self.main_window = MainWindow()
+        self.main_window.exit_app.connect(self.__on_exit_app)
+
+        if not self.args.silent:
+            self.main_window.show()
+
+        if self.args.test_start:
+            self.main_window.close()
+            self.main_window = None
+            self.__on_exit_app(0)
+
+    @pyqtSlot()
+    @pyqtSlot(int)
+    def __on_exit_app(self, exit_code=0):
+        threadpool = QThreadPool.globalInstance()
+        threadpool.waitForDone()
+        if self.timer is not None:
+            self.timer.stop()
+            self.timer.deleteLater()
+            self.timer = None
+        self.rcore.deleteLater()
+        del self.rcore
+        self.processEvents()
+        shutil.rmtree(paths.tmp_dir())
+        os.makedirs(paths.tmp_dir())
+
+        self.exit(exit_code)
+
+
+def start(args) -> int:
+    while True:
+        QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)
+        QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True)
+        app = Rare(args)
+        exit_code = app.exec()
+        del app
+        if exit_code != ExitCodes.LOGOUT:
+            break
+    return exit_code
diff --git a/rare/components/dialogs/install_dialog.py b/rare/components/dialogs/install_dialog.py
index 98383ec1d..4b1e405d4 100644
--- a/rare/components/dialogs/install_dialog.py
+++ b/rare/components/dialogs/install_dialog.py
@@ -164,9 +164,9 @@ def __init__(self, rgame: RareGame, options: InstallOptionsModel, parent=None):
 
         self.non_reload_option_changed("shortcut")
 
-        self.ui.cancel_button.clicked.connect(self.cancel_clicked)
-        self.ui.verify_button.clicked.connect(self.verify_clicked)
-        self.ui.install_button.clicked.connect(self.install_clicked)
+        self.ui.cancel_button.clicked.connect(self.__on_cancel)
+        self.ui.verify_button.clicked.connect(self.__on_verify)
+        self.ui.install_button.clicked.connect(self.__on_install)
 
         self.advanced.ui.install_prereqs_check.setChecked(self.options.install_prereqs)
 
@@ -184,7 +184,7 @@ def execute(self):
             self.get_download_info()
         else:
             self.setModal(True)
-            self.verify_clicked()
+            self.__on_verify()
             self.show()
 
     @pyqtSlot(str)
@@ -205,7 +205,7 @@ def setup_sdl_list(self, platform="Windows"):
                 layout = QVBoxLayout(widget)
                 layout.setSpacing(0)
                 for tag, info in sdl_data.items():
-                    cb = TagCheckBox(info["name"], info["tags"])
+                    cb = TagCheckBox(info["name"], info["description"], info["tags"])
                     if tag == "__required":
                         cb.setChecked(True)
                         cb.setDisabled(True)
@@ -247,7 +247,7 @@ def get_download_info(self):
         self.worker_running = True
         self.threadpool.start(info_worker)
 
-    def verify_clicked(self):
+    def __on_verify(self):
         self.error_box()
         message = self.tr("Updating...")
         self.ui.download_size_text.setText(message)
@@ -282,7 +282,7 @@ def non_reload_option_changed(self, option: str):
         elif option == "install_prereqs":
             self.options.install_prereqs = self.advanced.ui.install_prereqs_check.isChecked()
 
-    def cancel_clicked(self):
+    def __on_cancel(self):
         if self.config_tags is not None:
             config_helper.add_option(self.rgame.app_name, 'install_tags', ','.join(self.config_tags))
         else:
@@ -293,10 +293,20 @@ def cancel_clicked(self):
         self.reject_close = False
         self.close()
 
-    def install_clicked(self):
+    def __on_install(self):
         self.reject_close = False
         self.close()
 
+    @staticmethod
+    def same_platform(download: InstallDownloadModel) -> bool:
+        platform = download.igame.platform
+        if pf.system() == "Windows":
+            return platform == "Windows" or platform == "Win32"
+        elif pf.system() == "Darwin":
+            return platform == "Mac"
+        else:
+            return False
+
     @pyqtSlot(InstallDownloadModel)
     def on_worker_result(self, download: InstallDownloadModel):
         self.__download = download
@@ -314,17 +324,18 @@ def on_worker_result(self, download: InstallDownloadModel):
         self.ui.install_size_text.setStyleSheet("font-style: normal; font-weight: bold")
         self.ui.verify_button.setEnabled(self.options_changed)
         self.ui.cancel_button.setEnabled(True)
-        if pf.system() == "Windows" or ArgumentsSingleton().debug:
-            if download.igame.prereq_info and not download.igame.prereq_info.get("installed", False):
-                self.advanced.ui.install_prereqs_check.setEnabled(True)
-                self.advanced.ui.install_prereqs_label.setEnabled(True)
-                self.advanced.ui.install_prereqs_check.setChecked(True)
-                prereq_name = download.igame.prereq_info.get("name", "")
-                prereq_path = os.path.split(download.igame.prereq_info.get("path", ""))[-1]
-                prereq_desc = prereq_name if prereq_name else prereq_path
-                self.advanced.ui.install_prereqs_check.setText(
-                    self.tr("Also install: {}").format(prereq_desc)
-                )
+        has_prereqs = bool(download.igame.prereq_info) and not download.igame.prereq_info.get("installed", False)
+        if has_prereqs:
+            prereq_name = download.igame.prereq_info.get("name", "")
+            prereq_path = os.path.split(download.igame.prereq_info.get("path", ""))[-1]
+            prereq_desc = prereq_name if prereq_name else prereq_path
+            self.advanced.ui.install_prereqs_check.setText(self.tr("Also install: {}").format(prereq_desc))
+        else:
+            self.advanced.ui.install_prereqs_check.setText("")
+        # Offer to install prerequisites only on same platforms
+        self.advanced.ui.install_prereqs_label.setEnabled(has_prereqs)
+        self.advanced.ui.install_prereqs_check.setEnabled(has_prereqs)
+        self.advanced.ui.install_prereqs_check.setChecked(has_prereqs and self.same_platform(download))
         if self.options.silent:
             self.close()
 
@@ -361,13 +372,15 @@ def closeEvent(self, a0: QCloseEvent) -> None:
 
     def keyPressEvent(self, e: QKeyEvent) -> None:
         if e.key() == Qt.Key_Escape:
-            self.cancel_clicked()
+            e.accept()
+            self.__on_cancel()
 
 
 class TagCheckBox(QCheckBox):
-    def __init__(self, text, tags: List[str], parent=None):
+    def __init__(self, text, desc, tags: List[str], parent=None):
         super(TagCheckBox, self).__init__(parent)
         self.setText(text)
+        self.setToolTip(desc)
         self.tags = tags
 
     def isChecked(self) -> Union[bool, List[str]]:
diff --git a/rare/components/dialogs/launch_dialog.py b/rare/components/dialogs/launch_dialog.py
index ee81fd12d..947d55fe9 100644
--- a/rare/components/dialogs/launch_dialog.py
+++ b/rare/components/dialogs/launch_dialog.py
@@ -14,7 +14,7 @@
 
 
 class LaunchDialog(QDialog):
-    quit_app = pyqtSignal(int)
+    exit_app = pyqtSignal(int)
     start_app = pyqtSignal()
 
     def __init__(self, parent=None):
@@ -33,7 +33,7 @@ def __init__(self, parent=None):
         self.ui = Ui_LaunchDialog()
         self.ui.setupUi(self)
 
-        self.accept_close = False
+        self.reject_close = True
 
         self.progress_info = ElideLabel(parent=self)
         self.progress_info.setFixedHeight(False)
@@ -74,7 +74,7 @@ def login(self):
                     self.show()
                 self.launch()
             else:
-                self.quit_app.emit(0)
+                self.exit_app.emit(0)
 
     def launch(self):
         self.progress_info.setText(self.tr("Preparing Rare"))
@@ -87,11 +87,9 @@ def __on_progress(self, i: int, m: str):
 
     def __on_completed(self):
         logger.info("App starting")
-        self.accept_close = True
+        self.reject_close = False
         self.start_app.emit()
 
     def reject(self) -> None:
-        if self.accept_close:
+        if not self.reject_close:
             super(LaunchDialog, self).reject()
-        else:
-            pass
diff --git a/rare/components/dialogs/login/__init__.py b/rare/components/dialogs/login/__init__.py
index 66a1f3463..f9ca2b599 100644
--- a/rare/components/dialogs/login/__init__.py
+++ b/rare/components/dialogs/login/__init__.py
@@ -1,7 +1,6 @@
-from dataclasses import dataclass
 from logging import getLogger
 
-from PyQt5.QtCore import Qt
+from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot
 from PyQt5.QtWidgets import QLayout, QDialog, QMessageBox, QFrame
 from legendary.core import LegendaryCore
 
@@ -15,13 +14,6 @@
 logger = getLogger("LoginDialog")
 
 
-@dataclass
-class LoginPages:
-    landing: int
-    browser: int
-    import_egl: int
-
-
 class LandingPage(QFrame):
     def __init__(self, parent=None):
         super(LandingPage, self).__init__(parent=parent)
@@ -31,8 +23,7 @@ def __init__(self, parent=None):
 
 
 class LoginDialog(QDialog):
-    logged_in: bool = False
-    pages = LoginPages(landing=0, browser=1, import_egl=2)
+    exit_app: pyqtSignal = pyqtSignal(int)
 
     def __init__(self, core: LegendaryCore, parent=None):
         super(LoginDialog, self).__init__(parent=parent)
@@ -51,6 +42,8 @@ def __init__(self, core: LegendaryCore, parent=None):
         self.ui = Ui_LoginDialog()
         self.ui.setupUi(self)
 
+        self.logged_in: bool = False
+
         self.core = core
         self.args = ArgumentsSingleton()
 
@@ -59,16 +52,16 @@ def __init__(self, core: LegendaryCore, parent=None):
         self.ui.login_stack_layout.addWidget(self.login_stack)
 
         self.landing_page = LandingPage(self.login_stack)
-        self.login_stack.insertWidget(self.pages.landing, self.landing_page)
+        self.login_stack.insertWidget(0, self.landing_page)
 
         self.browser_page = BrowserLogin(self.core, self.login_stack)
-        self.login_stack.insertWidget(self.pages.browser, self.browser_page)
+        self.login_stack.insertWidget(1, self.browser_page)
         self.browser_page.success.connect(self.login_successful)
         self.browser_page.changed.connect(
             lambda: self.ui.next_button.setEnabled(self.browser_page.is_valid())
         )
         self.import_page = ImportLogin(self.core, self.login_stack)
-        self.login_stack.insertWidget(self.pages.import_egl, self.import_page)
+        self.login_stack.insertWidget(2, self.import_page)
         self.import_page.success.connect(self.login_successful)
         self.import_page.changed.connect(lambda: self.ui.next_button.setEnabled(self.import_page.is_valid()))
 
@@ -84,34 +77,34 @@ def __init__(self, core: LegendaryCore, parent=None):
         self.ui.back_button.clicked.connect(self.back_clicked)
         self.ui.next_button.clicked.connect(self.next_clicked)
 
-        self.login_stack.setCurrentIndex(self.pages.landing)
+        self.login_stack.setCurrentWidget(self.landing_page)
 
         self.layout().setSizeConstraint(QLayout.SetFixedSize)
 
     def back_clicked(self):
         self.ui.back_button.setEnabled(False)
         self.ui.next_button.setEnabled(True)
-        self.login_stack.slideInIndex(self.pages.landing)
+        self.login_stack.slideInWidget(self.landing_page)
 
     def browser_radio_clicked(self):
-        self.login_stack.slideInIndex(self.pages.browser)
+        self.login_stack.slideInWidget(self.browser_page)
         self.ui.back_button.setEnabled(True)
         self.ui.next_button.setEnabled(False)
 
     def import_radio_clicked(self):
-        self.login_stack.slideInIndex(self.pages.import_egl)
+        self.login_stack.slideInWidget(self.import_page)
         self.ui.back_button.setEnabled(True)
         self.ui.next_button.setEnabled(self.import_page.is_valid())
 
     def next_clicked(self):
-        if self.login_stack.currentIndex() == self.pages.landing:
+        if self.login_stack.currentWidget() is self.landing_page:
             if self.landing_page.ui.login_browser_radio.isChecked():
                 self.browser_radio_clicked()
             if self.landing_page.ui.login_import_radio.isChecked():
                 self.import_radio_clicked()
-        elif self.login_stack.currentIndex() == self.pages.browser:
+        elif self.login_stack.currentWidget() is self.browser_page:
             self.browser_page.do_login()
-        elif self.login_stack.currentIndex() == self.pages.import_egl:
+        elif self.login_stack.currentWidget() is self.import_page:
             self.import_page.do_login()
 
     def login(self):
diff --git a/rare/components/dialogs/uninstall_dialog.py b/rare/components/dialogs/uninstall_dialog.py
index 64bc64fec..76cee75eb 100644
--- a/rare/components/dialogs/uninstall_dialog.py
+++ b/rare/components/dialogs/uninstall_dialog.py
@@ -1,5 +1,5 @@
 from PyQt5.QtCore import Qt, pyqtSignal, QCoreApplication
-from PyQt5.QtGui import QCloseEvent
+from PyQt5.QtGui import QCloseEvent, QKeyEvent
 from PyQt5.QtWidgets import (
     QDialog,
     QLabel,
@@ -75,3 +75,8 @@ def __on_uninstall(self):
     def __on_cancel(self):
         self.options.values = (None, None, None)
         self.close()
+
+    def keyPressEvent(self, e: QKeyEvent) -> None:
+        if e.key() == Qt.Key_Escape:
+            e.accept()
+            self.__on_cancel()
diff --git a/rare/components/main_window.py b/rare/components/main_window.py
index 404f04a1c..318fc6b66 100644
--- a/rare/components/main_window.py
+++ b/rare/components/main_window.py
@@ -44,7 +44,7 @@ def __init__(self, parent=None):
 
         self.setWindowTitle("Rare - GUI for legendary")
         self.tab_widget = MainTabWidget(self)
-        self.tab_widget.exit_app.connect(self.on_exit_app)
+        self.tab_widget.exit_app.connect(self.__on_exit_app)
         self.setCentralWidget(self.tab_widget)
 
         # Set up status bar stuff (jumping through a lot of hoops)
@@ -112,7 +112,7 @@ def __init__(self, parent=None):
         self.timer.start()
 
         self.tray_icon: TrayIcon = TrayIcon(self)
-        self.tray_icon.exit_app.connect(self.on_exit_app)
+        self.tray_icon.exit_app.connect(self.__on_exit_app)
         self.tray_icon.show_app.connect(self.show)
         self.tray_icon.activated.connect(lambda r: self.toggle() if r == self.tray_icon.DoubleClick else None)
 
@@ -203,7 +203,7 @@ def timer_finished(self):
 
     @pyqtSlot()
     @pyqtSlot(int)
-    def on_exit_app(self, exit_code=0) -> None:
+    def __on_exit_app(self, exit_code=0) -> None:
         self.__exit_code = exit_code
         self.close()
 
@@ -221,43 +221,42 @@ def closeEvent(self, e: QCloseEvent) -> None:
                 return
 
         # FIXME: Move this to RareCore once the download manager is implemented
-        if not self.args.offline:
-            if self.rcore.queue_threadpool.activeThreadCount():
-                reply = QMessageBox.question(
-                    self,
-                    self.tr("Quit {}?").format(QApplication.applicationName()),
-                    self.tr(
-                        "There are currently running operations. "
-                        "Rare cannot exit until they are completed.\n\n"
-                        "Do you want to clear the queue?"
-                    ),
-                    buttons=(QMessageBox.Yes | QMessageBox.No),
-                    defaultButton=QMessageBox.No,
-                )
-                if reply == QMessageBox.Yes:
-                    self.rcore.queue_threadpool.clear()
-                    for qw in self.rcore.queued_workers():
-                        self.rcore.dequeue_worker(qw)
-                    self.update_statusbar()
+        if self.rcore.queue_threadpool.activeThreadCount():
+            reply = QMessageBox.question(
+                self,
+                self.tr("Quit {}?").format(QApplication.applicationName()),
+                self.tr(
+                    "There are currently running operations. "
+                    "Rare cannot exit until they are completed.\n\n"
+                    "Do you want to clear the queue?"
+                ),
+                buttons=(QMessageBox.Yes | QMessageBox.No),
+                defaultButton=QMessageBox.No,
+            )
+            if reply == QMessageBox.Yes:
+                self.rcore.queue_threadpool.clear()
+                for qw in self.rcore.queued_workers():
+                    self.rcore.dequeue_worker(qw)
+                self.update_statusbar()
+            e.ignore()
+            return
+        elif self.tab_widget.downloads_tab.is_download_active:
+            reply = QMessageBox.question(
+                self,
+                self.tr("Quit {}?").format(QApplication.applicationName()),
+                self.tr(
+                    "There is an active download. "
+                    "Quitting Rare now will stop the download.\n\n"
+                    "Are you sure you want to quit?"
+                ),
+                buttons=(QMessageBox.Yes | QMessageBox.No),
+                defaultButton=QMessageBox.No,
+            )
+            if reply == QMessageBox.Yes:
+                self.tab_widget.downloads_tab.stop_download(omit_queue=True)
+            else:
                 e.ignore()
                 return
-            elif self.tab_widget.downloads_tab.is_download_active:
-                reply = QMessageBox.question(
-                    self,
-                    self.tr("Quit {}?").format(QApplication.applicationName()),
-                    self.tr(
-                        "There is an active download. "
-                        "Quitting Rare now will stop the download.\n\n"
-                        "Are you sure you want to quit?"
-                    ),
-                    buttons=(QMessageBox.Yes | QMessageBox.No),
-                    defaultButton=QMessageBox.No,
-                )
-                if reply == QMessageBox.Yes:
-                    self.tab_widget.downloads_tab.stop_download(omit_queue=True)
-                else:
-                    e.ignore()
-                    return
         # FIXME: End of FIXME
         self.timer.stop()
         self.tray_icon.deleteLater()
diff --git a/rare/components/tabs/__init__.py b/rare/components/tabs/__init__.py
index 91a6d1778..c0c195ccf 100644
--- a/rare/components/tabs/__init__.py
+++ b/rare/components/tabs/__init__.py
@@ -2,7 +2,7 @@
 from PyQt5.QtWidgets import QMenu, QTabWidget, QWidget, QWidgetAction, QShortcut, QMessageBox
 
 from rare.shared import RareCore, LegendaryCoreSingleton, GlobalSignalsSingleton, ArgumentsSingleton
-from rare.utils.misc import icon
+from rare.utils.misc import icon, ExitCodes
 from .account import AccountWidget
 from .downloads import DownloadsTab
 from .games import GamesTab
@@ -51,7 +51,7 @@ def __init__(self, parent):
         self.setTabEnabled(button_index, False)
 
         self.account_widget = AccountWidget(self)
-        self.account_widget.logout.connect(self.logout)
+        self.account_widget.exit_app.connect(self.__on_exit_app)
         account_action = QWidgetAction(self)
         account_action.setDefaultWidget(self.account_widget)
         account_button = TabButtonWidget("mdi.account-circle", "Account", fallback_icon="fa.user")
@@ -93,25 +93,28 @@ def resizeEvent(self, event):
         self.tab_bar.setMinimumWidth(self.width())
         super(MainTabWidget, self).resizeEvent(event)
 
-    @pyqtSlot()
-    def logout(self):
+    @pyqtSlot(int)
+    def __on_exit_app(self, exit_code: int):
         # FIXME: Don't allow logging out if there are active downloads
         if self.downloads_tab.is_download_active:
             QMessageBox.warning(
                 self,
-                self.tr("Logout"),
-                self.tr("There are active downloads. Stop them before logging out."),
+                self.tr("Quit") if exit_code == ExitCodes.EXIT else self.tr("Logout"),
+                self.tr("There are active downloads. Stop them before trying to quit."),
             )
             return
         # FIXME: End of FIXME
-        reply = QMessageBox.question(
-            self,
-            self.tr("Logout"),
-            self.tr("Do you really want to logout <b>{}</b>?").format(self.core.lgd.userdata.get("display_name")),
-            buttons=(QMessageBox.Yes | QMessageBox.No),
-            defaultButton=QMessageBox.No,
-        )
+        if exit_code == ExitCodes.LOGOUT:
+            reply = QMessageBox.question(
+                self,
+                self.tr("Logout"),
+                self.tr("Do you really want to logout <b>{}</b>?").format(self.core.lgd.userdata.get("display_name")),
+                buttons=(QMessageBox.Yes | QMessageBox.No),
+                defaultButton=QMessageBox.No,
+            )
 
-        if reply == QMessageBox.Yes:
-            self.core.lgd.invalidate_userdata()
-            self.exit_app.emit(-133742)  # restart exit code
+            if reply == QMessageBox.Yes:
+                self.core.lgd.invalidate_userdata()
+            else:
+                return
+        self.exit_app.emit(exit_code)  # restart exit code
diff --git a/rare/components/tabs/account/__init__.py b/rare/components/tabs/account/__init__.py
index 91835ccee..7e45c5e4b 100644
--- a/rare/components/tabs/account/__init__.py
+++ b/rare/components/tabs/account/__init__.py
@@ -1,13 +1,14 @@
 import webbrowser
 
-from PyQt5.QtCore import pyqtSignal
-from PyQt5.QtWidgets import QWidget, QVBoxLayout, QMessageBox, QLabel, QPushButton
+from PyQt5.QtCore import pyqtSignal, pyqtSlot
+from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QPushButton
 
 from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton
-from rare.utils.misc import icon
+from rare.utils.misc import icon, ExitCodes
 
 
 class AccountWidget(QWidget):
+    exit_app: pyqtSignal = pyqtSignal(int)
     logout: pyqtSignal = pyqtSignal()
 
     def __init__(self, parent):
@@ -25,11 +26,22 @@ def __init__(self, parent):
                 "https://www.epicgames.com/account/personal?productName=epicgames"
             )
         )
-        self.logout_button = QPushButton(self.tr("Logout"))
-        self.logout_button.clicked.connect(self.logout)
+        self.logout_button = QPushButton(self.tr("Logout"), parent=self)
+        self.logout_button.clicked.connect(self.__on_logout)
+        self.quit_button = QPushButton(self.tr("Quit"), parent=self)
+        self.quit_button.clicked.connect(self.__on_quit)
 
         layout = QVBoxLayout(self)
         layout.addWidget(QLabel(self.tr("Account")))
         layout.addWidget(QLabel(self.tr("Logged in as <b>{}</b>").format(username)))
         layout.addWidget(self.open_browser)
         layout.addWidget(self.logout_button)
+        layout.addWidget(self.quit_button)
+
+    @pyqtSlot()
+    def __on_quit(self):
+        self.exit_app.emit(ExitCodes.EXIT)
+
+    @pyqtSlot()
+    def __on_logout(self):
+        self.exit_app.emit(ExitCodes.LOGOUT)
diff --git a/rare/components/tabs/downloads/thread.py b/rare/components/tabs/downloads/thread.py
index e19648903..9309ef25b 100644
--- a/rare/components/tabs/downloads/thread.py
+++ b/rare/components/tabs/downloads/thread.py
@@ -58,6 +58,10 @@ def __finish(self, result):
         self.rgame.signals.progress.finish.emit(not result.code == DlResultCode.FINISHED)
         self.result.emit(result)
 
+    def __status_callback(self, status: UIUpdate):
+        self.progress.emit(status, self.dl_size)
+        self.rgame.signals.progress.update.emit(int(status.progress))
+
     def run(self):
         cli = LegendaryCLI(self.core)
         self.item.download.dlm.logging_queue = cli.logging_queue
@@ -71,9 +75,7 @@ def run(self):
             time.sleep(1)
             while self.item.download.dlm.is_alive():
                 try:
-                    status = self.item.download.dlm.status_queue.get(timeout=1.0)
-                    self.rgame.signals.progress.update.emit(int(status.progress))
-                    self.progress.emit(status, self.dl_size)
+                    self.__status_callback(self.item.download.dlm.status_queue.get(timeout=1.0))
                 except queue.Empty:
                     pass
                 if self.dlm_signals.update:
diff --git a/rare/components/tabs/games/game_info/cloud_saves.py b/rare/components/tabs/games/game_info/cloud_saves.py
index 6381904cd..a39d492b6 100644
--- a/rare/components/tabs/games/game_info/cloud_saves.py
+++ b/rare/components/tabs/games/game_info/cloud_saves.py
@@ -117,6 +117,7 @@ def compute_save_path(self):
                 logger.warning(str(e))
                 resolver = WineResolver(self.core, self.rgame.raw_save_path, self.rgame.app_name)
                 if not resolver.wine_env.get("WINEPREFIX"):
+                    del resolver
                     self.cloud_save_path_edit.setText("")
                     QMessageBox.warning(self, "Warning", "No wine prefix selected. Please set it in settings")
                     return
@@ -145,7 +146,7 @@ def wine_resolver_finished(self, path, app_name):
                         self,
                         self.tr("Error - {}").format(self.rgame.title),
                         self.tr(
-                            "Error while calculating path for <b>{}</b>. Insufficient permisions to create <b>{}</b>"
+                            "Error while calculating path for <b>{}</b>. Insufficient permissions to create <b>{}</b>"
                         ).format(self.rgame.title, path),
                     )
                     return
@@ -205,7 +206,10 @@ def __update_widget(self):
         self.cloud_ui.cloud_sync.blockSignals(True)
         self.cloud_ui.cloud_sync.setChecked(self.rgame.auto_sync_saves)
         self.cloud_ui.cloud_sync.blockSignals(False)
+
         self.cloud_save_path_edit.setText(self.rgame.save_path if self.rgame.save_path else "")
+        if platform.system() == "Windows" and not self.rgame.save_path:
+            self.compute_save_path()
 
     def update_game(self, rgame: RareGame):
         if self.rgame:
diff --git a/rare/components/tabs/games/game_info/game_info.py b/rare/components/tabs/games/game_info/game_info.py
index c1d0d204d..a67352355 100644
--- a/rare/components/tabs/games/game_info/game_info.py
+++ b/rare/components/tabs/games/game_info/game_info.py
@@ -90,6 +90,7 @@ def __init__(self, parent=None):
         }
 
         # lk: hide unfinished things
+        self.ui.tags_group.setVisible(False)
         self.ui.requirements_group.setVisible(False)
 
     @pyqtSlot()
diff --git a/rare/components/tabs/games/integrations/import_group.py b/rare/components/tabs/games/integrations/import_group.py
index e623862cb..55ba9e812 100644
--- a/rare/components/tabs/games/integrations/import_group.py
+++ b/rare/components/tabs/games/integrations/import_group.py
@@ -4,10 +4,10 @@
 from enum import IntEnum
 from logging import getLogger
 from pathlib import Path
-from typing import List, Tuple, Optional
+from typing import List, Tuple, Optional, Set
 
 from PyQt5.QtCore import Qt, QModelIndex, pyqtSignal, QRunnable, QObject, QThreadPool, pyqtSlot
-from PyQt5.QtGui import QStandardItemModel
+from PyQt5.QtGui import QStandardItemModel, QShowEvent
 from PyQt5.QtWidgets import (
     QFileDialog,
     QGroupBox,
@@ -18,12 +18,13 @@
     QStackedWidget,
     QProgressBar,
     QSizePolicy,
+    QFormLayout,
 )
 
 from rare.lgndr.cli import LegendaryCLI
 from rare.lgndr.core import LegendaryCore
 from rare.lgndr.glue.arguments import LgndrImportGameArgs
-from rare.lgndr.glue.monkeys import LgndrIndirectStatus
+from rare.lgndr.glue.monkeys import LgndrIndirectStatus, get_boolean_choice_factory
 from rare.shared import RareCore
 from rare.ui.components.tabs.games.integrations.import_group import Ui_ImportGroup
 from rare.widgets.elide_label import ElideLabel
@@ -78,6 +79,7 @@ def __init__(
             import_force: bool = False
     ):
         super(ImportWorker, self).__init__()
+        self.setAutoDelete(True)
         self.signals = ImportWorker.Signals()
         self.core = core
 
@@ -128,7 +130,7 @@ def __import_game(self, path: Path, app_name: str, app_title: str):
             skip_dlcs=not self.import_dlcs,
             with_dlcs=self.import_dlcs,
             indirect_status=status,
-            get_boolean_choice=lambda prompt, default=True: self.import_dlcs
+            get_boolean_choice=get_boolean_choice_factory(self.import_dlcs)
         )
         cli.import_game(args)
         return status.success, status.message
@@ -137,7 +139,7 @@ def __import_game(self, path: Path, app_name: str, app_title: str):
 class AppNameCompleter(QCompleter):
     activated = pyqtSignal(str)
 
-    def __init__(self, app_names: List, parent=None):
+    def __init__(self, app_names: Set[Tuple[str, str]], parent=None):
         super(AppNameCompleter, self).__init__(parent)
         # pylint: disable=E1136
         super(AppNameCompleter, self).activated[QModelIndex].connect(self.__activated_idx)
@@ -183,8 +185,11 @@ def __init__(self, parent=None):
         self.rcore = RareCore.instance()
         self.core = RareCore.instance().core()
 
-        self.app_name_list = [rgame.app_name for rgame in self.rcore.games]
-        self.install_dir_list = [rgame.folder_name for rgame in self.rcore.games if not rgame.is_dlc]
+        self.worker: Optional[ImportWorker] = None
+        self.threadpool = QThreadPool.globalInstance()
+
+        self.__app_names: Set[str] = set()
+        self.__install_dirs: Set[str] = set()
 
         self.path_edit = PathEdit(
             self.core.get_default_install_dir(),
@@ -193,23 +198,25 @@ def __init__(self, parent=None):
             parent=self,
         )
         self.path_edit.textChanged.connect(self.path_changed)
-        self.ui.path_edit_layout.addWidget(self.path_edit)
+        self.ui.import_layout.setWidget(
+            self.ui.import_layout.indexOf(self.ui.path_edit_label), QFormLayout.FieldRole, self.path_edit
+        )
 
         self.app_name_edit = IndicatorLineEdit(
             placeholder=self.tr("Use in case the app name was not found automatically"),
-            completer=AppNameCompleter(
-                app_names=[(rgame.app_name, rgame.app_title) for rgame in self.rcore.games],
-            ),
             edit_func=self.app_name_edit_callback,
             parent=self,
         )
         self.app_name_edit.textChanged.connect(self.app_name_changed)
-        self.ui.app_name_layout.addWidget(self.app_name_edit)
+        self.ui.import_layout.setWidget(
+            self.ui.import_layout.indexOf(self.ui.app_name_label), QFormLayout.FieldRole, self.app_name_edit
+        )
 
         self.ui.import_folder_check.stateChanged.connect(self.import_folder_changed)
         self.ui.import_dlcs_check.setEnabled(False)
         self.ui.import_dlcs_check.stateChanged.connect(self.import_dlcs_changed)
 
+        self.ui.import_button_label.setText("")
         self.ui.import_button.setEnabled(False)
         self.ui.import_button.clicked.connect(
             lambda: self.__import(self.path_edit.text())
@@ -224,9 +231,17 @@ def __init__(self, parent=None):
         self.info_progress = QProgressBar(self.button_info_stack)
         self.button_info_stack.addWidget(self.info_label)
         self.button_info_stack.addWidget(self.info_progress)
-        self.ui.button_info_layout.addWidget(self.button_info_stack)
-
-        self.threadpool = QThreadPool.globalInstance()
+        self.ui.button_info_layout.insertWidget(0, self.button_info_stack)
+
+    def showEvent(self, a0: QShowEvent) -> None:
+        if a0.spontaneous():
+            return super().showEvent(a0)
+        self.__app_names = {rgame.app_name for rgame in self.rcore.games}
+        self.__install_dirs = {rgame.folder_name for rgame in self.rcore.games if not rgame.is_dlc}
+        self.app_name_edit.setCompleter(
+            AppNameCompleter(app_names={(rgame.app_name, rgame.app_title) for rgame in self.rcore.games})
+        )
+        super().showEvent(a0)
 
     def set_game(self, app_name: str):
         if app_name:
@@ -238,7 +253,7 @@ def path_edit_callback(self, path) -> Tuple[bool, str, int]:
         if os.path.exists(path):
             if os.path.exists(os.path.join(path, ".egstore")):
                 return True, path, IndicatorReasonsCommon.VALID
-            elif os.path.basename(path) in self.install_dir_list:
+            elif os.path.basename(path) in self.__install_dirs:
                 return True, path, IndicatorReasonsCommon.VALID
         else:
             return False, path, IndicatorReasonsCommon.DIR_NOT_EXISTS
@@ -257,7 +272,7 @@ def path_changed(self, path: str):
     def app_name_edit_callback(self, text) -> Tuple[bool, str, int]:
         if not text:
             return False, text, IndicatorReasonsCommon.UNDEFINED
-        if text in self.app_name_list:
+        if text in self.__app_names:
             return True, text, IndicatorReasonsCommon.VALID
         else:
             return False, text, IndicatorReasonsCommon.NOT_INSTALLED
@@ -267,14 +282,12 @@ def app_name_changed(self, app_name: str):
         self.info_label.setText("")
         self.ui.import_dlcs_check.setCheckState(Qt.Unchecked)
         self.ui.import_force_check.setCheckState(Qt.Unchecked)
-        if self.app_name_edit.is_valid:
-            self.ui.import_dlcs_check.setEnabled(
-                bool(self.core.get_dlc_for_game(app_name))
-            )
-            self.ui.import_button.setEnabled(self.path_edit.is_valid)
-        else:
-            self.ui.import_dlcs_check.setEnabled(False)
-            self.ui.import_button.setEnabled(False)
+        self.ui.import_dlcs_check.setEnabled(
+            self.app_name_edit.is_valid and bool(self.core.get_dlc_for_game(app_name))
+        )
+        self.ui.import_button.setEnabled(
+            not bool(self.worker) and (self.app_name_edit.is_valid and self.path_edit.is_valid)
+        )
 
     @pyqtSlot(int)
     def import_folder_changed(self, state: Qt.CheckState):
@@ -285,17 +298,26 @@ def import_folder_changed(self, state: Qt.CheckState):
             state
             or (self.app_name_edit.is_valid and bool(self.core.get_dlc_for_game(self.app_name_edit.text())))
         )
-        self.ui.import_button.setEnabled(state or (not state and self.app_name_edit.is_valid))
+        self.ui.import_button.setEnabled(
+            not bool(self.worker) and (state or (not state and self.app_name_edit.is_valid))
+        )
 
     @pyqtSlot(int)
     def import_dlcs_changed(self, state: Qt.CheckState):
-        self.ui.import_button.setEnabled(self.ui.import_folder_check.isChecked() or self.app_name_edit.is_valid)
+        self.ui.import_button.setEnabled(
+            not bool(self.worker) and (self.ui.import_folder_check.isChecked() or self.app_name_edit.is_valid)
+        )
 
     @pyqtSlot(str)
     def __import(self, path: Optional[str] = None):
+        self.ui.import_button.setDisabled(True)
+        self.info_label.setText(self.tr("Status: Importing games"))
+        self.info_progress.setValue(0)
+        self.button_info_stack.setCurrentWidget(self.info_progress)
+
         if not path:
             path = self.path_edit.text()
-        worker = ImportWorker(
+        self.worker = ImportWorker(
             self.core,
             path,
             self.app_name_edit.text(),
@@ -303,12 +325,9 @@ def __import(self, path: Optional[str] = None):
             self.ui.import_dlcs_check.isChecked(),
             self.ui.import_force_check.isChecked()
         )
-        worker.signals.result.connect(self.__on_import_result)
-        worker.signals.progress.connect(self.__on_import_progress)
-        self.threadpool.start(worker)
-        self.button_info_stack.setCurrentWidget(self.info_progress)
-        self.info_label.setText(self.tr("Importing games"))
-        self.ui.import_button.setDisabled(True)
+        self.worker.signals.progress.connect(self.__on_import_progress)
+        self.worker.signals.result.connect(self.__on_import_result)
+        self.threadpool.start(self.worker)
 
     @pyqtSlot(ImportedGame, int)
     def __on_import_progress(self, imported: ImportedGame, progress: int):
@@ -322,6 +341,7 @@ def __on_import_progress(self, imported: ImportedGame, progress: int):
 
     @pyqtSlot(list)
     def __on_import_result(self, result: List[ImportedGame]):
+        self.worker = None
         self.button_info_stack.setCurrentWidget(self.info_label)
         if len(result) == 1:
             res = result[0]
@@ -338,7 +358,7 @@ def __on_import_result(self, result: List[ImportedGame]):
                     self.tr("Error: Could not find AppName for <b>{}</b>").format(res.path)
                 )
         else:
-            self.info_label.setText("")
+            self.info_label.setText(self.tr("Status: Finished importing games"))
             success = [r for r in result if r.result == ImportResult.SUCCESS]
             failure = [r for r in result if r.result == ImportResult.FAILED]
             errored = [r for r in result if r.result == ImportResult.ERROR]
diff --git a/rare/components/tabs/settings/about.py b/rare/components/tabs/settings/about.py
index 5e461e4f4..35c520efc 100644
--- a/rare/components/tabs/settings/about.py
+++ b/rare/components/tabs/settings/about.py
@@ -19,19 +19,20 @@ def versiontuple(v):
         return tuple((9, 9, 9))  # It is a beta version and newer
 
 
-class About(QWidget, Ui_About):
+class About(QWidget):
     update_available_ready = pyqtSignal()
 
     def __init__(self, parent=None):
         super(About, self).__init__(parent=parent)
-        self.setupUi(self)
+        self.ui = Ui_About()
+        self.ui.setupUi(self)
 
-        self.version.setText(f"{__version__}  {__codename__}")
+        self.ui.version.setText(f"{__version__}  {__codename__}")
 
-        self.update_label.setEnabled(False)
-        self.update_lbl.setEnabled(False)
-        self.open_browser.setVisible(False)
-        self.open_browser.setEnabled(False)
+        self.ui.update_label.setEnabled(False)
+        self.ui.update_lbl.setEnabled(False)
+        self.ui.open_browser.setVisible(False)
+        self.ui.open_browser.setEnabled(False)
 
         self.manager = QtRequestManager("json")
         self.manager.get(
@@ -39,7 +40,7 @@ def __init__(self, parent=None):
             self.update_available_finished,
         )
 
-        self.open_browser.clicked.connect(
+        self.ui.open_browser.clicked.connect(
             lambda: webbrowser.open("https://github.com/RareDevs/Rare/releases/latest")
         )
 
@@ -62,11 +63,11 @@ def update_available_finished(self, data: dict):
 
         if self.update_available:
             logger.info(f"Update available: {__version__} -> {latest_tag}")
-            self.update_lbl.setText("{} -> {}".format(__version__, latest_tag))
+            self.ui.update_lbl.setText("{} -> {}".format(__version__, latest_tag))
             self.update_available_ready.emit()
         else:
-            self.update_lbl.setText(self.tr("You have the latest version"))
-        self.update_label.setEnabled(self.update_available)
-        self.update_lbl.setEnabled(self.update_available)
-        self.open_browser.setVisible(self.update_available)
-        self.open_browser.setEnabled(self.update_available)
+            self.ui.update_lbl.setText(self.tr("You have the latest version"))
+        self.ui.update_label.setEnabled(self.update_available)
+        self.ui.update_lbl.setEnabled(self.update_available)
+        self.ui.open_browser.setVisible(self.update_available)
+        self.ui.open_browser.setEnabled(self.update_available)
diff --git a/rare/components/tabs/settings/debug.py b/rare/components/tabs/settings/debug.py
index 326b31283..8c6b04b22 100644
--- a/rare/components/tabs/settings/debug.py
+++ b/rare/components/tabs/settings/debug.py
@@ -1,20 +1,23 @@
 from PyQt5.QtWidgets import QWidget, QVBoxLayout, QPushButton
 
 from rare.shared import GlobalSignalsSingleton
+from rare.utils.misc import ExitCodes
 
 
 class DebugSettings(QWidget):
     def __init__(self, parent=None):
         super(DebugSettings, self).__init__(parent=parent)
-        layout = QVBoxLayout(self)
 
         self.raise_runtime_exception_button = QPushButton("Raise Exception")
-        layout.addWidget(self.raise_runtime_exception_button)
         self.raise_runtime_exception_button.clicked.connect(self.raise_exception)
         self.restart_button = QPushButton("Restart")
-        layout.addWidget(self.restart_button)
-        self.restart_button.clicked.connect(lambda: GlobalSignalsSingleton().application.quit.emit(-133742))
+        self.restart_button.clicked.connect(
+            lambda: GlobalSignalsSingleton().application.quit.emit(ExitCodes.LOGOUT)
+        )
 
+        layout = QVBoxLayout(self)
+        layout.addWidget(self.raise_runtime_exception_button)
+        layout.addWidget(self.restart_button)
         layout.addStretch(1)
 
     def raise_exception(self):
diff --git a/rare/game_launch_helper/__init__.py b/rare/launcher/__init__.py
similarity index 75%
rename from rare/game_launch_helper/__init__.py
rename to rare/launcher/__init__.py
index 7600b5929..07d4052d6 100644
--- a/rare/game_launch_helper/__init__.py
+++ b/rare/launcher/__init__.py
@@ -1,14 +1,12 @@
 import json
-import logging
 import platform
 import subprocess
-import sys
 import time
 import traceback
 from argparse import Namespace
 from logging import getLogger
 from signal import signal, SIGINT, SIGTERM, strsignal
-from typing import Union, Optional
+from typing import Optional
 
 from PyQt5.QtCore import QObject, QProcess, pyqtSignal, QUrl, QRunnable, QThreadPool, QSettings, Qt, pyqtSlot
 from PyQt5.QtGui import QDesktopServices
@@ -24,10 +22,9 @@
 from .console import Console
 from .lgd_helper import get_launch_args, InitArgs, get_configured_process, LaunchArgs, GameArgsError
 
-logger = logging.getLogger("RareLauncher")
-
 DETACHED_APP_NAMES = [
-    "0a2d9f6403244d12969e11da6713137b"
+    "0a2d9f6403244d12969e11da6713137b"  # Fall Guys
+    "Fortnite"
 ]
 
 
@@ -40,6 +37,7 @@ class Signals(QObject):
 
     def __init__(self, core: LegendaryCore, args: InitArgs, rgame: RareGameSlim, sync_action=None):
         super(PreLaunchThread, self).__init__()
+        self.logger = getLogger(type(self).__name__)
         self.core = core
         self.signals = self.Signals()
         self.args = args
@@ -47,36 +45,36 @@ def __init__(self, core: LegendaryCore, args: InitArgs, rgame: RareGameSlim, syn
         self.sync_action = sync_action
 
     def run(self) -> None:
-        logger.info(f"Sync action: {self.sync_action}")
+        self.logger.info(f"Sync action: {self.sync_action}")
         if self.sync_action == CloudSaveDialog.UPLOAD:
             self.rgame.upload_saves(False)
         elif self.sync_action == CloudSaveDialog.DOWNLOAD:
             self.rgame.download_saves(False)
         else:
-            logger.info("No sync action")
+            self.logger.info("No sync action")
 
         args = self.prepare_launch(self.args)
         if not args:
             return
         self.signals.ready_to_launch.emit(args)
 
-    def prepare_launch(self, args: InitArgs) -> Union[LaunchArgs, None]:
+    def prepare_launch(self, args: InitArgs) -> Optional[LaunchArgs]:
         try:
-            args = get_launch_args(self.core, args)
+            launch_args = get_launch_args(self.core, args)
         except Exception as e:
             self.signals.error_occurred.emit(str(e))
             return None
-        if not args:
+        if not launch_args:
             return None
 
-        if args.pre_launch_command:
+        if launch_args.pre_launch_command:
             proc = get_configured_process()
-            proc.setProcessEnvironment(args.env)
+            proc.setProcessEnvironment(launch_args.environment)
             self.signals.started_pre_launch_command.emit()
-            proc.start(args.pre_launch_command[0], args.pre_launch_command[1:])
-            if args.pre_launch_wait:
+            proc.start(launch_args.pre_launch_command[0], launch_args.pre_launch_command[1:])
+            if launch_args.pre_launch_wait:
                 proc.waitForFinished(-1)
-        return args
+        return launch_args
 
 
 class SyncCheckWorker(QRunnable):
@@ -121,57 +119,63 @@ class RareLauncher(RareApp):
     exit_app = pyqtSignal()
 
     def __init__(self, args: InitArgs):
-        log_file = f"Rare_Launcher_{args.app_name}" + "_{0}.log"
-        super(RareLauncher, self).__init__(args, log_file)
+        super(RareLauncher, self).__init__(args, f"{type(self).__name__}_{args.app_name}_{{0}}.log")
+        self.socket: Optional[QLocalSocket] = None
+        self.console: Optional[Console] = None
+        self.game_process: QProcess = QProcess(self)
+        self.server: QLocalServer = QLocalServer(self)
+
         self._hook.deleteLater()
         self._hook = RareLauncherException(self, args, self)
-        self.logger = getLogger(f"Launcher_{args.app_name}")
 
-        self.success: bool = True
+        self.success: bool = False
         self.no_sync_on_exit = False
         self.args = args
         self.core = LegendaryCore()
-        self.rgame = RareGameSlim(self.core, self.core.get_game(args.app_name))
+        game = self.core.get_game(args.app_name)
+        if not game:
+            self.logger.error(f"Game {args.app_name} not found. Exiting")
+            return
+        self.rgame = RareGameSlim(self.core, game)
 
         lang = self.settings.value("language", self.core.language_code, type=str)
         self.load_translator(lang)
 
-        self.console: Optional[Console] = None
         if QSettings().value("show_console", False, bool):
             self.console = Console()
             self.console.show()
 
-        self.game_process: QProcess = QProcess(self)
-        self.game_process.finished.connect(self.game_finished)
-        self.game_process.errorOccurred.connect(
-            lambda err: self.error_occurred(self.game_process.errorString()))
+        self.game_process.finished.connect(self.__process_finished)
+        self.game_process.errorOccurred.connect(self.__process_errored)
         if self.console:
-            self.game_process.readyReadStandardOutput.connect(
-                lambda: self.console.log(
-                    self.game_process.readAllStandardOutput().data().decode("utf-8", "ignore")
-                )
-            )
-            self.game_process.readyReadStandardError.connect(
-                lambda: self.console.error(
-                    self.game_process.readAllStandardError().data().decode("utf-8", "ignore")
-                )
-            )
+            self.game_process.readyReadStandardOutput.connect(self.__proc_log_stdout)
+            self.game_process.readyReadStandardError.connect(self.__proc_log_stderr)
             self.console.term.connect(self.__proc_term)
             self.console.kill.connect(self.__proc_kill)
 
-        self.socket: Optional[QLocalSocket] = None
-        self.server: QLocalServer = QLocalServer(self)
         ret = self.server.listen(f"rare_{args.app_name}")
         if not ret:
             self.logger.error(self.server.errorString())
             self.logger.info("Server is running")
             self.server.close()
-            self.success = False
             return
         self.server.newConnection.connect(self.new_server_connection)
 
+        self.success = True
         self.start_time = time.time()
 
+    @pyqtSlot()
+    def __proc_log_stdout(self):
+        self.console.log(
+            self.game_process.readAllStandardOutput().data().decode("utf-8", "ignore")
+        )
+
+    @pyqtSlot()
+    def __proc_log_stderr(self):
+        self.console.error(
+            self.game_process.readAllStandardError().data().decode("utf-8", "ignore")
+        )
+
     @pyqtSlot()
     def __proc_term(self):
         self.game_process.terminate()
@@ -215,16 +219,17 @@ def check_saves_finished(self, exit_code: int):
 
         if action == CloudSaveDialog.UPLOAD:
             if self.console:
-                self.console.log("upload saves...")
+                self.console.log("Uploading saves...")
             self.rgame.upload_saves()
         elif action == CloudSaveDialog.DOWNLOAD:
             if self.console:
-                self.console.log("Download saves...")
+                self.console.log("Downloading saves...")
             self.rgame.download_saves()
         else:
             self.on_exit(exit_code)
 
-    def game_finished(self, exit_code):
+    @pyqtSlot(int, QProcess.ExitStatus)
+    def __process_finished(self, exit_code: int, exit_status: QProcess.ExitStatus):
         self.logger.info("Game finished")
 
         if self.rgame.auto_sync_saves:
@@ -232,6 +237,10 @@ def game_finished(self, exit_code):
         else:
             self.on_exit(exit_code)
 
+    @pyqtSlot(QProcess.ProcessError)
+    def __process_errored(self, error: QProcess.ProcessError):
+        self.error_occurred(self.game_process.errorString())
+
     def on_exit(self, exit_code: int):
         if self.console:
             self.console.on_process_exit(self.core.get_game(self.rgame.app_name).app_title, exit_code)
@@ -247,13 +256,14 @@ def on_exit(self, exit_code: int):
         )
         self.stop()
 
+    @pyqtSlot(object)
     def launch_game(self, args: LaunchArgs):
         # should never happen
         if not args:
             self.stop()
             return
         if self.console:
-            self.console.set_env(args.env)
+            self.console.set_env(args.environment)
         self.start_time = time.time()
 
         if args.is_origin_game:
@@ -261,9 +271,9 @@ def launch_game(self, args: LaunchArgs):
             self.stop()  # stop because it is no subprocess
             return
 
-        if args.cwd:
-            self.game_process.setWorkingDirectory(args.cwd)
-        self.game_process.setProcessEnvironment(args.env)
+        if args.working_directory:
+            self.game_process.setWorkingDirectory(args.working_directory)
+        self.game_process.setProcessEnvironment(args.environment)
         # send start message after process started
         self.game_process.started.connect(lambda: self.send_message(
             StateChangedModel(
@@ -273,22 +283,22 @@ def launch_game(self, args: LaunchArgs):
         ))
         if self.rgame.app_name in DETACHED_APP_NAMES and platform.system() == "Windows":
             self.game_process.deleteLater()
-            subprocess.Popen([args.executable] + args.args, cwd=args.cwd,
-                             env={i: args.env.value(i) for i in args.env.keys()})
+            subprocess.Popen([args.executable] + args.arguments, cwd=args.working_directory,
+                             env={i: args.environment.value(i) for i in args.environment.keys()})
             if self.console:
-                self.console.log("Launching game detached")
+                self.console.log("Launching game as a detached process")
             self.stop()
             return
         if self.args.dry_run:
-            logger.info("Dry run activated")
+            self.logger.info("Dry run activated")
             if self.console:
-                self.console.log(f"{args.executable} {' '.join(args.args)}")
+                self.console.log(f"{args.executable} {' '.join(args.arguments)}")
                 self.console.log(f"Do not start {self.rgame.app_name}")
                 self.console.accept_close = True
-            print(args.executable, " ".join(args.args))
+            print(args.executable, " ".join(args.arguments))
             self.stop()
             return
-        self.game_process.start(args.executable, args.args)
+        self.game_process.start(args.executable, args.arguments)
 
     def error_occurred(self, error_str: str):
         self.logger.warning(error_str)
@@ -322,9 +332,9 @@ def sync_ready(self):
             self.no_sync_on_exit = True
         if self.console:
             if action == CloudSaveDialog.DOWNLOAD:
-                self.console.log("Downloading saves")
+                self.console.log("Downloading saves...")
             elif action == CloudSaveDialog.UPLOAD:
-                self.console.log("Uloading saves")
+                self.console.log("Uploading saves...")
         self.start_prepare(action)
 
     def start(self, args: InitArgs):
@@ -334,11 +344,11 @@ def start(self, args: InitArgs):
                     raise ValueError("You are not logged in")
             except ValueError:
                 # automatically launch offline if available
-                self.logger.error("Not logged in. Try to launch game offline")
+                self.logger.error("Not logged in. Trying to launch the game in offline mode")
                 args.offline = True
 
         if not args.offline and self.rgame.auto_sync_saves:
-            logger.info("Start sync worker")
+            self.logger.info("Start sync worker")
             worker = SyncCheckWorker(self.core, self.rgame)
             worker.signals.error_occurred.connect(self.error_occurred)
             worker.signals.sync_state_ready.connect(self.sync_ready)
@@ -352,10 +362,13 @@ def stop(self):
             if self.console:
                 self.game_process.readyReadStandardOutput.disconnect()
                 self.game_process.readyReadStandardError.disconnect()
-            self.game_process.finished.disconnect()
-            self.game_process.errorOccurred.disconnect()
+            if self.game_process.receivers(self.game_process.finished):
+                self.game_process.finished.disconnect()
+            if self.game_process.receivers(self.game_process.errorOccurred):
+                self.game_process.errorOccurred.disconnect()
         except TypeError as e:
-            logger.error(f"Failed to disconnect signals: {e}")
+            self.logger.error(f"Failed to disconnect process signals: {e}")
+
         self.logger.info("Stopping server")
         try:
             self.server.close()
@@ -369,7 +382,7 @@ def stop(self):
             self.console.on_process_exit(self.rgame.app_name, 0)
 
 
-def start_game(args: Namespace):
+def launch(args: Namespace) -> int:
     args = InitArgs.from_argparse(args)
 
     QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)
@@ -381,15 +394,17 @@ def start_game(args: Namespace):
     # This prevents ghost QLocalSockets, which block the name, which makes it unable to start
     # No handling for SIGKILL
     def sighandler(s, frame):
-        logger.info(f"{strsignal(s)} received. Stopping")
+        app.logger.info(f"{strsignal(s)} received. Stopping")
         app.stop()
         app.exit(1)
     signal(SIGINT, sighandler)
     signal(SIGTERM, sighandler)
 
     if not app.success:
-        return
+        app.stop()
+        app.exit(1)
+        return 1
     app.start(args)
     # app.exit_app.connect(lambda: app.exit(0))
 
-    sys.exit(app.exec_())
+    return app.exec_()
diff --git a/rare/game_launch_helper/console.py b/rare/launcher/console.py
similarity index 93%
rename from rare/game_launch_helper/console.py
rename to rare/launcher/console.py
index f61b02ec8..06412fb1b 100644
--- a/rare/game_launch_helper/console.py
+++ b/rare/launcher/console.py
@@ -48,15 +48,15 @@ def __init__(self, parent=None):
 
         button_layout.addItem(QSpacerItem(0, 0, QSizePolicy.Expanding, QSizePolicy.Fixed))
 
-        # self.terminate_button = QPushButton(self.tr("Terminate"))
+        self.terminate_button = QPushButton(self.tr("Terminate"))
         # self.terminate_button.setVisible(platform.system() == "Windows")
-        # button_layout.addWidget(self.terminate_button)
-        # self.terminate_button.clicked.connect(lambda: self.term.emit())
-        #
-        # self.kill_button = QPushButton(self.tr("Kill"))
+        button_layout.addWidget(self.terminate_button)
+        self.terminate_button.clicked.connect(lambda: self.term.emit())
+
+        self.kill_button = QPushButton(self.tr("Kill"))
         # self.kill_button.setVisible(platform.system() == "Windows")
-        # button_layout.addWidget(self.kill_button)
-        # self.kill_button.clicked.connect(lambda: self.kill.emit())
+        button_layout.addWidget(self.kill_button)
+        self.kill_button.clicked.connect(lambda: self.kill.emit())
 
         layout.addLayout(button_layout)
 
diff --git a/rare/game_launch_helper/lgd_helper.py b/rare/launcher/lgd_helper.py
similarity index 81%
rename from rare/game_launch_helper/lgd_helper.py
rename to rare/launcher/lgd_helper.py
index 9d0bdd539..38a79aa0f 100644
--- a/rare/game_launch_helper/lgd_helper.py
+++ b/rare/launcher/lgd_helper.py
@@ -44,9 +44,9 @@ def from_argparse(cls, args):
 @dataclass
 class LaunchArgs:
     executable: str = ""
-    args: List[str] = None
-    cwd: str = None
-    env: QProcessEnvironment = None
+    arguments: List[str] = None
+    working_directory: str = None
+    environment: QProcessEnvironment = None
     pre_launch_command: str = ""
     pre_launch_wait: bool = False
     is_origin_game: bool = False  # only for windows to launch as url
@@ -60,7 +60,7 @@ def get_origin_params(core: LegendaryCore, app_name, offline: bool,
     origin_uri = core.get_origin_uri(app_name, offline)
     if platform.system() == "Windows":
         launch_args.executable = origin_uri
-        launch_args.args = []
+        launch_args.arguments = []
         # only set it here true, because on linux it is a launch command like every other game
         launch_args.is_origin_game = True
         return launch_args
@@ -71,12 +71,20 @@ def get_origin_params(core: LegendaryCore, app_name, offline: bool,
     command.append(origin_uri)
 
     env = core.get_app_environment(app_name)
-    launch_args.env = QProcessEnvironment.systemEnvironment()
-    for name, value in env.items():
-        launch_args.env.insert(name, value)
+    launch_args.environment = QProcessEnvironment.systemEnvironment()
+
+    if os.environ.get("container") == "flatpak":
+        flatpak_command = ["flatpak-spawn", "--host"]
+        for name, value in env.items():
+            flatpak_command.append(f"--env={name}={value}")
+        command = flatpak_command + command
+    else:
+        for name, value in env.items():
+            launch_args.environment.insert(name, value)
 
     launch_args.executable = command[0]
-    launch_args.args = command[1:]
+    launch_args.arguments = command[1:]
+
     return launch_args
 
 
@@ -100,10 +108,16 @@ def get_game_params(core: LegendaryCore, igame: InstalledGame, args: InitArgs,
         app_name=igame.app_name, offline=args.offline
     )
 
-    full_params = list()
+    full_params = []
+    launch_args.environment = QProcessEnvironment.systemEnvironment()
 
     if os.environ.get("container") == "flatpak":
         full_params.extend(["flatpak-spawn", "--host"])
+        for name, value in params.environment.items():
+            full_params.append(f"--env={name}={value}")
+    else:
+        for name, value in params.environment.items():
+            launch_args.environment.insert(name, value)
 
     full_params.extend(params.launch_command)
     full_params.append(
@@ -114,12 +128,9 @@ def get_game_params(core: LegendaryCore, igame: InstalledGame, args: InitArgs,
     full_params.extend(params.user_parameters)
 
     launch_args.executable = full_params[0]
-    launch_args.args = full_params[1:]
+    launch_args.arguments = full_params[1:]
+    launch_args.working_directory = params.working_directory
 
-    launch_args.env = QProcessEnvironment.systemEnvironment()
-    for name, value in params.environment.items():
-        launch_args.env.insert(name, value)
-    launch_args.cwd = params.working_directory
     return launch_args
 
 
diff --git a/rare/lgndr/cli.py b/rare/lgndr/cli.py
index 5c63e293a..74ca2a8a8 100644
--- a/rare/lgndr/cli.py
+++ b/rare/lgndr/cli.py
@@ -43,6 +43,9 @@ def __del__(self):
     def unlock_installed(func):
         @functools.wraps(func)
         def unlock(self, *args, **kwargs):
+            if not self.core.lgd.lock_installed():
+                self.logger.debug("Data is locked, trying to forcufully release it")
+                # self.core.lgd._installed_lock.release(force=True)
             try:
                 ret = func(self, *args, **kwargs)
             except Exception as e:
@@ -388,7 +391,6 @@ def uninstall_game(self, args: LgndrUninstallGameArgs) -> None:
         # Override logger for the local context to use message as part of the indirect return value
         logger = LgndrIndirectLogger(args.indirect_status, self.logger, logging.WARNING)
         get_boolean_choice = args.get_boolean_choice_main
-        # def get_boolean_choice(x, default): return True
         if not self.core.lgd.lock_installed():
             logger.fatal('Failed to acquire installed data lock, only one instance of Legendary may '
                          'install/import/move applications at a time.')
@@ -429,7 +431,6 @@ def _handle_uninstaller(self, igame: InstalledGame, yes=False, args: LgndrUninst
         # Override logger for the local context to use message as part of the indirect return value
         logger = LgndrIndirectLogger(args.indirect_status, self.logger, logging.WARNING)
         get_boolean_choice = args.get_boolean_choice_handler
-        # def get_boolean_choice(x, default): return True
         # noinspection PyShadowingBuiltins
         def print(x): self.logger.info(x) if x else None
 
diff --git a/rare/lgndr/core.py b/rare/lgndr/core.py
index 9dbfddcc1..1b86207ef 100644
--- a/rare/lgndr/core.py
+++ b/rare/lgndr/core.py
@@ -34,6 +34,9 @@ def __init__(self, override_config=None, timeout=10.0):
     def unlock_installed(func):
         @functools.wraps(func)
         def unlock(self, *args, **kwargs):
+            if not self.lgd.lock_installed():
+                self.log.debug("Data is locked, trying to forcufully release it")
+                # self.lgd._installed_lock.release(force=True)
             try:
                 ret = func(self, *args, **kwargs)
             except Exception as e:
diff --git a/rare/lgndr/glue/arguments.py b/rare/lgndr/glue/arguments.py
index 36e1a46b1..35f32cad5 100644
--- a/rare/lgndr/glue/arguments.py
+++ b/rare/lgndr/glue/arguments.py
@@ -4,9 +4,10 @@
 
 from rare.lgndr.glue.monkeys import (
     LgndrIndirectStatus,
-    GetBooleanChoiceProtocol,
-    get_boolean_choice,
-    verify_stdout,
+    get_boolean_choice_factory,
+    sdl_prompt_factory,
+    verify_stdout_factory,
+    ui_update_factory,
     DLManagerSignals,
 )
 from rare.lgndr.models.downloading import UIUpdate
@@ -33,7 +34,7 @@ class LgndrImportGameArgs:
     yes: bool = False
     # Rare: Extra arguments
     indirect_status: LgndrIndirectStatus = field(default_factory=LgndrIndirectStatus)
-    get_boolean_choice: GetBooleanChoiceProtocol = get_boolean_choice
+    get_boolean_choice: Callable[[str, bool], bool] = get_boolean_choice_factory(True)
 
 
 @dataclass
@@ -44,8 +45,8 @@ class LgndrUninstallGameArgs:
     yes: bool = False
     # Rare: Extra arguments
     indirect_status: LgndrIndirectStatus = field(default_factory=LgndrIndirectStatus)
-    get_boolean_choice_main: GetBooleanChoiceProtocol = get_boolean_choice
-    get_boolean_choice_handler: GetBooleanChoiceProtocol = get_boolean_choice
+    get_boolean_choice_main: Callable[[str, bool], bool] = get_boolean_choice_factory(True)
+    get_boolean_choice_handler: Callable[[str, bool], bool] = get_boolean_choice_factory(True)
 
 
 @dataclass
@@ -53,7 +54,7 @@ class LgndrVerifyGameArgs:
     app_name: str
     # Rare: Extra arguments
     indirect_status: LgndrIndirectStatus = field(default_factory=LgndrIndirectStatus)
-    verify_stdout: Callable[[int, int, float, float], None] = verify_stdout
+    verify_stdout: Callable[[int, int, float, float], None] = verify_stdout_factory(None)
 
 
 @dataclass
@@ -94,14 +95,11 @@ class LgndrInstallGameArgs:
     yes: bool = True
     # Rare: Extra arguments
     indirect_status: LgndrIndirectStatus = field(default_factory=LgndrIndirectStatus)
-    get_boolean_choice: GetBooleanChoiceProtocol = get_boolean_choice
-    sdl_prompt: Callable[[str, str], List[str]] = lambda app_name, title: [""]
-    verify_stdout: Callable[[int, int, float, float], None] = verify_stdout
+    get_boolean_choice: Callable[[str, bool], bool] = get_boolean_choice_factory(True)
+    verify_stdout: Callable[[int, int, float, float], None] = verify_stdout_factory(None)
 
-    # def __post_init__(self):
-    #     if self.sdl_prompt is None:
-    #         self.sdl_prompt: Callable[[str, str], list] = \
-    #             lambda app_name, title: self.install_tag if self.install_tag is not None else [""]
+    def __post_init__(self):
+        self.sdl_prompt: Callable[[Dict, str], List[str]] = sdl_prompt_factory(self.install_tag)
 
 
 @dataclass
@@ -119,8 +117,8 @@ class LgndrInstallGameRealArgs:
     # Rare: Extra arguments
     install_prereqs: bool = False
     indirect_status: LgndrIndirectStatus = field(default_factory=LgndrIndirectStatus)
-    ui_update: Callable[[UIUpdate], None] = lambda ui: None
-    dlm_signals: DLManagerSignals = DLManagerSignals()
+    ui_update: Callable[[UIUpdate], None] = ui_update_factory(None)
+    dlm_signals: DLManagerSignals = field(default_factory=DLManagerSignals)
 
 
 @dataclass
diff --git a/rare/lgndr/glue/monkeys.py b/rare/lgndr/glue/monkeys.py
index b764b23d3..bf784631f 100644
--- a/rare/lgndr/glue/monkeys.py
+++ b/rare/lgndr/glue/monkeys.py
@@ -1,20 +1,48 @@
 import logging
 from dataclasses import dataclass
+from typing import Callable, List, Optional, Dict
 
-from typing_extensions import Protocol
+from rare.lgndr.models.downloading import UIUpdate
 
+logger = logging.getLogger("LgndrMonkeys")
 
-class GetBooleanChoiceProtocol(Protocol):
-    def __call__(self, prompt: str, default: bool = ...) -> bool:
-        ...
 
+def get_boolean_choice_factory(value: bool = True) -> Callable[[str, bool], bool]:
+    def get_boolean_choice(prompt: str, default: bool) -> bool:
+        logger.debug("get_boolean_choice: %s, default: %s, choice: %s", prompt, default, value)
+        return value
+    return get_boolean_choice
 
-def get_boolean_choice(prompt: str, default: bool = True) -> bool:
-    return default
 
+def sdl_prompt_factory(install_tag: Optional[List[str]] = None) -> Callable[[Dict, str], List[str]]:
+    def sdl_prompt(sdl_data: Dict, title: str) -> List[str]:
+        logger.debug("sdl_prompt: %s", title)
+        for key in sdl_data.keys():
+            logger.debug("%s: %s %s", key, sdl_data[key]["tags"], sdl_data[key]["name"])
+        tags = install_tag if install_tag is not None else [""]
+        logger.debug("choice: %s, tags: %s", install_tag, tags)
+        return tags
+    return sdl_prompt
 
-def verify_stdout(a0: int, a1: int, a2: float, a3: float) -> None:
-    print(f"Verification progress: {a0}/{a1} ({a2:.01f}%) [{a3:.1f} MiB/s]\t\r")
+
+def verify_stdout_factory(
+    callback: Callable[[int, int, float, float], None] = None
+) -> Callable[[int, int, float, float], None]:
+    def verify_stdout(a0: int, a1: int, a2: float, a3: float) -> None:
+        if callback is not None and callable(callback):
+            callback(a0, a1, a2, a3)
+        else:
+            logger.info("Verification progress: %d/%d (%.01f%%) [%.1f MiB/s]", a0, a1, a2, a3)
+    return verify_stdout
+
+
+def ui_update_factory(callback: Callable[[UIUpdate], None] = None) -> Callable[[UIUpdate], None]:
+    def ui_update(status: UIUpdate) -> None:
+        if callback is not None and callable(callback):
+            callback(status)
+        else:
+            logger.info("Installation progress: %s", status)
+    return ui_update
 
 
 class DLManagerSignals:
diff --git a/rare/main.py b/rare/main.py
new file mode 100755
index 000000000..54c2d661e
--- /dev/null
+++ b/rare/main.py
@@ -0,0 +1,133 @@
+import multiprocessing
+import os
+import pathlib
+import sys
+from argparse import ArgumentParser
+
+
+def main() -> int:
+    # If we are on Windows, and we are in a "compiled" GUI application form
+    # stdout (and stderr?) will be None. So to avoid `'NoneType' object has no attribute 'write'`
+    # errors, redirect both of them to devnull
+    if os.name == "nt" and (getattr(sys, "frozen", False) or ("__compiled__" in globals())):
+        # Check if stdout and stderr are None before redirecting
+        # This is useful in the case of test builds that enable console
+        if sys.stdout is None:
+            sys.stdout = open(os.devnull, 'w')
+        if sys.stderr is None:
+            sys.stderr = open(os.devnull, 'w')
+
+    os.environ["QT_QPA_PLATFORMTHEME"] = ""
+
+    # fix cx_freeze
+    multiprocessing.freeze_support()
+
+    # insert legendary for installed via pip/setup.py submodule to path
+    # if not __name__ == "__main__":
+    #     sys.path.insert(0, os.path.join(os.path.dirname(__file__), "legendary"))
+
+    # CLI Options
+    parser = ArgumentParser()
+    parser.add_argument(
+        "-V", "--version", action="store_true", help="Shows version and exits"
+    )
+    parser.add_argument(
+        "-S",
+        "--silent",
+        action="store_true",
+        help="Launch Rare in background. Open it from System Tray Icon",
+    )
+    parser.add_argument("--debug", action="store_true", help="Launch in debug mode")
+    parser.add_argument(
+        "--offline", action="store_true", help="Launch Rare in offline mode"
+    )
+    parser.add_argument(
+        "--test-start", action="store_true", help="Quit immediately after launch"
+    )
+
+    parser.add_argument(
+        "--desktop-shortcut",
+        action="store_true",
+        dest="desktop_shortcut",
+        help="Use this, if there is no link on desktop to start Rare",
+    )
+    parser.add_argument(
+        "--startmenu-shortcut",
+        action="store_true",
+        dest="startmenu_shortcut",
+        help="Use this, if there is no link in start menu to launch Rare",
+    )
+    subparsers = parser.add_subparsers(title="Commands", dest="subparser")
+
+    launch_minimal_parser = subparsers.add_parser("start", aliases=["launch"])
+    launch_minimal_parser.add_argument("app_name", help="AppName of the game to launch",
+                                       metavar="<App Name>", action="store")
+    launch_minimal_parser.add_argument("--dry-run", help="Print arguments and exit", action="store_true")
+    launch_minimal_parser.add_argument("--offline", help="Launch game offline",
+                                       action="store_true")
+    launch_minimal_parser.add_argument('--wine-bin', dest='wine_bin', action='store', metavar='<wine binary>',
+                                       default=os.environ.get('LGDRY_WINE_BINARY', None),
+                                       help='Set WINE binary to use to launch the app')
+    launch_minimal_parser.add_argument('--wine-prefix', dest='wine_pfx', action='store', metavar='<wine pfx path>',
+                                       default=os.environ.get('LGDRY_WINE_PREFIX', None),
+                                       help='Set WINE prefix to use')
+    launch_minimal_parser.add_argument("--ask-sync-saves", help="Ask to sync cloud saves",
+                                       action="store_true")
+    launch_minimal_parser.add_argument("--skip-update-check", help="Do not check for updates",
+                                       action="store_true")
+
+    args = parser.parse_args()
+
+    if args.desktop_shortcut or args.startmenu_shortcut:
+        from rare.utils.paths import create_desktop_link
+
+        if args.desktop_shortcut:
+            create_desktop_link(app_name="rare_shortcut", link_type="desktop")
+
+        if args.startmenu_shortcut:
+            create_desktop_link(app_name="rare_shortcut", link_type="start_menu")
+
+        print("Link created")
+        return 0
+
+    if args.version:
+        from rare import __version__, __codename__
+        print(f"Rare {__version__} Codename: {__codename__}")
+        return 0
+
+    if args.subparser == "start" or args.subparser == "launch":
+        from rare.launcher import launch
+        return launch(args)
+
+    from rare.utils import singleton
+
+    try:
+        # this object only allows one instance per machine
+
+        me = singleton.SingleInstance()
+    except singleton.SingleInstanceException:
+        print("Rare is already running")
+        from rare.utils.paths import lock_file
+
+        with open(lock_file(), "w") as file:
+            file.write("show")
+            file.close()
+        return -1
+
+    from rare.components import start
+    return start(args)
+
+
+if __name__ == "__main__":
+    # run from source
+    # insert raw legendary submodule
+    # sys.path.insert(
+    #     0, os.path.join(pathlib.Path(__file__).parent.absolute(), "legendary")
+    # )
+
+    # insert source directory if running `main.py` as python script
+    # Required by AppImage
+    if "__compiled__" not in globals():
+        sys.path.insert(0, str(pathlib.Path(__file__).parents[1].absolute()))
+
+    sys.exit(main())
diff --git a/rare/models/game.py b/rare/models/game.py
index ea7028106..3d54eeefb 100644
--- a/rare/models/game.py
+++ b/rare/models/game.py
@@ -550,7 +550,7 @@ def launch(
 
         cmd_line = get_rare_executable()
         executable, args = cmd_line[0], cmd_line[1:]
-        args.extend(["start", self.app_name])
+        args.extend(["launch", self.app_name])
         if offline:
             args.append("--offline")
         if skip_update_check:
diff --git a/rare/models/install.py b/rare/models/install.py
index aa0bcbe7e..efd076f95 100644
--- a/rare/models/install.py
+++ b/rare/models/install.py
@@ -1,8 +1,7 @@
 import os
-import platform as pf
 from dataclasses import dataclass
 from datetime import datetime, timedelta
-from typing import List, Optional, Callable, Dict, Tuple
+from typing import List, Optional, Dict, Tuple
 
 from legendary.models.downloading import AnalysisResult, ConditionCheckResult
 from legendary.models.game import Game, InstalledGame
@@ -33,11 +32,7 @@ class InstallOptionsModel:
     overlay: bool = False
     update: bool = False
     silent: bool = False
-    install_prereqs: bool = pf.system() == "Windows"
-
-    def __post_init__(self):
-        self.sdl_prompt: Callable[[str, str], list] = \
-            lambda app_name, title: self.install_tag if self.install_tag is not None else [""]
+    install_prereqs: bool = False
 
     def as_install_kwargs(self) -> Dict:
         return {
diff --git a/rare/shared/rare_core.py b/rare/shared/rare_core.py
index 46768cb0a..abc682476 100644
--- a/rare/shared/rare_core.py
+++ b/rare/shared/rare_core.py
@@ -176,8 +176,8 @@ def deleteLater(self) -> None:
         del self.__args
         self.__args = None
 
+        del self.__eos_overlay
         RareCore.__instance = None
-
         super(RareCore, self).deleteLater()
 
     def __validate_install(self, rgame: RareGame):
diff --git a/rare/shared/workers/verify.py b/rare/shared/workers/verify.py
index 72362e454..2387bdd3c 100644
--- a/rare/shared/workers/verify.py
+++ b/rare/shared/workers/verify.py
@@ -7,7 +7,7 @@
 from rare.lgndr.cli import LegendaryCLI
 from rare.lgndr.core import LegendaryCore
 from rare.lgndr.glue.arguments import LgndrVerifyGameArgs
-from rare.lgndr.glue.monkeys import LgndrIndirectStatus
+from rare.lgndr.glue.monkeys import LgndrIndirectStatus, verify_stdout_factory
 from rare.models.game import RareGame
 from .worker import QueueWorker, QueueWorkerInfo
 
@@ -30,7 +30,7 @@ def __init__(self, core: LegendaryCore, args: Namespace, rgame: RareGame):
         self.args = args
         self.rgame = rgame
 
-    def status_callback(self, num: int, total: int, percentage: float, speed: float):
+    def __status_callback(self, num: int, total: int, percentage: float, speed: float):
         self.rgame.signals.progress.update.emit(num * 100 // total)
         self.signals.progress.emit(self.rgame, num, total, percentage, speed)
 
@@ -45,7 +45,9 @@ def run_real(self):
         cli = LegendaryCLI(self.core)
         status = LgndrIndirectStatus()
         args = LgndrVerifyGameArgs(
-            app_name=self.rgame.app_name, indirect_status=status, verify_stdout=self.status_callback
+            app_name=self.rgame.app_name,
+            indirect_status=status,
+            verify_stdout=verify_stdout_factory(self.__status_callback)
         )
 
         # lk: first pass, verify with the current manifest
diff --git a/rare/shared/workers/wine_resolver.py b/rare/shared/workers/wine_resolver.py
index b57813a9d..563e78b3a 100644
--- a/rare/shared/workers/wine_resolver.py
+++ b/rare/shared/workers/wine_resolver.py
@@ -38,7 +38,7 @@ def run_real(self):
             # pylint: disable=E1136
             self.signals.result_ready[str].emit("")
             return
-        if not os.path.exists(self.wine_exec) or not os.path.exists(wine.winepath(self.wine_exec)):
+        if not os.path.exists(self.wine_exec):
             # pylint: disable=E1136
             self.signals.result_ready[str].emit("")
             return
@@ -82,28 +82,39 @@ def run(self) -> None:
                 wine_env = wine.environ(self.core, rgame.app_name)
                 wine_exec = wine.wine(self.core, rgame.app_name)
 
-                # lk: this is the original way of gettijng the path by parsing "system.reg"
-                wine_prefix = wine.prefix(self.core, rgame.app_name)
-                reg = self.__cache.get(wine_prefix, None) or wine.read_registry("system.reg", wine_prefix)
-                self.__cache[wine_prefix] = reg
+                use_wine = False
+                if not use_wine:
+                    # lk: this is the original way of getting the path by parsing "system.reg"
+                    wine_prefix = wine.prefix(self.core, rgame.app_name)
+                    reg = self.__cache.get(wine_prefix, None) or wine.read_registry("system.reg", wine_prefix)
+                    self.__cache[wine_prefix] = reg
 
-                reg_path = reg_path.replace("SOFTWARE", "Software").replace("WOW6432Node", "Wow6432Node")
-                # lk: split and rejoin the registry path to avoid slash expansion
-                reg_path = "\\\\".join([x for x in reg_path.split("\\") if bool(x)])
+                    reg_path = reg_path.replace("SOFTWARE", "Software").replace("WOW6432Node", "Wow6432Node")
+                    # lk: split and rejoin the registry path to avoid slash expansion
+                    reg_path = "\\\\".join([x for x in reg_path.split("\\") if bool(x)])
 
-                install_dir = reg.get(reg_path, f'"{reg_key}"', fallback=None)
-
-                # lk: this is the alternative way of getting the path by using wine itself
-                # install_dir = wine.query_reg_key(wine_exec, wine_env, f"HKLM\\{reg_path}", reg_key)
+                    install_dir = reg.get(reg_path, f'"{reg_key}"', fallback=None)
+                else:
+                    # lk: this is the alternative way of getting the path by using wine itself
+                    install_dir = wine.query_reg_key(wine_exec, wine_env, f"HKLM\\{reg_path}", reg_key)
 
                 if install_dir:
+                    logger.debug("Found Wine install directory %s", install_dir)
                     install_dir = wine.convert_to_unix_path(wine_exec, wine_env, install_dir)
+                    if install_dir:
+                        logger.debug("Found Unix install directory %s", install_dir)
+                    else:
+                        logger.info("Could not find Unix install directory for %s", rgame.title)
+                else:
+                    logger.info("Could not find Wine install directory for %s", rgame.title)
 
             if install_dir:
                 if os.path.isdir(install_dir):
                     install_size = path_size(install_dir)
                     rgame.set_origin_attributes(install_dir, install_size)
-                    logger.debug(f"Found Origin game {rgame.title} ({install_dir}, {format_size(install_size)})")
+                    logger.info("Origin game %s (%s, %s)", rgame.title, install_dir, format_size(install_size))
                 else:
-                    logger.warning(f"Found Origin game {rgame.title} ({install_dir} does not exist)")
-        logger.info(f"Origin registry worker finished in {time.time() - t}s")
+                    logger.warning("Origin game %s (%s does not exist)", rgame.title, install_dir)
+            else:
+                logger.info("Origin game %s is not installed", rgame.title)
+        logger.info("Origin worker finished in %ss", time.time() - t)
diff --git a/rare/ui/components/tabs/games/game_info/game_info.py b/rare/ui/components/tabs/games/game_info/game_info.py
index 58cbd5114..983b97b43 100644
--- a/rare/ui/components/tabs/games/game_info/game_info.py
+++ b/rare/ui/components/tabs/games/game_info/game_info.py
@@ -14,11 +14,48 @@
 class Ui_GameInfo(object):
     def setupUi(self, GameInfo):
         GameInfo.setObjectName("GameInfo")
-        GameInfo.resize(419, 404)
+        GameInfo.resize(600, 404)
         self.main_layout = QtWidgets.QHBoxLayout(GameInfo)
         self.main_layout.setObjectName("main_layout")
         self.left_layout = QtWidgets.QVBoxLayout()
         self.left_layout.setObjectName("left_layout")
+        self.tags_group = QtWidgets.QGroupBox(GameInfo)
+        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
+        sizePolicy.setHorizontalStretch(0)
+        sizePolicy.setVerticalStretch(0)
+        sizePolicy.setHeightForWidth(self.tags_group.sizePolicy().hasHeightForWidth())
+        self.tags_group.setSizePolicy(sizePolicy)
+        self.tags_group.setObjectName("tags_group")
+        self.tags_layout = QtWidgets.QGridLayout(self.tags_group)
+        self.tags_layout.setHorizontalSpacing(0)
+        self.tags_layout.setObjectName("tags_layout")
+        self.completed_check = QtWidgets.QCheckBox(self.tags_group)
+        self.completed_check.setObjectName("completed_check")
+        self.tags_layout.addWidget(self.completed_check, 3, 0, 1, 2)
+        self.hidden_check = QtWidgets.QCheckBox(self.tags_group)
+        self.hidden_check.setObjectName("hidden_check")
+        self.tags_layout.addWidget(self.hidden_check, 0, 0, 1, 2)
+        self.custom1_edit = QtWidgets.QLineEdit(self.tags_group)
+        self.custom1_edit.setObjectName("custom1_edit")
+        self.tags_layout.addWidget(self.custom1_edit, 4, 1, 1, 1)
+        self.favorites_check = QtWidgets.QCheckBox(self.tags_group)
+        self.favorites_check.setObjectName("favorites_check")
+        self.tags_layout.addWidget(self.favorites_check, 1, 0, 1, 2)
+        self.custom1_check = QtWidgets.QCheckBox(self.tags_group)
+        self.custom1_check.setText("")
+        self.custom1_check.setObjectName("custom1_check")
+        self.tags_layout.addWidget(self.custom1_check, 4, 0, 1, 1)
+        self.backlog_check = QtWidgets.QCheckBox(self.tags_group)
+        self.backlog_check.setObjectName("backlog_check")
+        self.tags_layout.addWidget(self.backlog_check, 2, 0, 1, 2)
+        self.custom2_check = QtWidgets.QCheckBox(self.tags_group)
+        self.custom2_check.setText("")
+        self.custom2_check.setObjectName("custom2_check")
+        self.tags_layout.addWidget(self.custom2_check, 5, 0, 1, 1)
+        self.custom2_edit = QtWidgets.QLineEdit(self.tags_group)
+        self.custom2_edit.setObjectName("custom2_edit")
+        self.tags_layout.addWidget(self.custom2_edit, 5, 1, 1, 1)
+        self.left_layout.addWidget(self.tags_group)
         self.main_layout.addLayout(self.left_layout)
         self.right_layout = QtWidgets.QVBoxLayout()
         self.right_layout.setObjectName("right_layout")
@@ -286,6 +323,11 @@ def setupUi(self, GameInfo):
     def retranslateUi(self, GameInfo):
         _translate = QtCore.QCoreApplication.translate
         GameInfo.setWindowTitle(_translate("GameInfo", "Game Info"))
+        self.tags_group.setTitle(_translate("GameInfo", "Tags"))
+        self.completed_check.setText(_translate("GameInfo", "Completed"))
+        self.hidden_check.setText(_translate("GameInfo", "Hidden"))
+        self.favorites_check.setText(_translate("GameInfo", "Favorites"))
+        self.backlog_check.setText(_translate("GameInfo", "Backlog"))
         self.lbl_dev.setText(_translate("GameInfo", "Developer"))
         self.lbl_app_name.setText(_translate("GameInfo", "Application Name"))
         self.lbl_version.setText(_translate("GameInfo", "Version"))
diff --git a/rare/ui/components/tabs/games/game_info/game_info.ui b/rare/ui/components/tabs/games/game_info/game_info.ui
index 1c1c50aa2..239218888 100644
--- a/rare/ui/components/tabs/games/game_info/game_info.ui
+++ b/rare/ui/components/tabs/games/game_info/game_info.ui
@@ -6,7 +6,7 @@
    <rect>
     <x>0</x>
     <y>0</y>
-    <width>419</width>
+    <width>600</width>
     <height>404</height>
    </rect>
   </property>
@@ -15,7 +15,74 @@
   </property>
   <layout class="QHBoxLayout" name="main_layout" stretch="0,1">
    <item>
-    <layout class="QVBoxLayout" name="left_layout"/>
+    <layout class="QVBoxLayout" name="left_layout">
+     <item>
+      <widget class="QGroupBox" name="tags_group">
+       <property name="sizePolicy">
+        <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+         <horstretch>0</horstretch>
+         <verstretch>0</verstretch>
+        </sizepolicy>
+       </property>
+       <property name="title">
+        <string>Tags</string>
+       </property>
+       <layout class="QGridLayout" name="tags_layout">
+        <property name="horizontalSpacing">
+         <number>0</number>
+        </property>
+        <item row="3" column="0" colspan="2">
+         <widget class="QCheckBox" name="completed_check">
+          <property name="text">
+           <string>Completed</string>
+          </property>
+         </widget>
+        </item>
+        <item row="0" column="0" colspan="2">
+         <widget class="QCheckBox" name="hidden_check">
+          <property name="text">
+           <string>Hidden</string>
+          </property>
+         </widget>
+        </item>
+        <item row="4" column="1">
+         <widget class="QLineEdit" name="custom1_edit"/>
+        </item>
+        <item row="1" column="0" colspan="2">
+         <widget class="QCheckBox" name="favorites_check">
+          <property name="text">
+           <string>Favorites</string>
+          </property>
+         </widget>
+        </item>
+        <item row="4" column="0">
+         <widget class="QCheckBox" name="custom1_check">
+          <property name="text">
+           <string notr="true"/>
+          </property>
+         </widget>
+        </item>
+        <item row="2" column="0" colspan="2">
+         <widget class="QCheckBox" name="backlog_check">
+          <property name="text">
+           <string>Backlog</string>
+          </property>
+         </widget>
+        </item>
+        <item row="5" column="0">
+         <widget class="QCheckBox" name="custom2_check">
+          <property name="text">
+           <string notr="true"/>
+          </property>
+         </widget>
+        </item>
+        <item row="5" column="1">
+         <widget class="QLineEdit" name="custom2_edit"/>
+        </item>
+       </layout>
+      </widget>
+     </item>
+    </layout>
    </item>
    <item>
     <layout class="QVBoxLayout" name="right_layout">
diff --git a/rare/ui/components/tabs/games/integrations/import_group.py b/rare/ui/components/tabs/games/integrations/import_group.py
index 3f7796db1..cc28fda19 100644
--- a/rare/ui/components/tabs/games/integrations/import_group.py
+++ b/rare/ui/components/tabs/games/integrations/import_group.py
@@ -2,7 +2,7 @@
 
 # Form implementation generated from reading ui file 'rare/ui/components/tabs/games/integrations/import_group.ui'
 #
-# Created by: PyQt5 UI code generator 5.15.9
+# Created by: PyQt5 UI code generator 5.15.10
 #
 # WARNING: Any manual changes made to this file will be lost when pyuic5 is
 # run again.  Do not edit this file unless you know what you are doing.
@@ -14,7 +14,7 @@
 class Ui_ImportGroup(object):
     def setupUi(self, ImportGroup):
         ImportGroup.setObjectName("ImportGroup")
-        ImportGroup.resize(501, 184)
+        ImportGroup.resize(506, 184)
         ImportGroup.setWindowTitle("ImportGroup")
         ImportGroup.setWindowFilePath("")
         self.import_layout = QtWidgets.QFormLayout(ImportGroup)
@@ -23,15 +23,9 @@ def setupUi(self, ImportGroup):
         self.path_edit_label = QtWidgets.QLabel(ImportGroup)
         self.path_edit_label.setObjectName("path_edit_label")
         self.import_layout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.path_edit_label)
-        self.path_edit_layout = QtWidgets.QHBoxLayout()
-        self.path_edit_layout.setObjectName("path_edit_layout")
-        self.import_layout.setLayout(0, QtWidgets.QFormLayout.FieldRole, self.path_edit_layout)
         self.app_name_label = QtWidgets.QLabel(ImportGroup)
         self.app_name_label.setObjectName("app_name_label")
         self.import_layout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.app_name_label)
-        self.app_name_layout = QtWidgets.QHBoxLayout()
-        self.app_name_layout.setObjectName("app_name_layout")
-        self.import_layout.setLayout(1, QtWidgets.QFormLayout.FieldRole, self.app_name_layout)
         self.import_folder_label = QtWidgets.QLabel(ImportGroup)
         self.import_folder_label.setObjectName("import_folder_label")
         self.import_layout.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.import_folder_label)
@@ -51,7 +45,7 @@ def setupUi(self, ImportGroup):
         self.import_dlcs_check.setObjectName("import_dlcs_check")
         self.import_layout.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.import_dlcs_check)
         self.import_button_label = QtWidgets.QLabel(ImportGroup)
-        self.import_button_label.setText("")
+        self.import_button_label.setText("Error")
         self.import_button_label.setObjectName("import_button_label")
         self.import_layout.setWidget(5, QtWidgets.QFormLayout.LabelRole, self.import_button_label)
         self.button_info_layout = QtWidgets.QHBoxLayout()
diff --git a/rare/ui/components/tabs/games/integrations/import_group.ui b/rare/ui/components/tabs/games/integrations/import_group.ui
index c8499e03c..43ed7e5e6 100644
--- a/rare/ui/components/tabs/games/integrations/import_group.ui
+++ b/rare/ui/components/tabs/games/integrations/import_group.ui
@@ -6,7 +6,7 @@
    <rect>
     <x>0</x>
     <y>0</y>
-    <width>501</width>
+    <width>506</width>
     <height>184</height>
    </rect>
   </property>
@@ -30,9 +30,6 @@
      </property>
     </widget>
    </item>
-   <item row="0" column="1">
-    <layout class="QHBoxLayout" name="path_edit_layout"/>
-   </item>
    <item row="1" column="0">
     <widget class="QLabel" name="app_name_label">
      <property name="text">
@@ -40,9 +37,6 @@
      </property>
     </widget>
    </item>
-   <item row="1" column="1">
-    <layout class="QHBoxLayout" name="app_name_layout"/>
-   </item>
    <item row="2" column="0">
     <widget class="QLabel" name="import_folder_label">
      <property name="text">
@@ -84,7 +78,7 @@
    <item row="5" column="0">
     <widget class="QLabel" name="import_button_label">
      <property name="text">
-      <string notr="true"/>
+      <string notr="true">Error</string>
      </property>
     </widget>
    </item>
diff --git a/rare/utils/misc.py b/rare/utils/misc.py
index 171e189eb..3d0267b42 100644
--- a/rare/utils/misc.py
+++ b/rare/utils/misc.py
@@ -1,13 +1,12 @@
 import os
+from enum import IntEnum
 from logging import getLogger
-from typing import List, Union, Type
+from typing import List, Union, Type, Dict
 
 import qtawesome
 import requests
 from PyQt5.QtCore import (
-    pyqtSignal,
     QObject,
-    QRunnable,
     QSettings,
     QFile,
     QDir,
@@ -16,15 +15,18 @@
 from PyQt5.QtGui import QPalette, QColor, QFontMetrics
 from PyQt5.QtWidgets import qApp, QStyleFactory, QLabel
 from PyQt5.sip import wrappertype
-from legendary.core import LegendaryCore
-from requests.exceptions import HTTPError
 
 from rare.utils.paths import resources_path
 
 logger = getLogger("Utils")
-settings = QSettings("Rare", "Rare")
 
-color_role_map = {
+
+class ExitCodes(IntEnum):
+    EXIT = 0
+    LOGOUT = -133742
+
+
+color_role_map: Dict[int, str] = {
     0: "WindowText",
     1: "Button",
     2: "Light",
@@ -49,7 +51,7 @@
     # 21: "NColorRoles",
 }
 
-color_group_map = {
+color_group_map: Dict[int, str] = {
     0: "Active",
     1: "Disabled",
     2: "Inactive",
@@ -148,7 +150,8 @@ def get_translations():
 def get_latest_version():
     try:
         resp = requests.get(
-            "https://api.github.com/repos/RareDevs/Rare/releases/latest", timeout=2,
+            "https://api.github.com/repos/RareDevs/Rare/releases/latest",
+            timeout=2,
         )
         tag = resp.json()["tag_name"]
         return tag
@@ -160,7 +163,8 @@ def path_size(path: Union[str, os.PathLike]) -> int:
     return sum(
         os.stat(os.path.join(dp, f)).st_size
         for dp, dn, filenames in os.walk(path)
-        for f in filenames if os.path.isfile(os.path.join(dp,f ))
+        for f in filenames
+        if os.path.isfile(os.path.join(dp, f))
     )
 
 
@@ -171,40 +175,6 @@ def format_size(b: Union[int, float]) -> str:
         b /= 1024
 
 
-class CloudWorker(QRunnable):
-    class Signals(QObject):
-        # List[SaveGameFile]
-        result_ready = pyqtSignal(dict)
-
-    def __init__(self, core: LegendaryCore):
-        super(CloudWorker, self).__init__()
-        self.core = core
-        self.signals = CloudWorker.Signals()
-        self.setAutoDelete(True)
-
-    def run(self) -> None:
-        try:
-            saves = self.core.get_save_games()
-        except HTTPError:
-            self.signals.result_ready.emit(None)
-            return
-
-        save_games = set()
-        for igame in self.core.get_installed_list():
-            game = self.core.get_game(igame.app_name)
-            if self.core.is_installed(igame.app_name) and game.supports_cloud_saves:
-                save_games.add(igame.app_name)
-
-        latest_saves = dict()
-        for s in sorted(saves, key=lambda a: a.datetime):
-            if s.app_name in save_games:
-                if not latest_saves.get(s.app_name):
-                    latest_saves[s.app_name] = []
-                latest_saves[s.app_name].append(s)
-
-        self.signals.result_ready.emit(latest_saves)
-
-
 def icon(icn_str: str, fallback: str = None, **kwargs):
     try:
         return qtawesome.icon(icn_str, **kwargs)
@@ -221,7 +191,7 @@ def icon(icn_str: str, fallback: str = None, **kwargs):
     return qtawesome.icon("ei.error", **kwargs)
 
 
-def widget_object_name(widget: Union[QObject,wrappertype,Type], suffix: str) -> str:
+def widget_object_name(widget: Union[QObject, wrappertype, Type], suffix: str) -> str:
     suffix = f"_{suffix}" if suffix else ""
     if isinstance(widget, QObject):
         return f"{type(widget).__name__}{suffix}"
diff --git a/rare/utils/paths.py b/rare/utils/paths.py
index 5685e92fe..1af6a67f4 100644
--- a/rare/utils/paths.py
+++ b/rare/utils/paths.py
@@ -36,6 +36,12 @@ def lock_file() -> Path:
     return Path(QStandardPaths.writableLocation(QStandardPaths.TempLocation), "Rare.lock")
 
 
+def config_dir() -> Path:
+    # FIXME: This returns ~/.config/Rare/Rare/ for some reason while the settings are in ~/.config/Rare/Rare.conf
+    # Take the parent for now, but this should be investigated
+    return Path(QStandardPaths.writableLocation(QStandardPaths.AppConfigLocation)).parent
+
+
 def data_dir() -> Path:
     return Path(QStandardPaths.writableLocation(QStandardPaths.AppDataLocation))
 
@@ -129,6 +135,7 @@ def desktop_link_path(link_name: str, link_type: str) -> Path:
 
 
 def get_rare_executable() -> List[str]:
+    logger.debug(f"Trying to find executable: {sys.executable}, {sys.argv}")
     # lk: detect if nuitka
     if "__compiled__" in globals():
         executable = [sys.executable]
diff --git a/rare/utils/steam_grades.py b/rare/utils/steam_grades.py
index 07d7b9d7b..06a7a2b85 100644
--- a/rare/utils/steam_grades.py
+++ b/rare/utils/steam_grades.py
@@ -1,5 +1,5 @@
 import difflib
-import json
+import orjson
 import os
 from datetime import date
 
@@ -20,7 +20,7 @@ def get_rating(core: LegendaryCore, app_name: str):
     global __grades_json
     if __grades_json is None:
         if os.path.exists(p := os.path.join(data_dir(), "steam_ids.json")):
-            grades = json.loads(open(p).read())
+            grades = orjson.loads(open(p).read())
             __grades_json = grades
         else:
             grades = {}
@@ -37,8 +37,7 @@ def get_rating(core: LegendaryCore, app_name: str):
             return "fail"
         grades[app_name] = {"steam_id": steam_id, "grade": grade}
         with open(os.path.join(data_dir(), "steam_ids.json"), "w") as f:
-            f.write(json.dumps(grades))
-            f.close()
+            f.write(orjson.dumps(grades).decode("utf-8"))
         return grade
     else:
         return grades[app_name].get("grade")
@@ -52,8 +51,8 @@ def get_grade(steam_code):
     url = "https://www.protondb.com/api/v1/reports/summaries/"
     res = requests.get(f"{url}{steam_code}.json")
     try:
-        lista = json.loads(res.text)
-    except json.decoder.JSONDecodeError:
+        lista = orjson.loads(res.text)
+    except orjson.JSONDecodeError:
         return "fail"
 
     return lista["tier"]
@@ -63,17 +62,16 @@ def load_json() -> dict:
     file = os.path.join(cache_dir(), "game_list.json")
     if not os.path.exists(file):
         response = requests.get(url)
-        steam_ids = json.loads(response.text)["applist"]["apps"]
+        steam_ids = orjson.loads(response.text)["applist"]["apps"]
         ids = {}
         for game in steam_ids:
             ids[game["name"]] = game["appid"]
 
         with open(file, "w") as f:
-            f.write(json.dumps(ids))
-            f.close()
+            f.write(orjson.dumps(ids).decode("utf-8"))
         return ids
     else:
-        return json.loads(open(file, "r").read())
+        return orjson.loads(open(file, "r").read())
 
 
 def get_steam_id(title: str):
@@ -85,16 +83,15 @@ def get_steam_id(title: str):
         if not os.path.exists(file):
             response = requests.get(url)
             ids = {}
-            steam_ids = json.loads(response.text)["applist"]["apps"]
+            steam_ids = orjson.loads(response.text)["applist"]["apps"]
             for game in steam_ids:
                 ids[game["name"]] = game["appid"]
             __steam_ids_json = ids
 
             with open(file, "w") as f:
-                f.write(json.dumps(ids))
-                f.close()
+                f.write(orjson.dumps(ids).decode("utf-8"))
         else:
-            ids = json.loads(open(file, "r").read())
+            ids = orjson.loads(open(file, "r").read())
             __steam_ids_json = ids
     else:
         ids = __steam_ids_json
@@ -112,7 +109,7 @@ def get_steam_id(title: str):
 
 def check_time():  # this function check if it's time to update
     file = os.path.join(cache_dir(), "game_list.json")
-    json_table = json.loads(open(file, "r").read())
+    json_table = orjson.loads(open(file, "r").read())
 
     today = date.today()
     day = 0  # it controls how many days it's necessary for an update
diff --git a/rare/utils/wine.py b/rare/utils/wine.py
index d3624d1a9..306192f0b 100644
--- a/rare/utils/wine.py
+++ b/rare/utils/wine.py
@@ -2,10 +2,13 @@
 import shutil
 import subprocess
 from configparser import ConfigParser
+from logging import getLogger
 from typing import Mapping, Dict, List, Tuple
 
 from rare.lgndr.core import LegendaryCore
 
+logger = getLogger("Wine")
+
 
 # this is a copied function from legendary.utils.wine_helpers, but registry file can be specified
 def read_registry(registry: str, wine_pfx: str) -> ConfigParser:
@@ -19,16 +22,27 @@ def read_registry(registry: str, wine_pfx: str) -> ConfigParser:
     return reg
 
 
-def execute(cmd: List, wine_env: Mapping) -> Tuple[str, str]:
-    proc = subprocess.Popen(
-        cmd,
-        stdout=subprocess.PIPE,
-        stderr=subprocess.PIPE,
-        env=wine_env,
-        shell=False,
-        text=True,
-    )
-    return proc.communicate()
+def execute(command: List, wine_env: Mapping) -> Tuple[str, str]:
+    if os.environ.get("container") == "flatpak":
+        flatpak_command = ["flatpak-spawn", "--host"]
+        for name, value in wine_env.items():
+            flatpak_command.append(f"--env={name}={value}")
+        command = flatpak_command + command
+    try:
+        proc = subprocess.Popen(
+            command,
+            stdout=subprocess.PIPE,
+            stderr=subprocess.PIPE,
+            # Use the current environment if we are in flatpak or our own if we are on host
+            # In flatpak our environment is passed through `flatpak-spawn` arguments
+            env=os.environ.copy() if os.environ.get("container") == "flatpak" else wine_env,
+            shell=False,
+            text=True,
+        )
+        res = proc.communicate()
+    except (FileNotFoundError, PermissionError) as e:
+        res = ("", str(e))
+    return res
 
 
 def resolve_path(wine_exec: str, wine_env: Mapping, path: str) -> str:
@@ -38,7 +52,11 @@ def resolve_path(wine_exec: str, wine_env: Mapping, path: str) -> str:
     # lk: if path exists and needs a case-sensitive interpretation form
     # cmd = [wine_cmd, 'cmd', '/c', f'cd {path} & cd']
     out, err = execute(cmd, wine_env)
-    return out.strip().strip('"')
+    out, err = out.strip(), err.strip()
+    if not out:
+        logger.error("Failed to resolve wine path due to \"%s\"", err)
+        return out
+    return out.strip('"')
 
 
 def query_reg_path(wine_exec: str, wine_env: Mapping, reg_path: str):
@@ -48,6 +66,10 @@ def query_reg_path(wine_exec: str, wine_env: Mapping, reg_path: str):
 def query_reg_key(wine_exec: str, wine_env: Mapping, reg_path: str, reg_key) -> str:
     cmd = [wine_exec, "reg", "query", reg_path, "/v", reg_key]
     out, err = execute(cmd, wine_env)
+    out, err = out.strip(), err.strip()
+    if not out:
+        logger.error("Failed to query registry key due to \"%s\"", err)
+        return out
     lines = out.split("\n")
     keys: Dict = {}
     for line in lines:
@@ -63,16 +85,12 @@ def convert_to_windows_path(wine_exec: str, wine_env: Mapping, path: str) -> str
 
 def convert_to_unix_path(wine_exec: str, wine_env: Mapping, path: str) -> str:
     path = path.strip().strip('"')
-    cmd = [winepath(wine_exec), "-u", path]
+    cmd = [wine_exec, "winepath.exe", "-u", path]
     out, err = execute(cmd, wine_env)
-    return os.path.realpath(out.strip())
-
-
-def winepath(wine_exec: str) -> str:
-    _winepath = os.path.join(os.path.dirname(wine_exec), "winepath")
-    if not os.path.isfile(_winepath):
-        return ""
-    return _winepath
+    out, err = out.strip(), err.strip()
+    if not out:
+        logger.error("Failed to convert to unix path due to \"%s\"", err)
+    return os.path.realpath(out) if (out := out.strip()) else out
 
 
 def wine(core: LegendaryCore, app_name: str = "default") -> str:
@@ -85,8 +103,11 @@ def wine(core: LegendaryCore, app_name: str = "default") -> str:
 
 
 def environ(core: LegendaryCore, app_name: str = "default") -> Dict:
-    _environ = os.environ.copy()
+    # Get a clean environment if we are in flatpak, this environment will be pass
+    # to `flatpak-spawn`, otherwise use the system's.
+    _environ = {} if os.environ.get("container") == "flatpak" else os.environ.copy()
     _environ.update(core.get_app_environment(app_name))
+    _environ["WINEDEBUG"] = "-all"
     _environ["WINEDLLOVERRIDES"] = "winemenubuilder=d;mscoree=d;mshtml=d;"
     _environ["DISPLAY"] = ""
     return _environ
diff --git a/rare/widgets/indicator_edit.py b/rare/widgets/indicator_edit.py
index e68414d1d..1197407f6 100644
--- a/rare/widgets/indicator_edit.py
+++ b/rare/widgets/indicator_edit.py
@@ -145,10 +145,7 @@ def __init__(
         self.line_edit.layout().setContentsMargins(0, 0, 10, 0)
         self.line_edit.layout().addWidget(self.hint_label)
         # Add completer
-        if completer is not None:
-            completer.popup().setItemDelegate(QStyledItemDelegate(self))
-            completer.popup().setAlternatingRowColors(True)
-            self.line_edit.setCompleter(completer)
+        self.setCompleter(completer)
         layout.addWidget(self.line_edit)
         if edit_func is not None:
             self.indicator_label = QLabel()
@@ -195,6 +192,16 @@ def setHintText(self, text: str):
         self.hint_label.setFrameRect(self.line_edit.rect())
         self.hint_label.setText(text)
 
+    def setCompleter(self, completer: Optional[QCompleter]):
+        if old := self.line_edit.completer():
+            old.deleteLater()
+        if not completer:
+            self.line_edit.setCompleter(None)
+            return
+        completer.popup().setItemDelegate(QStyledItemDelegate(self))
+        completer.popup().setAlternatingRowColors(True)
+        self.line_edit.setCompleter(completer)
+
     def extend_reasons(self, reasons: Dict):
         self.__reasons.extend(reasons)
 
diff --git a/rare/widgets/rare_app.py b/rare/widgets/rare_app.py
index e64aead5b..7a7319585 100644
--- a/rare/widgets/rare_app.py
+++ b/rare/widgets/rare_app.py
@@ -20,8 +20,8 @@ class RareAppException(QObject):
     exception = pyqtSignal(object, object, object)
 
     def __init__(self, parent=None):
-        self.logger = logging.getLogger(type(self).__name__)
         super(RareAppException, self).__init__(parent=parent)
+        self.logger = logging.getLogger(type(self).__name__)
         sys.excepthook = self._excepthook
         self.exception.connect(self._on_exception)
 
@@ -37,14 +37,19 @@ def _on_exception(self, exc_type, exc_value, exc_tb):
         if self._handler(exc_type, exc_value, exc_tb):
             return
         self.logger.fatal(message)
-        QMessageBox.warning(None, exc_type.__name__, message)
-        QApplication.exit(1)
+        action = QMessageBox.warning(
+            None, exc_type.__name__, message,
+            buttons=QMessageBox.Ignore | QMessageBox.Close,
+            defaultButton=QMessageBox.Ignore
+        )
+        if action == QMessageBox.RejectRole:
+            QApplication.exit(1)
 
 
 class RareApp(QApplication):
     def __init__(self, args: Namespace, log_file: str):
-        self.logger = logging.getLogger(type(self).__name__)
         super(RareApp, self).__init__(sys.argv)
+        self.logger = logging.getLogger(type(self).__name__)
         self._hook = RareAppException(self)
         self.setQuitOnLastWindowClosed(False)
         self.setAttribute(Qt.AA_DontUseNativeDialogs, True)
diff --git a/requirements-flatpak.txt b/requirements-flatpak.txt
new file mode 100644
index 000000000..86f81354d
--- /dev/null
+++ b/requirements-flatpak.txt
@@ -0,0 +1,5 @@
+requests
+QtAwesome
+setuptools
+legendary-gl>=0.20.34
+pypresence
\ No newline at end of file
diff --git a/requirements-full.txt b/requirements-full.txt
index f9e309c1a..93436a61f 100644
--- a/requirements-full.txt
+++ b/requirements-full.txt
@@ -1,9 +1,9 @@
-typing_extensions
 requests
 PyQt5
 QtAwesome
 setuptools
-legendary-gl
+legendary-gl>=0.20.33
+orjson
 pywin32; platform_system == "Windows"
 pywebview[qt]; platform_system == "Linux"
 pywebview[qt]; platform_system == "FreeBSD"
diff --git a/requirements.txt b/requirements.txt
index f30e7c72e..cf445de55 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,7 +1,7 @@
-typing_extensions
 requests
 PyQt5
 QtAwesome
 setuptools
 legendary-gl>=0.20.34
+orjson
 pywin32; platform_system == "Windows"
diff --git a/setup.py b/setup.py
index 7a769de82..bad9d7fa6 100644
--- a/setup.py
+++ b/setup.py
@@ -8,12 +8,12 @@
 requirements = [
     "requests<3.0",
     "legendary-gl>=0.20.34",
+    "orjson",
     "setuptools",
     "wheel",
     "PyQt5",
     "QtAwesome",
     'pywin32; platform_system == "Windows"',
-    "typing_extensions"
 ]
 
 optional_reqs = dict(
@@ -45,7 +45,10 @@
     ],
     include_package_data=True,
     python_requires=">=3.9",
-    entry_points=dict(console_scripts=["rare=rare.__main__:main"]),
+    entry_points={
+        # 'console_scripts': ["rare = rare.main:main"],
+        'gui_scripts': ["rare = rare.main:main"],
+    },
     install_requires=requirements,
     extras_require=optional_reqs,
 )


Test patch:
diff --git a/.github/workflows/release-tests.yml b/.github/workflows/release-tests.yml
deleted file mode 100644
index cdd31cee8..000000000
--- a/.github/workflows/release-tests.yml
+++ /dev/null
@@ -1,247 +0,0 @@
-
-name: "Snapshot"
-
-
-on:
-  workflow_dispatch:
-
-
-jobs:
-  version:
-    name: "Version"
-    runs-on: ubuntu-latest
-    outputs:
-      tag_abbrev: ${{ steps.version.outputs.tag_abbrev }}
-      tag_offset: ${{ steps.version.outputs.tag_offset }}
-      sha_short: ${{ steps.version.outputs.sha_short }}
-      full_desc: ${{ steps.version.outputs.full_desc }}
-    steps:
-      - uses: actions/checkout@v3
-        with:
-          fetch-depth: 0
-      - name: Version
-        id: version
-        shell: bash
-        run: |
-          tag_abbrev=$(git tag --sort=v:refname | grep -oE "(^[0-9]+\.[0-9]+(.[0-9]+)?)$" | tail -1)
-          echo "tag_abbrev=$tag_abbrev" >> $GITHUB_OUTPUT
-          echo "tag_offset=$(git rev-list $tag_abbrev..HEAD --count)" >> $GITHUB_OUTPUT
-          echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
-          echo "full_desc=$(git describe --long --tags)" >> $GITHUB_OUTPUT
-
-  deb-package:
-    needs: version
-    runs-on: ubuntu-latest
-    steps:
-      - name: Install Makedeb
-        run: |
-          wget -qO - 'https://proget.hunterwittenborn.com/debian-feeds/makedeb.pub' | gpg --dearmor | sudo tee /usr/share/keyrings/makedeb-archive-keyring.gpg &> /dev/null
-          echo 'deb [signed-by=/usr/share/keyrings/makedeb-archive-keyring.gpg arch=all] https://proget.hunterwittenborn.com/ makedeb main' | sudo tee /etc/apt/sources.list.d/makedeb.list
-          sudo apt update
-          sudo apt install makedeb
-      - name: Prepare source directory
-        run: |
-          git clone https://mpr.hunterwittenborn.com/rare.git build
-          sed -i 's/source=.*/source=("rare-test::git+$url")/g' build/PKGBUILD
-          sed -i "s/\$pkgver/test/g" build/PKGBUILD
-
-      - name: build deb
-        run: |
-          cd build
-          makedeb -d
-          mv *.deb ../Rare.deb
-
-      - name: Upload to Artifacts
-        uses: actions/upload-artifact@v3
-        with:
-          name: Rare-${{ needs.version.outputs.tag_abbrev }}.${{ needs.version.outputs.tag_offset }}.deb
-          path: Rare.deb
-
-  appimage:
-    needs: version
-    runs-on: ubuntu-22.04
-    steps:
-      - uses: actions/checkout@v3
-      - name: Install build dependencies
-        run: |
-          sudo apt update
-          sudo apt install python3 python3-pip python3-setuptools patchelf desktop-file-utils libgdk-pixbuf2.0-dev fakeroot strace fuse
-      - name: Install appimage-builder
-        run: |
-          sudo wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage -O /usr/local/bin/appimagetool
-          sudo chmod +x /usr/local/bin/appimagetool
-          sudo pip3 install appimage-builder
-      - name: Build
-        run: |
-          cp rare/__main__.py .
-          appimage-builder --skip-test
-          mv Rare-*.AppImage Rare.AppImage
-          mv Rare-*.AppImage.zsync Rare.AppImage.zsync
-
-      - name: Upload to Artifacts
-        uses: actions/upload-artifact@v3
-        with:
-          name: Rare-${{ needs.version.outputs.tag_abbrev }}.${{ needs.version.outputs.tag_offset }}.AppImage
-          path: Rare.AppImage
-      - name: Upload to Artifacts (zsync)
-        uses: actions/upload-artifact@v3
-        with:
-          name: Rare-${{ needs.version.outputs.tag_abbrev }}.${{ needs.version.outputs.tag_offset }}.AppImage.zsync
-          path: Rare.AppImage.zsync
-
-  nuitka:
-    if: ${{ false }}
-    needs: version
-    runs-on: "windows-latest"
-    steps:
-      - uses: actions/checkout@v3
-      - uses: actions/setup-python@v4
-        with:
-          cache: pip
-          python-version: '3.9'
-          check-latest: true
-          architecture: x64
-      - name: Install build dependencies
-        run: pip3 install nuitka ordered-set
-      - name: Install target dependencies
-        run: |
-          pip3 install -r requirements.txt
-          pip3 install -r requirements-presence.txt
-      - name: Build
-        run: >-
-          python -m nuitka
-          --assume-yes-for-downloads
-          --msvc=latest
-          --lto=yes
-          --jobs=2
-          --static-libpython=no
-          --standalone
-          --enable-plugin=anti-bloat
-          --enable-plugin=pyqt5
-          --show-modules
-          --show-anti-bloat-changes
-          --follow-stdlib
-          --follow-imports
-          --nofollow-import-to="*.tests"
-          --nofollow-import-to="*.distutils"
-          --prefer-source-code
-          --include-package=pypresence
-          --include-package-data=qtawesome
-          --include-data-dir=rare\resources\images=rare\resources\images
-          --include-data-files=rare\resources\languages=rare\resources\languages="*.qm"
-          --windows-icon-from-ico=rare\resources\images\Rare.ico
-          --windows-company-name=Rare
-          --windows-product-name=Rare
-          --windows-file-description=rare.exe
-          --windows-file-version=${{ needs.version.outputs.tag_abbrev }}.${{ needs.version.outputs.tag_offset }}
-          --windows-product-version=${{ needs.version.outputs.tag_abbrev }}.${{ needs.version.outputs.tag_offset }}
-          --enable-console
-          rare
-      - name: Fix QtNetwork SSL
-        run: |
-          Copy-Item -Path "rare.dist\libcrypto-1_1.dll" -Destination "rare.dist\libcrypto-1_1-x64.dll"
-          Copy-Item -Path "rare.dist\libssl-1_1.dll" -Destination "rare.dist\libssl-1_1-x64.dll"
-      - name: Compress
-        run: |
-          python -c "import shutil; shutil.make_archive('Rare-Windows', 'zip', 'rare.dist')"
-
-      - name: Upload to Artifacts
-        uses: actions/upload-artifact@v3
-        with:
-          name: Rare-Windows-${{ needs.version.outputs.tag_abbrev }}.${{ needs.version.outputs.tag_offset }}
-          path: Rare-Windows.zip
-
-  cx_freeze_msi:
-    needs: version
-    runs-on: "windows-latest"
-    steps:
-      - uses: actions/checkout@v3
-      - uses: actions/setup-python@v4
-        with:
-          cache: pip
-          python-version: '3.11'
-          check-latest: true
-          architecture: x64
-      - name: Install Build Dependencies
-        run: pip3 install --upgrade cx_freeze wheel
-      - name: Install Target Dependencies
-        run: |
-          pip3 install -r requirements.txt
-          pip3 install -r requirements-presence.txt
-      - name: Build
-        run: |
-          python freeze.py bdist_msi
-          mv dist/*.msi Rare.msi
-
-      - name: Upload to Artifacts
-        uses: actions/upload-artifact@v3
-        with:
-          name: Rare-${{ needs.version.outputs.tag_abbrev }}.${{ needs.version.outputs.tag_offset }}.msi
-          path: Rare.msi
-
-  cx_freeze_zip:
-    needs: version
-    runs-on: "windows-latest"
-    steps:
-      - uses: actions/checkout@v3
-      - uses: actions/setup-python@v4
-        with:
-          cache: pip
-          python-version: '3.11'
-          check-latest: true
-          architecture: x64
-      - name: Install build dependencies
-        run: pip3 install cx_freeze
-      - name: Install target dependencies
-        run: |
-          pip3 install -r requirements.txt
-          pip3 install -r requirements-presence.txt
-          pip3 install .
-      - name: Build
-        run: cxfreeze -c rare/__main__.py --target-dir dist --target-name rare --icon rare/resources/images/Rare.ico -OO --base-name Win32GUI
-      - name: Compress
-        run: |
-          python -c "import shutil; shutil.make_archive('Rare-Windows', 'zip', 'dist')"
-
-      - name: Upload to Artifacts
-        uses: actions/upload-artifact@v3
-        with:
-          name: Rare-Windows-${{ needs.version.outputs.tag_abbrev }}.${{ needs.version.outputs.tag_offset }}.zip
-          path: Rare-Windows.zip
-
-  mac_os:
-    needs: version
-    runs-on: macos-latest
-    steps:
-      - uses: actions/checkout@v3
-      - uses: actions/setup-python@v4
-        with:
-          cache: pip
-          python-version: '3.11'
-          check-latest: true
-      - name: Install Build Dependencies
-        run: pip install pyinstaller
-      - name: Install Target Dependencies
-        run: |
-          pip install -r requirements.txt
-      - name: Move files
-        run: mv rare/__main__.py __main__.py
-      - name: Build
-        run: >-
-          pyinstaller -F --name Rare
-          --add-data "rare/resources/languages/*:rare/resources/languages"
-          --add-data "rare/resources/images/*:rare/resources/images/"
-          --windowed
-          --icon rare/resources/images/Rare.icns
-          --hidden-import=legendary
-          __main__.py
-      - name: Create dmg
-        run: |
-          git clone https://github.com/create-dmg/create-dmg
-          create-dmg/create-dmg Rare.dmg dist/Rare.App --volname Rare --volicon rare/resources/images/Rare.icns
-
-      - name: Upload to Artifacts
-        uses: actions/upload-artifact@v3
-        with:
-          name: Rare-${{ needs.version.outputs.tag_abbrev }}.${{ needs.version.outputs.tag_offset }}.dmg
-          path: Rare.dmg