├── .github └── workflows │ ├── build-flatpost-f41.yml │ ├── build-flatpost-publish-release.yml │ ├── build-flatpost-version.yml │ ├── build.yml │ └── release.yml ├── Makefile ├── README.md ├── VERSION.txt ├── data └── usr │ └── share │ ├── applications │ └── com.flatpost.flatpostapp.desktop │ ├── flatpost │ └── collections_data.json │ ├── icons │ └── hicolor │ │ ├── 1024x1024 │ │ └── apps │ │ │ └── com.flatpost.flatpostapp.png │ │ └── 64x64 │ │ └── apps │ │ └── com.flatpost.flatpostapp.png │ ├── licenses │ └── flatpost │ │ └── LICENSE │ └── mime │ └── packages │ └── flatpost.xml ├── packaging └── rpm │ └── flatpost.spec ├── screenshots ├── flatshop_agnostic.png ├── flatshop_agnostic2.png ├── flatshop_agnostic3.png └── flatshop_agnostic4.png └── src ├── flatpost.py └── fp_turbo.py /.github/workflows/build-flatpost-f41.yml: -------------------------------------------------------------------------------- 1 | name: Flatpost RPM Build - Fedora/Nobara 41 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | version: 7 | required: false 8 | type: string 9 | shasum: 10 | required: false 11 | type: string 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | container: 17 | image: fedora:41 18 | 19 | steps: 20 | - name: Install Git 21 | run: dnf install -y git 22 | 23 | - name: Checkout repository 24 | uses: actions/checkout@v4 25 | with: 26 | fetch-depth: 0 27 | 28 | - name: Configure Git safe directory 29 | run: git config --global --add safe.directory "$GITHUB_WORKSPACE" 30 | 31 | - name: Install build dependencies 32 | run: dnf install -y git rpm-build make python3-devel python3-build desktop-file-utils 33 | 34 | - name: Extract Version and SHA 35 | run: | 36 | # Get version from git tags (assuming semantic versioning format) 37 | VERSION=$(git describe --tags --abbrev=0 || echo "unknown") 38 | echo $VERSION 39 | 40 | # Get current commit SHA 41 | COMMIT_SHA=$(git rev-parse HEAD) 42 | echo $COMMIT_SHA 43 | 44 | # Store values in environment file 45 | echo "VERSION=$VERSION" >> $GITHUB_ENV 46 | echo "COMMIT_SHA=$COMMIT_SHA" >> $GITHUB_ENV 47 | 48 | - name: Build the project 49 | run: | 50 | # Use either provided input or extracted value 51 | git submodule update --init --recursive 52 | 53 | cd .. 54 | mkdir -p ~/rpmbuild/SOURCES/ 55 | cp -R flatpost flatpost-${{ env.VERSION }}/ 56 | tar -cvzf flatpost-${{ env.VERSION }}.tar.gz flatpost-${{ env.VERSION }} 57 | mv flatpost-${{ env.VERSION }}.tar.gz ~/rpmbuild/SOURCES/ 58 | rm -Rf flatpost-${{ env.VERSION }}/ 59 | cd flatpost/ 60 | 61 | sed -i "s|^%global tag .*|%global tag ${{ env.VERSION }}|g" packaging/rpm/flatpost.spec 62 | cat packaging/rpm/flatpost.spec | grep tag 63 | 64 | sed -i "s|^%global commit .*|%global commit ${{ env.COMMIT_SHA }}|g" packaging/rpm/flatpost.spec 65 | cat packaging/rpm/flatpost.spec | grep commit 66 | 67 | rpmbuild -ba packaging/rpm/flatpost.spec 68 | mv ~/rpmbuild/RPMS/noarch/flatpost-${{ env.VERSION }}*.rpm \ 69 | ~/rpmbuild/RPMS/flatpost-${{ env.VERSION }}.fc41.rpm 70 | mv ~/rpmbuild/SRPMS/flatpost-${{ env.VERSION }}*.src.rpm \ 71 | ~/rpmbuild/RPMS/flatpost-${{ env.VERSION }}.fc41.src.rpm 72 | 73 | - name: Upload Artifact 74 | uses: actions/upload-artifact@v4 75 | with: 76 | name: flatpost-${{ env.VERSION }}.fc41.rpm 77 | path: ~/rpmbuild/RPMS/flatpost-${{ env.VERSION }}.fc41.rpm 78 | 79 | - name: Upload Artifact 80 | uses: actions/upload-artifact@v4 81 | with: 82 | name: flatpost-${{ env.VERSION }}.fc41.src.rpm 83 | path: ~/rpmbuild/RPMS/flatpost-${{ env.VERSION }}.fc41.src.rpm 84 | -------------------------------------------------------------------------------- /.github/workflows/build-flatpost-publish-release.yml: -------------------------------------------------------------------------------- 1 | name: Flatpost Release 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | version: 7 | required: true 8 | type: string 9 | file1: 10 | required: true 11 | type: string 12 | name1: 13 | required: true 14 | type: string 15 | name2: 16 | type: string 17 | default: "" 18 | file2: 19 | type: string 20 | default: "" 21 | 22 | 23 | jobs: 24 | release: 25 | name: Publish 26 | runs-on: ubuntu-latest 27 | strategy: 28 | matrix: 29 | include: 30 | - name: ${{ inputs.name1 }} 31 | file: ${{ inputs.file1 }} 32 | - name: ${{ inputs.name2 }} 33 | file: ${{ inputs.file2 }} 34 | steps: 35 | - name: Download ${{ matrix.name }} from artifact 36 | uses: actions/download-artifact@v4 37 | if: ${{ matrix.name != '' }} 38 | with: 39 | name: ${{ matrix.name }} 40 | - name: Upload ${{ matrix.name }} to release 41 | uses: svenstaro/upload-release-action@v2 42 | if: ${{ matrix.name != '' }} 43 | with: 44 | repo_token: ${{ secrets.GITHUB_TOKEN }} 45 | file: ${{ matrix.file }} 46 | asset_name: ${{ matrix.name }} 47 | tag: ${{ inputs.version }} 48 | overwrite: true 49 | -------------------------------------------------------------------------------- /.github/workflows/build-flatpost-version.yml: -------------------------------------------------------------------------------- 1 | name: Flatpost Describe Version 2 | 3 | on: 4 | workflow_call: 5 | outputs: 6 | version: 7 | value: "${{ jobs.version.outputs.tag_abbrev }}.${{ jobs.version.outputs.tag_offset }}" 8 | branch: 9 | value: "${{ jobs.version.outputs.branch }}" 10 | 11 | 12 | jobs: 13 | version: 14 | name: Version 15 | runs-on: ubuntu-latest 16 | outputs: 17 | tag_abbrev: ${{ steps.describe.outputs.tag_abbrev }} 18 | tag_offset: ${{ steps.describe.outputs.tag_offset }} 19 | sha_short: ${{ steps.describe.outputs.sha_short }} 20 | full_desc: ${{ steps.describe.outputs.full_desc }} 21 | branch: ${{ steps.describe.outputs.branch }} 22 | steps: 23 | - uses: actions/checkout@v4 24 | with: 25 | fetch-depth: 0 26 | - name: Describe 27 | id: describe 28 | shell: bash 29 | run: | 30 | tag_abbrev=$(git tag --sort=v:refname | grep -oE "(^[0-9]+\.[0-9]+(.[0-9]+)?)$" | tail -1) 31 | echo "tag_abbrev=$tag_abbrev" >> $GITHUB_OUTPUT 32 | echo "tag_offset=$(git rev-list $tag_abbrev..HEAD --count)" >> $GITHUB_OUTPUT 33 | echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT 34 | echo "full_desc=$(git describe --long --tags)" >> $GITHUB_OUTPUT 35 | echo "branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> $GITHUB_OUTPUT 36 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build and upload development artifacts 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | branches: ["main"] 8 | 9 | jobs: 10 | version: 11 | name: Describe 12 | uses: ./.github/workflows/build-flatpost-version.yml 13 | 14 | fedora41-build: 15 | name: Fedora 41 Build 16 | uses: ./.github/workflows/build-flatpost-f41.yml 17 | needs: version 18 | with: 19 | version: ${{ needs.version.outputs.version }} 20 | shasum: ${{ github.sha }} 21 | 22 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Build and upload release 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | fedora41-build: 9 | name: Fedora 41 Build 10 | uses: ./.github/workflows/build-flatpost-f41.yml 11 | with: 12 | version: ${{ github.ref_name }} 13 | shasum: ${{ github.sha }} 14 | fedora41-release: 15 | name: Fedora 41 Release ${{ github.ref_name }} 16 | needs: fedora41-build 17 | uses: ./.github/workflows/build-flatpost-publish-release.yml 18 | with: 19 | version: ${{ github.ref_name }} 20 | file1: flatpost-${{ github.ref_name }}.fc41.rpm 21 | name1: flatpost-${{ github.ref_name }}.fc41.rpm 22 | file2: flatpost-${{ github.ref_name }}.fc41.src.rpm 23 | name2: flatpost-${{ github.ref_name }}.fc41.src.rpm 24 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PYTHON_SITE_PACKAGES := $(shell python3 -c "import site; print(site.getsitepackages()[0].replace('lib64', 'lib'))") 2 | TARGET_DIR := $(DESTDIR)$(PYTHON_SITE_PACKAGES)/flatpost 3 | BIN_DIR := $(DESTDIR)/usr/bin 4 | DESKTOP_DIR := $(DESTDIR)/usr/share/applications 5 | MIME_DIR := $(DESTDIR)/usr/share/mime/packages 6 | DATA_DIR := $(DESTDIR)/usr/share/flatpost 7 | ICON_DIR := $(DESTDIR)/usr/share/icons/hicolor 8 | LICENSE_DIR := $(DESTDIR)/usr/share/licenses/flatpost 9 | VERSION := $(shell cat VERSION.txt) 10 | 11 | .PHONY: all update-version install clean 12 | 13 | all: update-version install 14 | 15 | update-version: 16 | sed -i 's/^Version .*/Version $(VERSION)/' src/flatpost.py 17 | install: 18 | @echo "Installing Python files to $(TARGET_DIR)" 19 | mkdir -p $(TARGET_DIR) 20 | install -m 644 src/fp_turbo.py $(TARGET_DIR)/fp_turbo.py 21 | 22 | @echo "Main executable file to $(BIN_DIR)" 23 | mkdir -p $(BIN_DIR) 24 | install -m 755 src/flatpost.py $(BIN_DIR)/flatpost 25 | 26 | @echo "Installing desktop file to $(DESKTOP_DIR)" 27 | mkdir -p $(DESKTOP_DIR) 28 | install -m 644 data/usr/share/applications/com.flatpost.flatpostapp.desktop $(DESKTOP_DIR)/com.flatpost.flatpostapp.desktop 29 | 30 | @echo "Installing MIME file to $(MIME_DIR)" 31 | mkdir -p $(MIME_DIR) 32 | install -m 644 data/usr/share/mime/packages/flatpost.xml $(MIME_DIR)/flatpost.xml 33 | 34 | @echo "Installing data files to $(DATA_DIR)" 35 | mkdir -p $(DATA_DIR) 36 | install -m 644 data/usr/share/flatpost/collections_data.json $(DATA_DIR)/collections_data.json 37 | 38 | @echo "Installing icon file to $(ICON_DIR)" 39 | mkdir -p $(ICON_DIR)/{1024x1024,64x64}/apps 40 | install -m 644 data/usr/share/icons/hicolor/1024x1024/apps/com.flatpost.flatpostapp.png $(ICON_DIR)/1024x1024/apps/com.flatpost.flatpostapp.png 41 | install -m 644 data/usr/share/icons/hicolor/64x64/apps/com.flatpost.flatpostapp.png $(ICON_DIR)/64x64/apps/com.flatpost.flatpostapp.png 42 | 43 | @echo "Installing license file to $(LICENSE_DIR)" 44 | mkdir -p $(LICENSE_DIR) 45 | install -m 644 data/usr/share/licenses/flatpost/LICENSE $(LICENSE_DIR)/LICENSE 46 | 47 | clean: 48 | @echo "Cleaning up installed files" 49 | rm -rf $(TARGET_DIR) 50 | rm -f $(BIN_DIR)/flatpost 51 | rm -f $(DESKTOP_DIR)/com.flatpost.flatpostapp.desktop 52 | rm -f $(DATA_DIR)/collections_data.json 53 | rm -f $(ICON_DIR)/1024x1024/apps/com.flatpost.flatpostapp.png 54 | rm -f $(ICON_DIR)/64x64/apps/com.flatpost.flatpostapp.png 55 | rm -f $(LICENSE_DIR)/com.flatpost.flatpostapp.png 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![screenshot](screenshots/flatshop_agnostic.png) 2 | ![screenshot](screenshots/flatshop_agnostic2.png) 3 | ![screenshot](screenshots/flatshop_agnostic3.png) 4 | ![screenshot](screenshots/flatshop_agnostic4.png) 5 | 6 | 7 | I wanted a desktop environment agnostic flatpak store that didn't require pulling in gnome or kde dependencies. 8 | 9 | Built with python, gtk, libflatpak, appstream 10 | 11 | 12 | All basic flatpak functionality implementation is done. 13 | 14 | DONE: 15 | - Appstream metadata loading and search 16 | - Appstream metadata refresh 17 | - Collections metadata loading and search 18 | - Collections metadata refresh 19 | - Repository management functions 20 | - Repository management GUI 21 | - Installed package query functions 22 | - Available updates package query functions 23 | - GUI layout of system, collections, and categories entries. 24 | - GUI layout of application list 25 | - GUI layout of buttons 26 | - GUI layout of search 27 | - Donate/Support button and function. 28 | - Install button functions 29 | - Remove button functions 30 | - System mode backend 31 | - Search function 32 | - System mode toggle 33 | - Update button functions 34 | - Implement subcategories 35 | - Implement kind sorting in the installed/updates sections (desktop_app, addon, runtime, other..) 36 | - Implement kind sorting dropdown for current page 37 | - Implement kind sorting search filter 38 | - Refresh metadata button 39 | - Add install from .flatpakref functionality + drag and drop 40 | - Add install from .flatpakrepo functionality + drag and drop 41 | - Add per-app permission management backend 42 | - Add global permission management backend 43 | - Add per-app permission management GUI 44 | - Add global permission management GUI 45 | - Add Package information page/section. 46 | - Cleanup permissions GUI 47 | - Add permissions override viewing inside per-app permissions view. 48 | - Add 'update all' functionality. 49 | - add about section 50 | - General GUI layout/theming improvements 51 | 52 | TODO: 53 | - Document fp_turbo functions 54 | 55 | 56 | The fp_turbo.py library can double as a standalone CLI tool: 57 | ``` 58 | ./fp_turbo.py -h 59 | usage: fp_turbo.py [-h] [--id ID] [--repo REPO] [--list-all] [--categories] [--subcategories] [--list-installed] [--check-updates] [--list-repos] [--add-repo REPO_FILE] [--remove-repo REPO_NAME] [--toggle-repo ENABLE/DISABLE] 60 | [--install APP_ID] [--remove APP_ID] [--update APP_ID] [--system] [--refresh] [--refresh-local] [--add-file-perms PATH] [--remove-file-perms PATH] [--list-file-perms] [--list-other-perm-toggles PERM_NAME] 61 | [--toggle-other-perms ENABLE/DISABLE] [--perm-type PERM_TYPE] [--perm-option PERM_OPTION] [--list-other-perm-values PERM_NAME] [--add-other-perm-values TYPE] [--remove-other-perm-values TYPE] [--perm-value VALUE] 62 | [--override] [--global-add-file-perms PATH] [--global-remove-file-perms PATH] [--global-list-file-perms] [--global-list-other-perm-toggles PERM_NAME] [--global-toggle-other-perms ENABLE/DISABLE] 63 | [--global-list-other-perm-values PERM_NAME] [--global-add-other-perm-values TYPE] [--global-remove-other-perm-values TYPE] [--get-app-portal-permissions] [--get-portal-permissions TYPE] [--get-all-portal-permissions] 64 | [--set-app-portal-permissions TYPE] [--portal-perm-value TYPE] 65 | 66 | Search Flatpak packages 67 | 68 | options: 69 | -h, --help show this help message and exit 70 | --id ID Application ID to search for 71 | --repo REPO Filter results to specific repository 72 | --list-all List all available apps 73 | --categories Show apps grouped by category 74 | --subcategories Show apps grouped by subcategory 75 | --list-installed List all installed Flatpak applications 76 | --check-updates Check for available updates 77 | --list-repos List all configured Flatpak repositories 78 | --add-repo REPO_FILE Add a new repository from a .flatpakrepo file 79 | --remove-repo REPO_NAME 80 | Remove a Flatpak repository 81 | --toggle-repo ENABLE/DISABLE 82 | Enable or disable a repository 83 | --install APP_ID Install a Flatpak package 84 | --remove APP_ID Remove a Flatpak package 85 | --update APP_ID Update a Flatpak package 86 | --update-all Apply all available updates 87 | --system Install as system instead of user 88 | --refresh Install as system instead of user 89 | --refresh-local Install as system instead of user 90 | --add-file-perms PATH 91 | Add file permissions to an app (e.g. any defaults: host, host-os, host-etc, home, or "/path/to/directory" for custom paths) 92 | --remove-file-perms PATH 93 | Remove file permissions from an app (e.g. any defaults: host, host-os, host-etc, home, or "/path/to/directory" for custom paths) 94 | --list-file-perms List configured file permissions for an app 95 | --list-other-perm-toggles PERM_NAME 96 | List configured other permission toggles for an app (e.g. "shared", "sockets", "devices", "features", "persistent") 97 | --toggle-other-perms ENABLE/DISABLE 98 | Toggle other permissions on/off (True/False) 99 | --perm-type PERM_TYPE 100 | Type of permission to toggle (shared, sockets, devices, features) 101 | --perm-option PERM_OPTION 102 | Specific permission option to toggle (e.g. network, ipc) 103 | --list-other-perm-values PERM_NAME 104 | List configured other permission group values for an app (e.g. "environment", "session_bus", "system_bus") 105 | --add-other-perm-values TYPE 106 | Add a permission value (e.g. "environment", "session_bus", "system_bus") 107 | --remove-other-perm-values TYPE 108 | Remove a permission value (e.g. "environment", "session_bus", "system_bus") 109 | --perm-value VALUE The complete permission value to add or remove (e.g. "XCURSOR_PATH=/run/host/user-share/icons:/run/host/share/icons") 110 | --override Set global permission override instead of per-application 111 | --global-add-file-perms PATH 112 | Add file permissions to an app (e.g. any defaults: host, host-os, host-etc, home, or "/path/to/directory" for custom paths) 113 | --global-remove-file-perms PATH 114 | Remove file permissions from an app (e.g. any defaults: host, host-os, host-etc, home, or "/path/to/directory" for custom paths) 115 | --global-list-file-perms 116 | List configured file permissions for an app 117 | --global-list-other-perm-toggles PERM_NAME 118 | List configured other permission toggles for an app (e.g. "shared", "sockets", "devices", "features", "persistent") 119 | --global-toggle-other-perms ENABLE/DISABLE 120 | Toggle other permissions on/off (True/False) 121 | --global-list-other-perm-values PERM_NAME 122 | List configured other permission group values for an app (e.g. "environment", "session_bus", "system_bus") 123 | --global-add-other-perm-values TYPE 124 | Add a permission value (e.g. "environment", "session_bus", "system_bus") 125 | --global-remove-other-perm-values TYPE 126 | Remove a permission value (e.g. "environment", "session_bus", "system_bus") 127 | --get-app-portal-permissions 128 | Check specified portal permissions (e.g. "background", "notifications", "microphone", "speakers", "camera", "location") for a specified application ID. 129 | --get-portal-permissions TYPE 130 | List all current portal permissions for all applications 131 | --get-all-portal-permissions 132 | List all current portal permissions for all applications 133 | --set-app-portal-permissions TYPE 134 | Set specified portal permissions (e.g. "background", "notifications", "microphone", "speakers", "camera", "location") yes/no for a specified application ID. 135 | --portal-perm-value TYPE 136 | Set specified portal permissions value (yes/no) for a specified application ID. 137 | ``` 138 | 139 | Common CLI combinations: 140 | ``` 141 | ./fp_turbo.py --id 142 | ./fp_turbo.py --id --repo flatpak beta 143 | ./fp_turbo.py --id --repo flatpak-beta --system 144 | ./fp_turbo.py --list-all 145 | ./fp_turbo.py --list-all --system 146 | ./fp_turbo.py --categories 147 | ./fp_turbo.py --categories --system 148 | ./fp_turbo.py --subcategories 149 | ./fp_turbo.py --subcategories --system 150 | ./fp_turbo.py --list-installed 151 | ./fp_turbo.py --list-installed --system 152 | ./fp_turbo.py --check-updates 153 | ./fp_turbo.py --check-updates --system 154 | ./fp_turbo.py --list-repos 155 | ./fp_turbo.py --list-repos --system 156 | ./fp_turbo.py --add-repo <.flatpakrepo or url to .flatpakrepo file> 157 | ./fp_turbo.py --add-repo <.flatpakrepo or url to .flatpakrepo file> --system 158 | ./fp_turbo.py --remove-repo 159 | ./fp_turbo.py --remove-repo --system 160 | ./fp_turbo.py --toggle-repo --repo 161 | ./fp_turbo.py --toggle-repo --repo --system 162 | ./fp_turbo.py --install 163 | ./fp_turbo.py --install --repo 164 | ./fp_turbo.py --install --repo --system 165 | ./fp_turbo.py --remove 166 | ./fp_turbo.py --remove --system 167 | ./fp_turbo.py --update 168 | ./fp_turbo.py --update --system 169 | ./fp_turbo.py --update-all 170 | ./fp_turbo.py --update-all --system 171 | ./fp_turbo.py --id --list-file-perms 172 | ./fp_turbo.py --id --add-file-perms 173 | ./fp_turbo.py --id --remove-file-perms 174 | ./fp_turbo.py --id --list-other-perm-toggles 175 | ./fp_turbo.py --id --toggle-other-perms True --perm-type --perm-option 176 | ./fp_turbo.py --id --toggle-other-perms False --perm-type --perm-option 177 | ./fp_turbo.py --id --list-other-perm-values 178 | ./fp_turbo.py --id --add-other-perm-values --perm-value 179 | ./fp_turbo.py --id --remove-other-perm-values --perm-value 180 | ./fp_turbo.py --override --global-list-file-perms 181 | ./fp_turbo.py --override --global-add-file-perms 182 | ./fp_turbo.py --override --global-remove-file-perms 183 | ./fp_turbo.py --override --global-list-other-perm-toggles 184 | ./fp_turbo.py --override --global-toggle-other-perms True --perm-type --perm-option 185 | ./fp_turbo.py --override --global-toggle-other-perms False --perm-type --perm-option 186 | ./fp_turbo.py --override --global-list-other-perm-values 187 | ./fp_turbo.py --override --global-add-other-perm-values --perm-value 188 | ./fp_turbo.py --override --global-remove-other-perm-values --perm-value 189 | ./fp_turbo.py --id --list-file-perms --system 190 | ./fp_turbo.py --id --add-file-perms --system 191 | ./fp_turbo.py --id --remove-file-perms --system 192 | ./fp_turbo.py --id --add-file-perms "/path/to/directory" --perm-type persistent 193 | ./fp_turbo.py --id --remove-file-perms "/path/to/directory" --perm-type persistent 194 | ./fp_turbo.py --id --list-other-perm-toggles --system 195 | ./fp_turbo.py --id --toggle-other-perms True --perm-type --perm-option --system 196 | ./fp_turbo.py --id --toggle-other-perms False --perm-type --perm-option --system 197 | ./fp_turbo.py --id --list-other-perm-values --system 198 | ./fp_turbo.py --id --add-other-perm-values --perm-value --system 199 | ./fp_turbo.py --id --remove-other-perm-values --perm-value --system 200 | ./fp_turbo.py --override --global-list-file-perms --system 201 | ./fp_turbo.py --override --global-add-file-perms --system 202 | ./fp_turbo.py --override --global-remove-file-perms --system 203 | ./fp_turbo.py --override --global-list-other-perm-toggles --system 204 | ./fp_turbo.py --override --global-toggle-other-perms True --perm-type --perm-option --system 205 | ./fp_turbo.py --override --global-toggle-other-perms False --perm-type --perm-option --system 206 | ./fp_turbo.py --override --global-list-other-perm-values --system 207 | ./fp_turbo.py --override --global-add-other-perm-values --perm-value --system 208 | ./fp_turbo.py --override --global-remove-other-perm-values --perm-value --system 209 | ./fp_turbo.py --get-all-portal-permissions 210 | ./fp_turbo.py --get-portal-permissions 211 | ./fp_turbo.py --get-app-portal-permissions --id 212 | ./fp_turbo.py --set-app-portal-permissions --portal-perm-value --id 213 | ``` 214 | -------------------------------------------------------------------------------- /VERSION.txt: -------------------------------------------------------------------------------- 1 | 1.0.5 2 | -------------------------------------------------------------------------------- /data/usr/share/applications/com.flatpost.flatpostapp.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=Flatpost 3 | Exec=python3 /usr/bin/flatpost 4 | Icon=com.flatpost.flatpostapp 5 | Type=Application 6 | Categories=Utility; 7 | MimeType=application/vnd.flatpak.ref;application/vnd.flatpak.repo; 8 | -------------------------------------------------------------------------------- /data/usr/share/icons/hicolor/1024x1024/apps/com.flatpost.flatpostapp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GloriousEggroll/flatpost/a5a958861e5c26ac7ba67733b304e1b5578435b3/data/usr/share/icons/hicolor/1024x1024/apps/com.flatpost.flatpostapp.png -------------------------------------------------------------------------------- /data/usr/share/icons/hicolor/64x64/apps/com.flatpost.flatpostapp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GloriousEggroll/flatpost/a5a958861e5c26ac7ba67733b304e1b5578435b3/data/usr/share/icons/hicolor/64x64/apps/com.flatpost.flatpostapp.png -------------------------------------------------------------------------------- /data/usr/share/licenses/flatpost/LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2025, Thomas Crider 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 19 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 22 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 23 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /data/usr/share/mime/packages/flatpost.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Flatpak Reference File 6 | 7 | 8 | 9 | Flatpak Repository File 10 | 11 | 12 | -------------------------------------------------------------------------------- /packaging/rpm/flatpost.spec: -------------------------------------------------------------------------------- 1 | %global tag 1.0.0 2 | 3 | # Manual commit is auto-inserted by workflow 4 | %global commit 35fa9a68d94bde4ed68a6bd07482b2a87a8d42df 5 | 6 | %global shortcommit %(c=%{commit}; echo ${c:0:7}) 7 | 8 | %global build_timestamp %(date +"%Y%m%d") 9 | 10 | %global rel_build 1.%{build_timestamp}.%{shortcommit}%{?dist} 11 | 12 | Name: flatpost 13 | Version: %{tag} 14 | Release: %{rel_build} 15 | License: BSD 2-Clause 16 | Summary: Desktop environment agnostic Flathub software center. 17 | 18 | URL: https://github.com/gloriouseggroll/flatpost 19 | Source0: %{url}/archive/refs/tags/%{tag}.tar.gz#/%{name}-%{tag}.tar.gz 20 | 21 | BuildArch: noarch 22 | BuildRequires: python3-devel 23 | BuildRequires: make 24 | BuildRequires: desktop-file-utils 25 | 26 | Provides: nobara-updater 27 | 28 | # App Deps 29 | Requires: python 30 | Requires: python3 31 | Requires: python3-gobject 32 | Requires: python3-requests 33 | Requires: python3-pillow 34 | Requires: python3-svgwrite 35 | Requires: python3-fonttools 36 | Requires: python3-numpy 37 | 38 | Requires: flatpak 39 | Requires: glib2 40 | Requires: gtk3 41 | Requires: gtk4 42 | Requires: xdg-utils 43 | 44 | Requires(post): shared-mime-info 45 | Requires(postun): shared-mime-info 46 | Requires(posttrans): shared-mime-info 47 | 48 | Provides: flatpost 49 | 50 | %description 51 | Desktop environment agnostic Flathub software center. Allows for browsing, 52 | installation, removal, updating, and permission management of flatpak packages and repositories. 53 | 54 | %prep 55 | %autosetup -p 1 56 | 57 | %build 58 | make all DESTDIR=%{buildroot} 59 | 60 | %check 61 | desktop-file-validate %{buildroot}%{_datadir}/applications/com.flatpost.flatpostapp.desktop 62 | 63 | %post 64 | xdg-icon-resource forceupdate --theme hicolor &>/dev/null 65 | update-mime-database usr/share/mime &>/dev/null 66 | update-desktop-database -q 67 | 68 | %postun 69 | xdg-icon-resource forceupdate --theme hicolor &>/dev/null 70 | update-mime-database usr/share/mime &>/dev/null 71 | update-desktop-database -q 72 | 73 | %posttrans 74 | xdg-icon-resource forceupdate --theme hicolor &>/dev/null 75 | update-mime-database usr/share/mime &>/dev/null 76 | update-desktop-database -q 77 | 78 | %files 79 | %{python3_sitelib}/flatpost/ 80 | %{_bindir}/flatpost 81 | %{_datadir}/applications/com.flatpost.flatpostapp.desktop 82 | %{_datadir}/flatpost/collections_data.json 83 | %{_datadir}/icons/hicolor/1024x1024/apps/com.flatpost.flatpostapp.png 84 | %{_datadir}/icons/hicolor/64x64/apps/com.flatpost.flatpostapp.png 85 | %{_datadir}/mime/packages/flatpost.xml 86 | %license %{_datadir}/licenses/flatpost/LICENSE 87 | 88 | %clean 89 | rm -rf %{buildroot} 90 | 91 | %changelog 92 | * Fri Jun 28 2024 Your Name - 1.0-1 93 | - Initial package 94 | -------------------------------------------------------------------------------- /screenshots/flatshop_agnostic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GloriousEggroll/flatpost/a5a958861e5c26ac7ba67733b304e1b5578435b3/screenshots/flatshop_agnostic.png -------------------------------------------------------------------------------- /screenshots/flatshop_agnostic2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GloriousEggroll/flatpost/a5a958861e5c26ac7ba67733b304e1b5578435b3/screenshots/flatshop_agnostic2.png -------------------------------------------------------------------------------- /screenshots/flatshop_agnostic3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GloriousEggroll/flatpost/a5a958861e5c26ac7ba67733b304e1b5578435b3/screenshots/flatshop_agnostic3.png -------------------------------------------------------------------------------- /screenshots/flatshop_agnostic4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GloriousEggroll/flatpost/a5a958861e5c26ac7ba67733b304e1b5578435b3/screenshots/flatshop_agnostic4.png -------------------------------------------------------------------------------- /src/fp_turbo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | ### Documentation largely taken from: 4 | ### 5 | ### 1. https://lazka.github.io/pgi-docs/Flatpak-1.0 6 | ### 2. https://flathub.org/api/v2/docs#/ 7 | ### 8 | ### Classes AppStreamPackage and AppStreamSearcher extended from original by Tim Tim Lauridsen at: 9 | ### 10 | ### https://github.com/timlau/yumex-ng/blob/main/yumex/backend/flatpak/search.py 11 | 12 | # Original GPL v3 Code Copyright: 13 | # This program is free software: you can redistribute it and/or modify 14 | # it under the terms of the GNU General Public License as published by 15 | # the Free Software Foundation, either version 3 of the License, or 16 | # (at your option) any later version. 17 | # 18 | # This program is distributed in the hope that it will be useful, 19 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 20 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 21 | # GNU General Public License for more details. 22 | # 23 | # You should have received a copy of the GNU General Public License 24 | # along with this program. If not, see . 25 | # 26 | # Copyright (C) 2024 Tim Lauridsen 27 | # 28 | # Modifications copyright notice 29 | # Copyright (C) 2025 Thomas Crider 30 | # 31 | # Original code has been completely removed except 32 | # AppStreamPackage and AppStreamSearcher classes 33 | # which have been modified and extended. 34 | 35 | 36 | import gi 37 | gi.require_version("AppStream", "1.0") 38 | gi.require_version("Flatpak", "1.0") 39 | 40 | from gi.repository import Flatpak, GLib, Gio, AppStream 41 | from pathlib import Path 42 | import logging 43 | from enum import IntEnum 44 | import argparse 45 | import requests 46 | from urllib.parse import quote_plus, urlparse 47 | import tempfile 48 | import shutil 49 | import os 50 | import sys 51 | import json 52 | import time 53 | import dbus 54 | 55 | # Set up logging 56 | logging.basicConfig(level=logging.INFO) 57 | logger = logging.getLogger(__name__) 58 | 59 | class Match(IntEnum): 60 | NAME = 1 61 | ID = 2 62 | SUMMARY = 3 63 | NONE = 4 64 | 65 | class AppStreamComponentKind(IntEnum): 66 | """AppStream Component Kind enumeration.""" 67 | 68 | UNKNOWN = 0 69 | """Type invalid or not known.""" 70 | 71 | GENERIC = 1 72 | """A generic (= without specialized type) component.""" 73 | 74 | DESKTOP_APP = 2 75 | """An application with a .desktop-file.""" 76 | 77 | CONSOLE_APP = 3 78 | """A console application.""" 79 | 80 | WEB_APP = 4 81 | """A web application.""" 82 | 83 | SERVICE = 5 84 | """A system service launched by the init system.""" 85 | 86 | ADDON = 6 87 | """An extension of existing software, which does not run standalone.""" 88 | 89 | RUNTIME = 7 90 | """An application runtime platform.""" 91 | 92 | FONT = 8 93 | """A font.""" 94 | 95 | CODEC = 9 96 | """A multimedia codec.""" 97 | 98 | INPUT_METHOD = 10 99 | """An input-method provider.""" 100 | 101 | OPERATING_SYSTEM = 11 102 | """A computer operating system.""" 103 | 104 | FIRMWARE = 12 105 | """Firmware.""" 106 | 107 | DRIVER = 13 108 | """A driver.""" 109 | 110 | LOCALIZATION = 14 111 | """Software localization (usually l10n resources).""" 112 | 113 | REPOSITORY = 15 114 | """A remote software or data source.""" 115 | 116 | ICON_THEME = 16 117 | """An icon theme following the XDG specification.""" 118 | 119 | class AppStreamPackage: 120 | def __init__(self, comp: AppStream.Component, remote: Flatpak.Remote) -> None: 121 | self.component: AppStream.Component = comp 122 | self.remote: Flatpak.Remote = remote 123 | self.repo_name: str = remote.get_name() 124 | bundle: AppStream.Bundle = comp.get_bundle(AppStream.BundleKind.FLATPAK) 125 | self.flatpak_bundle: str = bundle.get_id() 126 | self.match = Match.NONE 127 | 128 | # Get icon and description 129 | self.icon_url = self._get_icon_url() 130 | self.icon_path_128 = self._get_icon_cache_path("128x128") 131 | self.icon_path_64 = self._get_icon_cache_path("64x64") 132 | self.icon_filename = self._get_icon_filename() 133 | self.description = self.component.get_description() 134 | self.screenshots = self.component.get_screenshots_all() 135 | 136 | # Get URLs from the component 137 | self.urls = self._get_urls() 138 | 139 | self.developer = self.component.get_developer().get_name() 140 | self.categories = self._get_categories() 141 | 142 | @property 143 | def id(self) -> str: 144 | return self.component.get_id() 145 | 146 | @property 147 | def name(self) -> str: 148 | return self.component.get_name() 149 | 150 | @property 151 | def summary(self) -> str: 152 | return self.component.get_summary() 153 | 154 | @property 155 | def version(self) -> str|None: 156 | releases = self.component.get_releases_plain() 157 | if releases: 158 | release = releases.index_safe(0) 159 | if release: 160 | version = release.get_version() 161 | return version 162 | return None 163 | 164 | @property 165 | def kind(self): 166 | kind = self.component.get_kind() 167 | kind_str = str(kind) 168 | 169 | for member in AppStreamComponentKind: 170 | if member.name in kind_str: 171 | return member.name 172 | 173 | def _get_icon_url(self) -> str: 174 | """Get the remote icon URL from the component""" 175 | icons = self.component.get_icons() 176 | 177 | # Find the first REMOTE icon 178 | remote_icon = next((icon for icon in icons if icon.get_kind() == AppStream.IconKind.REMOTE), None) 179 | return remote_icon.get_url() if remote_icon else "" 180 | 181 | def _get_icon_filename(self) -> str: 182 | """Get the cached icon filename from the component""" 183 | icons = self.component.get_icons() 184 | 185 | # Find the first CACHED icon 186 | cached_icon = next((icon for icon in icons if icon.get_kind() == AppStream.IconKind.CACHED), None) 187 | return cached_icon.get_filename() if cached_icon else "" 188 | 189 | def _get_icon_cache_path(self, size: str) -> str: 190 | 191 | # Appstream icon cache path for the flatpak repo queried 192 | icon_cache_path = Path(self.remote.get_appstream_dir().get_path() + "/icons/flatpak/" + size + "/") 193 | return str(icon_cache_path) 194 | 195 | def _get_urls(self) -> dict: 196 | """Get URLs from the component""" 197 | urls = { 198 | 'donation': self._get_url('donation'), 199 | 'homepage': self._get_url('homepage'), 200 | 'bugtracker': self._get_url('bugtracker') 201 | } 202 | return urls 203 | 204 | def _get_url(self, url_kind: str) -> str: 205 | """Helper method to get a specific URL type""" 206 | # Convert string to AppStream.UrlKind enum 207 | url_kind_enum = getattr(AppStream.UrlKind, url_kind.upper()) 208 | url = self.component.get_url(url_kind_enum) 209 | if url: 210 | return url 211 | return "" 212 | 213 | def _get_categories(self) -> list: 214 | categories_fetch = self.component.get_categories() 215 | categories = [] 216 | for category in categories_fetch: 217 | categories.append(category.lower()) 218 | return categories 219 | 220 | def search(self, keyword: str) -> Match: 221 | """Search for keyword in package details""" 222 | if keyword in self.name.lower(): 223 | return Match.NAME 224 | elif keyword in self.id.lower(): 225 | return Match.ID 226 | elif keyword in self.summary.lower(): 227 | return Match.SUMMARY 228 | else: 229 | return Match.NONE 230 | 231 | def __str__(self) -> str: 232 | return f"{self.name} - {self.summary} ({self.flatpak_bundle})" 233 | 234 | def get_details(self) -> dict: 235 | """Get all package details including icon and description""" 236 | return { 237 | "name": self.name, 238 | "id": self.id, 239 | "kind": self.kind, 240 | "summary": self.summary, 241 | "description": self.description, 242 | "version": self.version, 243 | "icon_url": self.icon_url, 244 | "icon_path_128": self.icon_path_128, 245 | "icon_path_64": self.icon_path_64, 246 | "icon_filename": self.icon_filename, 247 | "urls": self.urls, 248 | "developer": self.developer, 249 | #"architectures": self.architectures, 250 | "categories": self.categories, 251 | "bundle_id": self.flatpak_bundle, 252 | "match_type": self.match.name, 253 | "repo": self.repo_name, 254 | "screenshots": self.screenshots, 255 | "component": self.component, 256 | } 257 | 258 | class AppstreamSearcher: 259 | """Flatpak AppStream Package seacher""" 260 | 261 | def __init__(self, refresh=False) -> None: 262 | self.remotes: dict[str, list[AppStreamPackage]] = {} 263 | self.refresh_progress = 0 264 | self.refresh = refresh 265 | 266 | # Define category groups and their titles 267 | self.category_groups = { 268 | 'system': { 269 | 'installed': 'Installed', 270 | 'updates': 'Updates', 271 | 'repositories': 'Repositories' 272 | }, 273 | 'collections': { 274 | 'trending': 'Trending', 275 | 'popular': 'Popular', 276 | 'recently-added': 'New', 277 | 'recently-updated': 'Updated' 278 | }, 279 | 'categories': { 280 | 'office': 'Productivity', 281 | 'graphics': 'Graphics & Photography', 282 | 'audiovideo': 'Audio & Video', 283 | 'education': 'Education', 284 | 'network': 'Networking', 285 | 'game': 'Games', 286 | 'development': 'Developer Tools', 287 | 'science': 'Science', 288 | 'system': 'System', 289 | 'utility': 'Utilities' 290 | } 291 | } 292 | 293 | self.subcategory_groups = { 294 | 'audiovideo': { 295 | 'audiovideoediting': 'Audio & Video Editing', 296 | 'discburning': 'Disc Burning', 297 | 'midi': 'Midi', 298 | 'mixer': 'Mixer', 299 | 'player': 'Player', 300 | 'recorder': 'Recorder', 301 | 'sequencer': 'Sequencer', 302 | 'tuner': 'Tuner', 303 | 'tv': 'TV' 304 | }, 305 | 'development': { 306 | 'building': 'Building', 307 | 'database': 'Database', 308 | 'debugger': 'Debugger', 309 | 'guidesigner': 'GUI Designer', 310 | 'ide': 'IDE', 311 | 'profiling': 'Profiling', 312 | 'revisioncontrol': 'Revision Control', 313 | 'translation': 'Translation', 314 | 'webdevelopment': 'Web Development' 315 | }, 316 | 'game': { 317 | 'actiongame': 'Action Games', 318 | 'adventuregame': 'Adventure Games', 319 | 'arcadegame': 'Arcade Games', 320 | 'blocksgame': 'Blocks Games', 321 | 'boardgame': 'Board Games', 322 | 'cardgame': 'Card Games', 323 | 'emulator': 'Emulators', 324 | 'kidsgame': 'Kids\' Games', 325 | 'logicgame': 'Logic Games', 326 | 'roleplaying': 'Role Playing', 327 | 'shooter': 'Shooter', 328 | 'simulation': 'Simulation', 329 | 'sportsgame': 'Sports Games', 330 | 'strategygame': 'Strategy Games' 331 | }, 332 | 'graphics': { 333 | '2dgraphics': '2D Graphics', 334 | '3dgraphics': '3D Graphics', 335 | 'ocr': 'OCR', 336 | 'photography': 'Photography', 337 | 'publishing': 'Publishing', 338 | 'rastergraphics': 'Raster Graphics', 339 | 'scanning': 'Scanning', 340 | 'vectorgraphics': 'Vector Graphics', 341 | 'viewer': 'Viewer' 342 | }, 343 | 'network': { 344 | 'chat': 'Chat', 345 | 'email': 'Email', 346 | 'feed': 'Feed', 347 | 'filetransfer': 'File Transfer', 348 | 'hamradio': 'Ham Radio', 349 | 'instantmessaging': 'Instant Messaging', 350 | 'ircclient': 'IRC Client', 351 | 'monitor': 'Monitor', 352 | 'news': 'News', 353 | 'p2p': 'P2P', 354 | 'remoteaccess': 'Remote Access', 355 | 'telephony': 'Telephony', 356 | 'videoconference': 'Video Conference', 357 | 'webbrowser': 'Web Browser', 358 | 'webdevelopment': 'Web Development' 359 | }, 360 | 'office': { 361 | 'calendar': 'Calendar', 362 | 'chart': 'Chart', 363 | 'contactmanagement': 'Contact Management', 364 | 'database': 'Database', 365 | 'dictionary': 'Dictionary', 366 | 'email': 'Email', 367 | 'finance': 'Finance', 368 | 'presentation': 'Presentation', 369 | 'projectmanagement': 'Project Management', 370 | 'publishing': 'Publishing', 371 | 'spreadsheet': 'Spreadsheet', 372 | 'viewer': 'Viewer', 373 | 'wordprocessor': 'Word Processor' 374 | }, 375 | 'system': { 376 | 'emulator': 'Emulators', 377 | 'filemanager': 'File Manager', 378 | 'filesystem': 'Filesystem', 379 | 'filetools': 'File Tools', 380 | 'monitor': 'Monitor', 381 | 'security': 'Security', 382 | 'terminalemulator': 'Terminal Emulator' 383 | }, 384 | 'utility': { 385 | 'accessibility': 'Accessibility', 386 | 'archiving': 'Archiving', 387 | 'calculator': 'Calculator', 388 | 'clock': 'Clock', 389 | 'compression': 'Compression', 390 | 'filetools': 'File Tools', 391 | 'telephonytools': 'Telephony Tools', 392 | 'texteditor': 'Text Editor', 393 | 'texttools': 'Text Tools' 394 | } 395 | } 396 | 397 | def add_installation(self, inst: Flatpak.Installation): 398 | """Add enabled flatpak repositories from Flatpak.Installation""" 399 | remotes = inst.list_remotes() 400 | for remote in remotes: 401 | if not remote.get_disabled(): 402 | self.add_remote(remote, inst) 403 | 404 | def add_remote(self, remote: Flatpak.Remote, inst: Flatpak.Installation): 405 | """Add packages for a given Flatpak.Remote""" 406 | remote_name = remote.get_name() 407 | if remote_name not in self.remotes: 408 | self.remotes[remote_name] = self._load_appstream_metadata(remote, inst) 409 | def _load_appstream_metadata(self, remote: Flatpak.Remote, inst: Flatpak.Installation) -> list[AppStreamPackage]: 410 | """load AppStrean metadata and create AppStreamPackage objects""" 411 | packages = [] 412 | metadata = AppStream.Metadata.new() 413 | metadata.set_format_style(AppStream.FormatStyle.CATALOG) 414 | if self.refresh: 415 | if remote.get_name() == "flathub" or remote.get_name() == "flathub-beta": 416 | remote.set_gpg_verify(True) 417 | inst.modify_remote(remote, None) 418 | inst.update_appstream_full_sync(remote.get_name(), None, None, True) 419 | appstream_file = Path(remote.get_appstream_dir().get_path() + "/appstream.xml.gz") 420 | if not appstream_file.exists(): 421 | try: 422 | if remote.get_name() == "flathub" or remote.get_name() == "flathub-beta": 423 | remote.set_gpg_verify(True) 424 | inst.modify_remote(remote, None) 425 | inst.update_appstream_full_sync(remote.get_name(), None, None, True) 426 | except GLib.Error as e: 427 | logger.error(f"Failed to update AppStream metadata: {str(e)}") 428 | if appstream_file.exists(): 429 | metadata.parse_file(Gio.File.new_for_path(appstream_file.as_posix()), AppStream.FormatKind.XML) 430 | components: AppStream.ComponentBox = metadata.get_components() 431 | i = 0 432 | for i in range(components.get_size()): 433 | component = components.index_safe(i) 434 | #if component.get_kind() == AppStream.ComponentKind.DESKTOP_APP: 435 | packages.append(AppStreamPackage(component, remote)) 436 | return packages 437 | else: 438 | logger.debug(f"AppStream file not found: {appstream_file}") 439 | return [] 440 | 441 | def search_flatpak_repo(self, keyword: str, repo_name: str) -> list[AppStreamPackage]: 442 | search_results = [] 443 | packages = self.remotes[repo_name] 444 | found = None 445 | 446 | for package in packages: 447 | # Try matching exact ID first 448 | if keyword is package.id: 449 | found = package 450 | break 451 | # Next try matching exact name 452 | elif keyword.lower() is package.name.lower(): 453 | found = package 454 | break 455 | # Try matching case insensitive ID next 456 | elif keyword.lower() is package.id.lower(): 457 | found = package 458 | break 459 | # General keyword search 460 | elif keyword.lower() in str(package).lower(): 461 | found = package 462 | break 463 | if found: 464 | search_results.append(found) 465 | return search_results 466 | 467 | 468 | def search_flatpak(self, keyword: str, repo_name=None) -> list[AppStreamPackage]: 469 | """Search packages matching a keyword""" 470 | search_results = [] 471 | keyword = keyword 472 | 473 | if not repo_name: 474 | for remote_name in self.remotes.keys(): 475 | search_results.extend(self.search_flatpak_repo(keyword, remote_name)) 476 | else: 477 | if repo_name in self.remotes.keys(): 478 | search_results.extend(self.search_flatpak_repo(keyword, repo_name)) 479 | return search_results 480 | 481 | 482 | def get_all_apps(self, repo_name=None) -> list[AppStreamPackage]: 483 | """Get all available apps from specified or all repositories""" 484 | all_packages = [] 485 | if repo_name: 486 | if repo_name in self.remotes: 487 | all_packages = self.remotes[repo_name] 488 | else: 489 | for remote_name in self.remotes.keys(): 490 | all_packages.extend(self.remotes[remote_name]) 491 | return all_packages 492 | 493 | def get_categories_summary(self, repo_name=None) -> dict: 494 | """Get a summary of all apps grouped by category""" 495 | apps = self.get_all_apps(repo_name) 496 | categories = {} 497 | 498 | for app in apps: 499 | for category in app.categories: 500 | # Normalize category names to match our groups 501 | normalized_category = category.lower() 502 | 503 | # Map category to its group title 504 | for group_name, categories_dict in self.category_groups.items(): 505 | if normalized_category in categories_dict: 506 | display_category = categories_dict[normalized_category] 507 | break 508 | else: 509 | display_category = normalized_category.title() 510 | 511 | if display_category not in categories: 512 | categories[display_category] = [] 513 | categories[display_category].append(app) 514 | 515 | return categories 516 | 517 | def get_subcategories_summary(self, repo_name=None) -> list[tuple[str, str, list[AppStreamPackage]]]: 518 | """Get a summary of all apps grouped by category and subcategory.""" 519 | apps = self.get_all_apps(repo_name) 520 | subcategories = [] 521 | 522 | # Process each category and its subcategories 523 | for category, subcategories_dict in self.subcategory_groups.items(): 524 | for subcategory, title in subcategories_dict.items(): 525 | apps_in_subcategory = [] 526 | for app in apps: 527 | if category in app.categories and subcategory in app.categories: 528 | apps_in_subcategory.append(app) 529 | if apps_in_subcategory: 530 | subcategories.append((category, subcategory, apps_in_subcategory)) 531 | 532 | return subcategories 533 | 534 | def get_installed_apps(self, system=False) -> list[tuple[str, str, str]]: 535 | """Get a list of all installed Flatpak applications with their repository source""" 536 | installed_refs = [] 537 | 538 | installation = get_installation(system) 539 | 540 | def process_installed_refs(inst: Flatpak.Installation, system=False): 541 | for ref in inst.list_installed_refs(): 542 | app_id = ref.get_name() 543 | remote_name = ref.get_origin() 544 | if system is False: 545 | installed_refs.append((app_id, remote_name, "user")) 546 | else: 547 | installed_refs.append((app_id, remote_name, "system")) 548 | 549 | # Process both system-wide and user installations 550 | process_installed_refs(installation, system) 551 | 552 | # Remove duplicates while maintaining order 553 | seen = set() 554 | unique_installed = [(ref, repo, repo_type) for ref, repo, repo_type in installed_refs 555 | if not (ref in seen or seen.add(ref))] 556 | 557 | return unique_installed 558 | 559 | def check_updates(self, system=False) -> list[tuple[str, str, str]]: 560 | """Check for available updates for installed Flatpak applications""" 561 | updates = [] 562 | 563 | installation = get_installation(system) 564 | 565 | def check_updates_for_install(inst: Flatpak.Installation, system=False): 566 | for ref in inst.list_installed_refs_for_update(None): 567 | app_id = ref.get_name() 568 | # Get remote name from the installation 569 | remote_name = ref.get_origin() 570 | if system is False: 571 | updates.append((app_id, remote_name, "user")) 572 | else: 573 | updates.append((app_id, remote_name, "system")) 574 | 575 | # Process both system-wide and user installations 576 | check_updates_for_install(installation, system) 577 | 578 | return updates 579 | 580 | def fetch_flathub_category_apps(self, category): 581 | """Fetch applications from Flathub API for the specified category.""" 582 | try: 583 | # URL encode the category to handle special characters 584 | encoded_category = quote_plus(category) 585 | 586 | # Determine the base URL based on category type 587 | if category in self.category_groups['collections']: 588 | url = f"https://flathub.org/api/v2/collection/{encoded_category}" 589 | else: 590 | url = f"https://flathub.org/api/v2/collection/category/{encoded_category}" 591 | 592 | response = requests.get(url, timeout=10) 593 | 594 | if response.status_code == 200: 595 | data = response.json() 596 | 597 | # If this is a collections category, save it to our collections database 598 | if category in self.category_groups['collections']: 599 | if not hasattr(self, 'collections_db'): 600 | self.collections_db = [] 601 | self.collections_db.append({ 602 | 'category': category, 603 | 'data': data 604 | }) 605 | 606 | return data 607 | else: 608 | print(f"Failed to fetch apps: Status code {response.status_code}") 609 | return None 610 | except requests.RequestException as e: 611 | print(f"Error fetching apps: {str(e)}") 612 | return None 613 | 614 | def save_collections_data(self, filename='collections_data.json'): 615 | """Save all collected collections data to a JSON file.""" 616 | app_data_dir = Path.home() / ".local" / "share" / "flatpost" 617 | app_data_dir.mkdir(parents=True, exist_ok=True) 618 | json_path = app_data_dir / filename 619 | if not hasattr(self, 'collections_db') or not self.collections_db: 620 | return 621 | 622 | try: 623 | with open(json_path, 'w', encoding='utf-8') as f: 624 | json.dump(self.collections_db, f, indent=2, ensure_ascii=False) 625 | except IOError as e: 626 | print(f"Error saving collections data: {str(e)}") 627 | 628 | def update_collection_results(self, new_collection_results): 629 | """Update search results by replacing existing items and adding new ones.""" 630 | # Create a set of existing app_ids for efficient lookup 631 | existing_app_ids = {app.id for app in self.collection_results} 632 | 633 | # Create a list to store the updated results 634 | updated_results = [] 635 | 636 | # First add all existing results 637 | updated_results.extend(self.collection_results) 638 | 639 | # Add new results, replacing any existing ones 640 | for new_result in new_collection_results: 641 | app_id = new_result.id 642 | if app_id in existing_app_ids: 643 | # Replace existing result 644 | for i, existing in enumerate(updated_results): 645 | if existing.id == app_id: 646 | updated_results[i] = new_result 647 | break 648 | else: 649 | # Add new result 650 | updated_results.append(new_result) 651 | 652 | self.collection_results = updated_results 653 | 654 | def fetch_flathub_subcategory_apps(self, category: str, subcategory: str) -> dict|None: 655 | """Fetch applications from Flathub API for the specified category and subcategory.""" 656 | try: 657 | # URL encode the category and subcategory to handle special characters 658 | encoded_category = quote_plus(category) 659 | encoded_subcategory = quote_plus(subcategory) 660 | 661 | # Construct the API URL for subcategories 662 | url = f"https://flathub.org/api/v2/collection/category/{encoded_category}/subcategories?subcategory={encoded_subcategory}" 663 | 664 | response = requests.get(url, timeout=10) 665 | 666 | if response.status_code == 200: 667 | data = response.json() 668 | return data 669 | else: 670 | print(f"Failed to fetch apps: Status code {response.status_code}") 671 | return None 672 | except requests.RequestException as e: 673 | print(f"Error fetching apps: {str(e)}") 674 | return None 675 | 676 | def refresh_local(self, system=False): 677 | 678 | self._initialize_metadata() 679 | 680 | total_categories = sum(len(categories) for categories in self.category_groups.values()) 681 | current_category = 0 682 | # Search for each app in local repositories 683 | searcher = get_reposearcher(system) 684 | search_result = [] 685 | for group_name, categories in self.category_groups.items(): 686 | # Process categories one at a time to keep GUI responsive 687 | for category, title in categories.items(): 688 | self._process_system_category(searcher, category, system) 689 | # Update progress bar 690 | self.refresh_progress = (current_category / total_categories) * 100 691 | # make sure to reset these to empty before refreshing. 692 | return self.installed_results, self.updates_results 693 | 694 | 695 | def retrieve_metadata(self, system=False): 696 | """Retrieve and refresh metadata for Flatpak repositories.""" 697 | self._initialize_metadata() 698 | 699 | if not check_internet(): 700 | return self._handle_offline_mode() 701 | 702 | searcher = get_reposearcher(system, True) 703 | self.all_apps = searcher.get_all_apps() 704 | 705 | return self._process_categories(searcher, system) 706 | 707 | def _initialize_metadata(self): 708 | """Initialize empty lists for metadata storage.""" 709 | self.category_results = [] 710 | self.collection_results = [] 711 | self.installed_results = [] 712 | self.updates_results = [] 713 | self.all_apps = [] 714 | 715 | def _handle_offline_mode(self): 716 | """Handle metadata retrieval when offline.""" 717 | 718 | total_categories = sum(len(categories) for categories in self.category_groups.values()) 719 | current_category = 0 720 | # Search for each app in local repositories 721 | searcher = get_reposearcher() 722 | search_result = [] 723 | for group_name, categories in self.category_groups.items(): 724 | # Process categories one at a time to keep GUI responsive 725 | for category, title in categories.items(): 726 | self._process_system_category(searcher, category) 727 | 728 | # Define paths 729 | app_data_dir = Path.home() / ".local" / "share" / "flatpost" 730 | system_data_dir = Path("/usr/share/flatpost") 731 | 732 | # Ensure local directory exists 733 | app_data_dir.mkdir(parents=True, exist_ok=True) 734 | 735 | # Define file paths 736 | json_path = app_data_dir / "collections_data.json" 737 | 738 | # Helper function to copy file if it doesn't exist locally 739 | def copy_if_missing(source_path, dest_path): 740 | try: 741 | shutil.copy(str(source_path), str(dest_path)) 742 | logger.info(f"Copied {dest_path.name} to user directory") 743 | except IOError as e: 744 | logger.error(f"Failed to copy {dest_path.name}: {str(e)}") 745 | return False 746 | return True 747 | 748 | # Try to copy collections_data.json if needed 749 | if not json_path.exists() and not copy_if_missing( 750 | system_data_dir / "collections_data.json", 751 | json_path 752 | ): 753 | logger.error("Could not load or copy collections_data.json") 754 | return None, [], [], [], [] 755 | 756 | try: 757 | with open(json_path, 'r', encoding='utf-8') as f: 758 | collections_data = json.load(f) 759 | processed_data = self._process_offline_data(collections_data) 760 | 761 | return processed_data 762 | 763 | except (IOError, json.JSONDecodeError) as e: 764 | logger.error(f"Error loading offline data: {str(e)}") 765 | return None, [], [], [], [] 766 | 767 | 768 | def _process_offline_data(self, collections_data): 769 | """Process cached collections data when offline.""" 770 | for collection in collections_data: 771 | category = collection['category'] 772 | if category in self.category_groups['collections']: 773 | apps = [app['app_id'] for app in collection['data'].get('hits', [])] 774 | for app_id in apps: 775 | search_result = self.search_flatpak(app_id, 'flathub') 776 | self.collection_results.extend(search_result) 777 | return self._get_current_results() 778 | 779 | def _process_categories(self, searcher, system=False): 780 | """Process categories and retrieve metadata.""" 781 | total_categories = sum(len(categories) for categories in self.category_groups.values()) 782 | current_category = 0 783 | 784 | for group_name, categories in self.category_groups.items(): 785 | for category, title in categories.items(): 786 | if category not in self.category_groups['system']: 787 | self._process_category(searcher, category, current_category, total_categories, system) 788 | else: 789 | self._process_system_category(searcher, category, system) 790 | current_category += 1 791 | self.save_collections_data() 792 | 793 | return self._get_current_results() 794 | 795 | def _process_category(self, searcher, category, current_category, total_categories, system=False): 796 | """Process a single category and retrieve its metadata.""" 797 | 798 | if self._should_refresh(): 799 | self._refresh_category_data(searcher, category) 800 | 801 | app_data_dir = Path.home() / ".local" / "share" / "flatpost" 802 | app_data_dir.mkdir(parents=True, exist_ok=True) 803 | json_path = app_data_dir / "collections_data.json" 804 | try: 805 | with open(json_path, 'r', encoding='utf-8') as f: 806 | collections_data = json.load(f) 807 | self._update_from_collections(collections_data, category) 808 | except (IOError, json.JSONDecodeError) as e: 809 | pass 810 | 811 | self.refresh_progress = (current_category / total_categories) * 100 812 | 813 | def _update_from_collections(self, collections_data, category): 814 | """Update results from cached collections data.""" 815 | for collection in collections_data: 816 | if collection['category'] == category: 817 | apps = [app['app_id'] for app in collection['data'].get('hits', [])] 818 | for app_id in apps: 819 | search_result = self.search_flatpak(app_id, 'flathub') 820 | self.collection_results.extend(search_result) 821 | 822 | def _should_refresh(self): 823 | """Check if category data needs refresh.""" 824 | app_data_dir = Path.home() / ".local" / "share" / "flatpost" 825 | app_data_dir.mkdir(parents=True, exist_ok=True) 826 | json_path = app_data_dir / "collections_data.json" 827 | try: 828 | mod_time = os.path.getmtime(json_path) 829 | return (time.time() - mod_time) > 168 * 3600 830 | except OSError: 831 | return True 832 | 833 | def _refresh_category_data(self, searcher, category): 834 | """Refresh category data from Flathub API.""" 835 | try: 836 | api_data = self.fetch_flathub_category_apps(category) 837 | if api_data: 838 | apps = api_data['hits'] 839 | for app in apps: 840 | app_id = app['app_id'] 841 | search_result = searcher.search_flatpak(app_id, 'flathub') 842 | if category in self.category_groups['collections']: 843 | self.update_collection_results(search_result) 844 | else: 845 | self.category_results.extend(search_result) 846 | except requests.RequestException as e: 847 | logger.error(f"Error refreshing category {category}: {str(e)}") 848 | 849 | def _process_system_category(self, searcher, category, system=False): 850 | """Process system-related categories.""" 851 | if "installed" in category: 852 | installed_apps = get_installation(system).list_installed_refs() 853 | for app in installed_apps: 854 | search_result = searcher.search_flatpak(app.get_name(), app.get_origin()) 855 | self.installed_results.extend(search_result) 856 | elif "updates" in category and check_internet(): 857 | updates = get_installation(system).list_installed_refs_for_update() 858 | for app in updates: 859 | search_result = searcher.search_flatpak(app.get_name(), app.get_origin()) 860 | self.updates_results.extend(search_result) 861 | 862 | def _get_current_results(self): 863 | """Return current metadata results.""" 864 | return ( 865 | self.category_results, 866 | self.collection_results, 867 | self.installed_results, 868 | self.updates_results, 869 | self.all_apps 870 | ) 871 | 872 | def install_flatpak(app: AppStreamPackage, repo_name=None, system=False) -> tuple[bool, str]: 873 | """ 874 | Install a Flatpak package. 875 | 876 | Args: 877 | app (AppStreamPackage): The package to install. 878 | repo_name (str): Optional repository name to use for installation 879 | system (Optional[bool]): Whether to operate on user or system installation 880 | 881 | Returns: 882 | tuple[bool, str]: (success, message) 883 | """ 884 | 885 | if not repo_name: 886 | repo_name = "flathub" 887 | 888 | installation = get_installation(system) 889 | 890 | transaction = Flatpak.Transaction.new_for_installation(installation) 891 | available_apps = installation.list_remote_refs_sync(repo_name) 892 | match_found = None 893 | for available_app in available_apps: 894 | if available_app.get_name() in app.id: 895 | match_found = 1 896 | # Add the install operation 897 | transaction.add_install(repo_name, available_app.format_ref(), None) 898 | 899 | if not match_found: 900 | return False, f"No available package named {app.id} found in any repositories." 901 | 902 | try: 903 | transaction.run() 904 | except GLib.Error as e: 905 | return False, f"Installation failed: {e}" 906 | return True, f"Successfully installed {app.id}" 907 | 908 | def install_flatpakref(ref_file, system=False): 909 | """Add a new repository using a .flatpakrepo file""" 910 | # Get existing repositories 911 | installation = get_installation(system) 912 | 913 | if not ref_file.endswith('.flatpakref'): 914 | return False, "Flatpak ref file path or URL must end with .flatpakref extension." 915 | 916 | if not os.path.exists(ref_file): 917 | return False, f"Flatpak ref file '{ref_file}' does not exist." 918 | 919 | # Read the flatpakref file 920 | try: 921 | with open(ref_file, 'rb') as f: 922 | repo_data = f.read() 923 | except IOError as e: 924 | return False, f"Failed to read flatpakref file: {str(e)}" 925 | 926 | # Convert the data to GLib.Bytes 927 | repo_bytes = GLib.Bytes.new(repo_data) 928 | 929 | installation = get_installation(system) 930 | 931 | transaction = Flatpak.Transaction.new_for_installation(installation) 932 | 933 | # Add the install operation 934 | transaction.add_install_flatpakref(repo_bytes) 935 | # Run the transaction 936 | try: 937 | transaction.run() 938 | except GLib.Error as e: 939 | return False, f"Installation failed: {e}" 940 | return True, f"Successfully installed {ref_file}" 941 | 942 | 943 | def remove_flatpak(app: AppStreamPackage, system=False) -> tuple[bool, str]: 944 | """ 945 | Remove a Flatpak package using transactions. 946 | 947 | Args: 948 | app (AppStreamPackage): The package to install. 949 | system (Optional[bool]): Whether to operate on user or system installation 950 | 951 | Returns: 952 | Tuple[bool, str]: (success, message) 953 | """ 954 | 955 | # Get the appropriate installation based on user parameter 956 | installation = get_installation(system) 957 | installed = installation.list_installed_refs(None) 958 | # Create a new transaction for removal 959 | transaction = Flatpak.Transaction.new_for_installation(installation) 960 | match_found = None 961 | for installed_ref in installed: 962 | if installed_ref.get_name() in app.id: 963 | match_found = 1 964 | # Add the install operation 965 | transaction.add_uninstall(installed_ref.format_ref()) 966 | 967 | if not match_found: 968 | return False, f"No installed package named {app.id} found." 969 | try: 970 | transaction.run() 971 | except GLib.Error as e: 972 | return False, f"Failed to remove {app.id}: {e}" 973 | return True, f"Successfully removed {app.id}" 974 | 975 | def update_flatpak(app: AppStreamPackage, system=False) -> tuple[bool, str]: 976 | """ 977 | Remove a Flatpak package using transactions. 978 | 979 | Args: 980 | app (AppStreamPackage): The package to install. 981 | system (Optional[bool]): Whether to operate on user or system installation 982 | 983 | Returns: 984 | Tuple[bool, str]: (success, message) 985 | """ 986 | 987 | # Get the appropriate installation based on user parameter 988 | installation = get_installation(system) 989 | updates = installation.list_installed_refs_for_update(None) 990 | # Create a new transaction for removal 991 | transaction = Flatpak.Transaction.new_for_installation(installation) 992 | match_found = None 993 | for update in updates: 994 | if update.get_name() in app.id: 995 | match_found = 1 996 | # Add the install operation 997 | transaction.add_update(update.format_ref()) 998 | 999 | 1000 | if not match_found: 1001 | return False, f"No updateable package named {app.id} found." 1002 | # Run the transaction 1003 | try: 1004 | transaction.run() 1005 | except GLib.Error as e: 1006 | return False, f"Failed to update {app.id}: {e}" 1007 | return True, f"Successfully updated {app.id}" 1008 | 1009 | def update_all_flatpaks(apps: list[AppStreamPackage], system=False) -> tuple[bool, str]: 1010 | """ 1011 | Update multiple Flatpak packages using transactions. 1012 | 1013 | Args: 1014 | apps (Union[List[AppStreamPackage], AppStreamPackage]): One or more packages to update 1015 | system (Optional[bool]): Whether to operate on user or system installation 1016 | 1017 | Returns: 1018 | Tuple[bool, List[str]]: (success, list of status messages) 1019 | """ 1020 | 1021 | installation = get_installation(system) 1022 | updates = installation.list_installed_refs_for_update(None) 1023 | # Create a new transaction for removal 1024 | transaction = Flatpak.Transaction.new_for_installation(installation) 1025 | for update in updates: 1026 | transaction.add_update(update.format_ref()) 1027 | 1028 | try: 1029 | transaction.run() 1030 | return True, "Successfully updated all packages" 1031 | except GLib.Error as e: 1032 | return False, f"Failed to update all packages: {str(e)}" 1033 | 1034 | def get_installation(system=False): 1035 | if system is False: 1036 | installation = Flatpak.Installation.new_user() 1037 | else: 1038 | installation = Flatpak.Installation.new_system() 1039 | return installation 1040 | 1041 | def get_reposearcher(system=False, refresh=False): 1042 | installation = get_installation(system) 1043 | searcher = AppstreamSearcher(refresh) 1044 | searcher.add_installation(installation) 1045 | return searcher 1046 | 1047 | def check_internet(): 1048 | """Check if internet connection is available.""" 1049 | try: 1050 | requests.head('https://flathub.org', timeout=3) 1051 | return True 1052 | except requests.ConnectionError: 1053 | return False 1054 | 1055 | def repotoggle(repo, toggle=True, system=False): 1056 | """ 1057 | Enable or disable a Flatpak repository 1058 | 1059 | Args: 1060 | repo (str): Name of the repository to toggle 1061 | enable (toggle): True to enable, False to disable 1062 | 1063 | Returns: 1064 | tuple: (success, error_message) 1065 | """ 1066 | 1067 | if not repo: 1068 | return False, "Repository name cannot be empty" 1069 | 1070 | installation = get_installation(system) 1071 | 1072 | try: 1073 | remote = installation.get_remote_by_name(repo) 1074 | if not remote: 1075 | return False, f"Repository '{repo}' not found." 1076 | 1077 | remote.set_disabled(not toggle) 1078 | 1079 | # Modify the remote's disabled status 1080 | success = installation.modify_remote( 1081 | remote, 1082 | None 1083 | ) 1084 | if success: 1085 | if toggle: 1086 | message = f"Successfully enabled {repo}." 1087 | else: 1088 | message = f"Successfully disabled {repo}." 1089 | return True, message 1090 | 1091 | except GLib.GError as e: 1092 | return False, f"Failed to toggle repository: {str(e)}" 1093 | 1094 | return False, "Operation failed" 1095 | 1096 | def repolist(system=False): 1097 | installation = get_installation(system) 1098 | repos = installation.list_remotes() 1099 | return repos 1100 | 1101 | def repodelete(repo, system=False): 1102 | installation = get_installation(system) 1103 | installation.remove_remote(repo) 1104 | 1105 | def repoadd(repofile, system=False): 1106 | """Add a new repository using a .flatpakrepo file""" 1107 | # Get existing repositories 1108 | installation = get_installation(system) 1109 | existing_repos = installation.list_remotes() 1110 | 1111 | if not repofile.endswith('.flatpakrepo'): 1112 | return False, "Repository file path or URL must end with .flatpakrepo extension." 1113 | 1114 | if repofile_is_url(repofile): 1115 | try: 1116 | local_path = download_repo(repofile) 1117 | repofile = local_path 1118 | print(f"\nRepository added successfully: {repofile}") 1119 | except: 1120 | return False, f"Repository file '{repofile}' could not be downloaded." 1121 | 1122 | if not os.path.exists(repofile): 1123 | return False, f"Repository file '{repofile}' does not exist." 1124 | 1125 | # Get repository title from file name 1126 | title = os.path.basename(repofile).replace('.flatpakrepo', '') 1127 | 1128 | # Check for duplicate title (case insensitive) 1129 | existing_titles = [repo.get_name().casefold() for repo in existing_repos] 1130 | 1131 | if title.casefold() in existing_titles: 1132 | return False, "A repository with this title already exists." 1133 | 1134 | if title == "flathub": 1135 | title = "Flatpak Official Flathub" 1136 | if title == "flathub-beta": 1137 | title = "Flatpak Official Flathub (Beta)" 1138 | 1139 | # Read the repository file 1140 | try: 1141 | with open(repofile, 'rb') as f: 1142 | repo_data = f.read() 1143 | except IOError as e: 1144 | return False, f"Failed to read repository file: {str(e)}" 1145 | 1146 | # Convert the data to GLib.Bytes 1147 | repo_bytes = GLib.Bytes.new(repo_data) 1148 | 1149 | # Create a new remote from the repository file 1150 | try: 1151 | remote = Flatpak.Remote.new_from_file(title, repo_bytes) 1152 | 1153 | # Get URLs and normalize them by removing trailing slashes 1154 | new_url = remote.get_url().rstrip('/') 1155 | existing_urls = [repo.get_url().rstrip('/') for repo in existing_repos] 1156 | 1157 | # Check if URL already exists 1158 | if new_url in existing_urls: 1159 | return False, f"A repository with URL '{new_url}' already exists." 1160 | user = "user" 1161 | if system: 1162 | user = "system" 1163 | remote.set_gpg_verify(True) 1164 | installation.add_remote(remote, True, None) 1165 | except GLib.GError as e: 1166 | return False, f"Failed to add repository: {str(e)}" 1167 | return True, f"{remote.get_name()} repository successfully added for {user} installation." 1168 | 1169 | def repofile_is_url(string): 1170 | """Check if a string is a valid URL""" 1171 | try: 1172 | result = urlparse(string) 1173 | return all([result.scheme, result.netloc]) 1174 | except: 1175 | return False 1176 | 1177 | def download_repo(url): 1178 | """Download a repository file from URL to /tmp/""" 1179 | try: 1180 | # Create a deterministic filename based on the URL 1181 | url_path = urlparse(url).path 1182 | filename = os.path.basename(url_path) or 'repo' 1183 | tmp_path = Path(tempfile.gettempdir()) / f"{filename}" 1184 | 1185 | # Download the file 1186 | with requests.get(url, stream=True) as response: 1187 | response.raise_for_status() 1188 | 1189 | # Write the file in chunks, overwriting if it exists 1190 | with open(tmp_path, 'wb') as f: 1191 | for chunk in response.iter_content(chunk_size=8192): 1192 | f.write(chunk) 1193 | 1194 | return str(tmp_path) 1195 | except requests.RequestException as e: 1196 | raise argparse.ArgumentTypeError(f"Failed to download repository file: {str(e)}") 1197 | except IOError as e: 1198 | raise argparse.ArgumentTypeError(f"Failed to save repository file: {str(e)}") 1199 | 1200 | def get_metadata_path(app_id: str | None, override=False, system=False) -> str: 1201 | metadata_path = "" 1202 | if app_id: 1203 | # Get the application's metadata file 1204 | installation = get_installation(system) 1205 | app_path = installation.get_current_installed_app(app_id).get_deploy_dir() 1206 | if not app_path: 1207 | print(f"Application {app_id} not found") 1208 | return metadata_path 1209 | metadata_path = app_path + "/metadata" 1210 | elif override: 1211 | if system: 1212 | metadata_path = "/var/lib/flatpak/overrides/global" 1213 | if not os.path.exists(metadata_path): 1214 | os.makedirs(os.path.dirname(metadata_path), exist_ok=True) 1215 | with open(metadata_path, 'w') as f: 1216 | pass 1217 | else: 1218 | home_dir = os.path.expanduser("~") 1219 | metadata_path = f"{home_dir}/.local/share/flatpak/overrides/global" 1220 | if not os.path.exists(metadata_path): 1221 | os.makedirs(os.path.dirname(metadata_path), exist_ok=True) 1222 | with open(metadata_path, 'w') as f: 1223 | pass 1224 | if not os.path.exists(metadata_path): 1225 | print(f"Metadata file not found for {app_id}") 1226 | return metadata_path 1227 | return metadata_path 1228 | 1229 | def get_perm_key_file(app_id: str | None, override=False, system=False) -> GLib.KeyFile: 1230 | metadata_path = get_metadata_path(app_id, override, system) 1231 | # Create a new KeyFile object 1232 | key_file = GLib.KeyFile() 1233 | 1234 | # Read the existing metadata 1235 | try: 1236 | key_file.load_from_file(metadata_path, GLib.KeyFileFlags.NONE) 1237 | except GLib.Error as e: 1238 | print(f"Failed to read metadata file: {str(e)}") 1239 | return None 1240 | 1241 | return key_file 1242 | 1243 | def add_file_permissions(app_id: str, path: str, perm_type=None, system=False) -> tuple[bool, str]: 1244 | """ 1245 | Add filesystem permissions to a Flatpak application. 1246 | 1247 | Args: 1248 | app_id (str): The ID of the Flatpak application 1249 | path (str): The path to grant access to. Can be: 1250 | - "home" for home directory access 1251 | - "/path/to/directory" for custom directory access 1252 | perm_type (str): The type of permissions to remove (e.g. "filesystems", "persistent") default is "filesystems" 1253 | system (bool): Whether to modify system-wide or user installation 1254 | 1255 | Returns: 1256 | tuple[bool, str]: (success, message) 1257 | """ 1258 | try: 1259 | key_file = get_perm_key_file(app_id, False, system) 1260 | perm_type = perm_type or "filesystems" 1261 | # Handle special case for home directory 1262 | if path.lower() == "host": 1263 | filesystem_path = "host" 1264 | elif path.lower() == "host-os": 1265 | filesystem_path = "host-os" 1266 | elif path.lower() == "host-etc": 1267 | filesystem_path = "host-etc" 1268 | elif path.lower() == "home": 1269 | filesystem_path = "home" 1270 | else: 1271 | # Ensure path do not ends with a trailing slash 1272 | filesystem_path = path.rstrip('/') 1273 | 1274 | # Validate absolute paths start with / 1275 | if filesystem_path.startswith('/'): 1276 | filesystem_path = '/' + filesystem_path.lstrip('/') 1277 | 1278 | if not key_file.has_group("Context"): 1279 | key_file.set_string("Context", perm_type, "") 1280 | 1281 | context_keys = key_file.get_keys("Context") 1282 | if perm_type not in str(context_keys): 1283 | key_file.set_string("Context", perm_type, "") 1284 | 1285 | existing_paths = key_file.get_string("Context", perm_type) 1286 | if existing_paths is None or existing_paths == "": 1287 | # If no filesystems exist, set the exact path provided 1288 | key_file.set_string("Context", perm_type, filesystem_path) 1289 | else: 1290 | existing_paths_list = existing_paths.split(';') 1291 | normalized_new_path = os.path.abspath(filesystem_path.rstrip('/')) 1292 | normalized_existing_paths = [os.path.abspath(p.rstrip('/')) for p in existing_paths_list] 1293 | 1294 | if normalized_new_path not in normalized_existing_paths: 1295 | # Add new path with proper separator 1296 | separator = ';' if existing_paths.endswith(';') else ';' 1297 | key_file.set_string("Context", perm_type, 1298 | existing_paths + separator + filesystem_path) 1299 | 1300 | # Write the modified metadata back 1301 | try: 1302 | key_file.save_to_file(get_metadata_path(app_id, False, system)) 1303 | except GLib.Error as e: 1304 | return False, f"Failed to save metadata file: {str(e)}" 1305 | 1306 | return True, f"Successfully granted access to {path} for {app_id}" 1307 | 1308 | except GLib.Error as e: 1309 | return False, f"Failed to modify permissions: {str(e)}" 1310 | 1311 | 1312 | def remove_file_permissions(app_id: str, path: str, perm_type=None, system=False) -> tuple[bool, str]: 1313 | """ 1314 | Remove filesystem permissions from a Flatpak application. 1315 | 1316 | Args: 1317 | app_id (str): The ID of the Flatpak application 1318 | path (str): The path to revoke access to. Can be: 1319 | - "home" for home directory access 1320 | - "/path/to/directory" for custom directory access 1321 | perm_type (str): The type of permissions to remove (e.g. "filesystems", "persistent") default is "filesystems" 1322 | system (bool): Whether to modify system-wide or user installation 1323 | 1324 | Returns: 1325 | tuple[bool, str]: (success, message) 1326 | """ 1327 | try: 1328 | key_file = get_perm_key_file(app_id, False, system) 1329 | perm_type = perm_type or "filesystems" 1330 | 1331 | # Handle special case for home directory 1332 | if path.lower() == "host": 1333 | filesystem_path = "host" 1334 | elif path.lower() == "host-os": 1335 | filesystem_path = "host-os" 1336 | elif path.lower() == "host-etc": 1337 | filesystem_path = "host-etc" 1338 | elif path.lower() == "home": 1339 | filesystem_path = "home" 1340 | else: 1341 | # Ensure path do not ends with a trailing slash 1342 | filesystem_path = path.rstrip('/') 1343 | 1344 | # Get existing filesystem paths 1345 | existing_paths = key_file.get_string("Context", perm_type) 1346 | 1347 | if existing_paths is None: 1348 | return True, f"No filesystem permissions to remove for {app_id}" 1349 | 1350 | # Split existing paths and normalize them for comparison 1351 | existing_paths_list = existing_paths.split(';') 1352 | normalized_new_path = os.path.abspath(filesystem_path.rstrip('/')) 1353 | normalized_existing_paths = [os.path.abspath(p.rstrip('/')) for p in existing_paths_list] 1354 | 1355 | # Only remove if the path exists 1356 | if normalized_new_path not in normalized_existing_paths: 1357 | return True, f"No permission found for {path} in {app_id}" 1358 | 1359 | # Remove the path from the existing paths 1360 | filtered_paths_list = [p for p in existing_paths_list 1361 | if os.path.abspath(p.rstrip('/')) != normalized_new_path] 1362 | 1363 | # Join remaining paths back together 1364 | new_permissions = ";".join(filtered_paths_list) 1365 | if new_permissions: 1366 | # Save changes 1367 | key_file.set_string("Context", perm_type, new_permissions) 1368 | else: 1369 | key_file.remove_key("Context", perm_type) 1370 | 1371 | # Write the modified metadata back 1372 | try: 1373 | key_file.save_to_file(get_metadata_path(app_id, False, system)) 1374 | except GLib.Error as e: 1375 | return False, f"Failed to save metadata file: {str(e)}" 1376 | 1377 | return True, f"Successfully removed access to {path} for {app_id}" 1378 | 1379 | except GLib.Error as e: 1380 | return False, f"Failed to modify permissions: {str(e)}" 1381 | 1382 | def list_file_perms(app_id: str, system=False) -> tuple[bool, dict[str, list[str]]]|tuple[bool, dict[str, list[str]]]: 1383 | """ 1384 | List filesystem permissions for a Flatpak application. 1385 | 1386 | Args: 1387 | app_id (str): The ID of the Flatpak application 1388 | system (bool): Whether to check system-wide or user installation 1389 | 1390 | Returns: 1391 | tuple[bool, dict[str, list[str]]]: (success, permissions_dict) 1392 | permissions_dict contains: 1393 | - 'paths': list of filesystem paths 1394 | - 'special_paths': list of special paths (home, host, etc.) 1395 | """ 1396 | try: 1397 | key_file = get_perm_key_file(app_id, False, system) 1398 | 1399 | # Initialize result dictionary 1400 | result = { 1401 | "paths": [], 1402 | "special_paths": [] 1403 | } 1404 | 1405 | # Get existing filesystem paths 1406 | existing_paths = key_file.get_string("Context", "filesystems") 1407 | if existing_paths: 1408 | # Split and clean the paths 1409 | paths_list = [p.strip() for p in existing_paths.split(';')] 1410 | 1411 | # Separate special paths from regular ones 1412 | for path in paths_list: 1413 | if path in ["home", "host", "host-os", "host-etc"]: 1414 | result["special_paths"].append(path) 1415 | else: 1416 | result["paths"].append(path) 1417 | 1418 | return True, result 1419 | except GLib.Error: 1420 | return False, {"paths": [], "special_paths": []} 1421 | 1422 | 1423 | def list_other_perm_toggles(app_id: str, perm_type: str, system=False) -> tuple[bool, dict[str, list[str]]]|tuple[bool, dict[str, list[str]]]: 1424 | """ 1425 | List other permission toggles within "Context" for a Flatpak application. 1426 | 1427 | Args: 1428 | app_id (str): The ID of the Flatpak application 1429 | perm_type (str): The type of permissions to list (e.g. "shared", "sockets", "devices", "features", "persistent") 1430 | system (bool): Whether to check system-wide or user installation 1431 | 1432 | Returns: 1433 | tuple[bool, dict[str, list[str]]]: (success, permissions_dict) 1434 | permissions_dict contains: 1435 | - 'paths': list of filesystem paths 1436 | """ 1437 | try: 1438 | key_file = get_perm_key_file(app_id, False, system) 1439 | 1440 | # Initialize result dictionary 1441 | result = { 1442 | "paths": [] 1443 | } 1444 | 1445 | # Get existing filesystem paths 1446 | existing_paths = key_file.get_string("Context", perm_type) 1447 | if existing_paths: 1448 | # Split, clean, and filter out empty paths 1449 | paths_list = [p.strip() for p in existing_paths.split(';') if p.strip()] 1450 | 1451 | # Add filtered paths to result 1452 | result["paths"] = paths_list 1453 | 1454 | return True, result 1455 | except GLib.Error: 1456 | return False, {"paths": []} 1457 | 1458 | # Get existing filesystem paths 1459 | existing_paths = key_file.get_string("Context", perm_type) 1460 | if existing_paths: 1461 | # Split, clean, and filter out empty paths 1462 | paths_list = [p.strip() for p in existing_paths.split(';') if p.strip()] 1463 | 1464 | # Add filtered paths to result 1465 | result["paths"] = paths_list 1466 | 1467 | 1468 | def toggle_other_perms(app_id: str, perm_type: str, option: str, enable: bool, system=False) -> tuple[bool, str]: 1469 | """ 1470 | Toggle a specific permission option for a Flatpak application. 1471 | 1472 | Args: 1473 | app_id (str): The ID of the Flatpak application 1474 | perm_type (str): The type of permissions (shared, sockets, devices, features) 1475 | option (str): The specific permission to toggle 1476 | enable (bool): Whether to enable or disable the permission 1477 | system (bool): Whether to check system-wide or user installation 1478 | 1479 | Returns: 1480 | bool: True if successful, False if operation failed 1481 | """ 1482 | # Get the KeyFile object 1483 | key_file = get_perm_key_file(app_id, False, system) 1484 | 1485 | if not key_file: 1486 | return False, f"Failed to get permissions for {app_id}" 1487 | 1488 | try: 1489 | perms_list = [] 1490 | # Get all keys in the Context section 1491 | # Check if Context section exists 1492 | if not key_file.has_group("Context"): 1493 | key_file.set_string("Context", perm_type, "") 1494 | 1495 | # Now get the keys 1496 | context_keys = key_file.get_keys("Context") 1497 | 1498 | # Check if perm_type exists in the section 1499 | if perm_type not in str(context_keys): 1500 | # Create the key with an empty string 1501 | key_file.set_string("Context", perm_type, "") 1502 | 1503 | # Get the existing permissions 1504 | existing_perms = key_file.get_string("Context", perm_type) 1505 | 1506 | if existing_perms: 1507 | # Split into individual permissions 1508 | perms_list = [perm.strip() for perm in existing_perms.split(';') if perm.strip()] 1509 | 1510 | # Toggle permission 1511 | if enable: 1512 | if option not in perms_list: 1513 | perms_list.append(option) 1514 | else: 1515 | if option in perms_list: 1516 | perms_list.remove(option) 1517 | 1518 | # Join back with semicolons 1519 | new_perms = ";".join(perms_list) 1520 | 1521 | # Save changes 1522 | if new_perms: 1523 | key_file.set_string("Context", perm_type, new_perms) 1524 | else: 1525 | key_file.remove_key("Context", perm_type) 1526 | key_file.save_to_file(get_metadata_path(app_id, False, system)) 1527 | 1528 | return True, f"Successfully {'enabled' if enable else 'disabled'} {option} for {app_id}" 1529 | 1530 | except GLib.Error: 1531 | return False, f"Failed to toggle {option} for {app_id}" 1532 | 1533 | 1534 | def list_other_perm_values(app_id: str, perm_type: str, system=False) -> tuple[bool, dict[str, list[str]]]: 1535 | """ 1536 | List all permission values for a specified type from a Flatpak application's configuration. 1537 | 1538 | Args: 1539 | app_id (str): The ID of the Flatpak application 1540 | perm_type (str): The type of permissions to list (e.g. "environment", "session_bus", "system_bus") 1541 | system (bool): Whether to check system-wide or user installation 1542 | 1543 | Returns: 1544 | tuple[bool, dict[str, list[str]]]: (success, env_vars_dict) 1545 | env_vars_dict contains: 1546 | - 'paths': list of environment variables 1547 | """ 1548 | try: 1549 | key_file = get_perm_key_file(app_id, False, system) 1550 | 1551 | # Initialize result dictionary 1552 | result = { 1553 | "paths": [] 1554 | } 1555 | 1556 | match perm_type.lower(): 1557 | case "environment": 1558 | perm_type = "Environment" 1559 | case "session_bus": 1560 | perm_type = "Session Bus Policy" 1561 | case "system_bus": 1562 | perm_type = "System Bus Policy" 1563 | case _: 1564 | return False, {"paths": []} 1565 | 1566 | # Check if section exists using has_group() 1567 | if key_file.has_group(perm_type): 1568 | # Get all keys in the section 1569 | keys = key_file.get_keys(perm_type) 1570 | 1571 | # Convert ResultTuple to list of individual keys 1572 | keys = list(keys[0]) if hasattr(keys, '__iter__') else [] 1573 | 1574 | # Get each value and add to paths list 1575 | for key in keys: 1576 | value = key_file.get_string(perm_type, key) 1577 | if value: 1578 | result["paths"].append(f"{key}={value}") 1579 | 1580 | return True, result 1581 | except GLib.Error as e: 1582 | print(f"GLib.Error: {e}") 1583 | return False, {"paths": []} 1584 | except Exception as e: 1585 | print(f"Other error: {e}") 1586 | return False, {"paths": []} 1587 | 1588 | def add_permission_value(app_id: str, perm_type: str, value: str, system=False) -> tuple[bool, str]: 1589 | """ 1590 | Add a permission value to a Flatpak application's configuration. 1591 | 1592 | Args: 1593 | app_id (str): The ID of the Flatpak application 1594 | perm_type (str): The type of permissions (e.g. "environment", "session_bus", "system_bus") 1595 | value (str): The complete permission value to add (e.g. "XCURSOR_PATH=/run/host/user-share/icons:/run/host/share/icons") 1596 | system (bool): Whether to modify system-wide or user installation 1597 | 1598 | Returns: 1599 | tuple[bool, str]: (success, message) 1600 | """ 1601 | try: 1602 | key_file = get_perm_key_file(app_id, False, system) 1603 | 1604 | # Convert perm_type to the correct format 1605 | match perm_type.lower(): 1606 | case "environment": 1607 | perm_type = "Environment" 1608 | case "session_bus": 1609 | perm_type = "Session Bus Policy" 1610 | case "system_bus": 1611 | perm_type = "System Bus Policy" 1612 | case _: 1613 | return False, "Invalid permission type" 1614 | 1615 | # Split the value into key and actual value 1616 | parts = value.split('=', 1) 1617 | if len(parts) != 2: 1618 | return False, "Value must be in format 'key=value'" 1619 | 1620 | key, val = parts 1621 | 1622 | if perm_type in ['Session Bus Policy', 'System Bus Policy']: 1623 | if val not in ['talk', 'own']: 1624 | return False, "Value must be in format 'key=value' with value as 'talk' or 'own'" 1625 | 1626 | # Set the value 1627 | key_file.set_string(perm_type, key, val) 1628 | 1629 | # Save the changes 1630 | key_file.save_to_file(get_metadata_path(app_id, False, system)) 1631 | 1632 | return True, f"Successfully added {value} to {perm_type} section" 1633 | except GLib.Error as e: 1634 | return False, f"Error adding permission: {str(e)}" 1635 | 1636 | def remove_permission_value(app_id: str, perm_type: str, value: str, system=False) -> tuple[bool, str]: 1637 | """ 1638 | Remove a permission value from a Flatpak application's configuration. 1639 | 1640 | Args: 1641 | app_id (str): The ID of the Flatpak application 1642 | perm_type (str): The type of permissions (e.g. "environment", "session_bus", "system_bus") 1643 | value (str): The complete permission value to remove (e.g. "XCURSOR_PATH=/run/host/user-share/icons:/run/host/share/icons") 1644 | system (bool): Whether to modify system-wide or user installation 1645 | 1646 | Returns: 1647 | tuple[bool, str]: (success, message) 1648 | """ 1649 | try: 1650 | key_file = get_perm_key_file(app_id, False, system) 1651 | 1652 | # Convert perm_type to the correct format 1653 | match perm_type.lower(): 1654 | case "environment": 1655 | perm_type = "Environment" 1656 | case "session_bus": 1657 | perm_type = "Session Bus Policy" 1658 | case "system_bus": 1659 | perm_type = "System Bus Policy" 1660 | case _: 1661 | return False, "Invalid permission type" 1662 | 1663 | # Split the value into key and actual value 1664 | parts = value.split('=', 1) 1665 | if len(parts) != 2: 1666 | return False, "Value must be in format 'key=value'" 1667 | 1668 | key, val = parts 1669 | # Check if section exists 1670 | if not key_file.has_group(perm_type): 1671 | return False, f"Section {perm_type} does not exist" 1672 | 1673 | # Remove the value 1674 | key_file.remove_key(perm_type, key) 1675 | 1676 | # Save the changes 1677 | key_file.save_to_file(get_metadata_path(app_id, False, system)) 1678 | 1679 | return True, f"Successfully removed {value} from {perm_type} section" 1680 | except GLib.Error as e: 1681 | return False, f"Error removing permission: {str(e)}" 1682 | 1683 | def global_add_file_permissions(path: str, perm_type=None, override=True, system=False) -> tuple[bool, str]: 1684 | """ 1685 | Add filesystem permissions to all Flatpak applications globally. 1686 | 1687 | Args: 1688 | path (str): The path to grant access to. Can be: 1689 | - "home" for home directory access 1690 | - "/path/to/directory" for custom directory access 1691 | perm_type (str): The type of permissions to remove (e.g. "filesystems", "persistent") default is "filesystems" 1692 | override (bool): Whether to use global metadata file instead of per-app. 1693 | system (bool): Whether to modify system-wide or user installation 1694 | 1695 | Returns: 1696 | tuple[bool, str]: (success, message) 1697 | """ 1698 | 1699 | try: 1700 | key_file = get_perm_key_file(None, override, system) 1701 | perm_type = perm_type or "filesystems" 1702 | # Handle special case for home directory 1703 | if path.lower() == "host": 1704 | filesystem_path = "host" 1705 | elif path.lower() == "host-os": 1706 | filesystem_path = "host-os" 1707 | elif path.lower() == "host-etc": 1708 | filesystem_path = "host-etc" 1709 | elif path.lower() == "home": 1710 | filesystem_path = "home" 1711 | else: 1712 | # Ensure path do not ends with a trailing slash 1713 | filesystem_path = path.rstrip('/') 1714 | 1715 | # Validate absolute paths start with / 1716 | if filesystem_path.startswith('/'): 1717 | filesystem_path = '/' + filesystem_path.lstrip('/') 1718 | 1719 | if not key_file.has_group("Context"): 1720 | key_file.set_string("Context", perm_type, "") 1721 | 1722 | context_keys = key_file.get_keys("Context") 1723 | if perm_type not in str(context_keys): 1724 | key_file.set_string("Context", perm_type, "") 1725 | 1726 | existing_paths = key_file.get_string("Context", perm_type) 1727 | if existing_paths is None or existing_paths == "": 1728 | # If no filesystems exist, set the exact path provided 1729 | key_file.set_string("Context", perm_type, filesystem_path) 1730 | else: 1731 | existing_paths_list = existing_paths.split(';') 1732 | normalized_new_path = os.path.abspath(filesystem_path.rstrip('/')) 1733 | normalized_existing_paths = [os.path.abspath(p.rstrip('/')) for p in existing_paths_list] 1734 | 1735 | if normalized_new_path not in normalized_existing_paths: 1736 | # Add new path with proper separator 1737 | separator = ';' if existing_paths.endswith(';') else ';' 1738 | key_file.set_string("Context", perm_type, 1739 | existing_paths + separator + filesystem_path) 1740 | 1741 | # Write the modified metadata back 1742 | try: 1743 | key_file.save_to_file(get_metadata_path(None, override, system)) 1744 | except GLib.Error as e: 1745 | return False, f"Failed to save metadata file: {str(e)}" 1746 | 1747 | return True, f"Successfully granted access to {path} globally" 1748 | 1749 | except GLib.Error as e: 1750 | return False, f"Failed to modify permissions: {str(e)}" 1751 | 1752 | 1753 | def global_remove_file_permissions(path: str, perm_type=None, override=True, system=False) -> tuple[bool, str]: 1754 | """ 1755 | Remove filesystem permissions from all Flatpak applications globally. 1756 | 1757 | Args: 1758 | path (str): The path to revoke access to. Can be: 1759 | - "home" for home directory access 1760 | - "/path/to/directory" for custom directory access 1761 | perm_type (str): The type of permissions to remove (e.g. "filesystems", "persistent") default is "filesystems" 1762 | override (bool): Whether to use global metadata file instead of per-app. 1763 | system (bool): Whether to modify system-wide or user installation 1764 | 1765 | Returns: 1766 | tuple[bool, str]: (success, message) 1767 | """ 1768 | try: 1769 | key_file = get_perm_key_file(None, override, system) 1770 | perm_type = perm_type or "filesystems" 1771 | # Handle special case for home directory 1772 | if path.lower() == "host": 1773 | filesystem_path = "host" 1774 | elif path.lower() == "host-os": 1775 | filesystem_path = "host-os" 1776 | elif path.lower() == "host-etc": 1777 | filesystem_path = "host-etc" 1778 | elif path.lower() == "home": 1779 | filesystem_path = "home" 1780 | else: 1781 | # Ensure path do not ends with a trailing slash 1782 | filesystem_path = path.rstrip('/') 1783 | 1784 | # Get existing filesystem paths 1785 | existing_paths = key_file.get_string("Context", perm_type) 1786 | 1787 | if existing_paths is None: 1788 | return True, "No filesystem permissions to remove globally" 1789 | 1790 | # Split existing paths and normalize them for comparison 1791 | existing_paths_list = existing_paths.split(';') 1792 | normalized_new_path = os.path.abspath(filesystem_path.rstrip('/')) 1793 | normalized_existing_paths = [os.path.abspath(p.rstrip('/')) for p in existing_paths_list] 1794 | 1795 | # Only remove if the path exists 1796 | if normalized_new_path not in normalized_existing_paths: 1797 | return True, f"No permission found for {path} globally" 1798 | 1799 | # Remove the path from the existing paths 1800 | filtered_paths_list = [p for p in existing_paths_list 1801 | if os.path.abspath(p.rstrip('/')) != normalized_new_path] 1802 | 1803 | # Join remaining paths back together 1804 | new_permissions = ";".join(filtered_paths_list) 1805 | if new_permissions: 1806 | # Save changes 1807 | key_file.set_string("Context", perm_type, new_permissions) 1808 | else: 1809 | key_file.remove_key("Context", perm_type) 1810 | 1811 | # Write the modified metadata back 1812 | try: 1813 | key_file.save_to_file(get_metadata_path(None, override, system)) 1814 | except GLib.Error as e: 1815 | return False, f"Failed to save metadata file: {str(e)}" 1816 | 1817 | return True, f"Successfully removed access to {path} globally" 1818 | 1819 | except GLib.Error as e: 1820 | return False, f"Failed to modify permissions: {str(e)}" 1821 | 1822 | def global_list_file_perms(override=True, system=False) -> tuple[bool, dict[str, list[str]]]|tuple[bool, dict[str, list[str]]]: 1823 | """ 1824 | List filesystem permissions for all Flatpak applications globally. 1825 | 1826 | Args: 1827 | override (bool): Whether to use global metadata file instead of per-app. 1828 | system (bool): Whether to check system-wide or user installation 1829 | 1830 | Returns: 1831 | tuple[bool, dict[str, list[str]]]: (success, permissions_dict) 1832 | permissions_dict contains: 1833 | - 'paths': list of filesystem paths 1834 | - 'special_paths': list of special paths (home, host, etc.) 1835 | """ 1836 | try: 1837 | key_file = get_perm_key_file(None, override, system) 1838 | 1839 | # Initialize result dictionary 1840 | result = { 1841 | "paths": [], 1842 | "special_paths": [] 1843 | } 1844 | 1845 | # Get existing filesystem paths 1846 | existing_paths = key_file.get_string("Context", "filesystems") 1847 | if existing_paths: 1848 | # Split and clean the paths 1849 | paths_list = [p.strip() for p in existing_paths.split(';')] 1850 | 1851 | # Separate special paths from regular ones 1852 | for path in paths_list: 1853 | if path in ["home", "host", "host-os", "host-etc"]: 1854 | result["special_paths"].append(path) 1855 | else: 1856 | result["paths"].append(path) 1857 | 1858 | return True, result 1859 | except GLib.Error: 1860 | return False, {"paths": [], "special_paths": []} 1861 | 1862 | 1863 | def global_list_other_perm_toggles(perm_type: str, override=True, system=False) -> tuple[bool, dict[str, list[str]]]|tuple[bool, dict[str, list[str]]]: 1864 | """ 1865 | List other permission toggles within "Context" for all Flatpak applications globally. 1866 | 1867 | Args: 1868 | perm_type (str): The type of permissions to list (e.g. "shared", "sockets", "devices", "features", "persistent") 1869 | override (bool): Whether to use global metadata file instead of per-app. 1870 | system (bool): Whether to check system-wide or user installation 1871 | 1872 | Returns: 1873 | tuple[bool, dict[str, list[str]]]: (success, permissions_dict) 1874 | permissions_dict contains: 1875 | - 'paths': list of filesystem paths 1876 | """ 1877 | try: 1878 | key_file = get_perm_key_file(None, override, system) 1879 | 1880 | # Initialize result dictionary 1881 | result = { 1882 | "paths": [] 1883 | } 1884 | 1885 | # Get existing filesystem paths 1886 | existing_paths = key_file.get_string("Context", perm_type) 1887 | if existing_paths: 1888 | # Split, clean, and filter out empty paths 1889 | paths_list = [p.strip() for p in existing_paths.split(';') if p.strip()] 1890 | 1891 | # Add filtered paths to result 1892 | result["paths"] = paths_list 1893 | 1894 | return True, result 1895 | except GLib.Error: 1896 | return False, {"paths": []} 1897 | 1898 | # Get existing filesystem paths 1899 | existing_paths = key_file.get_string("Context", perm_type) 1900 | if existing_paths: 1901 | # Split, clean, and filter out empty paths 1902 | paths_list = [p.strip() for p in existing_paths.split(';') if p.strip()] 1903 | 1904 | # Add filtered paths to result 1905 | result["paths"] = paths_list 1906 | 1907 | 1908 | def global_toggle_other_perms(perm_type: str, option: str, enable: bool, override=True, system=False) -> tuple[bool, str]: 1909 | """ 1910 | Toggle a specific permission option for all Flatpak applications globally. 1911 | 1912 | Args: 1913 | perm_type (str): The type of permissions (shared, sockets, devices, features) 1914 | option (str): The specific permission to toggle 1915 | enable (bool): Whether to enable or disable the permission 1916 | override (bool): Whether to use global metadata file instead of per-app. 1917 | system (bool): Whether to check system-wide or user installation 1918 | 1919 | Returns: 1920 | bool: True if successful, False if operation failed 1921 | """ 1922 | # Get the KeyFile object 1923 | key_file = get_perm_key_file(None, override, system) 1924 | 1925 | if not key_file: 1926 | return False, "Failed to get permissions globally" 1927 | 1928 | try: 1929 | perms_list = [] 1930 | # Get all keys in the Context section 1931 | # Check if Context section exists 1932 | if not key_file.has_group("Context"): 1933 | key_file.set_string("Context", perm_type, "") 1934 | 1935 | # Now get the keys 1936 | context_keys = key_file.get_keys("Context") 1937 | 1938 | # Check if perm_type exists in the section 1939 | if perm_type not in str(context_keys): 1940 | # Create the key with an empty string 1941 | key_file.set_string("Context", perm_type, "") 1942 | 1943 | # Get the existing permissions 1944 | existing_perms = key_file.get_string("Context", perm_type) 1945 | 1946 | if existing_perms: 1947 | # Split into individual permissions 1948 | perms_list = [perm.strip() for perm in existing_perms.split(';') if perm.strip()] 1949 | 1950 | # Toggle permission 1951 | if enable: 1952 | if option not in perms_list: 1953 | perms_list.append(option) 1954 | else: 1955 | if option in perms_list: 1956 | perms_list.remove(option) 1957 | 1958 | # Join back with semicolons 1959 | new_perms = ";".join(perms_list) 1960 | 1961 | # Save changes 1962 | if new_perms: 1963 | key_file.set_string("Context", perm_type, new_perms) 1964 | else: 1965 | key_file.remove_key("Context", perm_type) 1966 | key_file.save_to_file(get_metadata_path(None, override, system)) 1967 | 1968 | return True, f"Successfully {'enabled' if enable else 'disabled'} {option} globally" 1969 | 1970 | except GLib.Error: 1971 | return False, f"Failed to toggle {option} globally" 1972 | 1973 | 1974 | def global_list_other_perm_values(perm_type: str, override=True, system=False) -> tuple[bool, dict[str, list[str]]]: 1975 | """ 1976 | List all permission values for a specified type for all Flatpak applications globally. 1977 | 1978 | Args: 1979 | perm_type (str): The type of permissions to list (e.g. "environment", "session_bus", "system_bus") 1980 | override (bool): Whether to use global metadata file instead of per-app. 1981 | system (bool): Whether to check system-wide or user installation 1982 | 1983 | Returns: 1984 | tuple[bool, dict[str, list[str]]]: (success, env_vars_dict) 1985 | env_vars_dict contains: 1986 | - 'paths': list of environment variables 1987 | """ 1988 | try: 1989 | key_file = get_perm_key_file(None, override, system) 1990 | 1991 | # Initialize result dictionary 1992 | result = { 1993 | "paths": [] 1994 | } 1995 | 1996 | match perm_type.lower(): 1997 | case "environment": 1998 | perm_type = "Environment" 1999 | case "session_bus": 2000 | perm_type = "Session Bus Policy" 2001 | case "system_bus": 2002 | perm_type = "System Bus Policy" 2003 | case _: 2004 | return False, {"paths": []} 2005 | 2006 | # Check if section exists using has_group() 2007 | if key_file.has_group(perm_type): 2008 | # Get all keys in the section 2009 | keys = key_file.get_keys(perm_type) 2010 | 2011 | # Convert ResultTuple to list of individual keys 2012 | keys = list(keys[0]) if hasattr(keys, '__iter__') else [] 2013 | 2014 | # Get each value and add to paths list 2015 | for key in keys: 2016 | value = key_file.get_string(perm_type, key) 2017 | if value: 2018 | result["paths"].append(f"{key}={value}") 2019 | 2020 | return True, result 2021 | except GLib.Error as e: 2022 | print(f"GLib.Error: {e}") 2023 | return False, {"paths": []} 2024 | except Exception as e: 2025 | print(f"Other error: {e}") 2026 | return False, {"paths": []} 2027 | 2028 | def global_add_permission_value(perm_type: str, value: str, override=True, system=False) -> tuple[bool, str]: 2029 | """ 2030 | Add a permission value to all Flatpak applications globally. 2031 | 2032 | Args: 2033 | perm_type (str): The type of permissions (e.g. "environment", "session_bus", "system_bus") 2034 | value (str): The complete permission value to add (e.g. "XCURSOR_PATH=/run/host/user-share/icons:/run/host/share/icons") 2035 | override (bool): Whether to use global metadata file instead of per-app. 2036 | system (bool): Whether to modify system-wide or user installation 2037 | 2038 | Returns: 2039 | tuple[bool, str]: (success, message) 2040 | """ 2041 | try: 2042 | key_file = get_perm_key_file(None, override, system) 2043 | 2044 | # Convert perm_type to the correct format 2045 | match perm_type.lower(): 2046 | case "environment": 2047 | perm_type = "Environment" 2048 | case "session_bus": 2049 | perm_type = "Session Bus Policy" 2050 | case "system_bus": 2051 | perm_type = "System Bus Policy" 2052 | case _: 2053 | return False, "Invalid permission type" 2054 | 2055 | # Split the value into key and actual value 2056 | parts = value.split('=', 1) 2057 | if len(parts) != 2: 2058 | return False, "Value must be in format 'key=value'" 2059 | 2060 | key, val = parts 2061 | 2062 | if perm_type in ['Session Bus Policy', 'System Bus Policy']: 2063 | if val not in ['talk', 'own']: 2064 | return False, "Value must be in format 'key=value' with value as 'talk' or 'own'" 2065 | 2066 | # Set the value 2067 | key_file.set_string(perm_type, key, val) 2068 | 2069 | # Save the changes 2070 | key_file.save_to_file(get_metadata_path(None, override, system)) 2071 | 2072 | return True, f"Successfully added {value} to {perm_type} section" 2073 | except GLib.Error as e: 2074 | return False, f"Error adding permission: {str(e)}" 2075 | 2076 | 2077 | def global_remove_permission_value(perm_type: str, value: str, override=True, system=False) -> tuple[bool, str]: 2078 | """ 2079 | Remove a permission value from all Flatpak applications globally. 2080 | 2081 | Args: 2082 | perm_type (str): The type of permissions (e.g. "environment", "session_bus", "system_bus") 2083 | value (str): The complete permission value to remove (e.g. "XCURSOR_PATH=/run/host/user-share/icons:/run/host/share/icons") 2084 | override (bool): Whether to use global metadata file instead of per-app. 2085 | system (bool): Whether to modify system-wide or user installation 2086 | 2087 | Returns: 2088 | tuple[bool, str]: (success, message) 2089 | """ 2090 | try: 2091 | key_file = get_perm_key_file(None, override, system) 2092 | 2093 | # Convert perm_type to the correct format 2094 | match perm_type.lower(): 2095 | case "environment": 2096 | perm_type = "Environment" 2097 | case "session_bus": 2098 | perm_type = "Session Bus Policy" 2099 | case "system_bus": 2100 | perm_type = "System Bus Policy" 2101 | case _: 2102 | return False, "Invalid permission type" 2103 | 2104 | # Split the value into key and actual value 2105 | parts = value.split('=', 1) 2106 | if len(parts) != 2: 2107 | return False, "Value must be in format 'key=value'" 2108 | 2109 | key, val = parts 2110 | # Check if section exists 2111 | if not key_file.has_group(perm_type): 2112 | return False, f"Section {perm_type} does not exist" 2113 | 2114 | # Remove the value 2115 | key_file.remove_key(perm_type, key) 2116 | 2117 | # Save the changes 2118 | key_file.save_to_file(get_metadata_path(None, override, system)) 2119 | 2120 | return True, f"Successfully removed {value} from {perm_type} section" 2121 | except GLib.Error as e: 2122 | return False, f"Error removing permission: {str(e)}" 2123 | 2124 | def portal_get_permission_store(): 2125 | bus = dbus.SessionBus() 2126 | portal_service = bus.get_object("org.freedesktop.impl.portal.PermissionStore", "/org/freedesktop/impl/portal/PermissionStore") 2127 | permission_store = dbus.Interface(portal_service, "org.freedesktop.impl.portal.PermissionStore") 2128 | return permission_store 2129 | 2130 | def portal_set_app_permissions(portal: str, app_id: str, status_str: str): 2131 | 2132 | portal_id = "" 2133 | # This is done separately incase user types "notification" instead of "notifications" 2134 | if portal.lower() in "notifications": 2135 | portal = "notifications" 2136 | 2137 | status = "no" 2138 | if status_str in ["yes", "true", "1", "enable"]: 2139 | status = "yes" 2140 | 2141 | match portal.lower(): 2142 | case "background": 2143 | portal_id = "background" 2144 | case "notifications": 2145 | portal_id = "notification" 2146 | case "microphone": 2147 | portal = "devices" 2148 | portal_id = "microphone" 2149 | case "speakers": 2150 | portal = "devices" 2151 | portal_id = "speakers" 2152 | case "camera": 2153 | portal = "devices" 2154 | portal_id = "camera" 2155 | case "location": 2156 | portal_id = "location" 2157 | try: 2158 | permission_store = portal_get_permission_store() 2159 | permission_store.SetPermission( 2160 | portal, # Category (string) 2161 | False, # Permission status (boolean: False means 'no') 2162 | portal_id, # Permission type (string) 2163 | app_id, # App ID (string) 2164 | [dbus.String(status)] # Array of permissions (string array) 2165 | ) 2166 | return True, f"Permission set to {status} for {app_id} in {portal_id} portal" 2167 | except: 2168 | return False, f"Failed to set permission for {app_id} in {portal_id} portal" 2169 | 2170 | def portal_get_app_permissions(app_id: str): 2171 | permissions = portal_lookup_all() 2172 | if not permissions: 2173 | return False, f"Permission not found for {app_id} in any portal" 2174 | 2175 | # Store results for each portal where we find the app 2176 | app_permissions = {} 2177 | 2178 | # Iterate through all portal entries 2179 | for portal_id, permission_data in permissions: 2180 | # Extract the DBus Dictionary containing app permissions 2181 | dbus_dict = permission_data[0] # First element contains the dictionary 2182 | 2183 | # Check if our target app_id exists in the dictionary 2184 | if app_id in dbus_dict: 2185 | # Get the array of values for this app 2186 | value_array = dbus_dict[app_id] 2187 | 2188 | # Return the first string value (typically 'yes' or 'no') 2189 | if len(value_array) > 0: 2190 | app_permissions[portal_id] = str(value_array[0]) 2191 | 2192 | # Format and return the results 2193 | if app_permissions: 2194 | return True, app_permissions 2195 | 2196 | return False, f"No permissions found for {app_id} in any portal" 2197 | 2198 | 2199 | def portal_lookup(portal: str): 2200 | try: 2201 | portal_id = "" 2202 | # This is done separately incase user types "notification" instead of "notifications" 2203 | if portal.lower() in "notifications": 2204 | portal = "notifications" 2205 | 2206 | match portal.lower(): 2207 | case "background": 2208 | portal_id = "background" 2209 | case "notifications": 2210 | portal_id = "notification" 2211 | case "microphone": 2212 | portal = "devices" 2213 | portal_id = "microphone" 2214 | case "speakers": 2215 | portal = "devices" 2216 | portal_id = "speakers" 2217 | case "camera": 2218 | portal = "devices" 2219 | portal_id = "camera" 2220 | case "location": 2221 | portal_id = "location" 2222 | 2223 | permission_store = portal_get_permission_store() 2224 | permissions = permission_store.Lookup( 2225 | portal, # Category (string) 2226 | portal_id # Permission type (string) 2227 | ) 2228 | if permissions: 2229 | return permissions 2230 | except dbus.exceptions.DBusException: 2231 | # We don't care if a lookup fails, that just means no options were set for the portal 2232 | return [] 2233 | 2234 | def portal_lookup_all(): 2235 | portal_permissions = [] 2236 | # This is done separately incase user types "notification" instead of "notifications" 2237 | 2238 | portal_names = ["background", "notifications", "microphone", "speakers", "camera", "location"] 2239 | for portal in portal_names: 2240 | try: 2241 | permissions = portal_lookup( 2242 | portal # Category (string) 2243 | ) 2244 | if permissions: 2245 | portal_permissions.append((portal, permissions)) 2246 | except dbus.exceptions.DBusException: 2247 | # We don't care if a lookup fails, that just means no options were set for the portal 2248 | return [] 2249 | return portal_permissions 2250 | 2251 | def screenshot_details(screenshot): 2252 | # Try to get the image with required parameters 2253 | try: 2254 | # get_image() requires 4 arguments: width, height, scale, device_scale 2255 | image = screenshot.get_image(800, 600, 1.0) 2256 | return image 2257 | except Exception as e: 2258 | print(f"Error getting image: {e}") 2259 | 2260 | def main(): 2261 | parser = argparse.ArgumentParser(description='Search Flatpak packages') 2262 | parser.add_argument('--id', help='Application ID to search for') 2263 | parser.add_argument('--repo', help='Filter results to specific repository') 2264 | parser.add_argument('--list-all', action='store_true', help='List all available apps') 2265 | parser.add_argument('--categories', action='store_true', help='Show apps grouped by category') 2266 | parser.add_argument('--subcategories', action='store_true', 2267 | help='Show apps grouped by subcategory') 2268 | parser.add_argument('--list-installed', action='store_true', 2269 | help='List all installed Flatpak applications') 2270 | parser.add_argument('--check-updates', action='store_true', 2271 | help='Check for available updates') 2272 | parser.add_argument('--list-repos', action='store_true', 2273 | help='List all configured Flatpak repositories') 2274 | parser.add_argument('--add-repo', type=str, metavar='REPO_FILE', 2275 | help='Add a new repository from a .flatpakrepo file') 2276 | parser.add_argument('--remove-repo', type=str, metavar='REPO_NAME', 2277 | help='Remove a Flatpak repository') 2278 | parser.add_argument('--toggle-repo', type=str, 2279 | metavar=('ENABLE/DISABLE'), 2280 | help='Enable or disable a repository') 2281 | parser.add_argument('--install', type=str, metavar='APP_ID', 2282 | help='Install a Flatpak package') 2283 | parser.add_argument('--remove', type=str, metavar='APP_ID', 2284 | help='Remove a Flatpak package') 2285 | parser.add_argument('--update', type=str, metavar='APP_ID', 2286 | help='Update a Flatpak package') 2287 | parser.add_argument('--update-all', action='store_true', 2288 | help='Update all Flatpak packages') 2289 | parser.add_argument('--system', action='store_true', help='Install as system instead of user') 2290 | parser.add_argument('--refresh', action='store_true', help='Install as system instead of user') 2291 | parser.add_argument('--refresh-local', action='store_true', help='Install as system instead of user') 2292 | parser.add_argument('--add-file-perms', type=str, metavar='PATH', 2293 | help='Add file permissions to an app (e.g. any defaults: host, host-os, host-etc, home, or "/path/to/directory" for custom paths)') 2294 | parser.add_argument('--remove-file-perms', type=str, metavar='PATH', 2295 | help='Remove file permissions from an app (e.g. any defaults: host, host-os, host-etc, home, or "/path/to/directory" for custom paths)') 2296 | parser.add_argument('--list-file-perms', action='store_true', 2297 | help='List configured file permissions for an app') 2298 | parser.add_argument('--list-other-perm-toggles', type=str, metavar='PERM_NAME', 2299 | help='List configured other permission toggles for an app (e.g. "shared", "sockets", "devices", "features")') 2300 | parser.add_argument('--toggle-other-perms', type=str, metavar=('ENABLE/DISABLE'), 2301 | help='Toggle other permissions on/off (True/False)') 2302 | parser.add_argument('--perm-type', type=str, 2303 | help='Type of permission to toggle (shared, sockets, devices, features, persistent)') 2304 | parser.add_argument('--perm-option', type=str, 2305 | help='Specific permission option to toggle (e.g. network, ipc)') 2306 | parser.add_argument('--list-other-perm-values', type=str, metavar='PERM_NAME', 2307 | help='List configured other permission group values for an app (e.g. "environment", "session_bus", "system_bus")') 2308 | parser.add_argument('--add-other-perm-values', type=str, metavar='TYPE', 2309 | help='Add a permission value (e.g. "environment", "session_bus", "system_bus")') 2310 | parser.add_argument('--remove-other-perm-values', type=str, metavar='TYPE', 2311 | help='Remove a permission value (e.g. "environment", "session_bus", "system_bus")') 2312 | parser.add_argument('--perm-value', type=str, metavar='VALUE', 2313 | help='The complete permission value to add or remove (e.g. "XCURSOR_PATH=/run/host/user-share/icons:/run/host/share/icons")') 2314 | parser.add_argument('--override', action='store_true', help='Set global permission override instead of per-application') 2315 | parser.add_argument('--global-add-file-perms', type=str, metavar='PATH', 2316 | help='Add file permissions to an app (e.g. any defaults: host, host-os, host-etc, home, or "/path/to/directory" for custom paths)') 2317 | parser.add_argument('--global-remove-file-perms', type=str, metavar='PATH', 2318 | help='Remove file permissions from an app (e.g. any defaults: host, host-os, host-etc, home, or "/path/to/directory" for custom paths)') 2319 | parser.add_argument('--global-list-file-perms', action='store_true', 2320 | help='List configured file permissions for an app') 2321 | parser.add_argument('--global-list-other-perm-toggles', type=str, metavar='PERM_NAME', 2322 | help='List configured other permission toggles for an app (e.g. "shared", "sockets", "devices", "features")') 2323 | parser.add_argument('--global-toggle-other-perms', type=str, metavar=('ENABLE/DISABLE'), 2324 | help='Toggle other permissions on/off (True/False)') 2325 | parser.add_argument('--global-list-other-perm-values', type=str, metavar='PERM_NAME', 2326 | help='List configured other permission group values for an app (e.g. "environment", "session_bus", "system_bus")') 2327 | parser.add_argument('--global-add-other-perm-values', type=str, metavar='TYPE', 2328 | help='Add a permission value (e.g. "environment", "session_bus", "system_bus")') 2329 | parser.add_argument('--global-remove-other-perm-values', type=str, metavar='TYPE', 2330 | help='Remove a permission value (e.g. "environment", "session_bus", "system_bus")') 2331 | parser.add_argument('--get-app-portal-permissions', action='store_true', 2332 | help='Check specified portal permissions (e.g. "background", "notifications", "microphone", "speakers", "camera", "location") for a specified application ID.') 2333 | parser.add_argument('--get-portal-permissions', type=str, metavar='TYPE', 2334 | help='List all current portal permissions for all applications') 2335 | parser.add_argument('--get-all-portal-permissions', action='store_true', 2336 | help='List all current portal permissions for all applications') 2337 | parser.add_argument('--set-app-portal-permissions', type=str, metavar='TYPE', 2338 | help='Set specified portal permissions (e.g. "background", "notifications", "microphone", "speakers", "camera", "location") yes/no for a specified application ID.') 2339 | parser.add_argument('--portal-perm-value', type=str, metavar='TYPE', 2340 | help='Set specified portal permissions value (yes/no) for a specified application ID.') 2341 | 2342 | args = parser.parse_args() 2343 | 2344 | # Handle repository operations 2345 | if args.toggle_repo: 2346 | handle_repo_toggle(args) 2347 | return 2348 | 2349 | if args.list_repos: 2350 | handle_list_repos(args) 2351 | return 2352 | 2353 | if args.add_repo: 2354 | handle_add_repo(args) 2355 | return 2356 | 2357 | if args.remove_repo: 2358 | handle_remove_repo(args) 2359 | return 2360 | 2361 | # Handle package operations 2362 | searcher = get_reposearcher(args.system) 2363 | 2364 | if args.install: 2365 | handle_install(args, searcher) 2366 | return 2367 | 2368 | if args.remove: 2369 | handle_remove(args, searcher) 2370 | return 2371 | 2372 | if args.update: 2373 | handle_update(args, searcher) 2374 | return 2375 | 2376 | if args.update_all: 2377 | handle_update_all(args, searcher) 2378 | return 2379 | 2380 | # Handle information operations 2381 | if args.list_installed: 2382 | handle_list_installed(args, searcher) 2383 | return 2384 | 2385 | if args.check_updates: 2386 | handle_check_updates(args, searcher) 2387 | return 2388 | 2389 | if args.list_all: 2390 | handle_list_all(args, searcher) 2391 | return 2392 | 2393 | if args.categories: 2394 | handle_categories(args, searcher) 2395 | return 2396 | 2397 | if args.subcategories: 2398 | handle_subcategories(args, searcher) 2399 | return 2400 | 2401 | if args.id: 2402 | if args.add_file_perms: 2403 | handle_add_file_perms(args, searcher) 2404 | return 2405 | if args.remove_file_perms: 2406 | handle_remove_file_perms(args, searcher) 2407 | return 2408 | if args.list_file_perms: 2409 | handle_list_file_perms(args, searcher) 2410 | return 2411 | if args.list_other_perm_toggles: 2412 | handle_list_other_perm_toggles(args, searcher) 2413 | return 2414 | if args.list_other_perm_values: 2415 | handle_list_other_perm_values(args, searcher) 2416 | return 2417 | if args.toggle_other_perms: 2418 | handle_toggle_other_perms(args, searcher) 2419 | return 2420 | if args.add_other_perm_values: 2421 | handle_add_other_perm_values(args, searcher) 2422 | return 2423 | if args.remove_other_perm_values: 2424 | handle_remove_other_perm_values(args, searcher) 2425 | return 2426 | if args.get_app_portal_permissions: 2427 | handle_get_app_portal_permissions(args, searcher) 2428 | return 2429 | if args.set_app_portal_permissions: 2430 | handle_set_app_portal_permissions(args, searcher) 2431 | return 2432 | else: 2433 | handle_search(args, searcher) 2434 | return 2435 | 2436 | if args.override: 2437 | if args.global_add_file_perms: 2438 | handle_global_add_file_perms(args, searcher) 2439 | return 2440 | if args.global_remove_file_perms: 2441 | handle_global_remove_file_perms(args, searcher) 2442 | return 2443 | if args.global_list_file_perms: 2444 | handle_global_list_file_perms(args, searcher) 2445 | return 2446 | if args.global_list_other_perm_toggles: 2447 | handle_global_list_other_perm_toggles(args, searcher) 2448 | return 2449 | if args.global_list_other_perm_values: 2450 | handle_global_list_other_perm_values(args, searcher) 2451 | return 2452 | if args.global_toggle_other_perms: 2453 | handle_global_toggle_other_perms(args, searcher) 2454 | return 2455 | if args.global_add_other_perm_values: 2456 | handle_global_add_other_perm_values(args, searcher) 2457 | return 2458 | if args.global_remove_other_perm_values: 2459 | handle_global_remove_other_perm_values(args, searcher) 2460 | return 2461 | else: 2462 | print("Missing options. Use -h for help.") 2463 | 2464 | if args.get_all_portal_permissions: 2465 | result = portal_lookup_all() 2466 | if result: 2467 | print("\nPortal Permissions:") 2468 | print("-" * 50) 2469 | for portal_id, permissions in result: 2470 | print(f"{portal_id}: {permissions}") 2471 | else: 2472 | print("No app permissions found set for any portals") 2473 | return 2474 | 2475 | if args.get_portal_permissions: 2476 | result = portal_lookup(args.get_portal_permissions) 2477 | if result: 2478 | print("\nPortal Permissions:") 2479 | print("-" * 50) 2480 | for permissions in result: 2481 | print(f"{args.get_portal_permissions}: {permissions}") 2482 | else: 2483 | print(f"No app permissions found for {args.get_portal_permissions} portal") 2484 | return 2485 | 2486 | print("Missing options. Use -h for help.") 2487 | 2488 | def handle_repo_toggle(args): 2489 | repo_name = args.repo 2490 | if not repo_name: 2491 | print("Error: must specify a repo.") 2492 | sys.exit(1) 2493 | 2494 | get_status = args.toggle_repo.lower() in ['true', 'enable'] 2495 | try: 2496 | success, message = repotoggle(repo_name, get_status, args.system) 2497 | print(f"{message}") 2498 | except GLib.Error as e: 2499 | print(f"{str(e)}") 2500 | 2501 | def handle_list_repos(args): 2502 | repos = repolist(args.system) 2503 | print("\nConfigured Repositories:") 2504 | for repo in repos: 2505 | print(f"- {repo.get_name()} ({repo.get_url()})") 2506 | 2507 | def handle_add_repo(args): 2508 | try: 2509 | success, message = repoadd(args.add_repo, args.system) 2510 | print(f"{message}") 2511 | except GLib.Error as e: 2512 | print(f"{str(e)}") 2513 | 2514 | def handle_remove_repo(args): 2515 | repodelete(args.remove_repo, args.system) 2516 | print(f"\nRepository removed successfully: {args.remove_repo}") 2517 | 2518 | def handle_install(args, searcher): 2519 | if args.install.endswith('.flatpakref'): 2520 | try: 2521 | success, message = install_flatpakref(args.install, args.system) 2522 | result_message = f"{message}" 2523 | except GLib.Error as e: 2524 | result_message = f"Installation of {args.install} failed: {str(e)}" 2525 | print(result_message) 2526 | else: 2527 | packagelist = searcher.search_flatpak(args.install, args.repo) 2528 | result_message = "" 2529 | for package in packagelist: 2530 | try: 2531 | success, message = install_flatpak(package, args.repo, args.system) 2532 | result_message = f"{message}" 2533 | break 2534 | except GLib.Error as e: 2535 | result_message = f"Installation of {args.install} failed: {str(e)}" 2536 | pass 2537 | print(result_message) 2538 | 2539 | def handle_remove(args, searcher): 2540 | packagelist = searcher.search_flatpak(args.remove, args.repo) 2541 | result_message = "" 2542 | for package in packagelist: 2543 | try: 2544 | success, message = remove_flatpak(package, args.system) 2545 | result_message = f"{message}" 2546 | break 2547 | except GLib.Error as e: 2548 | result_message = f"Removal of {args.remove} failed: {str(e)}" 2549 | pass 2550 | print(result_message) 2551 | 2552 | def handle_update(args, searcher): 2553 | packagelist = searcher.search_flatpak(args.update) 2554 | result_message = "" 2555 | for package in packagelist: 2556 | try: 2557 | success, message = update_flatpak(package, args.system) 2558 | result_message = f"{message}" 2559 | break 2560 | except GLib.Error as e: 2561 | result_message = f"Update of {args.update} failed: {str(e)}" 2562 | pass 2563 | print(result_message) 2564 | 2565 | def handle_update_all(args, searcher): 2566 | packagelist = searcher.search_flatpak(args.update) 2567 | result_message = "" 2568 | for package in packagelist: 2569 | try: 2570 | success, message = update_all_flatpaks(package, args.system) 2571 | result_message = f"{message}" 2572 | break 2573 | except GLib.Error as e: 2574 | result_message = f"Unable to apply updates: {str(e)}" 2575 | pass 2576 | print(result_message) 2577 | 2578 | def handle_list_installed(args, searcher): 2579 | installed_apps = searcher.get_installed_apps(args.system) 2580 | print(f"\nInstalled Flatpak Applications ({len(installed_apps)}):") 2581 | for app_id, repo_name, repo_type in installed_apps: 2582 | print(f"{app_id} (Repository: {repo_name}, Installation: {repo_type})") 2583 | 2584 | def handle_check_updates(args, searcher): 2585 | updates = searcher.check_updates(args.system) 2586 | print(f"\nAvailable Updates ({len(updates)}):") 2587 | for repo_name, app_id, repo_type in updates: 2588 | print(f"{app_id} (Repository: {repo_name}, Installation: {repo_type})") 2589 | 2590 | def handle_list_all(args, searcher): 2591 | apps = searcher.get_all_apps(args.repo) 2592 | for app in apps: 2593 | details = app.get_details() 2594 | print(f"Name: {details['name']}") 2595 | print(f"Categories: {', '.join(details['categories'])}") 2596 | print("-" * 50) 2597 | 2598 | def handle_categories(args, searcher): 2599 | categories = searcher.get_categories_summary(args.repo) 2600 | for category, apps in categories.items(): 2601 | print(f"\n{category.upper()}:") 2602 | for app in apps: 2603 | print(f" - {app.name} ({app.id})") 2604 | 2605 | def handle_subcategories(args, searcher): 2606 | """Handle showing apps grouped by subcategory.""" 2607 | subcategories = searcher.get_subcategories_summary(args.repo) 2608 | for category, subcategory, apps in subcategories: 2609 | print(f"\n{category.upper()} > {subcategory.upper()}:") 2610 | for app in apps: 2611 | print(f" - {app.name} ({app.id})") 2612 | 2613 | def handle_add_file_perms(args, searcher): 2614 | try: 2615 | success, message = add_file_permissions(args.id, args.add_file_perms, args.perm_type, args.system) 2616 | print(f"{message}") 2617 | except GLib.Error as e: 2618 | print(f"{str(e)}") 2619 | 2620 | def handle_remove_file_perms(args, searcher): 2621 | try: 2622 | success, message = remove_file_permissions(args.id, args.remove_file_perms, args.perm_type, args.system) 2623 | print(f"{message}") 2624 | except GLib.Error as e: 2625 | print(f"{str(e)}") 2626 | 2627 | def handle_list_file_perms(args, searcher): 2628 | try: 2629 | success, message = list_file_perms(args.id, args.system) 2630 | print(f"{message}") 2631 | except GLib.Error as e: 2632 | print(f"{str(e)}") 2633 | 2634 | def handle_list_other_perm_toggles(args, searcher): 2635 | try: 2636 | success, message = list_other_perm_toggles(args.id, args.list_other_perm_toggles, args.system) 2637 | print(f"{message}") 2638 | except GLib.Error as e: 2639 | print(f"{str(e)}") 2640 | 2641 | def handle_toggle_other_perms(args, searcher): 2642 | if not args.perm_type: 2643 | print("Error: must specify --perm-type") 2644 | return 2645 | if not args.perm_option: 2646 | print("Error: must specify --perm-option") 2647 | return 2648 | get_status = args.toggle_other_perms.lower() in ['true', 'enable'] 2649 | try: 2650 | success, message = toggle_other_perms(args.id, args.perm_type, args.perm_option, get_status, args.system) 2651 | print(f"{message}") 2652 | except GLib.Error as e: 2653 | print(f"{str(e)}") 2654 | 2655 | def handle_list_other_perm_values(args, searcher): 2656 | try: 2657 | success, message = list_other_perm_values(args.id, args.list_other_perm_values, args.system) 2658 | print(f"{message}") 2659 | except GLib.Error as e: 2660 | print(f"{str(e)}") 2661 | 2662 | def handle_add_other_perm_values(args, searcher): 2663 | if not args.id: 2664 | print("Error: must specify --id") 2665 | return 2666 | 2667 | if not args.add_other_perm_values: 2668 | print("Error: must specify which perm value") 2669 | return 2670 | 2671 | if not args.perm_value: 2672 | print("Error: must specify --perm-value") 2673 | return 2674 | try: 2675 | success, message = add_permission_value(args.id, args.add_other_perm_values, args.perm_value, args.system) 2676 | print(message) 2677 | except GLib.Error as e: 2678 | print(f"{str(e)}") 2679 | 2680 | def handle_remove_other_perm_values(args, searcher): 2681 | if not args.id: 2682 | print("Error: must specify --id") 2683 | return 2684 | 2685 | if not args.remove_other_perm_values: 2686 | print("Error: must specify which perm value") 2687 | return 2688 | 2689 | if not args.perm_value: 2690 | print("Error: must specify --perm-value") 2691 | return 2692 | 2693 | try: 2694 | success, message = remove_permission_value(args.id, args.remove_other_perm_values, args.perm_value, args.system) 2695 | print(message) 2696 | except GLib.Error as e: 2697 | print(f"{str(e)}") 2698 | 2699 | def handle_global_add_file_perms(args, searcher): 2700 | try: 2701 | success, message = global_add_file_permissions(args.global_add_file_perms, True, args.system) 2702 | print(f"{message}") 2703 | except GLib.Error as e: 2704 | print(f"{str(e)}") 2705 | 2706 | def handle_global_remove_file_perms(args, searcher): 2707 | try: 2708 | success, message = global_remove_file_permissions(args.global_remove_file_perms, True, args.system) 2709 | print(f"{message}") 2710 | except GLib.Error as e: 2711 | print(f"{str(e)}") 2712 | 2713 | def handle_global_list_file_perms(args, searcher): 2714 | try: 2715 | success, message = global_list_file_perms(True, args.system) 2716 | print(f"{message}") 2717 | except GLib.Error as e: 2718 | print(f"{str(e)}") 2719 | 2720 | def handle_global_list_other_perm_toggles(args, searcher): 2721 | try: 2722 | success, message = global_list_other_perm_toggles(args.global_list_other_perm_toggles, True, args.system) 2723 | print(f"{message}") 2724 | except GLib.Error as e: 2725 | print(f"{str(e)}") 2726 | 2727 | def handle_global_toggle_other_perms(args, searcher): 2728 | if not args.perm_type: 2729 | print("Error: must specify --perm-type") 2730 | return 2731 | if not args.perm_option: 2732 | print("Error: must specify --perm-option") 2733 | return 2734 | get_status = args.global_toggle_other_perms.lower() in ['true', 'enable'] 2735 | try: 2736 | success, message = global_toggle_other_perms(args.perm_type, args.perm_option, get_status, True, args.system) 2737 | print(f"{message}") 2738 | except GLib.Error as e: 2739 | print(f"{str(e)}") 2740 | 2741 | def handle_global_list_other_perm_values(args, searcher): 2742 | try: 2743 | success, message = global_list_other_perm_values(args.global_list_other_perm_values, True, args.system) 2744 | print(f"{message}") 2745 | except GLib.Error as e: 2746 | print(f"{str(e)}") 2747 | 2748 | def handle_global_add_other_perm_values(args, searcher): 2749 | if not args.global_add_other_perm_values: 2750 | print("Error: must specify which perm value") 2751 | return 2752 | if not args.perm_value: 2753 | print("Error: must specify --perm-value") 2754 | return 2755 | try: 2756 | success, message = global_add_permission_value(args.global_add_other_perm_values, args.perm_value, True, args.system) 2757 | print(message) 2758 | except GLib.Error as e: 2759 | print(f"{str(e)}") 2760 | 2761 | def handle_global_remove_other_perm_values(args, searcher): 2762 | if not args.global_remove_other_perm_values: 2763 | print("Error: must specify which perm value") 2764 | return 2765 | 2766 | if not args.perm_value: 2767 | print("Error: must specify --perm-value") 2768 | return 2769 | try: 2770 | success, message = global_remove_permission_value(args.global_remove_other_perm_values, args.perm_value, True, args.system) 2771 | print(message) 2772 | except GLib.Error as e: 2773 | print(f"{str(e)}") 2774 | 2775 | def handle_set_app_portal_permissions(args, searcher): 2776 | if not args.id: 2777 | print("Error: must specify --id") 2778 | return 2779 | if not args.set_app_portal_permissions: 2780 | print("Error: must specify which portal") 2781 | return 2782 | if not args.portal_perm_value: 2783 | print("Error: must specify --portal-perm-value") 2784 | return 2785 | if args.portal_perm_value.lower() in ['true', 'enable', 'yes', '1']: 2786 | status_str = "yes" 2787 | else: 2788 | status_str = "no" 2789 | try: 2790 | success, message = portal_set_app_permissions(args.set_app_portal_permissions, args.id, status_str) 2791 | print(f"{message}") 2792 | except dbus.exceptions.DBusException as e: 2793 | print(f"{str(e)}") 2794 | 2795 | def handle_get_app_portal_permissions(args, searcher): 2796 | if not args.id: 2797 | print("Error: must specify --id") 2798 | return 2799 | try: 2800 | success, message = portal_get_app_permissions(args.id) 2801 | print(f"{message}") 2802 | except dbus.exceptions.DBusException as e: 2803 | print(f"{str(e)}") 2804 | 2805 | 2806 | def handle_search(args, searcher): 2807 | if args.repo: 2808 | search_results = searcher.search_flatpak(args.id, args.repo) 2809 | else: 2810 | search_results = searcher.search_flatpak(args.id) 2811 | 2812 | if search_results: 2813 | for package in search_results: 2814 | details = package.get_details() 2815 | print(f"Name: {details['name']}") 2816 | print(f"ID: {details['id']}") 2817 | print(f"Kind: {details['kind']}") 2818 | print(f"Summary: {details['summary']}") 2819 | print(f"Description: {details['description']}") 2820 | print(f"Version: {details['version']}") 2821 | print(f"Icon URL: {details['icon_url']}") 2822 | print(f"Icon PATH 128x128: {details['icon_path_128']}") 2823 | print(f"Icon PATH 64x64: {details['icon_path_64']}") 2824 | print(f"Icon FILE: {details['icon_filename']}") 2825 | print(f"Developer: {details['developer']}") 2826 | print(f"Categories: {details['categories']}") 2827 | urls = details['urls'] 2828 | print(f"Donation URL: {urls['donation']}") 2829 | print(f"Homepage URL: {urls['homepage']}") 2830 | print(f"Bug Tracker URL: {urls['bugtracker']}") 2831 | print(f"Bundle ID: {details['bundle_id']}") 2832 | print(f"Match Type: {details['match_type']}") 2833 | print(f"Repo: {details['repo']}") 2834 | print("Screenshots:") 2835 | for i, screenshot in enumerate(details['screenshots'], 1): 2836 | print(f"\nScreenshot #{i}:") 2837 | image = screenshot_details(screenshot) 2838 | if image: 2839 | # Get image properties using the correct methods 2840 | print("\nImage Properties:") 2841 | print(f"URL: {image.get_url()}") 2842 | print(f"Width: {image.get_width()}") 2843 | print(f"Height: {image.get_height()}") 2844 | print(f"Scale: {image.get_scale()}") 2845 | print(f"Locale: {image.get_locale()}") 2846 | 2847 | print("-" * 50) 2848 | 2849 | if __name__ == "__main__": 2850 | main() 2851 | --------------------------------------------------------------------------------