├── .gitattributes ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug.yml │ ├── config.yml │ ├── feature-request.yml │ ├── feedback.md │ └── mirror.yml ├── dependabot.yml ├── pull_request_template.md └── workflows │ ├── build_flatpak.yml │ ├── close-issues.yml │ ├── close-stale-issues.yml │ ├── pre-commit.yml │ └── update-manifest.yml ├── .gitignore ├── .gitmodules ├── .pre-commit-config.yaml ├── CODE_OF_CONDUCT.md ├── CODING_GUIDE.md ├── CONTRIBUTING.md ├── COPYING.md ├── README.md ├── VERSION ├── VERSION_UPDATE.md ├── bottles ├── __init__.py ├── backend │ ├── __init__.py │ ├── cabextract.py │ ├── diff.py │ ├── dlls │ │ ├── __init__.py │ │ ├── dll.py │ │ ├── dxvk.py │ │ ├── latencyflex.py │ │ ├── meson.build │ │ ├── nvapi.py │ │ └── vkd3d.py │ ├── downloader.py │ ├── globals.py │ ├── health.py │ ├── logger.py │ ├── managers │ │ ├── __init__.py │ │ ├── backup.py │ │ ├── component.py │ │ ├── conf.py │ │ ├── data.py │ │ ├── dependency.py │ │ ├── epicgamesstore.py │ │ ├── importer.py │ │ ├── installer.py │ │ ├── journal.py │ │ ├── library.py │ │ ├── manager.py │ │ ├── meson.build │ │ ├── origin.py │ │ ├── queue.py │ │ ├── repository.py │ │ ├── runtime.py │ │ ├── sandbox.py │ │ ├── steam.py │ │ ├── steamgriddb.py │ │ ├── template.py │ │ ├── thumbnail.py │ │ ├── ubisoftconnect.py │ │ └── versioning.py │ ├── meson.build │ ├── models │ │ ├── __init__.py │ │ ├── config.py │ │ ├── enum.py │ │ ├── meson.build │ │ ├── result.py │ │ ├── samples.py │ │ └── vdict.py │ ├── params.py │ ├── repos │ │ ├── __init__.py │ │ ├── component.py │ │ ├── dependency.py │ │ ├── installer.py │ │ ├── meson.build │ │ └── repo.py │ ├── runner.py │ ├── state.py │ ├── utils │ │ ├── __init__.py │ │ ├── connection.py │ │ ├── decorators.py │ │ ├── display.py │ │ ├── file.py │ │ ├── generic.py │ │ ├── gpu.py │ │ ├── gsettings_stub.py │ │ ├── imagemagick.py │ │ ├── json.py │ │ ├── lnk.py │ │ ├── manager.py │ │ ├── meson.build │ │ ├── midi.py │ │ ├── nvidia.py │ │ ├── proc.py │ │ ├── singleton.py │ │ ├── snake.py │ │ ├── steam.py │ │ ├── terminal.py │ │ ├── threading.py │ │ ├── vdf.py │ │ ├── vulkan.py │ │ ├── wine.py │ │ └── yaml.py │ └── wine │ │ ├── __init__.py │ │ ├── catalogs.py │ │ ├── cmd.py │ │ ├── control.py │ │ ├── drives.py │ │ ├── eject.py │ │ ├── executor.py │ │ ├── expand.py │ │ ├── explorer.py │ │ ├── hh.py │ │ ├── icinfo.py │ │ ├── meson.build │ │ ├── msiexec.py │ │ ├── net.py │ │ ├── notepad.py │ │ ├── oleview.py │ │ ├── progman.py │ │ ├── reg.py │ │ ├── regedit.py │ │ ├── register.py │ │ ├── regkeys.py │ │ ├── regsvr32.py │ │ ├── rundll32.py │ │ ├── start.py │ │ ├── taskmgr.py │ │ ├── uninstaller.py │ │ ├── wineboot.py │ │ ├── winebridge.py │ │ ├── winecfg.py │ │ ├── winecommand.py │ │ ├── winedbg.py │ │ ├── winefile.py │ │ ├── winepath.py │ │ ├── wineprogram.py │ │ ├── wineserver.py │ │ ├── winhelp.py │ │ └── xcopy.py ├── frontend │ ├── __init__.py │ ├── bottle-details-page.blp │ ├── bottle-details-view.blp │ ├── bottle-picker-dialog.blp │ ├── bottle-row.blp │ ├── bottle_details_page.py │ ├── bottle_details_view.py │ ├── bottle_picker_dialog.py │ ├── bottles-list-view.blp │ ├── bottles.gresource.xml │ ├── bottles.py │ ├── bottles_list_view.py │ ├── check-row.blp │ ├── cli.py │ ├── common.py │ ├── component-entry-row.blp │ ├── component_entry_row.py │ ├── crash-report-dialog.blp │ ├── crash_report_dialog.py │ ├── dependencies-check-dialog.blp │ ├── dependencies_check_dialog.py │ ├── dependency-entry-row.blp │ ├── dependency_entry_row.py │ ├── details-dependencies-view.blp │ ├── details-installers-view.blp │ ├── details-preferences-page.blp │ ├── details-task-manager-view.blp │ ├── details-versioning-page.blp │ ├── details_dependencies_view.py │ ├── details_installers_view.py │ ├── details_preferences_page.py │ ├── details_task_manager_view.py │ ├── details_versioning_page.py │ ├── display-dialog.blp │ ├── display_dialog.py │ ├── dll-override-entry.blp │ ├── dll-overrides-dialog.blp │ ├── dll_overrides_dialog.py │ ├── drive-entry.blp │ ├── drives-dialog.blp │ ├── drives_dialog.py │ ├── duplicate-dialog.blp │ ├── duplicate_dialog.py │ ├── env-var-entry.blp │ ├── environment-variables-dialog.blp │ ├── environment_variables_dialog.py │ ├── exclusion-pattern-row.blp │ ├── exclusion-patterns-dialog.blp │ ├── exclusion_patterns_dialog.py │ ├── executable.py │ ├── filters.py │ ├── fsr-dialog.blp │ ├── fsr_dialog.py │ ├── gamescope-dialog.blp │ ├── gamescope_dialog.py │ ├── generic.py │ ├── generic_cli.py │ ├── gtk.py │ ├── help-overlay.blp │ ├── importer-row.blp │ ├── importer-view.blp │ ├── importer_row.py │ ├── importer_view.py │ ├── installer-dialog.blp │ ├── installer-row.blp │ ├── installer_dialog.py │ ├── installer_row.py │ ├── journal-dialog.blp │ ├── journal_dialog.py │ ├── launch-options-dialog.blp │ ├── launch_options_dialog.py │ ├── library-entry.blp │ ├── library-view.blp │ ├── library_entry.py │ ├── library_view.py │ ├── loading-view.blp │ ├── loading_view.py │ ├── local-resource-row.blp │ ├── main.py │ ├── mangohud-dialog.blp │ ├── mangohud_dialog.py │ ├── meson.build │ ├── new-bottle-dialog.blp │ ├── new_bottle_dialog.py │ ├── onboard-dialog.blp │ ├── onboard_dialog.py │ ├── operation.py │ ├── params.py │ ├── preferences.blp │ ├── preferences.py │ ├── program-row.blp │ ├── program_row.py │ ├── proton-alert-dialog.blp │ ├── proton_alert_dialog.py │ ├── rename-program-dialog.blp │ ├── rename_program_dialog.py │ ├── sandbox-dialog.blp │ ├── sandbox_dialog.py │ ├── sh.py │ ├── state-row.blp │ ├── state_row.py │ ├── style-dark.css │ ├── style.css │ ├── task-row.blp │ ├── upgrade-versioning-dialog.blp │ ├── upgrade_versioning_dialog.py │ ├── vkbasalt-dialog.blp │ ├── vkbasalt_dialog.py │ ├── vmtouch-dialog.blp │ ├── vmtouch_dialog.py │ ├── window.blp │ └── window.py ├── meson.build └── tests │ ├── __init__.py │ └── backend │ ├── __init__.py │ ├── manager │ ├── __init__.py │ └── test_manager.py │ ├── state │ ├── __init__.py │ └── test_events.py │ └── utils │ ├── __init__.py │ └── test_generic.py ├── build-aux ├── bottles-deps.yaml ├── com.usebottles.bottles.Devel.json ├── install.sh └── pypi-deps.yaml ├── data ├── com.usebottles.bottles.desktop.in.in ├── com.usebottles.bottles.gschema.xml ├── com.usebottles.bottles.metainfo.xml.in.in ├── data.gresource.xml.in ├── icons │ ├── hicolor │ │ ├── scalable │ │ │ └── apps │ │ │ │ ├── com.usebottles.bottles-program.svg │ │ │ │ ├── com.usebottles.bottles.Devel.svg │ │ │ │ ├── com.usebottles.bottles.svg │ │ │ │ └── com.usebottles.bottles_old.svg │ │ └── symbolic │ │ │ ├── actions │ │ │ ├── application-x-addon-symbolic.svg │ │ │ ├── applications-system-symbolic.svg │ │ │ ├── computer-symbolic.svg │ │ │ ├── document-save-symbolic.svg │ │ │ ├── external-link-symbolic.svg │ │ │ ├── go-next-symbolic.svg │ │ │ ├── go-previous-symbolic.svg │ │ │ ├── info-symbolic.svg │ │ │ ├── library-symbolic.svg │ │ │ ├── list-add-symbolic.svg │ │ │ ├── media-playback-start-symbolic.svg │ │ │ ├── media-playback-stop-symbolic.svg │ │ │ ├── open-menu-symbolic.svg │ │ │ ├── paper-symbolic.svg │ │ │ ├── preferences-desktop-apps-symbolic.svg │ │ │ ├── preferences-system-time-symbolic.svg │ │ │ ├── selection-mode-symbolic.svg │ │ │ ├── system-run-symbolic.svg │ │ │ ├── system-search-symbolic.svg │ │ │ ├── system-shutdown-symbolic.svg │ │ │ ├── system-software-install-symbolic.svg │ │ │ ├── view-more-symbolic.svg │ │ │ └── warning-symbolic.svg │ │ │ └── apps │ │ │ ├── bottle-symbolic.svg │ │ │ ├── bottles-steam-symbolic.svg │ │ │ ├── com.usebottles.bottles-symbolic.svg │ │ │ └── com.usebottles.bottles.Devel-symbolic.svg │ └── meson.build ├── images │ ├── bottles-welcome-night.svg │ ├── bottles-welcome.png │ └── bottles-welcome.svg ├── meson.build └── screenshots │ ├── 1.png │ ├── 2.png │ ├── 3.png │ ├── 4.png │ ├── 5.png │ └── 6.png ├── docs ├── screenshot-dark.png └── screenshot-light.png ├── meson.build ├── meson_options.txt ├── mypy.ini ├── po ├── LINGUAS ├── POTFILES ├── README.md ├── ar.po ├── az.po ├── be.po ├── bg.po ├── bn.po ├── bottles.pot ├── bs.po ├── ca.po ├── ckb.po ├── cs.po ├── da.po ├── de.po ├── el.po ├── eo.po ├── es.po ├── et.po ├── eu.po ├── fa.po ├── fi.po ├── fr.po ├── ga.po ├── gl.po ├── he.po ├── hi.po ├── hr.po ├── hu.po ├── id.po ├── ie.po ├── it.po ├── ja.po ├── ka.po ├── ko.po ├── lt.po ├── meson.build ├── ms.po ├── nb_NO.po ├── nl.po ├── pl.po ├── pt.po ├── pt_BR.po ├── ro.po ├── ru.po ├── sk.po ├── sl.po ├── sr.po ├── sv.po ├── ta.po ├── th.po ├── tr.po ├── uk.po ├── vi.po ├── zh_CN.po ├── zh_HK.po ├── zh_Hans.po ├── zh_Hant.po ├── zh_SG.po └── zh_TW.po ├── pyproject.toml ├── requirements.dev.txt └── requirements.txt /.gitattributes: -------------------------------------------------------------------------------- 1 | # Ref: https://git-scm.com/docs/gitattributes 2 | * text=auto eol=lf 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | liberapay: Bottles 3 | github: ['bottlesdevs'] 4 | custom: ['https://usebottles.com/funding'] 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: Documentation 4 | url: https://docs.usebottles.com/ 5 | about: Before posting, check if the topic has already been covered by our documentation. 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Suggest an idea for this project 3 | title: "[Request]: " 4 | labels: ["Feature request"] 5 | 6 | body: 7 | - type: textarea 8 | id: what-happened 9 | attributes: 10 | label: Tell us the problem or your need 11 | description: A clear and concise description of what the problem is. 12 | placeholder: Ex. I'm always frustrated when [...] 13 | validations: 14 | required: true 15 | 16 | - type: textarea 17 | id: your-solution 18 | attributes: 19 | label: Describe the solution you'd like 20 | description: A clear and concise description of what you want to happen. 21 | placeholder: To fix this, I would [...] 22 | validations: 23 | required: true 24 | 25 | - type: textarea 26 | id: other-solutions 27 | attributes: 28 | label: Other solutions? 29 | description: A clear and concise description of any alternative solutions or features you've considered. 30 | validations: 31 | required: false 32 | 33 | - type: textarea 34 | id: additional-context 35 | attributes: 36 | label: Additional context and references 37 | description: Add any other context or reference about the feature request here. 38 | validations: 39 | required: false 40 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feedback.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: General Feedback 3 | about: Send your feedback, start a discussion, or ask a question to the developers. 4 | labels: Feedback 5 | --- 6 | 7 | !!! PLEASE DON'T OPEN ISSUES FOR PROGRAMS NOT RUNNING IN BOTTLES, USE THE programs REPOSITORY INSTEAD !!! 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/mirror.yml: -------------------------------------------------------------------------------- 1 | name: Network issue report 2 | description: Report Network/Mirror issue to sysadmin 3 | labels: ["Network Issue"] 4 | body: 5 | - type: markdown 6 | id: introduction 7 | attributes: 8 | value: | 9 | 📝 Please use this template while reporting a Network/Mirror issue and provide as much info as possible. 10 | 11 | - type: checkboxes 12 | id: prerequisites 13 | attributes: 14 | label: Prerequisites 15 | options: 16 | - label: | 17 | I am sure that this problem has NEVER been discussed in [other issues](https://github.com/bottlesdevs/Bottles/issues). 18 | required: true 19 | 20 | - type: textarea 21 | id: what_happened 22 | attributes: 23 | label: What happened 24 | validations: 25 | required: true 26 | 27 | - type: textarea 28 | id: expected_behavior 29 | attributes: 30 | label: What you expected to happen 31 | validations: 32 | required: true 33 | 34 | - type: textarea 35 | id: how_to_reproduce 36 | attributes: 37 | label: How to reproduce it 38 | validations: 39 | required: true 40 | 41 | - type: textarea 42 | id: ping_result 43 | attributes: 44 | label: Ping proxy.usebottles.com 45 | description: Please run `ping proxy.usebottles.com` in a terminal and send us the output. 46 | render: log 47 | validations: 48 | required: true 49 | 50 | - type: textarea 51 | id: dig_result 52 | attributes: 53 | label: Dig proxy.usebottles.com 54 | description: Please run `dig proxy.usebottles.com` in a terminal and send us the output. 55 | render: log 56 | validations: 57 | required: true 58 | 59 | - type: input 60 | id: isp 61 | attributes: 62 | label: Your Internet Service Provider (ISP) name 63 | 64 | - type: input 65 | id: area 66 | attributes: 67 | label: Your Country/Area 68 | 69 | - type: textarea 70 | id: others 71 | attributes: 72 | label: Anything else we need to know 73 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: github-actions 9 | directory: "/" 10 | schedule: 11 | interval: daily 12 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Description 2 | Please include a summary of the change and which issue is fixed (if available). 3 | Please also include relevant motivation and context. 4 | 5 | Fixes #(issue) 6 | 7 | ## Type of change 8 | - [ ] Bug fix (non-breaking change which fixes an issue) 9 | - [ ] New feature (non-breaking change which adds functionality) 10 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 11 | - [ ] This change requires a documentation update 12 | 13 | # How Has This Been Tested? 14 | Please describe the tests that you ran to verify your changes. 15 | Provide instructions so we can reproduce. 16 | - [ ] Test A 17 | - [ ] Test B 18 | -------------------------------------------------------------------------------- /.github/workflows/build_flatpak.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: [main] 4 | pull_request: 5 | name: Build Flatpak 6 | jobs: 7 | flatpak: 8 | name: "build-packages" 9 | runs-on: ubuntu-latest 10 | container: 11 | image: ghcr.io/flathub-infra/flatpak-github-actions:gnome-48 12 | options: --privileged 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: flathub-infra/flatpak-github-actions/flatpak-builder@master 16 | with: 17 | bundle: bottles.flatpak 18 | manifest-path: build-aux/com.usebottles.bottles.Devel.json 19 | cache-key: flatpak-builder-${{ github.sha }} 20 | -------------------------------------------------------------------------------- /.github/workflows/close-issues.yml: -------------------------------------------------------------------------------- 1 | name: close-issues 2 | 3 | on: 4 | issues: 5 | types: [opened] 6 | 7 | jobs: 8 | comment: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions-ecosystem/action-regex-match@v2 12 | id: regex-match 13 | with: 14 | text: ${{ github.event.issue.body }} 15 | regex: '[Vv]ersion.*:.*202\d.\d\d?.\d\d?' 16 | - if: ${{ steps.regex-match.outputs.match != '' }} 17 | name: Close Issue 18 | uses: peter-evans/close-issue@v3 19 | with: 20 | close-reason: not_planned 21 | comment: | 22 | It seems like you're using an old version of Bottles. Please upgrade to the version from Flathub [here](https://flathub.org/apps/details/com.usebottles.bottles), and try to reproduce the bug. 23 | -------------------------------------------------------------------------------- /.github/workflows/pre-commit.yml: -------------------------------------------------------------------------------- 1 | name: pre-commit 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: [main] 7 | 8 | jobs: 9 | pre-commit: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | - uses: actions/setup-python@v3 14 | - uses: pre-commit/action@v3.0.1 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | .mypy_cache/ 3 | .pytest_cache/ 4 | /.project 5 | /.pydevproject 6 | /.settings 7 | /.cproject 8 | /.idea 9 | .flatpak-builder/ 10 | /build 11 | /build-dir 12 | /mesonbuild 13 | 14 | __pycache__ 15 | .coverage 16 | /install dir 17 | /work area 18 | 19 | /meson-test-run.txt 20 | /meson-test-run.xml 21 | /meson-cross-test-run.txt 22 | /meson-cross-test-run.xml 23 | 24 | /.flatpak 25 | /builddir 26 | 27 | .DS_Store 28 | *~ 29 | *.swp 30 | packagecache 31 | /MANIFEST 32 | /dist 33 | /meson.egg-info 34 | 35 | /docs/built_docs 36 | /docs/hotdoc-private* 37 | 38 | *.pyc 39 | /*venv* 40 | 41 | .buildconfig 42 | 43 | # Ignore AppImage build dirs 44 | /AppDir 45 | /appimage-builder-cache 46 | 47 | # Ignore generated files 48 | *.deb 49 | *.dsc 50 | *.changes 51 | .build 52 | *.buildinfo 53 | *.tar.gz 54 | 55 | 56 | # Ignore files generated during build 57 | *debian/files 58 | *debian/.* 59 | *debian/com.usebottles.bottles* 60 | *obj-x86_64-linux-gnu 61 | 62 | # Ignore flatpak build dirs 63 | /repo/ 64 | /flatpak/ 65 | .vscode/* 66 | /.vscode/* 67 | 68 | /build-flatpak.sh 69 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "build-aux/req2flatpak"] 2 | path = build-aux/req2flatpak 3 | url = https://github.com/johannesjh/req2flatpak.git 4 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v5.0.0 6 | hooks: 7 | - id: trailing-whitespace 8 | - id: end-of-file-fixer 9 | - id: check-yaml 10 | - id: check-xml 11 | - id: check-json 12 | - id: pretty-format-json 13 | args: ["--autofix", "--no-sort-keys", "--indent", "4"] 14 | - id: check-added-large-files 15 | 16 | - repo: https://github.com/asottile/pyupgrade 17 | rev: v3.19.1 18 | hooks: 19 | - id: pyupgrade 20 | args: ["--py312-plus"] 21 | 22 | - repo: https://github.com/astral-sh/ruff-pre-commit 23 | rev: v0.8.2 24 | hooks: 25 | - id: ruff 26 | args: [ "--fix" ] 27 | - id: ruff-format 28 | 29 | - repo: https://github.com/PyCQA/autoflake 30 | rev: v2.3.1 31 | hooks: 32 | - id: autoflake 33 | 34 | - repo: https://github.com/pre-commit/mirrors-mypy 35 | rev: v1.13.0 36 | hooks: 37 | - id: mypy 38 | args: ["--pretty"] 39 | additional_dependencies: ["pygobject-stubs", "types-PyYAML", "types-Markdown", "types-requests", "types-pycurl", "types-chardet", "pytest-stub", "types-orjson", "pathvalidate", "requirements-parser", "icoextract", "fvs", "patool", "pyfluidsynth", "git+https://gitlab.com/TheEvilSkeleton/vkbasalt-cli.git@main"] 40 | -------------------------------------------------------------------------------- /CODING_GUIDE.md: -------------------------------------------------------------------------------- 1 | ## Build & Run locally 2 | 3 | ### use flatpak 4 | 5 | #### Build & install 6 | 7 | ```bash 8 | flatpak-builder --install --user --force-clean ./.flatpak-builder/out ./build-aux/com.usebottles.bottles.Devel.json 9 | ``` 10 | 11 | #### Run 12 | 13 | ```bash 14 | flatpak run com.usebottles.bottles.Devel 15 | ``` 16 | 17 | #### Uninstall devel version 18 | 19 | ```bash 20 | flatpak uninstall com.usebottles.bottles.Devel 21 | ``` 22 | 23 | ## Unit Test 24 | 25 | ### run all tests 26 | 27 | ```bash 28 | pytest . 29 | ``` 30 | 31 | ## Dependencies 32 | 33 | Regenerate PYPI dependency manifest when requirements.txt changed 34 | 35 | ```bash 36 | python ./build-aux/req2flatpak/req2flatpak.py --requirements-file requirements.txt --yaml --target-platforms 312-x86_64 -o build-aux/pypi-deps.yaml 37 | ``` 38 | 39 | ## I18n files 40 | 41 | ### `po/POTFILES` 42 | 43 | List of source files containing translatable strings. 44 | Regenerate this file when you added/moved/removed/renamed files 45 | that contains translatable strings. 46 | 47 | ```bash 48 | cat > po/POTFILES <> po/POTFILES 53 | cat >> po/POTFILES < dict: 15 | """ 16 | Hash (SHA-1) all files in a directory and return 17 | them in a dictionary. Here we use SHA-1 instead of 18 | better ones like SHA-256 because we only need to 19 | compare the file hashes, it's faster, and it's 20 | not a security risk. 21 | """ 22 | _files = {} 23 | 24 | if path[-1] != os.sep: 25 | """ 26 | Be sure to add a trailing slash at the end of the path to 27 | prevent the correct path name in the result. 28 | """ 29 | path += os.sep 30 | 31 | for root, dirs, files in os.walk(path): 32 | dirs[:] = [d for d in dirs if d not in Diff.__ignored] 33 | for f in files: 34 | if f in Diff.__ignored: 35 | continue 36 | with open(os.path.join(root, f), "rb") as fr: 37 | _hash = hashlib.sha1(fr.read()).hexdigest() 38 | 39 | _key = os.path.join(root, f) 40 | _key = _key.replace(path, "") 41 | _files[_key] = _hash 42 | 43 | return _files 44 | 45 | @staticmethod 46 | def file_hashify(path: str) -> str: 47 | """Hash (SHA-1) a file and return it.""" 48 | with open(path, "rb") as fr: 49 | _hash = hashlib.sha1(fr.read()).hexdigest() 50 | 51 | return _hash 52 | 53 | @staticmethod 54 | def compare(parent: dict, child: dict) -> dict: 55 | """ 56 | Compare two hashes dictionaries and return the 57 | differences (added, removed, changed). 58 | """ 59 | 60 | added = [] 61 | changed = [] 62 | removed = [f for f in parent if f not in child] 63 | 64 | for f in child: 65 | if f not in parent: 66 | added.append(f) 67 | elif parent[f] != child[f]: 68 | changed.append(f) 69 | 70 | return {"added": added, "removed": removed, "changed": changed} 71 | -------------------------------------------------------------------------------- /bottles/backend/dlls/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bottlesdevs/Bottles/c85edf809a91cd84c82e1aa0548827d691b07a74/bottles/backend/dlls/__init__.py -------------------------------------------------------------------------------- /bottles/backend/dlls/dxvk.py: -------------------------------------------------------------------------------- 1 | # dxvk.py 2 | # 3 | # Copyright 2022 brombinmirko 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, in version 3 of the License. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | # 17 | 18 | from bottles.backend.dlls.dll import DLLComponent 19 | from bottles.backend.utils.manager import ManagerUtils 20 | 21 | 22 | class DXVKComponent(DLLComponent): 23 | dlls = { 24 | "x32": ["d3d8.dll", "d3d9.dll", "d3d10core.dll", "d3d11.dll", "dxgi.dll"], 25 | "x64": ["d3d8.dll", "d3d9.dll", "d3d10core.dll", "d3d11.dll", "dxgi.dll"], 26 | } 27 | 28 | @staticmethod 29 | def get_override_keys() -> str: 30 | return "d3d8,d3d9,d3d10core,d3d11,dxgi" 31 | 32 | @staticmethod 33 | def get_base_path(version: str) -> str: 34 | return ManagerUtils.get_dxvk_path(version) 35 | -------------------------------------------------------------------------------- /bottles/backend/dlls/latencyflex.py: -------------------------------------------------------------------------------- 1 | # dxvk.py 2 | # 3 | # Copyright 2022 brombinmirko 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, in version 3 of the License. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | # 17 | 18 | from bottles.backend.dlls.dll import DLLComponent 19 | from bottles.backend.utils.manager import ManagerUtils 20 | 21 | 22 | class LatencyFleXComponent(DLLComponent): 23 | dlls = { 24 | "wine/usr/lib/wine/x86_64-windows": [ 25 | "latencyflex_layer.dll", 26 | "latencyflex_wine.dll", 27 | ] 28 | } 29 | 30 | @staticmethod 31 | def get_override_keys() -> str: 32 | return "latencyflex_layer,latencyflex_wine" 33 | 34 | @staticmethod 35 | def get_base_path(version: str) -> str: 36 | return ManagerUtils.get_latencyflex_path(version) 37 | -------------------------------------------------------------------------------- /bottles/backend/dlls/meson.build: -------------------------------------------------------------------------------- 1 | pkgdatadir = join_paths(get_option('prefix'), get_option('datadir'), meson.project_name()) 2 | dllsdir = join_paths(pkgdatadir, 'bottles/backend/dlls') 3 | 4 | bottles_sources = [ 5 | '__init__.py', 6 | 'dll.py', 7 | 'dxvk.py', 8 | 'vkd3d.py', 9 | 'nvapi.py', 10 | 'latencyflex.py', 11 | ] 12 | 13 | install_data(bottles_sources, install_dir: dllsdir) 14 | -------------------------------------------------------------------------------- /bottles/backend/dlls/vkd3d.py: -------------------------------------------------------------------------------- 1 | # vkd3d.py 2 | # 3 | # Copyright 2022 brombinmirko 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, in version 3 of the License. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | # 17 | 18 | from bottles.backend.dlls.dll import DLLComponent 19 | from bottles.backend.utils.manager import ManagerUtils 20 | 21 | 22 | class VKD3DComponent(DLLComponent): 23 | dlls = { 24 | "x86": ["d3d12.dll", "d3d12core.dll"], 25 | "x64": ["d3d12.dll", "d3d12core.dll"], 26 | } 27 | 28 | @staticmethod 29 | def get_override_keys() -> str: 30 | return "d3d12,d3d12core" 31 | 32 | @staticmethod 33 | def get_base_path(version: str) -> str: 34 | return ManagerUtils.get_vkd3d_path(version) 35 | -------------------------------------------------------------------------------- /bottles/backend/managers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bottlesdevs/Bottles/c85edf809a91cd84c82e1aa0548827d691b07a74/bottles/backend/managers/__init__.py -------------------------------------------------------------------------------- /bottles/backend/managers/meson.build: -------------------------------------------------------------------------------- 1 | pkgdatadir = join_paths(get_option('prefix'), get_option('datadir'), meson.project_name()) 2 | managersdir = join_paths(pkgdatadir, 'bottles/backend/managers') 3 | 4 | bottles_sources = [ 5 | '__init__.py', 6 | 'backup.py', 7 | 'component.py', 8 | 'dependency.py', 9 | 'installer.py', 10 | 'library.py', 11 | 'manager.py', 12 | 'versioning.py', 13 | 'data.py', 14 | 'runtime.py', 15 | 'importer.py', 16 | 'conf.py', 17 | 'journal.py', 18 | 'repository.py', 19 | 'template.py', 20 | 'sandbox.py', 21 | 'steam.py', 22 | 'epicgamesstore.py', 23 | 'ubisoftconnect.py', 24 | 'origin.py', 25 | 'queue.py', 26 | 'steamgriddb.py', 27 | 'thumbnail.py' 28 | ] 29 | 30 | install_data(bottles_sources, install_dir: managersdir) 31 | -------------------------------------------------------------------------------- /bottles/backend/managers/origin.py: -------------------------------------------------------------------------------- 1 | # origin.py 2 | # 3 | # Copyright 2022 brombinmirko 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, in version 3 of the License. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | # 17 | 18 | import os 19 | 20 | from bottles.backend.models.config import BottleConfig 21 | from bottles.backend.utils.manager import ManagerUtils 22 | 23 | 24 | class OriginManager: 25 | @staticmethod 26 | def find_manifests_path(config: BottleConfig) -> str | None: 27 | """ 28 | Finds the Origin manifests path. 29 | """ 30 | paths = [ 31 | os.path.join( 32 | ManagerUtils.get_bottle_path(config), 33 | "drive_c/ProgramData/Origin/LocalContent", 34 | ) 35 | ] 36 | 37 | for path in paths: 38 | if os.path.exists(path): 39 | return path 40 | return None 41 | 42 | @staticmethod 43 | def is_origin_supported(config: BottleConfig) -> bool: 44 | """ 45 | Checks if Origin is supported. 46 | """ 47 | return OriginManager.find_manifests_path(config) is not None 48 | 49 | @staticmethod 50 | def get_installed_games(config: BottleConfig) -> list: 51 | """ 52 | Gets the games. 53 | """ 54 | games = [] 55 | return games 56 | -------------------------------------------------------------------------------- /bottles/backend/managers/queue.py: -------------------------------------------------------------------------------- 1 | # queue.py 2 | # 3 | # Copyright 2022 brombinmirko 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, in version 3 of the License. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | # 17 | 18 | 19 | class QueueManager: 20 | __queue = 0 21 | 22 | def __init__(self, end_fn, add_fn=None): 23 | self.__add_fn = add_fn 24 | self.__end_fn = end_fn 25 | 26 | def add_task(self): 27 | self.__queue += 1 28 | if self.__add_fn and self.__queue == 1: 29 | self.__add_fn() 30 | 31 | def end_task(self): 32 | self.__queue -= 1 33 | if self.__queue <= 0: 34 | self.__end_fn() 35 | -------------------------------------------------------------------------------- /bottles/backend/managers/steamgriddb.py: -------------------------------------------------------------------------------- 1 | # steamgriddb.py 2 | # 3 | # Copyright 2022 brombinmirko 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, in version 3 of the License. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | # 17 | 18 | import os 19 | import uuid 20 | import requests 21 | 22 | from bottles.backend.logger import Logger 23 | from bottles.backend.models.config import BottleConfig 24 | from bottles.backend.utils.manager import ManagerUtils 25 | 26 | logging = Logger() 27 | 28 | 29 | class SteamGridDBManager: 30 | @staticmethod 31 | def get_game_grid(name: str, config: BottleConfig): 32 | try: 33 | res = requests.get(f"https://steamgrid.usebottles.com/api/search/{name}") 34 | except: 35 | return 36 | 37 | if res.status_code == 200: 38 | return SteamGridDBManager.__save_grid(res.json(), config) 39 | 40 | @staticmethod 41 | def __save_grid(url: str, config: BottleConfig): 42 | grids_path = os.path.join(ManagerUtils.get_bottle_path(config), "grids") 43 | if not os.path.exists(grids_path): 44 | os.makedirs(grids_path) 45 | 46 | ext = url.split(".")[-1] 47 | filename = str(uuid.uuid4()) + "." + ext 48 | path = os.path.join(grids_path, filename) 49 | 50 | try: 51 | r = requests.get(url) 52 | with open(path, "wb") as f: 53 | f.write(r.content) 54 | except Exception: 55 | return 56 | 57 | return f"grid:{filename}" 58 | -------------------------------------------------------------------------------- /bottles/backend/managers/thumbnail.py: -------------------------------------------------------------------------------- 1 | # thumbnail.py 2 | # 3 | # Copyright 2022 brombinmirko 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, in version 3 of the License. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | # 17 | 18 | import os 19 | 20 | from bottles.backend.logger import Logger 21 | from bottles.backend.models.config import BottleConfig 22 | from bottles.backend.utils.manager import ManagerUtils 23 | 24 | logging = Logger() 25 | 26 | 27 | class ThumbnailManager: 28 | @staticmethod 29 | def get_path(config: BottleConfig, uri: str): 30 | if uri.startswith("grid:"): 31 | return ThumbnailManager.__load_grid(config, uri) 32 | # elif uri.startswith("epic:"): 33 | # return ThumbnailManager.__load_epic(config, uri) 34 | # elif uri.startswith("origin:"): 35 | # return ThumbnailManager.__load_origin(config, uri) 36 | logging.error("Unknown URI: " + uri) 37 | return None 38 | 39 | @staticmethod 40 | def __load_grid(config: BottleConfig, uri: str): 41 | bottle_path = ManagerUtils.get_bottle_path(config) 42 | file_name = uri[5:] 43 | path = os.path.join(bottle_path, "grids", file_name) 44 | 45 | if not os.path.exists(path): 46 | logging.error("Grid not found: " + path) 47 | return None 48 | 49 | return path 50 | -------------------------------------------------------------------------------- /bottles/backend/meson.build: -------------------------------------------------------------------------------- 1 | pkgdatadir = join_paths(get_option('prefix'), get_option('datadir'), meson.project_name()) 2 | backenddir = join_paths(pkgdatadir, 'bottles/backend') 3 | 4 | params_file = configure_file( 5 | input: 'params.py', 6 | output: 'params.py', 7 | configuration: conf 8 | ) 9 | 10 | subdir('wine') 11 | subdir('models') 12 | subdir('utils') 13 | subdir('dlls') 14 | subdir('repos') 15 | subdir('managers') 16 | 17 | bottles_sources = [ 18 | '__init__.py', 19 | 'globals.py', 20 | 'runner.py', 21 | 'diff.py', 22 | 'health.py', 23 | 'downloader.py', 24 | 'logger.py', 25 | 'cabextract.py', 26 | 'state.py', 27 | params_file 28 | ] 29 | 30 | install_data(bottles_sources, install_dir: backenddir) 31 | -------------------------------------------------------------------------------- /bottles/backend/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bottlesdevs/Bottles/c85edf809a91cd84c82e1aa0548827d691b07a74/bottles/backend/models/__init__.py -------------------------------------------------------------------------------- /bottles/backend/models/enum.py: -------------------------------------------------------------------------------- 1 | class Arch: 2 | WIN32 = "win32" 3 | WIN64 = "win64" 4 | -------------------------------------------------------------------------------- /bottles/backend/models/meson.build: -------------------------------------------------------------------------------- 1 | pkgdatadir = join_paths(get_option('prefix'), get_option('datadir'), meson.project_name()) 2 | modelsdir = join_paths(pkgdatadir, 'bottles/backend/models') 3 | 4 | bottles_sources = [ 5 | '__init__.py', 6 | 'result.py', 7 | 'samples.py', 8 | 'vdict.py', 9 | 'config.py', 10 | 'enum.py', 11 | ] 12 | 13 | install_data(bottles_sources, install_dir: modelsdir) 14 | -------------------------------------------------------------------------------- /bottles/backend/models/result.py: -------------------------------------------------------------------------------- 1 | # result.py 2 | # 3 | # Copyright 2022 brombinmirko 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, in version 3 of the License. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | # 17 | from typing import TypeVar, Generic 18 | 19 | T = TypeVar("T") 20 | 21 | 22 | class Result(Generic[T]): 23 | """ 24 | The Result object is the standard return object for every 25 | method in the backend. It is important to use this object 26 | to keep the code clean and consistent. 27 | """ 28 | 29 | status: bool = False 30 | data: T = None 31 | message: str = "" 32 | 33 | def __init__(self, status: bool = False, data: T = None, message: str = ""): 34 | self.status = status 35 | self.data = data 36 | self.message = message 37 | 38 | def set_status(self, v: bool): 39 | self.status = v 40 | 41 | @property 42 | def ok(self): 43 | return self.status 44 | 45 | @property 46 | def has_data(self): 47 | return bool(self.data) 48 | 49 | @property 50 | def ready(self): 51 | return self.ok and self.has_data 52 | -------------------------------------------------------------------------------- /bottles/backend/models/samples.py: -------------------------------------------------------------------------------- 1 | class Samples: 2 | data = {} 3 | environments = { 4 | "gaming": { 5 | "Runner": "wine", 6 | "Parameters": { 7 | "dxvk": True, 8 | # "nvapi": True, 9 | "vkd3d": True, 10 | "sync": "fsync", 11 | "fsr": False, 12 | "discrete_gpu": True, 13 | "pulseaudio_latency": False, 14 | }, 15 | "Installed_Dependencies": [ 16 | "d3dx9", 17 | "msls31", 18 | "arial32", 19 | "times32", 20 | "courie32", 21 | "d3dcompiler_43", 22 | "d3dcompiler_47", 23 | "mono", 24 | "gecko", 25 | ], 26 | }, 27 | "application": { 28 | "Runner": "wine", 29 | "Parameters": {"dxvk": True, "vkd3d": True, "pulseaudio_latency": False}, 30 | "Installed_Dependencies": [ 31 | "arial32", 32 | "times32", 33 | "courie32", 34 | "mono", 35 | "gecko", 36 | # "dotnet40", 37 | # "dotnet48" 38 | ], 39 | }, 40 | } 41 | bottles_to_steam_relations = { 42 | "MANGOHUD": ("mangohud", True), 43 | "OBS_VKCAPTURE": ("obsvkc", True), 44 | "ENABLE_VKBASALT": ("vkbasalt", True), 45 | "WINEESYNC": ("sync", "esync"), 46 | "WINEFSYNC": ("sync", "fsync"), 47 | "WINE_FULLSCREEN_FSR": ("fsr", True), 48 | "WINE_FULLSCREEN_FSR_STRENGTH": ("fsr_sharpening_strength", 2), 49 | "WINE_FULLSCREEN_FSR_MODE": ("fsr_quality_mode", "none"), 50 | "GAMESCOPE": ("gamescope", False), 51 | "DRI_PRIME": ("discrete_gpu", True), 52 | "__NV_PRIME_RENDER_OFFLOAD": ("discrete_gpu", True), 53 | "PULSE_LATENCY_MSEC": ("pulseaudio_latency", True), 54 | "PROTON_EAC_RUNTIME": ("use_eac_runtime", True), 55 | "PROTON_BATTLEYE_RUNTIME": ("use_be_runtime", True), 56 | } 57 | -------------------------------------------------------------------------------- /bottles/backend/params.py: -------------------------------------------------------------------------------- 1 | APP_VERSION = "@APP_VERSION@" 2 | BASE_ID = "@BASE_ID@" 3 | APP_ID = "@APP_ID@" 4 | -------------------------------------------------------------------------------- /bottles/backend/repos/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bottlesdevs/Bottles/c85edf809a91cd84c82e1aa0548827d691b07a74/bottles/backend/repos/__init__.py -------------------------------------------------------------------------------- /bottles/backend/repos/component.py: -------------------------------------------------------------------------------- 1 | # component.py 2 | # 3 | # Copyright 2022 brombinmirko 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, in version 3 of the License. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | # 17 | 18 | from bottles.backend.repos.repo import Repo 19 | 20 | 21 | class ComponentRepo(Repo): 22 | name = "components" 23 | 24 | def get(self, name: str, plain: bool = False) -> str | dict | bool: 25 | if name in self.catalog: 26 | entry = self.catalog[name] 27 | category = entry["Category"] 28 | subcategory = entry.get("Sub-category") 29 | 30 | if subcategory: 31 | url = f"{self.url}/{category}/{subcategory}/{name}.yml" 32 | else: 33 | url = f"{self.url}/{category}/{name}.yml" 34 | 35 | return self.get_manifest(url, plain) 36 | return False 37 | -------------------------------------------------------------------------------- /bottles/backend/repos/dependency.py: -------------------------------------------------------------------------------- 1 | # dependency.py 2 | # 3 | # Copyright 2022 brombinmirko 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, in version 3 of the License. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | # 17 | 18 | from bottles.backend.repos.repo import Repo 19 | 20 | 21 | class DependencyRepo(Repo): 22 | name = "dependencies" 23 | 24 | def get(self, name: str, plain: bool = False) -> str | dict | bool: 25 | if name in self.catalog: 26 | entry = self.catalog[name] 27 | url = f"{self.url}/{entry['Category']}/{name}.yml" 28 | return self.get_manifest(url, plain) 29 | return False 30 | -------------------------------------------------------------------------------- /bottles/backend/repos/installer.py: -------------------------------------------------------------------------------- 1 | # installer.py 2 | # 3 | # Copyright 2022 brombinmirko 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, in version 3 of the License. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | # 17 | 18 | from bottles.backend.repos.repo import Repo 19 | 20 | 21 | class InstallerRepo(Repo): 22 | name = "installers" 23 | 24 | def get(self, name: str, plain: bool = False) -> str | dict | bool: 25 | if name in self.catalog: 26 | entry = self.catalog[name] 27 | url = f"{self.url}/{entry['Category']}/{name}.yml" 28 | return self.get_manifest(url, plain) 29 | return False 30 | 31 | def get_review(self, name: str) -> str | dict | bool: 32 | if name in self.catalog: 33 | return self.get_manifest(f"{self.url}/Reviews/{name}.md", plain=True) 34 | return False 35 | 36 | def get_icon(self, name: str) -> str | bytes | None: 37 | if name in self.catalog: 38 | entry = self.catalog[name] 39 | icon = entry.get("Icon") 40 | if icon: 41 | return f"{self.url}/data/{name}/{icon}" 42 | return None 43 | -------------------------------------------------------------------------------- /bottles/backend/repos/meson.build: -------------------------------------------------------------------------------- 1 | pkgdatadir = join_paths(get_option('prefix'), get_option('datadir'), meson.project_name()) 2 | reposdir = join_paths(pkgdatadir, 'bottles/backend/repos') 3 | 4 | bottles_sources = [ 5 | '__init__.py', 6 | 'repo.py', 7 | 'dependency.py', 8 | 'component.py', 9 | 'installer.py', 10 | ] 11 | 12 | install_data(bottles_sources, install_dir: reposdir) 13 | -------------------------------------------------------------------------------- /bottles/backend/repos/repo.py: -------------------------------------------------------------------------------- 1 | # repo.py 2 | # 3 | # Copyright 2022 brombinmirko 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, in version 3 of the License. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | # 17 | 18 | from io import BytesIO 19 | 20 | import pycurl 21 | 22 | from bottles.backend.logger import Logger 23 | from bottles.backend.state import EventManager, Events 24 | from bottles.backend.utils import yaml 25 | from bottles.backend.utils.threading import RunAsync 26 | 27 | logging = Logger() 28 | 29 | 30 | class Repo: 31 | name: str = "" 32 | 33 | def __init__(self, url: str, index: str, offline: bool = False): 34 | self.url = url 35 | self.catalog = None 36 | 37 | def set_catalog(result, error=None): 38 | self.catalog = result 39 | EventManager.done(Events(self.name + ".fetching")) 40 | 41 | RunAsync(self.__get_catalog, callback=set_catalog, index=index, offline=offline) 42 | 43 | def __get_catalog(self, index: str, offline: bool = False): 44 | if index in ["", None] or offline: 45 | return {} 46 | 47 | try: 48 | buffer = BytesIO() 49 | 50 | c = pycurl.Curl() 51 | c.setopt(c.URL, index) 52 | c.setopt(c.FOLLOWLOCATION, True) 53 | c.setopt(c.WRITEDATA, buffer) 54 | c.perform() 55 | c.close() 56 | 57 | index = yaml.load(buffer.getvalue()) 58 | logging.info(f"Catalog {self.name} loaded") 59 | 60 | return index 61 | except (pycurl.error, yaml.YAMLError): 62 | logging.error(f"Cannot fetch {self.name} repository index.") 63 | return {} 64 | 65 | def get_manifest(self, url: str, plain: bool = False) -> str | dict | bool: 66 | try: 67 | buffer = BytesIO() 68 | 69 | c = pycurl.Curl() 70 | c.setopt(c.URL, url) 71 | c.setopt(c.FOLLOWLOCATION, True) 72 | c.setopt(c.WRITEDATA, buffer) 73 | c.perform() 74 | c.close() 75 | 76 | res = buffer.getvalue() 77 | 78 | if plain: 79 | return res.decode("utf-8") 80 | 81 | return yaml.load(res) 82 | except (pycurl.error, yaml.YAMLError): 83 | logging.error(f"Cannot fetch {self.name} manifest.") 84 | return False 85 | -------------------------------------------------------------------------------- /bottles/backend/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bottlesdevs/Bottles/c85edf809a91cd84c82e1aa0548827d691b07a74/bottles/backend/utils/__init__.py -------------------------------------------------------------------------------- /bottles/backend/utils/decorators.py: -------------------------------------------------------------------------------- 1 | # decorators.py 2 | # 3 | # Copyright 2022 brombinmirko 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, in version 3 of the License. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | # 17 | 18 | from functools import lru_cache, wraps 19 | from time import monotonic_ns 20 | 21 | 22 | def cache(_func=None, *, seconds: int = 600, maxsize: int = 128, typed: bool = False): 23 | """ 24 | Extension of functools lru_cache with a timeout 25 | 26 | Parameters: 27 | seconds (int): Timeout in seconds to clear the WHOLE cache, default = 10 minutes 28 | maxsize (int): Maximum Size of the Cache 29 | typed (bool): Same value of different type will be a different entry 30 | 31 | Source: 32 | """ 33 | 34 | def wrapper_cache(f): 35 | f = lru_cache(maxsize=maxsize, typed=typed)(f) 36 | f.delta = seconds * 10**9 37 | f.expiration = monotonic_ns() + f.delta 38 | 39 | @wraps(f) 40 | def wrapped_f(*args, **kwargs): 41 | if monotonic_ns() >= f.expiration: 42 | f.cache_clear() 43 | f.expiration = monotonic_ns() + f.delta 44 | return f(*args, **kwargs) 45 | 46 | wrapped_f.cache_info = f.cache_info 47 | wrapped_f.cache_clear = f.cache_clear 48 | return wrapped_f 49 | 50 | # To allow decorator to be used without arguments 51 | if _func is None: 52 | return wrapper_cache 53 | else: 54 | return wrapper_cache(_func) 55 | -------------------------------------------------------------------------------- /bottles/backend/utils/display.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | from functools import lru_cache 4 | 5 | 6 | class DisplayUtils: 7 | @staticmethod 8 | @lru_cache 9 | def get_x_display(): 10 | """Get the X display port.""" 11 | env_var = "DISPLAY" 12 | ports_range = range(3) 13 | 14 | if os.environ.get(env_var): 15 | return os.environ.get(env_var) 16 | 17 | for i in ports_range: 18 | _port = f":{i}" 19 | _proc = ( 20 | subprocess.Popen( 21 | f"xdpyinfo -display :{i}", 22 | stdout=subprocess.PIPE, 23 | stderr=subprocess.PIPE, 24 | shell=True, 25 | ) 26 | .communicate()[0] 27 | .decode("utf-8") 28 | .lower() 29 | ) 30 | if "x.org" in _proc: 31 | return _port 32 | 33 | return False 34 | 35 | @staticmethod 36 | def check_nvidia_device(): 37 | """Check if there is an nvidia device connected""" 38 | _query = "NVIDIA Corporation".lower() 39 | _proc = ( 40 | subprocess.Popen( 41 | "lspci | grep 'VGA'", 42 | stdout=subprocess.PIPE, 43 | stderr=subprocess.PIPE, 44 | shell=True, 45 | ) 46 | .communicate()[0] 47 | .decode("utf-8") 48 | .lower() 49 | ) 50 | 51 | if _query in _proc: 52 | return True 53 | return False 54 | 55 | @staticmethod 56 | def display_server_type(): 57 | """Return the display server type""" 58 | return os.environ.get("XDG_SESSION_TYPE", "x11").lower() 59 | -------------------------------------------------------------------------------- /bottles/backend/utils/gsettings_stub.py: -------------------------------------------------------------------------------- 1 | from bottles.backend.logger import Logger 2 | 3 | logging = Logger() 4 | 5 | 6 | class GSettingsStub: 7 | @staticmethod 8 | def get_boolean(key: str) -> bool: 9 | logging.warning(f"Stub GSettings key {key}=False") 10 | return False 11 | -------------------------------------------------------------------------------- /bottles/backend/utils/imagemagick.py: -------------------------------------------------------------------------------- 1 | # imagemagick.py 2 | # 3 | # Copyright 2022 brombinmirko 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, in version 3 of the License. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | # 17 | 18 | import os 19 | import subprocess 20 | 21 | 22 | class ImageMagickUtils: 23 | def __init__(self, path: str): 24 | self.path = path 25 | 26 | @staticmethod 27 | def __validate_path(path: str): 28 | if os.path.exists(path): 29 | return False 30 | if os.path.isdir(path): 31 | return False 32 | return True 33 | 34 | def list_assets(self): 35 | cmd = f"identify '{self.path}'" 36 | 37 | try: 38 | res = subprocess.check_output(["bash", "-c", cmd]) 39 | except: 40 | return [] 41 | 42 | assets = [] 43 | for r in res.decode().split("\n"): 44 | _r = r.replace(self.path, "").split() 45 | if len(_r) < 3: 46 | continue 47 | try: 48 | assets.append(int(_r[2].split("x")[0])) 49 | except ValueError: 50 | continue 51 | return assets 52 | 53 | def convert( 54 | self, 55 | dest: str, 56 | asset_size: int = 256, 57 | resize: int = 256, 58 | flatten: bool = True, 59 | alpha: bool = True, 60 | fallback: bool = True, 61 | ): 62 | if not self.__validate_path(dest): 63 | raise FileExistsError("Destination path already exists") 64 | 65 | assets = self.list_assets() 66 | asset_index = -1 67 | cmd = f"convert '{self.path}'" 68 | 69 | if asset_size not in assets: 70 | if not fallback: 71 | raise ValueError("Asset size not available") 72 | if len(assets) > 0: 73 | asset_size = max(assets) 74 | asset_index = assets.index(asset_size) 75 | else: 76 | asset_index = assets.index(asset_size) 77 | 78 | if asset_index != -1: 79 | cmd = f"convert '{self.path}[{asset_index}]'" 80 | if resize > 0: 81 | cmd += f" -thumbnail {resize}x{resize}" 82 | if alpha: 83 | cmd += " -alpha on -background none" 84 | if flatten: 85 | cmd += " -flatten" 86 | 87 | cmd += f" '{dest}'" 88 | subprocess.Popen(["bash", "-c", cmd]) 89 | -------------------------------------------------------------------------------- /bottles/backend/utils/json.py: -------------------------------------------------------------------------------- 1 | """This should be a drop-in replacement for the json module built in CPython""" 2 | 3 | import json 4 | import json as _json 5 | from typing import IO, Any 6 | 7 | from bottles.backend.models.config import DictCompatMixIn 8 | 9 | JSONDecodeError = json.JSONDecodeError 10 | 11 | 12 | class ExtJSONEncoder(_json.JSONEncoder): 13 | def default(self, o): 14 | if isinstance(o, DictCompatMixIn): 15 | return o.json_serialize_handler(o) 16 | return super().default(o) 17 | 18 | 19 | def load(fp: IO[str]) -> Any: 20 | """Deserialize fp (a .read()-supporting file-like object containing a JSON document) to a Python object.""" 21 | return _json.load(fp) 22 | 23 | 24 | def loads(s: str | bytes) -> Any: 25 | """Deserialize s (a str, bytes or bytearray instance containing a JSON document) to a Python object.""" 26 | return _json.loads(s) 27 | 28 | 29 | def dump( 30 | obj: Any, 31 | fp: IO[str], 32 | *, 33 | indent: str | int | None = None, 34 | cls: type[_json.JSONEncoder] | None = None, 35 | ) -> None: 36 | """ 37 | Serialize obj as a JSON formatted stream to fp (a .write()-supporting file-like object). 38 | 39 | :param obj: the object you want to serialize 40 | :param fp: the file-like object you want to write 41 | :param indent: `None` for compact output, `0` for newline only, non-negative integer for indent level 42 | :param cls: Custom JsonEncoder subclass, use ExtJsonEncoder if not provided 43 | """ 44 | if cls is None: # replace default JsonEncoder 45 | cls = ExtJSONEncoder 46 | return _json.dump(obj, fp, indent=indent, cls=cls) 47 | 48 | 49 | def dumps( 50 | obj: Any, 51 | *, 52 | indent: str | int | None = None, 53 | cls: type[_json.JSONEncoder] | None = None, 54 | ) -> str: 55 | """ 56 | Serialize obj to a JSON formatted str. 57 | 58 | :param obj: the object you want to serialize 59 | :param indent: `None` for compact output, `0` for newline only, non-negative integer for indent level 60 | :param cls: Custom JsonEncoder subclass, use ExtJsonEncoder if not provided 61 | :return: serialized result 62 | """ 63 | if cls is None: # replace default JsonEncoder 64 | cls = ExtJSONEncoder 65 | return _json.dumps(obj, indent=indent, cls=cls) 66 | -------------------------------------------------------------------------------- /bottles/backend/utils/lnk.py: -------------------------------------------------------------------------------- 1 | # lnk.py 2 | # 3 | # Copyright 2022 brombinmirko 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, in version 3 of the License. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | # 17 | 18 | import locale 19 | import struct 20 | from functools import lru_cache 21 | 22 | 23 | class LnkUtils: 24 | @staticmethod 25 | @lru_cache 26 | def get_data(path): 27 | """ 28 | Gets data from a .lnk file, and returns them in a dictionary. 29 | Thanks to @Winand and @Jared for the code. 30 | 31 | """ 32 | with open(path, "rb") as stream: 33 | content = stream.read() 34 | """ 35 | Skip first 20 bytes (HeaderSize and LinkCLSID) 36 | read the LinkFlags structure (4 bytes) 37 | """ 38 | lflags = struct.unpack("I", content[0x14:0x18])[0] 39 | position = 0x18 40 | 41 | if (lflags & 0x01) == 1: 42 | """ 43 | If the HasLinkTargetIDList bit is set then skip the stored IDList 44 | structure and header 45 | """ 46 | position = struct.unpack("H", content[0x4C:0x4E])[0] + 0x4E 47 | 48 | last_pos = position 49 | position += 0x04 50 | 51 | # get how long the file information is (LinkInfoSize) 52 | length = struct.unpack("I", content[last_pos:position])[0] 53 | 54 | """ 55 | Skip 12 bytes (LinkInfoHeaderSize, LinkInfoFlags and 56 | VolumeIDOffset) 57 | """ 58 | position += 0x0C 59 | 60 | # go to the LocalBasePath position 61 | lbpos = struct.unpack("I", content[position : position + 0x04])[0] 62 | position = last_pos + lbpos 63 | 64 | # read the string at the given position of the determined length 65 | size = (length + last_pos) - position - 0x02 66 | content = content[position : position + size].split(b"\x00", 1) 67 | 68 | decode = locale.getdefaultlocale()[1] 69 | if len(content) > 1 or decode is None: 70 | decode = "utf-16" 71 | 72 | try: 73 | return content[-1].decode(decode) 74 | except UnicodeDecodeError: 75 | return None 76 | -------------------------------------------------------------------------------- /bottles/backend/utils/meson.build: -------------------------------------------------------------------------------- 1 | pkgdatadir = join_paths(get_option('prefix'), get_option('datadir'), meson.project_name()) 2 | utilsdir = join_paths(pkgdatadir, 'bottles/backend/utils') 3 | 4 | bottles_sources = [ 5 | '__init__.py', 6 | 'display.py', 7 | 'gpu.py', 8 | 'manager.py', 9 | 'midi.py', 10 | 'vulkan.py', 11 | 'terminal.py', 12 | 'file.py', 13 | 'generic.py', 14 | 'wine.py', 15 | 'steam.py', 16 | 'lnk.py', 17 | 'decorators.py', 18 | 'snake.py', 19 | 'vdf.py', 20 | 'imagemagick.py', 21 | 'proc.py', 22 | 'yaml.py', 23 | 'nvidia.py', 24 | 'threading.py', 25 | 'connection.py', 26 | 'gsettings_stub.py', 27 | 'json.py', 28 | 'singleton.py' 29 | ] 30 | 31 | install_data(bottles_sources, install_dir: utilsdir) 32 | -------------------------------------------------------------------------------- /bottles/backend/utils/proc.py: -------------------------------------------------------------------------------- 1 | # proc.py 2 | # 3 | # Copyright 2022 brombinmirko 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, in version 3 of the License. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | # 17 | 18 | import os 19 | import subprocess 20 | 21 | 22 | class Proc: 23 | def __init__(self, pid): 24 | self.pid = pid 25 | 26 | def __get_data(self, data): 27 | try: 28 | with open(os.path.join("/proc", str(self.pid), data), "rb") as f: 29 | return f.read().decode("utf-8") 30 | except (FileNotFoundError, PermissionError): 31 | return "" 32 | 33 | def get_cmdline(self): 34 | return self.__get_data("cmdline") 35 | 36 | def get_env(self): 37 | return self.__get_data("environ") 38 | 39 | def get_cwd(self): 40 | return self.__get_data("cwd") 41 | 42 | def get_name(self): 43 | return self.__get_data("stat") 44 | 45 | def kill(self): 46 | subprocess.Popen( 47 | ["kill", str(self.pid)], 48 | stdout=subprocess.DEVNULL, 49 | stderr=subprocess.DEVNULL, 50 | ) 51 | 52 | 53 | class ProcUtils: 54 | @staticmethod 55 | def get_procs(): 56 | procs = [] 57 | for pid in os.listdir("/proc"): 58 | if pid.isdigit(): 59 | procs.append(Proc(pid)) 60 | return procs 61 | 62 | @staticmethod 63 | def get_by_cmdline(cmdline): 64 | _procs = ProcUtils.get_procs() 65 | return [proc for proc in _procs if cmdline in proc.get_cmdline()] 66 | 67 | @staticmethod 68 | def get_by_env(env): 69 | _procs = ProcUtils.get_procs() 70 | return [proc for proc in _procs if env in proc.get_env()] 71 | 72 | @staticmethod 73 | def get_by_cwd(cwd): 74 | _procs = ProcUtils.get_procs() 75 | return [proc for proc in _procs if cwd in proc.get_cwd()] 76 | 77 | @staticmethod 78 | def get_by_name(name): 79 | _procs = ProcUtils.get_procs() 80 | return [proc for proc in _procs if name in proc.get_name()] 81 | 82 | @staticmethod 83 | def get_by_pid(pid): 84 | return Proc(pid) 85 | -------------------------------------------------------------------------------- /bottles/backend/utils/singleton.py: -------------------------------------------------------------------------------- 1 | class Singleton(type): 2 | _instances = {} 3 | 4 | def __call__(cls, *args, **kwargs): 5 | if cls not in cls._instances: 6 | cls._instances[cls] = super().__call__(*args, **kwargs) 7 | return cls._instances[cls] 8 | -------------------------------------------------------------------------------- /bottles/backend/utils/vulkan.py: -------------------------------------------------------------------------------- 1 | # vulkan.py 2 | # 3 | # Copyright 2022 brombinmirko 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, in version 3 of the License. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | # 17 | 18 | import os 19 | from glob import glob 20 | import shutil 21 | import subprocess 22 | 23 | 24 | class VulkanUtils: 25 | __vk_icd_dirs = [ 26 | "/usr/share/vulkan", 27 | "/etc/vulkan", 28 | "/usr/local/share/vulkan", 29 | "/usr/local/etc/vulkan", 30 | ] 31 | if "FLATPAK_ID" in os.environ: 32 | __vk_icd_dirs += [ 33 | "/usr/lib/x86_64-linux-gnu/GL/vulkan", 34 | "/usr/lib/i386-linux-gnu/GL/vulkan", 35 | ] 36 | 37 | def __init__(self): 38 | self.loaders = self.__get_vk_icd_loaders() 39 | 40 | def __get_vk_icd_loaders(self): 41 | loaders = {"nvidia": [], "amd": [], "intel": []} 42 | 43 | for _dir in self.__vk_icd_dirs: 44 | _files = glob(f"{_dir}/icd.d/*.json", recursive=True) 45 | 46 | for file in _files: 47 | if "nvidia" in file.lower(): 48 | loaders["nvidia"] += [file] 49 | elif "amd" in file.lower() or "radeon" in file.lower(): 50 | loaders["amd"] += [file] 51 | elif "intel" in file.lower(): 52 | loaders["intel"] += [file] 53 | 54 | return loaders 55 | 56 | def get_vk_icd(self, vendor: str, as_string=False): 57 | vendors = ["nvidia", "amd", "intel"] 58 | icd = [] 59 | 60 | if vendor in vendors: 61 | icd = self.loaders[vendor] 62 | 63 | if as_string: 64 | icd = ":".join(icd) 65 | 66 | return icd 67 | 68 | @staticmethod 69 | def check_support(): 70 | return True 71 | 72 | @staticmethod 73 | def test_vulkan(): 74 | if shutil.which("vulkaninfo") is None: 75 | return "vulkaninfo tool not found" 76 | 77 | res = ( 78 | subprocess.Popen( 79 | "vulkaninfo", stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True 80 | ) 81 | .communicate()[0] 82 | .decode("utf-8") 83 | ) 84 | 85 | return res 86 | -------------------------------------------------------------------------------- /bottles/backend/utils/wine.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | class WineUtils: 5 | @staticmethod 6 | def get_user_dir(prefix_path: str): 7 | ignored = ["Public"] 8 | usersdir = os.path.join(prefix_path, "drive_c", "users") 9 | found = [] 10 | 11 | for user_dir in os.listdir(usersdir): 12 | if user_dir in ignored: 13 | continue 14 | found.append(user_dir) 15 | 16 | if len(found) == 0: 17 | raise Exception("No user directories found.") 18 | 19 | return found[0] 20 | -------------------------------------------------------------------------------- /bottles/backend/utils/yaml.py: -------------------------------------------------------------------------------- 1 | import yaml as _yaml 2 | 3 | from bottles.backend.models.config import BottleConfig 4 | 5 | try: 6 | from yaml import CSafeLoader as SafeLoader, CSafeDumper as SafeDumper 7 | except ImportError: 8 | from yaml import SafeLoader, SafeDumper 9 | 10 | YAMLError = _yaml.YAMLError 11 | SafeDumper.add_representer(BottleConfig, BottleConfig.yaml_serialize_handler) 12 | 13 | 14 | def dump(data, stream=None, **kwargs): 15 | """ 16 | Serialize a Python object into a YAML stream. 17 | If stream is None, return the produced string instead. 18 | Note: This function is a replacement for PyYAML's dump() function, 19 | using the CDumper class instead of the default Dumper, to achieve 20 | the best performance. 21 | """ 22 | return _yaml.dump(data, stream, Dumper=SafeDumper, **kwargs) 23 | 24 | 25 | # noinspection PyPep8Naming 26 | def load(stream, Loader=SafeLoader): 27 | """ 28 | Load a YAML stream. 29 | Note: This function is a replacement for PyYAML's safe_load() function, 30 | using the CLoader class instead of the default Loader, to achieve 31 | the best performance. 32 | """ 33 | return _yaml.load(stream, Loader=Loader) 34 | -------------------------------------------------------------------------------- /bottles/backend/wine/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bottlesdevs/Bottles/c85edf809a91cd84c82e1aa0548827d691b07a74/bottles/backend/wine/__init__.py -------------------------------------------------------------------------------- /bottles/backend/wine/cmd.py: -------------------------------------------------------------------------------- 1 | from bottles.backend.logger import Logger 2 | from bottles.backend.wine.wineprogram import WineProgram 3 | 4 | logging = Logger() 5 | 6 | 7 | class CMD(WineProgram): 8 | program = "Wine Command Line" 9 | command = "cmd" 10 | 11 | def run_batch( 12 | self, 13 | batch: str, 14 | terminal: bool = True, 15 | args: str = "", 16 | environment: dict | None = None, 17 | cwd: str | None = None, 18 | ): 19 | args = f"/c {batch} {args}" 20 | 21 | self.launch( 22 | args=args, 23 | communicate=True, 24 | terminal=terminal, 25 | environment=environment, 26 | cwd=cwd, 27 | action_name="run_batch", 28 | ) 29 | -------------------------------------------------------------------------------- /bottles/backend/wine/control.py: -------------------------------------------------------------------------------- 1 | from bottles.backend.logger import Logger 2 | from bottles.backend.wine.wineprogram import WineProgram 3 | 4 | logging = Logger() 5 | 6 | 7 | class Control(WineProgram): 8 | program = "Wine Control Panel" 9 | command = "control" 10 | 11 | def load_applet(self, name: str): 12 | args = name 13 | return self.launch(args=args, communicate=True, action_name="load_applet") 14 | 15 | def load_joystick(self): 16 | return self.load_applet("joy.cpl") 17 | 18 | def load_appwiz(self): 19 | return self.load_applet("appwiz.cpl") 20 | 21 | def load_inetcpl(self): 22 | return self.load_applet("inetcpl.cpl") 23 | -------------------------------------------------------------------------------- /bottles/backend/wine/drives.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from bottles.backend.logger import Logger 4 | from bottles.backend.models.config import BottleConfig 5 | from bottles.backend.utils.manager import ManagerUtils 6 | 7 | logging = Logger() 8 | 9 | 10 | class Drives: 11 | def __init__(self, config: BottleConfig): 12 | self.config = config 13 | bottle = ManagerUtils.get_bottle_path(self.config) 14 | self.dosdevices_path = os.path.join(bottle, "dosdevices") 15 | 16 | def get_all(self): 17 | """Get all the drives from the bottle""" 18 | drives = {} 19 | if os.path.exists(self.dosdevices_path): 20 | for drive in os.listdir(self.dosdevices_path): 21 | if os.path.islink(f"{self.dosdevices_path}/{drive}"): 22 | letter = os.path.basename(drive).replace(":", "").upper() 23 | if len(letter) == 1 and letter.isalpha(): 24 | path = os.readlink(f"{self.dosdevices_path}/{drive}") 25 | drives[letter] = path 26 | return drives 27 | 28 | def get_drive(self, letter: str): 29 | """Get a drive from the bottle""" 30 | if letter in self.get_all(): 31 | return self.get_all().get(letter) 32 | return None 33 | 34 | def set_drive_path(self, letter: str, path: str): 35 | """Change a drives path in the bottle""" 36 | letter = f"{letter}:".lower() 37 | drive_sym_path = os.path.join(self.dosdevices_path, letter) 38 | if not os.path.exists(self.dosdevices_path): 39 | os.makedirs(self.dosdevices_path) 40 | if not os.path.exists(drive_sym_path): 41 | os.symlink(path, drive_sym_path) 42 | logging.info(f"New drive {letter} added to the bottle") 43 | else: 44 | os.remove(drive_sym_path) 45 | os.symlink(path, drive_sym_path) 46 | logging.info(f"Drive {letter} path changed to {path}") 47 | 48 | def remove_drive(self, letter: str): 49 | """Remove a drive from the bottle""" 50 | if letter.upper() in self.get_all(): 51 | os.remove(f"{self.dosdevices_path}/{letter.lower()}:") 52 | logging.info(f"Drive {letter} removed from the bottle") 53 | -------------------------------------------------------------------------------- /bottles/backend/wine/eject.py: -------------------------------------------------------------------------------- 1 | from bottles.backend.logger import Logger 2 | from bottles.backend.wine.wineprogram import WineProgram 3 | 4 | logging = Logger() 5 | 6 | 7 | class Eject(WineProgram): 8 | program = "Wine Eject CLI" 9 | command = "eject" 10 | 11 | def cdrom(self, drive: str, unmount_only: bool = False): 12 | args = drive 13 | if unmount_only: 14 | args += " -u" 15 | return self.launch(args=args, communicate=True, action_name="cdrom") 16 | 17 | def all(self): 18 | args = "-a" 19 | return self.launch(args=args, communicate=True, action_name="all") 20 | -------------------------------------------------------------------------------- /bottles/backend/wine/expand.py: -------------------------------------------------------------------------------- 1 | from bottles.backend.logger import Logger 2 | from bottles.backend.wine.wineprogram import WineProgram 3 | 4 | logging = Logger() 5 | 6 | 7 | class Expand(WineProgram): 8 | program = "Wine cabinet expander" 9 | command = "expand" 10 | 11 | def extract(self, cabinet: str, filename: str): 12 | args = f"{cabinet} {filename}" 13 | return self.launch(args=args, communicate=True, action_name="extract") 14 | 15 | def extract_all(self, cabinet: str, filenames: list): 16 | for filename in filenames: 17 | self.extract(cabinet, filename) 18 | -------------------------------------------------------------------------------- /bottles/backend/wine/explorer.py: -------------------------------------------------------------------------------- 1 | from bottles.backend.logger import Logger 2 | from bottles.backend.wine.wineprogram import WineProgram 3 | 4 | logging = Logger() 5 | 6 | 7 | class Explorer(WineProgram): 8 | program = "Wine Explorer" 9 | command = "explorer" 10 | 11 | def launch_desktop( 12 | self, 13 | desktop: str = "shell", 14 | width: int = 0, 15 | height: int = 0, 16 | program: str | None = None, 17 | args: str | None = None, 18 | environment: dict | None = None, 19 | cwd: str | None = None, 20 | ): 21 | _args = f"/desktop={desktop}" 22 | 23 | if width and height: 24 | _args += f",{width}x{height}" 25 | if program: 26 | _args += f" {program}" 27 | if args: 28 | _args += args 29 | 30 | return self.launch( 31 | args=_args, 32 | communicate=True, 33 | action_name="launch_desktop", 34 | environment=environment, 35 | cwd=cwd, 36 | ) 37 | -------------------------------------------------------------------------------- /bottles/backend/wine/hh.py: -------------------------------------------------------------------------------- 1 | from bottles.backend.logger import Logger 2 | from bottles.backend.wine.wineprogram import WineProgram 3 | 4 | logging = Logger() 5 | 6 | 7 | class Hh(WineProgram): 8 | program = "Wine HTML help viewer" 9 | command = "hh" 10 | -------------------------------------------------------------------------------- /bottles/backend/wine/icinfo.py: -------------------------------------------------------------------------------- 1 | from bottles.backend.logger import Logger 2 | from bottles.backend.wine.wineprogram import WineProgram 3 | 4 | logging = Logger() 5 | 6 | 7 | class Icinfo(WineProgram): 8 | program = "List installed video compressors" 9 | command = "icinfo" 10 | 11 | def get_output(self): 12 | return self.launch(communicate=True, action_name="get_output") 13 | 14 | def get_dict(self): 15 | res = self.launch(communicate=True, action_name="get_dict") 16 | if not res.ready: 17 | return {} 18 | 19 | res = [r.strip() for r in res.split("\n")[1:]] 20 | _res = {} 21 | _latest = None 22 | 23 | for r in res: 24 | if not r: 25 | continue 26 | k, v = r.split(":") 27 | if r.startswith("vidc."): 28 | _latest = k 29 | _res[k] = {} 30 | _res[k]["name"] = k 31 | _res[k]["description"] = v 32 | else: 33 | _res[_latest][k] = v 34 | 35 | return _res 36 | -------------------------------------------------------------------------------- /bottles/backend/wine/meson.build: -------------------------------------------------------------------------------- 1 | pkgdatadir = join_paths(get_option('prefix'), get_option('datadir'), meson.project_name()) 2 | winedir = join_paths(pkgdatadir, 'bottles/backend/wine') 3 | 4 | bottles_sources = [ 5 | '__init__.py', 6 | 'catalogs.py', 7 | 'winecommand.py', 8 | 'wineprogram.py', 9 | 'uninstaller.py', 10 | 'winecfg.py', 11 | 'winedbg.py', 12 | 'wineserver.py', 13 | 'wineboot.py', 14 | 'winepath.py', 15 | 'cmd.py', 16 | 'taskmgr.py', 17 | 'control.py', 18 | 'regedit.py', 19 | 'reg.py', 20 | 'regkeys.py', 21 | 'net.py', 22 | 'msiexec.py', 23 | 'executor.py', 24 | 'start.py', 25 | 'register.py', 26 | 'regsvr32.py', 27 | 'winebridge.py', 28 | 'explorer.py', 29 | 'drives.py', 30 | 'eject.py', 31 | 'expand.py', 32 | 'hh.py', 33 | 'icinfo.py', 34 | 'notepad.py', 35 | 'oleview.py', 36 | 'progman.py', 37 | 'rundll32.py', 38 | 'winefile.py', 39 | 'winhelp.py', 40 | 'xcopy.py', 41 | ] 42 | 43 | install_data(bottles_sources, install_dir: winedir) 44 | -------------------------------------------------------------------------------- /bottles/backend/wine/net.py: -------------------------------------------------------------------------------- 1 | from bottles.backend.logger import Logger 2 | from bottles.backend.wine.wineprogram import WineProgram 3 | 4 | logging = Logger() 5 | 6 | 7 | class Net(WineProgram): 8 | program = "Wine Services manager" 9 | command = "net" 10 | 11 | def start(self, name: str | None = None): 12 | args = "start" 13 | 14 | if name is not None: 15 | args = f"start '{name}'" 16 | 17 | return self.launch(args=args, communicate=True, action_name="start") 18 | 19 | def stop(self, name: str | None = None): 20 | args = "stop" 21 | 22 | if name is not None: 23 | args = f"stop '{name}'" 24 | 25 | return self.launch(args=args, communicate=True, action_name="stop") 26 | 27 | def use(self, name: str | None = None): 28 | # this command has no documentation, not tested yet 29 | args = "use" 30 | 31 | if name is not None: 32 | args = f"use '{name}'" 33 | 34 | return self.launch(args=args, communicate=True, action_name="use") 35 | 36 | def list(self): 37 | services = [] 38 | res = self.start() 39 | 40 | if not res.ready: 41 | return services 42 | 43 | lines = res.data.strip().splitlines() 44 | for r in lines[1:]: 45 | r = r[4:] 46 | services.append(r) 47 | 48 | return services 49 | -------------------------------------------------------------------------------- /bottles/backend/wine/notepad.py: -------------------------------------------------------------------------------- 1 | from bottles.backend.logger import Logger 2 | from bottles.backend.wine.wineprogram import WineProgram 3 | 4 | logging = Logger() 5 | 6 | 7 | class Notepad(WineProgram): 8 | program = "Wine Notepad" 9 | command = "notepad" 10 | 11 | def open(self, path: str, as_ansi: bool = False, as_utf16: bool = False): 12 | args = path 13 | if as_ansi: 14 | args = f"/a {path}" 15 | elif as_utf16: 16 | args = f"/w {path}" 17 | return self.launch(args=args, communicate=True, action_name="open") 18 | 19 | def print(self, path: str, printer_name: str | None = None): 20 | args = f"/p {path}" 21 | if printer_name: 22 | args = f"/pt {path} {printer_name}" 23 | return self.launch(args=args, communicate=True, action_name="print") 24 | -------------------------------------------------------------------------------- /bottles/backend/wine/oleview.py: -------------------------------------------------------------------------------- 1 | from bottles.backend.logger import Logger 2 | from bottles.backend.wine.wineprogram import WineProgram 3 | 4 | logging = Logger() 5 | 6 | 7 | class Oleview(WineProgram): 8 | program = "OLE/COM object viewer" 9 | command = "oleview" 10 | -------------------------------------------------------------------------------- /bottles/backend/wine/progman.py: -------------------------------------------------------------------------------- 1 | from bottles.backend.logger import Logger 2 | from bottles.backend.wine.wineprogram import WineProgram 3 | 4 | logging = Logger() 5 | 6 | 7 | class Progman(WineProgram): 8 | program = "Wine Program Manager" 9 | command = "progman" 10 | -------------------------------------------------------------------------------- /bottles/backend/wine/regedit.py: -------------------------------------------------------------------------------- 1 | from bottles.backend.logger import Logger 2 | from bottles.backend.wine.wineprogram import WineProgram 3 | 4 | logging = Logger() 5 | 6 | 7 | class Regedit(WineProgram): 8 | program = "Wine Registry Editor" 9 | command = "regedit" 10 | -------------------------------------------------------------------------------- /bottles/backend/wine/regsvr32.py: -------------------------------------------------------------------------------- 1 | from bottles.backend.logger import Logger 2 | from bottles.backend.wine.wineprogram import WineProgram 3 | 4 | logging = Logger() 5 | 6 | 7 | class Regsvr32(WineProgram): 8 | program = "Wine DLL Registration Server" 9 | command = "regsvr32" 10 | 11 | def register(self, dll: str): 12 | args = f"/s {dll}" 13 | return self.launch(args=args, communicate=True, action_name="register") 14 | 15 | def unregister(self, dll: str): 16 | args = f"/s /u {dll}" 17 | return self.launch(args=args, communicate=True, action_name="unregister") 18 | 19 | def register_all(self, dlls: list): 20 | for dll in dlls: 21 | self.register(dll) 22 | 23 | def unregister_all(self, dlls: list): 24 | for dll in dlls: 25 | self.unregister(dll) 26 | -------------------------------------------------------------------------------- /bottles/backend/wine/rundll32.py: -------------------------------------------------------------------------------- 1 | from bottles.backend.logger import Logger 2 | from bottles.backend.wine.wineprogram import WineProgram 3 | 4 | logging = Logger() 5 | 6 | 7 | class RunDLL32(WineProgram): 8 | program = "32-bit DLLs loader and runner" 9 | command = "rundll32" 10 | -------------------------------------------------------------------------------- /bottles/backend/wine/start.py: -------------------------------------------------------------------------------- 1 | from bottles.backend.logger import Logger 2 | from bottles.backend.wine.wineprogram import WineProgram 3 | from bottles.backend.wine.winepath import WinePath 4 | 5 | logging = Logger() 6 | 7 | 8 | class Start(WineProgram): 9 | program = "Wine Starter" 10 | command = "start" 11 | 12 | def run( 13 | self, 14 | file: str, 15 | terminal: bool = True, 16 | args: str = "", 17 | environment: dict | None = None, 18 | pre_script: str | None = None, 19 | post_script: str | None = None, 20 | cwd: str | None = None, 21 | midi_soundfont: str | None = None, 22 | ): 23 | winepath = WinePath(self.config) 24 | 25 | if winepath.is_unix(file): 26 | # running unix paths with start is not recommended 27 | # as it can miss important files due to the wrong 28 | # current working directory 29 | _args = f"/unix /wait {file}" 30 | else: 31 | if cwd not in [None, ""] and winepath.is_windows(cwd): 32 | _args = f"/wait /dir {cwd} {file}" 33 | else: 34 | _args = f"/wait {file}" 35 | 36 | self.launch( 37 | args=(_args, args), 38 | communicate=True, 39 | terminal=terminal, 40 | environment=environment, 41 | pre_script=pre_script, 42 | post_script=post_script, 43 | cwd=cwd, 44 | midi_soundfont=midi_soundfont, 45 | minimal=False, 46 | action_name="run", 47 | ) 48 | -------------------------------------------------------------------------------- /bottles/backend/wine/taskmgr.py: -------------------------------------------------------------------------------- 1 | from bottles.backend.logger import Logger 2 | from bottles.backend.wine.wineprogram import WineProgram 3 | 4 | logging = Logger() 5 | 6 | 7 | class Taskmgr(WineProgram): 8 | program = "Wine Task Manager" 9 | command = "taskmgr" 10 | -------------------------------------------------------------------------------- /bottles/backend/wine/uninstaller.py: -------------------------------------------------------------------------------- 1 | from bottles.backend.logger import Logger 2 | from bottles.backend.wine.wineprogram import WineProgram 3 | 4 | logging = Logger() 5 | 6 | 7 | class Uninstaller(WineProgram): 8 | program = "Wine Uninstaller" 9 | command = "uninstaller" 10 | 11 | def get_uuid(self, name: str | None = None): 12 | args = " --list" 13 | 14 | if name is not None: 15 | args = f"--list | grep -i '{name}' | cut -f1 -d\\|" 16 | 17 | return self.launch(args=args, communicate=True, action_name="get_uuid") 18 | 19 | def from_uuid(self, uuid: str | None = None): 20 | args = "" 21 | 22 | if uuid not in [None, ""]: 23 | args = f"--remove {uuid}" 24 | 25 | return self.launch(args=args, action_name="from_uuid") 26 | 27 | def from_name(self, name: str): 28 | res = self.get_uuid(name) 29 | if not res.ready: 30 | """ 31 | No UUID found, at this point it is safe to assume that the 32 | program is not installed 33 | ref: 34 | """ 35 | return 36 | uuid = res.data.strip() 37 | for _uuid in uuid.splitlines(): 38 | self.from_uuid(_uuid) 39 | -------------------------------------------------------------------------------- /bottles/backend/wine/wineboot.py: -------------------------------------------------------------------------------- 1 | from bottles.backend.logger import Logger 2 | from bottles.backend.wine.wineprogram import WineProgram 3 | from bottles.backend.wine.wineserver import WineServer 4 | 5 | import os 6 | import signal 7 | 8 | logging = Logger() 9 | 10 | 11 | class WineBoot(WineProgram): 12 | program = "Wine Runtime tool" 13 | command = "wineboot" 14 | 15 | def send_status(self, status: int): 16 | if status == -2: 17 | return self.nv_stop_all_processes() 18 | 19 | states = {-1: "force", 0: "-k", 1: "-r", 2: "-s", 3: "-u", 4: "-i"} 20 | envs = { 21 | "WINEDEBUG": "-all", 22 | "DISPLAY": ":3.0", 23 | "WINEDLLOVERRIDES": "winemenubuilder=d", 24 | } 25 | 26 | if status == 0 and not WineServer(self.config).is_alive(): 27 | logging.info("There is no running wineserver.") 28 | return 29 | 30 | if status in states: 31 | args = f"{states[status]} /nogui" 32 | self.launch( 33 | args=args, 34 | environment=envs, 35 | communicate=True, 36 | action_name=f"send_status({states[status]})", 37 | ) 38 | else: 39 | raise ValueError(f"[{status}] is not a valid status for wineboot!") 40 | 41 | def force(self): 42 | return self.send_status(-1) 43 | 44 | def kill(self, force_if_stalled: bool = False): 45 | self.send_status(0) 46 | 47 | if force_if_stalled: 48 | wineserver = WineServer(self.config) 49 | if wineserver.is_alive(): 50 | wineserver.force_kill() 51 | wineserver.wait() 52 | 53 | def restart(self): 54 | return self.send_status(1) 55 | 56 | def shutdown(self): 57 | return self.send_status(2) 58 | 59 | def update(self): 60 | return self.send_status(3) 61 | 62 | def init(self): 63 | return self.send_status(4) 64 | 65 | def nv_stop_all_processes(self): 66 | try: 67 | for pid in os.listdir("/proc"): 68 | if pid.isdigit(): 69 | try: 70 | with open(f"/proc/{pid}/environ") as env_file: 71 | env_vars = env_file.read() 72 | if f"BOTTLE={self.config.Path}" in env_vars: 73 | os.kill(int(pid), signal.SIGTERM) 74 | logging.info(f"Killed process with PID {pid}.") 75 | except (FileNotFoundError, ProcessLookupError): 76 | continue 77 | except Exception as e: 78 | logging.error(f"Error stopping processes: {e}") 79 | -------------------------------------------------------------------------------- /bottles/backend/wine/winebridge.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from bottles.backend.logger import Logger 4 | from bottles.backend.wine.wineprogram import WineProgram 5 | from bottles.backend.wine.wineserver import WineServer 6 | 7 | logging = Logger() 8 | 9 | 10 | class WineBridge(WineProgram): 11 | program = "Wine Bridge" 12 | command = "WineBridge.exe" 13 | is_internal = True 14 | internal_path = "winebridge" 15 | 16 | def __wineserver_status(self): 17 | return WineServer(self.config).is_alive() 18 | 19 | def is_available(self): 20 | if os.path.isfile(self.get_command()): 21 | logging.info(f"{self.program} is available.") 22 | return True 23 | return False 24 | 25 | def get_procs(self): 26 | args = "getProcs" 27 | processes = [] 28 | 29 | if not self.__wineserver_status: 30 | return processes 31 | 32 | res = self.launch(args=args, communicate=True, action_name="get_procs") 33 | if not res.ready: 34 | return processes 35 | 36 | lines = res.data.split("\n") 37 | for r in lines: 38 | if r in ["", "\r"]: 39 | continue 40 | 41 | r = r.split("|") 42 | 43 | if len(r) < 3: 44 | continue 45 | 46 | processes.append( 47 | { 48 | "pid": r[1], 49 | "threads": r[2], 50 | "name": r[0], 51 | # "parent": r[3] 52 | } 53 | ) 54 | 55 | return processes 56 | 57 | def kill_proc(self, pid: str): 58 | args = f"killProc {pid}" 59 | return self.launch(args=args, communicate=True, action_name="kill_proc") 60 | 61 | def kill_proc_by_name(self, name: str): 62 | args = f"killProcByName {name}" 63 | return self.launch(args=args, communicate=True, action_name="kill_proc_by_name") 64 | 65 | def run_exe(self, exec_path: str): 66 | args = f"runExe {exec_path}" 67 | return self.launch(args=args, communicate=True, action_name="run_exe") 68 | -------------------------------------------------------------------------------- /bottles/backend/wine/winecfg.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from bottles.backend.logger import Logger 4 | from bottles.backend.wine.wineprogram import WineProgram 5 | from bottles.backend.wine.winedbg import WineDbg 6 | from bottles.backend.wine.wineboot import WineBoot 7 | 8 | logging = Logger() 9 | 10 | 11 | class WineCfg(WineProgram): 12 | program = "Wine Configuration" 13 | command = "winecfg" 14 | 15 | def set_windows_version(self, version): 16 | logging.info(f"Setting Windows version to {version}") 17 | 18 | winedbg = WineDbg(self.config) 19 | wineboot = WineBoot(self.config) 20 | 21 | wineboot.kill() 22 | 23 | res = self.launch( 24 | args=f"-v {version}", 25 | communicate=True, 26 | environment={ 27 | "DISPLAY": os.environ.get("DISPLAY", ":0"), 28 | "WAYLAND_DISPLAY": os.environ.get("WAYLAND_DISPLAY", ""), 29 | }, 30 | action_name="set_windows_version", 31 | ) 32 | 33 | winedbg.wait_for_process("winecfg") 34 | wineboot.restart() 35 | 36 | return res 37 | -------------------------------------------------------------------------------- /bottles/backend/wine/winefile.py: -------------------------------------------------------------------------------- 1 | from bottles.backend.logger import Logger 2 | from bottles.backend.wine.wineprogram import WineProgram 3 | 4 | logging = Logger() 5 | 6 | 7 | class WineFile(WineProgram): 8 | program = "Wine File Explorer" 9 | command = "winefile" 10 | 11 | def open_path(self, path: str = "C:\\\\"): 12 | args = path 13 | return self.launch(args=args, communicate=True, action_name="open_path") 14 | -------------------------------------------------------------------------------- /bottles/backend/wine/winepath.py: -------------------------------------------------------------------------------- 1 | import re 2 | from functools import lru_cache 3 | 4 | from bottles.backend.logger import Logger 5 | from bottles.backend.wine.wineprogram import WineProgram 6 | from bottles.backend.utils.manager import ManagerUtils 7 | 8 | logging = Logger() 9 | 10 | 11 | class WinePath(WineProgram): 12 | program = "Wine path converter" 13 | command = "winepath" 14 | 15 | @staticmethod 16 | @lru_cache 17 | def is_windows(path: str): 18 | return ":" in path or "\\" in path 19 | 20 | @staticmethod 21 | @lru_cache 22 | def is_unix(path: str): 23 | return not WinePath.is_windows(path) 24 | 25 | @staticmethod 26 | @lru_cache 27 | def __clean_path(path): 28 | return path.replace("\n", " ").replace("\r", " ").replace("\t", " ").strip() 29 | 30 | @lru_cache 31 | def to_unix(self, path: str, native: bool = False): 32 | if native: 33 | bottle_path = ManagerUtils.get_bottle_path(self.config) 34 | path = path.replace("\\", "/") 35 | path = path.replace( 36 | path[0:2], f"{bottle_path}/dosdevices/{path[0:2].lower()}" 37 | ) 38 | return self.__clean_path(path) 39 | args = f"--unix '{path}'" 40 | res = self.launch(args=args, communicate=True, action_name="--unix") 41 | return self.__clean_path(res.data) 42 | 43 | @lru_cache 44 | def to_windows(self, path: str, native: bool = False): 45 | if native: 46 | bottle_path = ManagerUtils.get_bottle_path(self.config) 47 | if "/drive_" in path: 48 | drive = re.search(r"drive_([a-z])/", path.lower()).group(1) 49 | path = path.replace( 50 | f"{bottle_path}/drive_{drive.lower()}", f"{drive.upper()}:" 51 | ) 52 | elif "/dosdevices" in path: 53 | drive = re.search(r"dosdevices/([a-z]):", path.lower()).group(1) 54 | path = path.replace( 55 | f"{bottle_path}/dosdevices/{drive.lower()}", f"{drive.upper()}:" 56 | ) 57 | path = path.replace("/", "\\") 58 | return self.__clean_path(path) 59 | 60 | args = f"--windows '{path}'" 61 | res = self.launch(args=args, communicate=True, action_name="--windows") 62 | return self.__clean_path(res.data) 63 | 64 | @lru_cache 65 | def to_long(self, path: str): 66 | args = f"--long '{path}'" 67 | res = self.launch(args=args, communicate=True, action_name="--long") 68 | return self.__clean_path(res.data) 69 | 70 | @lru_cache 71 | def to_short(self, path: str): 72 | args = f"--short '{path}'" 73 | res = self.launch(args=args, communicate=True, action_name="--short") 74 | return self.__clean_path(res.data) 75 | -------------------------------------------------------------------------------- /bottles/backend/wine/wineprogram.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from bottles.backend.logger import Logger 4 | from bottles.backend.globals import Paths 5 | from bottles.backend.models.config import BottleConfig 6 | from bottles.backend.wine.winecommand import WineCommand 7 | 8 | logging = Logger() 9 | 10 | 11 | class WineProgram: 12 | program: str = "unknown" 13 | command: str = "" 14 | config: BottleConfig 15 | colors: str = "default" 16 | is_internal: bool = False 17 | internal_path: str = "" 18 | 19 | def __init__(self, config: BottleConfig, silent=False): 20 | if not isinstance(config, BottleConfig): 21 | raise TypeError( 22 | "config should be BottleConfig type, but it was %s" % type(config) 23 | ) 24 | self.config = config 25 | self.silent = silent 26 | 27 | def get_command(self, args: str | None = None): 28 | command = self.command 29 | 30 | if self.is_internal: 31 | command = os.path.join(Paths.base, self.internal_path, command) 32 | 33 | if args is not None: 34 | command += f" {args}" 35 | 36 | return command 37 | 38 | def launch( 39 | self, 40 | args: tuple | str | None = None, 41 | terminal: bool = False, 42 | minimal: bool = True, 43 | communicate: bool = False, 44 | environment: dict | None = None, 45 | pre_script: str | None = None, 46 | post_script: str | None = None, 47 | cwd: str | None = None, 48 | midi_soundfont: str | None = None, 49 | action_name: str = "launch", 50 | ): 51 | if environment is None: 52 | environment = {} 53 | 54 | if not self.silent: 55 | logging.info(f"Using {self.program} -- {action_name}") 56 | 57 | if isinstance(args, tuple): 58 | wineprogram_args = args[0] 59 | program_args = args[1] 60 | else: 61 | wineprogram_args = args 62 | program_args = None 63 | 64 | command = self.get_command(wineprogram_args) 65 | res = WineCommand( 66 | self.config, 67 | command=command, 68 | terminal=terminal, 69 | minimal=minimal, 70 | communicate=communicate, 71 | colors=self.colors, 72 | environment=environment, 73 | pre_script=pre_script, 74 | post_script=post_script, 75 | cwd=cwd, 76 | midi_soundfont=midi_soundfont, 77 | arguments=program_args, 78 | ) 79 | 80 | # logging.info("Executing command:", res.command) 81 | res = res.run() 82 | return res 83 | 84 | def launch_terminal(self, args: str | None = None): 85 | self.launch(args=args, terminal=True, action_name="launch_terminal") 86 | 87 | def launch_minimal(self, args: str | None = None): 88 | self.launch(args=args, minimal=True, action_name="launch_minimal") 89 | -------------------------------------------------------------------------------- /bottles/backend/wine/winhelp.py: -------------------------------------------------------------------------------- 1 | from bottles.backend.logger import Logger 2 | from bottles.backend.wine.wineprogram import WineProgram 3 | 4 | logging = Logger() 5 | 6 | 7 | class WinHelp(WineProgram): 8 | program = "Microsoft help file viewer" 9 | command = "winhelp" 10 | -------------------------------------------------------------------------------- /bottles/backend/wine/xcopy.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from bottles.backend.logger import Logger 4 | from bottles.backend.wine.wineprogram import WineProgram 5 | 6 | logging = Logger() 7 | 8 | 9 | class Xcopy(WineProgram): 10 | program = "Wine Xcopy implementation" 11 | command = "xcopy" 12 | 13 | def copy( 14 | self, 15 | source: str, 16 | dest: str, 17 | dir_and_subs: bool = False, 18 | keep_empty_dirs: bool = False, 19 | quiet: bool = False, 20 | full_log: bool = False, 21 | simulate: bool = False, 22 | ask_confirm: bool = False, 23 | only_struct: bool = False, 24 | no_overwrite_notify: bool = False, 25 | use_short_names: bool = False, 26 | only_existing_in_dest: bool = False, 27 | overwrite_read_only_files: bool = False, 28 | include_hidden_and_sys_files: bool = False, 29 | continue_if_error: bool = False, 30 | copy_attributes: bool = False, 31 | after_date: datetime | None = None, 32 | ): 33 | args = f"{source} {dest} /i" 34 | 35 | if dir_and_subs: 36 | args += "/s" 37 | if keep_empty_dirs: 38 | args += "/e" 39 | if quiet: 40 | args += "/q" 41 | if full_log: 42 | args += "/f" 43 | if simulate: 44 | args += "/l" 45 | if ask_confirm: 46 | args += "/w" 47 | if only_struct: 48 | args += "/t" 49 | if no_overwrite_notify: 50 | args += "/y" 51 | if use_short_names: 52 | args += "/n" 53 | if only_existing_in_dest: 54 | args += "/u" 55 | if overwrite_read_only_files: 56 | args += "/r" 57 | if include_hidden_and_sys_files: 58 | args += "/h" 59 | if continue_if_error: 60 | args += "/c" 61 | if copy_attributes: 62 | args += "/a" 63 | if after_date: 64 | if isinstance(after_date, datetime): 65 | args += f"/d:{after_date.strftime('%m-%d-%Y')}" 66 | 67 | return self.launch(args=args, communicate=True, action_name="start") 68 | -------------------------------------------------------------------------------- /bottles/frontend/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bottlesdevs/Bottles/c85edf809a91cd84c82e1aa0548827d691b07a74/bottles/frontend/__init__.py -------------------------------------------------------------------------------- /bottles/frontend/bottle-details-view.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template $BottleDetailsView: Adw.Bin { 5 | Adw.Leaflet leaflet { 6 | can-navigate-back: true; 7 | can-unfold: false; 8 | hexpand: true; 9 | 10 | Box { 11 | orientation: vertical; 12 | 13 | Adw.HeaderBar sidebar_headerbar { 14 | show-end-title-buttons: false; 15 | 16 | title-widget: Adw.WindowTitle sidebar_title { 17 | title: _("Details"); 18 | }; 19 | 20 | [start] 21 | Button btn_back { 22 | icon-name: "go-previous-symbolic"; 23 | tooltip-text: _("Go Back"); 24 | } 25 | 26 | [end] 27 | Box default_actions {} 28 | } 29 | 30 | Box default_view {} 31 | } 32 | 33 | Adw.LeafletPage { 34 | navigatable: false; 35 | 36 | child: Separator panel_separator { 37 | orientation: vertical; 38 | 39 | styles [ 40 | "sidebar", 41 | ] 42 | }; 43 | } 44 | 45 | Box content { 46 | orientation: vertical; 47 | 48 | Adw.HeaderBar content_headerbar { 49 | show-start-title-buttons: false; 50 | 51 | title-widget: Adw.WindowTitle content_title {}; 52 | 53 | [start] 54 | Button btn_back_sidebar { 55 | icon-name: "go-previous-symbolic"; 56 | tooltip-text: _("Go Back"); 57 | visible: false; 58 | } 59 | 60 | [end] 61 | Box box_actions {} 62 | 63 | [end] 64 | MenuButton btn_operations { 65 | visible: false; 66 | tooltip-text: _("Operations"); 67 | popover: pop_tasks; 68 | 69 | Spinner spinner_tasks {} 70 | 71 | styles [ 72 | "flat", 73 | ] 74 | } 75 | } 76 | 77 | Stack stack_bottle { 78 | transition-type: crossfade; 79 | hexpand: true; 80 | vexpand: true; 81 | } 82 | } 83 | } 84 | } 85 | 86 | Popover pop_tasks { 87 | Box { 88 | orientation: vertical; 89 | spacing: 3; 90 | 91 | Box { 92 | orientation: vertical; 93 | 94 | ListBox list_tasks { 95 | selection-mode: none; 96 | 97 | styles [ 98 | "content", 99 | ] 100 | } 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /bottles/frontend/bottle-picker-dialog.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template $BottlePickerDialog: Adw.ApplicationWindow { 5 | title: _("Select Bottle"); 6 | default-width: 450; 7 | default-height: 450; 8 | 9 | Box { 10 | orientation: vertical; 11 | 12 | Adw.HeaderBar { 13 | show-end-title-buttons: false; 14 | 15 | [start] 16 | Button btn_cancel { 17 | label: _("Cancel"); 18 | } 19 | 20 | [end] 21 | Button btn_select { 22 | label: _("Select"); 23 | 24 | styles [ 25 | "suggested-action", 26 | ] 27 | } 28 | } 29 | 30 | ScrolledWindow { 31 | hexpand: true; 32 | vexpand: true; 33 | 34 | ListBox list_bottles {} 35 | } 36 | 37 | Button btn_open { 38 | label: _("Create New Bottle"); 39 | margin-top: 6; 40 | margin-start: 6; 41 | margin-bottom: 6; 42 | margin-end: 6; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /bottles/frontend/bottle-row.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template $BottleRow: Adw.ActionRow { 5 | activatable: true; 6 | use-markup: false; 7 | 8 | Adw.WrapBox wrap_box { 9 | valign: center; 10 | 11 | styles [ 12 | "tag", 13 | "caption", 14 | ] 15 | } 16 | 17 | Button button_run { 18 | halign: center; 19 | valign: center; 20 | icon-name: "system-run-symbolic"; 21 | 22 | styles [ 23 | "flat", 24 | ] 25 | } 26 | 27 | Image { 28 | icon-name: "go-next-symbolic"; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /bottles/frontend/bottles-list-view.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template $BottlesListView: Adw.Bin { 5 | ScrolledWindow { 6 | Box { 7 | hexpand: true; 8 | vexpand: true; 9 | orientation: vertical; 10 | 11 | SearchBar search_bar { 12 | SearchEntry entry_search { 13 | placeholder-text: _("Search your bottles…"); 14 | } 15 | } 16 | 17 | Adw.PreferencesPage pref_page { 18 | Adw.PreferencesGroup group_bottles { 19 | ListBox list_bottles { 20 | selection-mode: none; 21 | 22 | styles [ 23 | "boxed-list", 24 | ] 25 | } 26 | } 27 | 28 | Adw.PreferencesGroup group_steam { 29 | title: _("Steam Proton"); 30 | 31 | ListBox list_steam { 32 | selection-mode: none; 33 | 34 | styles [ 35 | "boxed-list", 36 | ] 37 | } 38 | } 39 | } 40 | 41 | Adw.StatusPage bottle_status { 42 | title: _("Bottles"); 43 | hexpand: true; 44 | vexpand: true; 45 | 46 | child: Button btn_create { 47 | valign: center; 48 | halign: center; 49 | label: _("Create New Bottle…"); 50 | 51 | styles [ 52 | "suggested-action", 53 | "pill", 54 | ] 55 | }; 56 | } 57 | 58 | Adw.StatusPage no_bottles_found { 59 | visible: false; 60 | icon-name: "system-search-symbolic"; 61 | title: _("No Results Found"); 62 | description: _("Try a different search."); 63 | hexpand: true; 64 | vexpand: true; 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /bottles/frontend/bottles.py: -------------------------------------------------------------------------------- 1 | #!@PYTHON@ 2 | 3 | # bottles.in 4 | # 5 | # Copyright 2020 brombinmirko 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program. If not, see . 19 | 20 | import os 21 | import sys 22 | import signal 23 | import gettext 24 | 25 | APP_VERSION = "@APP_VERSION@" 26 | pkgdatadir = "@pkgdatadir@" 27 | localedir = "@localedir@" 28 | # noinspection DuplicatedCode 29 | data_gresource_path = f"{pkgdatadir}/data.gresource" 30 | bottles_gresource_path = f"{pkgdatadir}/bottles.gresource" 31 | sys.path.insert(1, pkgdatadir) 32 | 33 | # Remove GTK_THEME variable to prevent breakages 34 | # REF: https://github.com/bottlesdevs/Bottles/pull/2886 35 | os.unsetenv("GTK_THEME") 36 | 37 | signal.signal(signal.SIGINT, signal.SIG_DFL) 38 | gettext.install("bottles", localedir) 39 | 40 | if __name__ == "__main__": 41 | from gi.repository import Gio 42 | 43 | data_resource = Gio.Resource.load(data_gresource_path) 44 | bottles_resource = Gio.Resource.load(bottles_gresource_path) 45 | # noinspection PyProtectedMember 46 | data_resource._register() 47 | bottles_resource._register() 48 | 49 | from bottles.frontend import main 50 | 51 | sys.exit(main.main(APP_VERSION)) 52 | -------------------------------------------------------------------------------- /bottles/frontend/check-row.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template $CheckRow: Adw.ActionRow { 5 | activatable-widget: check_button; 6 | active: bind check_button.active bidirectional; 7 | 8 | [prefix] 9 | CheckButton check_button { 10 | valign: center; 11 | can-focus: false; 12 | can-target: false; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /bottles/frontend/common.py: -------------------------------------------------------------------------------- 1 | # utils.py 2 | # 3 | # Copyright 2022 brombinmirko 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, in version 3 of the License. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | # 17 | 18 | import webbrowser 19 | 20 | 21 | def open_doc_url(widget, page): 22 | webbrowser.open_new_tab(f"https://docs.usebottles.com/{page}") 23 | -------------------------------------------------------------------------------- /bottles/frontend/component-entry-row.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template $ComponentEntryRow: Adw.ActionRow { 5 | title: _("Component version"); 6 | 7 | Spinner spinner { 8 | visible: false; 9 | } 10 | 11 | Button btn_remove { 12 | visible: false; 13 | tooltip-text: _("Uninstall"); 14 | valign: center; 15 | icon-name: "user-trash-symbolic"; 16 | 17 | styles [ 18 | "flat", 19 | ] 20 | } 21 | 22 | Button btn_browse { 23 | visible: false; 24 | tooltip-text: _("Browse Files"); 25 | valign: center; 26 | icon-name: "folder-open-symbolic"; 27 | 28 | styles [ 29 | "flat", 30 | ] 31 | } 32 | 33 | Button btn_err { 34 | visible: false; 35 | tooltip-text: _("The installation failed. This may be due to a repository error, partial download or checksum mismatch. Press to try again."); 36 | valign: center; 37 | icon-name: "emblem-important-symbolic"; 38 | 39 | styles [ 40 | "flat", 41 | ] 42 | } 43 | 44 | Button btn_download { 45 | visible: false; 46 | tooltip-text: _("Download & Install"); 47 | valign: center; 48 | icon-name: "document-save-symbolic"; 49 | 50 | styles [ 51 | "flat", 52 | ] 53 | } 54 | 55 | Box box_download_status { 56 | visible: false; 57 | 58 | Label label_task_status { 59 | label: _("0%"); 60 | } 61 | 62 | Image { 63 | icon-name: "document-save-symbolic"; 64 | } 65 | } 66 | 67 | Button btn_cancel { 68 | visible: false; 69 | valign: center; 70 | icon-name: "edit-delete-symbolic"; 71 | 72 | styles [ 73 | "flat", 74 | ] 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /bottles/frontend/crash-report-dialog.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template $CrashReportDialog: Adw.Window { 5 | resizable: false; 6 | deletable: true; 7 | modal: true; 8 | default-width: 550; 9 | title: _("Bottles Crash Report"); 10 | 11 | Box { 12 | orientation: vertical; 13 | 14 | Adw.HeaderBar { 15 | show-start-title-buttons: false; 16 | show-end-title-buttons: false; 17 | 18 | Button btn_cancel { 19 | label: _("_Cancel"); 20 | use-underline: true; 21 | action-name: "window.close"; 22 | } 23 | 24 | [end] 25 | Button btn_send { 26 | label: _("Send Report"); 27 | sensitive: false; 28 | 29 | styles [ 30 | "suggested-action", 31 | ] 32 | } 33 | } 34 | 35 | Box { 36 | spacing: 10; 37 | margin-top: 10; 38 | margin-start: 10; 39 | margin-bottom: 10; 40 | margin-end: 10; 41 | orientation: vertical; 42 | 43 | Label { 44 | halign: start; 45 | label: _("Bottles crashed last time. Please fill out a report attaching the following traceback to help us identify the problem preventing it from happening again."); 46 | wrap: true; 47 | } 48 | 49 | Label label_output { 50 | hexpand: true; 51 | wrap: true; 52 | selectable: true; 53 | max-width-chars: 78; 54 | xalign: 0; 55 | 56 | styles [ 57 | "monospace", 58 | "terminal", 59 | "card", 60 | ] 61 | } 62 | 63 | Box box_related { 64 | visible: false; 65 | spacing: 10; 66 | orientation: vertical; 67 | 68 | Box { 69 | Image { 70 | icon-name: "dialog-warning"; 71 | } 72 | 73 | Label label_notice { 74 | halign: start; 75 | label: _("We found one or more similar (or identical) reports. Please make sure to check carefully that it has not already been reported before submitting a new one. Each report requires effort on the part of the developers to diagnose, please respect their work and make sure you don\'t post duplicates."); 76 | wrap: true; 77 | max-width-chars: 78; 78 | } 79 | 80 | styles [ 81 | "custom_warning", 82 | ] 83 | } 84 | 85 | Adw.PreferencesGroup list_reports {} 86 | 87 | Expander { 88 | CheckButton check_unlock_send { 89 | label: _("I still want to report."); 90 | halign: start; 91 | } 92 | 93 | [label] 94 | Label { 95 | label: _("Advanced options"); 96 | } 97 | } 98 | } 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /bottles/frontend/dependencies-check-dialog.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template $DependenciesCheckDialog: Adw.Window { 5 | modal: true; 6 | deletable: true; 7 | resizable: false; 8 | default-width: 550; 9 | default-height: 500; 10 | 11 | Adw.Clamp { 12 | Adw.StatusPage { 13 | icon-name: "dialog-warning-symbolic"; 14 | title: _("Incomplete package"); 15 | description: _("This version of Bottles does not seem to provide all the necessary core dependencies, please contact the package maintainer or use an official version."); 16 | 17 | Button btn_quit { 18 | halign: center; 19 | label: _("Quit"); 20 | 21 | styles [ 22 | "pill", 23 | "suggested-action", 24 | ] 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /bottles/frontend/dependencies_check_dialog.py: -------------------------------------------------------------------------------- 1 | # dependencies_check_dialog.py 2 | # 3 | # Copyright 2025 The Bottles Contributors 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, in version 3 of the License. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | # 17 | 18 | from gi.repository import Gtk, Adw 19 | 20 | 21 | @Gtk.Template(resource_path="/com/usebottles/bottles/dependencies-check-dialog.ui") 22 | class DependenciesCheckDialog(Adw.Window): 23 | __gtype_name__ = "DependenciesCheckDialog" 24 | 25 | # region widgets 26 | btn_quit = Gtk.Template.Child() 27 | 28 | # endregion 29 | 30 | def __init__(self, window, **kwargs): 31 | super().__init__(**kwargs) 32 | self.set_transient_for(window) 33 | self.window = window 34 | 35 | self.btn_quit.connect("clicked", self.__quit) 36 | 37 | def __quit(self, *_args): 38 | self.window.proper_close() 39 | -------------------------------------------------------------------------------- /bottles/frontend/dependency-entry-row.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | Popover pop_actions { 5 | styles [ 6 | "menu", 7 | ] 8 | 9 | Box { 10 | margin-top: 6; 11 | margin-bottom: 6; 12 | margin-start: 6; 13 | margin-end: 6; 14 | orientation: vertical; 15 | 16 | $GtkModelButton btn_manifest { 17 | text: _("Show Manifest"); 18 | } 19 | 20 | $GtkModelButton btn_license { 21 | text: _("License"); 22 | } 23 | 24 | $GtkModelButton btn_reinstall { 25 | text: _("Reinstall"); 26 | visible: false; 27 | } 28 | 29 | $GtkModelButton btn_remove { 30 | text: _("Uninstall"); 31 | } 32 | 33 | Separator {} 34 | 35 | $GtkModelButton btn_report { 36 | text: _("Report a Bug…"); 37 | } 38 | } 39 | } 40 | 41 | template $DependencyEntryRow: Adw.ActionRow { 42 | title: _("Dependency name"); 43 | activatable-widget: btn_install; 44 | subtitle: _("Dependency description"); 45 | 46 | Box box_actions { 47 | spacing: 6; 48 | 49 | Label label_category { 50 | valign: center; 51 | label: _("Category"); 52 | 53 | styles [ 54 | "tag", 55 | "caption", 56 | ] 57 | } 58 | 59 | Spinner spinner { 60 | visible: false; 61 | } 62 | 63 | Button btn_install { 64 | tooltip-text: _("Download & Install this Dependency"); 65 | valign: center; 66 | 67 | Image { 68 | icon-name: "document-save-symbolic"; 69 | } 70 | 71 | styles [ 72 | "flat", 73 | ] 74 | } 75 | 76 | Button btn_err { 77 | visible: false; 78 | sensitive: false; 79 | tooltip-text: _("An installation error occurred. Restart Bottles to read the Crash Report or run it via terminal to read the output."); 80 | valign: center; 81 | icon-name: "emblem-important-symbolic"; 82 | } 83 | 84 | Separator { 85 | margin-top: 12; 86 | margin-bottom: 12; 87 | } 88 | 89 | MenuButton { 90 | valign: center; 91 | popover: pop_actions; 92 | icon-name: "view-more-symbolic"; 93 | tooltip-text: _("Dependency Menu"); 94 | 95 | styles [ 96 | "flat", 97 | ] 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /bottles/frontend/details-dependencies-view.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template $DetailsDependenciesView: Adw.Bin { 5 | Box { 6 | orientation: vertical; 7 | 8 | SearchBar search_bar { 9 | SearchEntry entry_search { 10 | placeholder-text: _("Search for dependencies…"); 11 | } 12 | } 13 | 14 | Stack stack { 15 | transition-type: crossfade; 16 | 17 | StackPage { 18 | name: "page_offline"; 19 | 20 | child: Adw.StatusPage { 21 | icon-name: "network-error-symbolic"; 22 | title: _("You're offline :("); 23 | vexpand: true; 24 | hexpand: true; 25 | description: _("Bottles is running in offline mode, so dependencies are not available."); 26 | }; 27 | } 28 | 29 | StackPage { 30 | name: "page_loading"; 31 | 32 | child: Adw.StatusPage { 33 | vexpand: true; 34 | hexpand: true; 35 | 36 | Spinner spinner_loading { 37 | valign: center; 38 | } 39 | }; 40 | } 41 | 42 | StackPage { 43 | name: "page_deps"; 44 | 45 | child: Adw.PreferencesPage { 46 | Adw.PreferencesGroup { 47 | description: _("Dependencies are resources that improve compatibility of Windows software.\n\nFiles on this page are provided by third parties under a proprietary license. By installing them, you agree with their respective licensing terms."); 48 | 49 | ListBox list_dependencies { 50 | selection-mode: none; 51 | 52 | styles [ 53 | "boxed-list", 54 | ] 55 | } 56 | } 57 | }; 58 | } 59 | } 60 | } 61 | } 62 | 63 | Popover pop_context { 64 | styles [ 65 | "menu", 66 | ] 67 | 68 | Box { 69 | orientation: vertical; 70 | margin-top: 6; 71 | margin-bottom: 6; 72 | margin-start: 6; 73 | margin-end: 6; 74 | 75 | $GtkModelButton btn_report { 76 | tooltip-text: _("Report a problem or a missing dependency."); 77 | text: _("Report Missing Dependency"); 78 | } 79 | 80 | $GtkModelButton btn_help { 81 | tooltip-text: _("Read Documentation."); 82 | text: _("Documentation"); 83 | } 84 | } 85 | } 86 | 87 | Box actions { 88 | spacing: 6; 89 | 90 | ToggleButton btn_search { 91 | active: bind search_bar.search-mode-enabled no-sync-create bidirectional; 92 | tooltip-text: _("Search"); 93 | icon-name: "system-search-symbolic"; 94 | } 95 | 96 | MenuButton { 97 | popover: pop_context; 98 | icon-name: "view-more-symbolic"; 99 | tooltip-text: _("Secondary Menu"); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /bottles/frontend/details-installers-view.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template $DetailsInstallersView: Adw.Bin { 5 | Box { 6 | orientation: vertical; 7 | 8 | SearchBar search_bar { 9 | SearchEntry entry_search { 10 | placeholder-text: _("Search for Programs…"); 11 | } 12 | } 13 | 14 | Adw.PreferencesPage pref_page { 15 | Adw.PreferencesGroup { 16 | description: _("Install programs curated by our community.\n\nFiles on this page are provided by third parties under a proprietary license. By installing them, you agree with their respective licensing terms."); 17 | 18 | ListBox list_installers { 19 | selection-mode: none; 20 | 21 | styles [ 22 | "boxed-list", 23 | ] 24 | } 25 | } 26 | } 27 | 28 | Adw.StatusPage status_page { 29 | icon-name: "system-software-install-symbolic"; 30 | title: _("No Installers Found"); 31 | vexpand: true; 32 | hexpand: true; 33 | description: _("The repository is unreachable or no installer is compatible with this bottle."); 34 | } 35 | } 36 | } 37 | 38 | Popover pop_context { 39 | styles [ 40 | "menu", 41 | ] 42 | 43 | Box { 44 | orientation: vertical; 45 | margin-top: 6; 46 | margin-bottom: 6; 47 | margin-start: 6; 48 | margin-end: 6; 49 | 50 | $GtkModelButton btn_help { 51 | tooltip-text: _("Read Documentation"); 52 | text: _("Documentation"); 53 | } 54 | } 55 | } 56 | 57 | Box actions { 58 | spacing: 6; 59 | 60 | ToggleButton btn_toggle_search { 61 | active: bind search_bar.search-mode-enabled no-sync-create bidirectional; 62 | tooltip-text: _("Search"); 63 | icon-name: "system-search-symbolic"; 64 | } 65 | 66 | MenuButton { 67 | popover: pop_context; 68 | icon-name: "view-more-symbolic"; 69 | tooltip-text: _("Secondary Menu"); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /bottles/frontend/details-task-manager-view.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | 3 | template $DetailsTaskManagerView: ScrolledWindow { 4 | TreeView treeview_processes { 5 | enable-grid-lines: horizontal; 6 | 7 | [internal-child selection] 8 | TreeSelection {} 9 | } 10 | } 11 | 12 | Box actions { 13 | spacing: 6; 14 | 15 | Button btn_update { 16 | tooltip-text: _("Refresh"); 17 | icon-name: "view-refresh-symbolic"; 18 | } 19 | 20 | Button btn_kill { 21 | tooltip-text: _("Stop process"); 22 | icon-name: "process-stop-symbolic"; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /bottles/frontend/details-versioning-page.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template $DetailsVersioningPage: Adw.PreferencesPage { 5 | Adw.PreferencesPage pref_page { 6 | Adw.PreferencesGroup { 7 | ListBox list_states { 8 | selection-mode: none; 9 | 10 | styles [ 11 | "boxed-list", 12 | ] 13 | } 14 | } 15 | } 16 | 17 | Adw.StatusPage status_page { 18 | icon-name: "preferences-system-time-symbolic"; 19 | title: _("No Snapshots Found"); 20 | description: _("Create your first snapshot to start saving states of your preferences."); 21 | } 22 | } 23 | 24 | Popover pop_context { 25 | styles [ 26 | "menu", 27 | ] 28 | 29 | Box { 30 | orientation: vertical; 31 | margin-top: 6; 32 | margin-bottom: 6; 33 | margin-start: 6; 34 | margin-end: 6; 35 | 36 | $GtkModelButton btn_help { 37 | tooltip-text: _("Read Documentation"); 38 | text: _("Documentation"); 39 | } 40 | } 41 | } 42 | 43 | Popover pop_state { 44 | Box { 45 | orientation: vertical; 46 | spacing: 6; 47 | 48 | styles [ 49 | "menu", 50 | ] 51 | 52 | Box { 53 | Entry entry_state_message { 54 | hexpand: true; 55 | placeholder-text: _("A short comment"); 56 | } 57 | 58 | Button btn_save { 59 | tooltip-text: _("Save the bottle state."); 60 | halign: end; 61 | icon-name: "object-select-symbolic"; 62 | 63 | styles [ 64 | "suggested-action", 65 | ] 66 | } 67 | 68 | styles [ 69 | "linked", 70 | ] 71 | } 72 | } 73 | } 74 | 75 | Box actions { 76 | spacing: 6; 77 | 78 | MenuButton btn_add { 79 | tooltip-text: _("Create new Snapshot"); 80 | popover: pop_state; 81 | icon-name: "list-add-symbolic"; 82 | } 83 | 84 | MenuButton { 85 | popover: pop_context; 86 | icon-name: "view-more-symbolic"; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /bottles/frontend/dll-override-entry.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template $DLLEntry: Adw.ComboRow { 5 | title: "DLL Name"; 6 | 7 | model: StringList { 8 | strings [ 9 | _("Builtin (Wine)"), 10 | _("Native (Windows)"), 11 | _("Builtin, then Native"), 12 | _("Native, then Builtin"), 13 | _("Disabled") 14 | ] 15 | }; 16 | 17 | Button btn_remove { 18 | valign: center; 19 | icon-name: "user-trash-symbolic"; 20 | tooltip-text: _("Remove"); 21 | 22 | styles [ 23 | "flat", 24 | ] 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /bottles/frontend/dll-overrides-dialog.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template $DLLOverridesDialog: Adw.PreferencesWindow { 5 | modal: true; 6 | default-width: 500; 7 | search-enabled: false; 8 | title: _("DLL Overrides"); 9 | 10 | Adw.PreferencesPage { 11 | Adw.PreferencesGroup { 12 | description: _("Dynamic Link Libraries can be specified to be builtin (provided by Wine) or native (provided by the program)."); 13 | title: _("DLL Overrides"); 14 | 15 | Adw.EntryRow entry_row { 16 | title: _("New Override"); 17 | show-apply-button: true; 18 | 19 | [suffix] 20 | MenuButton menu_invalid_override { 21 | valign: center; 22 | tooltip-text: _("Show Information"); 23 | icon-name: "warning-symbolic"; 24 | popover: popover_invalid_override; 25 | visible: false; 26 | 27 | styles [ 28 | "flat", 29 | ] 30 | } 31 | } 32 | } 33 | 34 | Adw.PreferencesGroup group_overrides { 35 | title: _("Overrides"); 36 | } 37 | } 38 | } 39 | 40 | Popover popover_invalid_override { 41 | Label { 42 | margin-start: 6; 43 | margin-end: 6; 44 | margin-top: 6; 45 | margin-bottom: 6; 46 | xalign: 0; 47 | wrap: true; 48 | wrap-mode: word_char; 49 | ellipsize: none; 50 | lines: 4; 51 | use-markup: true; 52 | max-width-chars: 40; 53 | label: _("This override is already managed by Bottles."); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /bottles/frontend/drive-entry.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template $DriveEntry: Adw.ActionRow { 5 | title: "C:"; 6 | subtitle: _("/point/to/path"); 7 | 8 | Box { 9 | spacing: 6; 10 | 11 | Button btn_remove { 12 | valign: center; 13 | tooltip-text: _("Remove"); 14 | icon-name: "user-trash-symbolic"; 15 | 16 | styles [ 17 | "flat", 18 | ] 19 | } 20 | 21 | Button btn_path { 22 | valign: center; 23 | tooltip-text: _("Choose a Directory"); 24 | icon-name: "document-open-symbolic"; 25 | 26 | styles [ 27 | "flat", 28 | ] 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /bottles/frontend/drives-dialog.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template $DrivesDialog: Adw.Window { 5 | modal: true; 6 | default-width: 500; 7 | default-height: 500; 8 | title: _("Drives"); 9 | 10 | ShortcutController { 11 | Shortcut { 12 | trigger: "Escape"; 13 | action: "action(window.close)"; 14 | } 15 | } 16 | 17 | Box { 18 | orientation: vertical; 19 | 20 | Adw.HeaderBar {} 21 | 22 | Adw.PreferencesPage { 23 | Adw.PreferencesGroup { 24 | description: _("These are paths from your host system that are mapped and recognized as devices by the runner (e.g. C: D:…)."); 25 | 26 | Adw.ComboRow combo_letter { 27 | title: _("Letter"); 28 | 29 | model: StringList str_list_letters {}; 30 | 31 | Button btn_save { 32 | valign: center; 33 | 34 | Image { 35 | icon-name: "object-select-symbolic"; 36 | } 37 | 38 | styles [ 39 | "flat", 40 | ] 41 | } 42 | } 43 | } 44 | 45 | Adw.PreferencesGroup list_drives { 46 | title: _("Existing Drives"); 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /bottles/frontend/duplicate-dialog.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template $DuplicateDialog: Adw.Window { 5 | modal: true; 6 | default-width: 400; 7 | default-height: 400; 8 | destroy-with-parent: true; 9 | 10 | Box { 11 | orientation: vertical; 12 | 13 | Adw.HeaderBar { 14 | show-end-title-buttons: false; 15 | 16 | title-widget: Adw.WindowTitle { 17 | title: _("Duplicate Bottle"); 18 | }; 19 | 20 | Button btn_cancel { 21 | label: _("_Cancel"); 22 | use-underline: true; 23 | action-name: "window.close"; 24 | } 25 | 26 | ShortcutController { 27 | scope: managed; 28 | 29 | Shortcut { 30 | trigger: "Escape"; 31 | action: "action(window.close)"; 32 | } 33 | } 34 | 35 | [end] 36 | Button btn_duplicate { 37 | label: _("Duplicate"); 38 | 39 | styles [ 40 | "suggested-action", 41 | ] 42 | } 43 | } 44 | 45 | Stack stack_switcher { 46 | Adw.PreferencesPage page_name { 47 | Adw.PreferencesGroup { 48 | description: _("Enter a name for the duplicate of the Bottle."); 49 | 50 | Adw.EntryRow entry_name { 51 | title: _("Name"); 52 | } 53 | } 54 | } 55 | 56 | StackPage { 57 | name: "page_duplicating"; 58 | 59 | child: Box page_duplicating { 60 | margin-top: 24; 61 | margin-bottom: 24; 62 | orientation: vertical; 63 | 64 | Label { 65 | halign: center; 66 | margin-top: 12; 67 | margin-bottom: 12; 68 | label: _("Duplicating…"); 69 | 70 | styles [ 71 | "title-1", 72 | ] 73 | } 74 | 75 | Label { 76 | margin-bottom: 6; 77 | label: _("This could take a while."); 78 | } 79 | 80 | ProgressBar progressbar { 81 | width-request: 300; 82 | halign: center; 83 | margin-top: 24; 84 | margin-bottom: 12; 85 | } 86 | }; 87 | } 88 | 89 | StackPage { 90 | name: "page_duplicated"; 91 | 92 | child: Adw.StatusPage page_duplicated { 93 | icon-name: "object-select-symbolic"; 94 | title: _("Bottle Duplicated"); 95 | }; 96 | } 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /bottles/frontend/env-var-entry.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template $EnvironmentVariableEntryRow: Adw.EntryRow { 5 | show-apply-button: true; 6 | 7 | Button btn_remove { 8 | valign: center; 9 | icon-name: "user-trash-symbolic"; 10 | tooltip-text: _("Remove"); 11 | 12 | styles [ 13 | "flat", 14 | ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /bottles/frontend/environment-variables-dialog.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template $EnvironmentVariablesDialog: Adw.Dialog { 5 | content-width: 600; 6 | content-height: 800; 7 | title: _("Environment Variables"); 8 | 9 | Box { 10 | orientation: vertical; 11 | 12 | Adw.HeaderBar { 13 | styles [ 14 | "flat", 15 | ] 16 | } 17 | 18 | Adw.PreferencesPage { 19 | Adw.PreferencesGroup { 20 | description: _("Environment variables are dynamic-named values that can affect the way running processes will behave in your bottle"); 21 | 22 | Adw.EntryRow entry_new_var { 23 | title: _("New Variable"); 24 | show-apply-button: true; 25 | } 26 | } 27 | 28 | Adw.PreferencesGroup group_vars {} 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /bottles/frontend/exclusion-pattern-row.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template $ExclusionPatternRow: Adw.ActionRow { 5 | title: _("Value"); 6 | 7 | Button btn_remove { 8 | valign: center; 9 | icon-name: "user-trash-symbolic"; 10 | 11 | styles [ 12 | "flat", 13 | ] 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /bottles/frontend/exclusion-patterns-dialog.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template $ExclusionPatternsDialog: Adw.Window { 5 | modal: true; 6 | default-width: 500; 7 | default-height: 500; 8 | 9 | ShortcutController { 10 | Shortcut { 11 | trigger: "Escape"; 12 | action: "action(window.close)"; 13 | } 14 | } 15 | 16 | Box { 17 | orientation: vertical; 18 | 19 | Adw.HeaderBar { 20 | title-widget: Adw.WindowTitle { 21 | title: _("Exclusion Patterns"); 22 | }; 23 | } 24 | 25 | Adw.PreferencesPage { 26 | Adw.PreferencesGroup { 27 | description: _("Define patterns that will be used to prevent some directories to being versioned."); 28 | 29 | Adw.EntryRow entry_name { 30 | title: _("Pattern"); 31 | show-apply-button: true; 32 | } 33 | } 34 | 35 | Adw.PreferencesGroup group_patterns { 36 | title: _("Existing Patterns"); 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /bottles/frontend/executable.py: -------------------------------------------------------------------------------- 1 | # executable.py 2 | # 3 | # Copyright 2022 brombinmirko 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, in version 3 of the License. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | # 17 | 18 | from gi.repository import Gtk 19 | 20 | from bottles.backend.utils.threading import RunAsync 21 | from bottles.backend.wine.executor import WineExecutor 22 | 23 | 24 | class ExecButton(Gtk.Button): 25 | def __init__(self, parent, data, config, **kwargs): 26 | super().__init__(**kwargs) 27 | self.parent = parent 28 | self.config = config 29 | self.data = data 30 | 31 | self.set_label(data.get("name")) 32 | self.connect("clicked", self.on_clicked) 33 | 34 | def on_clicked(self, widget): 35 | executor = WineExecutor( 36 | self.config, exec_path=self.data.get("file"), args=self.data.get("args") 37 | ) 38 | RunAsync(executor.run) 39 | self.parent.pop_run.popdown() # workaround #1640 40 | -------------------------------------------------------------------------------- /bottles/frontend/filters.py: -------------------------------------------------------------------------------- 1 | # filters.py: File for providing common GtkFileFilters 2 | # 3 | # Copyright 2023 Bottles Contributors 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, in version 3 of the License. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from gettext import gettext as _ 18 | 19 | from gi.repository import Gio, GObject, Gtk 20 | 21 | 22 | def add_executable_filters(dialog): 23 | # TODO: Investigate why `filter.add_mime_type(...)` does not show filter in all distributions. 24 | # Intended MIME types are: 25 | # - `application/x-ms-dos-executable` 26 | # - `application/x-msi` 27 | __set_filter(dialog, _("Supported Executables"), ["*.exe", "*.msi"]) 28 | 29 | 30 | def add_soundfont_filters(dialog): 31 | __set_filter(dialog, _("Supported SoundFonts"), ["*.sf2", "*.sf3"]) 32 | 33 | 34 | def add_yaml_filters(dialog): 35 | # TODO: Investigate why `filter.add_mime_type(...)` does not show filter in all distributions. 36 | # Intended MIME types are: 37 | # - `application/yaml` 38 | __set_filter(dialog, "YAML", ["*.yaml", "*.yml"]) 39 | 40 | 41 | def add_all_filters(dialog): 42 | __set_filter(dialog, _("All Files"), ["*"]) 43 | 44 | 45 | def __set_filter(dialog: GObject.Object, name: str, patterns: list[str]): 46 | """Set dialog named file filter from list of extension patterns.""" 47 | 48 | filter = Gtk.FileFilter() 49 | filter.set_name(name) 50 | for pattern in patterns: 51 | filter.add_pattern(pattern) 52 | 53 | if isinstance(dialog, Gtk.FileDialog): 54 | filters = dialog.get_filters() or Gio.ListStore.new(Gtk.FileFilter) 55 | filters.append(filter) 56 | dialog.set_filters(filters) 57 | elif isinstance(dialog, Gtk.FileChooserNative): 58 | dialog.add_filter(filter) 59 | else: 60 | raise TypeError 61 | -------------------------------------------------------------------------------- /bottles/frontend/fsr-dialog.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template $FsrDialog: Adw.Window { 5 | default-width: 500; 6 | modal: true; 7 | title: _("FidelityFX Super Resolution Settings"); 8 | 9 | ShortcutController { 10 | Shortcut { 11 | trigger: "Escape"; 12 | action: "action(window.close)"; 13 | } 14 | } 15 | 16 | Box { 17 | orientation: vertical; 18 | 19 | Adw.HeaderBar { 20 | show-start-title-buttons: false; 21 | show-end-title-buttons: false; 22 | 23 | Button { 24 | label: _("_Cancel"); 25 | use-underline: true; 26 | action-name: "window.close"; 27 | } 28 | 29 | [end] 30 | Button btn_save { 31 | label: _("Save"); 32 | 33 | styles [ 34 | "suggested-action", 35 | ] 36 | } 37 | } 38 | 39 | Adw.PreferencesPage { 40 | Adw.PreferencesGroup { 41 | Adw.ComboRow combo_quality_mode { 42 | title: "Quality Mode"; 43 | 44 | model: StringList str_list_quality_mode {}; 45 | } 46 | 47 | Adw.ActionRow { 48 | title: _("Sharpening Strength"); 49 | 50 | SpinButton { 51 | numeric: true; 52 | valign: center; 53 | 54 | adjustment: Adjustment spin_sharpening_strength { 55 | step-increment: 1; 56 | upper: 5; 57 | }; 58 | } 59 | } 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /bottles/frontend/generic_cli.py: -------------------------------------------------------------------------------- 1 | class MessageDialog: 2 | def __init__( 3 | self, parent, title="Warning", message="An error has occurred.", log=False 4 | ): 5 | print(message) 6 | -------------------------------------------------------------------------------- /bottles/frontend/help-overlay.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | 3 | ShortcutsWindow help_overlay { 4 | modal: true; 5 | 6 | ShortcutsSection { 7 | section-name: "shortcuts"; 8 | max-height: 10; 9 | 10 | ShortcutsGroup { 11 | title: C_("shortcut window", "General"); 12 | 13 | ShortcutsShortcut { 14 | title: C_("shortcut window", "New Bottle"); 15 | action-name: "app.new"; 16 | } 17 | 18 | ShortcutsShortcut { 19 | title: C_("shortcut window", "Import Bottle"); 20 | action-name: "app.import"; 21 | } 22 | 23 | ShortcutsShortcut { 24 | title: C_("shortcut window", "Preferences"); 25 | action-name: "app.preferences"; 26 | } 27 | 28 | ShortcutsShortcut { 29 | title: C_("shortcut window", "Documentation"); 30 | action-name: "app.help"; 31 | } 32 | 33 | ShortcutsShortcut { 34 | title: C_("shortcut window", "Show Shortcuts"); 35 | action-name: "win.show-help-overlay"; 36 | } 37 | 38 | ShortcutsShortcut { 39 | title: C_("shortcut window", "Quit"); 40 | action-name: "app.quit"; 41 | } 42 | 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /bottles/frontend/importer-row.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | Popover pop_actions { 5 | styles [ 6 | "menu", 7 | ] 8 | 9 | Box { 10 | orientation: vertical; 11 | spacing: 3; 12 | 13 | $GtkModelButton btn_browse { 14 | text: _("Browse Files"); 15 | } 16 | } 17 | } 18 | 19 | template $ImporterRow: Adw.ActionRow { 20 | /* Translators: A Wine prefix is a separate environment (C:\ drive) for the Wine program */ 21 | title: _("Wine prefix name"); 22 | 23 | Box { 24 | spacing: 6; 25 | 26 | Label label_manager { 27 | valign: center; 28 | label: _("Manager"); 29 | 30 | styles [ 31 | "tag", 32 | "caption", 33 | ] 34 | } 35 | 36 | Image img_lock { 37 | visible: false; 38 | tooltip-text: _("This Wine prefix was already imported in Bottles."); 39 | valign: center; 40 | icon-name: "channel-secure-symbolic"; 41 | 42 | styles [ 43 | "tag", 44 | "caption", 45 | ] 46 | } 47 | 48 | Button btn_import { 49 | valign: center; 50 | 51 | Image { 52 | icon-name: "document-save-symbolic"; 53 | } 54 | 55 | styles [ 56 | "flat", 57 | ] 58 | } 59 | 60 | Separator { 61 | margin-top: 12; 62 | margin-bottom: 12; 63 | } 64 | 65 | MenuButton { 66 | valign: center; 67 | popover: pop_actions; 68 | icon-name: "view-more-symbolic"; 69 | 70 | styles [ 71 | "flat", 72 | ] 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /bottles/frontend/importer-view.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template $ImporterView: Adw.Bin { 5 | Box { 6 | orientation: vertical; 7 | 8 | HeaderBar headerbar { 9 | title-widget: Adw.WindowTitle window_title {}; 10 | 11 | [start] 12 | Button btn_back { 13 | tooltip-text: _("Go Back"); 14 | icon-name: "go-previous-symbolic"; 15 | } 16 | 17 | [end] 18 | Box box_actions { 19 | MenuButton btn_import_backup { 20 | tooltip-text: _("Import a Bottle backup"); 21 | popover: pop_backup; 22 | icon-name: "document-send-symbolic"; 23 | } 24 | 25 | Button btn_find_prefixes { 26 | tooltip-text: _("Search again for prefixes"); 27 | icon-name: "view-refresh-symbolic"; 28 | } 29 | } 30 | } 31 | 32 | Adw.PreferencesPage { 33 | Adw.StatusPage status_page { 34 | vexpand: true; 35 | icon-name: "document-save-symbolic"; 36 | title: _("No Prefixes Found"); 37 | description: _("No external prefixes were found. Does Bottles have access to them?\nUse the icon on the top to import a bottle from a backup."); 38 | } 39 | 40 | Adw.PreferencesGroup group_prefixes { 41 | visible: false; 42 | 43 | ListBox list_prefixes { 44 | styles [ 45 | "boxed-list", 46 | ] 47 | } 48 | } 49 | } 50 | } 51 | } 52 | 53 | Popover pop_backup { 54 | styles [ 55 | "menu", 56 | ] 57 | 58 | Box { 59 | orientation: vertical; 60 | margin-top: 6; 61 | margin-bottom: 6; 62 | margin-start: 6; 63 | margin-end: 6; 64 | 65 | $GtkModelButton btn_import_config { 66 | tooltip-text: _("This is just the bottle configuration, it\'s perfect if you want to create a new one but without personal files."); 67 | text: _("Configuration"); 68 | } 69 | 70 | $GtkModelButton btn_import_full { 71 | tooltip-text: _("This is the complete archive of your bottle, including personal files."); 72 | text: _("Full Archive"); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /bottles/frontend/importer_row.py: -------------------------------------------------------------------------------- 1 | # importer_row.py 2 | # 3 | # Copyright 2025 The Bottles Contributors 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, in version 3 of the License. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | # 17 | 18 | from gettext import gettext as _ 19 | 20 | from gi.repository import Gtk, Adw 21 | 22 | from bottles.backend.utils.manager import ManagerUtils 23 | from bottles.backend.utils.threading import RunAsync 24 | from bottles.frontend.gtk import GtkUtils 25 | 26 | 27 | @Gtk.Template(resource_path="/com/usebottles/bottles/importer-row.ui") 28 | class ImporterRow(Adw.ActionRow): 29 | __gtype_name__ = "ImporterRow" 30 | 31 | # region Widgets 32 | label_manager = Gtk.Template.Child() 33 | btn_import = Gtk.Template.Child() 34 | btn_browse = Gtk.Template.Child() 35 | img_lock = Gtk.Template.Child() 36 | 37 | # endregion 38 | 39 | def __init__(self, im_manager, prefix, **kwargs): 40 | super().__init__(**kwargs) 41 | 42 | # common variables and references 43 | self.window = im_manager.window 44 | self.import_manager = im_manager.import_manager 45 | self.prefix = prefix 46 | 47 | # populate widgets 48 | self.set_title(prefix.get("Name")) 49 | self.label_manager.set_text(prefix.get("Manager")) 50 | 51 | if prefix.get("Lock"): 52 | self.img_lock.set_visible(True) 53 | 54 | self.label_manager.add_css_class("tag-%s" % prefix.get("Manager").lower()) 55 | 56 | # connect signals 57 | self.btn_browse.connect("clicked", self.browse_wineprefix) 58 | self.btn_import.connect("clicked", self.import_wineprefix) 59 | 60 | def browse_wineprefix(self, widget): 61 | ManagerUtils.browse_wineprefix(self.prefix) 62 | 63 | def import_wineprefix(self, widget): 64 | @GtkUtils.run_in_main_loop 65 | def set_imported(result, error=False): 66 | self.btn_import.set_visible(result.ok) 67 | self.img_lock.set_visible(result.ok) 68 | 69 | if result.ok: 70 | self.window.show_toast( 71 | _('"{0}" imported').format(self.prefix.get("Name")) 72 | ) 73 | 74 | self.set_sensitive(True) 75 | 76 | self.set_sensitive(False) 77 | 78 | RunAsync( 79 | self.import_manager.import_wineprefix, 80 | callback=set_imported, 81 | wineprefix=self.prefix, 82 | ) 83 | -------------------------------------------------------------------------------- /bottles/frontend/installer-row.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | Popover pop_actions { 5 | styles [ 6 | "menu", 7 | ] 8 | 9 | Box { 10 | orientation: vertical; 11 | margin-top: 6; 12 | margin-bottom: 6; 13 | margin-start: 6; 14 | margin-end: 6; 15 | 16 | $GtkModelButton btn_manifest { 17 | text: _("Show Manifest…"); 18 | } 19 | 20 | $GtkModelButton btn_review { 21 | text: _("Read Review…"); 22 | } 23 | 24 | Separator {} 25 | 26 | $GtkModelButton btn_report { 27 | text: _("Report a Bug…"); 28 | } 29 | } 30 | } 31 | 32 | template $InstallerRow: Adw.ActionRow { 33 | activatable-widget: btn_install; 34 | title: _("Installer name"); 35 | subtitle: _("Installer description"); 36 | 37 | Box { 38 | spacing: 6; 39 | 40 | Label label_grade { 41 | valign: center; 42 | label: _("Unknown"); 43 | 44 | styles [ 45 | "tag", 46 | "caption", 47 | ] 48 | } 49 | 50 | Button btn_install { 51 | tooltip-text: _("Install this Program"); 52 | valign: center; 53 | icon-name: "document-save-symbolic"; 54 | 55 | styles [ 56 | "flat", 57 | ] 58 | } 59 | 60 | Separator { 61 | margin-top: 12; 62 | margin-bottom: 12; 63 | } 64 | 65 | MenuButton { 66 | valign: center; 67 | popover: pop_actions; 68 | icon-name: "view-more-symbolic"; 69 | tooltip-text: _("Program Menu"); 70 | 71 | styles [ 72 | "flat", 73 | ] 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /bottles/frontend/journal-dialog.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | Popover pop_menu { 5 | Box { 6 | orientation: vertical; 7 | spacing: 3; 8 | 9 | $GtkModelButton btn_all { 10 | text: _("All messages"); 11 | } 12 | 13 | $GtkModelButton btn_critical { 14 | text: _("Critical"); 15 | } 16 | 17 | $GtkModelButton btn_error { 18 | text: _("Errors"); 19 | } 20 | 21 | $GtkModelButton btn_warning { 22 | text: _("Warnings"); 23 | } 24 | 25 | $GtkModelButton btn_info { 26 | text: _("Info"); 27 | } 28 | } 29 | } 30 | 31 | template $JournalDialog: Adw.Window { 32 | default-width: 800; 33 | default-height: 600; 34 | destroy-with-parent: true; 35 | 36 | Box { 37 | orientation: vertical; 38 | 39 | Adw.HeaderBar { 40 | title-widget: Adw.WindowTitle { 41 | title: _("Journal Browser"); 42 | }; 43 | 44 | [title] 45 | Box { 46 | SearchEntry search_entry { 47 | placeholder-text: _("Journal Browser"); 48 | } 49 | 50 | MenuButton { 51 | focus-on-click: false; 52 | tooltip-text: _("Change Logging Level."); 53 | popover: pop_menu; 54 | 55 | Label label_filter { 56 | label: _("All"); 57 | } 58 | } 59 | 60 | styles [ 61 | "linked", 62 | ] 63 | } 64 | } 65 | 66 | ScrolledWindow { 67 | hexpand: true; 68 | vexpand: true; 69 | 70 | TreeView tree_view { 71 | reorderable: true; 72 | hexpand: true; 73 | vexpand: true; 74 | 75 | [internal-child selection] 76 | TreeSelection {} 77 | } 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /bottles/frontend/library-view.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template $LibraryView: Adw.Bin { 5 | Box { 6 | orientation: vertical; 7 | 8 | Adw.StatusPage status_page { 9 | vexpand: true; 10 | hexpand: true; 11 | icon-name: "library-symbolic"; 12 | title: _("Library"); 13 | description: _("Add items here from your bottle\'s program list"); 14 | } 15 | 16 | ScrolledWindow scroll_window { 17 | hexpand: true; 18 | vexpand: true; 19 | 20 | FlowBox main_flow { 21 | max-children-per-line: bind template.items_per_line; 22 | row-spacing: 5; 23 | column-spacing: 5; 24 | halign: center; 25 | valign: start; 26 | margin-top: 5; 27 | margin-start: 5; 28 | margin-bottom: 5; 29 | margin-end: 5; 30 | homogeneous: true; 31 | selection-mode: none; 32 | activate-on-single-click: false; 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /bottles/frontend/loading-view.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template $LoadingView: Adw.Bin { 5 | WindowHandle { 6 | hexpand: true; 7 | vexpand: true; 8 | 9 | Box { 10 | orientation: vertical; 11 | 12 | Adw.StatusPage loading_status_page { 13 | title: _("Starting up…"); 14 | hexpand: true; 15 | vexpand: true; 16 | } 17 | 18 | Button btn_go_offline { 19 | margin-bottom: 20; 20 | valign: center; 21 | halign: center; 22 | label: _("Continue Offline"); 23 | 24 | styles [ 25 | "destructive-action", 26 | "pill", 27 | ] 28 | } 29 | 30 | Label label_fetched { 31 | styles [ 32 | "dim-label", 33 | ] 34 | } 35 | 36 | Label label_downloading { 37 | margin-bottom: 20; 38 | 39 | styles [ 40 | "dim-label", 41 | ] 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /bottles/frontend/loading_view.py: -------------------------------------------------------------------------------- 1 | # loading_view.py 2 | # 3 | # Copyright 2025 The Bottles Contributors 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, in version 3 of the License. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | # 17 | 18 | from gettext import gettext as _ 19 | 20 | from gi.repository import Gtk, Adw 21 | 22 | from bottles.backend.models.result import Result 23 | from bottles.backend.state import SignalManager, Signals 24 | from bottles.frontend.gtk import GtkUtils 25 | from bottles.frontend.params import APP_ID 26 | 27 | 28 | @Gtk.Template(resource_path="/com/usebottles/bottles/loading-view.ui") 29 | class LoadingView(Adw.Bin): 30 | __gtype_name__ = "LoadingView" 31 | __fetched = 0 32 | 33 | # region widgets 34 | label_fetched = Gtk.Template.Child() 35 | label_downloading = Gtk.Template.Child() 36 | btn_go_offline = Gtk.Template.Child() 37 | loading_status_page = Gtk.Template.Child() 38 | # endregion 39 | 40 | def __init__(self, **kwargs): 41 | super().__init__(**kwargs) 42 | self.loading_status_page.set_icon_name(APP_ID) 43 | self.btn_go_offline.connect("clicked", self.go_offline) 44 | 45 | @GtkUtils.run_in_main_loop 46 | def add_fetched(self, res: Result): 47 | total: int = res.data 48 | self.__fetched += 1 49 | self.label_downloading.set_text( 50 | _("Downloading ~{0} of packages…").format("20kb") 51 | ) 52 | self.label_fetched.set_text( 53 | _("Fetched {0} of {1} packages").format(self.__fetched, total) 54 | ) 55 | 56 | def go_offline(self, _widget): 57 | SignalManager.send(Signals.ForceStopNetworking, Result(status=True)) 58 | -------------------------------------------------------------------------------- /bottles/frontend/local-resource-row.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template $LocalResourceRow: Adw.ActionRow { 5 | subtitle: _("This resource is missing."); 6 | 7 | Button btn_path { 8 | valign: center; 9 | tooltip-text: _("Browse"); 10 | icon-name: "document-open-symbolic"; 11 | 12 | styles [ 13 | "flat", 14 | ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /bottles/frontend/mangohud-dialog.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template $MangoHudDialog : Adw.Window { 5 | default-width: 500; 6 | modal: true; 7 | 8 | title:_("MangoHud Settings"); 9 | 10 | ShortcutController { 11 | Shortcut { 12 | trigger: "Escape"; 13 | action: "action(window.close)"; 14 | } 15 | } 16 | 17 | Box { 18 | orientation: vertical; 19 | 20 | Adw.HeaderBar { 21 | show-start-title-buttons: false; 22 | show-end-title-buttons: false; 23 | 24 | Button { 25 | label: _("_Cancel"); 26 | use-underline: true; 27 | action-name: "window.close"; 28 | } 29 | 30 | [end] 31 | Button btn_save { 32 | label: _("Save"); 33 | 34 | styles [ 35 | "suggested-action", 36 | ] 37 | } 38 | } 39 | 40 | Adw.PreferencesPage { 41 | Adw.PreferencesGroup { 42 | Adw.ActionRow { 43 | title: _("Display On Game Start"); 44 | subtitle: _("Display HUD as soon as the game starts. Can be toggled in-game (default keybind: [⇧ Right Shift] + [F12])."); 45 | activatable-widget: display_on_game_start; 46 | 47 | Switch display_on_game_start { 48 | valign: center; 49 | } 50 | } 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /bottles/frontend/mangohud_dialog.py: -------------------------------------------------------------------------------- 1 | # mangohud_dialog.py 2 | # 3 | # Copyright 2025 The Bottles Contributors 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | from gi.repository import Gtk, GLib, Adw 19 | from bottles.backend.logger import Logger 20 | 21 | logging = Logger() 22 | 23 | 24 | @Gtk.Template(resource_path="/com/usebottles/bottles/mangohud-dialog.ui") 25 | class MangoHudDialog(Adw.Window): 26 | __gtype_name__ = "MangoHudDialog" 27 | 28 | # Region Widgets 29 | btn_save = Gtk.Template.Child() 30 | display_on_game_start = Gtk.Template.Child() 31 | 32 | def __init__(self, window, config, **kwargs): 33 | super().__init__(**kwargs) 34 | self.set_transient_for(window) 35 | 36 | # Common variables and references 37 | self.window = window 38 | self.manager = window.manager 39 | self.config = config 40 | 41 | # Connect signals 42 | self.btn_save.connect("clicked", self.__save) 43 | 44 | self.__update(config) 45 | 46 | def __update(self, config): 47 | parameters = config.Parameters 48 | self.display_on_game_start.set_active(parameters.mangohud_display_on_game_start) 49 | 50 | def __idle_save(self, *_args): 51 | settings = { 52 | "mangohud_display_on_game_start": self.display_on_game_start.get_active(), 53 | } 54 | 55 | for setting in settings.keys(): 56 | self.manager.update_config( 57 | config=self.config, 58 | key=setting, 59 | value=settings[setting], 60 | scope="Parameters", 61 | ) 62 | 63 | self.destroy() 64 | 65 | def __save(self, *_args): 66 | GLib.idle_add(self.__idle_save) 67 | -------------------------------------------------------------------------------- /bottles/frontend/params.py: -------------------------------------------------------------------------------- 1 | # Application details 2 | APP_NAME = "@APP_NAME@" 3 | APP_NAME_LOWER = APP_NAME.lower() 4 | BASE_ID = "@BASE_ID@" 5 | APP_ID = "@APP_ID@" 6 | APP_VERSION = "@APP_VERSION@" 7 | APP_MAJOR_VERSION = "@APP_MAJOR_VERSION@" 8 | APP_MINOR_VERSION = "@APP_MINOR_VERSION@" 9 | APP_ICON = "@APP_ID@" 10 | PROFILE = "@PROFILE@" 11 | 12 | # Internal settings not user editable 13 | ANIM_DURATION = 120 14 | 15 | # General purpose definitions 16 | EXECUTABLE_EXTS = (".exe", ".msi", ".bat", ".lnk") 17 | 18 | # URLs 19 | DOC_URL = "https://docs.usebottles.com" 20 | -------------------------------------------------------------------------------- /bottles/frontend/program-row.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | Popover pop_actions { 5 | styles [ 6 | "menu", 7 | ] 8 | 9 | Box { 10 | orientation: vertical; 11 | margin-top: 6; 12 | margin-bottom: 6; 13 | margin-start: 6; 14 | margin-end: 6; 15 | 16 | Box { 17 | homogeneous: true; 18 | 19 | Button btn_launch_terminal { 20 | tooltip-text: _("Launch with Terminal"); 21 | icon-name: "utilities-terminal-symbolic"; 22 | valign: center; 23 | } 24 | 25 | Button btn_browse { 26 | tooltip-text: _("Browse Path"); 27 | icon-name: "document-open-symbolic"; 28 | valign: center; 29 | } 30 | 31 | styles [ 32 | "linked", 33 | ] 34 | } 35 | 36 | Separator {} 37 | 38 | $GtkModelButton btn_launch_options { 39 | text: _("Change Launch Options…"); 40 | } 41 | 42 | $GtkModelButton btn_add_library { 43 | text: _("Add to Library"); 44 | } 45 | 46 | $GtkModelButton btn_add_entry { 47 | text: _("Add Desktop Entry"); 48 | } 49 | 50 | $GtkModelButton btn_add_steam { 51 | text: _("Add to Steam"); 52 | } 53 | 54 | $GtkModelButton btn_rename { 55 | text: _("Rename…"); 56 | } 57 | 58 | Separator {} 59 | 60 | $GtkModelButton btn_hide { 61 | text: _("Hide Program"); 62 | } 63 | 64 | $GtkModelButton btn_unhide { 65 | text: _("Show Program"); 66 | } 67 | 68 | $GtkModelButton btn_remove { 69 | text: _("Remove from List"); 70 | } 71 | 72 | Separator {} 73 | 74 | $GtkModelButton btn_uninstall { 75 | text: _("Uninstall"); 76 | } 77 | } 78 | } 79 | 80 | template $ProgramRow: Adw.ActionRow { 81 | title: _("Program name"); 82 | 83 | Box { 84 | spacing: 6; 85 | 86 | Button btn_launch_steam { 87 | tooltip-text: _("Launch with Steam"); 88 | valign: center; 89 | visible: false; 90 | icon-name: "bottles-steam-symbolic"; 91 | 92 | styles [ 93 | "flat", 94 | ] 95 | } 96 | 97 | Button btn_run { 98 | valign: center; 99 | icon-name: "media-playback-start-symbolic"; 100 | 101 | styles [ 102 | "flat", 103 | ] 104 | } 105 | 106 | Button btn_stop { 107 | valign: center; 108 | visible: false; 109 | icon-name: "media-playback-stop-symbolic"; 110 | 111 | styles [ 112 | "flat", 113 | ] 114 | } 115 | 116 | MenuButton btn_menu { 117 | valign: center; 118 | popover: pop_actions; 119 | icon-name: "view-more-symbolic"; 120 | 121 | styles [ 122 | "flat", 123 | ] 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /bottles/frontend/proton-alert-dialog.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template $ProtonAlertDialog: Adw.Window { 5 | title: _("Proton Disclaimer"); 6 | default-width: 500; 7 | default-height: 380; 8 | 9 | Box { 10 | orientation: vertical; 11 | 12 | Adw.HeaderBar { 13 | show-end-title-buttons: false; 14 | 15 | [start] 16 | Button btn_cancel { 17 | label: _("Cancel"); 18 | } 19 | 20 | [end] 21 | Button btn_use { 22 | label: _("Use Proton"); 23 | sensitive: false; 24 | 25 | styles [ 26 | "suggested-action", 27 | ] 28 | } 29 | } 30 | 31 | Label { 32 | margin-top: 10; 33 | margin-start: 10; 34 | margin-end: 10; 35 | wrap: true; 36 | label: _("Beware, using Proton-based runners in non-Steam bottles can cause problems and prevent them from behaving correctly.\n\nWe recommend using Wine-GE rather, a version of Proton meant to run outside of Steam.\n\nProceeding will automatically enable the Steam runtime (if present in the system and detected by Bottles) in order to allow it to access the necessary libraries and limit compatibility problems. Be aware that GloriousEggroll, the runner\'s provider, is not responsible for any problems and we ask that you do not report to them."); 37 | } 38 | 39 | CheckButton check_confirm { 40 | margin-top: 10; 41 | margin-bottom: 10; 42 | margin-start: 10; 43 | margin-end: 10; 44 | label: _("I got it."); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /bottles/frontend/proton_alert_dialog.py: -------------------------------------------------------------------------------- 1 | # proton_alert_dialog.py 2 | # 3 | # Copyright 2025 The Bottles Contributors 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, in version 3 of the License. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | # 17 | 18 | from gi.repository import Gtk, Adw 19 | 20 | 21 | @Gtk.Template(resource_path="/com/usebottles/bottles/proton-alert-dialog.ui") 22 | class ProtonAlertDialog(Adw.Window): 23 | __gtype_name__ = "ProtonAlertDialog" 24 | __resources = {} 25 | 26 | # region Widgets 27 | btn_use = Gtk.Template.Child() 28 | btn_cancel = Gtk.Template.Child() 29 | check_confirm = Gtk.Template.Child() 30 | 31 | # endregion 32 | 33 | def __init__(self, window, callback, **kwargs): 34 | super().__init__(**kwargs) 35 | self.set_transient_for(window) 36 | 37 | self.callback = callback 38 | 39 | # connect signals 40 | self.btn_use.connect("clicked", self.__callback, True) 41 | self.btn_cancel.connect("clicked", self.__callback, False) 42 | self.check_confirm.connect("toggled", self.__toggle_btn_use) 43 | 44 | def __callback(self, _, status): 45 | self.destroy() 46 | self.callback(status) 47 | self.close() 48 | 49 | def __toggle_btn_use(self, widget, *_args): 50 | self.btn_use.set_sensitive(widget.get_active()) 51 | -------------------------------------------------------------------------------- /bottles/frontend/rename-program-dialog.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template $RenameProgramDialog: Adw.Window { 5 | modal: true; 6 | deletable: false; 7 | default-width: 550; 8 | title: _("Rename"); 9 | 10 | Box { 11 | orientation: vertical; 12 | 13 | Adw.HeaderBar { 14 | [start] 15 | Button btn_cancel { 16 | label: _("Cancel"); 17 | } 18 | 19 | [end] 20 | Button btn_save { 21 | label: _("Save"); 22 | 23 | styles [ 24 | "suggested-action", 25 | ] 26 | } 27 | } 28 | 29 | Adw.PreferencesPage { 30 | Adw.PreferencesGroup { 31 | description: _("Choose a new name for the selected program."); 32 | 33 | Adw.EntryRow entry_name { 34 | title: _("New Name"); 35 | } 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /bottles/frontend/rename_program_dialog.py: -------------------------------------------------------------------------------- 1 | # rename_program_dialog.py 2 | # 3 | # Copyright 2025 The Bottles Contributors 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, in version 3 of the License. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | # 17 | 18 | from gi.repository import Gtk, Adw 19 | 20 | 21 | @Gtk.Template(resource_path="/com/usebottles/bottles/rename-program-dialog.ui") 22 | class RenameProgramDialog(Adw.Window): 23 | __gtype_name__ = "RenameProgramDialog" 24 | 25 | # region Widgets 26 | entry_name = Gtk.Template.Child() 27 | btn_cancel = Gtk.Template.Child() 28 | btn_save = Gtk.Template.Child() 29 | ev_controller = Gtk.EventControllerKey.new() 30 | 31 | # endregion 32 | 33 | def __init__(self, window, name, on_save, **kwargs): 34 | super().__init__(**kwargs) 35 | 36 | self.set_transient_for(window) 37 | 38 | # common variables and references 39 | self.window = window 40 | self.manager = window.manager 41 | self.on_save = on_save 42 | 43 | # set widget defaults 44 | self.entry_name.set_text(name) 45 | self.entry_name.add_controller(self.ev_controller) 46 | 47 | # connect signals 48 | self.ev_controller.connect("key-released", self.on_change) 49 | self.btn_cancel.connect("clicked", self.__close_window) 50 | self.btn_save.connect("clicked", self.__on_save) 51 | 52 | def __on_save(self, *_args): 53 | text = self.entry_name.get_text() 54 | self.on_save(new_name=text) 55 | self.destroy() 56 | 57 | def __close_window(self, *_args): 58 | self.destroy() 59 | 60 | def on_change(self, *_args): 61 | self.btn_save.set_sensitive(len(self.entry_name.get_text()) > 0) 62 | -------------------------------------------------------------------------------- /bottles/frontend/sandbox-dialog.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template $SandboxDialog: Adw.Window { 5 | modal: true; 6 | deletable: true; 7 | default-width: 550; 8 | title: _("Sandbox Settings"); 9 | 10 | ShortcutController { 11 | Shortcut { 12 | trigger: "Escape"; 13 | action: "action(window.close)"; 14 | } 15 | } 16 | 17 | Box { 18 | orientation: vertical; 19 | 20 | Adw.HeaderBar {} 21 | 22 | Adw.PreferencesPage { 23 | Adw.PreferencesGroup { 24 | Adw.ActionRow { 25 | title: _("Share Network"); 26 | activatable-widget: switch_net; 27 | 28 | Switch switch_net { 29 | valign: center; 30 | } 31 | } 32 | 33 | Adw.ActionRow { 34 | title: _("Share Sound"); 35 | activatable-widget: switch_sound; 36 | 37 | Switch switch_sound { 38 | valign: center; 39 | } 40 | } 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /bottles/frontend/sandbox_dialog.py: -------------------------------------------------------------------------------- 1 | # sandbox_dialog.py 2 | # 3 | # Copyright 2025 The Bottles Contributors 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, in version 3 of the License. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | # 17 | 18 | from gi.repository import Gtk, Adw 19 | 20 | 21 | @Gtk.Template(resource_path="/com/usebottles/bottles/sandbox-dialog.ui") 22 | class SandboxDialog(Adw.Window): 23 | __gtype_name__ = "SandboxDialog" 24 | 25 | # region Widgets 26 | switch_net = Gtk.Template.Child() 27 | switch_sound = Gtk.Template.Child() 28 | 29 | # endregion 30 | 31 | def __init__(self, window, config, **kwargs): 32 | super().__init__(**kwargs) 33 | self.set_transient_for(window) 34 | 35 | # common variables and references 36 | self.window = window 37 | self.manager = window.manager 38 | self.config = config 39 | self.__update(config) 40 | 41 | # connect signals 42 | self.switch_net.connect("state-set", self.__set_flag, "share_net") 43 | self.switch_sound.connect("state-set", self.__set_flag, "share_sound") 44 | 45 | def __set_flag(self, widget, state, flag): 46 | self.config = self.manager.update_config( 47 | config=self.config, key=flag, value=state, scope="Sandbox" 48 | ).data["config"] 49 | 50 | def __update(self, config): 51 | self.switch_net.set_active(config.Sandbox.share_net) 52 | self.switch_sound.set_active(config.Sandbox.share_sound) 53 | -------------------------------------------------------------------------------- /bottles/frontend/sh.py: -------------------------------------------------------------------------------- 1 | # sh.py 2 | # 3 | # Copyright 2025 The Bottles Contributors 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, in version 3 of the License. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | # 17 | 18 | import re 19 | 20 | _is_name = re.compile(r"""[_a-zA-Z][_a-zA-Z0-9]*""") 21 | 22 | 23 | class ShUtils: 24 | @staticmethod 25 | def is_name(text: str) -> bool: 26 | return bool(_is_name.fullmatch(text)) 27 | 28 | @staticmethod 29 | def split_assignment(text: str) -> tuple[str, str]: 30 | name, _, value = text.partition("=") 31 | return (name, value) 32 | -------------------------------------------------------------------------------- /bottles/frontend/state-row.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template $StateRow: Adw.ActionRow { 5 | activatable-widget: btn_restore; 6 | 7 | /* Translators: id as identification */ 8 | title: _("State id"); 9 | subtitle: _("State comment"); 10 | 11 | Spinner spinner { 12 | visible: false; 13 | } 14 | 15 | Button btn_restore { 16 | tooltip-text: _("Restore this Snapshot"); 17 | valign: center; 18 | icon-name: "document-open-recent-symbolic"; 19 | 20 | styles [ 21 | "flat", 22 | ] 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /bottles/frontend/style-dark.css: -------------------------------------------------------------------------------- 1 | .grade-Platinum { 2 | background-color: #d8d8d8; 3 | color: #000000; 4 | } 5 | 6 | .grade-Gold { 7 | background-color: #8d7637; 8 | color: #f2edde; 9 | } 10 | 11 | .grade-Silver { 12 | background-color: #6b6b6b; 13 | color: #e8e8e8; 14 | } 15 | 16 | .grade-Bronze { 17 | background-color: #5a3f1a; 18 | color: #dbbfa3; 19 | } 20 | 21 | /* Donate button */ 22 | .donate { 23 | color: #d65790; 24 | } 25 | -------------------------------------------------------------------------------- /bottles/frontend/task-row.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template $TaskRow: Adw.ActionRow { 5 | Box { 6 | spacing: 10; 7 | 8 | Spinner spinner_task { 9 | halign: center; 10 | valign: center; 11 | } 12 | 13 | Label label_task_status { 14 | visible: false; 15 | label: "n/a"; 16 | width-chars: 5; 17 | } 18 | 19 | Button btn_cancel { 20 | tooltip-text: _("Delete message"); 21 | halign: center; 22 | valign: center; 23 | 24 | Image { 25 | icon-name: "edit-delete-symbolic"; 26 | } 27 | 28 | styles [ 29 | "circular", 30 | "image-button", 31 | ] 32 | } 33 | } 34 | 35 | styles [ 36 | "message-entry", 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /bottles/frontend/vmtouch-dialog.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template $VmtouchDialog: Adw.Window { 5 | modal: true; 6 | default-width: 550; 7 | title: _("Vmtouch Settings"); 8 | 9 | ShortcutController { 10 | Shortcut { 11 | trigger: "Escape"; 12 | action: "action(window.close)"; 13 | } 14 | } 15 | 16 | Box { 17 | orientation: vertical; 18 | 19 | Adw.HeaderBar { 20 | show-end-title-buttons: false; 21 | 22 | [start] 23 | Button btn_cancel { 24 | label: _("_Cancel"); 25 | use-underline: true; 26 | action-name: "window.close"; 27 | } 28 | 29 | [end] 30 | Button btn_save { 31 | label: _("Save"); 32 | 33 | styles [ 34 | "suggested-action", 35 | ] 36 | } 37 | } 38 | 39 | Adw.PreferencesPage { 40 | Adw.PreferencesGroup { 41 | title: _("Files to cache"); 42 | description: _("Select which files should be cached alongside the main executable."); 43 | 44 | Adw.ActionRow { 45 | title: _("Cache work directory"); 46 | activatable-widget: switch_cache_cwd; 47 | 48 | Switch switch_cache_cwd { 49 | valign: center; 50 | } 51 | } 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /bottles/frontend/vmtouch_dialog.py: -------------------------------------------------------------------------------- 1 | # vmtouch_dialog.py 2 | # 3 | # Copyright 2025 The Bottles Contributors 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | # 18 | # SPDX-License-Identifier: GPL-3.0-only 19 | 20 | from gi.repository import Gtk, GLib, Adw 21 | 22 | 23 | @Gtk.Template(resource_path="/com/usebottles/bottles/vmtouch-dialog.ui") 24 | class VmtouchDialog(Adw.Window): 25 | __gtype_name__ = "VmtouchDialog" 26 | 27 | # region Widgets 28 | switch_cache_cwd = Gtk.Template.Child() 29 | btn_save = Gtk.Template.Child() 30 | btn_cancel = Gtk.Template.Child() 31 | 32 | # endregion 33 | 34 | def __init__(self, window, config, **kwargs): 35 | super().__init__(**kwargs) 36 | self.set_transient_for(window) 37 | 38 | # common variables and references 39 | self.window = window 40 | self.manager = window.manager 41 | self.config = config 42 | 43 | # connect signals 44 | self.btn_save.connect("clicked", self.__save) 45 | 46 | self.__update(config) 47 | 48 | def __update(self, config): 49 | self.switch_cache_cwd.set_active(config.Parameters.vmtouch_cache_cwd) 50 | 51 | def __idle_save(self, *_args): 52 | settings = {"vmtouch_cache_cwd": self.switch_cache_cwd.get_active()} 53 | 54 | for setting in settings.keys(): 55 | self.manager.update_config( 56 | config=self.config, 57 | key=setting, 58 | value=settings[setting], 59 | scope="Parameters", 60 | ) 61 | 62 | self.destroy() 63 | 64 | def __save(self, *_args): 65 | GLib.idle_add(self.__idle_save) 66 | -------------------------------------------------------------------------------- /bottles/frontend/window.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template $BottlesWindow: Adw.ApplicationWindow { 5 | title: "Bottles"; 6 | close-request => $on_close_request(); 7 | 8 | Adw.ToastOverlay toasts { 9 | Adw.Leaflet main_leaf { 10 | can-unfold: false; 11 | can-navigate-back: false; 12 | 13 | Box { 14 | orientation: vertical; 15 | 16 | HeaderBar headerbar { 17 | title-widget: Adw.ViewSwitcherTitle view_switcher_title { 18 | title: "Bottles"; 19 | stack: stack_main; 20 | }; 21 | 22 | Button btn_add { 23 | tooltip-text: _("Create New Bottle"); 24 | icon-name: "list-add-symbolic"; 25 | } 26 | 27 | Box box_actions {} 28 | 29 | styles [ 30 | "titlebar", 31 | ] 32 | 33 | [end] 34 | MenuButton { 35 | icon-name: "open-menu-symbolic"; 36 | menu-model: primary_menu; 37 | tooltip-text: _("Main Menu"); 38 | primary: true; 39 | } 40 | 41 | [end] 42 | ToggleButton btn_search { 43 | tooltip-text: _("Search"); 44 | icon-name: "system-search-symbolic"; 45 | visible: false; 46 | } 47 | 48 | [end] 49 | Button btn_donate { 50 | tooltip-text: _("Donate"); 51 | icon-name: "emblem-favorite-symbolic"; 52 | } 53 | 54 | [end] 55 | Button btn_noconnection { 56 | visible: false; 57 | tooltip-text: _("You don\'t seem connected to the internet. Without it you will not be able to download essential components. Click this icon when you have reestablished the connection."); 58 | icon-name: "network-error-symbolic"; 59 | } 60 | } 61 | 62 | SearchBar searchbar {} 63 | 64 | Adw.ViewStack stack_main { 65 | vexpand: true; 66 | } 67 | 68 | Adw.ViewSwitcherBar view_switcher_bar { 69 | stack: stack_main; 70 | reveal: bind view_switcher_title.title-visible; 71 | } 72 | } 73 | } 74 | } 75 | } 76 | 77 | menu primary_menu { 78 | section { 79 | item { 80 | label: _("_Import…"); 81 | action: "app.import"; 82 | } 83 | } 84 | 85 | section { 86 | item { 87 | label: _("_Preferences"); 88 | action: "app.preferences"; 89 | } 90 | 91 | item { 92 | label: _("_Keyboard Shortcuts"); 93 | action: "win.show-help-overlay"; 94 | } 95 | 96 | item { 97 | label: _("_Help"); 98 | action: "app.help"; 99 | } 100 | 101 | item { 102 | label: _("_About Bottles"); 103 | action: "app.about"; 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /bottles/meson.build: -------------------------------------------------------------------------------- 1 | pkgdatadir = join_paths(get_option('prefix'), get_option('datadir'), meson.project_name()) 2 | moduledir = join_paths(pkgdatadir, 'bottles') 3 | 4 | python = import('python') 5 | 6 | conf = configuration_data() 7 | conf.set('PYTHON', python.find_installation('python3').full_path()) 8 | conf.set('BASE_ID', BASE_ID) 9 | conf.set('APP_ID', APP_ID) 10 | conf.set('APP_NAME', APP_NAME) 11 | conf.set('APP_VERSION', APP_VERSION) 12 | conf.set('APP_MAJOR_VERSION', APP_MAJOR_VERSION) 13 | conf.set('APP_MINOR_VERSION', APP_MINOR_VERSION) 14 | conf.set('PROFILE', PROFILE) 15 | conf.set('localedir', join_paths(get_option('prefix'), get_option('localedir'))) 16 | conf.set('pkgdatadir', pkgdatadir) 17 | 18 | subdir('backend') 19 | subdir('frontend') 20 | 21 | bottles_sources = [ 22 | '__init__.py', 23 | ] 24 | 25 | install_data(bottles_sources, install_dir: moduledir) 26 | -------------------------------------------------------------------------------- /bottles/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bottlesdevs/Bottles/c85edf809a91cd84c82e1aa0548827d691b07a74/bottles/tests/__init__.py -------------------------------------------------------------------------------- /bottles/tests/backend/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bottlesdevs/Bottles/c85edf809a91cd84c82e1aa0548827d691b07a74/bottles/tests/backend/__init__.py -------------------------------------------------------------------------------- /bottles/tests/backend/manager/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bottlesdevs/Bottles/c85edf809a91cd84c82e1aa0548827d691b07a74/bottles/tests/backend/manager/__init__.py -------------------------------------------------------------------------------- /bottles/tests/backend/manager/test_manager.py: -------------------------------------------------------------------------------- 1 | """Core Manager tests""" 2 | 3 | from bottles.backend.managers.manager import Manager 4 | from bottles.backend.utils.gsettings_stub import GSettingsStub 5 | 6 | 7 | def test_manager_is_singleton(): 8 | assert Manager(is_cli=True) is Manager( 9 | is_cli=True 10 | ), "Manager should be singleton object" 11 | assert Manager(is_cli=True) is Manager( 12 | g_settings=GSettingsStub(), is_cli=True 13 | ), "Manager should be singleton even with different argument" 14 | 15 | 16 | def test_manager_default_gsettings_stub(): 17 | assert Manager().settings.get_boolean("anything") is False 18 | -------------------------------------------------------------------------------- /bottles/tests/backend/state/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bottlesdevs/Bottles/c85edf809a91cd84c82e1aa0548827d691b07a74/bottles/tests/backend/state/__init__.py -------------------------------------------------------------------------------- /bottles/tests/backend/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bottlesdevs/Bottles/c85edf809a91cd84c82e1aa0548827d691b07a74/bottles/tests/backend/utils/__init__.py -------------------------------------------------------------------------------- /bottles/tests/backend/utils/test_generic.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from bottles.backend.utils.generic import detect_encoding 4 | 5 | 6 | # CP932 is superset of Shift-JIS, which is default codec for Japanese in Windows 7 | # GBK is default codec for Chinese in Windows 8 | @pytest.mark.parametrize( 9 | "text, hint, codec", 10 | [ 11 | ("Hello, world!", None, "ascii"), 12 | (" ", None, "ascii"), 13 | ("Привет, мир!", None, "windows-1251"), 14 | ("こんにちは、世界!", "ja_JP", "cp932"), 15 | ("こんにちは、世界!", "ja_JP.utf-8", "utf-8"), 16 | ("你好,世界!", "zh_CN", "gbk"), 17 | ("你好,世界!", "zh_CN.UTF-8", "utf-8"), 18 | ("你好,世界!", "zh_CN.invalid_fallback", "gbk"), 19 | ("", None, "utf-8"), 20 | ], 21 | ) 22 | def test_detect_encoding(text: str, hint: str | None, codec: str | None): 23 | text_bytes = text.encode(codec) 24 | guess = detect_encoding(text_bytes, hint) 25 | assert guess.lower() == codec.lower() 26 | -------------------------------------------------------------------------------- /build-aux/install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | BUILD_DIR="build/" 3 | if [ -d "$BUILD_DIR" ]; then 4 | rm -r build 5 | fi 6 | mkdir build 7 | meson --prefix=$PWD/build build 8 | ninja -j$(nproc) -C build install 9 | -------------------------------------------------------------------------------- /data/com.usebottles.bottles.desktop.in.in: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=@APP_NAME@ 3 | Comment=Run Windows software 4 | Icon=@APP_ID@ 5 | Exec=bottles %u 6 | TryExec=bottles 7 | Terminal=false 8 | Type=Application 9 | Categories=Utility;Game;Graphics;3DGraphics;Emulator;GNOME;GTK; 10 | StartupNotify=true 11 | StartupWMClass=bottles 12 | MimeType=x-scheme-handler/bottles;application/x-ms-dos-executable;application/x-msi;application/x-ms-shortcut;application/x-wine-extension-msp; 13 | Keywords=wine;windows;gaming;emulate;emulator;game; 14 | X-GNOME-UsesNotifications=true 15 | -------------------------------------------------------------------------------- /data/icons/hicolor/symbolic/actions/application-x-addon-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /data/icons/hicolor/symbolic/actions/computer-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /data/icons/hicolor/symbolic/actions/document-save-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /data/icons/hicolor/symbolic/actions/external-link-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /data/icons/hicolor/symbolic/actions/go-next-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /data/icons/hicolor/symbolic/actions/go-previous-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /data/icons/hicolor/symbolic/actions/info-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /data/icons/hicolor/symbolic/actions/library-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /data/icons/hicolor/symbolic/actions/list-add-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /data/icons/hicolor/symbolic/actions/media-playback-start-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /data/icons/hicolor/symbolic/actions/media-playback-stop-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /data/icons/hicolor/symbolic/actions/open-menu-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /data/icons/hicolor/symbolic/actions/paper-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /data/icons/hicolor/symbolic/actions/preferences-desktop-apps-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /data/icons/hicolor/symbolic/actions/preferences-system-time-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /data/icons/hicolor/symbolic/actions/selection-mode-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /data/icons/hicolor/symbolic/actions/system-search-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /data/icons/hicolor/symbolic/actions/system-shutdown-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /data/icons/hicolor/symbolic/actions/system-software-install-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /data/icons/hicolor/symbolic/actions/view-more-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /data/icons/hicolor/symbolic/actions/warning-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /data/icons/hicolor/symbolic/apps/bottle-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /data/icons/hicolor/symbolic/apps/bottles-steam-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /data/icons/hicolor/symbolic/apps/com.usebottles.bottles-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 10 | 12 | 16 | 20 | 24 | 25 | -------------------------------------------------------------------------------- /data/icons/hicolor/symbolic/apps/com.usebottles.bottles.Devel-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 10 | 12 | 16 | 20 | 24 | 25 | -------------------------------------------------------------------------------- /data/icons/meson.build: -------------------------------------------------------------------------------- 1 | scalable_dir = 'hicolor' / 'scalable' / 'apps' 2 | install_data( 3 | scalable_dir / ('@0@.svg').format(APP_ID), 4 | install_dir: get_option('datadir') / 'icons' / scalable_dir 5 | ) 6 | 7 | symbolic_dir = 'hicolor' / 'symbolic' / 'apps' 8 | install_data( 9 | symbolic_dir / ('@0@-symbolic.svg').format(APP_ID), 10 | install_dir: get_option('datadir') / 'icons' / symbolic_dir 11 | ) 12 | -------------------------------------------------------------------------------- /data/images/bottles-welcome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bottlesdevs/Bottles/c85edf809a91cd84c82e1aa0548827d691b07a74/data/images/bottles-welcome.png -------------------------------------------------------------------------------- /data/meson.build: -------------------------------------------------------------------------------- 1 | conf = configuration_data() 2 | conf.set('APP_ID', APP_ID) 3 | conf.set('BASE_ID', BASE_ID) 4 | conf.set('APP_NAME', APP_NAME) 5 | conf.set('DEVELOPER_ID', DEVELOPER_ID) 6 | 7 | desktop = i18n.merge_file( 8 | input: configure_file( 9 | input: BASE_ID + '.desktop.in.in', 10 | output: BASE_ID + '.desktop.in', 11 | configuration: conf 12 | ), 13 | output: APP_ID + '.desktop', 14 | type: 'desktop', 15 | po_dir: '../po', 16 | install: true, 17 | install_dir: join_paths(get_option('datadir'), 'applications') 18 | ) 19 | 20 | desktop_utils = find_program('desktop-file-validate', required: false) 21 | if desktop_utils.found() 22 | test('Validate desktop file', desktop_utils, 23 | args: [desktop] 24 | ) 25 | endif 26 | 27 | appstream_file = i18n.merge_file( 28 | input: configure_file( 29 | input: BASE_ID + '.' + 'metainfo.xml.in.in', 30 | output: BASE_ID + '.' + 'metainfo.xml.in', 31 | configuration: conf 32 | ), 33 | output: APP_ID + '.' + 'metainfo.xml', 34 | po_dir: '../po', 35 | install: true, 36 | install_dir: get_option('datadir') / 'metainfo' 37 | ) 38 | 39 | gnome.compile_resources('data', 40 | configure_file( 41 | input: 'data.gresource.xml.in', 42 | output: 'data.gresource.xml', 43 | configuration: conf, 44 | ), 45 | gresource_bundle: true, 46 | install: true, 47 | install_dir: pkgdatadir, 48 | dependencies: [appstream_file] 49 | ) 50 | 51 | appstream_util = find_program('appstream-util', required: false) 52 | if appstream_util.found() 53 | test('Validate appstream file', appstream_util, 54 | args: ['validate', appstream_file] 55 | ) 56 | endif 57 | 58 | install_data('com.usebottles.bottles.gschema.xml', 59 | install_dir: join_paths(get_option('datadir'), 'glib-2.0/schemas') 60 | ) 61 | 62 | compile_schemas = find_program('glib-compile-schemas', required: false) 63 | if compile_schemas.found() 64 | test('Validate schema file', compile_schemas, 65 | args: ['--strict', '--dry-run', meson.current_source_dir()] 66 | ) 67 | endif 68 | 69 | subdir('icons') 70 | -------------------------------------------------------------------------------- /data/screenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bottlesdevs/Bottles/c85edf809a91cd84c82e1aa0548827d691b07a74/data/screenshots/1.png -------------------------------------------------------------------------------- /data/screenshots/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bottlesdevs/Bottles/c85edf809a91cd84c82e1aa0548827d691b07a74/data/screenshots/2.png -------------------------------------------------------------------------------- /data/screenshots/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bottlesdevs/Bottles/c85edf809a91cd84c82e1aa0548827d691b07a74/data/screenshots/3.png -------------------------------------------------------------------------------- /data/screenshots/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bottlesdevs/Bottles/c85edf809a91cd84c82e1aa0548827d691b07a74/data/screenshots/4.png -------------------------------------------------------------------------------- /data/screenshots/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bottlesdevs/Bottles/c85edf809a91cd84c82e1aa0548827d691b07a74/data/screenshots/5.png -------------------------------------------------------------------------------- /data/screenshots/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bottlesdevs/Bottles/c85edf809a91cd84c82e1aa0548827d691b07a74/data/screenshots/6.png -------------------------------------------------------------------------------- /docs/screenshot-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bottlesdevs/Bottles/c85edf809a91cd84c82e1aa0548827d691b07a74/docs/screenshot-dark.png -------------------------------------------------------------------------------- /docs/screenshot-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bottlesdevs/Bottles/c85edf809a91cd84c82e1aa0548827d691b07a74/docs/screenshot-light.png -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project( 2 | 'bottles', 3 | version: '51.18', 4 | meson_version: '>= 1.5.0', 5 | default_options: [ 6 | 'warning_level=2', 7 | ], 8 | license: 'GPL-3.0-only' 9 | ) 10 | 11 | 12 | APP_NAME = 'Bottles' 13 | DEVELOPER_ID = 'com.usebottles' 14 | BASE_ID = DEVELOPER_ID + '.' + meson.project_name() 15 | APP_ID = BASE_ID 16 | APP_VERSION = meson.project_version() 17 | _version_array = APP_VERSION.split('.') 18 | APP_MAJOR_VERSION = _version_array[0] 19 | APP_MINOR_VERSION = _version_array[1] 20 | PROFILE = get_option('profile') 21 | 22 | if PROFILE == 'development' 23 | APP_VERSION += '-' + run_command( 24 | 'git', 'rev-parse', '--short', 'HEAD', 25 | check: true 26 | ).stdout().strip() 27 | APP_NAME += ' (Development)' 28 | APP_ID += '.' + 'Devel' 29 | endif 30 | 31 | 32 | gnome = import('gnome') 33 | i18n = import('i18n') 34 | localedir = get_option('localedir') 35 | 36 | subdir('po') 37 | subdir('bottles') 38 | subdir('data') 39 | 40 | gnome.post_install( 41 | glib_compile_schemas: true, 42 | gtk_update_icon_cache: true, 43 | update_desktop_database: true, 44 | ) 45 | -------------------------------------------------------------------------------- /meson_options.txt: -------------------------------------------------------------------------------- 1 | option ( 2 | 'profile', 3 | type: 'combo', 4 | choices: [ 5 | 'default', 6 | 'development' 7 | ], 8 | value: 'default' 9 | ) 10 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | # TODO This errors needs to be fixed one by one 3 | disable_error_code = assignment, index, valid-type, misc, union-attr, arg-type, operator, call-overload, var-annotated, attr-defined 4 | -------------------------------------------------------------------------------- /po/LINGUAS: -------------------------------------------------------------------------------- 1 | it 2 | fr 3 | de 4 | hi 5 | pt 6 | es 7 | nb_NO 8 | pt_BR 9 | id 10 | da 11 | nl 12 | tr 13 | sv 14 | ru 15 | eo 16 | zh_Hans 17 | fi 18 | ja 19 | hr 20 | cs 21 | uk 22 | hu 23 | pl 24 | zh_Hant 25 | ko 26 | vi 27 | eu 28 | bg 29 | el 30 | gl 31 | sk 32 | ro 33 | ms 34 | ckb 35 | fa 36 | th 37 | ar 38 | bn 39 | sl 40 | ca 41 | lt 42 | sr 43 | et 44 | ta 45 | he 46 | be 47 | ie 48 | az 49 | bs 50 | ga 51 | ka 52 | -------------------------------------------------------------------------------- /po/README.md: -------------------------------------------------------------------------------- 1 | # Help translating Bottles :rocket: 2 | Help Bottles get translated in your language! 3 | 4 | ## Improve a translation :raising_hand: 5 | If you've found typos or just think you can improve a translation, contribute 6 | using the [Weblate](https://hosted.weblate.org/engage/bottles/) platform. 7 | 8 | Please, this is an open source, free, free project. Don't vandalize the 9 | translations, it's not funny, it's idiotic. 10 | 11 | ## Thanks! :two_hearts: :tada: 12 | A heartfelt thanks to anyone who wants to help us get Bottles to speak any language! 13 | -------------------------------------------------------------------------------- /po/meson.build: -------------------------------------------------------------------------------- 1 | i18n = import('i18n') 2 | fs = import('fs') 3 | 4 | # Add legacy language code alias 5 | # Fix missing locale for Chinese 6 | # See also: https://docs.weblate.org/en/latest/faq.html#why-does-weblate-use-language-codes-such-sr-latn-or-zh-hant 7 | # And: https://github.com/bottlesdevs/Bottles/issues/1692 8 | language_list = fs.read('LINGUAS').strip().split('\n') 9 | language_list += ['zh_CN', 'zh_HK', 'zh_SG', 'zh_TW'] 10 | 11 | i18n.gettext( 12 | 'bottles', 13 | install_dir: localedir, 14 | preset: 'glib', 15 | args: '--from-code=UTF-8', 16 | languages: language_list 17 | ) 18 | -------------------------------------------------------------------------------- /po/zh_CN.po: -------------------------------------------------------------------------------- 1 | zh_Hans.po -------------------------------------------------------------------------------- /po/zh_HK.po: -------------------------------------------------------------------------------- 1 | zh_Hant.po -------------------------------------------------------------------------------- /po/zh_SG.po: -------------------------------------------------------------------------------- 1 | zh_Hans.po -------------------------------------------------------------------------------- /po/zh_TW.po: -------------------------------------------------------------------------------- 1 | zh_Hant.po -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.pytest.ini_options] 2 | log_cli = true 3 | log_cli_level = "INFO" 4 | log_cli_format = "%(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s)" 5 | 6 | [tool.ruff] 7 | exclude = [ 8 | "bottles/backend/utils/nvidia.py", 9 | "bottles/backend/utils/vdf.py", 10 | ] 11 | 12 | [tool.ruff.lint] 13 | ignore = ["F401", "F402", "E722", "E741"] 14 | -------------------------------------------------------------------------------- /requirements.dev.txt: -------------------------------------------------------------------------------- 1 | # Updated using pur -r requirements.txt 2 | pytest==8.3.4 3 | requirements-parser==0.11.0 4 | mypy==1.15.0 5 | types_Markdown 6 | types-PyYAML 7 | types-Pygments 8 | types_Pygments 9 | types_colorama 10 | types_pycurl 11 | types_requests 12 | types_docutils 13 | pylint 14 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # Updated using pur -r requirements.txt 2 | wheel==0.45.1 3 | PyYAML==6.0.2 4 | pycurl==7.45.4 5 | chardet==5.2.0 6 | requests[use_chardet_on_py3]==2.32.3 7 | Markdown==3.7 8 | icoextract==0.1.5 9 | patool==3.1.3 10 | pathvalidate==3.2.3 11 | FVS==0.3.4 12 | orjson==3.10.15 13 | pycairo==1.27.0 14 | PyGObject==3.50.0 15 | charset-normalizer==3.4.1 16 | numpy==2.2.3 17 | pyfluidsynth==1.3.4 18 | idna==3.10 19 | urllib3==2.3.0 20 | certifi==2025.1.31 21 | pefile==2024.8.26 22 | --------------------------------------------------------------------------------