This commit is contained in:
Vinícius Moreira
2019-10-25 21:11:35 -03:00
committed by GitHub
71 changed files with 1009 additions and 378 deletions

View File

@@ -4,6 +4,49 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [0.7.1] 2019-10-25
### Features
- Snap:
- if the **stable** channel is not available while an application is being installed, a popup is displayed allowing the user to choose a different one ( e.g: dbeaver-ce )
### Improvements
- 3 password attempts for root authentication
- not changing the table applied filters after a uninstall
- cleaning the progress bar substatus after each upgrade
- sorted categories
- AppImage:
- showing an error popup when **AppImageLauncher** messes up with an application installation
- Flatpak:
- Runtimes now are categorized as "runtime"
- Formatting the API categories to the same format provided by the other packaging technologies
- AUR:
- showing a "user-friendly" popup when there are integrity issues with the source-files of a building package
- not waiting for the categories file to be retrieved from the cloud during application boot ( reduces boot time )
- caching cloud categories to the disk so they can be used in scenarios when it is not possible to retrieve them ( e.g: internet is off )
- mapping known search key words to the specific package name ( e.g:"google chrome" will become "google-chrome" )
- Snap:
- not waiting for the categories file to be retrieved from the cloud during application boot ( reduces boot time )
- caching cloud categories to the disk so they can be used in scenarios when it is not possible to retrieve them ( e.g: internet is off )
- showing a warning popup when the Snap API is out
- Snaps not treated as applications with be categorized as "runtime" at least
- minor thread improvements
### UI
- Screenshots panel:
- "downloading" label replaced by a progress bar
### Fixes
- application not initializing when there is no internet connection
- not loading application icons after some filters are applied to the table results
- not reloading the available categories after asynchronous data is fetched
- not keeping the update toggle-button state after a filter is applied
- AUR:
- update-checking for some scenarios
- not respecting **ignorepkg** settings in **pacman.conf**
- not able to handle **missing dependencies with symbols** ( e.g: libpng++ )
- not able to work with **.xpm** icons
- not mapping categories to the search results
## [0.7.0] 2019-10-18
### Features
- AppImage support ( see below )
@@ -34,7 +77,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### AppImage support
- Search, install, uninstall, downgrade, launch and retrieve the applications history
- Supported sources: [AppImageHub](https://appimage.github.io) ( **applications with no releases published to GitHub are currently not available** )
- Adds desktop entries ( menu shortcuts ) for the installed applications ( **~/.local/share/applications **)
- Adds desktop entries ( menu shortcuts ) for the installed applications ( **~/.local/share/applications**)
## [0.6.4] 2019-10-13
### Fixes

View File

@@ -2,3 +2,4 @@ include LICENSE CHANGELOG.md README.md
include requirements.txt
recursive-include bauh/view/resources *
recursive-include bauh/gems/*/resources *
recursive-include tests *

2
aur/bauh-tray Normal file
View File

@@ -0,0 +1,2 @@
#!/bin/bash
/usr/bin/bauh --tray=1

9
aur/bauh.desktop Normal file
View File

@@ -0,0 +1,9 @@
[Desktop Entry]
Type=Application
Name=bauh
Categories=System;
Comment=Manage your Flatpak / Snap / AppImage / AUR applications
Comment[pt]=Gerencie seus aplicativos Flatpak / Snap / AppImage / AUR
Comment[es]=Administre sus aplicaciones Flatpak / Snap / AppImage / AUR
Exec=/usr/bin/bauh
Icon=bauh

11
aur/bauh_tray.desktop Normal file
View File

@@ -0,0 +1,11 @@
[Desktop Entry]
Type=Application
Name=bauh ( tray )
Name[pt]=bauh ( bandeja )
Name[es]=bauh ( bandeja )
Categories=System;
Comment=Manage your Flatpak / Snap / AppImage / AUR applications
Comment[pt]=Gerencie seus aplicativos Flatpak / Snap / AppImage / AUR
Comment[es]=Administre sus aplicaciones Flatpak / Snap / AppImage / AUR
Exec=/usr/bin/bauh --tray=1
Icon=bauh

14
aur/copy_icons.py Normal file
View File

@@ -0,0 +1,14 @@
import glob
import shutil
import sys
from pathlib import Path
output_base = sys.argv[2]
for f in glob.glob(sys.argv[1] + '/*.png'):
res = f.split('/')[-1].split('.')[0]
dest_dir = output_base + '/' + res + '/apps'
Path(dest_dir).mkdir(parents=True, exist_ok=True)
shutil.copy(f, dest_dir + '/bauh.png')

BIN
aur/icons/128x128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

BIN
aur/icons/12x12.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 889 B

BIN
aur/icons/16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1011 B

BIN
aur/icons/24x24.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
aur/icons/256x256.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

BIN
aur/icons/32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
aur/icons/48x48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
aur/icons/512x512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
aur/icons/64x64.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
aur/icons/8x8.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 726 B

BIN
aur/icons/96x96.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@@ -1,24 +0,0 @@
# Generates a .desktop file based on the current python version. Used for AUR installation
import os
import sys
desktop_file = """
[Desktop Entry]
Type=Application
Name=bauh
Categories=System;
Comment=Manage your Flatpak / Snap / AppImage / AUR applications
Comment[pt]=Gerencie seus aplicativos Flatpak / Snap / AppImage / AUR
Comment[es]=Administre sus aplicaciones Flatpak / Snap / AppImage / AUR
Exec = {path}
Icon = {lib_path}/python{version}/site-packages/bauh/view/resources/img/logo.svg
"""
py_version = "{}.{}".format(sys.version_info.major, sys.version_info.minor)
app_cmd = os.getenv('BAUH_PATH', '/usr/bin/bauh')
with open('bauh.desktop', 'w+') as f:
f.write(desktop_file.format(lib_path=os.getenv('BAUH_LIB_PATH', '/usr/lib'),
version=py_version,
path=app_cmd))

View File

@@ -1,31 +0,0 @@
# Generates a .desktop file based on the current python version. Used for AUR installation
import os
import sys
desktop_file = """
[Desktop Entry]
Type = Application
Name = bauh ( tray )
Name[pt] = bauh ( bandeja )
Name[es] = bauh ( bandeja )
Categories = System;
Comment=Manage your Flatpak / Snap / AppImage / AUR applications
Comment[pt]=Gerencie seus aplicativos Flatpak / Snap / AppImage / AUR
Comment[es]=Administre sus aplicaciones Flatpak / Snap / AppImage / AUR
Exec = {path}
Icon = {lib_path}/python{version}/site-packages/bauh/view/resources/img/logo.svg
"""
py_version = "{}.{}".format(sys.version_info.major, sys.version_info.minor)
app_cmd = os.getenv('BAUH_PATH', '/usr/bin/bauh') + ' --tray=1'
with open('bauh_tray.desktop', 'w+') as f:
f.write(desktop_file.format(lib_path=os.getenv('BAUH_LIB_PATH', '/usr/lib'),
version=py_version,
path=app_cmd))
with open('bauh-tray', 'w') as f:
f.write(app_cmd)

View File

@@ -1,4 +1,4 @@
__version__ = '0.7.0'
__version__ = '0.7.1'
__app_name__ = 'bauh'
import os

View File

@@ -213,8 +213,9 @@ class SoftwareManager(ABC):
pass
@abstractmethod
def list_warnings(self) -> List[str]:
def list_warnings(self, internet_available: bool) -> List[str]:
"""
:param internet_available
:return: a list of warnings to be shown to the user
"""
pass

View File

@@ -59,9 +59,6 @@ class SingleSelectComponent(InputViewComponent):
def __init__(self, type_: SelectViewType, label: str, options: List[InputOption], default_option: InputOption = None, max_per_line: int = 1, id_: str = None):
super(SingleSelectComponent, self).__init__(id_=id_)
if options is None or len(options) < 2:
raise Exception("'options' must be a list with at least 2 elements")
self.type = type_
self.label = label
self.options = options

108
bauh/commons/category.py Normal file
View File

@@ -0,0 +1,108 @@
import logging
import os
import traceback
from pathlib import Path
from threading import Thread
from typing import Dict, List
import requests
from bauh.api.abstract.controller import SoftwareManager
from bauh.api.http import HttpClient
class CategoriesDownloader(Thread):
def __init__(self, id_: str, http_client: HttpClient, logger: logging.Logger, manager: SoftwareManager,
disk_cache: bool, url_categories_file: str, disk_cache_dir: str, categories_path: str):
super(CategoriesDownloader, self).__init__(daemon=True)
self.id_ = id_
self.http_client = http_client
self.logger = logger
self.manager = manager
self.disk_cache = disk_cache
self.url_categories_file = url_categories_file
self.disk_cache_dir = disk_cache_dir
self.categories_path = categories_path
def _msg(self, msg: str):
return '{}({}): {}'.format(self.__class__.__name__, self.id_, msg)
def _read_categories_from_disk(self) -> Dict[str, List[str]]:
if self.disk_cache and os.path.exists(self.categories_path):
self.logger.info(self._msg("Reading cached categories from the disk"))
with open(self.categories_path) as f:
categories = f.read()
return self._map_categories(categories)
return {}
def _map_categories(self, categories: str) -> Dict[str, List[str]]:
categories_map = {}
for l in categories.split('\n'):
if l:
data = l.split('=')
categories_map[data[0]] = [c.strip() for c in data[1].split(',') if c]
return categories_map
def _cache_categories_to_disk(self, categories: str):
self.logger.info(self._msg('Caching categories to the disk'))
try:
Path(self.disk_cache_dir).mkdir(parents=True, exist_ok=True)
with open(self.categories_path, 'w+') as f:
f.write(categories)
self.logger.info(self._msg("Categories cached to the disk as '{}'".format(self.categories_path)))
except:
self.logger.error(self._msg("Could not cache categories to the disk as '{}'".format(self.categories_path)))
traceback.print_exc()
def download_categories(self) -> Dict[str, List[str]]:
self.logger.info(self._msg('Downloading category definitions from {}'.format(self.url_categories_file)))
try:
res = self.http_client.get(self.url_categories_file)
if res:
try:
categories = self._map_categories(res.text)
self.logger.info(self._msg('Loaded categories for {} applications'.format(len(categories))))
if self.disk_cache and categories:
Thread(target=self._cache_categories_to_disk, args=(res.text,), daemon=True).start()
return categories
except:
self.logger.error(self._msg("Could not parse categories definitions"))
traceback.print_exc()
else:
self.logger.info(self._msg('Could not download {}'.format(self.url_categories_file)))
except requests.exceptions.ConnectionError:
self.logger.warning(self._msg('The internet connection seems to be off.'))
return {}
def _set_categories(self, categories: dict):
if categories:
self.logger.info(self._msg("Settings {} categories to {}".format(len(categories), self.manager.__class__.__name__)))
self.manager.categories = categories
def _download_and_set(self):
self._set_categories(self.download_categories())
def run(self):
cached = self._read_categories_from_disk()
if cached:
self._set_categories(cached)
Thread(target=self._download_and_set, daemon=True).start()
else:
self._download_and_set()
self.logger.info(self._msg('Finished'))

View File

@@ -100,7 +100,7 @@ class ProcessHandler:
if self.watcher:
self.watcher.print(msg)
def handle(self, process: SystemProcess) -> bool:
def handle(self, process: SystemProcess, error_output: StringIO = None) -> bool:
self._notify_watcher(' '.join(process.subproc.args) + '\n')
already_succeeded = False
@@ -126,6 +126,9 @@ class ProcessHandler:
if line:
self._notify_watcher(line)
if error_output is not None:
error_output.write(line)
if process.check_error_output:
if process.wrong_error_phrase and process.wrong_error_phrase in line:
continue

View File

@@ -17,7 +17,7 @@ from bauh.api.abstract.model import SoftwarePackage, PackageHistory, PackageUpda
from bauh.api.abstract.view import MessageType
from bauh.api.constants import HOME_PATH
from bauh.commons.html import bold
from bauh.commons.system import SystemProcess, new_subprocess, ProcessHandler, run_cmd
from bauh.commons.system import SystemProcess, new_subprocess, ProcessHandler, run_cmd, SimpleProcess
from bauh.gems.appimage import query, INSTALLATION_PATH, suggestions, db
from bauh.gems.appimage.model import AppImage
from bauh.gems.appimage.worker import DatabaseUpdater
@@ -298,12 +298,20 @@ class AppImageManager(SoftwareManager):
watcher.change_substatus(self.i18n['appimage.install.extract'].format(bold(file_name)))
try:
handler.handle(SystemProcess(new_subprocess([file_path, '--appimage-extract'], cwd=out_dir)))
res, output = handler.handle_simple(SimpleProcess([file_path, '--appimage-extract'], cwd=out_dir))
if 'Error: Failed to register AppImage in AppImageLauncherFS' in output:
watcher.show_message(title=self.i18n['error'],
body=self.i18n['appimage.install.appimagelauncher.error'].format(appimgl=bold('AppImageLauncher'), app=bold(pkg.name)),
type_=MessageType.ERROR)
handler.handle(SystemProcess(new_subprocess(['rm', '-rf', out_dir])))
return False
except:
watcher.show_message(title=self.i18n['error'],
body=traceback.format_exc(),
type_=MessageType.ERROR)
traceback.print_exc()
handler.handle(SystemProcess(new_subprocess(['rm', '-rf', out_dir])))
return False
watcher.change_substatus(self.i18n['appimage.install.desktop_entry'])
@@ -388,7 +396,7 @@ class AppImageManager(SoftwareManager):
return updates
def list_warnings(self) -> List[str]:
def list_warnings(self, internet_available: bool) -> List[str]:
pass
def list_suggestions(self, limit: int) -> List[PackageSuggestion]:

View File

@@ -15,4 +15,5 @@ appimage.downgrade.unknown_version.body=It was not possible to identify {} curre
appimage.downgrade.first_version={} is in its first published version
appimage.error.uninstall_current_version=It was not possible to uninstall the current version of {}
appimage.downgrade.install_version=It was not possible to install the version {} ({})
appimage.install.download.error=It was not possible to download the file {}. The file server can be down.
appimage.install.download.error=It was not possible to download the file {}. The file server can be down.
appimage.install.appimagelauncher.error={appimgl} is not allowing {app} to be installed. Uninstall {appimgl}, reboot your system and try to install {app} again.

View File

@@ -15,4 +15,5 @@ appimage.downgrade.unknown_version.body=No fue posible identificar la versión a
appimage.downgrade.first_version={} está en su primera versión publicada
appimage.downgrade.uninstall_current_version=No fue posible desinstalar la versión actual de {}
appimage.downgrade.install_version=No fue posible instalar la versión {} ({})
appimage.install.download.error=No fue posible descargar el archivo {}. El servidor del archivo puede estar inactivo.
appimage.install.download.error=No fue posible descargar el archivo {}. El servidor del archivo puede estar inactivo.
appimage.install.appimagelauncher.error = {appimgl} no permite la instalación de {app}. Desinstale {appimgl}, reinicie su sistema e intente instalar {app} nuevamente.

View File

@@ -15,4 +15,5 @@ appimage.downgrade.unknown_version.body=Não foi possível identificar a versão
appimage.downgrade.first_version={} se encontra em sua primeira versão publicada
appimage.downgrade.uninstall_current_version=Não foi possível desinstalar a versão atual de {}
appimage.downgrade.install_version=Não foi possivel instalar a versão {} ({})
appimage.install.download.error=Não foi possível baixar o arquivo {}. O servidor do arquivo pode estar fora do ar.
appimage.install.download.error=Não foi possível baixar o arquivo {}. O servidor do arquivo pode estar fora do ar.
appimage.install.appimagelauncher.error={appimgl} não está permitindo que o {app} seja instalado. Desinstale o {appimgl}, reinicie o sistema e tente instalar o {app} novamente.

View File

@@ -8,6 +8,8 @@ from multiprocessing import Process
from pathlib import Path
from threading import Thread
import requests
from bauh.api.http import HttpClient
from bauh.gems.appimage import LOCAL_PATH, db
@@ -68,7 +70,11 @@ class DatabaseUpdater(Thread if bool(int(os.getenv('BAUH_DEBUG', 0))) else Proce
def run(self):
if self.enabled:
while True:
self._download_databases()
try:
self._download_databases()
except requests.exceptions.ConnectionError:
self.logger.warning('The internet connection seems to be off.')
self.logger.info('Sleeping')
time.sleep(self.sleep)
else:

View File

@@ -1,4 +1,10 @@
import os
from bauh.api.constants import CACHE_PATH
ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
BUILD_DIR = '/tmp/bauh/aur'
ARCH_CACHE_PATH = CACHE_PATH + '/arch'
CATEGORIES_CACHE_DIR = ARCH_CACHE_PATH + '/categories'
CATEGORIES_FILE_PATH = CATEGORIES_CACHE_DIR + '/aur.txt'
URL_CATEGORIES_FILE = 'https://raw.githubusercontent.com/vinifmor/bauh-files/master/aur/categories.txt'

View File

@@ -1,9 +1,8 @@
import re
from typing import Set, List
import requests
from bauh.api.http import HttpClient
import urllib.parse
URL_INFO = 'https://aur.archlinux.org/rpc/?v=5&type=info&'
URL_SRC_INFO = 'https://aur.archlinux.org/cgit/aur.git/plain/.SRCINFO?h='
@@ -32,7 +31,7 @@ class AURClient:
return res['results'] if res and res.get('results') else []
def get_src_info(self, name: str) -> dict:
res = self.http_client.get(URL_SRC_INFO + name)
res = self.http_client.get(URL_SRC_INFO + urllib.parse.quote(name))
if res and res.text:
info = {}
@@ -48,4 +47,4 @@ class AURClient:
return info
def _map_names_as_queries(self, names) -> str:
return '&'.join(['arg[{}]={}'.format(i, n) for i, n in enumerate(names)])
return '&'.join(['arg[{}]={}'.format(i, urllib.parse.quote(n)) for i, n in enumerate(names)])

View File

@@ -15,14 +15,16 @@ from bauh.api.abstract.disk import DiskCacheLoader
from bauh.api.abstract.handler import ProcessWatcher
from bauh.api.abstract.model import PackageUpdate, PackageHistory, SoftwarePackage, PackageSuggestion, PackageStatus
from bauh.api.abstract.view import MessageType
from bauh.commons.category import CategoriesDownloader
from bauh.commons.html import bold
from bauh.commons.system import SystemProcess, ProcessHandler, new_subprocess, run_cmd, new_root_subprocess, \
SimpleProcess
from bauh.gems.arch import BUILD_DIR, aur, pacman, makepkg, pkgbuild, message, confirmation, disk, git, suggestions, gpg
from bauh.gems.arch import BUILD_DIR, aur, pacman, makepkg, pkgbuild, message, confirmation, disk, git, suggestions, \
gpg, URL_CATEGORIES_FILE, CATEGORIES_CACHE_DIR, CATEGORIES_FILE_PATH
from bauh.gems.arch.aur import AURClient
from bauh.gems.arch.mapper import ArchDataMapper
from bauh.gems.arch.model import ArchPackage
from bauh.gems.arch.worker import AURIndexUpdater, ArchDiskCacheUpdater, ArchCompilationOptimizer, CategoriesDownloader
from bauh.gems.arch.worker import AURIndexUpdater, ArchDiskCacheUpdater, ArchCompilationOptimizer
URL_GIT = 'https://aur.archlinux.org/{}.git'
URL_PKG_DOWNLOAD = 'https://aur.archlinux.org/cgit/aur.git/snapshot/{}.tar.gz'
@@ -33,6 +35,12 @@ RE_SPLIT_VERSION = re.compile(r'(=|>|<)')
SOURCE_FIELDS = ('source', 'source_x86_64')
RE_PRE_DOWNLOADABLE_FILES = re.compile(r'(https?|ftp)://.+\.\w+[^gpg|git]$')
SEARCH_OPTIMIZED_MAP = {
'google chrome': 'google-chrome',
'chrome google': 'google-chrome',
'googlechrome': 'google-chrome'
}
class ArchManager(SoftwareManager):
@@ -51,11 +59,12 @@ class ArchManager(SoftwareManager):
self.logger = context.logger
self.enabled = True
self.arch_distro = context.distro == 'arch'
self.categories_downloader = CategoriesDownloader(context.http_client, context.logger)
self.categories_map = {}
self.categories_mapper = CategoriesDownloader('AUR', context.http_client, context.logger, self, self.context.disk_cache,
URL_CATEGORIES_FILE, CATEGORIES_CACHE_DIR, CATEGORIES_FILE_PATH)
self.categories = {}
def _upgrade_search_result(self, apidata: dict, installed_pkgs: dict, downgrade_enabled: bool, res: SearchResult, disk_loader: DiskCacheLoader):
app = self.mapper.map_api_data(apidata, installed_pkgs['not_signed'])
app = self.mapper.map_api_data(apidata, installed_pkgs['not_signed'], self.categories)
app.downgrade_enabled = downgrade_enabled
if app.installed:
@@ -66,7 +75,7 @@ class ArchManager(SoftwareManager):
else:
res.new.append(app)
Thread(target=self.mapper.fill_package_build, args=(app,)).start()
Thread(target=self.mapper.fill_package_build, args=(app,), daemon=True).start()
def search(self, words: str, disk_loader: DiskCacheLoader, limit: int = -1) -> SearchResult:
self.comp_optimizer.join()
@@ -75,10 +84,12 @@ class ArchManager(SoftwareManager):
res = SearchResult([], [], 0)
installed = {}
read_installed = Thread(target=lambda: installed.update(pacman.list_and_map_installed()))
read_installed = Thread(target=lambda: installed.update(pacman.list_and_map_installed()), daemon=True)
read_installed.start()
api_res = self.aur_client.search(words)
mapped_words = SEARCH_OPTIMIZED_MAP.get(words)
api_res = self.aur_client.search(mapped_words if mapped_words else words)
if api_res and api_res.get('results'):
read_installed.join()
@@ -117,9 +128,8 @@ class ArchManager(SoftwareManager):
if pkgsinfo:
for pkgdata in pkgsinfo:
pkg = self.mapper.map_api_data(pkgdata, not_signed)
pkg = self.mapper.map_api_data(pkgdata, not_signed, self.categories)
pkg.downgrade_enabled = downgrade_enabled
pkg.categories = self.categories_map.get(pkg.name)
if disk_loader:
disk_loader.fill(pkg)
@@ -137,7 +147,7 @@ class ArchManager(SoftwareManager):
latest_version=data.get('version'), description=data.get('description'),
installed=True, mirror='aur')
pkg.categories = self.categories_map.get(pkg.name)
pkg.categories = self.categories.get(pkg.name)
pkg.downgrade_enabled = downgrade_enabled
if disk_loader:
@@ -161,6 +171,7 @@ class ArchManager(SoftwareManager):
apps = []
if installed and installed['not_signed']:
self.dcache_updater.join()
self.categories_mapper.join()
self._fill_aur_pkgs(installed['not_signed'], apps, disk_loader, internet_available)
@@ -490,6 +501,12 @@ class ArchManager(SoftwareManager):
handler.watcher.print(self.i18n['action.cancelled'])
return False
if check_res.get('validity_check'):
handler.watcher.show_message(title=self.i18n['arch.aur.install.validity_check.title'],
body=self.i18n['arch.aur.install.validity_check.body'].format(bold(pkgname)),
type_=MessageType.ERROR)
return False
return True
def _install_optdeps(self, pkgname: str, root_password: str, handler: ProcessHandler, pkgdir: str, change_progress: bool = True) -> bool:
@@ -559,7 +576,7 @@ class ArchManager(SoftwareManager):
if installed and self.context.disk_cache:
handler.watcher.change_substatus(self.i18n['status.caching_data'].format(bold(pkgname)))
if self.context.disk_cache:
disk.save_several({pkgname}, mirror=mirror, maintainer=maintainer, overwrite=True)
disk.save_several({pkgname}, mirror=mirror, maintainer=maintainer, overwrite=True, categories=self.categories)
self._update_progress(handler.watcher, 100, change_progress)
@@ -678,7 +695,7 @@ class ArchManager(SoftwareManager):
return False
def cache_to_disk(self, pkg: ArchPackage, icon_bytes: bytes, only_icon: bool):
pass
pass
def requires_root(self, action: str, pkg: ArchPackage):
return action != 'search'
@@ -687,16 +704,13 @@ class ArchManager(SoftwareManager):
self.dcache_updater.start()
self.comp_optimizer.start()
self.aur_index_updater.start()
categories = self.categories_downloader.get_categories()
if categories:
self.categories_map = categories
self.categories_mapper.start()
def list_updates(self, internet_available: bool) -> List[PackageUpdate]:
installed = self.read_installed(disk_loader=None, internet_available=internet_available).installed
return [PackageUpdate(p.id, p.latest_version, 'aur') for p in installed if p.update]
def list_warnings(self) -> List[str]:
def list_warnings(self, internet_available: bool) -> List[str]:
warnings = []
if self.arch_distro:
@@ -725,9 +739,10 @@ class ArchManager(SoftwareManager):
api_res = self.aur_client.get_info(sug_names)
if api_res:
self.categories_mapper.join()
for pkg in api_res:
if pkg.get('Name') in sug_names:
res.append(PackageSuggestion(self.mapper.map_api_data(pkg, {}), suggestions.ALL.get(pkg['Name'])))
res.append(PackageSuggestion(self.mapper.map_api_data(pkg, {}, self.categories), suggestions.ALL.get(pkg['Name'])))
return res

View File

@@ -22,7 +22,7 @@ def write(app: ArchPackage):
def fill_icon_path(app: ArchPackage, icon_paths: List[str], only_exact_match: bool):
ends_with = re.compile(r'.+/{}\.(png|svg)$'.format(app.icon_path if app.icon_path else app.name), re.IGNORECASE)
ends_with = re.compile(r'.+/{}\.(png|svg|xpm)$'.format(app.icon_path if app.icon_path else app.name), re.IGNORECASE)
for path in icon_paths:
if ends_with.match(path):
@@ -47,7 +47,7 @@ def set_icon_path(app: ArchPackage, icon_name: str = None):
break
def save_several(pkgnames: Set[str], mirror: str, overwrite: bool = True, maintainer: str = None) -> int:
def save_several(pkgnames: Set[str], mirror: str, overwrite: bool = True, maintainer: str = None, categories: dict = None) -> int:
to_cache = {n for n in pkgnames if overwrite or not os.path.exists(ArchPackage.disk_cache_path(n, mirror))}
desktop_files = pacman.list_desktop_entries(to_cache)
@@ -138,8 +138,10 @@ def save_several(pkgnames: Set[str], mirror: str, overwrite: bool = True, mainta
if to_write:
for p in to_write:
if categories:
p.categories = categories.get(p.name)
p.maintainer = maintainer
write(p)
return len(to_write)
return 0

View File

@@ -18,4 +18,7 @@ def check(pkgdir: str, handler: ProcessHandler) -> dict:
if gpg_keys:
res['gpg_key'] = gpg_keys[0]
if 'One or more files did not pass the validity check' in output:
res['validity_check'] = True
return res

View File

@@ -7,9 +7,9 @@ from bauh.gems.arch.model import ArchPackage
URL_PKG_DOWNLOAD = 'https://aur.archlinux.org/{}'
RE_LETTERS = re.compile(r'\.([a-zA-Z]+)-\d+$')
RE_ANY_LETTER = re.compile(r'[a-zA-Z]')
RE_VERSION_DELS = re.compile(r'[.:#@\-_]')
RE_VERSION_SPLIT = re.compile(r'[a-zA-Z]+|\d+|[\.\-_@#]+')
BAUH_PACKAGES = {'bauh', 'bauh-staging'}
RE_SFX = ('r', 're', 'release')
GA_SFX = ('ga', 'ge')
RC_SFX = ('rc',)
@@ -49,46 +49,47 @@ class ArchDataMapper:
pkg.url_download = URL_PKG_DOWNLOAD.format(package['URLPath']) if package.get('URLPath') else None
pkg.first_submitted = datetime.fromtimestamp(package['FirstSubmitted']) if package.get('FirstSubmitted') else None
pkg.last_modified = datetime.fromtimestamp(package['LastModified']) if package.get('LastModified') else None
pkg.update = self.check_update(pkg.version, pkg.latest_version)
pkg.update = self.check_update(pkg.version, pkg.latest_version, check_suffix=pkg.name in BAUH_PACKAGES)
@staticmethod
def check_update(version: str, latest_version: str) -> bool:
def check_update(version: str, latest_version: str, check_suffix: bool = False) -> bool:
if version and latest_version:
current_sfx = RE_LETTERS.findall(version)
latest_sf = RE_LETTERS.findall(latest_version)
if latest_sf and current_sfx:
current_sfx = current_sfx[0]
latest_sf = latest_sf[0]
if check_suffix:
current_sfx = RE_LETTERS.findall(version)
latest_sf = RE_LETTERS.findall(latest_version)
current_sfx_data = V_SUFFIX_MAP.get(current_sfx.lower())
latest_sfx_data = V_SUFFIX_MAP.get(latest_sf.lower())
if latest_sf and current_sfx:
current_sfx = current_sfx[0]
latest_sf = latest_sf[0]
if current_sfx_data and latest_sfx_data:
nversion = version.split(current_sfx)[0]
nlatest = latest_version.split(latest_sf)[0]
current_sfx_data = V_SUFFIX_MAP.get(current_sfx.lower())
latest_sfx_data = V_SUFFIX_MAP.get(latest_sf.lower())
if nversion == nlatest:
if current_sfx_data['c'] != latest_sfx_data['c']:
return latest_sfx_data['p'] < current_sfx_data['p']
else:
return ''.join(latest_version.split(latest_sf)) > ''.join(version.split(current_sfx))
if current_sfx_data and latest_sfx_data:
nversion = version.split(current_sfx)[0]
nlatest = latest_version.split(latest_sf)[0]
return nlatest > nversion
if nversion == nlatest:
if current_sfx_data['c'] != latest_sfx_data['c']:
return latest_sfx_data['p'] < current_sfx_data['p']
else:
return ''.join(latest_version.split(latest_sf)) > ''.join(version.split(current_sfx))
latest_split = RE_VERSION_DELS.split(latest_version)
version_split = RE_VERSION_DELS.split(version)
return nlatest > nversion
latest_split = RE_VERSION_SPLIT.findall(latest_version)
current_split = RE_VERSION_SPLIT.findall(version)
for idx in range(len(latest_split)):
if idx < len(version_split):
if idx < len(current_split):
latest_part = latest_split[idx]
version_part = version_split[idx]
current_part = current_split[idx]
if latest_part != version_part:
if RE_ANY_LETTER.findall(latest_part) or RE_ANY_LETTER.findall(version_part):
return latest_part > version_part
else:
dif = int(latest_part) - int(version_part)
if latest_part != current_part:
try:
dif = int(latest_part) - int(current_part)
if dif > 0:
return True
@@ -96,6 +97,14 @@ class ArchDataMapper:
return False
else:
continue
except ValueError:
if latest_part.isdigit():
return True
elif current_part.isdigit():
return False
else:
return latest_part > current_part
return False
def fill_package_build(self, pkg: ArchPackage):
@@ -104,14 +113,18 @@ class ArchDataMapper:
if res and res.status_code == 200 and res.text:
pkg.pkgbuild = res.text
def map_api_data(self, apidata: dict, installed: dict) -> ArchPackage:
def map_api_data(self, apidata: dict, installed: dict, categories: dict) -> ArchPackage:
data = installed.get(apidata.get('Name'))
app = ArchPackage(name=apidata.get('Name'), installed=bool(data), mirror='aur')
app.status = PackageStatus.LOADING_DATA
if categories:
app.categories = categories.get(app.name)
if data:
app.version = data.get('version')
app.description = data.get('description')
self.fill_api_data(app, apidata, fill_version=not data)
return app

View File

@@ -2,12 +2,10 @@ import datetime
from typing import List
from bauh.api.abstract.model import SoftwarePackage
from bauh.api.constants import CACHE_PATH
from bauh.commons import resource
from bauh.gems.arch import ROOT_DIR, ARCH_CACHE_PATH
from bauh.gems.arch import ROOT_DIR
CACHED_ATTRS = {'command', 'icon_path', 'mirror', 'maintainer', 'desktop_entry'}
CACHED_ATTRS = {'command', 'icon_path', 'mirror', 'maintainer', 'desktop_entry', 'categories'}
class ArchPackage(SoftwarePackage):
@@ -35,7 +33,7 @@ class ArchPackage(SoftwarePackage):
@staticmethod
def disk_cache_path(pkgname: str, mirror: str):
return CACHE_PATH + '/arch/installed/' + ('aur' if mirror == 'aur' else 'mirror') + '/' + pkgname
return ARCH_CACHE_PATH + '/installed/' + ('aur' if mirror == 'aur' else 'mirror') + '/' + pkgname
def get_pkg_build_url(self):
if self.package_base:

View File

@@ -1,4 +1,5 @@
import re
from threading import Thread
from typing import List, Set
from bauh.commons.system import run_cmd, new_subprocess, new_root_subprocess, SystemProcess
@@ -13,7 +14,7 @@ def is_enabled() -> bool:
def get_mirrors(pkgs: Set[str]) -> dict:
pkgre = '|'.join(pkgs)
pkgre = '|'.join(pkgs).replace('+', r'\+').replace('.', r'\.')
searchres = new_subprocess(['pacman', '-Ss', pkgre]).stdout
mirrors = {}
@@ -71,10 +72,18 @@ def check_installed(pkg: str) -> bool:
return bool(res)
def _fill_ignored(res: dict):
res['pkgs'] = list_ignored_packages()
def list_and_map_installed() -> dict: # returns a dict with with package names as keys and versions as values
installed = new_subprocess(['pacman', '-Qq']).stdout # retrieving all installed package names
allinfo = new_subprocess(['pacman', '-Qi'], stdin=installed).stdout # retrieving all installed packages info
ignored = {}
thread_ignored = Thread(target=_fill_ignored, args=(ignored,))
thread_ignored.start()
pkgs, current_pkg = {'mirrors': {}, 'not_signed': {}}, {}
for out in new_subprocess(['grep', '-E', '(Name|Description|Version|Validated By)'],
stdin=allinfo).stdout: # filtering only the Name and Validated By fields:
@@ -96,6 +105,18 @@ def list_and_map_installed() -> dict: # returns a dict with with package names
current_pkg = {}
if pkgs and pkgs.get('not_signed'):
thread_ignored.join()
if ignored['pkgs']:
to_del = set()
for pkg in pkgs['not_signed'].keys():
if pkg in ignored['pkgs']:
to_del.add(pkg)
for pkg in to_del:
del pkgs['not_signed'][pkg]
return pkgs
@@ -124,7 +145,7 @@ def list_icon_paths(pkgnames: Set[str]) -> List[str]:
installed_files = new_subprocess(['pacman', '-Qlq', *pkgnames])
icon_files = []
for out in new_subprocess(['grep', '-E', '.(png|svg)$'], stdin=installed_files.stdout).stdout:
for out in new_subprocess(['grep', '-E', '.(png|svg|xpm)$'], stdin=installed_files.stdout).stdout:
if out:
line = out.decode().strip()
if line:
@@ -179,3 +200,19 @@ def receive_key(key: str, root_password: str) -> SystemProcess:
def sign_key(key: str, root_password: str) -> SystemProcess:
return SystemProcess(new_root_subprocess(['pacman-key', '--lsign-key', key], root_password=root_password), check_error_output=False)
def list_ignored_packages(config_path: str = '/etc/pacman.conf') -> Set[str]:
pacman_conf = new_subprocess(['cat', config_path])
ignored = set()
grep = new_subprocess(['grep', '-Eo', r'\s*#*\s*ignorepkg\s*=\s*.+'], stdin=pacman_conf.stdout)
for o in grep.stdout:
if o:
line = o.decode().strip()
if not line.startswith('#'):
ignored.add(line.split('=')[1].strip())
pacman_conf.terminate()
grep.terminate()
return ignored

View File

@@ -60,4 +60,6 @@ arch.aur.install.pgp.receive_fail=Could not receive PGP key {}
arch.aur.install.unknown_key.title=Public key required
arch.install.aur.unknown_key.body=To continue {} installation is necessary to trust the following public key {}
arch.aur.install.unknown_key.status=Receiving public key {}
arch.aur.install.unknown_key.receive_error=Could not receive public key {}
arch.aur.install.unknown_key.receive_error=Could not receive public key {}
arch.aur.install.validity_check.title=Integrity issues
arch.aur.install.validity_check.body=Some of the source-files needed for {} installation are not healthy. The installation will be cancelled to prevent damage to your system.

View File

@@ -92,4 +92,6 @@ arch.aur.install.pgp.sign_fail=No fue posible recibir la clave PGP {}
arch.aur.install.unknown_key.title=Clave pública necesaria
arch.aur.install.unknown_key.status=Recibiendo la clave pública {}
arch.aur.install.unknown_key.receive_error=No fue posible recibir la clave pública {}
arch.install.aur.unknown_key.body=Para continuar la instalación de {} es necesario confiar en la siguiente clave pública {}
arch.install.aur.unknown_key.body=Para continuar la instalación de {} es necesario confiar en la siguiente clave pública {}
arch.aur.install.validity_check.title=Problemas de integridad
arch.aur.install.validity_check.body=Algunos de los archivos fuente necesarios para la instalación de {} no están en buen estado. La instalación se cancelará para evitar daños a su sistema.

View File

@@ -92,4 +92,6 @@ arch.aur.install.pgp.receive_fail=Não foi possível receber a chave PGP {}
arch.aur.install.unknown_key.title=Chave pública necessária
arch.install.aur.unknown_key.body=Para continuar a instalação de {} é ncessário confiar na seguinte chave pública {}
arch.aur.install.unknown_key.status=Recebendo a chave pública {}
arch.aur.install.unknown_key.receive_error=Não fui possível receber a chave pública {}
arch.aur.install.unknown_key.receive_error=Não fui possível receber a chave pública {}
arch.aur.install.validity_check.title=Problemas de integridade
arch.aur.install.validity_check.body=Alguns dos arquivos-fonte necessários para instalação de {} não estão íntegros. A instalação será cancelada para evitar danos no seu sistema.

View File

@@ -2,18 +2,15 @@ import logging
import os
import re
import time
import traceback
from math import ceil
from multiprocessing import Process
from threading import Thread
from typing import Dict, List
import requests
from bauh.api.abstract.context import ApplicationContext
from bauh.api.abstract.controller import SoftwareManager
from bauh.api.constants import HOME_PATH
from bauh.api.http import HttpClient
from bauh.gems.arch import pacman, disk
URL_INDEX = 'https://aur.archlinux.org/packages.gz'
@@ -47,7 +44,7 @@ class AURIndexUpdater(Thread):
self.logger.info('Pre-indexed {} AUR package names in memory'.format(len(self.man.names_index)))
else:
self.logger.warning('No data returned from: {}'.format(URL_INDEX))
except ConnectionError:
except requests.exceptions.ConnectionError:
self.logger.warning('No internet connection: could not pre-index packages')
time.sleep(60 * 20) # updates every 20 minutes
@@ -137,34 +134,3 @@ class ArchCompilationOptimizer(Thread if bool(os.getenv('BAUH_DEBUG', 0)) else P
self.logger.info("A custom optimized 'makepkg.conf' was generated at '{}'".format(HOME_PATH))
else:
self.logger.warning("A custom 'makepkg.conf' is already defined at '{}'".format(HOME_PATH))
class CategoriesDownloader:
URL_CATEGORIES_FILE = 'https://raw.githubusercontent.com/vinifmor/bauh-files/master/aur/categories.txt'
def __init__(self, http_client: HttpClient, logger: logging.Logger):
self.http_client = http_client
self.logger = logger
def get_categories(self) -> Dict[str, List[str]]:
self.logger.info('Downloading AUR category definitions from {}'.format(self.URL_CATEGORIES_FILE))
res = self.http_client.get(self.URL_CATEGORIES_FILE)
if res:
try:
categories_map = {}
for l in res.text.split('\n'):
if l:
data = l.split('=')
categories_map[data[0]] = [c.strip() for c in data[1].split(',') if c]
self.logger.info('Loaded categories for {} AUR packages'.format(len(categories_map)))
return categories_map
except:
self.logger.error("Could not parse AUR categories definitions")
traceback.print_exc()
return {}
else:
self.logger.info('Could not download {}'.format(self.URL_CATEGORIES_FILE))

View File

@@ -1,3 +1,4 @@
import json
import traceback
from datetime import datetime
from threading import Thread
@@ -24,6 +25,7 @@ class FlatpakManager(SoftwareManager):
super(FlatpakManager, self).__init__(context=context)
self.i18n = context.i18n
self.api_cache = context.cache_factory.new()
self.category_cache = context.cache_factory.new()
context.disk_loader_factory.map(FlatpakApplication, self.api_cache)
self.enabled = True
self.http_client = context.http_client
@@ -45,7 +47,8 @@ class FlatpakManager(SoftwareManager):
disk_loader.fill(app) # preloading cached disk data
if internet:
FlatpakAsyncDataLoader(app=app, api_cache=self.api_cache, manager=self, context=self.context).start()
FlatpakAsyncDataLoader(app=app, api_cache=self.api_cache, manager=self,
context=self.context, category_cache=self.category_cache).start()
else:
app.fill_cached_data(api_data)
@@ -247,7 +250,7 @@ class FlatpakManager(SoftwareManager):
return updates
def list_warnings(self) -> List[str]:
def list_warnings(self, internet_available: bool) -> List[str]:
if flatpak.is_installed():
if not flatpak.has_remotes_set():
return [self.i18n['flatpak.notification.no_remotes']]
@@ -279,11 +282,20 @@ class FlatpakManager(SoftwareManager):
flatpak.run(str(pkg.id))
def get_screenshots(self, pkg: SoftwarePackage) -> List[str]:
res = self.http_client.get_json('{}/apps/{}'.format(FLATHUB_API_URL, pkg.id))
screenshots_url = '{}/apps/{}'.format(FLATHUB_API_URL, pkg.id)
urls = []
if res and res.get('screenshots'):
for s in res['screenshots']:
if s.get('imgDesktopUrl'):
urls.append(s['imgDesktopUrl'])
try:
res = self.http_client.get_json(screenshots_url)
if res and res.get('screenshots'):
for s in res['screenshots']:
if s.get('imgDesktopUrl'):
urls.append(s['imgDesktopUrl'])
except Exception as e:
if e.__class__.__name__ == 'JSONDecodeError':
self.context.logger.error("Could not decode json from '{}'".format(screenshots_url))
else:
traceback.print_exc()
return urls

View File

@@ -17,6 +17,9 @@ class FlatpakApplication(SoftwarePackage):
self.runtime = runtime
self.commit = commit
if runtime:
self.categories = ['runtime']
def is_incomplete(self):
return self.description is None and self.icon_url

View File

@@ -1,4 +1,5 @@
import traceback
from io import StringIO
from threading import Thread
from bauh.api.abstract.cache import MemoryCache
@@ -12,7 +13,7 @@ from bauh.gems.flatpak.model import FlatpakApplication
class FlatpakAsyncDataLoader(Thread):
def __init__(self, app: FlatpakApplication, manager: SoftwareManager, context: ApplicationContext, api_cache: MemoryCache):
def __init__(self, app: FlatpakApplication, manager: SoftwareManager, context: ApplicationContext, api_cache: MemoryCache, category_cache: MemoryCache):
super(FlatpakAsyncDataLoader, self).__init__(daemon=True)
self.app = app
self.manager = manager
@@ -20,6 +21,21 @@ class FlatpakAsyncDataLoader(Thread):
self.api_cache = api_cache
self.persist = False
self.logger = context.logger
self.category_cache = category_cache
@staticmethod
def format_category(category: str) -> str:
word = StringIO()
last_l = None
for idx, l in enumerate(category):
if idx != 0 and last_l != ' ' and l.isupper() and idx + 1 < len(category) and category[idx + 1].islower():
word.write(' ')
last_l = l.lower()
word.write(last_l)
word.seek(0)
return word.read()
def run(self):
if self.app:
@@ -51,7 +67,17 @@ class FlatpakAsyncDataLoader(Thread):
self.app.icon_url = FLATHUB_URL + self.app.icon_url
if data.get('categories'):
self.app.categories = [c['name'] for c in data['categories']]
cats = []
for c in data['categories']:
cached = self.category_cache.get(c['name'])
if not cached:
cached = self.format_category(c['name'])
self.category_cache.add_non_existing(c['name'], cached)
cats.append(cached)
self.app.categories = cats
loaded_data = self.app.get_data_to_cache()

View File

@@ -1,2 +1,8 @@
import os
from bauh.api.constants import CACHE_PATH
ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
SNAP_CACHE_PATH = CACHE_PATH + '/snap'
CATEGORIES_FILE_PATH = SNAP_CACHE_PATH + '/categories.txt'
URL_CATEGORIES_FILE = 'https://raw.githubusercontent.com/vinifmor/bauh-files/master/snap/categories.txt'

View File

@@ -1,3 +1,4 @@
import re
import time
from datetime import datetime
from threading import Thread
@@ -8,12 +9,16 @@ from bauh.api.abstract.disk import DiskCacheLoader
from bauh.api.abstract.handler import ProcessWatcher
from bauh.api.abstract.model import SoftwarePackage, PackageHistory, PackageUpdate, PackageSuggestion, \
SuggestionPriority
from bauh.api.abstract.view import SingleSelectComponent, SelectViewType, InputOption
from bauh.commons.category import CategoriesDownloader
from bauh.commons.html import bold
from bauh.commons.system import SystemProcess, ProcessHandler
from bauh.gems.snap import snap, suggestions
from bauh.commons.system import SystemProcess, ProcessHandler, new_root_subprocess
from bauh.gems.snap import snap, suggestions, URL_CATEGORIES_FILE, SNAP_CACHE_PATH, CATEGORIES_FILE_PATH
from bauh.gems.snap.constants import SNAP_API_URL
from bauh.gems.snap.model import SnapApplication
from bauh.gems.snap.worker import SnapAsyncDataLoader, CategoriesDownloader
from bauh.gems.snap.worker import SnapAsyncDataLoader
RE_AVAILABLE_CHANNELS = re.compile(re.compile(r'(\w+)\s+(snap install.+)'))
class SnapManager(SoftwareManager):
@@ -28,7 +33,8 @@ class SnapManager(SoftwareManager):
self.logger = context.logger
self.ubuntu_distro = context.distro == 'ubuntu'
self.categories = {}
self.categories_downloader = CategoriesDownloader(self.http_client, self.logger)
self.categories_downloader = CategoriesDownloader('snap', self.http_client, self.logger, self, context.disk_cache,
URL_CATEGORIES_FILE, SNAP_CACHE_PATH, CATEGORIES_FILE_PATH)
def map_json(self, app_json: dict, installed: bool, disk_loader: DiskCacheLoader, internet: bool = True) -> SnapApplication:
app = SnapApplication(publisher=app_json.get('publisher'),
@@ -41,13 +47,27 @@ class SnapManager(SoftwareManager):
latest_version=app_json.get('version'),
description=app_json.get('description', app_json.get('summary')))
if app.publisher:
if app.publisher and app.publisher.endswith('*'):
app.verified_publisher = True
app.publisher = app.publisher.replace('*', '')
app.categories = self.categories.get(app.name.lower())
categories = self.categories.get(app.name.lower())
if categories:
app.categories = categories
app.installed = installed
if not app.is_application():
categories = app.categories
if categories is None:
categories = []
app.categories = categories
if 'runtime' not in categories:
categories.append('runtime')
api_data = self.api_cache.get(app_json['name'])
expired_data = api_data and api_data.get('expires_at') and api_data['expires_at'] <= datetime.utcnow()
@@ -88,6 +108,7 @@ class SnapManager(SoftwareManager):
def read_installed(self, disk_loader: DiskCacheLoader, limit: int = -1, only_apps: bool = False, pkg_types: Set[Type[SoftwarePackage]] = None, internet_available: bool = None) -> SearchResult:
if snap.is_snapd_running():
self.categories_downloader.join()
installed = [self.map_json(app_json, installed=True, disk_loader=disk_loader, internet=internet_available) for app_json in snap.read_installed(self.ubuntu_distro)]
return SearchResult(installed, None, len(installed))
else:
@@ -125,9 +146,31 @@ class SnapManager(SoftwareManager):
raise Exception("'get_history' is not supported by {}".format(pkg.__class__.__name__))
def install(self, pkg: SnapApplication, root_password: str, watcher: ProcessWatcher) -> bool:
res = ProcessHandler(watcher).handle(SystemProcess(subproc=snap.install_and_stream(pkg.name, pkg.confinement, root_password)))
res, output = ProcessHandler(watcher).handle_simple(snap.install_and_stream(pkg.name, pkg.confinement, root_password))
if res:
if 'error:' in output:
res = False
if 'not available on stable' in output:
channels = RE_AVAILABLE_CHANNELS.findall(output)
if channels:
opts = [InputOption(label=c[0], value=c[1]) for c in channels]
channel_select = SingleSelectComponent(type_=SelectViewType.RADIO, label='', options=opts, default_option=opts[0])
if watcher.request_confirmation(title=self.i18n['snap.install.available_channels.title'],
body=self.i18n['snap.install.available_channels.body'].format(bold(self.i18n['stable']), bold(pkg.name)) + ':',
components=[channel_select],
confirmation_label=self.i18n['continue'],
deny_label=self.i18n['cancel']):
self.logger.info("Installing '{}' with the custom command '{}'".format(pkg.name, channel_select.value))
res = ProcessHandler(watcher).handle(SystemProcess(new_root_subprocess(channel_select.value.value.split(' '), root_password=root_password)))
if res:
pkg.has_apps_field = snap.has_apps_field(pkg.name, self.ubuntu_distro)
return res
else:
self.logger.error("Could not find available channels in the installation output: {}".format(output))
else:
pkg.has_apps_field = snap.has_apps_field(pkg.name, self.ubuntu_distro)
return res
@@ -148,20 +191,24 @@ class SnapManager(SoftwareManager):
return ProcessHandler(watcher).handle(SystemProcess(subproc=snap.refresh_and_stream(pkg.name, root_password)))
def prepare(self):
categories = self.categories_downloader.get_categories()
if categories:
self.categories = categories
self.categories_downloader.start()
def list_updates(self, internet_available: bool) -> List[PackageUpdate]:
pass
def list_warnings(self) -> List[str]:
def list_warnings(self, internet_available: bool) -> List[str]:
if snap.is_installed() and not snap.is_snapd_running():
snap_bold = bold('Snap')
return [self.i18n['snap.notification.snapd_unavailable'].format(bold('snapd'), snap_bold),
self.i18n['snap.notification.snap.disable'].format(snap_bold, bold(self.i18n['manage_window.settings.gems']))]
if internet_available:
available, output = snap.is_api_available()
if not available:
self.logger.warning('It seems Snap API is not available. Search output: {}'.format(output))
return [self.i18n['snap.notifications.api.unavailable'].format(bold('Snaps'), bold('Snap'))]
def _fill_suggestion(self, pkg_name: str, priority: SuggestionPriority, out: List[PackageSuggestion]):
res = self.http_client.get_json(SNAP_API_URL + '/search?q=package_name:{}'.format(pkg_name))
@@ -182,6 +229,8 @@ class SnapManager(SoftwareManager):
sugs.sort(key=lambda t: t[1].value, reverse=True)
threads = []
self.categories_downloader.join()
for sug in sugs:
if limit <= 0 or len(res) < limit:

View File

@@ -19,7 +19,7 @@ class SnapApplication(SoftwarePackage):
def __init__(self, id: str = None, name: str = None, version: str = None, latest_version: str = None,
description: str = None, publisher: str = None, rev: str = None, notes: str = None,
confinement: str = None, has_apps_field: bool = None):
confinement: str = None, has_apps_field: bool = None, verified_publisher: bool = False):
super(SnapApplication, self).__init__(id=id, name=name, version=version,
latest_version=latest_version, description=description)
self.publisher = publisher
@@ -27,6 +27,7 @@ class SnapApplication(SoftwarePackage):
self.notes = notes
self.confinement = confinement
self.has_apps_field = has_apps_field
self.verified_publisher = verified_publisher
def supports_disk_cache(self):
return self.installed

View File

@@ -1,5 +1,8 @@
gem.snap.info=Applications published at https://snapcraft.io/store
snap.notification.snapd_unavailable={} seems not to be installed or enabled. {} packages will not be available.
snap.notification.snap.disable=If you do not want to use Snap applications, uncheck {} in {}
snap.notifications.api.unavailable=It seems the {} API is unavailable at the moment. It will not be possible to search for new {} applications.
snap.action.refresh.status=Refreshing
snap.action.refresh.label=Refresh
snap.action.refresh.label=Refresh
snap.install.available_channels.title=Available channels
snap.install.available_channels.body=There is no {} channel available for {}. But there are these below ( select one if you want to continue )

View File

@@ -12,5 +12,8 @@ snap.info.version=versión
snap.info.size=tamaño
snap.notification.snapd_unavailable={} no parece estar instalado o habilitado. Los paquetes {} estarán indisponibles.
snap.notification.snap.disable=Si no desea usar aplicativos Snap, desmarque {} en {}
snap.notifications.api.unavailable=Parece que la API de {} no está disponible en este momento. No será posible buscar nuevos aplicativos {}.
snap.action.refresh.status=Actualizando
snap.action.refresh.label=Actualizar
snap.action.refresh.label=Actualizar
snap.install.available_channels.title=Canales disponibles
snap.install.available_channels.body=No hay un canal {} ( stable ) disponible para {}. Pero hay estos otros abajo ( seleccione uno si desea continuar )

View File

@@ -12,5 +12,8 @@ snap.info.version=versão
snap.info.size=tamanho
snap.notification.snapd_unavailable={} não parece estar instalado ou habilitado. Os pacotes {} estarão indisponíveis.
snap.notification.snap.disable=Se não deseja usar aplicativos Snap, desmarque {} em {}
snap.notifications.api.unavailable=Parece que a API de {} está indisponível no momento. Não será possível buscar novos aplicativos {}.
snap.action.refresh.status=Atualizando
snap.action.refresh.label=Atualizar
snap.action.refresh.label=Atualizar
snap.install.available_channels.title=Canais disponíveis
snap.install.available_channels.body=Não há um canal {} ( stable ) disponível para {}. Porém existem estes outros abaixo ( selecione algum se quiser continuar )

View File

@@ -1,9 +1,10 @@
import logging
import re
import subprocess
from typing import List
from io import StringIO
from typing import List, Tuple
from bauh.commons.system import new_root_subprocess, run_cmd, new_subprocess
from bauh.commons.system import new_root_subprocess, run_cmd, new_subprocess, SimpleProcess
from bauh.gems.snap.model import SnapApplication
BASE_CMD = 'snap'
@@ -176,14 +177,15 @@ def uninstall_and_stream(app_name: str, root_password: str):
return new_root_subprocess([BASE_CMD, 'remove', app_name], root_password)
def install_and_stream(app_name: str, confinement: str, root_password: str) -> subprocess.Popen:
def install_and_stream(app_name: str, confinement: str, root_password: str) -> SimpleProcess:
install_cmd = [BASE_CMD, 'install', app_name] # default
if confinement == 'classic':
install_cmd.append('--classic')
return new_root_subprocess(install_cmd, root_password)
# return new_root_subprocess(install_cmd, root_password)
return SimpleProcess(install_cmd, root_password=root_password)
def downgrade_and_stream(app_name: str, root_password: str) -> subprocess.Popen:
@@ -226,3 +228,14 @@ def run(app: SnapApplication, logger: logging.Logger):
logger.error("No valid command found for '{}'".format(app_name))
else:
logger.error("No command found for '{}'".format(app_name))
def is_api_available() -> Tuple[bool, str]:
output = StringIO()
for o in SimpleProcess(['snap', 'search']).instance.stdout:
if o:
output.write(o.decode())
output.seek(0)
output = output.read()
return 'error:' not in output, output

View File

@@ -1,14 +1,10 @@
import logging
import os
import traceback
from threading import Thread
from typing import Dict, List
from bauh.api.abstract.cache import MemoryCache
from bauh.api.abstract.context import ApplicationContext
from bauh.api.abstract.controller import SoftwareManager
from bauh.api.abstract.model import PackageStatus
from bauh.api.http import HttpClient
from bauh.gems.snap import snap
from bauh.gems.snap.constants import SNAP_API_URL
from bauh.gems.snap.model import SnapApplication
@@ -72,34 +68,3 @@ class SnapAsyncDataLoader(Thread):
if self.persist:
self.manager.cache_to_disk(pkg=self.app, icon_bytes=None, only_icon=False)
class CategoriesDownloader:
URL_CATEGORIES_FILE = 'https://raw.githubusercontent.com/vinifmor/bauh-files/master/snap/categories.txt'
def __init__(self, http_client: HttpClient, logger: logging.Logger):
self.http_client = http_client
self.logger = logger
def get_categories(self) -> Dict[str, List[str]]:
self.logger.info('Downloading Snap category definitions from {}'.format(self.URL_CATEGORIES_FILE))
res = self.http_client.get(self.URL_CATEGORIES_FILE)
if res:
try:
categories_map = {}
for l in res.text.split('\n'):
if l:
data = l.split('=')
categories_map[data[0]] = [c.strip() for c in data[1].split(',') if c]
self.logger.info('Loaded categories for {} Snap applications'.format(len(categories_map)))
return categories_map
except:
self.logger.error("Could not parse categories definitions")
traceback.print_exc()
return {}
else:
self.logger.info('Could not download {}'.format(self.URL_CATEGORIES_FILE))

View File

@@ -307,13 +307,15 @@ class GenericSoftwareManager(SoftwareManager):
return updates
def list_warnings(self) -> List[str]:
def list_warnings(self, internet_available: bool = None) -> List[str]:
warnings = []
if self.managers:
int_available = internet.is_available(self.context.http_client, self.context.logger)
for man in self.managers:
if man.is_enabled():
man_warnings = man.list_warnings()
man_warnings = man.list_warnings(internet_available=int_available)
if man_warnings:
if warnings is None:

View File

@@ -55,8 +55,8 @@ class AboutDialog(QDialog):
layout.addWidget(gems_widget)
layout.addWidget(QLabel(''))
label_version = QLabel(locale_keys['version'].lower() + ': ' + __version__)
label_version.setStyleSheet('QLabel { font-size: 10px; font-weight: bold }')
label_version = QLabel(locale_keys['version'].lower() + ' ' + __version__)
label_version.setStyleSheet('QLabel { font-size: 11px; font-weight: bold }')
label_version.setAlignment(Qt.AlignCenter)
layout.addWidget(label_version)

View File

@@ -231,7 +231,7 @@ class AppsTable(QTableWidget):
col_update = None
if update_check_enabled and pkg.model.update:
col_update = UpdateToggleButton(pkg, self.window, self.i18n, pkg.model.update)
col_update = UpdateToggleButton(pkg, self.window, self.i18n, pkg.update_checked)
self.setCellWidget(pkg.table_index, 7, col_update)

View File

@@ -31,7 +31,8 @@ def update_info(pkgv: PackageView, pkgs_info: dict):
pkgs_info['updates'] += 1
if pkgv.model.categories:
pkgs_info['categories'].update([s.lower() for s in pkgv.model.categories])
for c in pkgv.model.categories:
pkgs_info['categories'].add(c.lower())
pkgs_info['pkgs'].append(pkgv)
pkgs_info['not_installed'] += 1 if not pkgv.model.installed else 0

View File

@@ -67,7 +67,7 @@ class ComboBoxQt(QComboBox):
class RadioSelectQt(QGroupBox):
def __init__(self, model: SingleSelectComponent):
super(RadioSelectQt, self).__init__(model.label + ' :')
super(RadioSelectQt, self).__init__(model.label + ' :' if model.label else None)
self.model = model
self.setStyleSheet("QGroupBox { font-weight: bold }")

View File

@@ -13,28 +13,41 @@ def is_root():
return os.getuid() == 0
def ask_root_password(locale_keys: dict):
def ask_root_password(i18n: dict):
diag = QInputDialog()
diag.setStyleSheet("""QLineEdit { border-radius: 5px; font-size: 16px }""")
diag.setInputMode(QInputDialog.TextInput)
diag.setTextEchoMode(QLineEdit.Password)
diag.setWindowIcon(QIcon(resource.get_path('img/lock.png')))
diag.setWindowTitle(locale_keys['popup.root.title'])
diag.setWindowTitle(i18n['popup.root.title'])
diag.setLabelText('')
diag.setCancelButtonText(locale_keys['popup.button.cancel'])
diag.setCancelButtonText(i18n['popup.button.cancel'])
diag.resize(400, 200)
ok = diag.exec_()
for attempt in range(3):
if ok:
if not validate_password(diag.textValue()):
show_message(title=locale_keys['popup.root.bad_password.title'],
body=locale_keys['popup.root.bad_password.body'],
type_=MessageType.ERROR)
ok = False
ok = diag.exec_()
return diag.textValue(), ok
if ok:
if not validate_password(diag.textValue()):
body = i18n['popup.root.bad_password.body']
if attempt == 2:
body += '. ' + i18n['popup.root.bad_password.last_try']
show_message(title=i18n['popup.root.bad_password.title'],
body=body,
type_=MessageType.ERROR)
ok = False
diag.setTextValue('')
if ok:
return diag.textValue(), ok
else:
break
return '', False
def validate_password(password: str) -> bool:

View File

@@ -4,12 +4,13 @@ from typing import List
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QIcon, QPixmap
from PyQt5.QtWidgets import QDialog, QLabel, QPushButton, QToolBar, QVBoxLayout
from PyQt5.QtWidgets import QDialog, QLabel, QPushButton, QToolBar, QVBoxLayout, QProgressBar, QApplication
from bauh.api.abstract.cache import MemoryCache
from bauh.api.http import HttpClient
from bauh.view.qt import qt_utils
from bauh.view.qt.components import new_spacer
from bauh.view.qt.thread import AnimateProgress
from bauh.view.qt.view_model import PackageView
@@ -28,6 +29,12 @@ class ScreenshotsDialog(QDialog):
self.resize(1280, 720)
self.i18n = i18n
self.http_client = http_client
self.progress_bar = QProgressBar()
self.progress_bar.setMaximumHeight(10 if QApplication.instance().style().objectName().lower() == 'windows' else 6)
self.progress_bar.setTextVisible(False)
self.thread_progress = AnimateProgress()
self.thread_progress.signal_change.connect(self._update_progress)
self.thread_progress.start()
# THERE ARE CRASHES WITH SOME RARE ICONS ( like insomnia ). IT CAN BE A QT BUG. IN THE MEANTIME, ONLY THE TYPE ICON WILL BE RENDERED
#
@@ -52,7 +59,9 @@ class ScreenshotsDialog(QDialog):
self.img_label = QLabel()
self.img_label.setStyleSheet('QLabel { font-weight: bold; text-align: center }')
self.bottom_bar.addWidget(self.img_label)
self.ref_img_label = self.bottom_bar.addWidget(self.img_label)
self.ref_img_label.setVisible(False)
self.ref_progress_bar = self.bottom_bar.addWidget(self.progress_bar)
self.bottom_bar.addWidget(new_spacer(50))
self.bt_next = QPushButton(self.i18n['screenshots.bt_next.label'].capitalize())
@@ -64,11 +73,14 @@ class ScreenshotsDialog(QDialog):
self.img_idx = 0
for idx, s in enumerate(self.screenshots):
t = Thread(target=self._download_img, args=(idx, s))
t = Thread(target=self._download_img, args=(idx, s), daemon=True)
t.start()
self._load_img()
def _update_progress(self, val: int):
self.progress_bar.setValue(val)
def _load_img(self):
if len(self.loaded_imgs) > self.img_idx:
img = self.loaded_imgs[self.img_idx]
@@ -79,9 +91,15 @@ class ScreenshotsDialog(QDialog):
else:
self.img_label.setText(img)
self.img.setPixmap(QPixmap())
self.thread_progress.stop = True
self.ref_progress_bar.setVisible(False)
self.ref_img_label.setVisible(True)
else:
self.img.setPixmap(QPixmap())
self.img_label.setText('...{}...'.format(self.i18n['screenshots,download.running']))
self.ref_img_label.setVisible(False)
self.ref_progress_bar.setVisible(True)
self.thread_progress.start()
if len(self.screenshots) == 1:
self.ref_bt_back.setVisible(False)

View File

@@ -64,8 +64,7 @@ class AsyncAction(QThread, ProcessWatcher):
self.signal_status.emit(status)
def change_substatus(self, substatus: str):
if substatus:
self.signal_substatus.emit(substatus)
self.signal_substatus.emit(substatus)
def change_progress(self, val: int):
if val is not None:
@@ -96,6 +95,7 @@ class UpdateSelectedApps(AsyncAction):
self.change_status('{} {} {}...'.format(self.locale_keys['manage_window.status.upgrading'], name, app.model.version))
success = bool(self.manager.update(app.model, self.root_password, self))
self.change_substatus('')
if not success:
break
@@ -352,6 +352,7 @@ class VerifyModels(QThread):
def __init__(self, apps: List[PackageView] = None):
super(VerifyModels, self).__init__()
self.apps = apps
self.work = True
def run(self):
@@ -361,6 +362,10 @@ class VerifyModels(QThread):
last_ready = 0
while True:
if not self.work:
break
current_ready = 0
for app in self.apps:
@@ -379,6 +384,7 @@ class VerifyModels(QThread):
time.sleep(0.1)
self.work = True
self.apps = None

View File

@@ -19,8 +19,6 @@ from bauh.api.http import HttpClient
from bauh.commons.html import bold
from bauh.view.core.config import Configuration
from bauh.view.core.controller import GenericSoftwareManager
from bauh.view.qt.screenshots import ScreenshotsDialog
from bauh.view.util import util, resource
from bauh.view.qt import dialog, commons, qt_utils
from bauh.view.qt.about import AboutDialog
from bauh.view.qt.apps_table import AppsTable, UpdateToggleButton
@@ -30,12 +28,14 @@ from bauh.view.qt.gem_selector import GemSelectorPanel
from bauh.view.qt.history import HistoryDialog
from bauh.view.qt.info import InfoDialog
from bauh.view.qt.root import is_root, ask_root_password
from bauh.view.qt.screenshots import ScreenshotsDialog
from bauh.view.qt.styles import StylesComboBox
from bauh.view.qt.thread import UpdateSelectedApps, RefreshApps, UninstallApp, DowngradeApp, GetAppInfo, \
GetAppHistory, SearchPackages, InstallPackage, AnimateProgress, VerifyModels, FindSuggestions, ListWarnings, \
AsyncAction, LaunchApp, ApplyFilters, CustomAction, GetScreenshots
from bauh.view.qt.view_model import PackageView
from bauh.view.qt.view_utils import load_icon
from bauh.view.util import util, resource
DARK_ORANGE = '#FF4500'
@@ -283,6 +283,7 @@ class ManageWindow(QWidget):
self._maximized = False
self.progress_controll_enabled = True
self.recent_installation = False
self.recent_uninstall = False
self.dialog_about = None
self.first_refresh = suggestions
@@ -314,6 +315,13 @@ class ManageWindow(QWidget):
self._update_table(pkgs_info=pkgs_info, signal=True)
self.update_bt_upgrade(pkgs_info)
if self.pkgs_available:
self._notify_model_data_change()
self.thread_verify_models.work = False
self.thread_verify_models.wait(50)
self.thread_verify_models.apps = self.pkgs_available
self.thread_verify_models.start()
def _finish_apply_filters_async(self, success: bool):
self.label_status.setText('')
self.ref_toolbar_search.setVisible(True)
@@ -400,6 +408,21 @@ class ManageWindow(QWidget):
def _notify_model_data_change(self):
self.table_apps.fill_async_data()
if not self.recent_installation:
self._reload_categories()
def _reload_categories(self):
categories = set()
for p in self.pkgs_available:
if p.model.categories:
for c in p.model.categories:
categories.add(c.lower())
if categories:
self._update_categories(categories, keep_selected=True)
def changeEvent(self, e: QEvent):
if isinstance(e, QWindowStateChangeEvent):
self._maximized = self.isMaximized()
@@ -431,7 +454,6 @@ class ManageWindow(QWidget):
def refresh_apps(self, keep_console: bool = True, top_app: PackageView = None, pkg_types: Set[Type[SoftwarePackage]] = None):
self.recent_installation = False
self.type_filter = None
self.input_search.clear()
if not keep_console:
@@ -439,7 +461,7 @@ class ManageWindow(QWidget):
self.ref_checkbox_updates.setVisible(False)
self.ref_checkbox_only_apps.setVisible(False)
self._begin_action(self.i18n['manage_window.status.refreshing'], keep_bt_installed=False, clear_filters=True)
self._begin_action(self.i18n['manage_window.status.refreshing'], keep_bt_installed=False, clear_filters=not self.recent_uninstall)
self.thread_refresh.app = top_app # the app will be on top when refresh happens
self.thread_refresh.pkg_types = pkg_types
@@ -449,8 +471,9 @@ class ManageWindow(QWidget):
self.finish_action()
self.ref_checkbox_only_apps.setVisible(bool(res['installed']))
self.ref_bt_upgrade.setVisible(True)
self.update_pkgs(res['installed'], as_installed=as_installed, types=res['types'])
self.update_pkgs(res['installed'], as_installed=as_installed, types=res['types'], keep_filters=self.recent_uninstall and res['types'])
self.first_refresh = False
self.recent_uninstall = False
self._hide_fields_after_recent_installation()
def uninstall_app(self, app: PackageView):
@@ -464,7 +487,7 @@ class ManageWindow(QWidget):
return
self._handle_console_option(True)
self._begin_action('{} {}'.format(self.i18n['manage_window.status.uninstalling'], app.model.name))
self._begin_action('{} {}'.format(self.i18n['manage_window.status.uninstalling'], app.model.name), clear_filters=False)
self.thread_uninstall.app = app
self.thread_uninstall.root_password = pwd
@@ -483,6 +506,7 @@ class ManageWindow(QWidget):
util.notify_user('{} ({}) {}'.format(pkgv.model.name, pkgv.model.get_type(), self.i18n['uninstalled']))
only_pkg_type = len([p for p in self.pkgs if p.model.get_type() == pkgv.model.get_type()]) >= 2
self.recent_uninstall = True
self.refresh_apps(pkg_types={pkgv.model.__class__} if only_pkg_type else None)
else:
if self._can_notify_user():
@@ -547,7 +571,7 @@ class ManageWindow(QWidget):
self.ref_bt_upgrade.setVisible(show_bt_upgrade)
def change_update_state(self, pkgs_info: dict, trigger_filters: bool = True):
def change_update_state(self, pkgs_info: dict, trigger_filters: bool = True, keep_selected: bool = False):
self.update_bt_upgrade(pkgs_info)
if pkgs_info['updates'] > 0:
@@ -556,13 +580,14 @@ class ManageWindow(QWidget):
if not self.ref_checkbox_updates.isVisible():
self.ref_checkbox_updates.setVisible(True)
if not self.filter_updates:
if not self.filter_updates and not keep_selected:
self._change_checkbox(self.checkbox_updates, True, 'filter_updates', trigger_filters)
if pkgs_info['napp_updates'] > 0 and self.filter_only_apps:
if pkgs_info['napp_updates'] > 0 and self.filter_only_apps and not keep_selected:
self._change_checkbox(self.checkbox_only_apps, False, 'filter_only_apps', trigger_filters)
else:
self._change_checkbox(self.checkbox_updates, False, 'filter_updates', trigger_filters)
if not keep_selected:
self._change_checkbox(self.checkbox_updates, False, 'filter_updates', trigger_filters)
self.ref_checkbox_updates.setVisible(False)
@@ -586,7 +611,7 @@ class ManageWindow(QWidget):
'display_limit': self.display_limit if updates <= 0 else None
}
def update_pkgs(self, new_pkgs: List[SoftwarePackage], as_installed: bool, types: Set[type] = None, ignore_updates: bool = False):
def update_pkgs(self, new_pkgs: List[SoftwarePackage], as_installed: bool, types: Set[type] = None, ignore_updates: bool = False, keep_filters: bool = False):
self.input_name_filter.setText('')
pkgs_info = commons.new_pkgs_info()
filters = self._gen_filters(ignore_updates)
@@ -620,15 +645,19 @@ class ManageWindow(QWidget):
self.thread_suggestions.start()
return
else:
self._change_checkbox(self.checkbox_only_apps, False, 'filter_only_apps', trigger=False)
self.checkbox_only_apps.setCheckable(False)
if not keep_filters:
self._change_checkbox(self.checkbox_only_apps, False, 'filter_only_apps', trigger=False)
self.checkbox_only_apps.setCheckable(False)
else:
self.checkbox_only_apps.setCheckable(True)
self._change_checkbox(self.checkbox_only_apps, True, 'filter_only_apps', trigger=False)
if not keep_filters:
self.checkbox_only_apps.setCheckable(True)
self._change_checkbox(self.checkbox_only_apps, True, 'filter_only_apps', trigger=False)
self.change_update_state(pkgs_info=pkgs_info, trigger_filters=False)
self.change_update_state(pkgs_info=pkgs_info, trigger_filters=False, keep_selected=keep_filters and bool(pkgs_info['pkgs_displayed']))
self._update_categories(pkgs_info['categories'], keep_selected=keep_filters and bool(pkgs_info['pkgs_displayed']))
self._update_type_filters(pkgs_info['available_types'], keep_selected=keep_filters and bool(pkgs_info['pkgs_displayed']))
self._apply_filters(pkgs_info, ignore_updates=ignore_updates)
self.change_update_state(pkgs_info=pkgs_info, trigger_filters=False)
self.change_update_state(pkgs_info=pkgs_info, trigger_filters=False, keep_selected=keep_filters and bool(pkgs_info['pkgs_displayed']))
self.pkgs_available = pkgs_info['pkgs']
@@ -640,9 +669,6 @@ class ManageWindow(QWidget):
if self.pkgs:
self.ref_input_name_filter.setVisible(True)
self._update_type_filters(pkgs_info['available_types'])
self._update_categories(pkgs_info['categories'])
self._update_table(pkgs_info=pkgs_info)
if new_pkgs:
@@ -660,19 +686,25 @@ class ManageWindow(QWidget):
for pkgv in pkgs_info['pkgs']:
commons.apply_filters(pkgv, filters, pkgs_info)
def _update_type_filters(self, available_types: dict = None):
def _update_type_filters(self, available_types: dict = None, keep_selected: bool = False):
if available_types is None:
self.ref_combo_filter_type.setVisible(self.combo_filter_type.count() > 1)
else:
self.type_filter = self.any_type_filter
keeping_selected = keep_selected and available_types and self.type_filter in available_types
if not keeping_selected:
self.type_filter = self.any_type_filter
if available_types and len(available_types) > 1:
if self.combo_filter_type.count() > 1:
for _ in range(self.combo_filter_type.count() - 1):
self.combo_filter_type.removeItem(1)
for app_type, icon_path in available_types.items():
sel_type = -1
for idx, item in enumerate(available_types.items()):
app_type, icon_path = item[0], item[1]
icon = self.cache_type_filter_icons.get(app_type)
if not icon:
@@ -681,28 +713,48 @@ class ManageWindow(QWidget):
self.combo_filter_type.addItem(icon, app_type.capitalize(), app_type)
if keeping_selected and app_type == self.type_filter:
sel_type = idx + 1
self.combo_filter_type.blockSignals(True)
self.combo_filter_type.setCurrentIndex(sel_type if sel_type > -1 else 0)
self.combo_filter_type.blockSignals(False)
self.ref_combo_filter_type.setVisible(True)
else:
self.ref_combo_filter_type.setVisible(False)
def _update_categories(self, categories: Set[str] = None):
def _update_categories(self, categories: Set[str] = None, keep_selected: bool = False):
if categories is None:
self.ref_combo_categories.setVisible(self.combo_categories.count() > 1)
else:
self.category_filter = self.any_category_filter
keeping_selected = keep_selected and categories and self.category_filter in categories
if not keeping_selected:
self.category_filter = self.any_category_filter
if categories:
if self.combo_categories.count() > 1:
for _ in range(self.combo_categories.count() - 1):
self.combo_categories.removeItem(1)
for c in categories:
selected_cat = -1
cat_list = list(categories)
cat_list.sort()
for idx, c in enumerate(cat_list):
i18n_cat = self.i18n.get(c)
cat_label = i18n_cat if i18n_cat else c
self.combo_categories.addItem(cat_label.capitalize(), c)
if keeping_selected and c == self.category_filter:
selected_cat = idx + 1
self.combo_categories.blockSignals(True)
self.combo_categories.setCurrentIndex(selected_cat if selected_cat > -1 else 0)
self.combo_categories.blockSignals(False)
self.ref_combo_categories.setVisible(True)
else:
self.ref_combo_categories.setVisible(False)
@@ -809,7 +861,7 @@ class ManageWindow(QWidget):
else:
self.combo_filter_type.setEnabled(False)
def finish_action(self):
def finish_action(self, keep_filters: bool = False):
self.ref_combo_styles.setVisible(True)
self.thread_animate_progress.stop = True
self.thread_animate_progress.wait(msecs=1000)
@@ -835,8 +887,8 @@ class ManageWindow(QWidget):
if self.pkgs:
self.ref_input_name_filter.setVisible(True)
self.update_bt_upgrade()
self._update_type_filters()
self._update_categories()
self._update_type_filters(keep_selected=keep_filters)
self._update_categories(keep_selected=keep_filters)
if self.ref_bt_installed.isVisible():
self.ref_bt_installed.setEnabled(True)

View File

@@ -38,6 +38,7 @@ manage_window.checkbox.show_details=Show details
popup.root.title=Requires your password to continue
popup.root.bad_password.title=Error
popup.root.bad_password.body=Wrong password
popup.root.bad_password.last_try=Attempts finished. Action cancelled.
popup.history.title=History
popup.history.selected.tooltip=Current version
popup.button.yes=Yes
@@ -140,6 +141,9 @@ filetransfer=file transfer
packagemanager=package manager
sportsgame=sports game
arcadegame=arcade game
player=player
screenshots,download.running=downloading image
screenshots.download.no_content=No content to display
screenshots.download.no_response=Image not found
screenshots.download.no_response=Image not found
continue=continue
stable=stable

View File

@@ -39,6 +39,7 @@ manage_window.checkbox.show_details=Mostrar detalles
popup.root.title=Requiere tu contraseña para continuar
popup.root.bad_password.title=Error
popup.root.bad_password.body=Contraseña incorrecta
popup.root.bad_password.last_try=Intentos finalizados. Acción cancelada.
popup.history.title=Historia
popup.history.selected.tooltip=Versión actual
popup.button.yes=Sí
@@ -181,8 +182,12 @@ filetransfer=transferencia de archivos
packagemanager=administrador de paquetes
sportsgame=juego de deportes
arcadegame=juego arcade
player=jugador
fonts=fuentes
screenshots.bt_next.label=próxima
screenshots.bt_back.label=anterior
screenshots,download.running=descargando imagen
screenshots.download.no_content=No hay contenido para mostrar
screenshots.download.no_response=Imagen no encontrada
screenshots.download.no_response=Imagen no encontrada
continue=continuar
stable=estable

View File

@@ -39,6 +39,7 @@ manage_window.checkbox.show_details=Mostrar detalhes
popup.root.title=Requer sua senha para prosseguir
popup.root.bad_password.title=Erro
popup.root.bad_password.body=Senha incorreta
popup.root.bad_password.last_try=Tentativas finalizadas. Ação cancelada.
popup.history.title=Histórico
popup.history.selected.tooltip=Versão atual
popup.button.yes=Sim
@@ -184,8 +185,12 @@ viewer=visualizador
packagemanager=gerenciador de pacotes
sportsgame=jogo de esportes
arcadegame=jogo arcade
player=jogador
fonts=fontes
screenshots.bt_next.label=próxima
screenshots.bt_back.label=anterior
screenshots,download.running=baixando imagem
screenshots.download.no_content=Sem conteúdo para exibir
screenshots.download.no_response=Imagem não encontrada
screenshots.download.no_response=Imagem não encontrada
continue=continuar
stable=estável

View File

@@ -3,7 +3,7 @@ import os
from setuptools import setup, find_packages
DESCRIPTION = (
"Graphical user interface to manage Flatpak / Snap applications."
"Graphical interface to manage Flatpak, Snap, AppImage and AUR packages"
)
AUTHOR = "Vinicius Moreira"

View File

@@ -24,6 +24,9 @@ class ArchDataMapperTest(TestCase):
self.assertFalse(ArchDataMapper.check_update('77.0.3865.120-1', '77.0.3865.90-1'))
self.assertTrue(ArchDataMapper.check_update('77.0.3865.a-1', '77.0.3865.b-1'))
self.assertFalse(ArchDataMapper.check_update('77.0.b.0-1', '77.0.a.1-1'))
self.assertFalse(ArchDataMapper.check_update('r25.e22697c-1', 'r8.19fe011-1'))
self.assertFalse(ArchDataMapper.check_update('1.1.0.r11.caacf30-1', 'r65.4c7144a-1'))
self.assertFalse(ArchDataMapper.check_update('1.2.16.r688.8b2c199-1', 'r2105.e91f0e9-3'))
def test_check_update_no_suffix_3_x_2_digits(self):
self.assertTrue(ArchDataMapper.check_update('1.0.0-1', '1.1-1'))
@@ -35,97 +38,97 @@ class ArchDataMapperTest(TestCase):
def test_check_update_release(self):
# RE
self.assertTrue(ArchDataMapper.check_update('1.0.0.R-1', '1.0.0.R-2'))
self.assertFalse(ArchDataMapper.check_update('1.0.0.R-2', '1.0.0.R-1'))
self.assertFalse(ArchDataMapper.check_update('1.0.1.R-1', '1.0.0.R-5'))
self.assertTrue(ArchDataMapper.check_update('1.0.1.R-1', '1.0.1.R-2'))
self.assertFalse(ArchDataMapper.check_update('1.0.2.R-1', '1.0.1.R-7'))
self.assertTrue(ArchDataMapper.check_update('1.0.0.R-1', '1.0.0.R-2', check_suffix=True))
self.assertFalse(ArchDataMapper.check_update('1.0.0.R-2', '1.0.0.R-1', check_suffix=True))
self.assertFalse(ArchDataMapper.check_update('1.0.1.R-1', '1.0.0.R-5', check_suffix=True))
self.assertTrue(ArchDataMapper.check_update('1.0.1.R-1', '1.0.1.R-2', check_suffix=True))
self.assertFalse(ArchDataMapper.check_update('1.0.2.R-1', '1.0.1.R-7', check_suffix=True))
self.assertTrue(ArchDataMapper.check_update('1.0.0.RE-1', '1.0.0.R-2'))
self.assertTrue(ArchDataMapper.check_update('1.0.0.RELEASE-2', '1.0.0.R-5'))
self.assertFalse(ArchDataMapper.check_update('1.0.1.R-1', '1.0.0.RELEASE-5'))
self.assertFalse(ArchDataMapper.check_update('1.0.1.R-1', '1.0.0.RE-1'))
self.assertTrue(ArchDataMapper.check_update('1.0.0.RE-1', '1.0.0.R-2', check_suffix=True))
self.assertTrue(ArchDataMapper.check_update('1.0.0.RELEASE-2', '1.0.0.R-5', check_suffix=True))
self.assertFalse(ArchDataMapper.check_update('1.0.1.R-1', '1.0.0.RELEASE-5', check_suffix=True))
self.assertFalse(ArchDataMapper.check_update('1.0.1.R-1', '1.0.0.RE-1', check_suffix=True))
# GA
self.assertFalse(ArchDataMapper.check_update('1.0.0.R-1', '1.0.0.Ge-1'))
self.assertFalse(ArchDataMapper.check_update('1.0.0.RE-1', '1.0.0.GA-1'))
self.assertFalse(ArchDataMapper.check_update('1.0.0.RELEASE-1', '1.0.0.Ge-1'))
self.assertFalse(ArchDataMapper.check_update('1.0.0.R-1', '1.0.0.GE-1'))
self.assertFalse(ArchDataMapper.check_update('1.0.0.RE-1', '1.0.0.GE-1'))
self.assertFalse(ArchDataMapper.check_update('1.0.0.RELEASE-1', '1.0.0.GE-1'))
self.assertFalse(ArchDataMapper.check_update('1.0.0.R-1', '1.0.0.GA-1'))
self.assertFalse(ArchDataMapper.check_update('1.0.0.RE-1', '1.0.0.GA-1'))
self.assertFalse(ArchDataMapper.check_update('1.0.0.RELEASE-1', '1.0.0.GA-1'))
self.assertFalse(ArchDataMapper.check_update('1.0.0.R-1', '1.0.0.Ge-1', check_suffix=True))
self.assertFalse(ArchDataMapper.check_update('1.0.0.RE-1', '1.0.0.GA-1', check_suffix=True))
self.assertFalse(ArchDataMapper.check_update('1.0.0.RELEASE-1', '1.0.0.Ge-1', check_suffix=True))
self.assertFalse(ArchDataMapper.check_update('1.0.0.R-1', '1.0.0.GE-1', check_suffix=True))
self.assertFalse(ArchDataMapper.check_update('1.0.0.RE-1', '1.0.0.GE-1', check_suffix=True))
self.assertFalse(ArchDataMapper.check_update('1.0.0.RELEASE-1', '1.0.0.GE-1', check_suffix=True))
self.assertFalse(ArchDataMapper.check_update('1.0.0.R-1', '1.0.0.GA-1', check_suffix=True))
self.assertFalse(ArchDataMapper.check_update('1.0.0.RE-1', '1.0.0.GA-1', check_suffix=True))
self.assertFalse(ArchDataMapper.check_update('1.0.0.RELEASE-1', '1.0.0.GA-1', check_suffix=True))
self.assertTrue(ArchDataMapper.check_update('1.0.0.R-5', '1.0.1.GA-1'))
self.assertTrue(ArchDataMapper.check_update('1.0.0.R-5', '1.0.1.GA-1', check_suffix=True))
# RCS
self.assertFalse(ArchDataMapper.check_update('1.0.0.R-1', '1.0.0.RC-1'))
self.assertFalse(ArchDataMapper.check_update('1.0.0.RE-1', '1.0.0.RC-1'))
self.assertFalse(ArchDataMapper.check_update('1.0.0.RELEASE-1', '1.0.0.RC-1'))
self.assertFalse(ArchDataMapper.check_update('1.0.0.R-1', '1.0.0.RC-1', check_suffix=True))
self.assertFalse(ArchDataMapper.check_update('1.0.0.RE-1', '1.0.0.RC-1', check_suffix=True))
self.assertFalse(ArchDataMapper.check_update('1.0.0.RELEASE-1', '1.0.0.RC-1', check_suffix=True))
self.assertTrue(ArchDataMapper.check_update('1.0.0.R-5', '1.0.1.RC-1'))
self.assertTrue(ArchDataMapper.check_update('1.0.0.R-5', '1.0.1.RC-1', check_suffix=True))
# BETA
self.assertFalse(ArchDataMapper.check_update('1.0.0.R-1', '1.0.0.B-2'))
self.assertFalse(ArchDataMapper.check_update('1.0.0.RE-1', '1.0.0.B-2'))
self.assertFalse(ArchDataMapper.check_update('1.0.0.RELEASE-1', '1.0.0.B-2'))
self.assertFalse(ArchDataMapper.check_update('1.0.0.R-1', '1.0.0.BETA-2'))
self.assertFalse(ArchDataMapper.check_update('1.0.0.RE-1', '1.0.0.BETA-2'))
self.assertFalse(ArchDataMapper.check_update('1.0.0.RELEASE-1', '1.0.0.BETA-2'))
self.assertFalse(ArchDataMapper.check_update('1.0.0.R-1', '1.0.0.B-2', check_suffix=True))
self.assertFalse(ArchDataMapper.check_update('1.0.0.RE-1', '1.0.0.B-2', check_suffix=True))
self.assertFalse(ArchDataMapper.check_update('1.0.0.RELEASE-1', '1.0.0.B-2', check_suffix=True))
self.assertFalse(ArchDataMapper.check_update('1.0.0.R-1', '1.0.0.BETA-2', check_suffix=True))
self.assertFalse(ArchDataMapper.check_update('1.0.0.RE-1', '1.0.0.BETA-2', check_suffix=True))
self.assertFalse(ArchDataMapper.check_update('1.0.0.RELEASE-1', '1.0.0.BETA-2', check_suffix=True))
self.assertTrue(ArchDataMapper.check_update('1.0.0.R-5', '1.0.1.BETA-1'))
self.assertTrue(ArchDataMapper.check_update('1.0.0.R-5', '1.0.1.BETA-1', check_suffix=True))
# ALPHA
self.assertFalse(ArchDataMapper.check_update('1.0.0.R-1', '1.0.0.Alpha-2'))
self.assertFalse(ArchDataMapper.check_update('1.0.0.RE-1', '1.0.0.ALPHA-2'))
self.assertFalse(ArchDataMapper.check_update('1.0.0.RELEASE-1', '1.0.0.ALPHA-2'))
self.assertFalse(ArchDataMapper.check_update('1.0.0.R-1', '1.0.0.ALPHA-2'))
self.assertFalse(ArchDataMapper.check_update('1.0.0.RE-1', '1.0.0.ALPHA-2'))
self.assertFalse(ArchDataMapper.check_update('1.0.0.RELEASE-1', '1.0.0.ALPHA-2'))
self.assertFalse(ArchDataMapper.check_update('1.0.0.R-1', '1.0.0.ALFA-2'))
self.assertFalse(ArchDataMapper.check_update('1.0.0.RE-1', '1.0.0.ALFA-2'))
self.assertFalse(ArchDataMapper.check_update('1.0.0.RELEASE-1', '1.0.0.ALFA-2'))
self.assertFalse(ArchDataMapper.check_update('1.0.0.R-1', '1.0.0.Alpha-2', check_suffix=True))
self.assertFalse(ArchDataMapper.check_update('1.0.0.RE-1', '1.0.0.ALPHA-2', check_suffix=True))
self.assertFalse(ArchDataMapper.check_update('1.0.0.RELEASE-1', '1.0.0.ALPHA-2', check_suffix=True))
self.assertFalse(ArchDataMapper.check_update('1.0.0.R-1', '1.0.0.ALPHA-2', check_suffix=True))
self.assertFalse(ArchDataMapper.check_update('1.0.0.RE-1', '1.0.0.ALPHA-2', check_suffix=True))
self.assertFalse(ArchDataMapper.check_update('1.0.0.RELEASE-1', '1.0.0.ALPHA-2', check_suffix=True))
self.assertFalse(ArchDataMapper.check_update('1.0.0.R-1', '1.0.0.ALFA-2', check_suffix=True))
self.assertFalse(ArchDataMapper.check_update('1.0.0.RE-1', '1.0.0.ALFA-2', check_suffix=True))
self.assertFalse(ArchDataMapper.check_update('1.0.0.RELEASE-1', '1.0.0.ALFA-2', check_suffix=True))
self.assertTrue(ArchDataMapper.check_update('1.0.0.R-5', '1.0.1.Alfa-1'))
self.assertTrue(ArchDataMapper.check_update('1.0.0.R-5', '1.0.1.Alfa-1', check_suffix=True))
# DEV
self.assertFalse(ArchDataMapper.check_update('1.0.0.R-1', '1.0.0.DeV-2'))
self.assertFalse(ArchDataMapper.check_update('1.0.0.RE-1', '1.0.0.DEV-2'))
self.assertFalse(ArchDataMapper.check_update('1.0.0.RELEASE-1', '1.0.0.DEVEL-2'))
self.assertFalse(ArchDataMapper.check_update('1.0.0.R-1', '1.0.0.DEV-2'))
self.assertFalse(ArchDataMapper.check_update('1.0.0.RE-1', '1.0.0.DEV-2'))
self.assertFalse(ArchDataMapper.check_update('1.0.0.RELEASE-1', '1.0.0.DEV-2'))
self.assertFalse(ArchDataMapper.check_update('1.0.0.R-1', '1.0.0.DeV-2', check_suffix=True))
self.assertFalse(ArchDataMapper.check_update('1.0.0.RE-1', '1.0.0.DEV-2', check_suffix=True))
self.assertFalse(ArchDataMapper.check_update('1.0.0.RELEASE-1', '1.0.0.DEVEL-2', check_suffix=True))
self.assertFalse(ArchDataMapper.check_update('1.0.0.R-1', '1.0.0.DEV-2', check_suffix=True))
self.assertFalse(ArchDataMapper.check_update('1.0.0.RE-1', '1.0.0.DEV-2', check_suffix=True))
self.assertFalse(ArchDataMapper.check_update('1.0.0.RELEASE-1', '1.0.0.DEV-2', check_suffix=True))
self.assertTrue(ArchDataMapper.check_update('1.0.0.R-5', '1.0.1.DEV-1'))
self.assertTrue(ArchDataMapper.check_update('1.0.0.R-5', '1.0.1.DEV-1', check_suffix=True))
def test_check_update_ga(self):
# GA
self.assertFalse(ArchDataMapper.check_update('1.0.0.GA-1', '1.0.0.GE-1'))
self.assertTrue(ArchDataMapper.check_update('1.0.0.GE-1', '1.0.0.GA-2'))
self.assertTrue(ArchDataMapper.check_update('1.0.0.GA-2', '1.0.1.GA-1'))
self.assertFalse(ArchDataMapper.check_update('1.1.0.GE-3', '1.0.6.GE-10'))
self.assertFalse(ArchDataMapper.check_update('1.0.0.GA-1', '1.0.0.GE-1', check_suffix=True))
self.assertTrue(ArchDataMapper.check_update('1.0.0.GE-1', '1.0.0.GA-2', check_suffix=True))
self.assertTrue(ArchDataMapper.check_update('1.0.0.GA-2', '1.0.1.GA-1', check_suffix=True))
self.assertFalse(ArchDataMapper.check_update('1.1.0.GE-3', '1.0.6.GE-10', check_suffix=True))
# RC
self.assertFalse(ArchDataMapper.check_update('1.0.0.GA-1', '1.0.0.RC-1'))
self.assertFalse(ArchDataMapper.check_update('1.0.0.GE-1', '1.0.0.RC-1'))
self.assertFalse(ArchDataMapper.check_update('1.0.0.GA-1', '1.0.0.RC-1'))
self.assertFalse(ArchDataMapper.check_update('1.0.0.GA-1', '1.0.0.RC-1', check_suffix=True))
self.assertFalse(ArchDataMapper.check_update('1.0.0.GE-1', '1.0.0.RC-1', check_suffix=True))
self.assertFalse(ArchDataMapper.check_update('1.0.0.GA-1', '1.0.0.RC-1', check_suffix=True))
self.assertTrue(ArchDataMapper.check_update('1.0.0.GE-1', '1.0.1.RC-1'))
self.assertTrue(ArchDataMapper.check_update('1.0.10.GA-10', '1.1.0.RC-1'))
self.assertFalse(ArchDataMapper.check_update('1.1.0.GE-1', '1.0.1.RC-1'))
self.assertTrue(ArchDataMapper.check_update('1.0.0.GE-1', '1.0.1.RC-1', check_suffix=True))
self.assertTrue(ArchDataMapper.check_update('1.0.10.GA-10', '1.1.0.RC-1', check_suffix=True))
self.assertFalse(ArchDataMapper.check_update('1.1.0.GE-1', '1.0.1.RC-1', check_suffix=True))
# BETA
self.assertFalse(ArchDataMapper.check_update('1.0.0.GA-1', '1.0.0.BETA-1'))
self.assertTrue(ArchDataMapper.check_update('1.0.0.GE-1', '1.0.1.BETA-1'))
self.assertFalse(ArchDataMapper.check_update('1.0.0.GA-1', '1.0.0.BETA-1', check_suffix=True))
self.assertTrue(ArchDataMapper.check_update('1.0.0.GE-1', '1.0.1.BETA-1', check_suffix=True))
# ALPHA
self.assertFalse(ArchDataMapper.check_update('1.0.0.GE-1', '1.0.0.ALPHA-1'))
self.assertTrue(ArchDataMapper.check_update('1.0.0.GA-1', '1.0.1.ALPHA-1'))
self.assertFalse(ArchDataMapper.check_update('1.0.0.GE-1', '1.0.0.ALPHA-1', check_suffix=True))
self.assertTrue(ArchDataMapper.check_update('1.0.0.GA-1', '1.0.1.ALPHA-1', check_suffix=True))
# DEV
self.assertFalse(ArchDataMapper.check_update('1.0.0.GA-1', '1.0.0.DEV-1'))
self.assertTrue(ArchDataMapper.check_update('1.0.0.GE-1', '1.0.1.DEV-1'))
self.assertFalse(ArchDataMapper.check_update('1.0.0.GA-1', '1.0.0.DEV-1', check_suffix=True))
self.assertTrue(ArchDataMapper.check_update('1.0.0.GE-1', '1.0.1.DEV-1', check_suffix=True))
def test_check_update_rc(self):
# RC
@@ -148,38 +151,38 @@ class ArchDataMapperTest(TestCase):
def test_check_update_beta(self):
# BETA
self.assertFalse(ArchDataMapper.check_update('1.0.0.BETA-1', '1.0.0.BETA-1'))
self.assertTrue(ArchDataMapper.check_update('1.0.0.BETA-1', '1.0.0.BETA-2'))
self.assertTrue(ArchDataMapper.check_update('1.0.0.BETA-2', '1.0.1.B-1'))
self.assertFalse(ArchDataMapper.check_update('1.1.0.B-3', '1.0.6.BETA-10'))
self.assertFalse(ArchDataMapper.check_update('1.0.0.BETA-1', '1.0.0.BETA-1', check_suffix=True))
self.assertTrue(ArchDataMapper.check_update('1.0.0.BETA-1', '1.0.0.BETA-2', check_suffix=True))
self.assertTrue(ArchDataMapper.check_update('1.0.0.BETA-2', '1.0.1.B-1', check_suffix=True))
self.assertFalse(ArchDataMapper.check_update('1.1.0.B-3', '1.0.6.BETA-10', check_suffix=True))
# ALPHA
self.assertFalse(ArchDataMapper.check_update('1.0.0.BETA-1', '1.0.0.ALPHA-1'))
self.assertTrue(ArchDataMapper.check_update('1.0.0.B-1', '1.0.1.ALPHA-1'))
self.assertFalse(ArchDataMapper.check_update('1.0.0.BETA-1', '1.0.0.ALPHA-1', check_suffix=True))
self.assertTrue(ArchDataMapper.check_update('1.0.0.B-1', '1.0.1.ALPHA-1', check_suffix=True))
# DEV
self.assertFalse(ArchDataMapper.check_update('1.0.0.BETA-1', '1.0.0.DEV-1'))
self.assertTrue(ArchDataMapper.check_update('1.0.0.B-1', '1.0.1.DEV-1'))
self.assertFalse(ArchDataMapper.check_update('1.0.0.BETA-1', '1.0.0.DEV-1', check_suffix=True))
self.assertTrue(ArchDataMapper.check_update('1.0.0.B-1', '1.0.1.DEV-1', check_suffix=True))
def test_check_update_alpha(self):
# ALPHA
self.assertFalse(ArchDataMapper.check_update('1.0.0.ALPHA-1', '1.0.0.ALPHA-1'))
self.assertFalse(ArchDataMapper.check_update('1.0.0.ALPHA-1', '1.0.0.ALFA-1'))
self.assertTrue(ArchDataMapper.check_update('1.0.0.ALFA-1', '1.0.0.ALPHA-2'))
self.assertTrue(ArchDataMapper.check_update('1.0.0.ALPHA-2', '1.0.1.ALFA-1'))
self.assertFalse(ArchDataMapper.check_update('1.1.0.ALFA-3', '1.0.6.ALFA-10'))
self.assertFalse(ArchDataMapper.check_update('1.0.0.ALPHA-1', '1.0.0.ALPHA-1', check_suffix=True))
self.assertFalse(ArchDataMapper.check_update('1.0.0.ALPHA-1', '1.0.0.ALFA-1', check_suffix=True))
self.assertTrue(ArchDataMapper.check_update('1.0.0.ALFA-1', '1.0.0.ALPHA-2', check_suffix=True))
self.assertTrue(ArchDataMapper.check_update('1.0.0.ALPHA-2', '1.0.1.ALFA-1', check_suffix=True))
self.assertFalse(ArchDataMapper.check_update('1.1.0.ALFA-3', '1.0.6.ALFA-10', check_suffix=True))
# DEV
self.assertFalse(ArchDataMapper.check_update('1.0.0.ALFA-1', '1.0.0.DEV-1'))
self.assertTrue(ArchDataMapper.check_update('1.0.0.ALPHA-1', '1.0.1.DEV-1'))
self.assertFalse(ArchDataMapper.check_update('1.0.0.ALFA-1', '1.0.0.DEV-1', check_suffix=True))
self.assertTrue(ArchDataMapper.check_update('1.0.0.ALPHA-1', '1.0.1.DEV-1', check_suffix=True))
def test_check_update_dev(self):
self.assertFalse(ArchDataMapper.check_update('1.0.0.DEV-1', '1.0.0.DEV-1'))
self.assertFalse(ArchDataMapper.check_update('1.0.0.DEV-1', '1.0.0.DEVEL-1'))
self.assertFalse(ArchDataMapper.check_update('1.0.0.DEVEL-1', '1.0.0.DEVELOPMENT-1'))
self.assertTrue(ArchDataMapper.check_update('1.0.0.DEV-1', '1.0.0.DEVEL-2'))
self.assertTrue(ArchDataMapper.check_update('1.0.0.DEVEL-2', '1.0.1.DEV-1'))
self.assertFalse(ArchDataMapper.check_update('1.1.0.DEV-3', '1.0.6.DEVELOPMENT-10'))
self.assertFalse(ArchDataMapper.check_update('1.0.0.DEV-1', '1.0.0.DEV-1', check_suffix=True))
self.assertFalse(ArchDataMapper.check_update('1.0.0.DEV-1', '1.0.0.DEVEL-1', check_suffix=True))
self.assertFalse(ArchDataMapper.check_update('1.0.0.DEVEL-1', '1.0.0.DEVELOPMENT-1', check_suffix=True))
self.assertTrue(ArchDataMapper.check_update('1.0.0.DEV-1', '1.0.0.DEVEL-2', check_suffix=True))
self.assertTrue(ArchDataMapper.check_update('1.0.0.DEVEL-2', '1.0.1.DEV-1', check_suffix=True))
self.assertFalse(ArchDataMapper.check_update('1.1.0.DEV-3', '1.0.6.DEVELOPMENT-10', check_suffix=True))
def test_check_update_unknown_suffix(self):
self.assertTrue(ArchDataMapper.check_update('1.0.0.BALL-1', '1.0.0.TAR-1'))

View File

@@ -0,0 +1,23 @@
import os
from unittest import TestCase
from bauh.gems.arch import pacman
FILE_DIR = os.path.dirname(os.path.abspath(__file__))
class PacmanTest(TestCase):
def test_list_ignored_packages(self):
ignored = pacman.list_ignored_packages(FILE_DIR + '/resources/pacman_ign_pkgs.conf')
self.assertIsNotNone(ignored)
self.assertEqual(2, len(ignored))
self.assertIn('google-chrome', ignored)
self.assertIn('firefox', ignored)
def test_list_ignored_packages__no_ignored_packages(self):
ignored = pacman.list_ignored_packages(FILE_DIR + '/resources/pacman.conf')
self.assertIsNotNone(ignored)
self.assertEqual(0, len(ignored))

View File

@@ -0,0 +1,98 @@
#
# /etc/pacman.conf
#
# See the pacman.conf(5) manpage for option and repository directives
#
# GENERAL OPTIONS
#
[options]
# The following paths are commented out with their default values listed.
# If you wish to use different paths, uncomment and update the paths.
#RootDir = /
#DBPath = /var/lib/pacman/
CacheDir = /var/cache/pacman/pkg/
#LogFile = /var/log/pacman.log
#GPGDir = /etc/pacman.d/gnupg/
#HookDir = /etc/pacman.d/hooks/
HoldPkg = pacman glibc manjaro-system
# If upgrades are available for these packages they will be asked for first
SyncFirst = manjaro-system archlinux-keyring manjaro-keyring
#XferCommand = /usr/bin/curl -C - -f %u > %o
#XferCommand = /usr/bin/wget --passive-ftp -c -O %o %u
#CleanMethod = KeepInstalled
#UseDelta = 0.7
Architecture = auto
# Pacman won't upgrade packages listed in IgnorePkg and members of IgnoreGroup
#IgnorePkg =
#IgnoreGroup =
#NoUpgrade =
#NoExtract =
# Misc options
#UseSyslog
#Color
#TotalDownload
# We cannot check disk space from within a chroot environment
CheckSpace
#VerbosePkgLists
# By default, pacman accepts packages signed by keys that its local keyring
# trusts (see pacman-key and its man page), as well as unsigned packages.
SigLevel = Required DatabaseOptional
LocalFileSigLevel = Optional
#RemoteFileSigLevel = Required
# NOTE: You must run `pacman-key --init` before first using pacman; the local
# keyring can then be populated with the keys of all official Manjaro Linux
# packagers with `pacman-key --populate archlinux manjaro`.
#
# REPOSITORIES
# - can be defined here or included from another file
# - pacman will search repositories in the order defined here
# - local/custom mirrors can be added here or in separate files
# - repositories listed first will take precedence when packages
# have identical names, regardless of version number
# - URLs will have $repo replaced by the name of the current repo
# - URLs will have $arch replaced by the name of the architecture
#
# Repository entries are of the format:
# [repo-name]
# Server = ServerName
# Include = IncludePath
#
# The header [repo-name] is crucial - it must be present and
# uncommented to enable the repo.
#
# The testing repositories are disabled by default. To enable, uncomment the
# repo name header and Include lines. You can add preferred servers immediately
# after the header, and they will be used before the default mirrors.
[core]
SigLevel = PackageRequired
Include = /etc/pacman.d/mirrorlist
[extra]
SigLevel = PackageRequired
Include = /etc/pacman.d/mirrorlist
[community]
SigLevel = PackageRequired
Include = /etc/pacman.d/mirrorlist
# If you want to run 32 bit applications on your x86_64 system,
# enable the multilib repositories as required here.
[multilib]
SigLevel = PackageRequired
Include = /etc/pacman.d/mirrorlist
# An example of a custom package repository. See the pacman manpage for
# tips on creating your own repositories.
#[custom]
#SigLevel = Optional TrustAll
#Server = file:///home/custompkgs

View File

@@ -0,0 +1,101 @@
#
# /etc/pacman.conf
#
# See the pacman.conf(5) manpage for option and repository directives
#
# GENERAL OPTIONS
#
[options]
# The following paths are commented out with their default values listed.
# If you wish to use different paths, uncomment and update the paths.
#RootDir = /
#DBPath = /var/lib/pacman/
CacheDir = /var/cache/pacman/pkg/
#LogFile = /var/log/pacman.log
#GPGDir = /etc/pacman.d/gnupg/
#HookDir = /etc/pacman.d/hooks/
HoldPkg = pacman glibc manjaro-system
# If upgrades are available for these packages they will be asked for first
SyncFirst = manjaro-system archlinux-keyring manjaro-keyring
#XferCommand = /usr/bin/curl -C - -f %u > %o
#XferCommand = /usr/bin/wget --passive-ftp -c -O %o %u
#CleanMethod = KeepInstalled
#UseDelta = 0.7
Architecture = auto
# Pacman won't upgrade packages listed in IgnorePkg and members of IgnoreGroup
#IgnorePkg =
#IgnoreGroup =
#NoUpgrade =
#NoExtract =
# Misc options
#UseSyslog
#Color
#TotalDownload
# We cannot check disk space from within a chroot environment
CheckSpace
#VerbosePkgLists
# By default, pacman accepts packages signed by keys that its local keyring
# trusts (see pacman-key and its man page), as well as unsigned packages.
SigLevel = Required DatabaseOptional
LocalFileSigLevel = Optional
#RemoteFileSigLevel = Required
# NOTE: You must run `pacman-key --init` before first using pacman; the local
# keyring can then be populated with the keys of all official Manjaro Linux
# packagers with `pacman-key --populate archlinux manjaro`.
#
# REPOSITORIES
# - can be defined here or included from another file
# - pacman will search repositories in the order defined here
# - local/custom mirrors can be added here or in separate files
# - repositories listed first will take precedence when packages
# have identical names, regardless of version number
# - URLs will have $repo replaced by the name of the current repo
# - URLs will have $arch replaced by the name of the architecture
#
# Repository entries are of the format:
# [repo-name]
# Server = ServerName
# Include = IncludePath
#
# The header [repo-name] is crucial - it must be present and
# uncommented to enable the repo.
#
# The testing repositories are disabled by default. To enable, uncomment the
# repo name header and Include lines. You can add preferred servers immediately
# after the header, and they will be used before the default mirrors.
[core]
SigLevel = PackageRequired
Include = /etc/pacman.d/mirrorlist
[extra]
SigLevel = PackageRequired
Include = /etc/pacman.d/mirrorlist
[community]
SigLevel = PackageRequired
Include = /etc/pacman.d/mirrorlist
# If you want to run 32 bit applications on your x86_64 system,
# enable the multilib repositories as required here.
[multilib]
SigLevel = PackageRequired
Include = /etc/pacman.d/mirrorlist
# An example of a custom package repository. See the pacman manpage for
# tips on creating your own repositories.
#[custom]
#SigLevel = Optional TrustAll
#Server = file:///home/custompkgs
ignorepkg=google-chrome
# ignorepkg=abc
ignorepkg = firefox

View File

View File

@@ -0,0 +1,20 @@
from unittest import TestCase
from bauh.gems.flatpak.worker import FlatpakAsyncDataLoader
class FlatpakAsyncDataLoaderTest(TestCase):
def test_format_category(self):
self.assertEqual('irc client', FlatpakAsyncDataLoader.format_category('IRCClient'))
self.assertEqual('text editor', FlatpakAsyncDataLoader.format_category('TextEditor'))
self.assertEqual('text editor', FlatpakAsyncDataLoader.format_category('Text Editor'))
self.assertEqual('text editor', FlatpakAsyncDataLoader.format_category('text editor'))
self.assertEqual('text editor', FlatpakAsyncDataLoader.format_category('Text editor'))
self.assertEqual('text editor', FlatpakAsyncDataLoader.format_category('text Editor'))
self.assertEqual('text editor', FlatpakAsyncDataLoader.format_category('textEditor'))
self.assertEqual('ide', FlatpakAsyncDataLoader.format_category('IDE'))
self.assertEqual('faster irc client', FlatpakAsyncDataLoader.format_category('Faster IRCClient'))
self.assertEqual('3d graphics', FlatpakAsyncDataLoader.format_category('3DGraphics'))
self.assertEqual('32d graphics', FlatpakAsyncDataLoader.format_category('32DGraphics'))
self.assertEqual('d32 graphics', FlatpakAsyncDataLoader.format_category('D32Graphics'))