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/). 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 ## [0.7.0] 2019-10-18
### Features ### Features
- AppImage support ( see below ) - AppImage support ( see below )

View File

@@ -2,3 +2,4 @@ include LICENSE CHANGELOG.md README.md
include requirements.txt include requirements.txt
recursive-include bauh/view/resources * recursive-include bauh/view/resources *
recursive-include bauh/gems/*/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' __app_name__ = 'bauh'
import os import os

View File

@@ -213,8 +213,9 @@ class SoftwareManager(ABC):
pass pass
@abstractmethod @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 :return: a list of warnings to be shown to the user
""" """
pass 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): 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_) 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.type = type_
self.label = label self.label = label
self.options = options 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: if self.watcher:
self.watcher.print(msg) 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') self._notify_watcher(' '.join(process.subproc.args) + '\n')
already_succeeded = False already_succeeded = False
@@ -126,6 +126,9 @@ class ProcessHandler:
if line: if line:
self._notify_watcher(line) self._notify_watcher(line)
if error_output is not None:
error_output.write(line)
if process.check_error_output: if process.check_error_output:
if process.wrong_error_phrase and process.wrong_error_phrase in line: if process.wrong_error_phrase and process.wrong_error_phrase in line:
continue 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.abstract.view import MessageType
from bauh.api.constants import HOME_PATH from bauh.api.constants import HOME_PATH
from bauh.commons.html import bold 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 import query, INSTALLATION_PATH, suggestions, db
from bauh.gems.appimage.model import AppImage from bauh.gems.appimage.model import AppImage
from bauh.gems.appimage.worker import DatabaseUpdater 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))) watcher.change_substatus(self.i18n['appimage.install.extract'].format(bold(file_name)))
try: 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: except:
watcher.show_message(title=self.i18n['error'], watcher.show_message(title=self.i18n['error'],
body=traceback.format_exc(), body=traceback.format_exc(),
type_=MessageType.ERROR) type_=MessageType.ERROR)
traceback.print_exc() traceback.print_exc()
handler.handle(SystemProcess(new_subprocess(['rm', '-rf', out_dir])))
return False return False
watcher.change_substatus(self.i18n['appimage.install.desktop_entry']) watcher.change_substatus(self.i18n['appimage.install.desktop_entry'])
@@ -388,7 +396,7 @@ class AppImageManager(SoftwareManager):
return updates return updates
def list_warnings(self) -> List[str]: def list_warnings(self, internet_available: bool) -> List[str]:
pass pass
def list_suggestions(self, limit: int) -> List[PackageSuggestion]: def list_suggestions(self, limit: int) -> List[PackageSuggestion]:

View File

@@ -16,3 +16,4 @@ 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.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.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

@@ -16,3 +16,4 @@ 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.uninstall_current_version=No fue posible desinstalar la versión actual de {}
appimage.downgrade.install_version=No fue posible instalar la versión {} ({}) 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

@@ -16,3 +16,4 @@ appimage.downgrade.first_version={} se encontra em sua primeira versão publicad
appimage.downgrade.uninstall_current_version=Não foi possível desinstalar a versão atual de {} 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.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 pathlib import Path
from threading import Thread from threading import Thread
import requests
from bauh.api.http import HttpClient from bauh.api.http import HttpClient
from bauh.gems.appimage import LOCAL_PATH, db 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): def run(self):
if self.enabled: if self.enabled:
while True: while True:
try:
self._download_databases() self._download_databases()
except requests.exceptions.ConnectionError:
self.logger.warning('The internet connection seems to be off.')
self.logger.info('Sleeping') self.logger.info('Sleeping')
time.sleep(self.sleep) time.sleep(self.sleep)
else: else:

View File

@@ -1,4 +1,10 @@
import os import os
from bauh.api.constants import CACHE_PATH
ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
BUILD_DIR = '/tmp/bauh/aur' 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 import re
from typing import Set, List from typing import Set, List
import requests
from bauh.api.http import HttpClient from bauh.api.http import HttpClient
import urllib.parse
URL_INFO = 'https://aur.archlinux.org/rpc/?v=5&type=info&' URL_INFO = 'https://aur.archlinux.org/rpc/?v=5&type=info&'
URL_SRC_INFO = 'https://aur.archlinux.org/cgit/aur.git/plain/.SRCINFO?h=' 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 [] return res['results'] if res and res.get('results') else []
def get_src_info(self, name: str) -> dict: 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: if res and res.text:
info = {} info = {}
@@ -48,4 +47,4 @@ class AURClient:
return info return info
def _map_names_as_queries(self, names) -> str: 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.handler import ProcessWatcher
from bauh.api.abstract.model import PackageUpdate, PackageHistory, SoftwarePackage, PackageSuggestion, PackageStatus from bauh.api.abstract.model import PackageUpdate, PackageHistory, SoftwarePackage, PackageSuggestion, PackageStatus
from bauh.api.abstract.view import MessageType from bauh.api.abstract.view import MessageType
from bauh.commons.category import CategoriesDownloader
from bauh.commons.html import bold from bauh.commons.html import bold
from bauh.commons.system import SystemProcess, ProcessHandler, new_subprocess, run_cmd, new_root_subprocess, \ from bauh.commons.system import SystemProcess, ProcessHandler, new_subprocess, run_cmd, new_root_subprocess, \
SimpleProcess 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.aur import AURClient
from bauh.gems.arch.mapper import ArchDataMapper from bauh.gems.arch.mapper import ArchDataMapper
from bauh.gems.arch.model import ArchPackage 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_GIT = 'https://aur.archlinux.org/{}.git'
URL_PKG_DOWNLOAD = 'https://aur.archlinux.org/cgit/aur.git/snapshot/{}.tar.gz' 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') SOURCE_FIELDS = ('source', 'source_x86_64')
RE_PRE_DOWNLOADABLE_FILES = re.compile(r'(https?|ftp)://.+\.\w+[^gpg|git]$') 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): class ArchManager(SoftwareManager):
@@ -51,11 +59,12 @@ class ArchManager(SoftwareManager):
self.logger = context.logger self.logger = context.logger
self.enabled = True self.enabled = True
self.arch_distro = context.distro == 'arch' self.arch_distro = context.distro == 'arch'
self.categories_downloader = CategoriesDownloader(context.http_client, context.logger) self.categories_mapper = CategoriesDownloader('AUR', context.http_client, context.logger, self, self.context.disk_cache,
self.categories_map = {} 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): 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 app.downgrade_enabled = downgrade_enabled
if app.installed: if app.installed:
@@ -66,7 +75,7 @@ class ArchManager(SoftwareManager):
else: else:
res.new.append(app) 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: def search(self, words: str, disk_loader: DiskCacheLoader, limit: int = -1) -> SearchResult:
self.comp_optimizer.join() self.comp_optimizer.join()
@@ -75,10 +84,12 @@ class ArchManager(SoftwareManager):
res = SearchResult([], [], 0) res = SearchResult([], [], 0)
installed = {} 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() 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'): if api_res and api_res.get('results'):
read_installed.join() read_installed.join()
@@ -117,9 +128,8 @@ class ArchManager(SoftwareManager):
if pkgsinfo: if pkgsinfo:
for pkgdata in 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.downgrade_enabled = downgrade_enabled
pkg.categories = self.categories_map.get(pkg.name)
if disk_loader: if disk_loader:
disk_loader.fill(pkg) disk_loader.fill(pkg)
@@ -137,7 +147,7 @@ class ArchManager(SoftwareManager):
latest_version=data.get('version'), description=data.get('description'), latest_version=data.get('version'), description=data.get('description'),
installed=True, mirror='aur') installed=True, mirror='aur')
pkg.categories = self.categories_map.get(pkg.name) pkg.categories = self.categories.get(pkg.name)
pkg.downgrade_enabled = downgrade_enabled pkg.downgrade_enabled = downgrade_enabled
if disk_loader: if disk_loader:
@@ -161,6 +171,7 @@ class ArchManager(SoftwareManager):
apps = [] apps = []
if installed and installed['not_signed']: if installed and installed['not_signed']:
self.dcache_updater.join() self.dcache_updater.join()
self.categories_mapper.join()
self._fill_aur_pkgs(installed['not_signed'], apps, disk_loader, internet_available) 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']) handler.watcher.print(self.i18n['action.cancelled'])
return False 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 return True
def _install_optdeps(self, pkgname: str, root_password: str, handler: ProcessHandler, pkgdir: str, change_progress: bool = True) -> bool: 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: if installed and self.context.disk_cache:
handler.watcher.change_substatus(self.i18n['status.caching_data'].format(bold(pkgname))) handler.watcher.change_substatus(self.i18n['status.caching_data'].format(bold(pkgname)))
if self.context.disk_cache: 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) self._update_progress(handler.watcher, 100, change_progress)
@@ -687,16 +704,13 @@ class ArchManager(SoftwareManager):
self.dcache_updater.start() self.dcache_updater.start()
self.comp_optimizer.start() self.comp_optimizer.start()
self.aur_index_updater.start() self.aur_index_updater.start()
categories = self.categories_downloader.get_categories() self.categories_mapper.start()
if categories:
self.categories_map = categories
def list_updates(self, internet_available: bool) -> List[PackageUpdate]: def list_updates(self, internet_available: bool) -> List[PackageUpdate]:
installed = self.read_installed(disk_loader=None, internet_available=internet_available).installed 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] 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 = [] warnings = []
if self.arch_distro: if self.arch_distro:
@@ -725,9 +739,10 @@ class ArchManager(SoftwareManager):
api_res = self.aur_client.get_info(sug_names) api_res = self.aur_client.get_info(sug_names)
if api_res: if api_res:
self.categories_mapper.join()
for pkg in api_res: for pkg in api_res:
if pkg.get('Name') in sug_names: 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 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): 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: for path in icon_paths:
if ends_with.match(path): if ends_with.match(path):
@@ -47,7 +47,7 @@ def set_icon_path(app: ArchPackage, icon_name: str = None):
break 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))} 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) 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: if to_write:
for p in to_write: for p in to_write:
if categories:
p.categories = categories.get(p.name)
p.maintainer = maintainer p.maintainer = maintainer
write(p) write(p)
return len(to_write) return len(to_write)
return 0 return 0

View File

@@ -18,4 +18,7 @@ def check(pkgdir: str, handler: ProcessHandler) -> dict:
if gpg_keys: if gpg_keys:
res['gpg_key'] = gpg_keys[0] 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 return res

View File

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

View File

@@ -2,12 +2,10 @@ import datetime
from typing import List from typing import List
from bauh.api.abstract.model import SoftwarePackage from bauh.api.abstract.model import SoftwarePackage
from bauh.api.constants import CACHE_PATH
from bauh.commons import resource 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', 'categories'}
CACHED_ATTRS = {'command', 'icon_path', 'mirror', 'maintainer', 'desktop_entry'}
class ArchPackage(SoftwarePackage): class ArchPackage(SoftwarePackage):
@@ -35,7 +33,7 @@ class ArchPackage(SoftwarePackage):
@staticmethod @staticmethod
def disk_cache_path(pkgname: str, mirror: str): 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): def get_pkg_build_url(self):
if self.package_base: if self.package_base:

View File

@@ -1,4 +1,5 @@
import re import re
from threading import Thread
from typing import List, Set from typing import List, Set
from bauh.commons.system import run_cmd, new_subprocess, new_root_subprocess, SystemProcess 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: def get_mirrors(pkgs: Set[str]) -> dict:
pkgre = '|'.join(pkgs) pkgre = '|'.join(pkgs).replace('+', r'\+').replace('.', r'\.')
searchres = new_subprocess(['pacman', '-Ss', pkgre]).stdout searchres = new_subprocess(['pacman', '-Ss', pkgre]).stdout
mirrors = {} mirrors = {}
@@ -71,10 +72,18 @@ def check_installed(pkg: str) -> bool:
return bool(res) 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 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 installed = new_subprocess(['pacman', '-Qq']).stdout # retrieving all installed package names
allinfo = new_subprocess(['pacman', '-Qi'], stdin=installed).stdout # retrieving all installed packages info 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': {}}, {} pkgs, current_pkg = {'mirrors': {}, 'not_signed': {}}, {}
for out in new_subprocess(['grep', '-E', '(Name|Description|Version|Validated By)'], for out in new_subprocess(['grep', '-E', '(Name|Description|Version|Validated By)'],
stdin=allinfo).stdout: # filtering only the Name and Validated By fields: 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 = {} 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 return pkgs
@@ -124,7 +145,7 @@ def list_icon_paths(pkgnames: Set[str]) -> List[str]:
installed_files = new_subprocess(['pacman', '-Qlq', *pkgnames]) installed_files = new_subprocess(['pacman', '-Qlq', *pkgnames])
icon_files = [] 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: if out:
line = out.decode().strip() line = out.decode().strip()
if line: if line:
@@ -179,3 +200,19 @@ def receive_key(key: str, root_password: str) -> SystemProcess:
def sign_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) 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

@@ -61,3 +61,5 @@ 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.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.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

@@ -93,3 +93,5 @@ 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.status=Recibiendo la clave pública {}
arch.aur.install.unknown_key.receive_error=No fue posible recibir 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

@@ -93,3 +93,5 @@ 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.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.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 os
import re import re
import time import time
import traceback
from math import ceil from math import ceil
from multiprocessing import Process from multiprocessing import Process
from threading import Thread from threading import Thread
from typing import Dict, List
import requests import requests
from bauh.api.abstract.context import ApplicationContext from bauh.api.abstract.context import ApplicationContext
from bauh.api.abstract.controller import SoftwareManager from bauh.api.abstract.controller import SoftwareManager
from bauh.api.constants import HOME_PATH from bauh.api.constants import HOME_PATH
from bauh.api.http import HttpClient
from bauh.gems.arch import pacman, disk from bauh.gems.arch import pacman, disk
URL_INDEX = 'https://aur.archlinux.org/packages.gz' 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))) self.logger.info('Pre-indexed {} AUR package names in memory'.format(len(self.man.names_index)))
else: else:
self.logger.warning('No data returned from: {}'.format(URL_INDEX)) 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') self.logger.warning('No internet connection: could not pre-index packages')
time.sleep(60 * 20) # updates every 20 minutes 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)) self.logger.info("A custom optimized 'makepkg.conf' was generated at '{}'".format(HOME_PATH))
else: else:
self.logger.warning("A custom 'makepkg.conf' is already defined at '{}'".format(HOME_PATH)) 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 import traceback
from datetime import datetime from datetime import datetime
from threading import Thread from threading import Thread
@@ -24,6 +25,7 @@ class FlatpakManager(SoftwareManager):
super(FlatpakManager, self).__init__(context=context) super(FlatpakManager, self).__init__(context=context)
self.i18n = context.i18n self.i18n = context.i18n
self.api_cache = context.cache_factory.new() self.api_cache = context.cache_factory.new()
self.category_cache = context.cache_factory.new()
context.disk_loader_factory.map(FlatpakApplication, self.api_cache) context.disk_loader_factory.map(FlatpakApplication, self.api_cache)
self.enabled = True self.enabled = True
self.http_client = context.http_client self.http_client = context.http_client
@@ -45,7 +47,8 @@ class FlatpakManager(SoftwareManager):
disk_loader.fill(app) # preloading cached disk data disk_loader.fill(app) # preloading cached disk data
if internet: 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: else:
app.fill_cached_data(api_data) app.fill_cached_data(api_data)
@@ -247,7 +250,7 @@ class FlatpakManager(SoftwareManager):
return updates return updates
def list_warnings(self) -> List[str]: def list_warnings(self, internet_available: bool) -> List[str]:
if flatpak.is_installed(): if flatpak.is_installed():
if not flatpak.has_remotes_set(): if not flatpak.has_remotes_set():
return [self.i18n['flatpak.notification.no_remotes']] return [self.i18n['flatpak.notification.no_remotes']]
@@ -279,11 +282,20 @@ class FlatpakManager(SoftwareManager):
flatpak.run(str(pkg.id)) flatpak.run(str(pkg.id))
def get_screenshots(self, pkg: SoftwarePackage) -> List[str]: 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 = [] urls = []
try:
res = self.http_client.get_json(screenshots_url)
if res and res.get('screenshots'): if res and res.get('screenshots'):
for s in res['screenshots']: for s in res['screenshots']:
if s.get('imgDesktopUrl'): if s.get('imgDesktopUrl'):
urls.append(s['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 return urls

View File

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

View File

@@ -1,4 +1,5 @@
import traceback import traceback
from io import StringIO
from threading import Thread from threading import Thread
from bauh.api.abstract.cache import MemoryCache from bauh.api.abstract.cache import MemoryCache
@@ -12,7 +13,7 @@ from bauh.gems.flatpak.model import FlatpakApplication
class FlatpakAsyncDataLoader(Thread): 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) super(FlatpakAsyncDataLoader, self).__init__(daemon=True)
self.app = app self.app = app
self.manager = manager self.manager = manager
@@ -20,6 +21,21 @@ class FlatpakAsyncDataLoader(Thread):
self.api_cache = api_cache self.api_cache = api_cache
self.persist = False self.persist = False
self.logger = context.logger 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): def run(self):
if self.app: if self.app:
@@ -51,7 +67,17 @@ class FlatpakAsyncDataLoader(Thread):
self.app.icon_url = FLATHUB_URL + self.app.icon_url self.app.icon_url = FLATHUB_URL + self.app.icon_url
if data.get('categories'): 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() loaded_data = self.app.get_data_to_cache()

View File

@@ -1,2 +1,8 @@
import os import os
from bauh.api.constants import CACHE_PATH
ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) 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 import time
from datetime import datetime from datetime import datetime
from threading import Thread 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.handler import ProcessWatcher
from bauh.api.abstract.model import SoftwarePackage, PackageHistory, PackageUpdate, PackageSuggestion, \ from bauh.api.abstract.model import SoftwarePackage, PackageHistory, PackageUpdate, PackageSuggestion, \
SuggestionPriority 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.html import bold
from bauh.commons.system import SystemProcess, ProcessHandler from bauh.commons.system import SystemProcess, ProcessHandler, new_root_subprocess
from bauh.gems.snap import snap, suggestions 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.constants import SNAP_API_URL
from bauh.gems.snap.model import SnapApplication 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): class SnapManager(SoftwareManager):
@@ -28,7 +33,8 @@ class SnapManager(SoftwareManager):
self.logger = context.logger self.logger = context.logger
self.ubuntu_distro = context.distro == 'ubuntu' self.ubuntu_distro = context.distro == 'ubuntu'
self.categories = {} 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: def map_json(self, app_json: dict, installed: bool, disk_loader: DiskCacheLoader, internet: bool = True) -> SnapApplication:
app = SnapApplication(publisher=app_json.get('publisher'), app = SnapApplication(publisher=app_json.get('publisher'),
@@ -41,13 +47,27 @@ class SnapManager(SoftwareManager):
latest_version=app_json.get('version'), latest_version=app_json.get('version'),
description=app_json.get('description', app_json.get('summary'))) 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.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 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']) 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() 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: 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(): 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)] 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)) return SearchResult(installed, None, len(installed))
else: else:
@@ -125,12 +146,34 @@ class SnapManager(SoftwareManager):
raise Exception("'get_history' is not supported by {}".format(pkg.__class__.__name__)) raise Exception("'get_history' is not supported by {}".format(pkg.__class__.__name__))
def install(self, pkg: SnapApplication, root_password: str, watcher: ProcessWatcher) -> bool: 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 '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: if res:
pkg.has_apps_field = snap.has_apps_field(pkg.name, self.ubuntu_distro) pkg.has_apps_field = snap.has_apps_field(pkg.name, self.ubuntu_distro)
return res 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
def is_enabled(self) -> bool: def is_enabled(self) -> bool:
return self.enabled return self.enabled
@@ -148,20 +191,24 @@ class SnapManager(SoftwareManager):
return ProcessHandler(watcher).handle(SystemProcess(subproc=snap.refresh_and_stream(pkg.name, root_password))) return ProcessHandler(watcher).handle(SystemProcess(subproc=snap.refresh_and_stream(pkg.name, root_password)))
def prepare(self): def prepare(self):
categories = self.categories_downloader.get_categories() self.categories_downloader.start()
if categories:
self.categories = categories
def list_updates(self, internet_available: bool) -> List[PackageUpdate]: def list_updates(self, internet_available: bool) -> List[PackageUpdate]:
pass 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(): if snap.is_installed() and not snap.is_snapd_running():
snap_bold = bold('Snap') snap_bold = bold('Snap')
return [self.i18n['snap.notification.snapd_unavailable'].format(bold('snapd'), snap_bold), 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']))] 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]): 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)) 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) sugs.sort(key=lambda t: t[1].value, reverse=True)
threads = [] threads = []
self.categories_downloader.join()
for sug in sugs: for sug in sugs:
if limit <= 0 or len(res) < limit: 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, 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, 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, super(SnapApplication, self).__init__(id=id, name=name, version=version,
latest_version=latest_version, description=description) latest_version=latest_version, description=description)
self.publisher = publisher self.publisher = publisher
@@ -27,6 +27,7 @@ class SnapApplication(SoftwarePackage):
self.notes = notes self.notes = notes
self.confinement = confinement self.confinement = confinement
self.has_apps_field = has_apps_field self.has_apps_field = has_apps_field
self.verified_publisher = verified_publisher
def supports_disk_cache(self): def supports_disk_cache(self):
return self.installed return self.installed

View File

@@ -1,5 +1,8 @@
gem.snap.info=Applications published at https://snapcraft.io/store 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.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.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.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.info.size=tamaño
snap.notification.snapd_unavailable={} no parece estar instalado o habilitado. Los paquetes {} estarán indisponibles. 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.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.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.info.size=tamanho
snap.notification.snapd_unavailable={} não parece estar instalado ou habilitado. Os pacotes {} estarão indisponíveis. 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.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.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 logging
import re import re
import subprocess 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 from bauh.gems.snap.model import SnapApplication
BASE_CMD = 'snap' 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) 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 install_cmd = [BASE_CMD, 'install', app_name] # default
if confinement == 'classic': if confinement == 'classic':
install_cmd.append('--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: 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)) logger.error("No valid command found for '{}'".format(app_name))
else: else:
logger.error("No command found for '{}'".format(app_name)) 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 import traceback
from threading import Thread from threading import Thread
from typing import Dict, List
from bauh.api.abstract.cache import MemoryCache from bauh.api.abstract.cache import MemoryCache
from bauh.api.abstract.context import ApplicationContext from bauh.api.abstract.context import ApplicationContext
from bauh.api.abstract.controller import SoftwareManager from bauh.api.abstract.controller import SoftwareManager
from bauh.api.abstract.model import PackageStatus from bauh.api.abstract.model import PackageStatus
from bauh.api.http import HttpClient
from bauh.gems.snap import snap from bauh.gems.snap import snap
from bauh.gems.snap.constants import SNAP_API_URL from bauh.gems.snap.constants import SNAP_API_URL
from bauh.gems.snap.model import SnapApplication from bauh.gems.snap.model import SnapApplication
@@ -72,34 +68,3 @@ class SnapAsyncDataLoader(Thread):
if self.persist: if self.persist:
self.manager.cache_to_disk(pkg=self.app, icon_bytes=None, only_icon=False) 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 return updates
def list_warnings(self) -> List[str]: def list_warnings(self, internet_available: bool = None) -> List[str]:
warnings = [] warnings = []
if self.managers: if self.managers:
int_available = internet.is_available(self.context.http_client, self.context.logger)
for man in self.managers: for man in self.managers:
if man.is_enabled(): if man.is_enabled():
man_warnings = man.list_warnings() man_warnings = man.list_warnings(internet_available=int_available)
if man_warnings: if man_warnings:
if warnings is None: if warnings is None:

View File

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

View File

@@ -231,7 +231,7 @@ class AppsTable(QTableWidget):
col_update = None col_update = None
if update_check_enabled and pkg.model.update: 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) 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 pkgs_info['updates'] += 1
if pkgv.model.categories: 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['pkgs'].append(pkgv)
pkgs_info['not_installed'] += 1 if not pkgv.model.installed else 0 pkgs_info['not_installed'] += 1 if not pkgv.model.installed else 0

View File

@@ -67,7 +67,7 @@ class ComboBoxQt(QComboBox):
class RadioSelectQt(QGroupBox): class RadioSelectQt(QGroupBox):
def __init__(self, model: SingleSelectComponent): 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.model = model
self.setStyleSheet("QGroupBox { font-weight: bold }") self.setStyleSheet("QGroupBox { font-weight: bold }")

View File

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

View File

@@ -4,12 +4,13 @@ from typing import List
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt
from PyQt5.QtGui import QIcon, QPixmap 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.abstract.cache import MemoryCache
from bauh.api.http import HttpClient from bauh.api.http import HttpClient
from bauh.view.qt import qt_utils from bauh.view.qt import qt_utils
from bauh.view.qt.components import new_spacer from bauh.view.qt.components import new_spacer
from bauh.view.qt.thread import AnimateProgress
from bauh.view.qt.view_model import PackageView from bauh.view.qt.view_model import PackageView
@@ -28,6 +29,12 @@ class ScreenshotsDialog(QDialog):
self.resize(1280, 720) self.resize(1280, 720)
self.i18n = i18n self.i18n = i18n
self.http_client = http_client 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 # 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 = QLabel()
self.img_label.setStyleSheet('QLabel { font-weight: bold; text-align: center }') 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.bottom_bar.addWidget(new_spacer(50))
self.bt_next = QPushButton(self.i18n['screenshots.bt_next.label'].capitalize()) self.bt_next = QPushButton(self.i18n['screenshots.bt_next.label'].capitalize())
@@ -64,11 +73,14 @@ class ScreenshotsDialog(QDialog):
self.img_idx = 0 self.img_idx = 0
for idx, s in enumerate(self.screenshots): 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() t.start()
self._load_img() self._load_img()
def _update_progress(self, val: int):
self.progress_bar.setValue(val)
def _load_img(self): def _load_img(self):
if len(self.loaded_imgs) > self.img_idx: if len(self.loaded_imgs) > self.img_idx:
img = self.loaded_imgs[self.img_idx] img = self.loaded_imgs[self.img_idx]
@@ -79,9 +91,15 @@ class ScreenshotsDialog(QDialog):
else: else:
self.img_label.setText(img) self.img_label.setText(img)
self.img.setPixmap(QPixmap()) self.img.setPixmap(QPixmap())
self.thread_progress.stop = True
self.ref_progress_bar.setVisible(False)
self.ref_img_label.setVisible(True)
else: else:
self.img.setPixmap(QPixmap()) 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: if len(self.screenshots) == 1:
self.ref_bt_back.setVisible(False) self.ref_bt_back.setVisible(False)

View File

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

View File

@@ -19,8 +19,6 @@ from bauh.api.http import HttpClient
from bauh.commons.html import bold from bauh.commons.html import bold
from bauh.view.core.config import Configuration from bauh.view.core.config import Configuration
from bauh.view.core.controller import GenericSoftwareManager 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 import dialog, commons, qt_utils
from bauh.view.qt.about import AboutDialog from bauh.view.qt.about import AboutDialog
from bauh.view.qt.apps_table import AppsTable, UpdateToggleButton 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.history import HistoryDialog
from bauh.view.qt.info import InfoDialog from bauh.view.qt.info import InfoDialog
from bauh.view.qt.root import is_root, ask_root_password 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.styles import StylesComboBox
from bauh.view.qt.thread import UpdateSelectedApps, RefreshApps, UninstallApp, DowngradeApp, GetAppInfo, \ from bauh.view.qt.thread import UpdateSelectedApps, RefreshApps, UninstallApp, DowngradeApp, GetAppInfo, \
GetAppHistory, SearchPackages, InstallPackage, AnimateProgress, VerifyModels, FindSuggestions, ListWarnings, \ GetAppHistory, SearchPackages, InstallPackage, AnimateProgress, VerifyModels, FindSuggestions, ListWarnings, \
AsyncAction, LaunchApp, ApplyFilters, CustomAction, GetScreenshots AsyncAction, LaunchApp, ApplyFilters, CustomAction, GetScreenshots
from bauh.view.qt.view_model import PackageView from bauh.view.qt.view_model import PackageView
from bauh.view.qt.view_utils import load_icon from bauh.view.qt.view_utils import load_icon
from bauh.view.util import util, resource
DARK_ORANGE = '#FF4500' DARK_ORANGE = '#FF4500'
@@ -283,6 +283,7 @@ class ManageWindow(QWidget):
self._maximized = False self._maximized = False
self.progress_controll_enabled = True self.progress_controll_enabled = True
self.recent_installation = False self.recent_installation = False
self.recent_uninstall = False
self.dialog_about = None self.dialog_about = None
self.first_refresh = suggestions self.first_refresh = suggestions
@@ -314,6 +315,13 @@ class ManageWindow(QWidget):
self._update_table(pkgs_info=pkgs_info, signal=True) self._update_table(pkgs_info=pkgs_info, signal=True)
self.update_bt_upgrade(pkgs_info) 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): def _finish_apply_filters_async(self, success: bool):
self.label_status.setText('') self.label_status.setText('')
self.ref_toolbar_search.setVisible(True) self.ref_toolbar_search.setVisible(True)
@@ -400,6 +408,21 @@ class ManageWindow(QWidget):
def _notify_model_data_change(self): def _notify_model_data_change(self):
self.table_apps.fill_async_data() 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): def changeEvent(self, e: QEvent):
if isinstance(e, QWindowStateChangeEvent): if isinstance(e, QWindowStateChangeEvent):
self._maximized = self.isMaximized() 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): def refresh_apps(self, keep_console: bool = True, top_app: PackageView = None, pkg_types: Set[Type[SoftwarePackage]] = None):
self.recent_installation = False self.recent_installation = False
self.type_filter = None
self.input_search.clear() self.input_search.clear()
if not keep_console: if not keep_console:
@@ -439,7 +461,7 @@ class ManageWindow(QWidget):
self.ref_checkbox_updates.setVisible(False) self.ref_checkbox_updates.setVisible(False)
self.ref_checkbox_only_apps.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.app = top_app # the app will be on top when refresh happens
self.thread_refresh.pkg_types = pkg_types self.thread_refresh.pkg_types = pkg_types
@@ -449,8 +471,9 @@ class ManageWindow(QWidget):
self.finish_action() self.finish_action()
self.ref_checkbox_only_apps.setVisible(bool(res['installed'])) self.ref_checkbox_only_apps.setVisible(bool(res['installed']))
self.ref_bt_upgrade.setVisible(True) 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.first_refresh = False
self.recent_uninstall = False
self._hide_fields_after_recent_installation() self._hide_fields_after_recent_installation()
def uninstall_app(self, app: PackageView): def uninstall_app(self, app: PackageView):
@@ -464,7 +487,7 @@ class ManageWindow(QWidget):
return return
self._handle_console_option(True) 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.app = app
self.thread_uninstall.root_password = pwd 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'])) 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 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) self.refresh_apps(pkg_types={pkgv.model.__class__} if only_pkg_type else None)
else: else:
if self._can_notify_user(): if self._can_notify_user():
@@ -547,7 +571,7 @@ class ManageWindow(QWidget):
self.ref_bt_upgrade.setVisible(show_bt_upgrade) 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) self.update_bt_upgrade(pkgs_info)
if pkgs_info['updates'] > 0: if pkgs_info['updates'] > 0:
@@ -556,12 +580,13 @@ class ManageWindow(QWidget):
if not self.ref_checkbox_updates.isVisible(): if not self.ref_checkbox_updates.isVisible():
self.ref_checkbox_updates.setVisible(True) 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) 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) self._change_checkbox(self.checkbox_only_apps, False, 'filter_only_apps', trigger_filters)
else: else:
if not keep_selected:
self._change_checkbox(self.checkbox_updates, False, 'filter_updates', trigger_filters) self._change_checkbox(self.checkbox_updates, False, 'filter_updates', trigger_filters)
self.ref_checkbox_updates.setVisible(False) self.ref_checkbox_updates.setVisible(False)
@@ -586,7 +611,7 @@ class ManageWindow(QWidget):
'display_limit': self.display_limit if updates <= 0 else None '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('') self.input_name_filter.setText('')
pkgs_info = commons.new_pkgs_info() pkgs_info = commons.new_pkgs_info()
filters = self._gen_filters(ignore_updates) filters = self._gen_filters(ignore_updates)
@@ -620,15 +645,19 @@ class ManageWindow(QWidget):
self.thread_suggestions.start() self.thread_suggestions.start()
return return
else: else:
if not keep_filters:
self._change_checkbox(self.checkbox_only_apps, False, 'filter_only_apps', trigger=False) self._change_checkbox(self.checkbox_only_apps, False, 'filter_only_apps', trigger=False)
self.checkbox_only_apps.setCheckable(False) self.checkbox_only_apps.setCheckable(False)
else: else:
if not keep_filters:
self.checkbox_only_apps.setCheckable(True) self.checkbox_only_apps.setCheckable(True)
self._change_checkbox(self.checkbox_only_apps, True, 'filter_only_apps', trigger=False) 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._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'] self.pkgs_available = pkgs_info['pkgs']
@@ -640,9 +669,6 @@ class ManageWindow(QWidget):
if self.pkgs: if self.pkgs:
self.ref_input_name_filter.setVisible(True) 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) self._update_table(pkgs_info=pkgs_info)
if new_pkgs: if new_pkgs:
@@ -660,11 +686,14 @@ class ManageWindow(QWidget):
for pkgv in pkgs_info['pkgs']: for pkgv in pkgs_info['pkgs']:
commons.apply_filters(pkgv, filters, pkgs_info) 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: if available_types is None:
self.ref_combo_filter_type.setVisible(self.combo_filter_type.count() > 1) self.ref_combo_filter_type.setVisible(self.combo_filter_type.count() > 1)
else: else:
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 self.type_filter = self.any_type_filter
if available_types and len(available_types) > 1: if available_types and len(available_types) > 1:
@@ -672,7 +701,10 @@ class ManageWindow(QWidget):
for _ in range(self.combo_filter_type.count() - 1): for _ in range(self.combo_filter_type.count() - 1):
self.combo_filter_type.removeItem(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) icon = self.cache_type_filter_icons.get(app_type)
if not icon: if not icon:
@@ -681,15 +713,24 @@ class ManageWindow(QWidget):
self.combo_filter_type.addItem(icon, app_type.capitalize(), app_type) 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) self.ref_combo_filter_type.setVisible(True)
else: else:
self.ref_combo_filter_type.setVisible(False) 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: if categories is None:
self.ref_combo_categories.setVisible(self.combo_categories.count() > 1) self.ref_combo_categories.setVisible(self.combo_categories.count() > 1)
else: else:
keeping_selected = keep_selected and categories and self.category_filter in categories
if not keeping_selected:
self.category_filter = self.any_category_filter self.category_filter = self.any_category_filter
if categories: if categories:
@@ -697,12 +738,23 @@ class ManageWindow(QWidget):
for _ in range(self.combo_categories.count() - 1): for _ in range(self.combo_categories.count() - 1):
self.combo_categories.removeItem(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) i18n_cat = self.i18n.get(c)
cat_label = i18n_cat if i18n_cat else c cat_label = i18n_cat if i18n_cat else c
self.combo_categories.addItem(cat_label.capitalize(), 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) self.ref_combo_categories.setVisible(True)
else: else:
self.ref_combo_categories.setVisible(False) self.ref_combo_categories.setVisible(False)
@@ -809,7 +861,7 @@ class ManageWindow(QWidget):
else: else:
self.combo_filter_type.setEnabled(False) 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.ref_combo_styles.setVisible(True)
self.thread_animate_progress.stop = True self.thread_animate_progress.stop = True
self.thread_animate_progress.wait(msecs=1000) self.thread_animate_progress.wait(msecs=1000)
@@ -835,8 +887,8 @@ class ManageWindow(QWidget):
if self.pkgs: if self.pkgs:
self.ref_input_name_filter.setVisible(True) self.ref_input_name_filter.setVisible(True)
self.update_bt_upgrade() self.update_bt_upgrade()
self._update_type_filters() self._update_type_filters(keep_selected=keep_filters)
self._update_categories() self._update_categories(keep_selected=keep_filters)
if self.ref_bt_installed.isVisible(): if self.ref_bt_installed.isVisible():
self.ref_bt_installed.setEnabled(True) 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.title=Requires your password to continue
popup.root.bad_password.title=Error popup.root.bad_password.title=Error
popup.root.bad_password.body=Wrong password popup.root.bad_password.body=Wrong password
popup.root.bad_password.last_try=Attempts finished. Action cancelled.
popup.history.title=History popup.history.title=History
popup.history.selected.tooltip=Current version popup.history.selected.tooltip=Current version
popup.button.yes=Yes popup.button.yes=Yes
@@ -140,6 +141,9 @@ filetransfer=file transfer
packagemanager=package manager packagemanager=package manager
sportsgame=sports game sportsgame=sports game
arcadegame=arcade game arcadegame=arcade game
player=player
screenshots,download.running=downloading image screenshots,download.running=downloading image
screenshots.download.no_content=No content to display 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.title=Requiere tu contraseña para continuar
popup.root.bad_password.title=Error popup.root.bad_password.title=Error
popup.root.bad_password.body=Contraseña incorrecta 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.title=Historia
popup.history.selected.tooltip=Versión actual popup.history.selected.tooltip=Versión actual
popup.button.yes=Sí popup.button.yes=Sí
@@ -181,8 +182,12 @@ filetransfer=transferencia de archivos
packagemanager=administrador de paquetes packagemanager=administrador de paquetes
sportsgame=juego de deportes sportsgame=juego de deportes
arcadegame=juego arcade arcadegame=juego arcade
player=jugador
fonts=fuentes
screenshots.bt_next.label=próxima screenshots.bt_next.label=próxima
screenshots.bt_back.label=anterior screenshots.bt_back.label=anterior
screenshots,download.running=descargando imagen screenshots,download.running=descargando imagen
screenshots.download.no_content=No hay contenido para mostrar 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.title=Requer sua senha para prosseguir
popup.root.bad_password.title=Erro popup.root.bad_password.title=Erro
popup.root.bad_password.body=Senha incorreta 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.title=Histórico
popup.history.selected.tooltip=Versão atual popup.history.selected.tooltip=Versão atual
popup.button.yes=Sim popup.button.yes=Sim
@@ -184,8 +185,12 @@ viewer=visualizador
packagemanager=gerenciador de pacotes packagemanager=gerenciador de pacotes
sportsgame=jogo de esportes sportsgame=jogo de esportes
arcadegame=jogo arcade arcadegame=jogo arcade
player=jogador
fonts=fontes
screenshots.bt_next.label=próxima screenshots.bt_next.label=próxima
screenshots.bt_back.label=anterior screenshots.bt_back.label=anterior
screenshots,download.running=baixando imagem screenshots,download.running=baixando imagem
screenshots.download.no_content=Sem conteúdo para exibir 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 from setuptools import setup, find_packages
DESCRIPTION = ( DESCRIPTION = (
"Graphical user interface to manage Flatpak / Snap applications." "Graphical interface to manage Flatpak, Snap, AppImage and AUR packages"
) )
AUTHOR = "Vinicius Moreira" 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.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.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('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): def test_check_update_no_suffix_3_x_2_digits(self):
self.assertTrue(ArchDataMapper.check_update('1.0.0-1', '1.1-1')) 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): def test_check_update_release(self):
# RE # RE
self.assertTrue(ArchDataMapper.check_update('1.0.0.R-1', '1.0.0.R-2')) 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')) 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')) 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')) 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')) 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.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')) 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')) 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')) self.assertFalse(ArchDataMapper.check_update('1.0.1.R-1', '1.0.0.RE-1', check_suffix=True))
# GA # GA
self.assertFalse(ArchDataMapper.check_update('1.0.0.R-1', '1.0.0.Ge-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')) 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')) 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')) 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')) 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')) 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')) 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')) 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')) 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 # RCS
self.assertFalse(ArchDataMapper.check_update('1.0.0.R-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')) 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')) 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 # BETA
self.assertFalse(ArchDataMapper.check_update('1.0.0.R-1', '1.0.0.B-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')) 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')) 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')) 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')) 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')) 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 # ALPHA
self.assertFalse(ArchDataMapper.check_update('1.0.0.R-1', '1.0.0.Alpha-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')) 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')) 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')) 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')) 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')) 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')) 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')) 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')) 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 # DEV
self.assertFalse(ArchDataMapper.check_update('1.0.0.R-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')) 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')) 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')) 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')) 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')) 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): def test_check_update_ga(self):
# GA # GA
self.assertFalse(ArchDataMapper.check_update('1.0.0.GA-1', '1.0.0.GE-1')) 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')) 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')) 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')) self.assertFalse(ArchDataMapper.check_update('1.1.0.GE-3', '1.0.6.GE-10', check_suffix=True))
# RC # RC
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')) 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')) 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.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')) 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')) self.assertFalse(ArchDataMapper.check_update('1.1.0.GE-1', '1.0.1.RC-1', check_suffix=True))
# BETA # BETA
self.assertFalse(ArchDataMapper.check_update('1.0.0.GA-1', '1.0.0.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')) self.assertTrue(ArchDataMapper.check_update('1.0.0.GE-1', '1.0.1.BETA-1', check_suffix=True))
# ALPHA # ALPHA
self.assertFalse(ArchDataMapper.check_update('1.0.0.GE-1', '1.0.0.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')) self.assertTrue(ArchDataMapper.check_update('1.0.0.GA-1', '1.0.1.ALPHA-1', check_suffix=True))
# DEV # DEV
self.assertFalse(ArchDataMapper.check_update('1.0.0.GA-1', '1.0.0.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')) self.assertTrue(ArchDataMapper.check_update('1.0.0.GE-1', '1.0.1.DEV-1', check_suffix=True))
def test_check_update_rc(self): def test_check_update_rc(self):
# RC # RC
@@ -148,38 +151,38 @@ class ArchDataMapperTest(TestCase):
def test_check_update_beta(self): def test_check_update_beta(self):
# BETA # BETA
self.assertFalse(ArchDataMapper.check_update('1.0.0.BETA-1', '1.0.0.BETA-1')) 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')) 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')) 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')) self.assertFalse(ArchDataMapper.check_update('1.1.0.B-3', '1.0.6.BETA-10', check_suffix=True))
# ALPHA # ALPHA
self.assertFalse(ArchDataMapper.check_update('1.0.0.BETA-1', '1.0.0.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')) self.assertTrue(ArchDataMapper.check_update('1.0.0.B-1', '1.0.1.ALPHA-1', check_suffix=True))
# DEV # DEV
self.assertFalse(ArchDataMapper.check_update('1.0.0.BETA-1', '1.0.0.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')) self.assertTrue(ArchDataMapper.check_update('1.0.0.B-1', '1.0.1.DEV-1', check_suffix=True))
def test_check_update_alpha(self): def test_check_update_alpha(self):
# ALPHA # 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.ALPHA-1', check_suffix=True))
self.assertFalse(ArchDataMapper.check_update('1.0.0.ALPHA-1', '1.0.0.ALFA-1')) 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')) 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')) 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')) self.assertFalse(ArchDataMapper.check_update('1.1.0.ALFA-3', '1.0.6.ALFA-10', check_suffix=True))
# DEV # DEV
self.assertFalse(ArchDataMapper.check_update('1.0.0.ALFA-1', '1.0.0.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')) self.assertTrue(ArchDataMapper.check_update('1.0.0.ALPHA-1', '1.0.1.DEV-1', check_suffix=True))
def test_check_update_dev(self): 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.DEV-1', check_suffix=True))
self.assertFalse(ArchDataMapper.check_update('1.0.0.DEV-1', '1.0.0.DEVEL-1')) 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')) 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')) 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')) 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')) 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): def test_check_update_unknown_suffix(self):
self.assertTrue(ArchDataMapper.check_update('1.0.0.BALL-1', '1.0.0.TAR-1')) 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'))