diff --git a/CHANGELOG.md b/CHANGELOG.md
index 05bbd277..f17c9148 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,7 +4,18 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
-## [0.X.X]
+## [0.10.0]
+### Features
+- Debian
+ - allowing Debian packages to be managed
+ - required dependencies: **aptitude**
+ - basic actions supported: **search**, **install**, **uninstall**, **upgrade**
+ - custom actions supported:
+ - **synchronize packages**: synchronize the available packages on the repository (`aptitude update`)
+ - **index applications**: maps runnable installed packages (automatically done during initialization)
+ - **software sources**: launches the application responsible for managing software sources (at the moment only `software-properties-gtk` is supported)
+ - custom package actions supported:
+ - **purge**: removes the packages and all related configuration files
### Improvements
- General
diff --git a/README.md b/README.md
index a3949e61..3900e723 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
[](https://github.com/vinifmor/bauh/releases/) [](https://pypi.org/project/bauh) [](https://aur.archlinux.org/packages/bauh) [](https://aur.archlinux.org/packages/bauh-staging) [](https://github.com/vinifmor/bauh/blob/master/LICENSE) [](https://ko-fi.com/vinifmor) [](https://twitter.com/bauh4linux)
-**bauh** (ba-oo), formerly known as **fpakman**, is a graphical interface for managing your Linux software (packages/applications). It currently supports the following formats: AppImage, ArchLinux repositories/AUR, Flatpak, Snap and Web applications.
+**bauh** (ba-oo), formerly known as **fpakman**, is a graphical interface for managing your Linux software (packages/applications). It currently supports the following formats: AppImage, ArchLinux repositories/AUR, Debian packages, Flatpak, Snap and Web applications.
Key features
- A management panel where you can: search, install, uninstall, upgrade, downgrade and launch you applications (and more...)
@@ -26,6 +26,7 @@ Key features
6. [Supported types](#types)
- [AppImage](#type_appimage)
- [Arch packages/AUR](#type_arch)
+ - [Debian](#type_deb)
- [Flatpak](#type_flatpak)
- [Snap](#type_snap)
- [Native Web applications](#type_web)
@@ -69,6 +70,7 @@ Key features
##### Optional dependencies (they should be installed with apt-get/apt)
+- `aptitude`: Debian package management
- `timeshift`: system backup
- `aria2`: multi-threaded downloads
- `axel`: multi-threaded downloads alternative
@@ -285,6 +287,22 @@ aur_rebuild_detector: true # it checks if packages built with old library versio
prefer_repository_provider: true # when there is just one repository provider for a given a dependency and several from AUR, it will be automatically picked.
```
+##### Debian packages
+- Basic actions supported: **search**, **install**, **uninstall**, **upgrade**
+- Custom actions supported:
+ - **synchronize packages**: synchronize the available packages on the repository (`aptitude update`)
+ - **index applications**: maps runnable installed packages (automatically done during initialization)
+ - **software sources**: launches the application responsible for managing software sources (at the moment only `software-properties-gtk` is supported)
+- Custom package actions supported:
+ - **purge**: removes the packages and all related configuration files
+- Files:
+ - runnable applications index: `~/.cache/bauh/debian/apps_idx.json` (or `/var/cache/bauh/debian/apps_idx.json` for **root**)
+ - package suggestions: `~/.cache/bauh/debian/suggestions.txt` (or `/var/cache/bauh/debian/suggestions.txt` for **root**)
+ - configuration: `~/.config/bauh/debian.yml` or `/etc/bauh/debian.yml`
+ - `index_apps.exp`: time period (**in minutes**) in which the installed applications cache is considered up-to-date during startup (default: `1440` -> 24 hours)
+ - `sync_pkgs.time`: time period (**in minutes**) in which the packages synchronization must be done on startup (default: `1440` -> 24 hours)
+ - `suggestions.exp`: it defines the period (**in hours**) in which the suggestions stored in disc will be considered up to date. Use 0 if you always want to update them.
+ - `pkg_sources.app`: it defines the application for managing the software sources. A `null` value detects one of the supported applications automatically.
##### Flatpak
diff --git a/bauh/__init__.py b/bauh/__init__.py
index 4d04c6b9..f9b95658 100644
--- a/bauh/__init__.py
+++ b/bauh/__init__.py
@@ -1,4 +1,4 @@
-__version__ = '0.9.29'
+__version__ = '0.10.0'
__app_name__ = 'bauh'
import os
diff --git a/bauh/api/abstract/context.py b/bauh/api/abstract/context.py
index d327eed2..da729d48 100644
--- a/bauh/api/abstract/context.py
+++ b/bauh/api/abstract/context.py
@@ -14,7 +14,7 @@ class ApplicationContext:
def __init__(self, download_icons: bool, http_client: HttpClient, app_root_dir: str, i18n: I18n,
cache_factory: MemoryCacheFactory, disk_loader_factory: DiskCacheLoaderFactory,
logger: logging.Logger, file_downloader: FileDownloader, distro: str, app_name: str,
- internet_checker: InternetChecker, root_user: bool):
+ internet_checker: InternetChecker, root_user: bool, screen_width: int = -1, screen_height: int = -1):
"""
:param download_icons: if packages icons should be downloaded
:param http_client: a shared instance of http client
@@ -27,6 +27,8 @@ class ApplicationContext:
:param distro
:param app_name
:param internet_checker
+ :param screen_width
+ :param screen_height
"""
self.download_icons = download_icons
self.http_client = http_client
@@ -44,6 +46,8 @@ class ApplicationContext:
self.root_user = root_user
self.root_password = None
self.internet_checker = internet_checker
+ self.screen_width = screen_width
+ self.screen_height = screen_height
def is_system_x86_64(self):
return self.arch_x86_64
diff --git a/bauh/api/abstract/controller.py b/bauh/api/abstract/controller.py
index e80f12b4..142d5bb4 100644
--- a/bauh/api/abstract/controller.py
+++ b/bauh/api/abstract/controller.py
@@ -4,7 +4,7 @@ import shutil
from abc import ABC, abstractmethod
from enum import Enum
from pathlib import Path
-from typing import List, Set, Type, Tuple, Optional, Generator
+from typing import List, Set, Type, Tuple, Optional, Generator, TypeVar
import yaml
@@ -15,10 +15,12 @@ from bauh.api.abstract.model import SoftwarePackage, PackageUpdate, PackageHisto
CustomSoftwareAction
from bauh.api.abstract.view import ViewComponent
+P = TypeVar('P', bound=SoftwarePackage)
+
class SearchResult:
- def __init__(self, installed: Optional[List[SoftwarePackage]], new: Optional[List[SoftwarePackage]], total: int):
+ def __init__(self, installed: Optional[List[P]], new: Optional[List[P]], total: int):
"""
:param installed: already installed packages
:param new: new packages found
@@ -40,9 +42,19 @@ class SearchResult:
self.total = total
@classmethod
- def empty(cls):
+ def empty(cls) -> "SearchResult":
return cls(installed=[], new=[], total=0)
+ def __eq__(self, other):
+ if isinstance(other, SearchResult):
+ if self.installed == other.installed:
+ return self.new == other.new
+
+ return False
+
+ def __hash__(self):
+ return hash(self.installed) + hash(self.new)
+
class UpgradeRequirement:
@@ -69,7 +81,7 @@ class UpgradeRequirement:
class UpgradeRequirements:
def __init__(self, to_install: Optional[List[UpgradeRequirement]], to_remove: Optional[List[UpgradeRequirement]],
- to_upgrade: List[UpgradeRequirement], cannot_upgrade: Optional[List[UpgradeRequirement]]):
+ to_upgrade: Optional[List[UpgradeRequirement]], cannot_upgrade: Optional[List[UpgradeRequirement]]):
"""
:param to_install: additional packages that must be installed with the upgrade
:param to_remove: non upgrading packages that should be removed due to conflicts with upgrading packages
diff --git a/bauh/api/exception.py b/bauh/api/exception.py
index 8d00ac1a..ae00707c 100644
--- a/bauh/api/exception.py
+++ b/bauh/api/exception.py
@@ -1,3 +1,6 @@
+from typing import Optional
+
class NoInternetException(Exception):
pass
+
diff --git a/bauh/commons/singleton.py b/bauh/commons/singleton.py
new file mode 100644
index 00000000..b502c17d
--- /dev/null
+++ b/bauh/commons/singleton.py
@@ -0,0 +1,9 @@
+class Singleton(type):
+
+ __instance = {}
+
+ def __call__(cls, *args, **kwargs):
+ if cls not in cls.__instance:
+ cls.__instance[cls] = super(Singleton, cls).__call__(*args, **kwargs)
+
+ return cls.__instance[cls]
diff --git a/bauh/commons/system.py b/bauh/commons/system.py
index c63cb0a7..094b1910 100644
--- a/bauh/commons/system.py
+++ b/bauh/commons/system.py
@@ -4,7 +4,7 @@ import sys
import time
from io import StringIO
from subprocess import PIPE
-from typing import List, Tuple, Set, Dict, Optional
+from typing import List, Tuple, Set, Dict, Optional, Iterable
# default environment variables for subprocesses.
from bauh.api.abstract.handler import ProcessWatcher
@@ -26,7 +26,7 @@ USE_GLOBAL_INTERPRETER = bool(os.getenv('VIRTUAL_ENV'))
def gen_env(global_interpreter: bool, lang: str = DEFAULT_LANG, extra_paths: Optional[Set[str]] = None) -> dict:
custom_env = dict(os.environ)
- if lang:
+ if lang is not None:
custom_env['LANG'] = lang
if global_interpreter: # to avoid subprocess calls to the virtualenv python interpreter instead of the global one.
@@ -62,7 +62,7 @@ class SystemProcess:
class SimpleProcess:
- def __init__(self, cmd: List[str], cwd: str = '.', expected_code: int = 0,
+ def __init__(self, cmd: Iterable[str], cwd: str = '.', expected_code: int = 0,
global_interpreter: bool = USE_GLOBAL_INTERPRETER, lang: str = DEFAULT_LANG, root_password: Optional[str] = None,
extra_paths: Set[str] = None, error_phrases: Set[str] = None, wrong_error_phrases: Set[str] = None,
shell: bool = False, success_phrases: Set[str] = None, extra_env: Optional[Dict[str, str]] = None,
@@ -117,10 +117,13 @@ class ProcessHandler:
def __init__(self, watcher: ProcessWatcher = None):
self.watcher = watcher
- def _notify_watcher(self, msg: str):
+ def _notify_watcher(self, msg: str, as_substatus: bool = False):
if self.watcher:
self.watcher.print(msg)
+ if as_substatus:
+ self.watcher.change_substatus(msg)
+
def handle(self, process: SystemProcess, error_output: StringIO = None, output_handler=None) -> bool:
self._notify_watcher(' '.join(process.subproc.args) + '\n')
@@ -181,7 +184,8 @@ class ProcessHandler:
return process.subproc.returncode is None or process.subproc.returncode == 0
- def handle_simple(self, proc: SimpleProcess, output_handler=None, notify_watcher: bool = True) -> Tuple[bool, str]:
+ def handle_simple(self, proc: SimpleProcess, output_handler=None, notify_watcher: bool = True,
+ output_as_substatus: bool = False) -> Tuple[bool, str]:
if notify_watcher:
self._notify_watcher((proc.instance.args if isinstance(proc.instance.args, str) else ' '.join(proc.instance.args)) + '\n')
@@ -205,7 +209,7 @@ class ProcessHandler:
output_handler(line)
if notify_watcher:
- self._notify_watcher(line)
+ self._notify_watcher(line, as_substatus=output_as_substatus)
proc.instance.wait()
output.seek(0)
diff --git a/bauh/commons/util.py b/bauh/commons/util.py
index 01d2b962..8acd65c7 100644
--- a/bauh/commons/util.py
+++ b/bauh/commons/util.py
@@ -1,4 +1,21 @@
+import logging
+from abc import ABC
from datetime import datetime
+from logging import Logger
+from typing import Optional
+
+
+class NullLoggerFactory(ABC):
+
+ __instance: Optional[Logger] = None
+
+ @classmethod
+ def logger(cls) -> Logger:
+ if cls.__instance is None:
+ cls.__instance = logging.getLogger('__null__')
+ cls.__instance.addHandler(logging.NullHandler())
+
+ return cls.__instance
def deep_update(source: dict, overrides: dict):
diff --git a/bauh/gems/appimage/resources/locale/ca b/bauh/gems/appimage/resources/locale/ca
index 97cd839c..03dad8b6 100644
--- a/bauh/gems/appimage/resources/locale/ca
+++ b/bauh/gems/appimage/resources/locale/ca
@@ -44,7 +44,6 @@ appimage.update_database.uncompressing=Uncompressing files
appimage.upgrade.failed=It was not possible to upgrade the following applications: {apps}
appimage.task.db_update=Actualització de bases de dades
appimage.task.db_update.checking=Checking for updates
-appimage.task.suggestions=Downloading suggestions
appimage.task.symlink_check=Checking symlinks
appimage.uninstall.error.remove_folder=No s’ha pogut suprimir el directori d’instal·lació de l’aplicació {}
appimage.warning.missing_db_files={appimage} database files were not found. Some actions will not work properly.
diff --git a/bauh/gems/appimage/resources/locale/de b/bauh/gems/appimage/resources/locale/de
index d91e2644..f61662d3 100644
--- a/bauh/gems/appimage/resources/locale/de
+++ b/bauh/gems/appimage/resources/locale/de
@@ -44,7 +44,6 @@ appimage.update_database.uncompressing=Uncompressing files
appimage.upgrade.failed=It was not possible to upgrade the following applications: {apps}
appimage.task.db_update=Updating databases
appimage.task.db_update.checking=Checking for updates
-appimage.task.suggestions=Downloading suggestions
appimage.task.symlink_check=Checking symlinks
appimage.uninstall.error.remove_folder=Anwendungordner {} konnte nicht entfernt werden
appimage.warning.missing_db_files={appimage} database files were not found. Some actions will not work properly.
diff --git a/bauh/gems/appimage/resources/locale/en b/bauh/gems/appimage/resources/locale/en
index 14efc718..5a9fc536 100644
--- a/bauh/gems/appimage/resources/locale/en
+++ b/bauh/gems/appimage/resources/locale/en
@@ -44,7 +44,6 @@ appimage.update_database.uncompressing=Uncompressing files
appimage.upgrade.failed=It was not possible to upgrade the following applications: {apps}
appimage.task.db_update=Updating databases
appimage.task.db_update.checking=Checking for updates
-appimage.task.suggestions=Downloading suggestions
appimage.task.symlink_check=Checking symlinks
appimage.uninstall.error.remove_folder=Could not remove the application installation directory {}
appimage.warning.missing_db_files={appimage} database files were not found. Some actions will not work properly.
diff --git a/bauh/gems/appimage/resources/locale/es b/bauh/gems/appimage/resources/locale/es
index 3b11a4b3..454b69d8 100644
--- a/bauh/gems/appimage/resources/locale/es
+++ b/bauh/gems/appimage/resources/locale/es
@@ -44,7 +44,6 @@ appimage.update_database.uncompressing=Descomprindo archivos
appimage.upgrade.failed=No fue posible actualizar las siguientes aplicaciones: {apps}
appimage.task.db_update=Actualizando base de datos
appimage.task.db_update.checking=Buscando actualizaciones
-appimage.task.suggestions=Descargando sugerencias
appimage.task.symlink_check=Verificando links simbólicos
appimage.uninstall.error.remove_folder=No se pudo eliminar el directorio de instalación de la aplicación {}
appimage.warning.missing_db_files=No se encontraron archivos de la base de datos {appimage}. Algunas acciones no funcionarán correctamente.
diff --git a/bauh/gems/appimage/resources/locale/fr b/bauh/gems/appimage/resources/locale/fr
index 3c7b57e6..792d2682 100644
--- a/bauh/gems/appimage/resources/locale/fr
+++ b/bauh/gems/appimage/resources/locale/fr
@@ -45,7 +45,6 @@ appimage.update_database.uncompressing=Uncompressing files
appimage.upgrade.failed=It was not possible to upgrade the following applications: {apps}
appimage.task.db_update=Mise à jour des bases de données
appimage.task.db_update.checking=Checking for updates
-appimage.task.suggestions=Downloading suggestions
appimage.task.symlink_check=Verification des symlinks
appimage.uninstall.error.remove_folder=Impossible de supprimer le répertoire d'installation {}
appimage.warning.missing_db_files={appimage} database files were not found. Some actions will not work properly.
diff --git a/bauh/gems/appimage/resources/locale/it b/bauh/gems/appimage/resources/locale/it
index a347d33e..cfd6b3c9 100644
--- a/bauh/gems/appimage/resources/locale/it
+++ b/bauh/gems/appimage/resources/locale/it
@@ -45,7 +45,6 @@ appimage.update_database.uncompressing=Uncompressing files
appimage.upgrade.failed=It was not possible to upgrade the following applications: {apps}
appimage.task.db_update=Aggiornamento dei database
appimage.task.db_update.checking=Checking for updates
-appimage.task.suggestions=Downloading suggestions
appimage.task.symlink_check=Checking symlinks
appimage.uninstall.error.remove_folder=Impossibile rimuovere la directory di installazione dell'applicazione {}
appimage.warning.missing_db_files={appimage} database files were not found. Some actions will not work properly.
diff --git a/bauh/gems/appimage/resources/locale/pt b/bauh/gems/appimage/resources/locale/pt
index 0910990c..284dbb2f 100644
--- a/bauh/gems/appimage/resources/locale/pt
+++ b/bauh/gems/appimage/resources/locale/pt
@@ -1,7 +1,7 @@
appimage.config.database.expiration=Expiração do banco de dados
appimage.config.database.expiration.tip=Define o período (em minutos) no qual o banco de dados será considerado atualizado durante o processo de inicialização. Use 0 se quiser que ele sempre seja atualizado.
appimage.config.suggestions.expiration=Expiração das sugestões
-appimage.config.suggestions.expiration.tip=Define o período (em horas) no qual as sugestões armazenadas em disco serão consideradas atualizadas. Use 0 se quiser que elas sempre seja atualizadas.
+appimage.config.suggestions.expiration.tip=Define o período (em horas) no qual as sugestões armazenadas em disco serão consideradas atualizadas. Use 0 se quiser que elas sempre sejam atualizadas.
appimage.custom_action.install_file=Instalar arquivo AppImage
appimage.custom_action.install_file.desc=Instala um arquivo AppImage no sistema
appimage.custom_action.install_file.details=Detalhes de instalação de AppImage
@@ -44,7 +44,6 @@ appimage.update_database.uncompressing=Descomprimindo arquivos
appimage.upgrade.failed=Não foi possível atualizar as seguintes aplicações: {apps}
appimage.task.db_update=Atualizando base de dados
appimage.task.db_update.checking=Checando atualizações
-appimage.task.suggestions=Baixando sugestões
appimage.task.symlink_check=Verificando links simbólicos
appimage.uninstall.error.remove_folder=Não foi possível remover o diretóri ode instalação do aplicativo {}
appimage.warning.missing_db_files=Arquivos do banco de dados {appimage} não foram encontrados. Algumas ações não funcionarão corretamente.
diff --git a/bauh/gems/appimage/resources/locale/ru b/bauh/gems/appimage/resources/locale/ru
index f7a9128f..3b37d2ab 100644
--- a/bauh/gems/appimage/resources/locale/ru
+++ b/bauh/gems/appimage/resources/locale/ru
@@ -44,7 +44,6 @@ appimage.update_database.uncompressing=Uncompressing files
appimage.upgrade.failed=It was not possible to upgrade the following applications: {apps}
appimage.task.db_update=Обновление базы данных
appimage.task.db_update.checking=Checking for updates
-appimage.task.suggestions=Downloading suggestions
appimage.task.symlink_check=Checking symlinks
appimage.uninstall.error.remove_folder=Не удалось удалить каталог приложения {}
appimage.warning.missing_db_files={appimage} database files were not found. Some actions will not work properly.
diff --git a/bauh/gems/appimage/resources/locale/tr b/bauh/gems/appimage/resources/locale/tr
index 7cac510f..1b8f4f1c 100644
--- a/bauh/gems/appimage/resources/locale/tr
+++ b/bauh/gems/appimage/resources/locale/tr
@@ -44,7 +44,6 @@ appimage.update_database.uncompressing=Uncompressing files
appimage.upgrade.failed=It was not possible to upgrade the following applications: {apps}
appimage.task.db_update=Veritabanları güncelleniyor
appimage.task.db_update.checking=Checking for updates
-appimage.task.suggestions=Downloading suggestions
appimage.task.symlink_check=Checking symlinks
appimage.uninstall.error.remove_folder={} uygulama kurulum dizini kaldırılamadı
appimage.warning.missing_db_files={appimage} database files were not found. Some actions will not work properly.
diff --git a/bauh/gems/appimage/worker.py b/bauh/gems/appimage/worker.py
index e416f5f9..ea6ede95 100644
--- a/bauh/gems/appimage/worker.py
+++ b/bauh/gems/appimage/worker.py
@@ -295,7 +295,7 @@ class AppImageSuggestionsDownloader(Thread):
self.taskman = taskman
self.config = appimage_config
self.task_id = 'appim.suggestions'
- self.taskman.register_task(id_=self.task_id, label=i18n['appimage.task.suggestions'], icon_path=get_icon_path())
+ self.taskman.register_task(id_=self.task_id, label=i18n['task.download_suggestions'], icon_path=get_icon_path())
def should_download(self, appimage_config: dict) -> bool:
try:
diff --git a/bauh/gems/arch/controller.py b/bauh/gems/arch/controller.py
index 06939bc0..693af00b 100644
--- a/bauh/gems/arch/controller.py
+++ b/bauh/gems/arch/controller.py
@@ -635,7 +635,7 @@ class ArchManager(SoftwareManager):
if ignored:
for p in pkgs:
if p.name in ignored:
- p.update_ignored = True
+ p.updates_ignored = True
return SearchResult(pkgs, None, len(pkgs))
diff --git a/bauh/gems/debian/__init__.py b/bauh/gems/debian/__init__.py
new file mode 100644
index 00000000..e4121fd3
--- /dev/null
+++ b/bauh/gems/debian/__init__.py
@@ -0,0 +1,12 @@
+import os
+
+from bauh.api.paths import CACHE_DIR, CONFIG_DIR
+from bauh.commons import resource
+
+ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
+
+DEBIAN_CACHE_DIR = f'{CACHE_DIR}/debian'
+APP_INDEX_FILE = f'{DEBIAN_CACHE_DIR}/apps_idx.json'
+CONFIG_FILE = f'{CONFIG_DIR}/debian.yml'
+PACKAGE_SYNC_TIMESTAMP_FILE = f'{DEBIAN_CACHE_DIR}/sync_pkgs.ts'
+DEBIAN_ICON_PATH = resource.get_path('img/debian.svg', ROOT_DIR)
diff --git a/bauh/gems/debian/aptitude.py b/bauh/gems/debian/aptitude.py
new file mode 100644
index 00000000..76b82cf8
--- /dev/null
+++ b/bauh/gems/debian/aptitude.py
@@ -0,0 +1,424 @@
+import re
+import time
+import traceback
+from contextlib import contextmanager
+from enum import Enum
+from logging import Logger
+from math import ceil
+from queue import Queue
+from threading import Thread
+from typing import Iterable, Optional, Pattern, Dict, Set, Tuple, Generator, Collection
+
+from bauh.api.abstract.handler import ProcessWatcher
+from bauh.commons import system
+from bauh.commons.html import bold
+from bauh.commons.system import SimpleProcess
+from bauh.commons.util import size_to_byte
+from bauh.gems.debian.common import strip_maintainer_email, strip_section
+from bauh.gems.debian.model import DebianPackage, DebianTransaction
+from bauh.view.util.translation import I18n
+
+
+def map_package_name(string: str):
+ name_split = string.split(':')
+
+ if len(name_split) > 2:
+ return ':'.join(name_split[0:-1])
+
+ return name_split[0]
+
+
+class AptitudeAction(Enum):
+ INSTALL = 0
+ UPGRADE = 1
+ REMOVE = 3
+
+
+class Aptitude:
+
+ def __init__(self, logger: Logger):
+ self._log = logger
+ self._re_show_attr: Optional[Pattern] = None
+ self._env: Optional[Dict[str, str]] = None
+ self._list_attrs: Optional[Set[str]] = None
+ self._re_to_install: Optional[Pattern] = None
+ self._re_transaction_pkg: Optional[Pattern] = None
+ self._size_attrs: Optional[Tuple[str]] = None
+ self._default_lang = ''
+ self._ignored_fields: Optional[Set[str]] = None
+
+ def show(self, pkgs: Iterable[str], attrs: Optional[Collection[str]] = None, verbose: bool = False) \
+ -> Optional[Dict[str, Dict[str, object]]]:
+
+ if pkgs:
+ force_verbose = verbose if verbose else ('compressed size' in attrs if attrs else False)
+ code, output = system.execute(f"aptitude show -q {' '.join(pkgs)}{' -v' if force_verbose else ''}",
+ shell=True, custom_env=self.env)
+
+ if code == 0 and output:
+ info, pkg = dict(), None
+ for field, val in self.re_show_attr.findall('\n' + output):
+ final_field, final_val = field.strip().lower(), val.strip()
+
+ if final_field == 'package':
+ pkg = {}
+ info[final_val] = pkg
+ elif final_field not in self.ignored_fields and (not attrs or final_field in attrs):
+ if final_val:
+ if final_field in self.list_attrs:
+ final_val = tuple((v.strip() for v in final_val.split(',') if v))
+ elif final_field in self.size_attrs:
+ size_split = final_val.split(' ')
+
+ if len(size_split) >= 1:
+ number = float(size_split[0].replace(',', '.'))
+ measure = size_split[1].strip().lower() if len(size_split) >= 2 else 'b'
+ final_val = size_to_byte(number, measure)
+ else:
+ self._log.warning(f"Unhandled value ({val}) for attribute '{field}'")
+ final_val = None
+
+ pkg[final_field] = final_val
+
+ return info
+
+ def simulate_removal(self, packages: Iterable[str], purge: bool = False) -> Optional[DebianTransaction]:
+ code, output = system.execute(self.gen_remove_cmd(packages, purge, simulate=True), shell=True,
+ custom_env=self.env)
+
+ if code == 0 and output:
+ return self.map_transaction_output(output)
+
+ def map_transaction_output(self, output: str) -> DebianTransaction:
+ to_install, to_upgrade, to_remove = None, None, None
+ current_collection = None
+
+ for line in output.split('\n'):
+ if line.startswith('The following NEW packages will be installed:'):
+ to_install = set()
+ current_collection = to_install
+ elif line.startswith('The following packages will be upgraded:'):
+ to_upgrade = set()
+ current_collection = to_upgrade
+ elif line.startswith('The following packages will be REMOVED:'):
+ to_remove = set()
+ current_collection = to_remove
+ elif line.startswith('Would download/install/remove packages'):
+ break
+ elif current_collection is not None and line.startswith(' '):
+ for n, _, v, __, lv, ___, size in self.re_transaction_pkg.findall(line):
+ pkg = DebianPackage(name=n, version=v, latest_version=lv if lv else v, transaction_size=0)
+
+ if size:
+ size_split = size.strip().split(' ')
+
+ try:
+ number = float(size_split[0].strip().replace(',', '.'))
+ measure = size_split[1].strip()[0].lower() if len(size_split) >= 2 else 'b'
+ pkg.transaction_size = size_to_byte(number, measure)
+ except:
+ traceback.print_exc()
+ pass
+
+ current_collection.add(pkg)
+
+ return DebianTransaction(to_install=tuple(to_install) if to_install else tuple(),
+ to_remove=tuple(to_remove) if to_remove else tuple(),
+ to_upgrade=tuple(to_upgrade) if to_upgrade else tuple())
+
+ def simulate_upgrade(self, packages: Iterable[str]) -> DebianTransaction:
+ code, output = system.execute(self.gen_transaction_cmd('upgrade', packages, simulate=True),
+ shell=True, custom_env=self.env)
+
+ if code == 0 and output:
+ return self.map_transaction_output(output)
+
+ def upgrade(self, packages: Iterable[str], root_password: Optional[str]) -> SimpleProcess:
+ cmd = self.gen_transaction_cmd('upgrade', packages).split(' ')
+ return SimpleProcess(cmd=cmd, shell=True, lang=self._default_lang,
+ root_password=root_password)
+
+ def update(self, root_password: Optional[str]) -> SimpleProcess:
+ return SimpleProcess(('aptitude', 'update'), root_password=root_password, shell=True, lang=self._default_lang)
+
+ def simulate_installation(self, packages: Iterable[str]) -> Optional[DebianTransaction]:
+ code, output = system.execute(self.gen_transaction_cmd('install', packages, simulate=True),
+ shell=True, custom_env=self.env)
+
+ if code == 0 and output:
+ return self.map_transaction_output(output)
+
+ def install(self, packages: Iterable[str], root_password: Optional[str]) -> SimpleProcess:
+ cmd = self.gen_transaction_cmd('install', packages).split(' ')
+ return SimpleProcess(cmd=cmd, shell=True, lang=self._default_lang, root_password=root_password)
+
+ def read_installed(self) -> Generator[DebianPackage, None, None]:
+ yield from self.search(query='~i')
+
+ def read_updates(self) -> Generator[Tuple[str, str], None, None]:
+ _, output = system.execute(f"aptitude search ~U -q -F '%p^%V' --disable-columns --no-gui",
+ shell=True,
+ custom_env=self.env)
+
+ if output:
+ for line in output.split('\n'):
+ line_split = line.strip().split('^')
+
+ if len(line_split) == 2 and line_split[1] != '':
+ yield line_split[0], line_split[1]
+
+ def search(self, query: str, fill_size: bool = False) -> Generator[DebianPackage, None, None]:
+ attrs = f"%p^%v^%V^%m^%s^{'%I^' if fill_size else ''}%d"
+ _, output = system.execute(f"aptitude search {query} -q -F '{attrs}' --disable-columns",
+ shell=True,
+ custom_env=self.env)
+
+ if output:
+ no_attrs = 7 if fill_size else 6
+
+ for line in output.split('\n'):
+ line_split = line.strip().split('^', maxsplit=no_attrs - 1)
+
+ if len(line_split) == no_attrs:
+ latest_version = line_split[2] if line_split[2] != '' else None
+
+ size = None
+
+ if fill_size:
+ size_split = line_split[no_attrs - 2].split(' ')
+ number = float(size_split[0].replace(',', '.'))
+ measure = size_split[1][0].lower() if len(size_split) >= 2 else 'b'
+
+ try:
+ size = size_to_byte(number, measure)
+ except:
+ traceback.print_exc()
+
+ if latest_version is not None:
+ installed_version = line_split[1] if line_split[1] != '' else None
+ section = strip_section(line_split[4])
+
+ yield DebianPackage(name=line_split[0],
+ version=installed_version if installed_version else latest_version,
+ latest_version=latest_version,
+ installed=bool(installed_version),
+ update=installed_version is not None and installed_version != latest_version,
+ maintainer=strip_maintainer_email(line_split[3]),
+ categories=(section,) if section else None,
+ uncompressed_size=size,
+ description=line_split[no_attrs - 1])
+
+ def search_by_name(self, names: Iterable[str], fill_size: bool = False) -> Generator[DebianPackage, None, None]:
+ query = f"'({'|'.join(f'?exact-name({n})' for n in names)})'"
+ yield from self.search(query=query, fill_size=fill_size)
+
+ def remove(self, packages: Iterable[str], root_password: Optional[str], purge: bool = False) -> SimpleProcess:
+ return SimpleProcess(cmd=self.gen_remove_cmd(packages, purge).split(' '), shell=True,
+ lang=self._default_lang, root_password=root_password)
+
+ def read_installed_names(self) -> Generator[str, None, None]:
+ code, output = system.execute("aptitude search ~i -q -F '%p' --disable-columns",
+ shell=True,
+ custom_env=self.env)
+
+ if output:
+ for line in output.split('\n'):
+ if line:
+ yield line
+
+ @property
+ def re_show_attr(self) -> Pattern:
+ if self._re_show_attr is None:
+ self._re_show_attr = re.compile(r'(\n\w+[a-zA-Z0-9\-\s]*):\s+(.+)')
+
+ return self._re_show_attr
+
+ @property
+ def env(self) -> Dict[str, str]:
+ if self._env is None:
+ self._env = system.gen_env(global_interpreter=system.USE_GLOBAL_INTERPRETER,
+ lang=self._default_lang)
+
+ return self._env
+
+ @property
+ def list_attrs(self) -> Set[str]:
+ if self._list_attrs is None:
+ self._list_attrs = {'depends', 'provides', 'replaces', 'recommends', 'suggests', 'conflicts',
+ 'state', 'predepends', 'breaks'}
+
+ return self._list_attrs
+
+ @property
+ def re_transaction_pkg(self) -> Pattern:
+ if self._re_transaction_pkg is None:
+ self._re_transaction_pkg = re.compile(r'([a-zA-Z0-9\-_@~.+]+)({\w+})?\s*\[([a-zA-Z0-9\-_@~.+:]+)'
+ r'(\s+->\s+([a-zA-Z0-9\-_@~.+:]+))?](\s*<([\-+]?[0-9.,]+\s+\w+)>)?')
+
+ return self._re_transaction_pkg
+
+ @property
+ def size_attrs(self) -> Tuple[str]:
+ if self._size_attrs is None:
+ self._size_attrs = {'compressed size', 'uncompressed size'}
+
+ return self._size_attrs
+
+ @property
+ def ignored_fields(self) -> Set[str]:
+ if self._ignored_fields is None:
+ self._ignored_fields = {'sha1', 'sha256', 'sha512', 'checksum-filesize'}
+
+ return self._ignored_fields
+
+ @classmethod
+ def gen_remove_cmd(cls, packages: Iterable[str], purge: bool, simulate: bool = False) -> str:
+ return cls.gen_transaction_cmd('purge' if purge else 'remove', packages, simulate)
+
+ @staticmethod
+ def gen_transaction_cmd(type_: str, packages: Iterable[str], simulate: bool = False) -> str:
+ return f"aptitude {type_} -q -y --no-gui --full-resolver {' '.join(packages)}" \
+ f" -o Aptitude::ProblemResolver::RemoveScore=9999999" \
+ f" -o Aptitude::ProblemResolver::EssentialRemoveScore=9999999" \
+ f"{' -V -s -Z' if simulate else ''}"
+
+
+class AptitudeOutputHandler(Thread):
+
+ def __init__(self, i18n: I18n, targets: Iterable[str], re_download: Pattern, watcher: ProcessWatcher,
+ action: AptitudeAction):
+ super(AptitudeOutputHandler, self).__init__()
+ self._i18n = i18n
+ self._re_download = re_download
+ self._watcher = watcher
+ self._targets = set(targets) if targets is not None else None
+ self._unpacking = 0
+ self._removing = 0
+ self._downloading = 0
+ self._to_process = Queue()
+ self._work = True
+ self._action = action
+
+ def stop_working(self):
+ self._work = False
+
+ def handle(self, string: str):
+ self._to_process.put(string)
+
+ @property
+ def total_targets(self) -> int:
+ return len(self._targets) if self._targets else 0
+
+ @property
+ def processed(self) -> int:
+ return self._removing if self._action == AptitudeAction.REMOVE else self._unpacking
+
+ def _get_progress(self, current: int) -> str:
+ if self.total_targets > 0:
+ if self._action == AptitudeAction.REMOVE:
+ total = self._removing
+ else:
+ if self.total_targets == self._unpacking:
+ total = self.total_targets
+ else:
+ total = ceil((self._unpacking + self._downloading) / 2)
+
+ return f'({total / self.total_targets * 100:.2f}%) [{current}/{self.total_targets}] '
+
+ return ''
+
+ def run(self):
+ while self._work:
+ time.sleep(0.001)
+
+ if self._to_process.empty():
+ continue
+
+ string = self._to_process.get()
+
+ if self.total_targets > 0 and self.total_targets == self._unpacking:
+ self._watcher.change_substatus(self._i18n['debian.output.finishing'])
+ continue
+
+ if string:
+ if self._action != AptitudeAction.REMOVE and string.startswith('Unpacking '):
+ unpacking = string.split(' ')
+
+ if len(unpacking) >= 2 and unpacking[1]:
+ pkg = map_package_name(unpacking[1].strip())
+
+ if self._targets and pkg in self._targets:
+ self._unpacking += 1
+
+ msg = f"{self._get_progress(self._unpacking)}" \
+ f"{self._i18n['debian.output.unpacking'].format(pkg=bold(pkg))}"
+
+ self._watcher.change_substatus(msg)
+
+ continue
+
+ if self._action == AptitudeAction.REMOVE and string.startswith('Removing '):
+ unpacking = string.split(' ')
+
+ if len(unpacking) >= 2 and unpacking[1]:
+ pkg = unpacking[1].strip()
+
+ if self._targets and pkg in self._targets:
+ self._removing += 1
+
+ msg = f"{self._get_progress(self._removing)}" \
+ f"{self._i18n['debian.output.removing'].format(pkg=bold(pkg))}"
+
+ self._watcher.change_substatus(msg)
+
+ continue
+
+ download = self._re_download.findall(string)
+
+ if download:
+ data = download[0].split(' ')
+
+ if len(data) >= 4:
+ pkg = data[3].strip()
+
+ if self._targets and pkg in self._targets:
+ self._downloading += 1
+
+ msg = f"{self._get_progress(self._downloading)}" \
+ f"{self._i18n['debian.output.downloading'].format(pkg=bold(pkg))}"
+
+ self._watcher.change_substatus(msg)
+
+ continue
+
+ _processed = self.processed
+ if self._targets and _processed > 0:
+ self._watcher.change_substatus(self._get_progress(_processed).strip())
+ continue
+
+ self._watcher.change_substatus(' ')
+
+
+class AptitudeOutputHandlerFactory:
+
+ def __init__(self, i18n: I18n):
+ self._i18n = i18n
+ self._re_download: Optional[Pattern] = None
+
+ @property
+ def re_download(self) -> Pattern:
+ if self._re_download is None:
+ self._re_download = re.compile(r'Get:\s+\d+\s+https?://(.+)')
+
+ return self._re_download
+
+ @contextmanager
+ def start(self, watcher: ProcessWatcher, targets: Iterable[str], action: AptitudeAction):
+ handler = AptitudeOutputHandler(i18n=self._i18n, re_download=self.re_download,
+ watcher=watcher, targets=targets, action=action)
+ handler.start()
+
+ yield handler.handle
+
+ handler.stop_working()
+ handler.join()
diff --git a/bauh/gems/debian/common.py b/bauh/gems/debian/common.py
new file mode 100644
index 00000000..910525ea
--- /dev/null
+++ b/bauh/gems/debian/common.py
@@ -0,0 +1,32 @@
+from typing import Dict, Optional
+
+from bauh.gems.debian.model import DebianPackage
+
+
+def strip_maintainer_email(maintainer: str) -> str:
+ return maintainer.split('<')[0].strip()
+
+
+def strip_section(section: str) -> Optional[str]:
+ if section:
+ section_split = section.split('/')
+ return section_split[1] if len(section_split) > 1 else section
+
+
+def fill_show_data(pkg: DebianPackage, data: Dict[str, object]):
+ if data:
+ for attr, val in data.items():
+ final_attr = attr.replace(' ', '_')
+
+ if not val or val == '':
+ final_val = None
+ else:
+ if attr == 'maintainer':
+ final_val = strip_maintainer_email(str(val))
+ elif attr == 'section':
+ final_attr = 'categories'
+ final_val = (strip_section(str(val)),)
+ else:
+ final_val = val
+
+ setattr(pkg, final_attr, final_val)
diff --git a/bauh/gems/debian/config.py b/bauh/gems/debian/config.py
new file mode 100644
index 00000000..c2307ca1
--- /dev/null
+++ b/bauh/gems/debian/config.py
@@ -0,0 +1,16 @@
+from bauh.commons.config import YAMLConfigManager
+from bauh.gems.debian import CONFIG_FILE
+
+
+class DebianConfigManager(YAMLConfigManager):
+
+ def __init__(self):
+ super(DebianConfigManager, self).__init__(config_file_path=CONFIG_FILE)
+
+ def get_default_config(self) -> dict:
+ return {
+ 'suggestions.exp': 24, # hours
+ 'index_apps.exp': 1440, # 24 hours
+ 'sync_pkgs.time': 1440, # 24 hours
+ 'pkg_sources.app': None
+ }
diff --git a/bauh/gems/debian/controller.py b/bauh/gems/debian/controller.py
new file mode 100644
index 00000000..c2b5ded4
--- /dev/null
+++ b/bauh/gems/debian/controller.py
@@ -0,0 +1,857 @@
+import os.path
+import shutil
+import traceback
+from operator import attrgetter
+from pathlib import Path
+from shutil import which
+from subprocess import Popen
+from threading import Thread
+from typing import List, Optional, Tuple, Set, Type, Dict, Iterable, Generator
+
+from bauh.api.abstract.context import ApplicationContext
+from bauh.api.abstract.controller import SoftwareManager, SoftwareAction, TransactionResult, UpgradeRequirements, \
+ SearchResult, UpgradeRequirement
+from bauh.api.abstract.disk import DiskCacheLoader
+from bauh.api.abstract.handler import TaskManager, ProcessWatcher
+from bauh.api.abstract.model import SoftwarePackage, PackageSuggestion, PackageUpdate, PackageHistory, \
+ CustomSoftwareAction
+from bauh.api.abstract.view import ViewComponent, TextInputComponent, PanelComponent, FormComponent, MessageType, \
+ SingleSelectComponent, InputOption, SelectViewType
+from bauh.api.paths import CONFIG_DIR
+from bauh.commons.html import bold
+from bauh.commons.system import get_human_size_str, ProcessHandler
+from bauh.commons.util import NullLoggerFactory
+from bauh.gems.debian import DEBIAN_ICON_PATH
+from bauh.gems.debian.aptitude import Aptitude, AptitudeOutputHandlerFactory, AptitudeAction
+from bauh.gems.debian.common import fill_show_data
+from bauh.gems.debian.config import DebianConfigManager
+from bauh.gems.debian.gui import DebianViewBridge
+from bauh.gems.debian.index import ApplicationIndexer, ApplicationIndexError, ApplicationsMapper
+from bauh.gems.debian.model import DebianPackage, DebianApplication
+from bauh.gems.debian.suggestions import DebianSuggestionsDownloader
+from bauh.gems.debian.tasks import UpdateApplicationIndex, MapApplications, SynchronizePackages
+
+
+class DebianPackageManager(SoftwareManager):
+
+ def __init__(self, context: ApplicationContext):
+ super(DebianPackageManager, self).__init__(context)
+ self._i18n = context.i18n
+ self._log = context.logger
+ self._types: Optional[Set[Type[SoftwarePackage]]] = None
+ self._enabled = True
+ self._app_indexer: Optional[ApplicationIndexer] = None
+ self._apps_index: Optional[Dict[str, DebianApplication]] = None
+ self._configman: Optional[DebianConfigManager] = None
+ self._action_launch_sources: Optional[CustomSoftwareAction] = None
+ self._default_actions: Optional[Iterable[CustomSoftwareAction]] = None
+ self._view: Optional[DebianViewBridge] = None
+ self._app_mapper: Optional[ApplicationsMapper] = None
+ self._aptitude: Optional[Aptitude] = None
+ self._output_handler: Optional[AptitudeOutputHandlerFactory] = None
+ self._known_sources_apps: Optional[Tuple[str, ...]] = None
+ self._install_show_attrs: Optional[Set[str]] = None
+ self._file_ignored_updates: Optional[str] = None
+ self._suggestions_downloader: Optional[DebianSuggestionsDownloader] = None
+
+ def _update_apps_index(self, apps: Iterable[DebianApplication]):
+ self._apps_index = {app.name: app for app in apps} if apps else dict()
+
+ def search(self, words: str, disk_loader: Optional[DiskCacheLoader], limit: int, is_url: bool) -> SearchResult:
+ res = SearchResult.empty()
+
+ if not is_url:
+ for pkg in self.aptitude.search(words):
+ if pkg.installed:
+ pkg.bind_app(self.apps_index.get(pkg.name))
+ res.installed.append(pkg)
+ else:
+ res.new.append(pkg)
+
+ return res
+
+ def _fill_ignored_updates(self, output: Set[str]):
+
+ try:
+ with open(self.file_ignored_updates) as f:
+ ignored_str = f.read()
+ except FileNotFoundError:
+ return
+
+ if ignored_str:
+ for line in ignored_str.split('\n'):
+ line_clean = line.strip()
+
+ if line_clean:
+ output.add(line_clean)
+
+ def read_installed(self, disk_loader: Optional[DiskCacheLoader], pkg_types: Optional[Set[Type[SoftwarePackage]]],
+ internet_available: bool, limit: int = -1, only_apps: bool = False,
+ names: Optional[Iterable[str]] = None) -> SearchResult:
+
+ ignored_updates = set()
+ fill_ignored_updates = Thread(target=self._fill_ignored_updates, args=(ignored_updates,))
+ fill_ignored_updates.start()
+
+ res = SearchResult(installed=[], new=None, total=0)
+
+ for pkg in self.aptitude.read_installed():
+ if fill_ignored_updates.is_alive():
+ fill_ignored_updates.join()
+
+ pkg.bind_app(self.apps_index.get(pkg.name))
+ pkg.updates_ignored = bool(ignored_updates and pkg.name in ignored_updates)
+ res.installed.append(pkg)
+
+ return res
+
+ def downgrade(self, pkg: SoftwarePackage, root_password: str, handler: ProcessWatcher) -> bool:
+ return False
+
+ def upgrade(self, requirements: UpgradeRequirements, root_password: str, watcher: ProcessWatcher) -> bool:
+ handler = ProcessHandler(watcher)
+
+ targets = (r.pkg.name for r in (*requirements.to_upgrade, *(requirements.to_install or ())))
+
+ with self.output_handler.start(watcher=watcher, targets=targets, action=AptitudeAction.UPGRADE) as handle:
+ to_upgrade = (r.pkg.name for r in requirements.to_upgrade)
+ success, _ = handler.handle_simple(self.aptitude.upgrade(packages=to_upgrade,
+ root_password=root_password),
+ output_handler=handle)
+ return success
+
+ def _fill_updates(self, output: Dict[str, str]):
+ for name, version in self.aptitude.read_updates():
+ output[name] = version
+
+ def uninstall(self, pkg: DebianPackage, root_password: str, watcher: ProcessWatcher,
+ disk_loader: Optional[DiskCacheLoader], purge: bool = False) -> TransactionResult:
+
+ watcher.change_substatus(self._i18n['debian.simulate_operation'])
+
+ transaction = self.aptitude.simulate_removal((pkg.name,), purge=purge)
+
+ if not transaction or not transaction.to_remove:
+ return TransactionResult.fail()
+
+ if pkg not in transaction.to_remove:
+ watcher.show_message(title=self._i18n['popup.title.error'],
+ body=self._i18n['debian.remove.impossible'].format(pkg=bold(pkg.name)),
+ type_=MessageType.ERROR)
+ return TransactionResult.fail()
+
+ watcher.change_substatus('')
+
+ deps = tuple(p for p in transaction.to_remove if p.name != pkg.name)
+
+ if deps:
+ # updates are required to be filled in case the dependencies are currently displayed on the view
+ updates = dict()
+ fill_updates = Thread(target=self._fill_updates, args=(updates,))
+ fill_updates.start()
+
+ deps_data = self.aptitude.show((p.name for p in deps), attrs=('description', 'maintainer', 'section'))
+
+ if deps_data:
+ for p in deps:
+ fill_show_data(p, deps_data.get(p.name))
+
+ if not self.view.confirm_removal(source_pkg=pkg.name, dependencies=deps, watcher=watcher):
+ return TransactionResult.fail()
+
+ fill_updates.join()
+
+ if updates:
+ for p in deps:
+ latest_version = updates.get(p.name)
+
+ if latest_version is not None and p.version != latest_version:
+ p.latest_version = latest_version
+ p.update = True
+
+ watcher.change_substatus(self._i18n['debian.uninstall.removing'])
+
+ handler = ProcessHandler(watcher)
+ to_remove = tuple(p.name for p in transaction.to_remove)
+ with self.output_handler.start(watcher=watcher, targets=to_remove, action=AptitudeAction.REMOVE) as handle:
+ removed, _ = handler.handle_simple(self.aptitude.remove(packages=to_remove, root_password=root_password,
+ purge=purge),
+ output_handler=handle)
+
+ if not removed:
+ return TransactionResult.fail()
+
+ watcher.change_substatus(self._i18n['debian.uninstall.validating'])
+
+ current_installed_names = set(self.aptitude.read_installed_names())
+
+ watcher.change_substatus('')
+
+ all_removed, apps_removed, not_removed_names = [], set(), set()
+
+ for p in transaction.to_remove:
+ if p.name not in current_installed_names:
+ instance = p if p != pkg else pkg
+
+ all_removed.append(instance)
+
+ if instance.app:
+ apps_removed.add(instance.app)
+
+ instance.installed = False
+ instance.version = instance.latest_version
+ instance.update = False
+ instance.bind_app(None)
+ else:
+ not_removed_names.add(p.name)
+
+ if apps_removed: # updating apps index
+ watcher.print(self._i18n['debian.app_index.updating'] + ' ...')
+ watcher.change_substatus(self._i18n['debian.app_index.updating'])
+ indexed_apps = set(self.app_indexer.read_index())
+
+ if indexed_apps:
+ new_index = indexed_apps.difference(apps_removed)
+ try:
+ self.app_indexer.update_index(new_index, update_timestamp=False)
+ self._update_apps_index(new_index)
+ self._log.info(f"Debian applications removed from the index: "
+ f"{', '.join((a.name for a in apps_removed))}")
+ except ApplicationIndexError:
+ pass
+
+ watcher.change_substatus('')
+
+ success = True
+ if not_removed_names:
+ success = pkg.name not in not_removed_names
+ not_removed_str = ', '.join((bold(p) for p in sorted(not_removed_names)))
+ watcher.show_message(title=self._i18n[f"popup.title.{'warning' if success else 'error'}"],
+ body=self._i18n['debian.uninstall.failed_to_remove'].format(no=len(not_removed_names),
+ pkgs=not_removed_str),
+ type_=MessageType.WARNING if success else MessageType.ERROR)
+
+ return TransactionResult(success=success, installed=None, removed=all_removed)
+
+ def _map_dependents(self, packages_data: Dict[str, Dict[str, object]]) -> Optional[Dict[str, Set[str]]]:
+ if packages_data:
+ dependents = None
+ if packages_data:
+ dependents = dict()
+
+ for p, data in packages_data.items():
+ for attr in ('depends', 'predepends'):
+ dep_exps = data.get(attr)
+
+ if isinstance(dep_exps, tuple):
+ for exp in dep_exps:
+ dep = exp.split(' ')[0].strip()
+ dep_dependents = dependents.get(dep)
+ if dep_dependents is None:
+ dep_dependents = set()
+ dependents[dep] = dep_dependents
+
+ dep_dependents.add(p)
+
+ return dependents
+
+ def get_upgrade_requirements(self, pkgs: List[DebianPackage], root_password: str, watcher: ProcessWatcher) \
+ -> UpgradeRequirements:
+
+ transaction = self.aptitude.simulate_upgrade((p.name for p in pkgs))
+
+ if transaction:
+ size_to_query = (f'{p.name}={p.latest_version}' for p in (*transaction.to_upgrade,
+ *transaction.to_install,
+ *transaction.to_remove))
+
+ update_extra_attrs = ('compressed size', *(('depends', 'predepends') if transaction.to_install else ()))
+ update_data = self.aptitude.show(pkgs=size_to_query, attrs=update_extra_attrs)
+
+ to_install = None
+ if transaction.to_install:
+ dependents = self._map_dependents(update_data)
+
+ to_install = []
+ for p in transaction.to_install:
+ p_data = update_data.get(p.name) if update_data else None
+ req_size = p_data.get('compressed size') if p_data else None
+
+ reason = None
+ if dependents:
+ p_dependents = dependents.get(p.name)
+
+ if p_dependents:
+ deps_str = ', '.join((bold(d) for d in p_dependents))
+ reason = self._i18n['debian.transaction.dependency_of'].format(pkgs=deps_str)
+
+ to_install.append(UpgradeRequirement(pkg=p, reason=reason, required_size=req_size,
+ extra_size=p.transaction_size))
+
+ to_install.sort(key=attrgetter('pkg.name'))
+ to_install = tuple(to_install)
+
+ to_remove = None
+ if transaction.to_remove:
+ to_remove = []
+
+ for p in transaction.to_remove:
+ to_remove.append(UpgradeRequirement(pkg=p, required_size=0, extra_size=p.transaction_size))
+
+ to_remove.sort(key=attrgetter('pkg.name'))
+ to_remove = tuple(to_remove)
+
+ to_upgrade = None
+
+ if transaction.to_upgrade:
+ to_upgrade = []
+
+ for p in transaction.to_upgrade:
+ p_data = update_data.get(p.name) if update_data else None
+
+ if p_data:
+ req_size = p_data.get('compressed size')
+ else:
+ req_size = None
+
+ to_upgrade.append(UpgradeRequirement(pkg=p, required_size=req_size, extra_size=p.transaction_size))
+
+ to_upgrade.sort(key=attrgetter('pkg.name'))
+ to_upgrade = tuple(to_upgrade)
+
+ return UpgradeRequirements(to_install=to_install, to_upgrade=to_upgrade,
+ to_remove=to_remove, cannot_upgrade=None)
+ else:
+ cannot_upgrade = [UpgradeRequirement(pkg=p, reason=self._i18n['error']) for p in pkgs]
+ cannot_upgrade.sort(key=attrgetter('pkg.name'))
+ return UpgradeRequirements(cannot_upgrade=cannot_upgrade, to_install=None, to_remove=None,
+ to_upgrade=[])
+
+ def get_managed_types(self) -> Set[Type[SoftwarePackage]]:
+ if self._types is None:
+ self._types = {DebianPackage}
+
+ return self._types
+
+ def get_info(self, pkg: DebianPackage) -> Optional[dict]:
+ info = {'00.name': pkg.name, '01.version': pkg.version,
+ "02.description": pkg.description}
+
+ if pkg.installed and pkg.app:
+ info['03.exec'] = pkg.app.exe_path
+
+ extra_info = self.aptitude.show((f'{pkg.name}={pkg.version}',), verbose=True)
+
+ if extra_info:
+ extra_info = extra_info.get(pkg.name)
+
+ if extra_info:
+ ignored_fields = {'package', 'version', 'description'}
+
+ for field, val in extra_info.items():
+ if field not in ignored_fields and not field.startswith('description'):
+ final_val = get_human_size_str(val) if field in self.aptitude.size_attrs else val
+ final_field = f'04.{field}'
+
+ if final_field not in self._i18n:
+ self._i18n.default[final_field] = field
+
+ info[final_field] = final_val # for sorting
+
+ return info
+
+ def get_history(self, pkg: SoftwarePackage) -> PackageHistory:
+ pass
+
+ def install(self, pkg: SoftwarePackage, root_password: str, disk_loader: Optional[DiskCacheLoader],
+ watcher: ProcessWatcher) -> TransactionResult:
+
+ watcher.change_substatus(self._i18n['debian.simulate_operation'])
+ transaction = self.aptitude.simulate_installation((pkg.name, ))
+
+ if transaction is None or not transaction.to_install:
+ return TransactionResult.fail()
+
+ if transaction.to_remove or (transaction.to_install and len(transaction.to_install) > 1):
+ watcher.change_substatus(self._i18n['debian.transaction.get_data'])
+
+ deps = tuple(p for p in transaction.to_install or () if p.name != pkg.name)
+ removal = tuple(p for p in transaction.to_remove or ())
+ all_pkgs = [*deps, *removal]
+
+ pkgs_data = self.aptitude.show(pkgs=(f'{d.name}={d.version}' for d in all_pkgs),
+ attrs=self.install_show_attrs)
+
+ if pkgs_data:
+ for p in all_pkgs:
+ fill_show_data(p, pkgs_data.get(p.name))
+
+ if not self.view.confirm_transaction(to_install=deps, removal=removal, watcher=watcher):
+ return TransactionResult.fail()
+
+ watcher.change_substatus(self._i18n['debian.installing_pkgs'])
+ handler = ProcessHandler(watcher)
+
+ targets = (p.name for p in transaction.to_install)
+ with self.output_handler.start(watcher=watcher, targets=targets, action=AptitudeAction.INSTALL) as handle:
+ installed, _ = handler.handle_simple(self.aptitude.install(packages=(pkg.name,),
+ root_password=root_password),
+ output_handler=handle)
+
+ if installed:
+ self._refresh_apps_index(watcher)
+
+ watcher.change_substatus(self._i18n['debian.install.validating'])
+
+ currently_installed = set(self.aptitude.read_installed_names())
+
+ installed_instances = []
+ if currently_installed:
+ for p in transaction.to_install:
+ instance = p if p != pkg else pkg
+ if instance.name in currently_installed:
+ instance.installed = True
+ instance.bind_app(self.apps_index.get(instance.name))
+ installed_instances.append(instance)
+
+ removed = None
+
+ if transaction.to_remove:
+ removed = [p for p in transaction.to_remove if p.name not in currently_installed]
+
+ not_removed = set(transaction.to_remove).difference(removed)
+
+ if not_removed:
+ not_removed_str = ' '.join(p.name for p in not_removed)
+ self._log.warning(f"The following packages were not removed: {not_removed_str}")
+
+ return TransactionResult(installed=installed_instances, removed=removed,
+ success=bool(installed_instances and pkg in installed_instances))
+ else:
+ watcher.change_substatus('')
+ return TransactionResult.fail()
+
+ def _refresh_apps_index(self, watcher: ProcessWatcher):
+ watcher.change_substatus(self._i18n['debian.app_index.checking'])
+ self._log.info("Reading the cached Debian applications")
+ indexed_apps = self.app_indexer.read_index()
+ self._log.info("Mapping the Debian applications")
+ current_apps = self.app_mapper.map_executable_applications()
+
+ if current_apps != indexed_apps:
+ watcher.print(self._i18n['debian.app_index.updating'] + '...')
+ watcher.change_substatus(self._i18n['debian.app_index.updating'])
+
+ try:
+ self.app_indexer.update_index(current_apps)
+ self._update_apps_index(current_apps)
+
+ if indexed_apps is not None:
+ new_apps = current_apps.difference(indexed_apps)
+
+ if new_apps:
+ self._log.info(f"Debian applications added to the index: "
+ f"{','.join((a.name for a in new_apps))}")
+
+ except ApplicationIndexError:
+ pass
+
+ def is_enabled(self) -> bool:
+ return self._enabled
+
+ def set_enabled(self, enabled: bool):
+ self._enabled = enabled
+
+ def can_work(self) -> Tuple[bool, Optional[str]]:
+ if not which('aptitude'):
+ return False, self._i18n['missing_dep'].format(dep=bold('aptitude'))
+
+ return True, None
+
+ def requires_root(self, action: SoftwareAction, pkg: Optional[SoftwarePackage]) -> bool:
+ if action == action.PREPARE:
+ deb_config = self.configman.get_config()
+ return SynchronizePackages.should_synchronize(deb_config, NullLoggerFactory.logger())
+
+ return action != SoftwareAction.SEARCH
+
+ def prepare(self, task_manager: Optional[TaskManager], root_password: Optional[str],
+ internet_available: Optional[bool]):
+
+ deb_config = self.configman.get_config()
+
+ if SynchronizePackages.should_synchronize(deb_config, self._log):
+ sync_pkgs = SynchronizePackages(taskman=task_manager, i18n=self._i18n, logger=self._log,
+ root_password=root_password, aptitude=self.aptitude)
+ sync_pkgs.start()
+
+ if DebianSuggestionsDownloader.should_download(deb_config, self._log, only_positive_exp=True):
+ self.suggestions_downloader.register_task(task_manager)
+ self.suggestions_downloader.start()
+
+ self.index_apps(root_password=root_password, watcher=None, taskman=task_manager,
+ deb_config=deb_config, check_expiration=True)
+
+ def list_updates(self, internet_available: bool) -> List[PackageUpdate]:
+ ignored_updates = set()
+ fill_ignored_updates = Thread(target=self._fill_ignored_updates, args=(ignored_updates,))
+ fill_ignored_updates.start()
+
+ updates = list()
+ for name, version in self.aptitude.read_updates():
+ if fill_ignored_updates.is_alive():
+ fill_ignored_updates.join()
+
+ if name not in ignored_updates:
+ updates.append(PackageUpdate(pkg_id=name, name=name, version=version, pkg_type='debian'))
+
+ return updates
+
+ def list_warnings(self, internet_available: bool) -> Optional[List[str]]:
+ pass
+
+ def _fill_installed_names(self, output: Set[str]):
+ output.update(self.aptitude.read_installed_names())
+
+ def _fill_suggestions(self, output: Dict[str, int]):
+ self.suggestions_downloader.register_task(None)
+ suggestions = self.suggestions_downloader.read(self.configman.read_config())
+
+ if suggestions:
+ output.update(suggestions)
+
+ def list_suggestions(self, limit: int, filter_installed: bool) -> List[PackageSuggestion]:
+ name_priority = dict()
+
+ fill_suggestions = Thread(target=self._fill_suggestions, args=(name_priority,))
+ fill_suggestions.start()
+
+ if filter_installed:
+ installed = set()
+ fill_installed = Thread(target=self._fill_installed_names, args=(installed, ))
+ fill_installed.start()
+ else:
+ installed, fill_installed = None, None
+
+ fill_suggestions.join()
+
+ if fill_installed:
+ fill_installed.join()
+
+ if not name_priority:
+ self._log.info("No Debian package suggestions found")
+ return []
+
+ self._log.info(f"Found {len(name_priority)} Debian package suggestions")
+
+ to_load = tuple(name_priority.keys()) if not installed else {*name_priority.keys()}.difference(installed)
+
+ if not to_load:
+ return []
+
+ suggestions = []
+
+ for pkg in self.aptitude.search_by_name(to_load):
+ prio = name_priority.get(pkg.name)
+
+ if prio:
+ suggestions.append(PackageSuggestion(package=pkg, priority=prio))
+
+ return suggestions
+
+ def is_default_enabled(self) -> bool:
+ return True
+
+ def launch(self, pkg: SoftwarePackage):
+ if isinstance(pkg, DebianPackage):
+ final_cmd = pkg.app.exe_path.replace('%U', '')
+ Popen(final_cmd, shell=True)
+
+ def get_settings(self, screen_width: int, screen_height: int) -> Optional[ViewComponent]:
+ deb_config = self.configman.get_config()
+
+ comps_width = int(screen_width * 0.105)
+
+ sources_app = deb_config.get('pkg_sources.app')
+
+ if isinstance(sources_app, str) and sources_app not in self.known_sources_apps:
+ self._log.warning(f"'pkg_sources.app' ({sources_app}) is not supported. A 'None' value will be considered")
+ sources_app = None
+
+ lb_source_auto = self._i18n['debian.config.pkg_sources.app.auto']
+ source_opts = [InputOption(id_='auto', value=None, label=lb_source_auto)]
+
+ source_opts.extend((InputOption(id_=a, value=a, label=a) for a in self.known_sources_apps if which(a)))
+
+ source_auto_tip = self._i18n['debian.config.pkg_sources.app.tip'].format(auto=f'"{lb_source_auto}"')
+ input_sources = SingleSelectComponent(id_='pkg_sources.app',
+ label=self._i18n['debian.config.pkg_sources.app'],
+ tooltip=source_auto_tip,
+ options=source_opts,
+ default_option=next(o for o in source_opts if o.value == sources_app),
+ type_=SelectViewType.COMBO,
+ max_width=comps_width)
+
+ try:
+ app_cache_exp = int(deb_config.get('index_apps.exp', 0))
+ except ValueError:
+ self._log.error(f"Unexpected value form Debian configuration property 'index_apps.exp': "
+ f"{deb_config['index_apps.exp']}. Zero (0) will be considered instead.")
+ app_cache_exp = 0
+
+ ti_index_apps_exp = TextInputComponent(id_='index_apps.exp',
+ label=self._i18n['debian.config.index_apps.exp'],
+ tooltip=self._i18n['debian.config.index_apps.exp.tip'],
+ value=str(app_cache_exp), only_int=True,
+ max_width=comps_width)
+
+ try:
+ sync_pkgs_time = int(deb_config.get('sync_pkgs.time', 0))
+ except ValueError:
+ self._log.error(f"Unexpected value form Debian configuration property 'sync_pkgs.time': {deb_config['sync_pkgs.time']}. "
+ f"Zero (0) will be considered instead.")
+ sync_pkgs_time = 0
+
+ ti_sync_pkgs = TextInputComponent(id_='sync_pkgs.time',
+ label=self._i18n['debian.config.sync_pkgs.time'],
+ tooltip=self._i18n['debian.config.sync_pkgs.time.tip'],
+ value=str(sync_pkgs_time), only_int=True,
+ max_width=comps_width)
+
+ try:
+ suggestions_exp = int(deb_config.get('suggestions.exp', 0))
+ except ValueError:
+ self._log.error(f"Unexpected value form Debian configuration property 'suggestions.exp': {deb_config['suggestions.exp']}. "
+ f"Zero (0) will be considered instead.")
+ suggestions_exp = 0
+
+ ti_suggestions_exp = TextInputComponent(id_='suggestions.exp',
+ label=self._i18n['debian.config.suggestions.exp'],
+ tooltip=self._i18n['debian.config.suggestions.exp.tip'],
+ value=str(suggestions_exp), only_int=True,
+ max_width=comps_width)
+
+ return PanelComponent([FormComponent([input_sources, ti_sync_pkgs, ti_index_apps_exp, ti_suggestions_exp])])
+
+ def save_settings(self, component: PanelComponent) -> Tuple[bool, Optional[List[str]]]:
+ deb_config = self.configman.get_config()
+
+ container = component.components[0]
+
+ if isinstance(container, FormComponent):
+ deb_config['pkg_sources.app'] = container.get_single_select_component('pkg_sources.app').get_selected()
+ deb_config['index_apps.exp'] = container.get_text_input('index_apps.exp').get_int_value()
+ deb_config['sync_pkgs.time'] = container.get_text_input('sync_pkgs.time').get_int_value()
+ deb_config['suggestions.exp'] = container.get_text_input('suggestions.exp').get_int_value()
+
+ try:
+ self.configman.save_config(deb_config)
+ return True, None
+ except:
+ return False, [traceback.format_exc()]
+
+ def gen_custom_actions(self) -> Generator[CustomSoftwareAction, None, None]:
+ if self._default_actions is None:
+ self._default_actions = (CustomSoftwareAction(i18n_label_key='debian.action.sync_pkgs',
+ i18n_status_key='debian.task.sync_pkgs.status',
+ i18n_description_key='debian.action.sync_pkgs.desc',
+ icon_path=DEBIAN_ICON_PATH,
+ manager_method='synchronize_packages',
+ requires_root=True),
+ CustomSoftwareAction(i18n_label_key='debian.action.index_apps',
+ i18n_status_key='debian.task.app_index.status',
+ i18n_description_key='debian.action.index_apps.desc',
+ icon_path=DEBIAN_ICON_PATH,
+ manager_method='index_apps',
+ requires_root=False)
+ )
+
+ yield from self._default_actions
+
+ for _ in self.get_installed_source_apps():
+ yield self.action_launch_sources
+ break
+
+ def _write_ignored_updates(self, packages: Iterable[str]):
+ Path(os.path.dirname(self.file_ignored_updates)).mkdir(parents=True, exist_ok=True)
+
+ with open(self.file_ignored_updates, 'w+') as f:
+ f.write('\n'.join(n for n in sorted(packages)))
+
+ def ignore_update(self, pkg: DebianPackage):
+ ignored_packages = set()
+ self._fill_ignored_updates(ignored_packages)
+
+ if pkg.name not in ignored_packages:
+ pkg.updates_ignored = True
+ ignored_packages.add(pkg.name)
+ self._write_ignored_updates(ignored_packages)
+
+ def revert_ignored_update(self, pkg: DebianPackage):
+ ignored_packages = set()
+ self._fill_ignored_updates(ignored_packages)
+
+ if pkg.name in ignored_packages:
+ pkg.updates_ignored = False
+ ignored_packages.remove(pkg.name)
+ self._write_ignored_updates(ignored_packages)
+
+ def synchronize_packages(self, root_password: Optional[str], watcher: ProcessWatcher) -> bool:
+ return SynchronizePackages(i18n=self._i18n, logger=self._log, root_password=root_password, watcher=watcher,
+ aptitude=self.aptitude, taskman=TaskManager()).run()
+
+ def index_apps(self, root_password: Optional[str], watcher: Optional[ProcessWatcher],
+ taskman: Optional[TaskManager] = None, deb_config: Optional[dict] = None,
+ check_expiration: bool = False) -> bool:
+ _config = deb_config if deb_config else self.configman.get_config()
+ _taskman = taskman if taskman else TaskManager()
+
+ map_apps = MapApplications(taskman=_taskman, app_indexer=self.app_indexer,
+ i18n=self._i18n, logger=self._log, deb_config=deb_config,
+ app_mapper=self.app_mapper, check_expiration=check_expiration,
+ watcher=watcher)
+ map_apps.start()
+
+ gen_app_index = UpdateApplicationIndex(taskman=_taskman, app_indexer=self.app_indexer,
+ i18n=self._i18n, logger=self._log,
+ mapping_apps=map_apps)
+ gen_app_index.start()
+
+ map_apps.join()
+ self._update_apps_index(map_apps.apps)
+ gen_app_index.join()
+ return True
+
+ def purge(self, pkg: DebianPackage, root_password: str, watcher: ProcessWatcher) -> bool:
+ if not self.view.confirm_purge(pkg.name, watcher):
+ return False
+
+ res = self.uninstall(pkg=pkg, root_password=root_password, watcher=watcher, disk_loader=None, purge=True)
+ return res.success
+
+ def launch_sources_app(self, root_password: str, watcher: ProcessWatcher) -> bool:
+ deb_config = self.configman.get_config()
+
+ sources_app = deb_config.get('pkg_sources.app')
+
+ if isinstance(sources_app, str) and sources_app not in self.known_sources_apps:
+ watcher.show_message(title=self._i18n['popup.title.error'],
+ body=self._i18n['debian.action.sources.unsupported'].format(app=bold(sources_app)))
+ return False
+
+ if sources_app:
+ if not which(sources_app):
+ watcher.show_message(title=self._i18n['popup.title.error'],
+ body=self._i18n['debian.action.sources.not_installed'],
+ type_=MessageType.ERROR)
+ return False
+
+ Popen(sources_app, shell=True)
+ return True
+
+ for app in self.get_installed_source_apps():
+ Popen(app, shell=True)
+ return True
+
+ watcher.show_message(title=self._i18n['popup.title.error'],
+ body=self._i18n['debian.action.sources.not_installed'],
+ type_=MessageType.ERROR)
+ return False
+
+ @property
+ def app_indexer(self) -> ApplicationIndexer:
+ if self._app_indexer is None:
+ self._app_indexer = ApplicationIndexer(self._log)
+
+ return self._app_indexer
+
+ @property
+ def apps_index(self) -> Dict[str, DebianApplication]:
+ if self._apps_index is None:
+ self._update_apps_index(self.app_indexer.read_index())
+
+ return self._apps_index
+
+ @property
+ def install_show_attrs(self) -> Set[str]:
+ if self._install_show_attrs is None:
+ self._install_show_attrs = {'description', 'maintainer', 'section', 'compressed size'}
+
+ return self._install_show_attrs
+
+ @property
+ def configman(self) -> DebianConfigManager:
+ if self._configman is None:
+ self._configman = DebianConfigManager()
+
+ return self._configman
+
+ @property
+ def view(self) -> DebianViewBridge:
+ if self._view is None:
+ self._view = DebianViewBridge(screen_width=self.context.screen_width,
+ screen_heigth=self.context.screen_height,
+ i18n=self._i18n)
+
+ return self._view
+
+ @property
+ def app_mapper(self) -> ApplicationsMapper:
+ if self._app_mapper is None:
+ self._app_mapper = ApplicationsMapper(logger=self._log)
+
+ return self._app_mapper
+
+ @property
+ def aptitude(self) -> Aptitude:
+ if self._aptitude is None:
+ self._aptitude = Aptitude(self._log)
+
+ return self._aptitude
+
+ @property
+ def output_handler(self) -> AptitudeOutputHandlerFactory:
+ if self._output_handler is None:
+ self._output_handler = AptitudeOutputHandlerFactory(i18n=self._i18n)
+
+ return self._output_handler
+
+ @property
+ def action_launch_sources(self) -> CustomSoftwareAction:
+ if self._action_launch_sources is None:
+ self._action_launch_sources = CustomSoftwareAction(i18n_label_key='debian.action.sources',
+ i18n_status_key='debian.task.sources.status',
+ i18n_description_key='debian.action.sources.desc',
+ icon_path=DEBIAN_ICON_PATH,
+ manager_method='launch_sources_app',
+ requires_confirmation=False,
+ requires_root=False)
+
+ return self._action_launch_sources
+
+ @property
+ def known_sources_apps(self) -> Tuple[str, ...]:
+ if self._known_sources_apps is None:
+ self._known_sources_apps = ('software-properties-gtk',)
+
+ return self._known_sources_apps
+
+ @property
+ def file_ignored_updates(self) -> str:
+ if self._file_ignored_updates is None:
+ self._file_ignored_updates = f'{CONFIG_DIR}/debian/updates_ignored.txt'
+
+ return self._file_ignored_updates
+
+ @property
+ def suggestions_downloader(self) -> DebianSuggestionsDownloader:
+ if not self._suggestions_downloader:
+ self._suggestions_downloader = DebianSuggestionsDownloader(i18n=self._i18n, logger=self._log,
+ http_client=self.context.http_client)
+
+ return self._suggestions_downloader
+
+ def get_installed_source_apps(self) -> Generator[str, None, None]:
+ for app in self.known_sources_apps:
+ if shutil.which(app):
+ yield app
diff --git a/bauh/gems/debian/gui.py b/bauh/gems/debian/gui.py
new file mode 100644
index 00000000..aa279301
--- /dev/null
+++ b/bauh/gems/debian/gui.py
@@ -0,0 +1,140 @@
+from operator import attrgetter
+from typing import Collection, Optional, Tuple, List
+
+from bauh.api.abstract.handler import ProcessWatcher
+from bauh.api.abstract.view import InputOption, MultipleSelectComponent, TextComponent
+from bauh.commons.html import bold
+from bauh.commons.system import get_human_size_str
+from bauh.gems.debian import DEBIAN_ICON_PATH
+from bauh.gems.debian.model import DebianPackage
+from bauh.view.util.translation import I18n
+
+
+class DebianViewBridge:
+
+ def __init__(self, screen_width: int, screen_heigth: int, i18n: I18n):
+ self._i18n = i18n
+ self._width = screen_width
+ self._height = screen_heigth
+
+ def _map_to_install(self, pkgs: Optional[Collection[DebianPackage]]) -> Optional[Tuple[List[InputOption], str, str]]:
+ if pkgs:
+ download_size, install_size = 0, 0
+
+ views = []
+ for p in pkgs:
+ if p.compressed_size is not None and p.compressed_size >= 0:
+ compressed = get_human_size_str(p.compressed_size)
+ download_size += p.compressed_size
+
+ else:
+ compressed = '?'
+
+ if p.transaction_size is not None:
+ install_size += p.transaction_size
+ uncompressed = get_human_size_str(p.transaction_size)
+ else:
+ uncompressed = '?'
+
+ views.append(InputOption(label=f"{p.name} ({compressed} | {uncompressed})",
+ value=p.name, read_only=True, icon_path=DEBIAN_ICON_PATH,
+ tooltip=p.description if p.description else '?'))
+
+ dsize = get_human_size_str(download_size) if download_size > 0 else '?'
+ isize = get_human_size_str(install_size) if install_size > 0 else '?'
+ return views, dsize, isize
+
+ def _map_to_remove(self, pkgs: Optional[Collection[DebianPackage]]) -> Optional[Tuple[List[InputOption], str]]:
+ if pkgs:
+ freed_space = 0
+
+ views = []
+ for p in pkgs:
+ if p.transaction_size is not None:
+ size = p.transaction_size * (-1 if p.transaction_size < 0 else 1)
+ freed_space += size
+ uncompressed = get_human_size_str(size)
+ else:
+ uncompressed = '?'
+
+ views.append(InputOption(label=f"{p.name} (-{uncompressed})",
+ value=p.name, read_only=True, icon_path=DEBIAN_ICON_PATH,
+ tooltip=p.description if p.description else '?'))
+
+ return views, f'-{get_human_size_str(freed_space)}' if freed_space > 0 else '?'
+
+ def confirm_transaction(self, to_install: Optional[Collection[DebianPackage]],
+ removal: Optional[Collection[DebianPackage]],
+ watcher: ProcessWatcher) -> bool:
+
+ components = []
+
+ to_remove_data = self._map_to_remove(removal)
+
+ text_width, select_width = 672, 595
+
+ if to_remove_data:
+ to_remove_data[0].sort(key=attrgetter('label'))
+ lb_rem = self._i18n['debian.transaction.to_remove'].format(no=bold(str(len(to_remove_data[0]))),
+ fspace=bold(to_remove_data[1]))
+
+ components.append(TextComponent(html=lb_rem, min_width=text_width))
+ components.append(MultipleSelectComponent(id_='rem', options=to_remove_data[0], label=None,
+ default_options={*to_remove_data[0]},
+ max_width=select_width))
+
+ to_install_data = self._map_to_install(to_install)
+
+ if to_install_data:
+ to_install_data[0].sort(key=attrgetter('label'))
+ lb_deps = self._i18n['debian.transaction.to_install'].format(no=bold(str(len(to_install_data[0]))),
+ dsize=bold(to_install_data[1]),
+ isize=bold(to_install_data[2]))
+
+ components.append(TextComponent(html=f'
{lb_deps}', min_width=text_width))
+ components.append(MultipleSelectComponent(id_='inst', label='', options=to_install_data[0],
+ default_options={*to_install_data[0]},
+ max_width=select_width))
+
+ return watcher.request_confirmation(title=self._i18n['debian.transaction.title'],
+ components=components,
+ confirmation_label=self._i18n['popup.button.continue'],
+ deny_label=self._i18n['popup.button.cancel'],
+ body=None,
+ min_width=text_width,
+ min_height=54)
+
+ def confirm_removal(self, source_pkg: str, dependencies: Collection[DebianPackage], watcher: ProcessWatcher) -> bool:
+ dep_views = []
+ freed_space = 0
+ for p in sorted(dependencies, key=attrgetter('name')):
+ if p.transaction_size is not None:
+ size = p.transaction_size * (-1 if p.transaction_size < 0 else 1)
+ freed_space += size
+ size_str = get_human_size_str(size)
+ else:
+ size_str = '?'
+
+ dep_views.append(InputOption(label=f"{p.name}: -{size_str}", value=p.name, read_only=True,
+ icon_path=DEBIAN_ICON_PATH, tooltip=p.description))
+
+ deps_container = MultipleSelectComponent(id_='deps', label='', options=dep_views, default_options={*dep_views},
+ max_width=537)
+
+ freed_space_str = bold('-' + get_human_size_str(freed_space))
+ body_text = TextComponent(html=self._i18n['debian.remove_deps'].format(no=bold(str(len(dependencies))),
+ pkg=bold(source_pkg),
+ fspace=freed_space_str),
+ min_width=653)
+
+ return watcher.request_confirmation(title=self._i18n['debian.transaction.title'],
+ components=[body_text, deps_container],
+ confirmation_label=self._i18n['popup.button.continue'],
+ deny_label=self._i18n['popup.button.cancel'],
+ body=None)
+
+ def confirm_purge(self, pkg_name: str, watcher: ProcessWatcher) -> bool:
+ msg = self._i18n['debian.action.purge.confirmation'].format(pkg=bold(pkg_name))
+ return watcher.request_confirmation(title=self._i18n['debian.action.purge'],
+ body=msg,
+ confirmation_label=self._i18n['popup.button.continue'])
diff --git a/bauh/gems/debian/index.py b/bauh/gems/debian/index.py
new file mode 100644
index 00000000..f2f8418d
--- /dev/null
+++ b/bauh/gems/debian/index.py
@@ -0,0 +1,223 @@
+import json
+import os
+import re
+import traceback
+from concurrent.futures import ThreadPoolExecutor
+from datetime import datetime, timedelta
+from json import JSONDecodeError
+from logging import Logger
+from pathlib import Path
+from typing import Optional, Set, Generator, Iterable
+
+from bauh.commons import system
+from bauh.gems.debian import APP_INDEX_FILE
+from bauh.gems.debian.model import DebianApplication
+
+
+class ApplicationIndexError(Exception):
+
+ def __init__(self, cause: Optional[str] = None):
+ self.cause = cause
+
+
+class ApplicationIndexer:
+
+ def __init__(self, logger: Logger, index_file_path: str = APP_INDEX_FILE):
+ self._log = logger
+ self._file_path = index_file_path
+ self._file_timestamp_path = f'{self._file_path}.ts'
+
+ def is_expired(self, deb_config: dict) -> bool:
+
+ try:
+ exp_minutes = int(deb_config.get('index_apps.exp', 0))
+ except ValueError:
+ self._log.error(f"Invalid value for Debian configuration property 'index_apps.exp': "
+ f"{deb_config['index_apps.exp']}")
+ return True
+
+ if exp_minutes <= 0:
+ self._log.warning("Debian applications index will always be updated ('index_apps.exp' <= 0 )'")
+ return True
+
+ if not os.path.exists(self._file_path):
+ self._log.info(f"Debian applications index not found. A new one must be generated ({self._file_path})")
+ return True
+
+ try:
+ with open(self._file_timestamp_path) as f:
+ timestamp_str = f.read().strip()
+ except FileNotFoundError:
+ self._log.info(f"Debian applications index timestamp not found ({self._file_timestamp_path})")
+ return True
+
+ try:
+ timestamp = datetime.fromtimestamp(float(timestamp_str))
+ except:
+ self._log.error(f'Could not parse the Debian applications index timestamp: {timestamp_str} '
+ f'({self._file_timestamp_path})')
+ traceback.print_exc()
+ return True
+
+ expired = timestamp + timedelta(minutes=exp_minutes) <= datetime.utcnow()
+
+ if expired:
+ self._log.info("Debian applications index has expired. A new one must be generated.")
+ else:
+ self._log.info("Debian applications index is up-to-date")
+
+ return expired
+
+ def read_index(self) -> Generator[DebianApplication, None, None]:
+ try:
+ with open(self._file_path) as f:
+ idx_str = f.read().strip()
+
+ if not idx_str:
+ self._log.warning(f"Debian applications index is empty ({self._file_path})")
+ else:
+ try:
+ for name, data in json.loads(idx_str).items():
+ exe_path, icon_path = data.get('exe_path'), data.get('icon_path')
+
+ if not all((name, exe_path, icon_path)):
+ self._log.warning(f"Invalid entry in the Debian applications index ({self._file_path}): name={name}, exe_path={exe_path}, icon_path={icon_path}")
+ else:
+ categories = data.get('categories')
+ yield DebianApplication(name=name, exe_path=exe_path, icon_path=icon_path,
+ categories=tuple(categories) if categories else None)
+
+ except JSONDecodeError:
+ self._log.error(f"The Debian applications index is corrupted ({self._file_path}). "
+ f"Could not decode the JSON.")
+
+ except FileNotFoundError:
+ self._log.warning(f"Debian applications index not found ({self._file_path})")
+ except OSError as e:
+ self._log.error(f"Debian applications index could not be read ({self._file_path}). OSError: {e.errno}")
+
+ def update_index(self, apps: Set[DebianApplication], update_timestamp: bool = True):
+ idx_dir = os.path.dirname(self._file_path)
+
+ try:
+ Path(idx_dir).mkdir(exist_ok=True, parents=True)
+ except OSError:
+ self._log.error(f"Could not create directory '{idx_dir}'")
+ raise ApplicationIndexError()
+
+ idx = {}
+ if apps:
+ for app in apps:
+ idx.update(app.to_index())
+
+ try:
+ with open(self._file_path, 'w+') as f:
+ if idx:
+ f.write(json.dumps(idx, sort_keys=True, indent=4))
+ else:
+ f.write('')
+
+ except OSError:
+ self._log.error(f"Could not write to the Debian applications index file: {self._file_path}")
+ raise ApplicationIndexError()
+
+ if update_timestamp:
+ index_timestamp = datetime.utcnow().timestamp()
+ try:
+ with open(self._file_timestamp_path, 'w+') as f:
+ f.write(str(index_timestamp))
+
+ self._log.info("Debian applications index timestamp updated")
+
+ except OSError:
+ self._log.error(f"Could not write to the Debian applications index timestamp file: "
+ f"{self._file_timestamp_path}")
+ raise ApplicationIndexError()
+
+
+class ApplicationsMapper:
+
+ def __init__(self, logger: Logger, workers: int = 10):
+ self._log = logger
+ self._re_desktop_file = re.compile(r'(.+):\s+(/usr/share/applications/.+\.desktop)')
+ self._re_desktop_file_fields = re.compile('(Exec|TryExec|Icon|Categories|NoDisplay|Terminal)\s*=\s*(.+)')
+ self._workers = workers
+
+ def _read_file(self, file_path: str) -> Optional[str]:
+ try:
+ with open(file_path) as file:
+ return file.read()
+ except (FileNotFoundError, OSError) as e:
+ self._log.error(f"Error when checking desktop file '{file_path}' ({file_path}):"
+ f" {e.__class__.__name__}")
+
+ def _add_if_application_desktop_file(self, pkg_name: str, desktop_files: Iterable[str], output: Set[DebianApplication]):
+ for file_path in sorted(desktop_files):
+ content = self._read_file(file_path)
+
+ if content:
+ data = {}
+
+ gui_app = True
+
+ for f, v in self._re_desktop_file_fields.findall(content):
+ if f in ('NoDisplay', 'Terminal') and v.strip().lower() == 'true':
+ gui_app = False
+ break
+
+ if f not in data:
+ data[f.strip()] = v.strip()
+
+ if not gui_app:
+ continue
+
+ exe = data.get('Exec')
+
+ if not exe:
+ exe = data.get('TryExec')
+
+ if not exe:
+ continue
+
+ icon = data.get('Icon')
+
+ if not icon:
+ continue
+
+ categories = data.get('Categories')
+
+ if categories:
+ categories = tuple(sorted({c.strip() for c in categories.split(';') if c}))
+
+ output.add(DebianApplication(name=pkg_name, exe_path=exe, icon_path=icon, categories=categories))
+ break
+
+ def map_executable_applications(self) -> Optional[Set[DebianApplication]]:
+ exitcode, output = system.execute('dpkg-query -S .desktop', shell=True)
+
+ if exitcode == 0 and output:
+
+ pkg_files = dict()
+
+ for found in self._re_desktop_file.findall(output):
+ pkg_name = found[0].strip()
+ files = pkg_files.get(pkg_name)
+
+ if files is None:
+ files = set()
+ pkg_files[pkg_name] = files
+
+ files.add(found[1].strip())
+
+ if pkg_files:
+ apps_found, check_jobs = set(), []
+
+ with ThreadPoolExecutor(self._workers) as pool:
+ for pkg_name, files in pkg_files.items():
+ check_jobs.append(pool.submit(self._add_if_application_desktop_file,
+ pkg_name, files, apps_found))
+
+ for job in check_jobs:
+ job.done()
+
+ return apps_found
diff --git a/bauh/gems/debian/model.py b/bauh/gems/debian/model.py
new file mode 100644
index 00000000..6fcac582
--- /dev/null
+++ b/bauh/gems/debian/model.py
@@ -0,0 +1,167 @@
+from typing import Optional, Tuple, Collection, Iterable, Generator
+
+from bauh.api.abstract.model import SoftwarePackage, CustomSoftwareAction
+from bauh.commons import resource
+from bauh.gems.debian import DEBIAN_ICON_PATH, ROOT_DIR
+
+
+class DebianApplication:
+ """
+ For packages that represent applications
+ """
+
+ def __init__(self, name: str, exe_path: str, icon_path: str, categories: Optional[Tuple] = None):
+ self.name = name
+ self.exe_path = exe_path
+ self.icon_path = icon_path
+ self.categories = categories
+
+ def __eq__(self, other):
+ if isinstance(other, DebianApplication):
+ return self.__dict__ == other.__dict__
+
+ return False
+
+ def __hash__(self) -> int:
+ hash_sum = 0
+
+ for k, v in self.__dict__.items():
+ hash_sum += hash(v)
+
+ return hash_sum
+
+ def __repr__(self):
+ return f"{self.__class__.__name__} ({', '.join((f'{k}={v}' for k, v in self.__dict__.items()))})"
+
+ def to_index(self) -> dict:
+ return {self.name: {f: v for f, v in self.__dict__.items() if f != 'name'}}
+
+
+class DebianPackage(SoftwarePackage):
+
+ __actions: Optional[Tuple[CustomSoftwareAction]] = None
+
+ @classmethod
+ def custom_actions(cls) -> Tuple[CustomSoftwareAction]:
+ if cls.__actions is None:
+ cls.__actions = ((CustomSoftwareAction(i18n_label_key='debian.action.purge',
+ i18n_status_key='debian.action.purge.status',
+ i18n_description_key='debian.action.purge.desc',
+ icon_path=resource.get_path('img/clean.svg', ROOT_DIR),
+ manager_method='purge',
+ requires_root=True,
+ requires_confirmation=False),))
+
+ return cls.__actions
+
+ def __init__(self, name: str = None, version: Optional[str] = None, latest_version: Optional[str] = None,
+ description: Optional[str] = None, maintainer: Optional[str] = None, installed: bool = False,
+ update: bool = False, app: Optional[DebianApplication] = None, compressed_size: Optional[int] = None,
+ uncompressed_size: Optional[int] = None, categories: Tuple[str] = None,
+ updates_ignored: Optional[bool] = None, transaction_size: Optional[int] = None):
+ super(DebianPackage, self).__init__(id=name, name=name, version=version, installed=installed,
+ description=description, update=update,
+ latest_version=latest_version if latest_version is not None else version)
+ self.maintainer = maintainer
+ self.compressed_size = compressed_size
+ self.uncompressed_size = uncompressed_size
+ self.categories = categories
+ self.app = app
+ self.bind_app(app)
+ self.updates_ignored = updates_ignored
+ self.transaction_size = transaction_size # size in bytes related to a transaction (install, upgrade, remove)
+
+ def bind_app(self, app: Optional[DebianApplication]):
+ self.app = app
+
+ if app and app.categories:
+ self.categories = app.categories
+
+ def has_history(self) -> bool:
+ return False
+
+ def has_screenshots(self) -> bool:
+ return False
+
+ def has_info(self) -> bool:
+ return True
+
+ def can_be_downgraded(self) -> bool:
+ return False
+
+ def get_type(self):
+ return 'debian'
+
+ def get_default_icon_path(self) -> str:
+ return self.get_type_icon_path()
+
+ def get_type_icon_path(self) -> str:
+ return DEBIAN_ICON_PATH
+
+ def is_application(self):
+ return bool(self.app)
+
+ def get_data_to_cache(self) -> dict:
+ pass
+
+ def fill_cached_data(self, data: dict):
+ pass
+
+ def can_be_run(self) -> bool:
+ return bool(self.app)
+
+ def get_publisher(self) -> str:
+ return self.maintainer
+
+ def supports_backup(self) -> bool:
+ return True
+
+ def get_disk_icon_path(self) -> Optional[str]:
+ if self.app:
+ return self.app.icon_path
+
+ def get_custom_actions(self) -> Optional[Iterable[CustomSoftwareAction]]:
+ if self.installed:
+ return self.custom_actions()
+
+ def is_update_ignored(self) -> bool:
+ return bool(self.updates_ignored)
+
+ def supports_ignored_updates(self) -> bool:
+ return True
+
+ def __eq__(self, other):
+ if isinstance(other, DebianPackage):
+ return self.name == other.name
+
+ return False
+
+ def __hash__(self):
+ return hash(self.name)
+
+ def __repr__(self) -> str:
+ attrs = ', '.join((f'{p}={v}' for p, v in sorted(self.__dict__.items())))
+ return f"{self.__class__.__name__} ({attrs})"
+
+
+class DebianTransaction:
+
+ def __init__(self, to_install: Optional[Collection[DebianPackage]],
+ to_remove: Optional[Collection[DebianPackage]],
+ to_upgrade: Optional[Collection[DebianPackage]]):
+
+ self.to_install = to_install
+ self.to_remove = to_remove
+ self.to_upgrade = to_upgrade
+
+ @property
+ def all_packages(self) -> Generator[DebianPackage, None, None]:
+ for pkgs in (self.to_install, self.to_remove, self.to_upgrade):
+ if pkgs:
+ yield from pkgs
+
+ def __eq__(self, other) -> bool:
+ return self.__dict__ == other.__dict__ if isinstance(other, DebianTransaction) else False
+
+ def __hash__(self) -> int:
+ return sum((hash(v)for v in self.__dict__.values()))
diff --git a/bauh/gems/debian/resources/img/clean.svg b/bauh/gems/debian/resources/img/clean.svg
new file mode 100644
index 00000000..fba112a9
--- /dev/null
+++ b/bauh/gems/debian/resources/img/clean.svg
@@ -0,0 +1,158 @@
+
+
diff --git a/bauh/gems/debian/resources/img/debian.svg b/bauh/gems/debian/resources/img/debian.svg
new file mode 100644
index 00000000..c112ee02
--- /dev/null
+++ b/bauh/gems/debian/resources/img/debian.svg
@@ -0,0 +1,33 @@
+
+
+
+
\ No newline at end of file
diff --git a/bauh/gems/debian/resources/locale/ca b/bauh/gems/debian/resources/locale/ca
new file mode 100644
index 00000000..79fb13f6
--- /dev/null
+++ b/bauh/gems/debian/resources/locale/ca
@@ -0,0 +1,84 @@
+debian.action.index_apps=Index applications
+debian.action.index_apps.desc=Identifies all installed packages associated with .desktop files and maps them as applications
+debian.action.purge=Remove completely
+debian.action.purge.confirmation=The complete removal of {pkg} will erase the software files and also the configuration ones
+debian.action.purge.desc=Removes all software and configuration files associated with the package
+debian.action.purge.status=Removing {} completely
+debian.action.sync_pkgs=Synchronize packages
+debian.action.sync_pkgs.desc=Synchronizes the available software packages on the repositories
+debian.action.sync_pkgs.status=Synchronizing packages
+debian.action.sources=Software sources
+debian.action.sources.desc=Starts the application for managing software sources
+debian.action.sources.not_installed=The application for managing software sources is not installed
+debian.action.sources.status=Launching application
+debian.action.sources.unsupported=The application {app} is not supported and will not be started
+debian.app_index.checking=Checking the apps index
+debian.app_index.updating=Updating the apps index
+debian.config.index_apps.exp=Apps cache expiration
+debian.config.index_apps.exp.tip=Time period in MINUTES in which the installed applications cache is considered up-to-date during startup
+debian.config.pkg_sources.app=Software sources
+debian.config.pkg_sources.app.tip=Application for managing the software sources. {auto} detects one of the supported applications automatically.
+debian.config.pkg_sources.app.auto=Auto
+debian.config.suggestions.exp=Suggestions expiration
+debian.config.suggestions.exp.tip=It defines the period (in hours) in which the suggestions stored in disc will be considered up to date. Use 0 if you always want to update them.
+debian.config.sync_pkgs.time=Packages synchronization period
+debian.config.sync_pkgs.time.tip=Time period in MINUTES in which the packages synchronization must be done on startup
+debian.info.00.name=name
+debian.info.01.version=version
+debian.info.02.description=description
+debian.info.03.exec=executable
+debian.info.04.architecture=architecture
+debian.info.04.architecture.all=all
+debian.info.04.archive=archive
+debian.info.04.automatically installed=automatically installed
+debian.info.04.automatically installed.no=no
+debian.info.04.automatically installed.yes=yes
+debian.info.04.breaks=breaks
+debian.info.04.compressed size=Download size
+debian.info.04.conflicts=conflicts
+debian.info.04.depends=dependencies
+debian.info.04.filename=file
+debian.info.04.homepage=web page
+debian.info.04.maintainer=maintainer
+debian.info.04.md5sum=MD5
+debian.info.04.multi-arch=multi-architecture
+debian.info.04.multi-arch.foreign=foreign
+debian.info.04.multi-arch.same=same
+debian.info.04.replaces=replaces
+debian.info.04.predepends=pre dependencies
+debian.info.04.priority=priority
+debian.info.04.priority.important=important
+debian.info.04.priority.optional=optional
+debian.info.04.provided by=provided by
+debian.info.04.provides=provides
+debian.info.04.recommends=recommends
+debian.info.04.section=section
+debian.info.04.state=state
+debian.info.04.state.installed=installed
+debian.info.04.state.not installed=not installed
+debian.info.04.state.not installed (configuration files remain)=not installed (configuration files installed)
+debian.info.04.suggests=suggests
+debian.info.04.uncompressed size=installation size
+debian.install.validating=Validating installed packages
+debian.installing_pkgs=Installing packages
+debian.output.downloading=Downloading {pkg}
+debian.output.removing=Removing {pkg}
+debian.output.finishing=Finishing
+debian.output.unpacking=Unpacking {pkg}
+debian.remove.impossible=It is not possible to remove the package {pkg}
+debian.remove_deps=The following {no} dependent packages of {pkg} will be removed as well ({fspace})
+debian.simulate_operation=Simulating the operation
+debian.task.app_index.status=Indexing installed applications
+debian.task.map_apps.check_files=Checking {type} files
+debian.task.map_apps.read_cache=Reading apps from the index (cache)
+debian.task.map_apps.status=Mapping installed applications
+debian.task.update_apps_idx.status=Updating index
+debian.task.sync_pkgs.status=Synchronizing packages
+debian.transaction.dependency_of=Dependency of {pkgs}
+debian.transaction.get_data=Retrieving the operation's data
+debian.transaction.title=Operation details
+debian.transaction.to_install=The following additional packages ({no}) will be installed (Download: {dsize} | Installation: {isize})
+debian.transaction.to_remove=The following packages ({no}) will be removed (Freed space: {fspace})
+debian.uninstall.failed_to_remove=It was not possible to remove the following {no} packages: {pkgs}
+debian.uninstall.removing=Removing packages
+debian.uninstall.validating=Validating removed packages
diff --git a/bauh/gems/debian/resources/locale/de b/bauh/gems/debian/resources/locale/de
new file mode 100644
index 00000000..34c27357
--- /dev/null
+++ b/bauh/gems/debian/resources/locale/de
@@ -0,0 +1,84 @@
+debian.action.index_apps=Index applications
+debian.action.index_apps.desc=Identifies all installed packages associated with .desktop files and maps them as applications
+debian.action.purge=Remove completely
+debian.action.purge.confirmation=The complete removal of {pkg} will erase the software files and also the configuration ones
+debian.action.purge.desc=Removes all software and configuration files associated with the package
+debian.action.purge.status=Removing {} completely
+debian.action.sync_pkgs=Synchronize packages
+debian.action.sync_pkgs.desc=Synchronizes the available software packages on the repositories
+debian.action.sync_pkgs.status=Synchronizing packages
+debian.action.sources=Software sources
+debian.action.sources.desc=Starts the application for managing software sources
+debian.action.sources.not_installed=The application for managing software sources is not installed
+debian.action.sources.status=Launching application
+debian.action.sources.unsupported=The application {app} is not supported and will not be started
+debian.app_index.checking=Checking the apps index
+debian.app_index.updating=Updating the apps index
+debian.config.index_apps.exp=Apps cache expiration
+debian.config.index_apps.exp.tip=Time period in MINUTES in which the installed applications cache is considered up-to-date during startup
+debian.config.pkg_sources.app=Software sources
+debian.config.pkg_sources.app.tip=Application for managing the software sources. {auto} detects one of the supported applications automatically.
+debian.config.pkg_sources.app.auto=Auto
+debian.config.suggestions.exp=Suggestions expiration
+debian.config.suggestions.exp.tip=It defines the period (in hours) in which the suggestions stored in disc will be considered up to date. Use 0 if you always want to update them.
+debian.config.sync_pkgs.time=Packages synchronization period
+debian.config.sync_pkgs.time.tip=Time period in MINUTES in which the packages synchronization must be done on startup
+debian.info.00.name=name
+debian.info.01.version=version
+debian.info.02.description=description
+debian.info.03.exec=executable
+debian.info.04.architecture=architecture
+debian.info.04.architecture.all=all
+debian.info.04.archive=archive
+debian.info.04.automatically installed=automatically installed
+debian.info.04.automatically installed.no=no
+debian.info.04.automatically installed.yes=yes
+debian.info.04.breaks=breaks
+debian.info.04.compressed size=Download size
+debian.info.04.conflicts=conflicts
+debian.info.04.depends=dependencies
+debian.info.04.filename=file
+debian.info.04.homepage=web page
+debian.info.04.maintainer=maintainer
+debian.info.04.md5sum=MD5
+debian.info.04.multi-arch=multi-architecture
+debian.info.04.multi-arch.foreign=foreign
+debian.info.04.multi-arch.same=same
+debian.info.04.replaces=replaces
+debian.info.04.predepends=pre dependencies
+debian.info.04.priority=priority
+debian.info.04.priority.important=important
+debian.info.04.priority.optional=optional
+debian.info.04.provided by=provided by
+debian.info.04.provides=provides
+debian.info.04.recommends=recommends
+debian.info.04.section=section
+debian.info.04.state=state
+debian.info.04.state.installed=installed
+debian.info.04.state.not installed=not installed
+debian.info.04.state.not installed (configuration files remain)=not installed (configuration files installed)
+debian.info.04.suggests=suggests
+debian.info.04.uncompressed size=installation size
+debian.install.validating=Validating installed packages
+debian.installing_pkgs=Installing packages
+debian.output.downloading=Downloading {pkg}
+debian.output.finishing=Finishing
+debian.output.removing=Removing {pkg}
+debian.output.unpacking=Unpacking {pkg}
+debian.remove.impossible=It is not possible to remove the package {pkg}
+debian.remove_deps=The following {no} dependent packages of {pkg} will be removed as well ({fspace})
+debian.simulate_operation=Simulating the operation
+debian.task.app_index.status=Indexing installed applications
+debian.task.map_apps.check_files=Checking {type} files
+debian.task.map_apps.read_cache=Reading apps from the index (cache)
+debian.task.map_apps.status=Mapping installed applications
+debian.task.update_apps_idx.status=Updating index
+debian.task.sync_pkgs.status=Synchronizing packages
+debian.transaction.dependency_of=Dependency of {pkgs}
+debian.transaction.get_data=Retrieving the operation's data
+debian.transaction.title=Operation details
+debian.transaction.to_install=The following additional packages ({no}) will be installed (Download: {dsize} | Installation: {isize})
+debian.transaction.to_remove=The following packages ({no}) will be removed (Freed space: {fspace})
+debian.uninstall.failed_to_remove=It was not possible to remove the following {no} packages: {pkgs}
+debian.uninstall.removing=Removing packages
+debian.uninstall.validating=Validating removed packages
diff --git a/bauh/gems/debian/resources/locale/en b/bauh/gems/debian/resources/locale/en
new file mode 100644
index 00000000..34c27357
--- /dev/null
+++ b/bauh/gems/debian/resources/locale/en
@@ -0,0 +1,84 @@
+debian.action.index_apps=Index applications
+debian.action.index_apps.desc=Identifies all installed packages associated with .desktop files and maps them as applications
+debian.action.purge=Remove completely
+debian.action.purge.confirmation=The complete removal of {pkg} will erase the software files and also the configuration ones
+debian.action.purge.desc=Removes all software and configuration files associated with the package
+debian.action.purge.status=Removing {} completely
+debian.action.sync_pkgs=Synchronize packages
+debian.action.sync_pkgs.desc=Synchronizes the available software packages on the repositories
+debian.action.sync_pkgs.status=Synchronizing packages
+debian.action.sources=Software sources
+debian.action.sources.desc=Starts the application for managing software sources
+debian.action.sources.not_installed=The application for managing software sources is not installed
+debian.action.sources.status=Launching application
+debian.action.sources.unsupported=The application {app} is not supported and will not be started
+debian.app_index.checking=Checking the apps index
+debian.app_index.updating=Updating the apps index
+debian.config.index_apps.exp=Apps cache expiration
+debian.config.index_apps.exp.tip=Time period in MINUTES in which the installed applications cache is considered up-to-date during startup
+debian.config.pkg_sources.app=Software sources
+debian.config.pkg_sources.app.tip=Application for managing the software sources. {auto} detects one of the supported applications automatically.
+debian.config.pkg_sources.app.auto=Auto
+debian.config.suggestions.exp=Suggestions expiration
+debian.config.suggestions.exp.tip=It defines the period (in hours) in which the suggestions stored in disc will be considered up to date. Use 0 if you always want to update them.
+debian.config.sync_pkgs.time=Packages synchronization period
+debian.config.sync_pkgs.time.tip=Time period in MINUTES in which the packages synchronization must be done on startup
+debian.info.00.name=name
+debian.info.01.version=version
+debian.info.02.description=description
+debian.info.03.exec=executable
+debian.info.04.architecture=architecture
+debian.info.04.architecture.all=all
+debian.info.04.archive=archive
+debian.info.04.automatically installed=automatically installed
+debian.info.04.automatically installed.no=no
+debian.info.04.automatically installed.yes=yes
+debian.info.04.breaks=breaks
+debian.info.04.compressed size=Download size
+debian.info.04.conflicts=conflicts
+debian.info.04.depends=dependencies
+debian.info.04.filename=file
+debian.info.04.homepage=web page
+debian.info.04.maintainer=maintainer
+debian.info.04.md5sum=MD5
+debian.info.04.multi-arch=multi-architecture
+debian.info.04.multi-arch.foreign=foreign
+debian.info.04.multi-arch.same=same
+debian.info.04.replaces=replaces
+debian.info.04.predepends=pre dependencies
+debian.info.04.priority=priority
+debian.info.04.priority.important=important
+debian.info.04.priority.optional=optional
+debian.info.04.provided by=provided by
+debian.info.04.provides=provides
+debian.info.04.recommends=recommends
+debian.info.04.section=section
+debian.info.04.state=state
+debian.info.04.state.installed=installed
+debian.info.04.state.not installed=not installed
+debian.info.04.state.not installed (configuration files remain)=not installed (configuration files installed)
+debian.info.04.suggests=suggests
+debian.info.04.uncompressed size=installation size
+debian.install.validating=Validating installed packages
+debian.installing_pkgs=Installing packages
+debian.output.downloading=Downloading {pkg}
+debian.output.finishing=Finishing
+debian.output.removing=Removing {pkg}
+debian.output.unpacking=Unpacking {pkg}
+debian.remove.impossible=It is not possible to remove the package {pkg}
+debian.remove_deps=The following {no} dependent packages of {pkg} will be removed as well ({fspace})
+debian.simulate_operation=Simulating the operation
+debian.task.app_index.status=Indexing installed applications
+debian.task.map_apps.check_files=Checking {type} files
+debian.task.map_apps.read_cache=Reading apps from the index (cache)
+debian.task.map_apps.status=Mapping installed applications
+debian.task.update_apps_idx.status=Updating index
+debian.task.sync_pkgs.status=Synchronizing packages
+debian.transaction.dependency_of=Dependency of {pkgs}
+debian.transaction.get_data=Retrieving the operation's data
+debian.transaction.title=Operation details
+debian.transaction.to_install=The following additional packages ({no}) will be installed (Download: {dsize} | Installation: {isize})
+debian.transaction.to_remove=The following packages ({no}) will be removed (Freed space: {fspace})
+debian.uninstall.failed_to_remove=It was not possible to remove the following {no} packages: {pkgs}
+debian.uninstall.removing=Removing packages
+debian.uninstall.validating=Validating removed packages
diff --git a/bauh/gems/debian/resources/locale/es b/bauh/gems/debian/resources/locale/es
new file mode 100644
index 00000000..305e1bc0
--- /dev/null
+++ b/bauh/gems/debian/resources/locale/es
@@ -0,0 +1,84 @@
+debian.action.index_apps=Indexar aplicaciones
+debian.action.index_apps.desc=Identifica todos los paquetes instalados asociados con archivos .desktop y los mapea como aplicaciones
+debian.action.purge=Eliminar completamente
+debian.action.purge.confirmation=La eliminación completa de {pkg} borrará los archivos de software y también los de configuración
+debian.action.purge.desc=Elimina los archivos de software y configuración asociados al paquete
+debian.action.purge.status=Eliminando {} completamente
+debian.action.sync_pkgs=Sincronizar paquetes
+debian.action.sync_pkgs.desc=Sincroniza los paquetes de software disponibles en los repositorios
+debian.action.sync_pkgs.status=Sincronizando paquetes
+debian.action.sources=Fuentes de software
+debian.action.sources.desc=Inicia la aplicación para administrar fuentes de software
+debian.action.sources.not_installed=La aplicación para administrar fuentes de software no está instalada
+debian.action.sources.status=Iniciando la aplicación
+debian.action.sources.unsupported=La aplicación {app} no es compatible y no se iniciará
+debian.app_index.checking=Verificando el índice de applicationes
+debian.app_index.updating=Actualizando el índice de aplicaciones
+debian.config.index_apps.exp=Caducidad de caché de aplicaciones
+debian.config.index_apps.exp.tip=Periodo de tiempo en MINUTOS en que el cache de las aplicaciones instaladas es considerado actualizado durante la inicialización
+debian.config.pkg_sources.app=Fuentes de software
+debian.config.pkg_sources.app.tip=Aplicación para administrar las fuentes de software. {auto} detecta una de las aplicaciones compatibles automáticamente.
+debian.config.pkg_sources.app.auto=Auto
+debian.config.suggestions.exp=Expiración de sugerencias
+debian.config.suggestions.exp.tip=Define el período (en horas) en el que la sugerencias almacenadas en disco seran consideradas actualizadas. Use 0 si desea siempre actualizarlas.
+debian.config.sync_pkgs.time=Período de sincronización de paquetes
+debian.config.sync_pkgs.time.tip=Período de tiempo en MINUTOS en que se debe realizar la sincronización de paquetes durante la inicialización
+debian.info.00.name=nombre
+debian.info.01.version=versión
+debian.info.02.description=descripción
+debian.info.03.exec=ejecutable
+debian.info.04.architecture=arquitectura
+debian.info.04.architecture.all=todas
+debian.info.04.archive=archivamiento
+debian.info.04.automatically installed=instalado automáticamente
+debian.info.04.automatically installed.no=no
+debian.info.04.automatically installed.yes=si
+debian.info.04.breaks=roturas
+debian.info.04.compressed size=Tamaño de descarga
+debian.info.04.conflicts=conflictos
+debian.info.04.depends=dependencias
+debian.info.04.filename=archivo
+debian.info.04.homepage=página web
+debian.info.04.maintainer=mantenedor
+debian.info.04.md5sum=MD5
+debian.info.04.multi-arch=multi-arquitectura
+debian.info.04.multi-arch.foreign=diferente
+debian.info.04.multi-arch.same=misma
+debian.info.04.replaces=reemplaza
+debian.info.04.predepends=pre dependencias
+debian.info.04.priority=prioridad
+debian.info.04.priority.important=importante
+debian.info.04.priority.opcional=opcional
+debian.info.04.provided by=proporcionado por
+debian.info.04.provides=proporciona
+debian.info.04.recommends=recomienda
+debian.info.04.section=sección
+debian.info.04.state=estado
+debian.info.04.state.installed=instalado
+debian.info.04.state.not installed=no instalado
+debian.info.04.state.not installed (configuration files remain)=no instalado (archivos de configuración instalados)
+debian.info.04.suggests=sugiere
+debian.info.04.uncompressed size=tamaño de instalación
+debian.install.validating=Validando paquetes instalados
+debian.installing_pkgs=Instalando paquetes
+debian.output.downloading=Descargando {pkg}
+debian.output.finishing=Finalizando
+debian.output.removing=Eliminando {pkg}
+debian.output.unpacking=Desempacando {pkg}
+debian.remove.impossible=No es posible eliminar el paquete {pkg}
+debian.remove_deps=Los siguientes {no} paquetes dependientes de {pkg} serán eliminados también ({fspace})
+debian.simulate_operation=Simulando la operación
+debian.task.app_index.status=Indexando aplicaciones instaladas
+debian.task.map_apps.check_files=Comprobando archivos {type}
+debian.task.map_apps.read_cache=Leyendo aplicaciones del índice (cache)
+debian.task.map_apps.status=Mapeando las aplicaciones instaladas
+debian.task.update_apps_idx.status=Actualizando índice
+debian.task.sync_pkgs.status=Sincronizando paquetes
+debian.transaction.dependency_of=Dependencia de {pkgs}
+debian.transaction.get_data=Recuperando datos de la operación
+debian.transaction.title=Detalles de la operación
+debian.transaction.to_install=Los siguientes paquetes adicionales ({no}) serán instalados (Descarga: {dsize} | Instalación: {isize})
+debian.transaction.to_remove=Los siguientes paquetes ({no}) serán eliminados (Espacio liberado: {fspace})
+debian.uninstall.failed_to_remove=No fue posible eliminar los siguientes {no} paquetes: {pkgs}
+debian.uninstall.removing=Removendo paquetes
+debian.uninstall.validating=Validando paquetes eliminados
diff --git a/bauh/gems/debian/resources/locale/fr b/bauh/gems/debian/resources/locale/fr
new file mode 100644
index 00000000..352b9660
--- /dev/null
+++ b/bauh/gems/debian/resources/locale/fr
@@ -0,0 +1,83 @@
+debian.action.index_apps=Index applications
+debian.action.index_apps.desc=Identifies all installed packages associated with .desktop files and maps them as applications
+debian.action.purge=Remove completely
+debian.action.purge.confirmation=The complete removal of {pkg} will erase the software files and also the configuration ones
+debian.action.purge.desc=Removes all software and configuration files associated with the package
+debian.action.purge.status=Removing {} completely
+debian.action.sync_pkgs=Synchronize packages
+debian.action.sync_pkgs.desc=Synchronizes the available software packages on the repositories
+debian.action.sync_pkgs.status=Synchronizing packages
+debian.action.sources=Software sources
+debian.action.sources.desc=Starts the application for managing software sources
+debian.action.sources.not_installed=The application for managing software sources is not installed
+debian.action.sources.status=Launching application
+debian.action.sources.unsupported=The application {app} is not supported and will not be started
+debian.app_index.checking=Checking the apps index
+debian.app_index.updating=Updating the apps index
+debian.config.index_apps.exp=Apps cache expiration
+debian.config.index_apps.exp.tip=Time period in MINUTES in which the installed applications cache is considered up-to-date during startup
+debian.config.pkg_sources.app=Software sources
+debian.config.pkg_sources.app.tip=Application for managing the software sources. {auto} detects one of the supported applications automatically.
+debian.config.pkg_sources.app.auto=Auto
+debian.config.suggestions.exp=Suggestions expiration
+debian.config.suggestions.exp.tip=It defines the period (in hours) in which the suggestions stored in disc will be considered up to date. Use 0 if you always want to update them.
+debian.config.sync_pkgs.time=Packages synchronization period
+debian.config.sync_pkgs.time.tip=Time period in MINUTES in which the packages synchronization must be done on startup
+debian.info.00.name=name
+debian.info.01.version=version
+debian.info.02.description=description
+debian.info.03.exec=executable
+debian.info.04.architecture=architecture
+debian.info.04.architecture.all=all
+debian.info.04.archive=archive
+debian.info.04.automatically installed=automatically installed
+debian.info.04.automatically installed.no=no
+debian.info.04.automatically installed.yes=yes
+debian.info.04.breaks=breaks
+debian.info.04.compressed size=Download size
+debian.info.04.conflicts=conflicts
+debian.info.04.depends=dependencies
+debian.info.04.filename=file
+debian.info.04.homepage=web page
+debian.info.04.maintainer=maintainer
+debian.info.04.md5sum=MD5
+debian.info.04.multi-arch=multi-architecture
+debian.info.04.multi-arch.foreign=foreign
+debian.info.04.multi-arch.same=same
+debian.info.04.replaces=replaces
+debian.info.04.predepends=pre dependencies
+debian.info.04.priority=priority
+debian.info.04.priority.important=important
+debian.info.04.priority.optional=optional
+debian.info.04.provided by=provided by
+debian.info.04.provides=provides
+debian.info.04.recommends=recommends
+debian.info.04.section=section
+debian.info.04.state=state
+debian.info.04.state.installed=installed
+debian.info.04.state.not installed=not installed
+debian.info.04.state.not installed (configuration files remain)=not installed (configuration files installed)
+debian.info.04.suggests=suggests
+debian.info.04.uncompressed size=installation size
+debian.install.validating=Validating installed packages
+debian.output.downloading=Downloading {pkg}
+debian.output.finishing=Finishing
+debian.output.removing=Removing {pkg}
+debian.output.unpacking=Unpacking {pkg}
+debian.remove.impossible=It is not possible to remove the package {pkg}
+debian.remove_deps=The following {no} dependent packages of {pkg} will be removed as well ({fspace})
+debian.simulate_operation=Simulating the operation
+debian.task.app_index.status=Indexing installed applications
+debian.task.map_apps.check_files=Checking {type} files
+debian.task.map_apps.read_cache=Reading apps from the index (cache)
+debian.task.map_apps.status=Mapping installed applications
+debian.task.update_apps_idx.status=Updating index
+debian.task.sync_pkgs.status=Synchronizing packages
+debian.transaction.dependency_of=Dependency of {pkgs}
+debian.transaction.get_data=Retrieving the operation's data
+debian.transaction.title=Operation details
+debian.transaction.to_install=The following additional packages ({no}) will be installed (Download: {dsize} | Installation: {isize})
+debian.transaction.to_remove=The following packages ({no}) will be removed (Freed space: {fspace})
+debian.uninstall.failed_to_remove=It was not possible to remove the following {no} packages: {pkgs}
+debian.uninstall.removing=Removing packages
+debian.uninstall.validating=Validating removed packages
\ No newline at end of file
diff --git a/bauh/gems/debian/resources/locale/it b/bauh/gems/debian/resources/locale/it
new file mode 100644
index 00000000..41360778
--- /dev/null
+++ b/bauh/gems/debian/resources/locale/it
@@ -0,0 +1,84 @@
+debian.action.index_apps=Index applications
+debian.action.index_apps.desc=Identifies all installed packages associated with .desktop files and maps them as applications
+debian.action.purge=Remove completely
+debian.action.purge.confirmation=The complete removal of {pkg} will erase the software files and also the configuration ones
+debian.action.purge.desc=Removes all software and configuration files associated with the package
+debian.action.purge.status=Removing {} completely
+debian.action.sync_pkgs=Synchronize packages
+debian.action.sync_pkgs.desc=Synchronizes the available software packages on the repositories
+debian.action.sync_pkgs.status=Synchronizing packages
+debian.action.sources=Software sources
+debian.action.sources.desc=Starts the application for managing software sources
+debian.action.sources.not_installed=The application for managing software sources is not installed
+debian.action.sources.status=Launching application
+debian.action.sources.unsupported=The application {app} is not supported and will not be started
+debian.app_index.checking=Checking the apps index
+debian.app_index.updating=Updating the apps index
+debian.config.index_apps.exp=Apps cache expiration
+debian.config.index_apps.exp.tip=Time period in MINUTES in which the installed applications cache is considered up-to-date during startup
+debian.config.pkg_sources.app=Software sources
+debian.config.pkg_sources.app.tip=Application for managing the software sources. {auto} detects one of the supported applications automatically.
+debian.config.pkg_sources.app.auto=Auto
+debian.config.suggestions.exp=Suggestions expiration
+debian.config.suggestions.exp.tip=It defines the period (in hours) in which the suggestions stored in disc will be considered up to date. Use 0 if you always want to update them.
+debian.config.sync_pkgs.time=Packages synchronization period
+debian.config.sync_pkgs.time.tip=Time period in MINUTES in which the packages synchronization must be done on startup
+debian.info.00.name=name
+debian.info.01.version=version
+debian.info.02.description=description
+debian.info.03.exec=executable
+debian.info.04.architecture=architecture
+debian.info.04.architecture.all=all
+debian.info.04.archive=archive
+debian.info.04.automatically installed=automatically installed
+debian.info.04.automatically installed.no=no
+debian.info.04.automatically installed.yes=yes
+debian.info.04.breaks=breaks
+debian.info.04.compressed size=Download size
+debian.info.04.conflicts=conflicts
+debian.info.04.depends=dependencies
+debian.info.04.filename=file
+debian.info.04.homepage=web page
+debian.info.04.maintainer=maintainer
+debian.info.04.md5sum=MD5
+debian.info.04.multi-arch=multi-architecture
+debian.info.04.multi-arch.foreign=foreign
+debian.info.04.multi-arch.same=same
+debian.info.04.replaces=replaces
+debian.info.04.predepends=pre dependencies
+debian.info.04.priority=priority
+debian.info.04.priority.important=important
+debian.info.04.priority.optional=optional
+debian.info.04.provided by=provided by
+debian.info.04.provides=provides
+debian.info.04.recommends=recommends
+debian.info.04.section=section
+debian.info.04.state=state
+debian.info.04.state.installed=installed
+debian.info.04.state.not installed=not installed
+debian.info.04.state.not installed (configuration files remain)=not installed (configuration files installed)
+debian.info.04.suggests=suggests
+debian.info.04.uncompressed size=installation size
+debian.install.validating=Validating installed packages
+debian.installing_pkgs=Installing packages
+debian.output.downloading=Downloading {pkg}
+debian.output.finishing=Finishing
+debian.output.removing=Removing {pkg}
+debian.output.unpacking=Unpacking {pkg}
+debian.remove.impossible=It is not possible to remove the package {pkg}
+debian.remove_deps=The following {no} dependent packages of {pkg} will be removed as well ({fspace})
+debian.simulate_operation=Simulating the operation
+debian.task.app_index.status=Indexing installed applications
+debian.task.map_apps.check_files=Checking {type} files
+debian.task.map_apps.read_cache=Reading apps from the index (cache)
+debian.task.map_apps.status=Mapping installed applications
+debian.task.update_apps_idx.status=Updating index
+debian.task.sync_pkgs.status=Synchronizing packages
+debian.transaction.dependency_of=Dependency of {pkgs}
+debian.transaction.get_data=Retrieving the operation's data
+debian.transaction.title=Operation details
+debian.transaction.to_install=The following additional packages ({no}) will be installed (Download: {dsize} | Installation: {isize})
+debian.transaction.to_remove=The following packages ({no}) will be removed (Freed space: {fspace})
+debian.uninstall.failed_to_remove=It was not possible to remove the following {no} packages: {pkgs}
+debian.uninstall.removing=Removing packages
+debian.uninstall.validating=Validating removed packages
\ No newline at end of file
diff --git a/bauh/gems/debian/resources/locale/pt b/bauh/gems/debian/resources/locale/pt
new file mode 100644
index 00000000..a4de0efd
--- /dev/null
+++ b/bauh/gems/debian/resources/locale/pt
@@ -0,0 +1,84 @@
+debian.action.index_apps=Indexar aplicações
+debian.action.index_apps.desc=Identifica todos os pacotes instalados associados a arquivos .desktop e os mapeia como aplicações
+debian.action.purge=Remover completamente
+debian.action.purge.confirmation=A remoção completa de {pkg} apagará os arquivos de software e também os de configuração
+debian.action.purge.desc=Remove todos os arquivos de software e configuração associados ao pacote
+debian.action.purge.status=Removendo {} completamente
+debian.action.sync_pkgs=Sincronizar pacotes
+debian.action.sync_pkgs.desc=Sincroniza os pacotes de software disponíveis nos repositórios
+debian.action.sync_pkgs.status=Sincronizando pacotes
+debian.action.sources=Fontes de software
+debian.action.sources.desc=Inicia a aplicação detectada para gerenciamento de fontes de software
+debian.action.sources.not_installed=A aplicação para gerenciamento de fontes de software não está instalada
+debian.action.sources.status=Iniciando aplicação
+debian.action.sources.unsupported=A aplicação {app} não é suportada e não será inicializada
+debian.app_index.checking=Veriricando o índice de aplicações
+debian.app_index.updating=Atualizando o índice de aplicações
+debian.config.index_apps.exp=Validade do cache de aplicações
+debian.config.index_apps.exp.tip=Período de tempo em MINUTOS em que o cache das aplicações instaladas é considerado válido durante a inicialização
+debian.config.pkg_sources.app=Fontes de software
+debian.config.pkg_sources.app.tip=Aplicação para gerenciamento de fontes de software. {auto} detecta uma das aplicações suportadas automaticamente.
+debian.config.pkg_sources.app.auto=Auto
+debian.config.suggestions.exp=Validade de sugestões
+debian.config.suggestions.exp.tip=Define o período (em horas) no qual as sugestões armazenadas em disco serão consideradas atualizadas. Use 0 se quiser que elas sempre sejam atualizadas.
+debian.config.sync_pkgs.time=Período de sincronização de pacotes
+debian.config.sync_pkgs.time.tip=Período de tempo em MINUTOS em que a sincronização de pacotes deve ocorrer durante a inicialização
+debian.info.00.name=nome
+debian.info.01.version=versão
+debian.info.02.description=descrição
+debian.info.03.exec=executável
+debian.info.04.architecture=arquitetura
+debian.info.04.architecture.all=todas
+debian.info.04.archive=arquivamento
+debian.info.04.automatically installed=instalado automaticamente
+debian.info.04.automatically installed.no=não
+debian.info.04.automatically installed.yes=sim
+debian.info.04.breaks=Quebra
+debian.info.04.compressed size=Tamanho de download
+debian.info.04.conflicts=Conflita
+debian.info.04.depends=dependências
+debian.info.04.filename=arquivo
+debian.info.04.homepage=página web
+debian.info.04.maintainer=mantenedor
+debian.info.04.md5sum=MD5
+debian.info.04.multi-arch=multi-arquitetura
+debian.info.04.multi-arch.foreign=diferente
+debian.info.04.multi-arch.same=mesma
+debian.info.04.replaces=substitui
+debian.info.04.predepends=pré-dependências
+debian.info.04.priority=prioridade
+debian.info.04.priority.important=importante
+debian.info.04.priority.optional=opcional
+debian.info.04.provided by=provido por
+debian.info.04.provides=provê
+debian.info.04.recommends=recomenda
+debian.info.04.section=seção
+debian.info.04.state=estado
+debian.info.04.state.installed=instalado
+debian.info.04.state.not installed=não instalado
+debian.info.04.state.not installed (configuration files remain)=não instalado (arquivos de configuração instalados)
+debian.info.04.suggests=sugere
+debian.info.04.uncompressed size=tamanho de instalação
+debian.install.validating=Validando pacotes instalados
+debian.installing_pkgs=Instalando pacotes
+debian.output.downloading=Baixando {pkg}
+debian.output.finishing=Finalizando
+debian.output.removing=Removendo {pkg}
+debian.output.unpacking=Desempacotando {pkg}
+debian.remove.impossible=Não é possível remover o pacote {pkg}
+debian.remove_deps=Os seguintes {no} pacotes dependentes de {pkg} serão removidos também ({fspace})
+debian.simulate_operation=Simulando a operação
+debian.task.app_index.status=Indexando aplicações instaladas
+debian.task.map_apps.check_files=Verificando arquivos {type}
+debian.task.map_apps.read_cache=Lendo aplicações do índice (cache)
+debian.task.map_apps.status=Mapeando aplicações instaladas
+debian.task.update_apps_idx.status=Atualizando índice
+debian.task.sync_pkgs.status=Sincronizando pacotes
+debian.transaction.dependency_of=Dependência de {pkgs}
+debian.transaction.get_data=Obtendo dados da operação
+debian.transaction.title=Detalhes da operação
+debian.transaction.to_install=Os seguintes pacotes adicionais ({no}) serão instaladas (Download: {dsize} | Instalação: {isize})
+debian.transaction.to_remove=Os seguintes pacotes ({no}) serão removidos (Espaço liberado: {fspace})
+debian.uninstall.failed_to_remove=Não foi possível remover os seguintes {no} pacotes: {pkgs}
+debian.uninstall.removing=Removendo pacotes
+debian.uninstall.validating=Validando pacotes removidos
diff --git a/bauh/gems/debian/resources/locale/ru b/bauh/gems/debian/resources/locale/ru
new file mode 100644
index 00000000..41360778
--- /dev/null
+++ b/bauh/gems/debian/resources/locale/ru
@@ -0,0 +1,84 @@
+debian.action.index_apps=Index applications
+debian.action.index_apps.desc=Identifies all installed packages associated with .desktop files and maps them as applications
+debian.action.purge=Remove completely
+debian.action.purge.confirmation=The complete removal of {pkg} will erase the software files and also the configuration ones
+debian.action.purge.desc=Removes all software and configuration files associated with the package
+debian.action.purge.status=Removing {} completely
+debian.action.sync_pkgs=Synchronize packages
+debian.action.sync_pkgs.desc=Synchronizes the available software packages on the repositories
+debian.action.sync_pkgs.status=Synchronizing packages
+debian.action.sources=Software sources
+debian.action.sources.desc=Starts the application for managing software sources
+debian.action.sources.not_installed=The application for managing software sources is not installed
+debian.action.sources.status=Launching application
+debian.action.sources.unsupported=The application {app} is not supported and will not be started
+debian.app_index.checking=Checking the apps index
+debian.app_index.updating=Updating the apps index
+debian.config.index_apps.exp=Apps cache expiration
+debian.config.index_apps.exp.tip=Time period in MINUTES in which the installed applications cache is considered up-to-date during startup
+debian.config.pkg_sources.app=Software sources
+debian.config.pkg_sources.app.tip=Application for managing the software sources. {auto} detects one of the supported applications automatically.
+debian.config.pkg_sources.app.auto=Auto
+debian.config.suggestions.exp=Suggestions expiration
+debian.config.suggestions.exp.tip=It defines the period (in hours) in which the suggestions stored in disc will be considered up to date. Use 0 if you always want to update them.
+debian.config.sync_pkgs.time=Packages synchronization period
+debian.config.sync_pkgs.time.tip=Time period in MINUTES in which the packages synchronization must be done on startup
+debian.info.00.name=name
+debian.info.01.version=version
+debian.info.02.description=description
+debian.info.03.exec=executable
+debian.info.04.architecture=architecture
+debian.info.04.architecture.all=all
+debian.info.04.archive=archive
+debian.info.04.automatically installed=automatically installed
+debian.info.04.automatically installed.no=no
+debian.info.04.automatically installed.yes=yes
+debian.info.04.breaks=breaks
+debian.info.04.compressed size=Download size
+debian.info.04.conflicts=conflicts
+debian.info.04.depends=dependencies
+debian.info.04.filename=file
+debian.info.04.homepage=web page
+debian.info.04.maintainer=maintainer
+debian.info.04.md5sum=MD5
+debian.info.04.multi-arch=multi-architecture
+debian.info.04.multi-arch.foreign=foreign
+debian.info.04.multi-arch.same=same
+debian.info.04.replaces=replaces
+debian.info.04.predepends=pre dependencies
+debian.info.04.priority=priority
+debian.info.04.priority.important=important
+debian.info.04.priority.optional=optional
+debian.info.04.provided by=provided by
+debian.info.04.provides=provides
+debian.info.04.recommends=recommends
+debian.info.04.section=section
+debian.info.04.state=state
+debian.info.04.state.installed=installed
+debian.info.04.state.not installed=not installed
+debian.info.04.state.not installed (configuration files remain)=not installed (configuration files installed)
+debian.info.04.suggests=suggests
+debian.info.04.uncompressed size=installation size
+debian.install.validating=Validating installed packages
+debian.installing_pkgs=Installing packages
+debian.output.downloading=Downloading {pkg}
+debian.output.finishing=Finishing
+debian.output.removing=Removing {pkg}
+debian.output.unpacking=Unpacking {pkg}
+debian.remove.impossible=It is not possible to remove the package {pkg}
+debian.remove_deps=The following {no} dependent packages of {pkg} will be removed as well ({fspace})
+debian.simulate_operation=Simulating the operation
+debian.task.app_index.status=Indexing installed applications
+debian.task.map_apps.check_files=Checking {type} files
+debian.task.map_apps.read_cache=Reading apps from the index (cache)
+debian.task.map_apps.status=Mapping installed applications
+debian.task.update_apps_idx.status=Updating index
+debian.task.sync_pkgs.status=Synchronizing packages
+debian.transaction.dependency_of=Dependency of {pkgs}
+debian.transaction.get_data=Retrieving the operation's data
+debian.transaction.title=Operation details
+debian.transaction.to_install=The following additional packages ({no}) will be installed (Download: {dsize} | Installation: {isize})
+debian.transaction.to_remove=The following packages ({no}) will be removed (Freed space: {fspace})
+debian.uninstall.failed_to_remove=It was not possible to remove the following {no} packages: {pkgs}
+debian.uninstall.removing=Removing packages
+debian.uninstall.validating=Validating removed packages
\ No newline at end of file
diff --git a/bauh/gems/debian/resources/locale/tr b/bauh/gems/debian/resources/locale/tr
new file mode 100644
index 00000000..b9458348
--- /dev/null
+++ b/bauh/gems/debian/resources/locale/tr
@@ -0,0 +1,83 @@
+debian.action.index_apps=Index applications
+debian.action.index_apps.desc=Identifies all installed packages associated with .desktop files and maps them as applications
+debian.action.purge=Remove completely
+debian.action.purge.confirmation=The complete removal of {pkg} will erase the software files and also the configuration ones
+debian.action.purge.desc=Removes all software and configuration files associated with the package
+debian.action.purge.status=Removing {} completely
+debian.action.sync_pkgs=Synchronize packages
+debian.action.sync_pkgs.desc=Synchronizes the available software packages on the repositories
+debian.action.sync_pkgs.status=Synchronizing packages
+debian.action.sources=Software sources
+debian.action.sources.desc=Starts the application for managing software sources
+debian.action.sources.not_installed=The application for managing software sources is not installed
+debian.action.sources.status=Launching application
+debian.action.sources.unsupported=The application {app} is not supported and will not be started
+debian.app_index.checking=Checking the apps index
+debian.app_index.updating=Updating the apps index
+debian.config.index_apps.exp=Apps cache expiration
+debian.config.index_apps.exp.tip=Time period in MINUTES in which the installed applications cache is considered up-to-date during startup
+debian.config.pkg_sources.app=Software sources
+debian.config.pkg_sources.app.tip=Application for managing the software sources. {auto} detects one of the supported applications automatically.
+debian.config.pkg_sources.app.auto=Auto
+debian.config.suggestions.exp=Suggestions expiration
+debian.config.suggestions.exp.tip=It defines the period (in hours) in which the suggestions stored in disc will be considered up to date. Use 0 if you always want to update them.
+debian.config.sync_pkgs.time=Packages synchronization period
+debian.config.sync_pkgs.time.tip=Time period in MINUTES in which the packages synchronization must be done on startup
+debian.info.00.name=name
+debian.info.01.version=version
+debian.info.02.description=description
+debian.info.03.exec=executable
+debian.info.04.architecture=architecture
+debian.info.04.architecture.all=all
+debian.info.04.archive=archive
+debian.info.04.automatically installed=automatically installed
+debian.info.04.automatically installed.no=no
+debian.info.04.automatically installed.yes=yes
+debian.info.04.breaks=breaks
+debian.info.04.compressed size=Download size
+debian.info.04.conflicts=conflicts
+debian.info.04.depends=dependencies
+debian.info.04.homepage=web page
+debian.info.04.maintainer=maintainer
+debian.info.04.md5sum=MD5
+debian.info.04.multi-arch=multi-architecture
+debian.info.04.multi-arch.foreign=foreign
+debian.info.04.multi-arch.same=same
+debian.info.04.replaces=replaces
+debian.info.04.predepends=pre dependencies
+debian.info.04.priority=priority
+debian.info.04.priority.important=important
+debian.info.04.priority.optional=optional
+debian.info.04.provided by=provided by
+debian.info.04.provides=provides
+debian.info.04.recommends=recommends
+debian.info.04.section=section
+debian.info.04.state=state
+debian.info.04.state.installed=installed
+debian.info.04.state.not installed=not installed
+debian.info.04.state.not installed (configuration files remain)=not installed (configuration files installed)
+debian.info.04.suggests=suggests
+debian.info.04.uncompressed size=installation size
+debian.install.validating=Validating installed packages
+debian.installing_pkgs=Installing packages
+debian.output.downloading=Downloading {pkg}
+debian.output.finishing=Finishing
+debian.output.removing=Removing {pkg}
+debian.output.unpacking=Unpacking {pkg}
+debian.remove.impossible=It is not possible to remove the package {pkg}
+debian.remove_deps=The following {no} dependent packages of {pkg} will be removed as well ({fspace})
+debian.simulate_operation=Simulating the operation
+debian.task.app_index.status=Indexing installed applications
+debian.task.map_apps.check_files=Checking {type} files
+debian.task.map_apps.read_cache=Reading apps from the index (cache)
+debian.task.map_apps.status=Mapping installed applications
+debian.task.update_apps_idx.status=Updating index
+debian.task.sync_pkgs.status=Synchronizing packages
+debian.transaction.dependency_of=Dependency of {pkgs}
+debian.transaction.get_data=Retrieving the operation's data
+debian.transaction.title=Operation details
+debian.transaction.to_install=The following additional packages ({no}) will be installed (Download: {dsize} | Installation: {isize})
+debian.transaction.to_remove=The following packages ({no}) will be removed (Freed space: {fspace})
+debian.uninstall.failed_to_remove=It was not possible to remove the following {no} packages: {pkgs}
+debian.uninstall.removing=Removing packages
+debian.uninstall.validating=Validating removed packages
\ No newline at end of file
diff --git a/bauh/gems/debian/suggestions.py b/bauh/gems/debian/suggestions.py
new file mode 100644
index 00000000..da361ebf
--- /dev/null
+++ b/bauh/gems/debian/suggestions.py
@@ -0,0 +1,192 @@
+import os
+import traceback
+from datetime import datetime, timedelta
+from logging import Logger
+from pathlib import Path
+from threading import Thread
+from typing import Optional, Dict
+
+from bauh.api.abstract.handler import TaskManager
+from bauh.api.abstract.model import SuggestionPriority
+from bauh.api.http import HttpClient
+from bauh.gems.debian import DEBIAN_ICON_PATH, DEBIAN_CACHE_DIR
+from bauh.view.util.translation import I18n
+
+
+class DebianSuggestionsDownloader(Thread):
+
+ _file_suggestions: Optional[str] = None
+ _file_suggestions_ts: Optional[str] = None
+ _url_suggestions: Optional[str] = None
+
+ @classmethod
+ def file_suggestions(cls) -> str:
+ if cls._file_suggestions is None:
+ cls._file_suggestions = f'{DEBIAN_CACHE_DIR}/suggestions.txt'
+
+ return cls._file_suggestions
+
+ @classmethod
+ def file_suggestions_timestamp(cls) -> str:
+ if cls._file_suggestions_ts is None:
+ cls._file_suggestions_ts = f'{cls.file_suggestions()}.ts'
+
+ return cls._file_suggestions_ts
+
+ @classmethod
+ def url_suggestions(cls) -> str:
+ if cls._url_suggestions is None:
+ cls._url_suggestions = 'https://raw.githubusercontent.com/vinifmor/bauh-files' \
+ '/master/debian/suggestions.txt'
+
+ return cls._url_suggestions
+
+ def __init__(self, logger: Logger, http_client: HttpClient, i18n: I18n):
+ super(DebianSuggestionsDownloader, self).__init__()
+ self._log = logger
+ self.i18n = i18n
+ self.http_client = http_client
+ self._taskman: Optional[TaskManager] = None
+ self.task_id = 'debian.suggs'
+
+ def register_task(self, taskman: Optional[TaskManager]):
+ self._taskman = taskman
+ if taskman:
+ self._taskman.register_task(id_=self.task_id, label=self.i18n['task.download_suggestions'],
+ icon_path=DEBIAN_ICON_PATH)
+
+ @property
+ def taskman(self) -> TaskManager:
+ if self._taskman is None:
+ self._taskman = TaskManager()
+
+ return self._taskman
+
+ @classmethod
+ def should_download(cls, debian_config: dict, logger: Logger, only_positive_exp: bool = False) -> bool:
+ try:
+ exp_hours = int(debian_config['suggestions.exp'])
+ except ValueError:
+ logger.error(f"The Debian configuration property 'suggestions.expiration' has a non int value set: "
+ f"{debian_config['suggestions']['expiration']}")
+ return not only_positive_exp
+
+ if exp_hours <= 0:
+ logger.info("Suggestions cache is disabled")
+ return not only_positive_exp
+
+ if not os.path.exists(cls.file_suggestions()):
+ logger.info(f"'{cls.file_suggestions()}' not found. It must be downloaded")
+ return True
+
+ if not os.path.exists(cls.file_suggestions()):
+ logger.info(f"'{cls.file_suggestions()}' not found. The suggestions file must be downloaded.")
+ return True
+
+ with open(cls.file_suggestions_timestamp()) as f:
+ timestamp_str = f.read()
+
+ try:
+ suggestions_timestamp = datetime.fromtimestamp(float(timestamp_str))
+ except:
+ logger.error(f'Could not parse the Debian cached suggestions timestamp: {timestamp_str}')
+ traceback.print_exc()
+ return True
+
+ update = suggestions_timestamp + timedelta(hours=exp_hours) <= datetime.utcnow()
+ return update
+
+ def _save(self, text: str, timestamp: float):
+ self._log.info(f"Caching suggestions to '{self.file_suggestions()}'")
+
+ cache_dir = os.path.dirname(self.file_suggestions())
+
+ try:
+ Path(cache_dir).mkdir(parents=True, exist_ok=True)
+ cache_dir_ok = True
+ except OSError:
+ self._log.error(f"Could not create cache directory '{cache_dir}'")
+ traceback.print_exc()
+ cache_dir_ok = False
+
+ if cache_dir_ok:
+ try:
+ with open(self.file_suggestions(), 'w+') as f:
+ f.write(text)
+ except:
+ self._log.error(f"An exception happened while writing the file '{self.file_suggestions()}'")
+ traceback.print_exc()
+
+ try:
+ with open(self.file_suggestions_timestamp(), 'w+') as f:
+ f.write(str(timestamp))
+ except:
+ self._log.error(f"An exception happened while writing the file '{self.file_suggestions_timestamp()}'")
+ traceback.print_exc()
+
+ def parse_suggestions(self, suggestions_str: str) -> Dict[str, SuggestionPriority]:
+ output = dict()
+ for line in suggestions_str.split('\n'):
+ clean_line = line.strip()
+
+ if clean_line:
+ line_split = clean_line.split(':', 1)
+
+ if len(line_split) == 2:
+ try:
+ prio = int(line_split[0])
+ except ValueError:
+ self._log.warning(f"Could not parse Debian package suggestion: {line}")
+ continue
+
+ output[line_split[1]] = SuggestionPriority(prio)
+
+ return output
+
+ def read_cached(self) -> Optional[Dict[str, SuggestionPriority]]:
+ self._log.info(f"Reading cached suggestions file '{self.file_suggestions()}'")
+
+ try:
+ with open(self.file_suggestions()) as f:
+ sugs_str = f.read()
+ except FileNotFoundError:
+ self._log.warning(f"Cached suggestions file does not exist ({self.file_suggestions()})")
+ return
+
+ if not sugs_str:
+ self._log.warning(f"Cached suggestions file '{self.file_suggestions()}' is empty")
+ return
+
+ return self.parse_suggestions(sugs_str)
+
+ def download(self) -> Optional[Dict[str, SuggestionPriority]]:
+ self.taskman.update_progress(self.task_id, progress=1, substatus=None)
+
+ self._log.info(f"Downloading suggestions from {self.url_suggestions()}")
+ res = self.http_client.get(self.url_suggestions())
+
+ suggestions = None
+ if res.status_code == 200 and res.text:
+ self.taskman.update_progress(self.task_id, progress=50, substatus=None)
+ suggestions = self.parse_suggestions(res.text)
+
+ if suggestions:
+ self._save(text=res.text, timestamp=datetime.utcnow().timestamp())
+ else:
+ self._log.warning("No Debian suggestions to cache")
+ else:
+ self._log.warning(f"Could not retrieve Debian suggestions. "
+ f"Response (status={res.status_code}, text={res.text})")
+
+ self.taskman.update_progress(self.task_id, progress=100, substatus=None)
+ self.taskman.finish_task(self.task_id)
+ return suggestions
+
+ def read(self, debian_config: dict) -> Optional[Dict[str, int]]:
+ if self.should_download(debian_config=debian_config, logger=self._log):
+ return self.download()
+
+ return self.read_cached()
+
+ def run(self):
+ self.download()
diff --git a/bauh/gems/debian/tasks.py b/bauh/gems/debian/tasks.py
new file mode 100644
index 00000000..51a18730
--- /dev/null
+++ b/bauh/gems/debian/tasks.py
@@ -0,0 +1,196 @@
+import time
+import traceback
+from datetime import datetime, timedelta
+from logging import Logger
+from threading import Thread
+from typing import Optional, Set
+
+from bauh.api.abstract.handler import TaskManager, ProcessWatcher
+from bauh.commons.html import bold
+from bauh.commons.system import ProcessHandler
+from bauh.gems.debian import DEBIAN_ICON_PATH, PACKAGE_SYNC_TIMESTAMP_FILE
+from bauh.gems.debian.aptitude import Aptitude
+from bauh.gems.debian.index import ApplicationIndexer, ApplicationsMapper
+from bauh.gems.debian.model import DebianApplication
+from bauh.view.util.translation import I18n
+
+
+class MapApplications(Thread):
+
+ def __init__(self, taskman: TaskManager, app_indexer: ApplicationIndexer, i18n: I18n, logger: Logger,
+ deb_config: dict, app_mapper: ApplicationsMapper, check_expiration: bool = True,
+ watcher: Optional[ProcessWatcher] = None):
+ super(MapApplications, self).__init__()
+ self._taskman = taskman
+ self._i18n = i18n
+ self._log = logger
+ self._id = 'debian.map_apps'
+ self._indexer = app_indexer
+ self._config = deb_config
+ self._app_mapper = app_mapper
+ self._taskman.register_task(self._id, self._i18n['debian.task.map_apps.status'], DEBIAN_ICON_PATH)
+ self.apps: Optional[Set[DebianApplication]] = None
+ self.cached: Optional[bool] = None
+ self._check_expiration = check_expiration
+ self._watcher = watcher
+
+ def _change_substatus(self, msg: str):
+ if self._watcher:
+ self._watcher.change_substatus(msg)
+
+ def run(self):
+ ti = time.time()
+ self._log.info("Begin: mapping Debian applications")
+
+ self._taskman.update_progress(self._id, 1, None)
+
+ if not self._check_expiration or self._indexer.is_expired(self._config):
+ status = self._i18n['debian.task.map_apps.check_files'].format(type="'.desktop")
+ self._taskman.update_progress(self._id, 1, status)
+ self._change_substatus(status)
+ self.apps = self._app_mapper.map_executable_applications()
+ self.cached = False
+ else:
+ status = self._i18n['debian.task.map_apps.read_cache']
+ self._taskman.update_progress(self._id, 1, status)
+ self._change_substatus(status)
+ self.apps = {app for app in self._indexer.read_index()}
+ self.cached = True
+
+ self._log.info(f"Number of Debian applications found: {len(self.apps) if self.apps else 0}")
+ self._taskman.update_progress(self._id, 100, None)
+ self._taskman.finish_task(self._id)
+
+ tf = time.time()
+ self._log.info(f"Finish: mapping Debian applications ({tf - ti:.4f} seconds)")
+
+
+class UpdateApplicationIndex(Thread):
+
+ def __init__(self, taskman: TaskManager, app_indexer: ApplicationIndexer,
+ i18n: I18n, logger: Logger, mapping_apps: MapApplications,
+ watcher: Optional[ProcessWatcher] = None):
+ super(UpdateApplicationIndex, self).__init__()
+ self._taskman = taskman
+ self._i18n = i18n
+ self._log = logger
+ self._id = 'debian.app_idx'
+ self._indexer = app_indexer
+ self._mapping_apps = mapping_apps
+ self._taskman.register_task(self._id, self._i18n['debian.task.app_index.status'], DEBIAN_ICON_PATH)
+ self._watcher = watcher
+
+ def _change_substatus(self, msg: str):
+ if self._watcher:
+ self._watcher.change_substatus(msg)
+
+ def run(self):
+ ti = time.time()
+ self._log.info("Begin: Debian applications indexation")
+
+ self._taskman.update_progress(self._id, 1, self._i18n['task.waiting_task'].format(bold(self._i18n['debian.task.map_apps.status'])))
+ self._mapping_apps.join()
+
+ finish_msg = None
+ if self._mapping_apps.cached:
+ finish_msg = self._i18n['task.canceled']
+ else:
+ status = self._i18n['debian.task.update_apps_idx.status']
+ self._taskman.update_progress(self._id, 50, status)
+ self._change_substatus(status)
+
+ try:
+ self._indexer.update_index(self._mapping_apps.apps)
+ except:
+ finish_msg = self._i18n['error']
+
+ self._taskman.update_progress(self._id, 100, finish_msg)
+ self._taskman.finish_task(self._id)
+
+ tf = time.time()
+ self._log.info(f"Finish: Debian applications indexation ({tf - ti:.4f} seconds)")
+
+
+class SynchronizePackages(Thread):
+
+ def __init__(self, taskman: TaskManager, i18n: I18n, logger: Logger, root_password: Optional[str],
+ aptitude: Aptitude, watcher: Optional[ProcessWatcher] = None):
+ super(SynchronizePackages, self).__init__()
+ self._id = 'debian.sync_pkgs'
+ self._taskman = taskman
+ self._i18n = i18n
+ self._log = logger
+ self._root_password = root_password
+ self._watcher = watcher
+ self._aptitude = aptitude
+ self._taskman.register_task(self._id, self._i18n['debian.task.sync_pkgs.status'], DEBIAN_ICON_PATH)
+
+ def _notify_output(self, output: str):
+ self._taskman.update_output(self._id, output)
+
+ @staticmethod
+ def should_synchronize(deb_config: dict, logger: Logger) -> bool:
+ try:
+ period = int(deb_config.get('sync_pkgs.time', 0))
+ except ValueError:
+ logger.error(f"Invalid value for Debian configuration property 'sync_pkgs.time': "
+ f"{deb_config['sync_pkgs.time']}")
+ return True
+
+ if period <= 0:
+ logger.warning("Packages synchronization will always be done ('sync_pkgs.time' <= 0 )'")
+ return True
+
+ try:
+ with open(PACKAGE_SYNC_TIMESTAMP_FILE) as f:
+ timestamp_str = f.read().strip()
+ except FileNotFoundError:
+ logger.info(f"No packages synchronization timestamp found ({PACKAGE_SYNC_TIMESTAMP_FILE})")
+ return True
+
+ try:
+ last_timestamp = datetime.fromtimestamp(float(timestamp_str))
+ except:
+ logger.error(f'Could not parse the packages synchronization timestamp: {timestamp_str} '
+ f'({PACKAGE_SYNC_TIMESTAMP_FILE})')
+ traceback.print_exc()
+ return True
+
+ expired = last_timestamp + timedelta(minutes=period) <= datetime.utcnow()
+
+ if expired:
+ logger.info("Packages synchronization is outdated")
+ else:
+ logger.info("Packages synchronization is up-to-date")
+
+ return expired
+
+ def run(self) -> bool:
+ ti = time.time()
+ self._log.info("Begin: packages synchronization")
+ self._taskman.update_progress(self._id, 1, None)
+
+ handler = ProcessHandler(self._watcher)
+ updated, _ = handler.handle_simple(self._aptitude.update(self._root_password),
+ output_handler=self._notify_output)
+ self._taskman.update_progress(self._id, 99, None)
+
+ if updated:
+ index_timestamp = datetime.utcnow().timestamp()
+ finish_msg = None
+ try:
+ with open(PACKAGE_SYNC_TIMESTAMP_FILE, 'w+') as f:
+ f.write(str(index_timestamp))
+ except OSError:
+ finish_msg = self._i18n['error']
+ self._log.error(f"Could not write the packages synchronization timestamp to file "
+ f"'{PACKAGE_SYNC_TIMESTAMP_FILE}'")
+ else:
+ finish_msg = self._i18n['error']
+
+ self._taskman.update_progress(self._id, 100, finish_msg)
+ self._taskman.finish_task(self._id)
+
+ tf = time.time()
+ self._log.info(f"Finish: packages synchronization ({tf - ti:.4f} seconds)")
+ return updated
diff --git a/bauh/gems/web/resources/locale/ca b/bauh/gems/web/resources/locale/ca
index 0bb0b7b7..4271b195 100644
--- a/bauh/gems/web/resources/locale/ca
+++ b/bauh/gems/web/resources/locale/ca
@@ -81,7 +81,6 @@ web.settings.suggestions.cache_exp=Suggestions expiration
web.settings.suggestions.cache_exp.tip=It defines the period (in HOURS) in which suggestions stored on the disk are considered up to date during the initialization process. Use 0 so that they are always updated
web.task.download_settings=Updating environment settings
web.task.search_index=Indexing suggestions
-web.task.suggestions=Downloading suggestions
web.task.suggestions.saving=Saving to disk
web.uninstall.error.install_dir.not_found=The installation directory {} was not found
web.uninstall.error.remove_dir=It was not possible to remove {}
diff --git a/bauh/gems/web/resources/locale/en b/bauh/gems/web/resources/locale/en
index 64012bc9..07b5b1e6 100644
--- a/bauh/gems/web/resources/locale/en
+++ b/bauh/gems/web/resources/locale/en
@@ -81,7 +81,6 @@ web.settings.suggestions.cache_exp=Suggestions expiration
web.settings.suggestions.cache_exp.tip=It defines the period (in HOURS) in which suggestions stored on the disk are considered up to date during the initialization process. Use 0 so that they are always updated.
web.task.download_settings=Updating environment settings
web.task.search_index=Indexing suggestions
-web.task.suggestions=Downloading suggestions
web.task.suggestions.saving=Saving to disk
web.uninstall.error.install_dir.not_found=The installation directory {} was not found
web.uninstall.error.remove_dir=It was not possible to remove {}
diff --git a/bauh/gems/web/resources/locale/es b/bauh/gems/web/resources/locale/es
index 0844df57..ba7987d9 100644
--- a/bauh/gems/web/resources/locale/es
+++ b/bauh/gems/web/resources/locale/es
@@ -81,7 +81,6 @@ web.settings.suggestions.cache_exp=Expiración de sugerencias
web.settings.suggestions.cache_exp.tip=Define el período (en HORAS) en el que las sugerencias almacenadas en el disco son consideradas actualizadas durante el proceso de inicialización. Utilice 0 para que siempre sean actualizadas.
web.task.download_settings=Actualizando configuraciones de ambiente
web.task.search_index=Indexando sugerencias
-web.task.suggestions=Descargando sugerencias
web.task.suggestions.saving=Guardando en disco
web.uninstall.error.install_dir.not_found=No se encontró el directorio de instalación {}
web.uninstall.error.remove_dir=No fue posible eliminar {}
diff --git a/bauh/gems/web/resources/locale/fr b/bauh/gems/web/resources/locale/fr
index edc29679..83afe698 100644
--- a/bauh/gems/web/resources/locale/fr
+++ b/bauh/gems/web/resources/locale/fr
@@ -81,7 +81,6 @@ web.settings.suggestions.cache_exp=Suggestions expiration
web.settings.suggestions.cache_exp.tip=It defines the period (in HOURS) in which suggestions stored on the disk are considered up to date during the initialization process. Use 0 so that they are always updated
web.task.download_settings=Mise à jour des paramètres d'environnement
web.task.search_index=Indexing suggestions
-web.task.suggestions=Téléchargement de suggestions
web.task.suggestions.saving=Saving to disk
web.uninstall.error.install_dir.not_found=Impossible de trouver le dossier d'installation {}
web.uninstall.error.remove_dir=Impossible d'enlever {}
diff --git a/bauh/gems/web/resources/locale/it b/bauh/gems/web/resources/locale/it
index 92451abb..6bf797a3 100644
--- a/bauh/gems/web/resources/locale/it
+++ b/bauh/gems/web/resources/locale/it
@@ -81,7 +81,6 @@ web.settings.suggestions.cache_exp=Suggestions expiration
web.settings.suggestions.cache_exp.tip=It defines the period (in HOURS) in which suggestions stored on the disk are considered up to date during the initialization process. Use 0 so that they are always updated
web.task.download_settings=Updating environment settings
web.task.search_index=Indexing suggestions
-web.task.suggestions=Downloading suggestions
web.task.suggestions.saving=Saving to disk
web.uninstall.error.install_dir.not_found=The installation directory {} was not found
web.uninstall.error.remove_dir=It was not possible to remove {}
diff --git a/bauh/gems/web/resources/locale/pt b/bauh/gems/web/resources/locale/pt
index 62f02b45..008853aa 100644
--- a/bauh/gems/web/resources/locale/pt
+++ b/bauh/gems/web/resources/locale/pt
@@ -80,7 +80,6 @@ web.settings.suggestions.cache_exp=Expiração de sugestões
web.settings.suggestions.cache_exp.tip=Define o período (em HORAS) em que as sugestões armazenadas em disco são consideradas atualizadas durante a inicialização. Use 0 para que elas sejam sempre atualizadas.
web.task.download_settings=Atualizando configurações de ambiente
web.task.search_index=Indexando sugestões
-web.task.suggestions=Baixando sugestões
web.task.suggestions.saving=Salvando em disco
web.uninstall.error.install_dir.not_found=O diretório de instalação {} não foi encontrado
web.uninstall.error.remove=Não foi possível remover {}
diff --git a/bauh/gems/web/resources/locale/ru b/bauh/gems/web/resources/locale/ru
index 8b1ae1b4..4a8668f3 100644
--- a/bauh/gems/web/resources/locale/ru
+++ b/bauh/gems/web/resources/locale/ru
@@ -81,7 +81,6 @@ web.settings.suggestions.cache_exp=Suggestions expiration
web.settings.suggestions.cache_exp.tip=It defines the period (in HOURS) in which suggestions stored on the disk are considered up to date during the initialization process. Use 0 so that they are always updated
web.task.download_settings=Updating environment settings
web.task.search_index=Indexing suggestions
-web.task.suggestions=Downloading suggestions
web.task.suggestions.saving=Saving to disk
web.uninstall.error.install_dir.not_found=Каталог установки {} не найден
web.uninstall.error.remove_dir=Не удалось удалить {}
diff --git a/bauh/gems/web/resources/locale/tr b/bauh/gems/web/resources/locale/tr
index 8c2db1f4..a605aaf2 100644
--- a/bauh/gems/web/resources/locale/tr
+++ b/bauh/gems/web/resources/locale/tr
@@ -81,7 +81,6 @@ web.settings.suggestions.cache_exp=Suggestions expiration
web.settings.suggestions.cache_exp.tip=It defines the period (in HOURS) in which suggestions stored on the disk are considered up to date during the initialization process. Use 0 so that they are always updated
web.task.download_settings=Ortam ayarları güncelleniyor
web.task.search_index=Indexing suggestions
-web.task.suggestions=Downloading suggestions
web.task.suggestions.saving=Saving to disk
web.uninstall.error.install_dir.not_found={} kurulum dizini bulunamadı
web.uninstall.error.remove_dir={} kaldırmak mümkün olmadı
diff --git a/bauh/gems/web/worker.py b/bauh/gems/web/worker.py
index 953dce59..bcff6488 100644
--- a/bauh/gems/web/worker.py
+++ b/bauh/gems/web/worker.py
@@ -30,7 +30,7 @@ class SuggestionsLoader(Thread):
self.suggestions = suggestions
self.create_config = create_config
self.internet_connection = internet_connection
- self.task_name = self.i18n['web.task.suggestions']
+ self.task_name = self.i18n['task.download_suggestions']
self.taskman.register_task(self.task_id, self.task_name, get_icon_path())
def run(self):
diff --git a/bauh/manage.py b/bauh/manage.py
index 2a4ea28b..29e582a5 100644
--- a/bauh/manage.py
+++ b/bauh/manage.py
@@ -56,21 +56,24 @@ def new_manage_panel(app_args: Namespace, app_config: dict, logger: logging.Logg
app = new_qt_application(app_config=app_config, logger=logger, quit_on_last_closed=True)
+ screen_size = app.primaryScreen().size()
+ context.screen_width, context.screen_height = screen_size.width(), screen_size.height()
+
if app_args.settings: # only settings window
manager.cache_available_managers()
- return app, SettingsWindow(manager=manager, i18n=i18n, screen_size=app.primaryScreen().size(), window=None)
+ return app, SettingsWindow(manager=manager, i18n=i18n, screen_size=screen_size, window=None)
else:
manage_window = ManageWindow(i18n=i18n,
manager=manager,
icon_cache=icon_cache,
- screen_size=app.primaryScreen().size(),
+ screen_size=screen_size,
config=app_config,
context=context,
http_client=http_client,
icon=util.get_default_icon()[1],
logger=logger)
- prepare = PreparePanel(screen_size=app.primaryScreen().size(),
+ prepare = PreparePanel(screen_size=screen_size,
context=context,
manager=manager,
i18n=i18n,
diff --git a/bauh/view/core/controller.py b/bauh/view/core/controller.py
index 07089cd1..6c88db27 100755
--- a/bauh/view/core/controller.py
+++ b/bauh/view/core/controller.py
@@ -163,7 +163,7 @@ class GenericSoftwareManager(SoftwareManager):
def _search(self, word: str, is_url: bool, man: SoftwareManager, disk_loader, res: SearchResult):
if self._can_work(man):
mti = time.time()
- apps_found = man.search(words=word, disk_loader=disk_loader, is_url=is_url)
+ apps_found = man.search(words=word, disk_loader=disk_loader, is_url=is_url, limit=-1)
mtf = time.time()
self.logger.info(f'{man.__class__.__name__} took {mtf - mti:.8f} seconds')
@@ -650,6 +650,7 @@ class GenericSoftwareManager(SoftwareManager):
for man in working_managers:
for action in man.gen_custom_actions():
+ action.manager = man
yield action
app_config = self.configman.get_config()
diff --git a/bauh/view/qt/components.py b/bauh/view/qt/components.py
index 43a23c24..a3fbdd23 100644
--- a/bauh/view/qt/components.py
+++ b/bauh/view/qt/components.py
@@ -996,6 +996,12 @@ def to_widget(comp: ViewComponent, i18n: I18n, parent: QWidget = None) -> QWidge
elif isinstance(comp, TextComponent):
label = QLabel(comp.value)
+ if comp.min_width is not None and comp.min_width > 0:
+ label.setMinimumWidth(comp.min_width)
+
+ if comp.max_width is not None and comp.max_width > 0:
+ label.setMinimumWidth(comp.max_width)
+
if comp.size is not None:
label.setStyleSheet("QLabel { font-size: " + str(comp.size) + "px }")
diff --git a/bauh/view/qt/info.py b/bauh/view/qt/info.py
index a3a1a56a..d16757de 100644
--- a/bauh/view/qt/info.py
+++ b/bauh/view/qt/info.py
@@ -1,3 +1,5 @@
+from collections.abc import Iterable
+
from PyQt5.QtCore import QSize, Qt
from PyQt5.QtGui import QIcon, QCursor
from PyQt5.QtWidgets import QDialog, QVBoxLayout, QGroupBox, \
@@ -53,8 +55,9 @@ class InfoDialog(QDialog):
for idx, attr in enumerate(sorted(pkg_info.keys())):
if attr not in IGNORED_ATTRS and pkg_info[attr]:
i18n_key = pkg_info['__app__'].model.gem_name + '.info.' + attr.lower()
+ val = pkg_info[attr]
- if isinstance(pkg_info[attr], list):
+ if not isinstance(val, str) and isinstance(pkg_info[attr], Iterable):
val = ' '.join([str(e).strip() for e in pkg_info[attr] if e])
show_val = '\n'.join(['* ' + str(e).strip() for e in pkg_info[attr] if e])
else:
diff --git a/bauh/view/qt/thread.py b/bauh/view/qt/thread.py
index b411b48b..b510b6b4 100644
--- a/bauh/view/qt/thread.py
+++ b/bauh/view/qt/thread.py
@@ -529,8 +529,9 @@ class UpgradeSelected(AsyncAction):
self.change_substatus('')
if success:
- updated = len(requirements.to_upgrade)
- updated_types.update((req.pkg.__class__ for req in requirements.to_upgrade))
+ if requirements.to_upgrade:
+ updated = len(requirements.to_upgrade)
+ updated_types.update((req.pkg.__class__ for req in requirements.to_upgrade))
if should_trim:
self._trim_disk(root_password)
diff --git a/bauh/view/resources/locale/ca b/bauh/view/resources/locale/ca
index 7e95e8cf..a6705553 100644
--- a/bauh/view/resources/locale/ca
+++ b/bauh/view/resources/locale/ca
@@ -112,22 +112,32 @@ cancel=cancel·la
categories=categories
category=categoria
category.2dgraphics=gràfics 2d
+category.acessories=acessories
+category.admin=admin
category.administration=administració
category.amusement=diversió
+category.application=application
category.arcadegame=joc «arcade»
+category.archiving=archiving
category.art=art
category.audio=àudio
category.audiovideo=àudio/vídeo
category.books=llibres
category.browser=navegador
+category.calculator=calculator
category.cloud=núvol
category.communication=comunicació
+category.compression=compression
+category.core=core
category.database=base de dades
category.databases=bases de dades
category.design=disseny
category.desktopsettings=paràmetres de l’escriptori
+category.devel=desenvolupament
category.development=desenvolupament
category.devices=dispositius
+category.dictionary=dictionary
+category.doc=documentation
category.education=educació
category.emulator=emulador
category.entertainment=entreteniment
@@ -140,12 +150,17 @@ category.games=jocs
category.graphics=gràfics
category.health=salut
category.instantmessaging=missatgeria
+category.libs=biblioteca
category.library=biblioteca
+category.localization=localization
+category.math=math
category.media=mitjans
category.messaging=missatgeria
+category.monitor=monitoring
category.movie=pel·lícula
category.movies=pel·lícules
category.music=música
+category.net=xarxa
category.network=xarxa
category.networks=xarxes
category.news=notícies
@@ -154,7 +169,9 @@ category.office=oficina
category.packagemanager=gestor de paquets
category.personalisation=personalització
category.photo=foto
+category.photography=foto
category.player=jugador
+category.presentation=presentation
category.productivity=productivitat
category.reference=referència
category.science=ciència
@@ -162,6 +179,7 @@ category.security=seguretat
category.server=servidor
category.settings=configuració
category.social=social
+category.sound=sound
category.sportsgame=joc d’esport
category.system=sistema
category.terminalemulator=terminal
@@ -176,6 +194,7 @@ category.videoeditor=editor de vídeo
category.viewer=visor
category.weather=el temps
category.web=web
+category.webbrowser=navegador
category.webdevelopment=desenvolupament web
change=modifica
clean=netejar
@@ -362,6 +381,7 @@ notification.update_selected.success=aplicacions actualitzades correctament
ok=ok
others=altres
popup.button.cancel=Cancel·la
+popup.button.continue=Continuar
popup.button.no=No
popup.button.yes=Sí
popup.history.selected.tooltip=Versió actual
@@ -372,6 +392,8 @@ popup.root.continue=Continuar
popup.root.msg=Requireix la vostra contrasenya per a continuar
popup.root.title=Autenticació
popup.screenshots.no_screenshot.body=no s’han pogut recuperar captures de pantalla del {}
+popup.title.error=Error
+popup.title.warning=Avis
prepare.bt_hide_details=Hide details
prepare.bt_icon.no_output=There are no progress details for this task to be displayed
prepare.bt_icon.output=Click here to display the progress details of this task
@@ -398,9 +420,11 @@ status.caching_data=S’estan emmagatzemant {} dades a la memòria cau al disc
style=estil
success=success
summary=resum
+task.canceled=canceled
task.checking_config=Checking configuration file
task.checking_config.saving=Creating file
task.download_categories=Download de categories
+task.download_suggestions=Downloading de suggeriments
task.waiting_task=Waiting for {}
type=tipus
uninstall=desinstal·la
diff --git a/bauh/view/resources/locale/de b/bauh/view/resources/locale/de
index 12c94b42..b1f09a84 100644
--- a/bauh/view/resources/locale/de
+++ b/bauh/view/resources/locale/de
@@ -112,21 +112,31 @@ cancel=Abbrechen
categories=Kategorien
category=Kategorie
category.2dgraphics=2D-Grafik
+category.acessories=acessories
+category.admin=admin
category.administration=Verwaltung
category.amusement=amusement
+category.application=application
category.arcadegame=Arcade-Spiel
+category.archiving=archiving
category.art=art
category.audio=Audio
category.audiovideo=Audio/Video
category.books=books
category.browser=browser
+category.calculator=calculator
category.cloud=cloud
category.communication=Kommunikation
+category.compression=compression
+category.core=core
category.database=database
category.databases=databases
category.design=design
category.desktopsettings=Desktop-Einstellungen
+category.devel=Entwicklung
category.development=Entwicklung
+category.dictionary=dictionary
+category.doc=documentation
category.education=education
category.emulator=Emulator
category.entertainment=entertainment
@@ -139,12 +149,17 @@ category.games=games
category.graphics=graphics
category.health=health
category.instantmessaging=Kommunikation
+category.libs=library
category.library=library
+category.localization=localization
+category.math=math
category.media=media
category.messaging=messaging
+category.monitor=monitoring
category.movie=movie
category.movies=movies
category.music=music
+category.net=network
category.network=network
category.networks=networks
category.news=news
@@ -153,7 +168,9 @@ category.office=office
category.packagemanager=Paketmanager
category.personalisation=personalization
category.photo=photo
+category.photography=photo
category.player=Spieler
+category.presentation=presentation
category.productivity=productivity
category.reference=reference
category.science=science
@@ -161,6 +178,7 @@ category.security=security
category.server=server
category.settings=Einstellungen
category.social=social
+category.sound=sound
category.sportsgame=Sportspiel
category.system=system
category.terminalemulator=Terminal
@@ -175,6 +193,7 @@ category.videoeditor=video editor
category.viewer=viewer
category.weather=weather
category.web=web
+category.webbrowser=browser
category.webdevelopment=Web-Entwicklung
change=Anwenden
clean=Bereinigen
@@ -361,6 +380,7 @@ notification.update_selected.success=Anwendung(en) erfolgreich aktualisiert
ok=ok
others=Weitere
popup.button.cancel=Abbrechen
+popup.button.continue=Fortsetzen
popup.button.no=Nein
popup.button.yes=Ja
popup.history.selected.tooltip=Aktuelle Version
@@ -371,6 +391,8 @@ popup.root.continue=Fortfahren
popup.root.msg=Zum Fortfahren wird das Passwort benötigt.
popup.root.title=Authentifizierung
popup.screenshots.no_screenshot.body={} Screenshots konnten nicht heruntergeladen werden.
+popup.title.error=Fehler
+popup.title.warning=Warnung
prepare.bt_hide_details=Hide details
prepare.bt_icon.no_output=There are no progress details for this task to be displayed
prepare.bt_icon.output=Click here to display the progress details of this task
@@ -398,9 +420,11 @@ status.caching_data={} Daten auf der Festplatte zwischenspeichern
style=Stil
success=success
summary=Zusammenfassung
+task.canceled=canceled
task.checking_config=Checking configuration file
task.checking_config.saving=Creating file
task.download_categories=Downloading categories
+task.download_suggestions=Downloading suggestions
task.waiting_task=Waiting for {}
type=Typ
uninstall=Deinstallation
diff --git a/bauh/view/resources/locale/en b/bauh/view/resources/locale/en
index 1d54a14c..5dfcc963 100644
--- a/bauh/view/resources/locale/en
+++ b/bauh/view/resources/locale/en
@@ -111,21 +111,31 @@ bt.not_now=Not now
cancel=cancel
categories=categories
category.2dgraphics=2d graphics
+category.acessories=acessories
+category.admin=admin
category.administration=administration
category.amusement=amusement
+category.application=application
category.arcadegame=arcade game
+category.archiving=archiving
category.art=art
category.audio=audio
category.audiovideo=audio / video
category.books=books
category.browser=browser
+category.calculator=calculator
category.cloud=cloud
category.communication=communication
+category.compression=compression
+category.core=core
category.database=database
category.databases=databases
category.design=design
category.desktopsettings=desktop settings
+category.devel=development
category.development=development
+category.dictionary=dictionary
+category.doc=documentation
category.education=education
category.emulator=emulator
category.entertainment=entertainment
@@ -136,14 +146,21 @@ category.fonts=fonts
category.game=game
category.games=games
category.graphics=graphics
+category.hardwaresettings=hardware
category.health=health
category.instantmessaging=messaging
+category.libs=library
category.library=library
+category.localization=localization
+category.math=math
+category.mail=e-mail
category.media=media
category.messaging=messaging
+category.monitor=monitoring
category.movie=movie
category.movies=movies
category.music=music
+category.net=network
category.network=network
category.networks=networks
category.news=news
@@ -151,8 +168,10 @@ category.none=None
category.office=office
category.packagemanager=package manager
category.personalisation=personalization
-category.photo=photo
+category.photo=photography
+category.photography=photography
category.player=player
+category.presentation=presentation
category.productivity=productivity
category.reference=reference
category.science=science
@@ -160,6 +179,7 @@ category.security=security
category.server=server
category.settings=settings
category.social=social
+category.sound=sound
category.sportsgame=sports game
category.system=system
category.terminalemulator=terminal
@@ -174,6 +194,7 @@ category.videoeditor=video editor
category.viewer=viewer
category.weather=weather
category.web=web
+category.webbrowser=browser
category.webdevelopment=web development
category=category
change=change
@@ -362,6 +383,7 @@ notification.update_selected.success=app(s) updated successfully
ok=ok
others=others
popup.button.cancel=Cancel
+popup.button.continue=Continue
popup.button.no=No
popup.button.yes=Yes
popup.history.selected.tooltip=Current version
@@ -372,6 +394,8 @@ popup.root.continue=Continue
popup.root.msg=Requires your password to continue
popup.root.title=Authentication
popup.screenshots.no_screenshot.body=it was not possible to retrieve {} screenshots
+popup.title.error=Error
+popup.title.warning=Warning
prepare.bt_hide_details=Hide details
prepare.bt_icon.no_output=There are no progress details for this task to be displayed
prepare.bt_icon.output=Click here to display the progress details of this task
@@ -399,9 +423,11 @@ status.caching_data=Caching {} data to disk
style=style
success=success
summary=summary
+task.canceled=canceled
task.checking_config=Checking configuration file
task.checking_config.saving=Creating file
task.download_categories=Downloading categories
+task.download_suggestions=Downloading suggestions
task.waiting_task=Waiting for {}
type=type
uninstall=uninstall
diff --git a/bauh/view/resources/locale/es b/bauh/view/resources/locale/es
index f29725d8..53cb55f9 100644
--- a/bauh/view/resources/locale/es
+++ b/bauh/view/resources/locale/es
@@ -112,22 +112,32 @@ cancel=cancelar
categories=categorías
category=categoría
category.2dgraphics=gráficos 2d
+category.acessories=accesorios
+category.admin=administrador
category.administration=administración
category.amusement=diversión
+category.application=aplicación
category.applications=aplicaciones
category.arcadegame=juego de máquina
+category.archiving=archivo
category.art=arte
category.audio=audio
category.audiovideo=audio y vídeo
category.books=libros
category.browser=navegador
+category.calculator=calculadora
category.cloud=nube
category.communication=comunicación
+category.compression=compresión
+category.core=núcleo
category.database=base de datos
category.databases=bases de datos
category.design=diseño
category.desktopsettings=configuraciones
+category.devel=desarrollo
category.development=desarrollo
+category.dictionary=diccionario
+category.doc=documentación
category.education=educación
category.emulator=emulador
category.entertainment=entretenimiento
@@ -140,12 +150,17 @@ category.games=juegos
category.graphics=gráficos
category.health=salud
category.instantmessaging=mensajería
+category.libs=biblioteca
category.library=biblioteca
+category.localization=localización
+category.math=matemáticas
category.media=media
category.messaging=mensajería
+category.monitor=monitoreo
category.movie=película
category.movies=películas
category.music=música
+category.net=red
category.network=red
category.networks=redes
category.news=noticias
@@ -153,8 +168,10 @@ category.none=ninguna
category.office=oficina
category.packagemanager=administrador de paquetes
category.personalisation=personalización
-category.photo=foto
+category.photo=fotografía
+category.photography=fotografía
category.player=jugador
+category.presentation=presentación
category.productivity=productividad
category.reference=referencia
category.science=ciencias
@@ -162,6 +179,7 @@ category.security=seguridad
category.server=servidor
category.settings=configuraciones
category.social=social
+category.sound=sonido
category.sportsgame=juego de deportes
category.system=sistema
category.terminalemulator=terminal
@@ -176,6 +194,7 @@ category.videoeditor=editor de vídeos
category.viewer=viewer
category.weather=tiempo
category.web=web
+category.webbrowser=navegador
category.webdevelopment=desarrollo web
change=cambiar
clean=limpiar
@@ -363,6 +382,7 @@ notification.update_selected.success=aplicación(es) actualizada(s) correctament
ok=ok
others=otros
popup.button.cancel=Cancelar
+popup.button.continue=Continuar
popup.button.no=No
popup.button.yes=Sí
popup.history.selected.tooltip=Versión actual
@@ -373,6 +393,8 @@ popup.root.continue=Continuar
popup.root.msg=Proporcione su contraseña para continuar
popup.root.title=Autenticación
popup.screenshots.no_screenshot.body=No fue posible recuperar las fotos de {}
+popup.title.error=Error
+popup.title.warning=Aviso
prepare.bt_hide_details=Ocultar detalles
prepare.bt_icon.no_output=No hay detalles del progreso de esta tarea para mostrar
prepare.bt_icon.output=Haga clic aquí para mostrar los detalles del progreso de esta tarea
@@ -401,9 +423,11 @@ status.caching_data=Almacenando en antememoria los datos de {} para el disco
style=estilo
success=éxito
summary=resumen
+task.canceled=cancelada
task.checking_config=Verificando archivo de configuración
task.checking_config.saving=Creando archivo
task.download_categories=Descargando categorías
+task.download_suggestions=Descargando sugerencias
task.waiting_task=Esperando por {}
type=tipo
uninstall=desinstalar
diff --git a/bauh/view/resources/locale/fr b/bauh/view/resources/locale/fr
index c8a95bef..3e59d1da 100644
--- a/bauh/view/resources/locale/fr
+++ b/bauh/view/resources/locale/fr
@@ -111,21 +111,31 @@ bt.not_now=Pas maintenant
cancel=Annuler
categories=categories
category.2dgraphics=graphisqme 2d
+category.acessories=acessories
+category.admin=admin
category.administration=administration
category.amusement=amusement
+category.application=application
category.arcadegame=jeu d'arcade
+category.archiving=archiving
category.art=art
category.audio=son
category.audiovideo=son / video
category.books=livres
category.browser=navigateur
+category.calculator=calculator
category.cloud=cloud
category.communication=communication
+category.compression=compression
+category.core=core
category.database=base de données
category.databases=bases de données
category.design=design
category.desktopsettings=configuration
+category.devel=développement
category.development=développement
+category.dictionary=dictionary
+category.doc=documentation
category.education=éducation
category.emulator=émulateur
category.entertainment=divertissement
@@ -138,12 +148,17 @@ category.games=jeux
category.graphics=graphiques
category.health=santé
category.instantmessaging=messagerie instantanée
+category.libs=bibliothèque
category.library=bibliothèque
+category.localization=localization
+category.math=math
category.media=media
category.messaging=messagerie
+category.monitor=monitoring
category.movie=film
category.movies=films
category.music=musique
+category.net=réseau
category.network=réseau
category.networks=réseaux
category.news=presse
@@ -152,7 +167,9 @@ category.office=bureautique
category.packagemanager=gestionnaire de paquets
category.personalisation=personalisation
category.photo=photo
+category.photography=photo
category.player=lecteur
+category.presentation=presentation
category.productivity=productivité
category.reference=reference
category.science=science
@@ -160,6 +177,7 @@ category.security=securité
category.server=serveur
category.settings=paramères
category.social=social
+category.sound=sound
category.sportsgame=jeu de sports
category.system=système
category.terminalemulator=terminal
@@ -174,6 +192,7 @@ category.videoeditor=éditeur vidéo
category.viewer=viewer
category.weather=météo
category.web=web
+category.webbrowser=navigateur
category.webdevelopment=développement web
category=categorie
change=changement
@@ -358,6 +377,7 @@ notification.update_selected.success=app(s) mises à jour avec succès
ok=ok
others=autres
popup.button.cancel=Annuler
+popup.button.continue=Continuer
popup.button.no=Non
popup.button.yes=Oui
popup.history.selected.tooltip=Version Actuelle
@@ -368,6 +388,8 @@ popup.root.bad_password.title=Erreur
popup.root.continue=continuer
popup.root.title=Nécessite votre mot de passe pour continuer
popup.screenshots.no_screenshot.body=impossible de récupérer les captures d'écran de {}
+popup.title.error=Erreur
+popup.title.warning=Avertissement
prepare.bt_hide_details=Cacher les details
prepare.bt_icon.no_output=Pas de progrès à afficher pour cette tâche
prepare.bt_icon.output=Cliquez ici pour afficher l'avancement de cette tâche
@@ -395,9 +417,11 @@ status.caching_data=Mise en cache des données de {} sur disque
style=style
success=succès
summary=résumé
+task.canceled=canceled
task.checking_config=Checking configuration file
task.checking_config.saving=Creating file
task.download_categories=Téléchargement des catégories
+task.download_suggestions=Téléchargement des suggestions
task.waiting_task=Waiting for {}
type=type
uninstall=désinstaller
diff --git a/bauh/view/resources/locale/it b/bauh/view/resources/locale/it
index 039ce1b1..9480b5db 100644
--- a/bauh/view/resources/locale/it
+++ b/bauh/view/resources/locale/it
@@ -112,21 +112,31 @@ cancel=cancella
categories=categorie
category=categoria
category.2dgraphics=Grafica 2D
+category.acessories=acessories
+category.admin=admin
category.administration=amministrazione
category.amusement=amusement
+category.application=application
category.arcadegame=Gioco arcade
+category.archiving=archiving
category.art=art
category.audio=audio
category.audiovideo=audio / video
category.books=books
category.browser=browser
+category.calculator=calculator
category.cloud=cloud
category.communication=comunicazione
+category.compression=compression
+category.core=core
category.database=database
category.databases=databases
category.design=design
category.desktopsettings=impostazioni del desktop
+category.devel=development
category.development=development
+category.dictionary=dictionary
+category.doc=documentation
category.education=education
category.emulator=emulatore
category.entertainment=entertainment
@@ -139,12 +149,17 @@ category.games=games
category.graphics=graphics
category.health=health
category.instantmessaging=messaggistica
+category.libs=library
category.library=library
+category.localization=localization
+category.math=math
category.media=media
category.messaging=messaggistica
+category.monitor=monitoring
category.movie=movie
category.movies=movies
category.music=music
+category.net=network
category.network=network
category.networks=networks
category.news=news
@@ -153,7 +168,9 @@ category.office=office
category.packagemanager=gestore pacchetti
category.personalisation=personalization
category.photo=photo
+category.photography=photo
category.player=giocatore
+category.presentation=presentation
category.productivity=productivity
category.reference=reference
category.science=science
@@ -161,6 +178,7 @@ category.security=security
category.server=server
category.settings=impostazioni
category.social=social
+category.sound=sound
category.sportsgame=gioco di sport
category.system=system
category.terminalemulator=terminale
@@ -175,6 +193,7 @@ category.videoeditor=video editor
category.viewer=viewer
category.weather=weather
category.web=web
+category.webbrowser=browser
category.webdevelopment=sviluppo web
change=Cambia
clean=pulire
@@ -363,6 +382,7 @@ notification.update_selected.success=app aggiornate correttamente
ok=ok
others=altre
popup.button.cancel=Cancella
+popup.button.continue=Continuare
popup.button.no=No
popup.button.yes=Si
popup.history.selected.tooltip=Version corrente
@@ -373,6 +393,8 @@ popup.root.continue=Continuare
popup.root.msg=Richiede la tua password per continuare
popup.root.title=Autenticazione
popup.screenshots.no_screenshot.body=non è stato possibile recuperare {} schermate
+popup.title.error=Errore
+popup.title.warning=Avviso
prepar_panel.bt_skip.label=saltare
prepare.bt_hide_details=Hide details
prepare.bt_icon.no_output=There are no progress details for this task to be displayed
@@ -401,9 +423,11 @@ status.caching_data=Memorizza {} dati sul disco
style=stile
success=success
summary=riepilogo
+task.canceled=canceled
task.checking_config=Checking configuration file
task.checking_config.saving=Creating file
task.download_categories=Download delle categorie
+task.download_suggestions=Download dei suggerimenti
task.waiting_task=Waiting for {}
type=tipe
uninstall=Disinstalla
diff --git a/bauh/view/resources/locale/pt b/bauh/view/resources/locale/pt
index 89ec530a..5e156621 100644
--- a/bauh/view/resources/locale/pt
+++ b/bauh/view/resources/locale/pt
@@ -111,21 +111,31 @@ bt.not_now=Agora não
cancel=cancelar
categories=categorias
category.2dgraphics=gŕaficos 2d
+category.acessories=acessórios
+category.admin=administrador
category.administration=administração
category.amusement=diversão
+category.application=aplicação
category.arcadegame=jogo arcade
+category.archiving=arquivamento
category.art=arte
category.audio=áudio
category.audiovideo=áudio / vídeo
category.books=livros
category.browser=navegador
+category.calculator=calculadora
category.cloud=nuvem
category.communication=comunicação
+category.compression=compressão
+category.core=núcleo
category.database=banco de dados
category.databases=bancos de dados
category.design=desenho
category.desktopsettings=configurações
+category.devel=desenvolvimento
category.development=desenvolvimento
+category.dictionary=dicionário
+category.doc=documentação
category.education=educação
category.emulator=emulador
category.entertainment=entretenimento
@@ -138,12 +148,17 @@ category.games=jogos
category.graphics=gráficos
category.health=saúde
category.instantmessaging=mensagem
+category.libs=biblioteca
category.library=biblioteca
+category.localization=localização
+category.math=matemática
category.media=mídia
category.messaging=mensagem
+category.monitor=monitoria
category.movie=filme
category.movies=filmes
category.music=música
+category.net=rede
category.network=rede
category.networks=redes
category.news=notícias
@@ -151,8 +166,10 @@ category.none=Nenhuma
category.office=escritório
category.packagemanager=gerenciador de pacotes
category.personalisation=personalização
-category.photo=foto
+category.photo=fotografia
+category.photography=fotografia
category.player=jogador
+category.presentation=apresentação
category.productivity=produtividade
category.reference=referência
category.science=ciências
@@ -160,6 +177,7 @@ category.security=segurança
category.server=servidor
category.settings=configuração
category.social=social
+category.sound=som
category.sportsgame=jogo de esportes
category.system=sistema
category.terminalemulator=terminal
@@ -174,6 +192,7 @@ category.videoeditor=edito de vídeo
category.viewer=visualizador
category.weather=tempo
category.web=web
+category.webbrowser=navegador
category.webdevelopment=desenvolvimento web
category=categoria
change=alterar
@@ -362,6 +381,7 @@ notification.update_selected.success=aplicativo(s) atualizado(s) com sucesso
ok=ok
others=outros
popup.button.cancel=Cancelar
+popup.button.continue=Continuar
popup.button.no=Não
popup.button.yes=Sim
popup.history.selected.tooltip=Versão atual
@@ -372,6 +392,8 @@ popup.root.continue=Prosseguir
popup.root.msg=Requer sua senha para prosseguir
popup.root.title=Autenticação
popup.screenshots.no_screenshot.body=Não foi possível obter as fotos de {}
+popup.title.error=Erro
+popup.title.warning=Aviso
prepare.bt_hide_details=Ocultar detalhes
prepare.bt_icon.no_output=Não existe detalhes do progresso dessa tarefa para ser exibido
prepare.bt_icon.output=Clique aqui para exibir detalhes do progresso dessa tarefa
@@ -399,9 +421,11 @@ status.caching_data=Cacheando dados de {} para o disco
style=estilo
success=sucesso
summary=resumo
+task.canceled=canceleda
task.checking_config=Verificando arquivo de configuração
task.checking_config.saving=Criando arquivo
task.download_categories=Baixando categorias
+task.download_suggestions=Baixando sugestões
task.waiting_task=Aguardando {}
type=tipo
uninstall=desinstalar
diff --git a/bauh/view/resources/locale/ru b/bauh/view/resources/locale/ru
index 8ac7182a..d429a3dc 100644
--- a/bauh/view/resources/locale/ru
+++ b/bauh/view/resources/locale/ru
@@ -112,21 +112,31 @@ cancel=Отмена
categories=Категории
category=Категория
category.2dgraphics=2D-графика
+category.acessories=acessories
+category.admin=admin
category.administration=Администрирование
category.amusement=amusement
+category.application=application
category.arcadegame=Аркада
+category.archiving=archiving
category.art=art
category.audio=Аудио
category.audiovideo=Аудио/Видео
category.books=books
category.browser=browser
+category.calculator=calculator
category.cloud=cloud
category.communication=Общение
+category.compression=compression
+category.core=core
category.database=database
category.databases=databases
category.design=design
category.desktopsettings=Настройки рабочего стола
+category.devel=Разработка
category.development=Разработка
+category.dictionary=dictionary
+category.doc=documentation
category.education=education
category.emulator=Эмуляторы
category.entertainment=entertainment
@@ -139,12 +149,17 @@ category.games=games
category.graphics=graphics
category.health=health
category.instantmessaging=Мессенджер
+category.libs=library
category.library=library
+category.localization=localization
+category.math=math
category.media=media
category.messaging=Сообщения
+category.monitor=monitoring
category.movie=movie
category.movies=movies
category.music=music
+category.net=network
category.network=network
category.networks=networks
category.news=news
@@ -153,7 +168,9 @@ category.office=Офис
category.packagemanager=Пакетный менеджер
category.personalisation=personalization
category.photo=photo
+category.photography=photo
category.player=Проигрыватель
+category.presentation=presentation
category.productivity=productivity
category.reference=reference
category.science=science
@@ -161,6 +178,7 @@ category.security=security
category.server=server
category.settings=Настройки
category.social=social
+category.sound=sound
category.sportsgame=Спортивная игра
category.system=system
category.terminalemulator=Эмулятор терминала
@@ -175,6 +193,7 @@ category.videoeditor=video editor
category.viewer=viewer
category.weather=weather
category.web=Сеть
+category.webbrowser=browser
category.webdevelopment=Web-разработка
change=Изменить
clean=Очистка
@@ -361,6 +380,7 @@ notification.update_selected.success=Приложение(я) успешно о
ok=ok
others=Другие
popup.button.cancel=Отмена
+popup.button.continue=Продолжить
popup.button.no=Нет
popup.button.yes=Да
popup.history.selected.tooltip=Текущая версия
@@ -371,6 +391,8 @@ popup.root.continue=Продолжить
popup.root.msg=Требуется ваш пароль, чтобы продолжить
popup.root.title=Аутентификация
popup.screenshots.no_screenshot.body=не удалось получить скриншоты {}
+popup.title.error=Ошибка
+popup.title.warning=Предупреждение
prepare.bt_hide_details=Hide details
prepare.bt_icon.no_output=There are no progress details for this task to be displayed
prepare.bt_icon.output=Click here to display the progress details of this task
@@ -398,9 +420,11 @@ status.caching_data=Кэширование данных {} на диск
style=Стиль
success=success
summary=Суммарно
+task.canceled=canceled
task.checking_config=Checking configuration file
task.checking_config.saving=Creating file
task.download_categories=Downloading categories
+task.download_suggestions=Downloading suggestions
task.waiting_task=Waiting for {}
type=Тип
uninstall=Деинсталляция
diff --git a/bauh/view/resources/locale/tr b/bauh/view/resources/locale/tr
index 32c1d0a2..1c74f674 100644
--- a/bauh/view/resources/locale/tr
+++ b/bauh/view/resources/locale/tr
@@ -111,21 +111,31 @@ bt.not_now=Şimdi değil
cancel=vazgeç
categories=Kategoriler
category.2dgraphics=2d grafikler
+category.acessories=acessories
+category.admin=admin
category.administration=yönetim
category.amusement=eğlence
+category.application=application
category.arcadegame=arcade oyunlar
+category.archiving=archiving
category.art=sanat
category.audio=ses
category.audiovideo=ses / video
category.books=kitaplar
category.browser=tarayıcı
+category.calculator=calculator
category.cloud=bulut
category.communication=iletişim
+category.compression=compression
+category.core=core
category.database=veritabanı
category.databases=veritabanları
category.design=tasarım
category.desktopsettings=masaüstü ayarları
+category.devel=gelişim
category.development=gelişim
+category.dictionary=dictionary
+category.doc=documentation
category.education=eğitim
category.emulator=emülatör
category.entertainment=eğlence
@@ -138,12 +148,17 @@ category.games=oyunlar
category.graphics=grafikler
category.health=sağlık
category.instantmessaging=mesajlaşma
+category.libs=kütüphane
category.library=kütüphane
+category.localization=localization
+category.math=math
category.media=medya
category.messaging=mesajlaşma
+category.monitor=monitoring
category.movie=film
category.movies=filmler
category.music=müzik
+category.net=ağ
category.network=ağ
category.networks=ağlar
category.news=haberler
@@ -152,7 +167,9 @@ category.office=ofis
category.packagemanager=paket yönetici
category.personalisation=kişiselleştirme
category.photo=fotoğraf
+category.photography=fotoğraf
category.player=oyuncu
+category.presentation=presentation
category.productivity=verimlilik
category.reference=referans
category.science=bilim
@@ -160,6 +177,7 @@ category.security=güvenlik
category.server=sunucu
category.settings=ayarlar
category.social=sosyal
+category.sound=sound
category.sportsgame=spor oyunları
category.system=sistem
category.terminalemulator=uçbirim
@@ -174,6 +192,7 @@ category.videoeditor=video düzenleyici
category.viewer=gösterici
category.weather=hava durumu
category.web=web
+category.webbrowser=tarayıcı
category.webdevelopment=web gelişimi
category=kategori
change=değiştir
@@ -361,6 +380,7 @@ notification.update_selected.success=uygulama(lar) başarıyla güncellendi
ok=tamam
others=diğerleri
popup.button.cancel=Vazgeç
+popup.button.continue=Devam
popup.button.no=Hayır
popup.button.yes=Evet
popup.history.selected.tooltip=Mevcut sürüm
@@ -371,6 +391,8 @@ popup.root.continue=devam
popup.root.msg=Devam etmek için şifre girmelisiniz
popup.root.title=Doğrulama
popup.screenshots.no_screenshot.body={} ekran görüntüsünü almak mümkün değildi
+popup.title.error=Hata
+popup.title.warning=Uyarı
prepare.bt_hide_details=Hide details
prepare.bt_icon.no_output=There are no progress details for this task to be displayed
prepare.bt_icon.output=Click here to display the progress details of this task
@@ -398,9 +420,11 @@ status.caching_data={} verilerini diske önbellekle
style=Görünüm biçimi
success=başarılı
summary=özet
+task.canceled=canceled
task.checking_config=Checking configuration file
task.checking_config.saving=Creating file
task.download_categories=Kategoriler indiriliyor
+task.download_suggestions=Downloading suggestions
task.waiting_task=Waiting for {}
type=tür
uninstall=kaldır
diff --git a/tests/__init__.py b/tests/__init__.py
index e69de29b..17431ac7 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -0,0 +1,10 @@
+
+class AnyInstance(object):
+ def __init__(self, instance_class: type):
+ self._class = instance_class
+
+ def __eq__(self, other):
+ return other is not None and isinstance(other, self._class)
+
+ def __repr__(self):
+ return f''
diff --git a/tests/gems/debian/__init__.py b/tests/gems/debian/__init__.py
new file mode 100644
index 00000000..a2b38d41
--- /dev/null
+++ b/tests/gems/debian/__init__.py
@@ -0,0 +1,3 @@
+import os
+
+DEBIAN_TESTS_DIR = os.path.dirname(os.path.abspath(__file__))
diff --git a/tests/gems/debian/resources/app_idx_full.json b/tests/gems/debian/resources/app_idx_full.json
new file mode 100644
index 00000000..bd3d4989
--- /dev/null
+++ b/tests/gems/debian/resources/app_idx_full.json
@@ -0,0 +1,17 @@
+{
+ "firefox": {
+ "categories": [
+ "GNOME",
+ "GTK",
+ "Network",
+ "WebBrowser"
+ ],
+ "exe_path": "firefox %u",
+ "icon_path": "firefox"
+ },
+
+ "synaptic": {
+ "exe_path": "synaptic-pkexec",
+ "icon_path": "synaptic"
+ }
+}
diff --git a/tests/gems/debian/test_aptitude.py b/tests/gems/debian/test_aptitude.py
new file mode 100644
index 00000000..eb16a73a
--- /dev/null
+++ b/tests/gems/debian/test_aptitude.py
@@ -0,0 +1,169 @@
+from unittest import TestCase
+from unittest.mock import Mock, patch
+
+from bauh import __app_name__
+from bauh.commons import system
+from bauh.commons.system import USE_GLOBAL_INTERPRETER
+from bauh.gems.debian.aptitude import Aptitude, map_package_name
+from bauh.gems.debian.model import DebianPackage
+
+
+class MapPackageNameTest(TestCase):
+
+ def test__it_must_return_the_same_input_when_no_colon(self):
+ self.assertEqual('my_package', map_package_name('my_package'))
+
+ def test__it_must_return_the_only_the_word_before_the_first_colon(self):
+ self.assertEqual('my_package', map_package_name('my_package:i386'))
+
+ def test__it_must_return_a_name_with_colon_when_several_colons_are_present(self):
+ self.assertEqual('my_package:i386', map_package_name('my_package:i386:amd64'))
+
+
+class AptitudeTest(TestCase):
+
+ def setUp(self):
+ self.aptitude = Aptitude(Mock())
+
+ @patch(f'{__app_name__}.gems.debian.aptitude.system.execute', return_value=(0, """
+gimp-cbmplugs^^1.2.2-1build1^Distro Developers ^universe/graphics^plugins for The GIMP to import/export Commodore 64 files
+gimp-gmic^2.4.5-1.0^2.4.5-1.1^Distro Developers ^plugin^GREYC's Magic for Image Computing - GIMP Plugin
+gimp-gutenprint^5.3.3-4^5.3.3-4^Distro Developers ^plugin^print plugin for the GIMP
+gimp-help^^^^^
+ """))
+ def test_search__must_return_installed_and_not_installed_packages_with_updates(self, execute: Mock):
+ query = 'gimp'
+ res = [p for p in self.aptitude.search(query=query)]
+
+ execute.assert_called_once_with(f"aptitude search {query} -q -F '%p^%v^%V^%m^%s^%d' --disable-columns",
+ shell=True, custom_env=system.gen_env(USE_GLOBAL_INTERPRETER, lang=''))
+
+ exp = [
+ DebianPackage(name='gimp-cbmplugs', version='1.2.2-1build1', latest_version='1.2.2-1build1',
+ maintainer='Distro Developers',
+ description='plugins for The GIMP to import/export Commodore 64 files',
+ categories=('graphics',),
+ installed=False, update=False),
+ DebianPackage(name='gimp-gmic', version='2.4.5-1.0', latest_version='2.4.5-1.1',
+ maintainer='Distro Developers',
+ description="GREYC's Magic for Image Computing - GIMP Plugin",
+ categories=('plugin',),
+ installed=True, update=True),
+ DebianPackage(name='gimp-gutenprint', version='5.3.3-4', latest_version='5.3.3-4',
+ maintainer='Distro Developers',
+ description="print plugin for the GIMP",
+ categories=('plugin',),
+ installed=True, update=False),
+ ]
+
+ self.assertEqual([p.__dict__ for p in exp], [p.__dict__ for p in res])
+
+ @patch(f'{__app_name__}.gems.debian.aptitude.system.execute', return_value=(0, """
+Package: firefox
+Version: 97.0+distro1+una
+State: installed (95.0.1+distro1.1+una), upgrade available (97.0+distro1+una)
+Automatically installed: no
+Priority: optional
+Section: web
+Maintainer: Distro Dev
+Architecture: amd64
+Uncompressed Size: 236 M
+PreDepends: distro-system-adjustments (>= 2021.12.16)
+Breaks: firefox-dbg (< 95.0.1+distro1+una), firefox-dev (< 95.0.1+distro1+una), firefox-geckodriver (< 95.0.1+distro1+una), firefox-mozsymbols (< 95.0.1+distro1+una)
+Replaces: firefox-dbg (< 95.0.1+distro1+una), firefox-dev (< 95.0.1+distro1+una), firefox-geckodriver (< 95.0.1+distro1+una), firefox-mozsymbols (< 95.0.1+distro1+una)
+Provides: gnome-www-browser, www-browser
+Description: The Firefox web browser
+ The Mozilla Firefox Web Browser.
+
+Package: gcc
+Version: 4:9.3.0-1distro2
+State: installed
+Automatically installed: no
+Priority: optional
+Section: devel
+Maintainer: Distro Developers
+Architecture: amd64
+Uncompressed Size: 51,2 k
+Depends: cpp (= 4:9.3.0-1distro2), gcc-9 (>= 9.3.0-3~)
+Recommends: libc6-dev | libc-dev
+Suggests: gcc-multilib, make, manpages-dev, autoconf, automake, libtool, flex, bison, gdb, gcc-doc
+Conflicts: gcc-doc (< 1:2.95.3), gcc-doc:i386 (< 1:2.95.3), gcc:i386
+Provides: c-compiler, gcc-x86-64-linux-gnu (= 4:9.3.0-1distro2), gcc:amd64 (= 4:9.3.0-1distro2)
+Description: GNU C compiler
+ This is the GNU C compiler, a fairly portable optimizing compiler for C.
+
+ This is a dependency package providing the default GNU C compiler.
+
+"""))
+ def test_show__all_attributes(self, execute: Mock):
+ info = self.aptitude.show(('firefox', 'gcc'))
+ execute.assert_called_once_with('aptitude show -q firefox gcc', shell=True,
+ custom_env=system.gen_env(global_interpreter=system.USE_GLOBAL_INTERPRETER,
+ lang=''))
+
+ expected = {
+ 'firefox': {
+ 'version': '97.0+distro1+una',
+ 'state': ('installed (95.0.1+distro1.1+una)', 'upgrade available (97.0+distro1+una)'),
+ 'automatically installed': 'no',
+ 'priority': 'optional',
+ 'section': 'web',
+ 'maintainer': 'Distro Dev ',
+ 'architecture': 'amd64',
+ 'uncompressed size': 236000000,
+ 'predepends': ('distro-system-adjustments (>= 2021.12.16)', ),
+ 'breaks': ('firefox-dbg (< 95.0.1+distro1+una)', 'firefox-dev (< 95.0.1+distro1+una)',
+ 'firefox-geckodriver (< 95.0.1+distro1+una)', 'firefox-mozsymbols (< 95.0.1+distro1+una)'),
+ 'replaces': ('firefox-dbg (< 95.0.1+distro1+una)', 'firefox-dev (< 95.0.1+distro1+una)',
+ 'firefox-geckodriver (< 95.0.1+distro1+una)', 'firefox-mozsymbols (< 95.0.1+distro1+una)'),
+ 'provides': ('gnome-www-browser', 'www-browser'),
+ 'description': 'The Firefox web browser'
+ },
+ 'gcc': {
+ 'version': '4:9.3.0-1distro2',
+ 'state': ('installed', ),
+ 'automatically installed': 'no',
+ 'priority': 'optional',
+ 'section': 'devel',
+ 'maintainer': 'Distro Developers ',
+ 'architecture': 'amd64',
+ 'uncompressed size': 51200,
+ 'depends': ('cpp (= 4:9.3.0-1distro2)', 'gcc-9 (>= 9.3.0-3~)'),
+ 'recommends': ('libc6-dev | libc-dev', ),
+ 'suggests': ('gcc-multilib', 'make', 'manpages-dev', 'autoconf', 'automake',
+ 'libtool', 'flex', 'bison', 'gdb', 'gcc-doc'),
+ 'conflicts': ('gcc-doc (< 1:2.95.3)', 'gcc-doc:i386 (< 1:2.95.3)', 'gcc:i386'),
+ 'provides': ('c-compiler', 'gcc-x86-64-linux-gnu (= 4:9.3.0-1distro2)', 'gcc:amd64 (= 4:9.3.0-1distro2)'),
+ 'description': 'GNU C compiler'
+ }
+ }
+
+ self.assertEqual(expected, info)
+
+ @patch(f'{__app_name__}.gems.debian.aptitude.system.execute', return_value=(0, """
+ gir1.2-javascriptcoregtk-4.0^2.34.1-0distro0.20.04.1^2.34.4-0distro0.20.04.1^Distro Developers ^library^JavaScript engine library from WebKitGTK - GObject introspection data
+ gir1.2-nm-1.0^1.22.10-1distro2.2^1.22.10-1distro2.3^Distro Developers ^library^GObject introspection data for the libnm library
+ xwayland^2:1.20.13-1distro1~20.04.2^2:1.20.13-1distro1~20.04.2^Distro X-SWAT ^X11^Xwayland X server
+ """))
+ def test_read_installed__with_updates_available(self, execute: Mock):
+ returned = [p for p in self.aptitude.read_installed()]
+ execute.assert_called_once()
+
+ expected = [DebianPackage(name='gir1.2-javascriptcoregtk-4.0', version='2.34.1-0distro0.20.04.1',
+ latest_version='2.34.4-0distro0.20.04.1',
+ maintainer='Distro Developers', update=True, installed=True,
+ categories=('library',),
+ description='JavaScript engine library from WebKitGTK - GObject introspection data'),
+ DebianPackage(name='gir1.2-nm-1.0', version='1.22.10-1distro2.2',
+ latest_version='1.22.10-1distro2.3',
+ maintainer='Distro Developers', update=True, installed=True,
+ categories=('library',),
+ description='GObject introspection data for the libnm library'),
+ DebianPackage(name='xwayland', version='2:1.20.13-1distro1~20.04.2',
+ latest_version='2:1.20.13-1distro1~20.04.2',
+ maintainer='Distro X-SWAT', update=False, installed=True,
+ categories=('X11',),
+ description='Xwayland X server')
+ ]
+
+ self.assertEqual([p.__dict__ for p in expected], [p.__dict__ for p in returned])
diff --git a/tests/gems/debian/test_controller.py b/tests/gems/debian/test_controller.py
new file mode 100644
index 00000000..5a423a5a
--- /dev/null
+++ b/tests/gems/debian/test_controller.py
@@ -0,0 +1,99 @@
+from unittest import TestCase
+from unittest.mock import MagicMock, patch, Mock
+
+from bauh import __app_name__
+from bauh.api.abstract.controller import SearchResult
+from bauh.gems.debian.controller import DebianPackageManager
+from bauh.gems.debian.model import DebianPackage, DebianApplication
+
+
+class DebianPackageManagerTest(TestCase):
+
+ def setUp(self):
+ self.controller = DebianPackageManager(MagicMock())
+ self.controller._apps_index = {}
+
+ @patch(f'{__app_name__}.gems.debian.controller.Aptitude.read_installed', return_value=iter((
+ DebianPackage(name='gir1.2-javascriptcoregtk-4.0', version='2.34.1-0distro0.20.04.1',
+ latest_version='2.34.1-0distro0.20.04.1',
+ maintainer='Distro Developers', update=False, installed=True,
+ description='JavaScript engine library from WebKitGTK - GObject introspection data'),
+ DebianPackage(name='xwayland', version='2:1.20.13-1distro1~20.04.2',
+ latest_version='2:1.20.13-1distro1~20.04.2',
+ maintainer='Distro X-SWAT', update=False, installed=True,
+ description='Xwayland X server')
+ )))
+ def test_read_installed__must_associated_packages_found_to_applications_if_appliable(self, read_installed: Mock):
+ app = DebianApplication(name='xwayland', exe_path='xwayland', icon_path='xwayland', categories=('app',))
+
+ self.controller.__apps_index = {'xwayland': app}
+
+ result = self.controller.read_installed(disk_loader=None, pkg_types=None, internet_available=False)
+ read_installed.assert_called_once()
+
+ self.assertIsNone(result.new)
+
+ expected = [DebianPackage(name='gir1.2-javascriptcoregtk-4.0', version='2.34.1-0distro0.20.04.1', latest_version='2.34.1-0distro0.20.04.1',
+ maintainer='Distro Developers', update=False, installed=True,
+ description='JavaScript engine library from WebKitGTK - GObject introspection data'),
+ DebianPackage(name='xwayland', version='2:1.20.13-1distro1~20.04.2', latest_version='2:1.20.13-1distro1~20.04.2',
+ maintainer='Distro X-SWAT', update=False, installed=True,
+ description='Xwayland X server', app=app)
+ ]
+
+ self.assertEqual(expected, result.installed)
+
+ @patch(f'{__app_name__}.gems.debian.controller.Aptitude.read_installed', return_value=iter((
+ DebianPackage(name='gir1.2-javascriptcoregtk-4.0', version='2.34.1-0distro0.20.04.1',
+ latest_version='2.34.1-0distro0.20.04.1',
+ maintainer='Distro Developers', update=False, installed=True,
+ description='JavaScript engine library from WebKitGTK - GObject introspection data'),
+ DebianPackage(name='xwayland', version='2:1.20.13-1distro1~20.04.2',
+ latest_version='2:1.20.13-1distro1~20.04.2',
+ maintainer='Distro X-SWAT', update=False, installed=True,
+ description='Xwayland X server')
+ )))
+ def test_read_installed__internet_not_available(self, read_installed: Mock):
+ result = self.controller.read_installed(disk_loader=None, pkg_types=None, internet_available=False)
+ read_installed.assert_called_once()
+
+ self.assertIsNone(result.new)
+
+ expected = [DebianPackage(name='gir1.2-javascriptcoregtk-4.0', version='2.34.1-0distro0.20.04.1', latest_version='2.34.1-0distro0.20.04.1',
+ maintainer='Distro Developers', update=False, installed=True,
+ description='JavaScript engine library from WebKitGTK - GObject introspection data'),
+ DebianPackage(name='xwayland', version='2:1.20.13-1distro1~20.04.2', latest_version='2:1.20.13-1distro1~20.04.2',
+ maintainer='Distro X-SWAT', update=False, installed=True,
+ description='Xwayland X server')
+ ]
+
+ self.assertEqual(expected, result.installed)
+
+ @patch(f'{__app_name__}.gems.debian.controller.Aptitude.read_installed')
+ def test_search__must_return_empty_result_when_url(self, read_installed: Mock):
+ words = 'i'
+ res = self.controller.search(words=words, disk_loader=None, limit=-1, is_url=True)
+ read_installed.assert_not_called()
+ self.assertEqual(SearchResult.empty(), res)
+
+ @patch(f'{__app_name__}.gems.debian.controller.Aptitude.search', return_value=iter((
+ DebianPackage(name='xpto', version='1.0', latest_version='1.0', installed=True, update=False, description=''),
+ DebianPackage(name='test', version='1.0', latest_version='1.0', installed=False, update=False, description=''),
+ DebianPackage(name='myapp', version='1.0', latest_version='1.0', installed=True, update=False, description=''),
+ )))
+ def test_search__returned_packages_should_be_associated_with_apps_if_appliable(self, search: Mock):
+ app = DebianApplication(name='myapp', exe_path='myapp', icon_path='myapp',
+ categories=('app',))
+ self.controller._apps_index = {'myapp': app}
+
+ words = 'test'
+ res = self.controller.search(words=words, disk_loader=None, limit=-1, is_url=False)
+ search.assert_called_once_with(words)
+
+ self.assertEqual([DebianPackage(name='xpto', version='1.0', latest_version='1.0', installed=True, update=False, description=''),
+ DebianPackage(name='myapp', version='1.0', latest_version='1.0', installed=True, update=False,
+ description='', app=app)
+ ], res.installed)
+
+ self.assertEqual([DebianPackage(name='test', version='1.0', latest_version='1.0', installed=False, update=False, description='')],
+ res.new)
diff --git a/tests/gems/debian/test_index.py b/tests/gems/debian/test_index.py
new file mode 100644
index 00000000..01a5de24
--- /dev/null
+++ b/tests/gems/debian/test_index.py
@@ -0,0 +1,171 @@
+import json
+import os.path
+from unittest import TestCase
+from unittest.mock import Mock, patch, call
+
+from bauh import __app_name__
+from bauh.gems.debian.index import ApplicationsMapper, ApplicationIndexer
+from bauh.gems.debian.model import DebianApplication
+from tests.gems.debian import DEBIAN_TESTS_DIR
+
+def mock_read_file(fpath: str):
+ if fpath.endswith('firefox.desktop'):
+ return """
+ [Desktop Entry]
+ Version=1.0
+ Name=Firefox Web Browser
+ Comment=Browse the World Wide Web
+ GenericName=Web Browser
+ Keywords=Internet;WWW;Browser;Web;Explorer
+ Exec=firefox %u
+ Terminal=false
+ X-MultipleArgs=false
+ Type=Application
+ Icon=firefox
+ Categories=GNOME;GTK;Network;WebBrowser;
+ """
+ elif fpath.endswith('synaptic.desktop'):
+ return """
+ [Desktop Entry]
+ Name=Synaptic Package Manager
+ GenericName=Package Manager
+ Comment=Install, remove and upgrade software packages
+ Exec=synaptic-pkexec
+ Icon=synaptic
+ Terminal=false
+ Type=Application
+ """
+ elif fpath.endswith('no-icon.desktop'):
+ return """
+ [Desktop Entry]
+ Name=No-icon
+ Exec=no-icon
+ Terminal=false
+ Type=Application
+ Categories=PackageManager;GTK;System;Settings;
+ """
+ elif fpath.endswith('no-exe.desktop'):
+ return """
+ [Desktop Entry]
+ Name=No-exe
+ Terminal=false
+ Type=Application
+ Icon=no-exe
+ Categories=PackageManager;GTK;System;Settings;
+ """
+ elif fpath.endswith('no-display.desktop'):
+ return """
+ [Desktop Entry]
+ Name=No-display
+ Exec=no-display
+ Terminal=false
+ Type=Application
+ Icon=no-display
+ NoDisplay=true
+ Categories=PackageManager;GTK;System;Settings;
+ """
+ elif fpath.endswith('terminal-app.desktop'):
+ return """
+ [Desktop Entry]
+ Name=Terminal-App
+ Exec=terminal-app
+ Terminal=true
+ Type=Application
+ Icon=terminal-app
+ Categories=PackageManager;GTK;System;Settings;
+ """
+
+
+class ApplicationsMapperTest(TestCase):
+
+ def setUp(self):
+ self.mapper = ApplicationsMapper(logger=Mock(), workers=1)
+
+ @patch(f'{__app_name__}.gems.debian.index.ApplicationsMapper._read_file', side_effect=mock_read_file)
+ @patch(f'{__app_name__}.gems.debian.index.system.execute', return_value=(0, """
+ firefox: /usr/share/applications/firefox.desktop
+ app-install-data: /usr/share/app-install/desktop/firefox-launchpad-plugin.desktop
+ app-install-data: /usr/share/app-install/desktop/firefox:firefox.desktop
+ xfce4-helpers: /usr/share/xfce4/helpers/firefox.desktop
+ synaptic: /usr/share/applications/synaptic.desktop
+ no-icon: /usr/share/applications/no-icon.desktop
+ no-exe: /usr/share/applications/no-exe.desktop
+ no-display: /usr/share/applications/no-display.desktop
+ terminal-app: /usr/share/applications/terminal-app.desktop
+ """))
+ def test_map_executable_applications__return_applications_with_exec_and_icon(self, execute: Mock, read_file: Mock):
+ apps = self.mapper.map_executable_applications()
+ execute.assert_called_once_with('dpkg-query -S .desktop', shell=True)
+ read_file.assert_has_calls([call('/usr/share/applications/firefox.desktop'),
+ call('/usr/share/applications/synaptic.desktop'),
+ call('/usr/share/applications/no-icon.desktop'),
+ call('/usr/share/applications/no-exe.desktop')], any_order=True)
+
+ self.assertEqual({
+ DebianApplication(name='firefox', exe_path='firefox %u', icon_path='firefox',
+ categories=('GNOME', 'GTK', 'Network', 'WebBrowser')),
+ DebianApplication(name='synaptic', exe_path='synaptic-pkexec', icon_path='synaptic',
+ categories=None)
+ }, apps)
+
+
+class ApplicationIndexerTest(TestCase):
+
+ def setUp(self):
+ self.update_idx_file_path = f'{DEBIAN_TESTS_DIR}/resources/apps_idx.json'
+ self.update_idx_ts_file_path = f'{self.update_idx_file_path}.ts'
+ self.app_indexer = ApplicationIndexer(logger=Mock(),
+ index_file_path=self.update_idx_file_path)
+
+ if os.path.exists(self.update_idx_file_path):
+ os.remove(self.update_idx_file_path)
+
+ def tearDown(self) -> None:
+ if os.path.exists(self.update_idx_file_path):
+ os.remove(self.update_idx_file_path)
+
+ if os.path.exists(self.update_idx_ts_file_path):
+ os.remove(self.update_idx_ts_file_path)
+
+ def test_update_index(self):
+ apps = {
+ DebianApplication(name='firefox', exe_path='firefox %u', icon_path='firefox',
+ categories=('GNOME', 'GTK', 'Network', 'WebBrowser')),
+ DebianApplication(name='synaptic', exe_path='synaptic-pkexec', icon_path='synaptic',
+ categories=None)
+ }
+
+ self.app_indexer.update_index(apps)
+
+ self.assertTrue(os.path.exists(self.update_idx_file_path))
+
+ with open(self.update_idx_file_path) as f:
+ index_content = f.read()
+
+ self.assertEqual({'firefox': {'exe_path': 'firefox %u', 'icon_path': 'firefox',
+ 'categories': ['GNOME', 'GTK', 'Network', 'WebBrowser']},
+ 'synaptic': {'exe_path': 'synaptic-pkexec', 'icon_path': 'synaptic',
+ 'categories': None}
+ }, json.loads(index_content))
+
+ self.assertTrue(os.path.isfile(self.update_idx_ts_file_path))
+
+ with open(self.update_idx_ts_file_path) as f:
+ ts_str = f.read()
+
+ try:
+ float(ts_str)
+ except ValueError:
+ self.assertFalse(False, "index timestamp must be a float number")
+
+ def test_read_index(self):
+ self.app_indexer._file_path = f'{DEBIAN_TESTS_DIR}/resources/app_idx_full.json'
+
+ expected = {
+ DebianApplication(name='firefox', exe_path='firefox %u', icon_path='firefox',
+ categories=('GNOME', 'GTK', 'Network', 'WebBrowser')),
+ DebianApplication(name='synaptic', exe_path='synaptic-pkexec', icon_path='synaptic',
+ categories=None)
+ }
+
+ self.assertEqual(expected, {app for app in self.app_indexer.read_index()})