diff --git a/CHANGELOG.md b/CHANGELOG.md
index 45f391fef..be3f9a9dd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,62 @@ 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.10.1] 2022-03-31
+
+### Features
+- Flatpak
+ - new custom action "Full update": fully updates all installed Flatpak apps and components (useful if you are having issues with runtime updates)
+
+
+
+
+
+
+### Improvements
+- General
+ - code refactoring
+ - backup:
+ - single mode: now supports two remove methods [#244](https://github.com/vinifmor/bauh/issues/244)
+ - self: it removes only self generated backups/snapshots (default)
+ - all: it removes all existing backup/snapshots on the disc
+
+
+
+
+
+- AppImage
+ - Limiting the UI components width of the file installation and upgrade windows
+
+- Arch
+ - text length of some popups reduced
+
+- Snap
+ - allowing the actions output locale to be decided by the Snap client
+
+- UI
+ - only displaying the "Installed" filter when installed packages are available on the table
+ - settings: margin between components reduced [#241](https://github.com/vinifmor/bauh/issues/241)
+ - "close" button added to the screenshots window (some distributions hide the default "x" on the dialog frame) [#246](https://github.com/vinifmor/bauh/issues/246)
+
+### Fixes
+- Arch
+ - regression: not displaying ignored updates
+ - dependency size: display a '?' instead of '0' ('?' should only be displayed when the size is unknown)
+
+- Debian
+ - packages descriptions are not displayed on the system's default language (when available)
+
+- Flatpak:
+ - executed commands are not displayed on the system default language and encoding (requires Flatpak >= 1.12) [#242](https://github.com/vinifmor/bauh/issues/242)
+ - applications and runtimes descriptions are not displayed on the system default language (when available) [#242](https://github.com/vinifmor/bauh/issues/242)
+
+- Web
+ - using the wrong locale format for the Accept-Language header
+
+- UI:
+ - rare crash when updating table items
+ - some action errors not being displayed on the details component when they are concatenated with sudo output
+
## [0.10.0] 2022-03-14
### Features
diff --git a/README.md b/README.md
index 9b554c81b..faa24bac4 100644
--- a/README.md
+++ b/README.md
@@ -313,6 +313,8 @@ prefer_repository_provider: true # when there is just one repository provider f
```
installation_level: null # defines a default installation level: "user" or "system". (null will display a popup asking the level)
```
+- Custom actions supported:
+ - **Full update**: it completely updates the Flatpak apps and components. Useful if you are having issues with runtime updates.
#### Snap
@@ -436,12 +438,13 @@ disk:
after_upgrade: false # it trims the disk after a successful packages upgrade (`fstrim -a -v`). 'true' will automatically perform the trim and 'null' will display a confirmation dialog
backup:
enabled: true # generate timeshift snapshots before an action (if timeshift is installed on the system)
- mode: 'incremental' # incremental=generates a new snapshot based on another pre-exising one. 'only_one'=deletes all pre-existing snapshots and generates a fresh one.
+ mode: 'incremental' # incremental=generates a new snapshot based on another pre-exising one. 'only_one'=deletes all pre-existing self created snapshots and generates a fresh one.
install: null # defines if the backup should be performed before installing a package. Allowed values: null (a dialog will be displayed asking if a snapshot should be generated), true: generates the backup without asking. false: disables the backup for this operation
uninstall: null # defines if the backup should be performed before uninstalling a package. Allowed values: null (a dialog will be displayed asking if a snapshot should be generated), true: generates the backup without asking. false: disables the backup for this operation
upgrade: null # defines if the backup should be performed before upgrading a package. Allowed values: null (a dialog will be displayed asking if a snapshot should be generated), true: generates the backup without asking. false: disables the backup for this operation
downgrade: null # defines if the backup should be performed before downgrading a package. Allowed values: null (a dialog will be displayed asking if a snapshot should be generated), true: generates the backup without asking. false: disables the backup for this operation
type: rsync # defines the Timeshift backup mode -> 'rsync' (default) or 'btrfs'
+ remove_method: self # define which backups should be removed in the 'only_one' mode. 'self': only self generated copies. 'all': all existing backups on the disc.
boot:
load_apps: true # if the installed applications or suggestions should be loaded on the management panel after the initialization process. Default: true.
```
diff --git a/bauh/__init__.py b/bauh/__init__.py
index f9b956585..91a5a6a54 100644
--- a/bauh/__init__.py
+++ b/bauh/__init__.py
@@ -1,4 +1,4 @@
-__version__ = '0.10.0'
+__version__ = '0.10.1'
__app_name__ = 'bauh'
import os
diff --git a/bauh/api/abstract/handler.py b/bauh/api/abstract/handler.py
index ef766d429..393ede9c2 100644
--- a/bauh/api/abstract/handler.py
+++ b/bauh/api/abstract/handler.py
@@ -21,7 +21,8 @@ def request_confirmation(self, title: str, body: Optional[str], components: List
confirmation_label: str = None, deny_label: str = None, deny_button: bool = True,
window_cancel: bool = False, confirmation_button: bool = True,
min_width: Optional[int] = None,
- min_height: Optional[int] = None) -> bool:
+ min_height: Optional[int] = None,
+ max_width: Optional[int] = None) -> bool:
"""
request a user confirmation. In the current GUI implementation, it shows a popup to the user.
:param title: popup title
@@ -34,6 +35,7 @@ def request_confirmation(self, title: str, body: Optional[str], components: List
:param confirmation_button: if the confirmation button should be displayed
:param min_width: minimum width for the confirmation dialog
:param min_height: minimum height for the confirmation dialog
+ :param max_width: maximum width for the confirmation dialog
:return: if the request was confirmed by the user
"""
pass
diff --git a/bauh/commons/system.py b/bauh/commons/system.py
index f9513b54f..795d85a0d 100644
--- a/bauh/commons/system.py
+++ b/bauh/commons/system.py
@@ -1,10 +1,11 @@
import os
+import re
import subprocess
import sys
import time
from io import StringIO
from subprocess import PIPE
-from typing import List, Tuple, Set, Dict, Optional, Iterable
+from typing import List, Tuple, Set, Dict, Optional, Iterable, Union, IO, Any
# default environment variables for subprocesses.
from bauh.api.abstract.handler import ProcessWatcher
@@ -13,7 +14,7 @@
GLOBAL_PY_LIBS = '/usr/lib/python{}'.format(PY_VERSION)
PATH = os.getenv('PATH')
-DEFAULT_LANG = 'en'
+DEFAULT_LANG = ''
GLOBAL_INTERPRETER_PATH = ':'.join(PATH.split(':')[1:])
@@ -22,8 +23,10 @@
USE_GLOBAL_INTERPRETER = bool(os.getenv('VIRTUAL_ENV'))
+RE_SUDO_OUTPUT = re.compile(r'[sudo]\s*[\w\s]+:\s*')
-def gen_env(global_interpreter: bool, lang: str = DEFAULT_LANG, extra_paths: Optional[Set[str]] = None) -> dict:
+
+def gen_env(global_interpreter: bool, lang: Optional[str] = DEFAULT_LANG, extra_paths: Optional[Set[str]] = None) -> dict:
custom_env = dict(os.environ)
if lang is not None:
@@ -63,7 +66,7 @@ def wait(self):
class SimpleProcess:
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,
+ global_interpreter: bool = USE_GLOBAL_INTERPRETER, lang: Optional[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,
custom_user: Optional[str] = None):
@@ -79,16 +82,17 @@ def __init__(self, cmd: Iterable[str], cwd: str = '.', expected_code: int = 0,
final_cmd.extend(cmd)
- self.instance = self._new(final_cmd, cwd, global_interpreter, lang, stdin=pwdin, extra_paths=extra_paths, extra_env=extra_env)
+ self.instance = self._new(final_cmd, cwd, global_interpreter, lang=lang, stdin=pwdin,
+ extra_paths=extra_paths, extra_env=extra_env)
self.expected_code = expected_code
self.error_phrases = error_phrases
self.wrong_error_phrases = wrong_error_phrases
self.success_phrases = success_phrases
- def _new(self, cmd: List[str], cwd: str, global_interpreter: bool, lang: str, stdin = None,
+ def _new(self, cmd: List[str], cwd: str, global_interpreter: bool, lang: Optional[str], stdin = None,
extra_paths: Set[str] = None, extra_env: Optional[Dict[str, str]] = None) -> subprocess.Popen:
- env = gen_env(global_interpreter, lang, extra_paths=extra_paths)
+ env = gen_env(global_interpreter=global_interpreter, lang=lang, extra_paths=extra_paths)
if extra_env:
for var, val in extra_env.items():
@@ -197,8 +201,8 @@ def handle_simple(self, proc: SimpleProcess, output_handler=None, notify_watcher
except UnicodeDecodeError:
continue
- if line.startswith('[sudo] password'):
- continue
+ if line.startswith('[sudo]'):
+ line = RE_SUDO_OUTPUT.split(line)[1]
output.write(line)
@@ -240,7 +244,7 @@ def handle_simple(self, proc: SimpleProcess, output_handler=None, notify_watcher
def run_cmd(cmd: str, expected_code: int = 0, ignore_return_code: bool = False, print_error: bool = True,
cwd: str = '.', global_interpreter: bool = USE_GLOBAL_INTERPRETER, extra_paths: Set[str] = None,
- custom_user: Optional[str] = None) -> Optional[str]:
+ custom_user: Optional[str] = None, lang: Optional[str] = DEFAULT_LANG) -> Optional[str]:
"""
runs a given command and returns its default output
:return:
@@ -248,7 +252,7 @@ def run_cmd(cmd: str, expected_code: int = 0, ignore_return_code: bool = False,
args = {
"shell": True,
"stdout": PIPE,
- "env": gen_env(global_interpreter, extra_paths=extra_paths),
+ "env": gen_env(global_interpreter=global_interpreter, lang=lang, extra_paths=extra_paths),
'cwd': cwd
}
@@ -265,8 +269,8 @@ def run_cmd(cmd: str, expected_code: int = 0, ignore_return_code: bool = False,
pass
-def new_subprocess(cmd: List[str], cwd: str = '.', shell: bool = False, stdin = None,
- global_interpreter: bool = USE_GLOBAL_INTERPRETER, lang: str = DEFAULT_LANG,
+def new_subprocess(cmd: Iterable[str], cwd: str = '.', shell: bool = False, stdin: Optional[Union[None, int, IO[Any]]] = None,
+ global_interpreter: bool = USE_GLOBAL_INTERPRETER, lang: Optional[str] = DEFAULT_LANG,
extra_paths: Set[str] = None, custom_user: Optional[str] = None) -> subprocess.Popen:
args = {
"stdout": PIPE,
@@ -281,10 +285,10 @@ def new_subprocess(cmd: List[str], cwd: str = '.', shell: bool = False, stdin =
return subprocess.Popen(final_cmd, **args)
-def new_root_subprocess(cmd: List[str], root_password: Optional[str], cwd: str = '.',
+def new_root_subprocess(cmd: Iterable[str], root_password: Optional[str], cwd: str = '.',
global_interpreter: bool = USE_GLOBAL_INTERPRETER, lang: str = DEFAULT_LANG,
- extra_paths: Set[str] = None) -> subprocess.Popen:
- pwdin, final_cmd = None, []
+ extra_paths: Set[str] = None, shell: bool = False) -> subprocess.Popen:
+ pwdin, final_cmd = subprocess.DEVNULL, []
if isinstance(root_password, str):
final_cmd.extend(['sudo', '-S'])
@@ -292,7 +296,11 @@ def new_root_subprocess(cmd: List[str], root_password: Optional[str], cwd: str =
final_cmd.extend(cmd)
- return subprocess.Popen(final_cmd, stdin=pwdin, stdout=PIPE, stderr=PIPE, cwd=cwd, env=gen_env(global_interpreter, lang, extra_paths))
+ if shell:
+ final_cmd = ' '.join(final_cmd)
+
+ return subprocess.Popen(final_cmd, stdin=pwdin, stdout=PIPE, stderr=PIPE, cwd=cwd,
+ env=gen_env(global_interpreter, lang, extra_paths), shell=shell)
def notify_user(msg: str, app_name: str, icon_path: str):
diff --git a/bauh/commons/view_utils.py b/bauh/commons/view_utils.py
index 1ade21ae2..3d0791f0c 100644
--- a/bauh/commons/view_utils.py
+++ b/bauh/commons/view_utils.py
@@ -1,13 +1,12 @@
-from typing import List, Tuple, Optional
+from typing import Tuple, Optional, Iterable
from bauh.api.abstract.view import SelectViewType, InputOption, SingleSelectComponent
-
SIZE_UNITS = ((1, 'B'), (1024, 'Kb'), (1048576, 'Mb'), (1073741824, 'Gb'),
(1099511627776, 'Tb'), (1125899906842624, 'Pb'))
-def new_select(label: str, tip: str, id_: str, opts: List[Tuple[Optional[str], object, Optional[str]]], value: object, max_width: int,
+def new_select(label: str, tip: Optional[str], id_: str, opts: Iterable[Tuple[Optional[str], object, Optional[str]]], value: object, max_width: int,
type_: SelectViewType = SelectViewType.RADIO, capitalize_label: bool = True):
inp_opts = [InputOption(label=o[0].capitalize(), value=o[1], tooltip=o[2]) for o in opts]
def_opt = [o for o in inp_opts if o.value == value]
diff --git a/bauh/gems/appimage/controller.py b/bauh/gems/appimage/controller.py
index aa946ca34..7fe48172e 100644
--- a/bauh/gems/appimage/controller.py
+++ b/bauh/gems/appimage/controller.py
@@ -85,27 +85,31 @@ def __init__(self, context: ApplicationContext):
self._search_unfilled_attrs: Optional[Tuple[str, ...]] = None
def install_file(self, root_password: Optional[str], watcher: ProcessWatcher) -> bool:
+ max_width = 350
file_chooser = FileChooserComponent(label=self.i18n['file'].capitalize(),
allowed_extensions={'AppImage', '*'},
- search_path=get_default_manual_installation_file_dir())
- input_name = TextInputComponent(label=self.i18n['name'].capitalize())
- input_version = TextInputComponent(label=self.i18n['version'].capitalize())
+ search_path=get_default_manual_installation_file_dir(),
+ max_width=max_width)
+ input_name = TextInputComponent(label=self.i18n['name'].capitalize(), max_width=max_width)
+ input_version = TextInputComponent(label=self.i18n['version'].capitalize(), max_width=max_width)
file_chooser.observers.append(ManualInstallationFileObserver(input_name, input_version))
- input_description = TextInputComponent(label=self.i18n['description'].capitalize())
+ input_description = TextInputComponent(label=self.i18n['description'].capitalize(), max_width=max_width)
cat_ops = [InputOption(label=self.i18n['category.none'].capitalize(), value=0)]
cat_ops.extend([InputOption(label=self.i18n.get(f'category.{c.lower()}', c.lower()).capitalize(), value=c) for c in self.context.default_categories])
inp_cat = SingleSelectComponent(label=self.i18n['category'], type_=SelectViewType.COMBO, options=cat_ops,
- default_option=cat_ops[0])
+ default_option=cat_ops[0], max_width=max_width)
- form = FormComponent(label='', components=[file_chooser, input_name, input_version, input_description, inp_cat], spaces=False)
+ form = FormComponent(label='', components=[file_chooser, input_name, input_version, input_description, inp_cat],
+ spaces=False)
while True:
if watcher.request_confirmation(title=self.i18n['appimage.custom_action.install_file.details'], body=None,
components=[form],
confirmation_label=self.i18n['proceed'].capitalize(),
- deny_label=self.i18n['cancel'].capitalize()):
+ deny_label=self.i18n['cancel'].capitalize(),
+ min_height=100, max_width=max_width + 150):
if not file_chooser.file_path or not os.path.isfile(file_chooser.file_path) or not file_chooser.file_path.lower().strip().endswith('.appimage'):
watcher.request_confirmation(title=self.i18n['error'].capitalize(),
body=self.i18n['appimage.custom_action.install_file.invalid_file'],
@@ -139,17 +143,20 @@ def install_file(self, root_password: Optional[str], watcher: ProcessWatcher) ->
return res
def update_file(self, pkg: AppImage, root_password: Optional[str], watcher: ProcessWatcher):
+ max_width = 350
file_chooser = FileChooserComponent(label=self.i18n['file'].capitalize(),
allowed_extensions={'AppImage', '*'},
- search_path=get_default_manual_installation_file_dir())
- input_version = TextInputComponent(label=self.i18n['version'].capitalize())
+ search_path=get_default_manual_installation_file_dir(),
+ max_width=max_width)
+ input_version = TextInputComponent(label=self.i18n['version'].capitalize(), max_width=max_width)
file_chooser.observers.append(ManualInstallationFileObserver(None, input_version))
while True:
if watcher.request_confirmation(title=self.i18n['appimage.custom_action.manual_update.details'], body=None,
components=[FormComponent(label='', components=[file_chooser, input_version], spaces=False)],
confirmation_label=self.i18n['proceed'].capitalize(),
- deny_label=self.i18n['cancel'].capitalize()):
+ deny_label=self.i18n['cancel'].capitalize(),
+ min_height=100, max_width=max_width + 150):
if not file_chooser.file_path or not os.path.isfile(file_chooser.file_path) or not file_chooser.file_path.lower().strip().endswith('.appimage'):
watcher.request_confirmation(title=self.i18n['error'].capitalize(),
diff --git a/bauh/gems/arch/confirmation.py b/bauh/gems/arch/confirmation.py
index b461f999f..616be095d 100644
--- a/bauh/gems/arch/confirmation.py
+++ b/bauh/gems/arch/confirmation.py
@@ -1,4 +1,4 @@
-from typing import Set, List, Tuple, Dict, Optional
+from typing import Set, Tuple, Dict, Collection
from bauh.api.abstract.handler import ProcessWatcher
from bauh.api.abstract.view import MultipleSelectComponent, InputOption, FormComponent, SingleSelectComponent, \
@@ -26,7 +26,7 @@ def request_optional_deps(pkgname: str, pkg_repos: dict, watcher: ProcessWatcher
i18n['repository'],
d['repository'].lower(),
i18n['size'].capitalize(),
- get_human_size_str(size) if size else '?'), p)
+ get_human_size_str(size) if size is not None else '?'), p)
op.icon_path = _get_repo_icon(d['repository'])
opts.append(op)
@@ -44,8 +44,8 @@ def request_optional_deps(pkgname: str, pkg_repos: dict, watcher: ProcessWatcher
return {o.value for o in view_opts.values}
-def request_install_missing_deps(pkgname: Optional[str], deps: List[Tuple[str, str]], watcher: ProcessWatcher, i18n: I18n) -> bool:
- msg = '{}
'.format(i18n['arch.missing_deps.body'].format(name=bold(pkgname) if pkgname else '', deps=bold(str(len(deps)))))
+def request_install_missing_deps(deps: Collection[Tuple[str, str]], watcher: ProcessWatcher, i18n: I18n) -> bool:
+ msg = f"{i18n['arch.missing_deps.body'].format(deps=bold(str(len(deps))))}:
"
opts = []
@@ -58,13 +58,14 @@ def request_install_missing_deps(pkgname: Optional[str], deps: List[Tuple[str, s
i18n['repository'],
dep[1].lower(),
i18n['size'].capitalize(),
- get_human_size_str(size) if size else '?'), dep[0])
+ get_human_size_str(size) if size is not None else '?'), dep[0])
op.read_only = True
op.icon_path = _get_repo_icon(dep[1])
opts.append(op)
comp = MultipleSelectComponent(label='', options=opts, default_options=set(opts))
- return watcher.request_confirmation(i18n['arch.missing_deps.title'], msg, [comp], confirmation_label=i18n['continue'].capitalize(), deny_label=i18n['cancel'].capitalize())
+ return watcher.request_confirmation(i18n['arch.missing_deps.title'], msg, [comp], confirmation_label=i18n['continue'].capitalize(), deny_label=i18n['cancel'].capitalize(),
+ min_width=600)
def request_providers(providers_map: Dict[str, Set[str]], repo_map: Dict[str, str], watcher: ProcessWatcher, i18n: I18n) -> Set[str]:
diff --git a/bauh/gems/arch/controller.py b/bauh/gems/arch/controller.py
index d638d9e88..76cfea3d3 100644
--- a/bauh/gems/arch/controller.py
+++ b/bauh/gems/arch/controller.py
@@ -635,7 +635,7 @@ def read_installed(self, disk_loader: Optional[DiskCacheLoader], limit: int = -1
if ignored:
for p in pkgs:
if p.name in ignored:
- p.updates_ignored = True
+ p.update_ignored = True
return SearchResult(pkgs, None, len(pkgs))
@@ -2075,7 +2075,7 @@ def _save_pkgbuild(self, context: TransactionContext):
def _ask_and_install_missing_deps(self, context: TransactionContext, missing_deps: List[Tuple[str, str]]) -> bool:
context.watcher.change_substatus(self.i18n['arch.missing_deps_found'].format(bold(context.name)))
- if not confirmation.request_install_missing_deps(context.name, missing_deps, context.watcher, self.i18n):
+ if not confirmation.request_install_missing_deps(missing_deps, context.watcher, self.i18n):
context.watcher.print(self.i18n['action.cancelled'])
return False
@@ -2265,7 +2265,8 @@ def _install_optdeps(self, context: TransactionContext) -> bool:
sorted_deps = sorting.sort(to_sort, {**deps_data, **subdeps_data}, provided_map)
- if display_deps_dialog and not confirmation.request_install_missing_deps(None, sorted_deps, context.watcher, self.i18n):
+ if display_deps_dialog and not confirmation.request_install_missing_deps(sorted_deps, context.watcher,
+ self.i18n):
context.watcher.print(self.i18n['action.cancelled'])
return True # because the main package installation was successful
diff --git a/bauh/gems/arch/pacman.py b/bauh/gems/arch/pacman.py
index f7382f4ac..24200df7d 100644
--- a/bauh/gems/arch/pacman.py
+++ b/bauh/gems/arch/pacman.py
@@ -55,10 +55,6 @@ def get_repositories(pkgs: Iterable[str]) -> dict:
return repositories
-def is_available_in_repositories(pkg_name: str) -> bool:
- return bool(run_cmd('pacman -Ss ' + pkg_name))
-
-
def get_info(pkg_name, remote: bool = False) -> str:
return run_cmd('pacman -{}i {}'.format('Q' if not remote else 'S', pkg_name), print_error=False)
@@ -397,32 +393,6 @@ def list_repository_updates() -> Dict[str, str]:
return res
-def map_sorting_data(pkgnames: List[str]) -> Dict[str, dict]:
- allinfo = new_subprocess(['pacman', '-Qi', *pkgnames]).stdout
-
- pkgs, current_pkg = {}, {}
- mapped_attrs = 0
- for out in new_subprocess(["grep", "-Po", "(Name|Provides|Depends On)\s*:\s*\K(.+)"], stdin=allinfo).stdout:
- if out:
- line = out.decode().strip()
-
- if line:
- if mapped_attrs == 0:
- current_pkg['name'] = line
- elif mapped_attrs == 1:
- provides = set() if line == 'None' else set(line.split(' '))
- provides.add(current_pkg['name'])
- current_pkg['provides'] = provides
- elif mapped_attrs == 2:
- current_pkg['depends'] = line.split(':')[1].strip()
- pkgs[current_pkg['name']] = current_pkg
- del current_pkg['name']
-
- mapped_attrs = 0
- current_pkg = {}
- return pkgs
-
-
def get_build_date(pkgname: str) -> str:
output = run_cmd('pacman -Qi {}'.format(pkgname))
@@ -508,7 +478,7 @@ def map_update_sizes(pkgs: List[str]) -> Dict[str, int]: # bytes:
output = run_cmd('pacman -Si {}'.format(' '.join(pkgs)))
if output:
- return {pkgs[idx]: size_to_byte(float(size[0]), size[1]) for idx, size in enumerate(RE_INSTALLED_SIZE.findall(output))}
+ return {pkgs[idx]: size_to_byte(float(size[0].replace(',', '.')), size[1]) for idx, size in enumerate(RE_INSTALLED_SIZE.findall(output))}
return {}
@@ -517,7 +487,7 @@ def map_download_sizes(pkgs: List[str]) -> Dict[str, int]: # bytes:
output = run_cmd('pacman -Si {}'.format(' '.join(pkgs)))
if output:
- return {pkgs[idx]: size_to_byte(float(size[0]), size[1]) for idx, size in enumerate(RE_DOWNLOAD_SIZE.findall(output))}
+ return {pkgs[idx]: size_to_byte(float(size[0].replace(',', '.')), size[1]) for idx, size in enumerate(RE_DOWNLOAD_SIZE.findall(output))}
return {}
@@ -526,7 +496,7 @@ def get_installed_size(pkgs: List[str]) -> Dict[str, int]: # bytes
output = run_cmd('pacman -Qi {}'.format(' '.join(pkgs)))
if output:
- return {pkgs[idx]: size_to_byte(float(size[0]), size[1]) for idx, size in enumerate(RE_INSTALLED_SIZE.findall(output))}
+ return {pkgs[idx]: size_to_byte(float(size[0].replace(',', '.')), size[1]) for idx, size in enumerate(RE_INSTALLED_SIZE.findall(output))}
return {}
@@ -688,11 +658,11 @@ def map_updates_data(pkgs: Iterable[str], files: bool = False) -> dict:
latest_field = 'c'
elif field == 'Download Size':
size = val.split(' ')
- data['ds'] = size_to_byte(float(size[0]), size[1])
+ data['ds'] = size_to_byte(float(size[0].replace(',', '.')), size[1])
latest_field = 'ds'
elif field == 'Installed Size':
size = val.split(' ')
- data['s'] = size_to_byte(float(size[0]), size[1])
+ data['s'] = size_to_byte(float(size[0].replace(',', '.')), size[1])
latest_field = 's'
elif latest_name and latest_field == 's':
res[latest_name] = data
@@ -801,67 +771,6 @@ def map_optional_deps(names: Iterable[str], remote: bool, not_installed: bool =
return res
-def map_all_deps(names: Iterable[str], only_installed: bool = False) -> Dict[str, Set[str]]:
- output = run_cmd('pacman -Qi {}'.format(' '.join(names)))
-
- if output:
- res = {}
- deps_fields = {'Depends On', 'Optional Deps'}
- latest_name, deps, latest_field = None, None, None
-
- for l in output.split('\n'):
- if l:
- if l[0] != ' ':
- line = l.strip()
- field_sep_idx = line.index(':')
- field = line[0:field_sep_idx].strip()
-
- if field == 'Name':
- latest_field = field
- val = line[field_sep_idx + 1:].strip()
- latest_name = val
- deps = None
- elif field in deps_fields:
- latest_field = field
- val = line[field_sep_idx + 1:].strip()
- opt_deps = latest_field == 'Optional Deps'
-
- if deps is None:
- deps = set()
-
- if val != 'None':
- if ':' in val:
- dep_info = val.split(':')
- desc = dep_info[1].strip()
-
- if desc and opt_deps and only_installed and '[installed]' not in desc:
- continue
-
- deps.add(dep_info[0].strip())
- else:
- deps.update({dep.strip() for dep in val.split(' ') if dep})
-
- elif latest_name and deps is not None:
- res[latest_name] = deps
- latest_name, deps, latest_field = None, None, None
-
- elif latest_name and deps is not None:
- opt_deps = latest_field == 'Optional Deps'
-
- if ':' in l:
- dep_info = l.split(':')
- desc = dep_info[1].strip()
-
- if desc and opt_deps and only_installed and '[installed]' not in desc:
- continue
-
- deps.add(dep_info[0].strip())
- else:
- deps.update({dep.strip() for dep in l.split(' ') if dep})
-
- return res
-
-
def map_required_dependencies(*names: str) -> Dict[str, Set[str]]:
output = run_cmd('pacman -Qi {}'.format(' '.join(names) if names else ''))
@@ -1035,114 +944,6 @@ def map_replaces(names: Iterable[str], remote: bool = False) -> Dict[str, Set[st
return res
-def _list_unnecessary_deps(pkgs: Iterable[str], already_checked: Set[str], all_provided: Dict[str, Set[str]], recursive: bool = False) -> Set[str]:
- output = run_cmd('pacman -Qi {}'.format(' '.join(pkgs)))
-
- if output:
- res = set()
- deps_field = False
-
- for l in output.split('\n'):
- if l:
- if l[0] != ' ':
- line = l.strip()
- field_sep_idx = line.index(':')
- field = line[0:field_sep_idx].strip()
-
- if field == 'Depends On':
- deps_field = True
- val = line[field_sep_idx + 1:].strip()
-
- if val != 'None':
- if ':' in val:
- dep_info = val.split(':')
-
- real_deps = all_provided.get(dep_info[0].strip())
-
- if real_deps:
- res.update(real_deps)
- else:
- for dep in val.split(' '):
- if dep:
- real_deps = all_provided.get(dep.strip())
-
- if real_deps:
- res.update(real_deps)
-
- elif deps_field:
- latest_field = False
-
- elif deps_field:
- if ':' in l:
- dep_info = l.split(':')
-
- real_deps = all_provided.get(dep_info[0].strip())
-
- if real_deps:
- res.update(real_deps)
- else:
- for dep in l.split(' '):
- if dep:
- real_deps = all_provided.get(dep.strip())
-
- if real_deps:
- res.update(real_deps)
-
- if res:
- res = {dep for dep in res if dep not in already_checked}
- already_checked.update(res)
-
- if recursive and res:
- subdeps = _list_unnecessary_deps(res, already_checked, all_provided)
-
- if subdeps:
- res.update(subdeps)
-
- return res
-
-
-def list_unnecessary_deps(pkgs: Iterable[str], all_provided: Dict[str, Set[str]] = None) -> Set[str]:
- all_checked = set(pkgs)
- all_deps = _list_unnecessary_deps(pkgs, all_checked, map_provided(remote=False) if not all_provided else all_provided, recursive=True)
-
- unnecessary = set(pkgs)
- if all_deps:
- requirements_map = map_required_by(all_deps)
-
- to_clean = set()
- for dep, required_by in requirements_map.items():
- if not required_by or not required_by.difference(unnecessary):
- unnecessary.add(dep)
- to_clean.add(dep)
- elif required_by.difference(all_checked): # checking if there are requirements outside the context
- to_clean.add(dep)
-
- if to_clean:
- for dep in to_clean:
- del requirements_map[dep]
-
- if requirements_map:
- while True:
- to_clean = set()
- for dep, required_by in requirements_map.items():
- if not required_by.difference(unnecessary):
- unnecessary.add(dep)
- to_clean.add(dep)
-
- if to_clean:
- for dep in to_clean:
- del requirements_map[dep]
- else:
- break
-
- if requirements_map: # if it reaches this points it is possible to exist mutual dependent packages
- for dep, required_by in requirements_map.items():
- if not required_by.difference({*requirements_map.keys(), *unnecessary}):
- unnecessary.add(dep)
-
- return unnecessary.difference(pkgs)
-
-
def list_installed_names() -> Set[str]:
output = run_cmd('pacman -Qq', print_error=False)
return {name.strip() for name in output.split('\n') if name} if output else set()
diff --git a/bauh/gems/arch/resources/locale/ca b/bauh/gems/arch/resources/locale/ca
index 4a90cbb26..c551cb668 100644
--- a/bauh/gems/arch/resources/locale/ca
+++ b/bauh/gems/arch/resources/locale/ca
@@ -230,7 +230,7 @@ arch.install.optdeps.request.title=Dependències opcionals
arch.installing.package=S’està instal·lant el paquet {}
arch.checking_unnecessary_deps=Checking if there are packages no longer needed
arch.makepkg.optimizing=Optimitzant la recopilació
-arch.missing_deps.body=S’han d’instal·lar les {deps} dependències següents abans de continuar amb la instal·lació de {name}
+arch.missing_deps.body=The following dependencies ({deps}) will be installed
arch.missing_deps.title=Dependències mancants
arch.missing_deps_found=Dependències mancants per a {}
arch.mthread_downloaded.error.cache_dir=It was not possible to create the cache directory {}
diff --git a/bauh/gems/arch/resources/locale/de b/bauh/gems/arch/resources/locale/de
index 37e579bd7..19ada1a5a 100644
--- a/bauh/gems/arch/resources/locale/de
+++ b/bauh/gems/arch/resources/locale/de
@@ -230,7 +230,7 @@ arch.install.optdeps.request.title=Optionale Abhängigkeiten
arch.installing.package=Paket {} installieren
arch.checking_unnecessary_deps=Checking if there are packages no longer needed
arch.makepkg.optimizing=Optimiert die Zusammenstellung
-arch.missing_deps.body=Die folgenden {deps} Abhängigkeiten müssten installiert sein, bevor mit der {name} Installation fortgefahren werden kann
+arch.missing_deps.body=The following dependencies ({deps}) will be installed
arch.missing_deps.title=Fehlende Abhängigkeiten
arch.missing_deps_found=Fehlende Abhängigkeiten für {}
arch.mthread_downloaded.error.cache_dir=It was not possible to create the cache directory {}
diff --git a/bauh/gems/arch/resources/locale/en b/bauh/gems/arch/resources/locale/en
index 9a139db8f..0a7ad9c6b 100644
--- a/bauh/gems/arch/resources/locale/en
+++ b/bauh/gems/arch/resources/locale/en
@@ -230,7 +230,7 @@ arch.install.optdeps.request.title=Optional dependencies
arch.installing.package=Installing {} package
arch.checking_unnecessary_deps=Checking if there are packages no longer needed
arch.makepkg.optimizing=Optimizing the compilation
-arch.missing_deps.body=The following {deps} dependencies must be installed so the {name} installation can continue
+arch.missing_deps.body=The following dependencies ({deps}) will be installed
arch.missing_deps.title=Missing dependencies
arch.missing_deps_found=Missing dependencies for {}
arch.mthread_downloaded.error.cache_dir=It was not possible to create the cache directory {}
diff --git a/bauh/gems/arch/resources/locale/es b/bauh/gems/arch/resources/locale/es
index 7b794f1ac..9035ab68c 100644
--- a/bauh/gems/arch/resources/locale/es
+++ b/bauh/gems/arch/resources/locale/es
@@ -230,7 +230,7 @@ arch.install.optdeps.request.title=Dependencias opcionales
arch.installing.package=Instalando el paquete {}
arch.checking_unnecessary_deps=Verificando se hay paquetes innecesarios
arch.makepkg.optimizing=Optimizing the compilation
-arch.missing_deps.body=Deben instalarse las siguientes {deps} dependencias para que la instalación de {name} pueda continuar
+arch.missing_deps.body=Las siguientes dependencias ({deps}) serán instaladas
arch.missing_deps.title=Dependencias faltantes
arch.missing_deps_found=Dependencias faltantes para {}
arch.mthread_downloaded.error.cache_dir=No fue posible crear el directorio de caché {}
diff --git a/bauh/gems/arch/resources/locale/fr b/bauh/gems/arch/resources/locale/fr
index e76087336..ebdbd0091 100644
--- a/bauh/gems/arch/resources/locale/fr
+++ b/bauh/gems/arch/resources/locale/fr
@@ -230,7 +230,7 @@ arch.install.optdeps.request.title=Dépendances optionnelles
arch.installing.package=Installation du paquet {}
arch.checking_unnecessary_deps=Calcul des paquets devenus inutiles
arch.makepkg.optimizing=Optimisation de la compilation
-arch.missing_deps.body=Les dependances {deps} doivent être installées pour que l'installation de {name} se poursuive
+arch.missing_deps.body=The following dependencies ({deps}) will be installed
arch.missing_deps.title=Dépendances manquantes
arch.missing_deps_found=Dépendances manquantes pour {}
arch.mthread_downloaded.error.cache_dir=Impossible de créer le dossier de cache {}
diff --git a/bauh/gems/arch/resources/locale/it b/bauh/gems/arch/resources/locale/it
index f842df16b..1d4fd5a0a 100644
--- a/bauh/gems/arch/resources/locale/it
+++ b/bauh/gems/arch/resources/locale/it
@@ -230,7 +230,7 @@ arch.install.optdeps.request.title=Dipendenze opzionali
arch.installing.package=Installazione del pacchetto {}
arch.checking_unnecessary_deps=Checking if there are packages no longer needed
arch.makepkg.optimizing=Ottimizzando la compilazione
-arch.missing_deps.body=Le seguenti {deps} dipendenze devono essere installate prima che l'installazione di {name} continui
+arch.missing_deps.body=The following dependencies ({deps}) will be installed
arch.missing_deps.title=Dipendenze mancanti
arch.missing_deps_found=Dipendenze mancanti per {}
arch.mthread_downloaded.error.cache_dir=It was not possible to create the cache directory {}
diff --git a/bauh/gems/arch/resources/locale/pt b/bauh/gems/arch/resources/locale/pt
index 434eb9f94..899415e21 100644
--- a/bauh/gems/arch/resources/locale/pt
+++ b/bauh/gems/arch/resources/locale/pt
@@ -229,7 +229,7 @@ arch.install.optdeps.request.title=Dependências opcionais
arch.installing.package=Instalando o pacote {}
arch.checking_unnecessary_deps=Verificando se há pacotes não mais necessários
arch.makepkg.optimizing=Otimizando a compilação
-arch.missing_deps.body=As seguintes {deps} dependências devem ser instaladas para que a instalação de {name} continue
+arch.missing_deps.body=As seguintes dependências ({deps}) serão instaladas
arch.missing_deps.title=Dependências ausentes
arch.missing_deps_found=Dependencias ausentes para {}
arch.mthread_downloaded.error.cache_dir=Não foi possível criar o diretório para cache {}
diff --git a/bauh/gems/arch/resources/locale/ru b/bauh/gems/arch/resources/locale/ru
index a3f4c91ec..e755aa405 100644
--- a/bauh/gems/arch/resources/locale/ru
+++ b/bauh/gems/arch/resources/locale/ru
@@ -230,7 +230,7 @@ arch.install.optdeps.request.title=Необязательные зависимо
arch.installing.package=Установка пакета {}
arch.checking_unnecessary_deps=Checking if there are packages no longer needed
arch.makepkg.optimizing=Оптимизация компиляции
-arch.missing_deps.body=Необходимо установить следующие зависимости , чтобы продолжить установку {name}
+arch.missing_deps.body=The following dependencies ({deps}) will be installed
arch.missing_deps.title=Отсутствующие зависимости
arch.missing_deps_found=Отсутствуют зависимости для {}
arch.mthread_downloaded.error.cache_dir=It was not possible to create the cache directory {}
diff --git a/bauh/gems/arch/resources/locale/tr b/bauh/gems/arch/resources/locale/tr
index 116b6aa53..8d4731bf4 100644
--- a/bauh/gems/arch/resources/locale/tr
+++ b/bauh/gems/arch/resources/locale/tr
@@ -230,7 +230,7 @@ arch.install.optdeps.request.title=İsteğe bağlı bağımlılıklar
arch.installing.package={} Paketi yükleniyor
arch.checking_unnecessary_deps=Artık gerekli olmayan paketler olup olmadığını kontrol et
arch.makepkg.optimizing=Derlemeyi optimize et
-arch.missing_deps.body={name} kurulumunun devam edebilmesi için aşağıdaki {deps} bağımlılık kurulmalıdır
+arch.missing_deps.body=The following dependencies ({deps}) will be installed
arch.missing_deps.title=Eksik bağımlılıklar
arch.missing_deps_found={} için eksik bağımlılıklar
arch.mthread_downloaded.error.cache_dir=It was not possible to create the cache directory {}
diff --git a/bauh/gems/arch/updates.py b/bauh/gems/arch/updates.py
index 888e38338..e4c558441 100644
--- a/bauh/gems/arch/updates.py
+++ b/bauh/gems/arch/updates.py
@@ -335,7 +335,7 @@ def _map_requirement(self, pkg: ArchPackage, context: UpdateRequirementsContext,
current_size = installed_sizes.get(pkg.name) if installed_sizes else None
- if current_size is not None and pkgdata['s']:
+ if current_size is not None and pkgdata['s'] is not None:
requirement.extra_size = pkgdata['s'] - current_size
required_by = set()
diff --git a/bauh/gems/debian/aptitude.py b/bauh/gems/debian/aptitude.py
index 76b82cf8c..3a7d18e31 100644
--- a/bauh/gems/debian/aptitude.py
+++ b/bauh/gems/debian/aptitude.py
@@ -46,6 +46,7 @@ def __init__(self, logger: Logger):
self._size_attrs: Optional[Tuple[str]] = None
self._default_lang = ''
self._ignored_fields: Optional[Set[str]] = None
+ self._re_none: Optional[Pattern] = None
def show(self, pkgs: Iterable[str], attrs: Optional[Collection[str]] = None, verbose: bool = False) \
-> Optional[Dict[str, Dict[str, object]]]:
@@ -135,11 +136,10 @@ def simulate_upgrade(self, packages: Iterable[str]) -> DebianTransaction:
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)
+ return SimpleProcess(cmd=cmd, shell=True, 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)
+ return SimpleProcess(('aptitude', 'update'), root_password=root_password, shell=True)
def simulate_installation(self, packages: Iterable[str]) -> Optional[DebianTransaction]:
code, output = system.execute(self.gen_transaction_cmd('install', packages, simulate=True),
@@ -150,7 +150,7 @@ def simulate_installation(self, packages: Iterable[str]) -> Optional[DebianTrans
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)
+ return SimpleProcess(cmd=cmd, shell=True, root_password=root_password)
def read_installed(self) -> Generator[DebianPackage, None, None]:
yield from self.search(query='~i')
@@ -169,9 +169,7 @@ def read_updates(self) -> Generator[Tuple[str, str], None, None]:
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)
+ _, output = system.execute(f"aptitude search {query} -q -F '{attrs}' --disable-columns", shell=True)
if output:
no_attrs = 7 if fill_size else 6
@@ -180,7 +178,7 @@ def search(self, query: str, fill_size: bool = False) -> Generator[DebianPackage
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
+ latest_version = line_split[2] if not self.re_none.match(line_split[2]) else None
size = None
@@ -195,7 +193,7 @@ def search(self, query: str, fill_size: bool = False) -> Generator[DebianPackage
traceback.print_exc()
if latest_version is not None:
- installed_version = line_split[1] if line_split[1] != '' else None
+ installed_version = line_split[1] if not self.re_none.match(line_split[1]) else None
section = strip_section(line_split[4])
yield DebianPackage(name=line_split[0],
@@ -214,7 +212,7 @@ def search_by_name(self, names: Iterable[str], fill_size: bool = False) -> Gener
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)
+ 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",
@@ -236,8 +234,7 @@ def re_show_attr(self) -> Pattern:
@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)
+ self._env = system.gen_env(global_interpreter=system.USE_GLOBAL_INTERPRETER)
return self._env
@@ -282,6 +279,13 @@ def gen_transaction_cmd(type_: str, packages: Iterable[str], simulate: bool = Fa
f" -o Aptitude::ProblemResolver::EssentialRemoveScore=9999999" \
f"{' -V -s -Z' if simulate else ''}"
+ @property
+ def re_none(self) -> Pattern:
+ if self._re_none is None:
+ self._re_none = re.compile(r'^<\w+>$')
+
+ return self._re_none
+
class AptitudeOutputHandler(Thread):
diff --git a/bauh/gems/flatpak/__init__.py b/bauh/gems/flatpak/__init__.py
index 0f6ec4c3a..a775678ba 100644
--- a/bauh/gems/flatpak/__init__.py
+++ b/bauh/gems/flatpak/__init__.py
@@ -18,6 +18,7 @@
VERSION_1_3 = parse_version('1.3')
VERSION_1_4 = parse_version('1.4')
VERSION_1_5 = parse_version('1.5')
+VERSION_1_12 = parse_version('1.12')
def get_icon_path() -> str:
diff --git a/bauh/gems/flatpak/controller.py b/bauh/gems/flatpak/controller.py
index 3a11f1a57..eae5c8d5a 100644
--- a/bauh/gems/flatpak/controller.py
+++ b/bauh/gems/flatpak/controller.py
@@ -15,7 +15,7 @@
from bauh.api.abstract.disk import DiskCacheLoader
from bauh.api.abstract.handler import ProcessWatcher, TaskManager
from bauh.api.abstract.model import PackageHistory, PackageUpdate, SoftwarePackage, PackageSuggestion, \
- SuggestionPriority, PackageStatus
+ SuggestionPriority, PackageStatus, CustomSoftwareAction
from bauh.api.abstract.view import MessageType, FormComponent, SingleSelectComponent, InputOption, SelectViewType, \
ViewComponent, PanelComponent
from bauh.commons.boot import CreateConfigFile
@@ -46,6 +46,7 @@ def __init__(self, context: ApplicationContext):
self.suggestions_cache = context.cache_factory.new(None)
self.logger = context.logger
self.configman = FlatpakConfigManager()
+ self._action_full_update: Optional[CustomSoftwareAction] = None
def get_managed_types(self) -> Set["type"]:
return {FlatpakApplication}
@@ -224,10 +225,11 @@ def downgrade(self, pkg: FlatpakApplication, root_password: Optional[str], watch
commit = history.history[history.pkg_status_idx + 1]['commit']
watcher.change_substatus(self.i18n['flatpak.downgrade.reverting'])
watcher.change_progress(50)
- success, _ = ProcessHandler(watcher).handle_simple(flatpak.downgrade(pkg.ref,
- commit,
- pkg.installation,
- root_password))
+ success, _ = ProcessHandler(watcher).handle_simple(flatpak.downgrade(app_ref=pkg.ref,
+ commit=commit,
+ installation=pkg.installation,
+ root_password=root_password,
+ version=flatpak.get_version()))
watcher.change_progress(100)
return success
@@ -254,13 +256,15 @@ def upgrade(self, requirements: UpgradeRequirements, root_password: Optional[str
if req.pkg.update_component:
res, _ = ProcessHandler(watcher).handle_simple(flatpak.install(app_id=ref,
installation=req.pkg.installation,
- origin=req.pkg.origin))
+ origin=req.pkg.origin,
+ version=flatpak_version))
else:
res, _ = ProcessHandler(watcher).handle_simple(flatpak.update(app_ref=ref,
installation=req.pkg.installation,
related=related,
- deps=deps))
+ deps=deps,
+ version=flatpak_version))
watcher.change_substatus('')
if not res:
@@ -280,7 +284,9 @@ def uninstall(self, pkg: FlatpakApplication, root_password: Optional[str], watch
if not self._make_exports_dir(watcher):
return TransactionResult.fail()
- uninstalled, _ = ProcessHandler(watcher).handle_simple(flatpak.uninstall(pkg.ref, pkg.installation))
+ flatpak_version = flatpak.get_version()
+ uninstalled, _ = ProcessHandler(watcher).handle_simple(flatpak.uninstall(pkg.ref, pkg.installation,
+ flatpak_version))
if uninstalled:
if self.suggestions_cache:
@@ -445,7 +451,8 @@ def install(self, pkg: FlatpakApplication, root_password: Optional[str], disk_lo
if not self._make_exports_dir(handler.watcher):
return TransactionResult(success=False, installed=[], removed=[])
- installed, output = handler.handle_simple(flatpak.install(str(pkg.id), pkg.origin, pkg.installation))
+ installed, output = handler.handle_simple(flatpak.install(str(pkg.id), pkg.origin, pkg.installation,
+ flatpak_version))
if not installed and 'error: No ref chosen to resolve matches' in output:
ref_opts = RE_INSTALL_REFS.findall(output)
@@ -459,7 +466,8 @@ def install(self, pkg: FlatpakApplication, root_password: Optional[str], disk_lo
confirmation_label=self.i18n['proceed'].capitalize(),
deny_label=self.i18n['cancel'].capitalize()):
ref = ref_select.get_selected()
- installed, output = handler.handle_simple(flatpak.install(ref, pkg.origin, pkg.installation))
+ installed, output = handler.handle_simple(flatpak.install(ref, pkg.origin, pkg.installation,
+ flatpak_version))
pkg.ref = ref
pkg.runtime = 'runtime' in ref
else:
@@ -727,3 +735,25 @@ def revert_ignored_update(self, pkg: FlatpakApplication):
self._write_ignored_updates(ignored_keys)
pkg.updates_ignored = False
+
+ def gen_custom_actions(self) -> Generator[CustomSoftwareAction, None, None]:
+ yield self.action_full_update
+
+ def full_update(self, root_password: Optional[str], watcher: ProcessWatcher) -> bool:
+ handler = ProcessHandler(watcher)
+ return handler.handle_simple(flatpak.full_update(flatpak.get_version()))[0]
+
+ @property
+ def action_full_update(self) -> CustomSoftwareAction:
+ if self._action_full_update is None:
+ self._action_full_update = CustomSoftwareAction(i18n_label_key='flatpak.action.full_update',
+ i18n_description_key='flatpak.action.full_update.description',
+ i18n_status_key='flatpak.action.full_update.status',
+ backup=True,
+ manager=self,
+ requires_internet=True,
+ icon_path=get_icon_path(),
+ manager_method='full_update',
+ requires_root=False)
+
+ return self._action_full_update
diff --git a/bauh/gems/flatpak/flatpak.py b/bauh/gems/flatpak/flatpak.py
index 1882e7f95..bd2741387 100755
--- a/bauh/gems/flatpak/flatpak.py
+++ b/bauh/gems/flatpak/flatpak.py
@@ -9,9 +9,10 @@
from packaging.version import parse as parse_version
from bauh.api.exception import NoInternetException
-from bauh.commons.system import new_subprocess, run_cmd, SimpleProcess, ProcessHandler
+from bauh.commons.system import new_subprocess, run_cmd, SimpleProcess, ProcessHandler, DEFAULT_LANG
from bauh.commons.util import size_to_byte
-from bauh.gems.flatpak import EXPORTS_PATH, VERSION_1_3, VERSION_1_2, VERSION_1_5
+from bauh.gems.flatpak import EXPORTS_PATH, VERSION_1_3, VERSION_1_2, VERSION_1_5, VERSION_1_12
+from bauh.gems.flatpak.constants import FLATHUB_URL
RE_SEVERAL_SPACES = re.compile(r'\s+')
RE_COMMIT = re.compile(r'(Latest commit|Commit)\s*:\s*(.+)')
@@ -57,7 +58,7 @@ def get_fields(app_id: str, branch: str, fields: List[str]) -> List[str]:
info = new_subprocess(cmd).stdout
res = []
- for o in new_subprocess(['grep', '-E', '({}):.+'.format('|'.join(fields)), '-o'], stdin=info).stdout:
+ for o in new_subprocess(('grep', '-E', '({}):.+'.format('|'.join(fields)), '-o'), stdin=info).stdout:
if o:
res.append(o.decode().split(':')[-1].strip())
@@ -70,20 +71,20 @@ def is_installed():
def get_version() -> Optional[Version]:
- res = run_cmd('{} --version'.format('flatpak'), print_error=False)
+ res = run_cmd('flatpak --version', print_error=False)
return parse_version(res.split(' ')[1].strip()) if res else None
-def get_app_info(app_id: str, branch: str, installation: str):
+def get_app_info(app_id: str, branch: str, installation: str) -> Optional[str]:
try:
- return run_cmd('{} info {} {}'.format('flatpak', app_id, branch, '--{}'.format(installation)))
+ return run_cmd(f'flatpak info {app_id} {branch} --{installation}')
except:
traceback.print_exc()
return ''
def get_commit(app_id: str, branch: str, installation: str) -> Optional[str]:
- info = run_cmd('flatpak info {} {} --{}'.format(app_id, branch, installation))
+ info = run_cmd(f'flatpak info {app_id} {branch} --{installation}')
if info:
commits = RE_COMMIT.findall(info)
@@ -95,7 +96,7 @@ def list_installed(version: Version) -> List[dict]:
apps = []
if version < VERSION_1_2:
- app_list = new_subprocess(['flatpak', 'list', '-d'])
+ app_list = new_subprocess(('flatpak', 'list', '-d'), lang=None)
for o in app_list.stdout:
if o:
@@ -117,8 +118,9 @@ def list_installed(version: Version) -> List[dict]:
})
else:
- cols = 'application,ref,arch,branch,description,origin,options,{}version'.format('' if version < VERSION_1_3 else 'name,')
- app_list = new_subprocess(['flatpak', 'list', '--columns=' + cols])
+ name_col = '' if version < VERSION_1_3 else 'name,'
+ cols = f'application,ref,arch,branch,description,origin,options,{name_col}version'
+ app_list = new_subprocess(('flatpak', 'list', f'--columns={cols}'), lang=None)
for o in app_list.stdout:
if o:
@@ -157,8 +159,8 @@ def list_installed(version: Version) -> List[dict]:
return apps
-def update(app_ref: str, installation: str, related: bool = False, deps: bool = False) -> SimpleProcess:
- cmd = ['flatpak', 'update', '-y', app_ref, '--{}'.format(installation)]
+def update(app_ref: str, installation: str, version: Version, related: bool = False, deps: bool = False) -> SimpleProcess:
+ cmd = ['flatpak', 'update', '-y', app_ref, f'--{installation}']
if not related:
cmd.append('--no-related')
@@ -166,12 +168,19 @@ def update(app_ref: str, installation: str, related: bool = False, deps: bool =
if not deps:
cmd.append('--no-deps')
- return SimpleProcess(cmd=cmd, extra_paths={EXPORTS_PATH}, shell=True)
+ return SimpleProcess(cmd=cmd, extra_paths={EXPORTS_PATH}, shell=True,
+ lang=DEFAULT_LANG if version < VERSION_1_12 else None)
-def uninstall(app_ref: str, installation: str) -> SimpleProcess:
- return SimpleProcess(cmd=['flatpak', 'uninstall', app_ref, '-y', '--{}'.format(installation)],
+def full_update(version: VERSION_1_12) -> SimpleProcess:
+ return SimpleProcess(cmd=('flatpak', 'update', '-y'), extra_paths={EXPORTS_PATH}, shell=True,
+ lang=DEFAULT_LANG if version < VERSION_1_12 else None)
+
+
+def uninstall(app_ref: str, installation: str, version: Version) -> SimpleProcess:
+ return SimpleProcess(cmd=('flatpak', 'uninstall', app_ref, '-y', f'--{installation}'),
extra_paths={EXPORTS_PATH},
+ lang=DEFAULT_LANG if version < VERSION_1_12 else None,
shell=True)
@@ -189,21 +198,21 @@ def read_updates(version: Version, installation: str) -> Dict[str, set]:
res = {'partial': set(), 'full': set()}
if version < VERSION_1_2:
try:
- output = run_cmd('{} update --no-related --no-deps --{}'.format('flatpak', installation), ignore_return_code=True)
+ output = run_cmd(f'flatpak update --no-related --no-deps --{installation}', ignore_return_code=True)
- if 'Updating in {}'.format(installation) in output:
- for line in output.split('Updating in {}:\n'.format(installation))[1].split('\n'):
+ if f'Updating in {installation}' in output:
+ for line in output.split(f'Updating in {installation}:\n')[1].split('\n'):
if not line.startswith('Is this ok'):
res['full'].add('{}/{}'.format(installation, line.split('\t')[0].strip()))
except:
traceback.print_exc()
else:
- updates = new_subprocess(['flatpak', 'update', '--{}'.format(installation)]).stdout
+ updates = new_subprocess(('flatpak', 'update', f'--{installation}')).stdout
reg = r'[0-9]+\.\s+.+'
try:
- for o in new_subprocess(['grep', '-E', reg, '-o', '--color=never'], stdin=updates).stdout:
+ for o in new_subprocess(('grep', '-E', reg, '-o', '--color=never'), stdin=updates).stdout:
if o:
line_split = o.decode().strip().split('\t')
@@ -213,7 +222,7 @@ def read_updates(version: Version, installation: str) -> Dict[str, set]:
elif version >= VERSION_1_2:
update_id = f'{line_split[2]}/{line_split[4]}/{installation}/{line_split[5]}'
else:
- update_id = '{}/{}/{}'.format(line_split[2], line_split[4], installation)
+ update_id = f'{line_split[2]}/{line_split[4]}/{installation}'
if version >= VERSION_1_3 and len(line_split) >= 6:
if line_split[4].strip().lower() in OPERATION_UPDATE_SYMBOLS:
@@ -229,19 +238,20 @@ def read_updates(version: Version, installation: str) -> Dict[str, set]:
return res
-def downgrade(app_ref: str, commit: str, installation: str, root_password: Optional[str]) -> SimpleProcess:
- cmd = ['flatpak', 'update', '--no-related', '--no-deps', '--commit={}'.format(commit), app_ref, '-y', '--{}'.format(installation)]
+def downgrade(app_ref: str, commit: str, installation: str, root_password: Optional[str], version: Version) -> SimpleProcess:
+ cmd = ('flatpak', 'update', '--no-related', '--no-deps', f'--commit={commit}', app_ref, '-y', f'--{installation}')
return SimpleProcess(cmd=cmd,
- root_password=root_password if installation=='system' else None,
+ root_password=root_password if installation == 'system' else None,
extra_paths={EXPORTS_PATH},
- success_phrases={'Changes complete.', 'Updates complete.'},
- wrong_error_phrases={'Warning'})
+ lang=DEFAULT_LANG if version < VERSION_1_12 else None,
+ success_phrases={'Changes complete.', 'Updates complete.'} if version < VERSION_1_12 else None,
+ wrong_error_phrases={'Warning'} if version < VERSION_1_12 else None)
def get_app_commits(app_ref: str, origin: str, installation: str, handler: ProcessHandler) -> Optional[List[str]]:
try:
- p = SimpleProcess(['flatpak', 'remote-info', '--log', origin, app_ref, '--{}'.format(installation)])
+ p = SimpleProcess(('flatpak', 'remote-info', '--log', origin, app_ref, f'--{installation}'))
success, output = handler.handle_simple(p)
if output.startswith('error:'):
return
@@ -252,7 +262,7 @@ def get_app_commits(app_ref: str, origin: str, installation: str, handler: Proce
def get_app_commits_data(app_ref: str, origin: str, installation: str, full_str: bool = True) -> List[dict]:
- log = run_cmd('{} remote-info --log {} {} --{}'.format('flatpak', origin, app_ref, installation))
+ log = run_cmd(f'flatpak remote-info --log {origin} {app_ref} --{installation}')
if not log:
raise NoInternetException()
@@ -282,13 +292,13 @@ def get_app_commits_data(app_ref: str, origin: str, installation: str, full_str:
def search(version: Version, word: str, installation: str, app_id: bool = False) -> List[dict]:
- res = run_cmd('{} search {} --{}'.format('flatpak', word, installation))
+ res = run_cmd(f'flatpak search {word} --{installation}', lang=None)
found = []
- split_res = res.split('\n')
+ split_res = res.strip().split('\n')
- if split_res and split_res[0].lower() != 'no matches found':
+ if split_res and '\t' in split_res[0]:
for info in split_res:
if info:
info_list = info.split('\t')
@@ -359,25 +369,28 @@ def search(version: Version, word: str, installation: str, app_id: bool = False)
return found
-def install(app_id: str, origin: str, installation: str) -> SimpleProcess:
- return SimpleProcess(cmd=['flatpak', 'install', origin, app_id, '-y', '--{}'.format(installation)],
+def install(app_id: str, origin: str, installation: str, version: Version) -> SimpleProcess:
+ return SimpleProcess(cmd=('flatpak', 'install', origin, app_id, '-y', f'--{installation}'),
extra_paths={EXPORTS_PATH},
- wrong_error_phrases={'Warning'},
+ lang=DEFAULT_LANG if version < VERSION_1_12 else None,
+ wrong_error_phrases={'Warning'} if version < VERSION_1_12 else None,
shell=True)
def set_default_remotes(installation: str, root_password: Optional[str] = None) -> SimpleProcess:
- cmd = ['flatpak', 'remote-add', '--if-not-exists', 'flathub', 'https://flathub.org/repo/flathub.flatpakrepo', '--{}'.format(installation)]
+ cmd = ('flatpak', 'remote-add', '--if-not-exists', 'flathub', f'{FLATHUB_URL}/repo/flathub.flatpakrepo',
+ f'--{installation}')
+
return SimpleProcess(cmd, root_password=root_password)
def has_remotes_set() -> bool:
- return bool(run_cmd('{} remotes'.format('flatpak')).strip())
+ return bool(run_cmd('flatpak remotes').strip())
def list_remotes() -> Dict[str, Set[str]]:
res = {'system': set(), 'user': set()}
- output = run_cmd('{} remotes'.format('flatpak')).strip()
+ output = run_cmd('flatpak remotes').strip()
if output:
lines = output.split('\n')
@@ -394,11 +407,11 @@ def list_remotes() -> Dict[str, Set[str]]:
def run(app_id: str):
- subprocess.Popen(['flatpak run {}'.format(app_id)], shell=True, env={**os.environ})
+ subprocess.Popen((f'flatpak run {app_id}',), shell=True, env={**os.environ})
def map_update_download_size(app_ids: Iterable[str], installation: str, version: Version) -> Dict[str, int]:
- success, output = ProcessHandler().handle_simple(SimpleProcess(['flatpak', 'update', '--{}'.format(installation)]))
+ success, output = ProcessHandler().handle_simple(SimpleProcess(('flatpak', 'update', f'--{installation}')))
if version >= VERSION_1_2:
res = {}
p = re.compile(r'^\d+.\t')
@@ -425,12 +438,12 @@ def map_update_download_size(app_ids: Iterable[str], installation: str, version:
if size and len(size) > 1:
try:
- res[related_id[0].strip()] = size_to_byte(float(size[0]), size[1].strip())
+ res[related_id[0].strip()] = size_to_byte(float(size[0].replace(',', '.')), size[1].strip())
except:
traceback.print_exc()
else:
try:
- res[related_id[0].strip()] = size_to_byte(float(size_tuple[0]), size_tuple[1].strip())
+ res[related_id[0].strip()] = size_to_byte(float(size_tuple[0].replace(',', '.')), size_tuple[1].strip())
except:
traceback.print_exc()
return res
diff --git a/bauh/gems/flatpak/resources/locale/ca b/bauh/gems/flatpak/resources/locale/ca
index 41c104205..8a66e7312 100644
--- a/bauh/gems/flatpak/resources/locale/ca
+++ b/bauh/gems/flatpak/resources/locale/ca
@@ -1,3 +1,6 @@
+flatpak.action.full_update=Full update
+flatpak.action.full_update.description=It completely updates the Flatpak apps and components. Useful if you are having issues with runtime updates.
+flatpak.action.full_update.status=Updating Flatpak apps and components
flatpak.config.install_level=level
flatpak.config.install_level.ask.tip={app} will ask the level that should be applied during the app installation
flatpak.config.install_level.system=system
diff --git a/bauh/gems/flatpak/resources/locale/de b/bauh/gems/flatpak/resources/locale/de
index afd6ce473..46ae49459 100644
--- a/bauh/gems/flatpak/resources/locale/de
+++ b/bauh/gems/flatpak/resources/locale/de
@@ -1,3 +1,6 @@
+flatpak.action.full_update=Full update
+flatpak.action.full_update.description=It completely updates the Flatpak apps and components. Useful if you are having issues with runtime updates.
+flatpak.action.full_update.status=Updating Flatpak apps and components
flatpak.config.install_level=level
flatpak.config.install_level.ask.tip={app} will ask the level that should be applied during the app installation
flatpak.config.install_level.system=system
diff --git a/bauh/gems/flatpak/resources/locale/en b/bauh/gems/flatpak/resources/locale/en
index 726d62140..aafc924d3 100644
--- a/bauh/gems/flatpak/resources/locale/en
+++ b/bauh/gems/flatpak/resources/locale/en
@@ -1,3 +1,6 @@
+flatpak.action.full_update=Full update
+flatpak.action.full_update.description=It completely updates the Flatpak apps and components. Useful if you are having issues with runtime updates.
+flatpak.action.full_update.status=Updating Flatpak apps and components
flatpak.config.install_level=level
flatpak.config.install_level.ask.tip={app} will ask the level that should be applied during the app installation
flatpak.config.install_level.system=system
diff --git a/bauh/gems/flatpak/resources/locale/es b/bauh/gems/flatpak/resources/locale/es
index 24e88dff5..16d1d37fd 100644
--- a/bauh/gems/flatpak/resources/locale/es
+++ b/bauh/gems/flatpak/resources/locale/es
@@ -1,3 +1,6 @@
+flatpak.action.full_update=Actualización completa
+flatpak.action.full_update.description=Actualiza completamente las aplicaciones y componentes Flatpak. Útil si hay problemas con las actualizaciones de runtimes.
+flatpak.action.full_update.status=Actualizando las aplicaciones y componentes Flatpak
flatpak.config.install_level=nivel
flatpak.config.install_level.ask.tip={app} preguntará el nivel que debe aplicarse durante la instalación de la aplicación
flatpak.config.install_level.system=sistema
diff --git a/bauh/gems/flatpak/resources/locale/fr b/bauh/gems/flatpak/resources/locale/fr
index df08b4c02..549ddcb58 100644
--- a/bauh/gems/flatpak/resources/locale/fr
+++ b/bauh/gems/flatpak/resources/locale/fr
@@ -1,3 +1,6 @@
+flatpak.action.full_update=Full update
+flatpak.action.full_update.description=It completely updates the Flatpak apps and components. Useful if you are having issues with runtime updates.
+flatpak.action.full_update.status=Updating Flatpak apps and components
flatpak.config.install_level=niveau
flatpak.config.install_level.ask.tip={app} va demander le niveau à appliquer durant l'installation
flatpak.config.install_level.system=système
diff --git a/bauh/gems/flatpak/resources/locale/it b/bauh/gems/flatpak/resources/locale/it
index 76ae88d75..54012fc57 100644
--- a/bauh/gems/flatpak/resources/locale/it
+++ b/bauh/gems/flatpak/resources/locale/it
@@ -1,3 +1,6 @@
+flatpak.action.full_update=Full update
+flatpak.action.full_update.description=It completely updates the Flatpak apps and components. Useful if you are having issues with runtime updates.
+flatpak.action.full_update.status=Updating Flatpak apps and components
flatpak.config.install_level=level
flatpak.config.install_level.ask.tip={app} will ask the level that should be applied during the app installation
flatpak.config.install_level.system=system
diff --git a/bauh/gems/flatpak/resources/locale/pt b/bauh/gems/flatpak/resources/locale/pt
index 724bcb8ac..f4af8a3a6 100644
--- a/bauh/gems/flatpak/resources/locale/pt
+++ b/bauh/gems/flatpak/resources/locale/pt
@@ -1,3 +1,6 @@
+flatpak.action.full_update=Atualização completa
+flatpak.action.full_update.description=Atualiza completamente aplicações e componentes Flatpak instalados. Útil se você estiver com problemas de atualização dos runtimes.
+flatpak.action.full_update.status=Atualizando aplicações e componentes Flatpak
flatpak.config.install_level=nível
flatpak.config.install_level.ask.tip=O {app} perguntará qual o nível deverá ser aplicado durante a instalação do aplicativo
flatpak.config.install_level.system=sistema
diff --git a/bauh/gems/flatpak/resources/locale/ru b/bauh/gems/flatpak/resources/locale/ru
index 80016c54c..6e4e7bca9 100644
--- a/bauh/gems/flatpak/resources/locale/ru
+++ b/bauh/gems/flatpak/resources/locale/ru
@@ -1,3 +1,6 @@
+flatpak.action.full_update=Full update
+flatpak.action.full_update.description=It completely updates the Flatpak apps and components. Useful if you are having issues with runtime updates.
+flatpak.action.full_update.status=Updating Flatpak apps and components
flatpak.config.install_level=Уровень
flatpak.config.install_level.ask.tip={app} запросит уровень, который должен быть применен во время установки приложения
flatpak.config.install_level.system=Система
diff --git a/bauh/gems/flatpak/resources/locale/tr b/bauh/gems/flatpak/resources/locale/tr
index 1901e4475..95c170bb5 100644
--- a/bauh/gems/flatpak/resources/locale/tr
+++ b/bauh/gems/flatpak/resources/locale/tr
@@ -1,3 +1,6 @@
+flatpak.action.full_update=Full update
+flatpak.action.full_update.description=It completely updates the Flatpak apps and components. Useful if you are having issues with runtime updates.
+flatpak.action.full_update.status=Updating Flatpak apps and components
flatpak.config.install_level=seviye
flatpak.config.install_level.ask.tip={app} uygulama kurulumu sırasında uygulanması gereken seviyeyi soracak
flatpak.config.install_level.system=sistem
diff --git a/bauh/gems/flatpak/worker.py b/bauh/gems/flatpak/worker.py
index 529c4afc4..e16df1bbd 100644
--- a/bauh/gems/flatpak/worker.py
+++ b/bauh/gems/flatpak/worker.py
@@ -55,7 +55,9 @@ def run(self):
if not self.app.name:
self.app.name = data.get('name')
- self.app.description = data.get('description', data.get('summary', None))
+ if not self.app.description:
+ self.app.description = data.get('description', data.get('summary', None))
+
self.app.icon_url = data.get('iconMobileUrl', None)
self.app.latest_version = data.get('currentReleaseVersion', self.app.version)
diff --git a/bauh/gems/snap/snap.py b/bauh/gems/snap/snap.py
index 002383db1..f3d4f2ed3 100644
--- a/bauh/gems/snap/snap.py
+++ b/bauh/gems/snap/snap.py
@@ -6,22 +6,21 @@
from bauh.commons.system import SimpleProcess
-BASE_CMD = 'snap'
-
def is_installed() -> bool:
- return bool(shutil.which(BASE_CMD))
+ return bool(shutil.which('snap'))
def uninstall_and_stream(app_name: str, root_password: Optional[str]) -> SimpleProcess:
- return SimpleProcess(cmd=[BASE_CMD, 'remove', app_name],
+ return SimpleProcess(cmd=('snap', 'remove', app_name),
root_password=root_password,
+ lang=None,
shell=True)
def install_and_stream(app_name: str, confinement: str, root_password: Optional[str], channel: Optional[str] = None) -> SimpleProcess:
- install_cmd = [BASE_CMD, 'install', app_name] # default
+ install_cmd = ['snap', 'install', app_name] # default
if confinement == 'classic':
install_cmd.append('--classic')
@@ -29,34 +28,35 @@ def install_and_stream(app_name: str, confinement: str, root_password: Optional[
if channel:
install_cmd.append(f'--channel={channel}')
- return SimpleProcess(install_cmd, root_password=root_password, shell=True)
+ return SimpleProcess(install_cmd, root_password=root_password, shell=True, lang=None)
def downgrade_and_stream(app_name: str, root_password: Optional[str]) -> SimpleProcess:
- return SimpleProcess(cmd=[BASE_CMD, 'revert', app_name],
+ return SimpleProcess(cmd=('snap', 'revert', app_name),
root_password=root_password,
- shell=True)
+ shell=True,
+ lang=None)
def refresh_and_stream(app_name: str, root_password: Optional[str], channel: Optional[str] = None) -> SimpleProcess:
- cmd = [BASE_CMD, 'refresh', app_name]
+ cmd = ['snap', 'refresh', app_name]
if channel:
cmd.append(f'--channel={channel}')
return SimpleProcess(cmd=cmd,
root_password=root_password,
- error_phrases={'no updates available'},
+ lang=None,
shell=True)
def run(cmd: str):
- subprocess.Popen([f'{BASE_CMD} run {cmd}'], shell=True, env={**os.environ})
+ subprocess.Popen((f'snap run {cmd}',), shell=True, env={**os.environ})
def is_api_available() -> Tuple[bool, str]:
output = StringIO()
- for o in SimpleProcess([BASE_CMD, 'search']).instance.stdout:
+ for o in SimpleProcess(('snap', 'search'), lang=None).instance.stdout:
if o:
output.write(o.decode())
diff --git a/bauh/gems/web/controller.py b/bauh/gems/web/controller.py
index 615359a6a..629b99169 100644
--- a/bauh/gems/web/controller.py
+++ b/bauh/gems/web/controller.py
@@ -59,6 +59,8 @@
RE_SEVERAL_SPACES = re.compile(r'\s+')
RE_SYMBOLS_SPLIT = re.compile(r'[\-|_\s:.]')
+DEFAULT_LANGUAGE_HEADER = 'en-US, en'
+
class WebApplicationManager(SoftwareManager):
@@ -77,12 +79,28 @@ def __init__(self, context: ApplicationContext, suggestions_loader: Optional[Sug
self.idxman = SearchIndexManager(logger=context.logger)
self._custom_actions: Optional[Iterable[CustomSoftwareAction]] = None
- def _get_lang_header(self) -> str:
+ def get_accept_language_header(self) -> str:
try:
- system_locale = locale.getdefaultlocale()
- return system_locale[0] if system_locale else 'en_US'
+ syslocale = locale.getdefaultlocale()
+
+ if syslocale:
+ locale_split = syslocale[0].split('_')
+
+ if len(locale_split) == 2:
+ sys_lang = f'{locale_split[0]}-{locale_split[1]}, {locale_split[0]}'
+
+ if DEFAULT_LANGUAGE_HEADER.split(',')[1].strip() not in sys_lang:
+ return f'{sys_lang}, {DEFAULT_LANGUAGE_HEADER}'
+
+ return sys_lang
+
+ else:
+ return f'{syslocale[0]}, {DEFAULT_LANGUAGE_HEADER}'
+ else:
+ return DEFAULT_LANGUAGE_HEADER
+
except:
- return 'en_US'
+ return DEFAULT_LANGUAGE_HEADER
def clean_environment(self, root_password: Optional[str], watcher: ProcessWatcher) -> bool:
handler = ProcessHandler(watcher)
@@ -221,7 +239,7 @@ def serialize_to_disk(self, pkg: SoftwarePackage, icon_bytes: Optional[bytes], o
super(WebApplicationManager, self).serialize_to_disk(pkg=pkg, icon_bytes=None, only_icon=False)
def _request_url(self, url: str) -> Optional[Response]:
- headers = {'Accept-language': self._get_lang_header(), 'User-Agent': UA_CHROME}
+ headers = {'Accept-language': self.get_accept_language_header(), 'User-Agent': UA_CHROME}
try:
return self.http_client.get(url, headers=headers, ignore_ssl=True, single_call=True, session=False, allow_redirects=True)
diff --git a/bauh/view/core/config.py b/bauh/view/core/config.py
index a9fa7e919..e9c7c5d81 100644
--- a/bauh/view/core/config.py
+++ b/bauh/view/core/config.py
@@ -3,6 +3,9 @@
FILE_PATH = f'{CONFIG_DIR}/config.yml'
+BACKUP_DEFAULT_REMOVE_METHOD = 'self'
+BACKUP_REMOVE_METHODS = {BACKUP_DEFAULT_REMOVE_METHOD, 'all'}
+
class CoreConfigManager(YAMLConfigManager):
@@ -63,7 +66,8 @@ def get_default_config(self) -> dict:
'downgrade': None,
'upgrade': None,
'mode': 'incremental',
- 'type': 'rsync'
+ 'type': 'rsync',
+ 'remove_method': 'self'
},
'boot': {
'load_apps': True
diff --git a/bauh/view/core/settings.py b/bauh/view/core/settings.py
index 88820a550..561bf12cb 100644
--- a/bauh/view/core/settings.py
+++ b/bauh/view/core/settings.py
@@ -16,7 +16,7 @@
FileChooserComponent, RangeInputComponent
from bauh.commons.view_utils import new_select
from bauh.view.core import timeshift
-from bauh.view.core.config import CoreConfigManager
+from bauh.view.core.config import CoreConfigManager, BACKUP_REMOVE_METHODS, BACKUP_DEFAULT_REMOVE_METHOD
from bauh.view.core.downloader import AdaptableFileDownloader
from bauh.view.util import translation
from bauh.view.util.translation import I18n
@@ -327,7 +327,7 @@ def _gen_general_settings(self, core_config: dict, screen_width: int, screen_hei
sub_comps = [FormComponent([select_locale, select_store_pwd, select_sysnotify, select_load_apps, select_sugs, inp_sugs, inp_reboot], spaces=False)]
return TabComponent(self.i18n['core.config.tab.general'].capitalize(), PanelComponent(sub_comps), None, 'core.gen')
- def _gen_bool_component(self, label: str, tooltip: str, value: bool, id_: str, max_width: int = 200) -> SingleSelectComponent:
+ def _gen_bool_component(self, label: str, tooltip: Optional[str], value: bool, id_: str, max_width: int = 200) -> SingleSelectComponent:
opts = [InputOption(label=self.i18n['yes'].capitalize(), value=True),
InputOption(label=self.i18n['no'].capitalize(), value=False)]
@@ -397,6 +397,7 @@ def _save_settings(self, general: PanelComponent,
core_config['backup']['enabled'] = bkp_form.get_component('enabled').get_selected()
core_config['backup']['mode'] = bkp_form.get_component('mode').get_selected()
core_config['backup']['type'] = bkp_form.get_component('type').get_selected()
+ core_config['backup']['remove_method'] = bkp_form.get_component('remove_method').get_selected()
core_config['backup']['install'] = bkp_form.get_component('install').get_selected()
core_config['backup']['uninstall'] = bkp_form.get_component('uninstall').get_selected()
core_config['backup']['upgrade'] = bkp_form.get_component('upgrade').get_selected()
@@ -580,5 +581,25 @@ def _gen_backup_settings(self, core_config: dict, screen_width: int, screen_heig
max_width=default_width,
id_='type')
- sub_comps = [FormComponent([enabled_opt, mode, type_, install_mode, uninstall_mode, upgrade_mode, downgrade_mode], spaces=False)]
+ remove_method = core_config['backup']['remove_method']
+
+ if not remove_method or remove_method not in BACKUP_REMOVE_METHODS:
+ remove_method = BACKUP_DEFAULT_REMOVE_METHOD
+
+ remove_i18n = 'core.config.backup.remove_method'
+ remove_opts = ((self.i18n[f'{remove_i18n}.{m}'], m, self.i18n[f'{remove_i18n}.{m}.tip'])
+ for m in sorted(BACKUP_REMOVE_METHODS))
+
+ remove_label = f'{self.i18n[remove_i18n]} ({self.i18n["core.config.backup.mode"]} ' \
+ f'"{self.i18n["core.config.backup.mode.only_one"].capitalize()}")'
+
+ sel_remove = new_select(label=remove_label,
+ tip=None,
+ value=remove_method,
+ opts=remove_opts,
+ max_width=default_width,
+ capitalize_label=False,
+ id_='remove_method')
+
+ sub_comps = [FormComponent([enabled_opt, type_, mode, sel_remove, install_mode, uninstall_mode, upgrade_mode, downgrade_mode], spaces=False)]
return TabComponent(self.i18n['core.config.tab.backup'].capitalize(), PanelComponent(sub_comps), None, 'core.bkp')
diff --git a/bauh/view/core/timeshift.py b/bauh/view/core/timeshift.py
index e2d4ad73f..2ed1f99ae 100644
--- a/bauh/view/core/timeshift.py
+++ b/bauh/view/core/timeshift.py
@@ -1,7 +1,11 @@
+import re
import shutil
-from typing import Optional
+from typing import Optional, Generator
-from bauh.commons.system import SimpleProcess
+from bauh import __app_name__
+from bauh.commons.system import SimpleProcess, new_root_subprocess
+
+RE_SNAPSHOTS = re.compile(r'\d+\s+>\s+([\w\-_]+)\s+.+<{}>'.format(__app_name__))
def is_available() -> bool:
@@ -9,8 +13,26 @@ def is_available() -> bool:
def delete_all_snapshots(root_password: Optional[str]) -> SimpleProcess:
- return SimpleProcess(['timeshift', '--delete-all', '--scripted'], root_password=root_password)
+ return SimpleProcess(('timeshift', '--delete-all', '--scripted'), root_password=root_password)
+
+
+def delete(snapshot_name: str, root_password: Optional[str]) -> SimpleProcess:
+ return SimpleProcess(('timeshift', '--delete', '--snapshot', snapshot_name),
+ shell=True, root_password=root_password)
def create_snapshot(root_password: Optional[str], mode: str) -> SimpleProcess:
- return SimpleProcess(['timeshift', '--create', '--scripted', '--{}'.format(mode)], root_password=root_password)
+ return SimpleProcess(('timeshift', '--create', '--scripted', f'--{mode}', '--comments', f'<{__app_name__}>'),
+ root_password=root_password)
+
+
+def read_created_snapshots(root_password: Optional[str]) -> Generator[str, None, None]:
+ proc = new_root_subprocess(cmd=('timeshift', '--list'), root_password=root_password, shell=True)
+ proc.wait()
+
+ if proc.returncode == 0:
+ output = '\n'.join((o.decode() for o in proc.stdout))
+
+ if output:
+ for name in RE_SNAPSHOTS.findall(output):
+ yield name
diff --git a/bauh/view/qt/commons.py b/bauh/view/qt/commons.py
index 8ba0a2210..e5384a13c 100644
--- a/bauh/view/qt/commons.py
+++ b/bauh/view/qt/commons.py
@@ -13,6 +13,7 @@ def new_pkgs_info() -> dict:
'napp_updates': 0,
'pkgs_displayed': [],
'not_installed': 0,
+ 'installed': 0,
'categories': set(),
'pkgs': []} # total packages
@@ -41,7 +42,11 @@ def update_info(pkgv: PackageView, pkgs_info: dict):
pkgs_info['categories'].add(cat)
pkgs_info['pkgs'].append(pkgv)
- pkgs_info['not_installed'] += 1 if not pkgv.model.installed else 0
+
+ if pkgv.model.installed:
+ pkgs_info['installed'] += 1
+ else:
+ pkgs_info['not_installed'] += 1
def apply_filters(pkg: PackageView, filters: dict, info: dict, limit: bool = True):
diff --git a/bauh/view/qt/components.py b/bauh/view/qt/components.py
index a3fbdd23d..8170f5596 100644
--- a/bauh/view/qt/components.py
+++ b/bauh/view/qt/components.py
@@ -865,6 +865,7 @@ def _update_value():
def _wrap(self, comp: QWidget, model: ViewComponent) -> QWidget:
field_container = QWidget()
field_container.setLayout(QHBoxLayout())
+ field_container.layout().setContentsMargins(0, 0, 0, 0)
if model.max_width > 0:
field_container.setMaximumWidth(int(model.max_width))
diff --git a/bauh/view/qt/dialog.py b/bauh/view/qt/dialog.py
index ae867a0f7..13ffc0f56 100644
--- a/bauh/view/qt/dialog.py
+++ b/bauh/view/qt/dialog.py
@@ -35,7 +35,7 @@ def __init__(self, title: str, body: Optional[str], i18n: I18n, icon: QIcon = QI
widgets: Optional[List[QWidget]] = None, confirmation_button: bool = True, deny_button: bool = True,
window_cancel: bool = False, confirmation_label: Optional[str] = None, deny_label: Optional[str] = None,
confirmation_icon: bool = True, min_width: Optional[int] = None,
- min_height: Optional[int] = None):
+ min_height: Optional[int] = None, max_width: Optional[int] = None):
super(ConfirmationDialog, self).__init__()
if not window_cancel:
@@ -46,6 +46,9 @@ def __init__(self, title: str, body: Optional[str], i18n: I18n, icon: QIcon = QI
self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Preferred)
self.setMinimumWidth(min_width if min_width and min_width > 0 else 250)
+ if max_width is not None and max_width > 0:
+ self.setMaximumWidth(max_width)
+
if isinstance(min_height, int) and min_height > 0:
self.setMinimumHeight(min_height)
diff --git a/bauh/view/qt/screenshots.py b/bauh/view/qt/screenshots.py
index cc3b719ec..5d52aaf77 100644
--- a/bauh/view/qt/screenshots.py
+++ b/bauh/view/qt/screenshots.py
@@ -47,7 +47,22 @@ def __init__(self, pkg: PackageView, http_client: HttpClient, icon_cache: Memory
self.setWindowIcon(QIcon(pkg.model.get_type_icon_path()))
self.setLayout(QVBoxLayout())
+ self.bt_close = QPushButton(self.i18n['screenshots.bt_close'])
+ self.bt_close.setObjectName('close')
+ self.bt_close.clicked.connect(self.close)
+ self.bt_close.setCursor(QCursor(Qt.PointingHandCursor))
+
+ self.upper_buttons = QWidget()
+ self.upper_buttons.setObjectName('upper_buttons')
+ self.upper_buttons.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed)
+ self.upper_buttons.setContentsMargins(0, 0, 0, 0)
+ self.upper_buttons.setLayout(QHBoxLayout())
+ self.upper_buttons.layout().setAlignment(Qt.AlignRight)
+ self.upper_buttons.layout().addWidget(self.bt_close)
+ self.layout().addWidget(self.upper_buttons)
+
self.layout().addWidget(new_spacer())
+
self.img = QLabel()
self.img.setObjectName('image')
self.layout().addWidget(self.img)
diff --git a/bauh/view/qt/thread.py b/bauh/view/qt/thread.py
index a692d5174..adece32f8 100644
--- a/bauh/view/qt/thread.py
+++ b/bauh/view/qt/thread.py
@@ -5,7 +5,7 @@
from datetime import datetime, timedelta
from io import StringIO
from pathlib import Path
-from typing import List, Type, Set, Tuple, Optional, Iterable
+from typing import List, Type, Set, Tuple, Optional
import requests
from PyQt5.QtCore import QThread, pyqtSignal, QObject
@@ -26,7 +26,7 @@
from bauh.commons.system import ProcessHandler, SimpleProcess
from bauh.commons.view_utils import get_human_size_str
from bauh.view.core import timeshift
-from bauh.view.core.config import CoreConfigManager
+from bauh.view.core.config import CoreConfigManager, BACKUP_REMOVE_METHODS, BACKUP_DEFAULT_REMOVE_METHOD
from bauh.view.qt import commons
from bauh.view.qt.commons import sort_packages
from bauh.view.qt.view_model import PackageView, PackageViewStatus
@@ -47,8 +47,9 @@ class AsyncAction(QThread, ProcessWatcher):
signal_root_password = pyqtSignal()
signal_progress_control = pyqtSignal(bool)
- def __init__(self, root_password: Optional[Tuple[bool, str]] = None):
+ def __init__(self, i18n: I18n, root_password: Optional[Tuple[bool, str]] = None):
super(AsyncAction, self).__init__()
+ self.i18n = i18n
self.wait_confirmation = False
self.confirmation_res = None
self.root_password = root_password
@@ -59,13 +60,14 @@ def request_confirmation(self, title: str, body: str, components: List[ViewCompo
window_cancel: bool = False,
confirmation_button: bool = True,
min_width: Optional[int] = None,
- min_height: Optional[int] = None) -> bool:
+ min_height: Optional[int] = None,
+ max_width: Optional[int] = None) -> bool:
self.wait_confirmation = True
self.signal_confirmation.emit({'title': title, 'body': body, 'components': components,
'confirmation_label': confirmation_label, 'deny_label': deny_label,
'deny_button': deny_button, 'window_cancel': window_cancel,
'confirmation_button': confirmation_button, 'min_width': min_width,
- 'min_height': min_height})
+ 'min_height': min_height, 'max_width': max_width})
self.wait_user()
return self.confirmation_res
@@ -129,7 +131,7 @@ def request_reboot(self, msg: str) -> bool:
return False
- def _generate_backup(self, app_config: dict, i18n: I18n, root_password: Optional[str]) -> bool:
+ def _generate_backup(self, app_config: dict, root_password: Optional[str]) -> bool:
if app_config['backup']['mode'] not in ('only_one', 'incremental'):
self.show_message(title=self.i18n['error'].capitalize(),
body='{}: {}'.format(self.i18n['action.backup.invalid_mode'],bold(app_config['backup']['mode'])),
@@ -139,25 +141,45 @@ def _generate_backup(self, app_config: dict, i18n: I18n, root_password: Optional
handler = ProcessHandler(self)
if app_config['backup']['mode'] == 'only_one':
- self.change_substatus('[{}] {}'.format(i18n['core.config.tab.backup'].lower(), i18n['action.backup.substatus.delete']))
- deleted, _ = handler.handle_simple(timeshift.delete_all_snapshots(root_password))
-
- if not deleted and not self.request_confirmation(title=i18n['core.config.tab.backup'],
- body='{}. {}'.format(i18n['action.backup.error.delete'],
- i18n['action.backup.error.proceed']),
- confirmation_label=i18n['yes'].capitalize(),
- deny_label=i18n['no'].capitalize()):
+ remove_method = app_config['backup']['remove_method']
+
+ if remove_method not in BACKUP_REMOVE_METHODS:
+ remove_method = BACKUP_DEFAULT_REMOVE_METHOD
+
+ delete_failed = False
+
+ if remove_method == 'self':
+ previous_snapshots = tuple(timeshift.read_created_snapshots(root_password))
+
+ if previous_snapshots:
+ substatus = f"[{self.i18n['core.config.tab.backup'].lower()}] {self.i18n['action.backup.substatus.delete']}"
+ self.change_substatus(substatus)
+
+ for snapshot in reversed(previous_snapshots):
+ deleted, _ = handler.handle_simple(timeshift.delete(snapshot, root_password))
+
+ if not deleted:
+ delete_failed = True
+ else:
+ deleted, _ = handler.handle_simple(timeshift.delete_all_snapshots(root_password))
+ delete_failed = not deleted
+
+ if delete_failed and not self.request_confirmation(title=self.i18n['core.config.tab.backup'],
+ body=f"{self.i18n['action.backup.error.delete']}. "
+ f"{self.i18n['action.backup.error.proceed']}",
+ confirmation_label=self.i18n['yes'].capitalize(),
+ deny_label=self.i18n['no'].capitalize()):
self.change_substatus('')
return False
- self.change_substatus('[{}] {}'.format(i18n['core.config.tab.backup'].lower(), i18n['action.backup.substatus.create']))
+ self.change_substatus('[{}] {}'.format(self.i18n['core.config.tab.backup'].lower(), self.i18n['action.backup.substatus.create']))
created, _ = handler.handle_simple(timeshift.create_snapshot(root_password, app_config['backup']['type']))
- if not created and not self.request_confirmation(title=i18n['core.config.tab.backup'],
- body='{}. {}'.format(i18n['action.backup.error.create'],
- i18n['action.backup.error.proceed']),
- confirmation_label=i18n['yes'].capitalize(),
- deny_label=i18n['no'].capitalize()):
+ if not created and not self.request_confirmation(title=self.i18n['core.config.tab.backup'],
+ body='{}. {}'.format(self.i18n['action.backup.error.create'],
+ self.i18n['action.backup.error.proceed']),
+ confirmation_label=self.i18n['yes'].capitalize(),
+ deny_label=self.i18n['no'].capitalize()):
self.change_substatus('')
return False
@@ -173,24 +195,24 @@ def _check_backup_requirements(self, app_config: dict, pkg: Optional[PackageView
return bool(app_config['backup']['enabled']) and timeshift.is_available()
- def _should_backup(self, action_key: str, app_config: dict, i18n: I18n) -> bool:
+ def _should_backup(self, action_key: str, app_config: dict) -> bool:
# backup -> true: do not ask, only execute | false: do not ask or execute | None: ask
backup = app_config['backup'][action_key] if action_key else None
if backup is None:
- return self.request_confirmation(title=i18n['core.config.tab.backup'],
- body=i18n['action.backup.msg'],
- confirmation_label=i18n['yes'].capitalize(),
- deny_label=i18n['no'].capitalize())
+ return self.request_confirmation(title=self.i18n['core.config.tab.backup'],
+ body=self.i18n['action.backup.msg'],
+ confirmation_label=self.i18n['yes'].capitalize(),
+ deny_label=self.i18n['no'].capitalize())
else:
return backup
- def request_backup(self, action_key: Optional[str], pkg: Optional[PackageView], i18n: I18n, app_config: dict, root_password: Optional[str], backup_only: bool = False) -> Tuple[bool, Optional[str]]:
+ def request_backup(self, action_key: Optional[str], pkg: Optional[PackageView], app_config: dict, root_password: Optional[str], backup_only: bool = False) -> Tuple[bool, Optional[str]]:
if not backup_only:
if not self._check_backup_requirements(app_config=app_config, pkg=pkg, action_key=action_key):
return True, root_password
- if not self._should_backup(action_key=action_key, app_config=app_config, i18n=i18n):
+ if not self._should_backup(action_key=action_key, app_config=app_config):
return True, root_password
pwd = root_password
@@ -200,7 +222,7 @@ def request_backup(self, action_key: Optional[str], pkg: Optional[PackageView],
if not valid_password:
return False, None
- return self._generate_backup(app_config=app_config, i18n=i18n, root_password=pwd), pwd
+ return self._generate_backup(app_config=app_config, root_password=pwd), pwd
class UpgradeSelected(AsyncAction):
@@ -210,10 +232,9 @@ class UpgradeSelected(AsyncAction):
def __init__(self, manager: SoftwareManager, internet_checker: InternetChecker, i18n: I18n,
screen_width: int, pkgs: List[PackageView] = None):
- super(UpgradeSelected, self).__init__()
+ super(UpgradeSelected, self).__init__(i18n=i18n)
self.pkgs = pkgs
self.manager = manager
- self.i18n = i18n
self.internet_checker = internet_checker
self.screen_width = screen_width
@@ -491,7 +512,7 @@ def run(self):
# backup dialog ( if enabled, supported and accepted )
should_backup = bkp_supported
should_backup = should_backup and self._check_backup_requirements(app_config=app_config, pkg=None, action_key='upgrade')
- should_backup = should_backup and self._should_backup(action_key='upgrade', app_config=app_config, i18n=self.i18n)
+ should_backup = should_backup and self._should_backup(action_key='upgrade', app_config=app_config)
# trim dialog ( if enabled and accepted )
if app_config['disk']['trim']['after_upgrade'] is not False:
@@ -511,7 +532,6 @@ def run(self):
if should_backup:
proceed, root_password = self.request_backup(action_key='upgrade',
app_config=app_config,
- i18n=self.i18n,
root_password=root_password,
pkg=None,
backup_only=True)
@@ -550,8 +570,8 @@ def run(self):
class RefreshApps(AsyncAction):
- def __init__(self, manager: SoftwareManager, pkg_types: Set[Type[SoftwarePackage]] = None):
- super(RefreshApps, self).__init__()
+ def __init__(self, i18n: I18n, manager: SoftwareManager, pkg_types: Set[Type[SoftwarePackage]] = None):
+ super(RefreshApps, self).__init__(i18n=i18n)
self.manager = manager
self.pkg_types = pkg_types
@@ -580,18 +600,16 @@ def run(self):
class UninstallPackage(AsyncAction):
def __init__(self, manager: SoftwareManager, icon_cache: MemoryCache, i18n: I18n, pkg: PackageView = None):
- super(UninstallPackage, self).__init__()
+ super(UninstallPackage, self).__init__(i18n=i18n)
self.pkg = pkg
self.manager = manager
self.icon_cache = icon_cache
self.root_pwd = None
- self.i18n = i18n
def run(self):
if self.pkg:
proceed, _ = self.request_backup(action_key='uninstall',
pkg=self.pkg,
- i18n=self.i18n,
root_password=self.root_pwd,
app_config=CoreConfigManager().get_config())
if not proceed:
@@ -622,10 +640,9 @@ def run(self):
class DowngradePackage(AsyncAction):
def __init__(self, manager: SoftwareManager, i18n: I18n, pkg: PackageView = None):
- super(DowngradePackage, self).__init__()
+ super(DowngradePackage, self).__init__(i18n=i18n)
self.manager = manager
self.pkg = pkg
- self.i18n = i18n
self.root_pwd = None
def run(self):
@@ -634,7 +651,6 @@ def run(self):
proceed, _ = self.request_backup(action_key='downgrade',
pkg=self.pkg,
- i18n=self.i18n,
root_password=self.root_pwd,
app_config=CoreConfigManager().get_config())
@@ -657,8 +673,8 @@ def run(self):
class ShowPackageInfo(AsyncAction):
- def __init__(self, manager: SoftwareManager, pkg: PackageView = None):
- super(ShowPackageInfo, self).__init__()
+ def __init__(self, i18n: I18n, manager: SoftwareManager, pkg: PackageView = None):
+ super(ShowPackageInfo, self).__init__(i18n=i18n)
self.pkg = pkg
self.manager = manager
@@ -678,10 +694,9 @@ def run(self):
class ShowPackageHistory(AsyncAction):
def __init__(self, manager: SoftwareManager, i18n: I18n, pkg: PackageView = None):
- super(ShowPackageHistory, self).__init__()
+ super(ShowPackageHistory, self).__init__(i18n=i18n)
self.pkg = pkg
self.manager = manager
- self.i18n = i18n
def run(self):
if self.pkg:
@@ -695,8 +710,8 @@ def run(self):
class SearchPackages(AsyncAction):
- def __init__(self, manager: SoftwareManager):
- super(SearchPackages, self).__init__()
+ def __init__(self, i18n: I18n, manager: SoftwareManager):
+ super(SearchPackages, self).__init__(i18n=i18n)
self.word = None
self.manager = manager
@@ -717,11 +732,10 @@ def run(self):
class InstallPackage(AsyncAction):
def __init__(self, manager: SoftwareManager, icon_cache: MemoryCache, i18n: I18n, pkg: PackageView = None):
- super(InstallPackage, self).__init__()
+ super(InstallPackage, self).__init__(i18n=i18n)
self.pkg = pkg
self.manager = manager
self.icon_cache = icon_cache
- self.i18n = i18n
self.root_pwd = None
def run(self):
@@ -730,7 +744,6 @@ def run(self):
proceed, _ = self.request_backup(action_key='install',
pkg=self.pkg,
- i18n=self.i18n,
root_password=self.root_pwd,
app_config=CoreConfigManager().get_config())
@@ -895,8 +908,8 @@ def run(self):
class FindSuggestions(AsyncAction):
- def __init__(self, man: SoftwareManager):
- super(FindSuggestions, self).__init__()
+ def __init__(self, i18n: I18n, man: SoftwareManager):
+ super(FindSuggestions, self).__init__(i18n=i18n)
self.man = man
self.filter_installed = False
@@ -922,8 +935,8 @@ def run(self):
class LaunchPackage(AsyncAction):
- def __init__(self, manager: SoftwareManager, pkg: PackageView = None):
- super(LaunchPackage, self).__init__()
+ def __init__(self, i18n: I18n, manager: SoftwareManager, pkg: PackageView = None):
+ super(LaunchPackage, self).__init__(i18n=i18n)
self.pkg = pkg
self.manager = manager
@@ -943,8 +956,8 @@ class ApplyFilters(AsyncAction):
signal_table = pyqtSignal(object)
- def __init__(self, filters: dict = None, pkgs: List[PackageView] = None):
- super(ApplyFilters, self).__init__()
+ def __init__(self, i18n: I18n, filters: dict = None, pkgs: List[PackageView] = None):
+ super(ApplyFilters, self).__init__(i18n=i18n)
self.pkgs = pkgs
self.filters = filters
self.wait_table_update = False
@@ -979,12 +992,11 @@ def run(self):
class CustomAction(AsyncAction):
def __init__(self, manager: SoftwareManager, i18n: I18n, custom_action: CustomSoftwareAction = None, pkg: PackageView = None, root_password: Optional[str] = None):
- super(CustomAction, self).__init__()
+ super(CustomAction, self).__init__(i18n=i18n)
self.manager = manager
self.pkg = pkg
self.custom_action = custom_action
self.root_pwd = root_password
- self.i18n = i18n
def run(self):
res = {'success': False, 'pkg': self.pkg, 'action': self.custom_action, 'error': None, 'error_type': MessageType.ERROR}
@@ -992,7 +1004,6 @@ def run(self):
if self.custom_action.backup:
proceed, _ = self.request_backup(app_config=CoreConfigManager().get_config(),
action_key=None,
- i18n=self.i18n,
root_password=self.root_pwd,
pkg=self.pkg)
if not proceed:
@@ -1021,8 +1032,8 @@ def run(self):
class ShowScreenshots(AsyncAction):
- def __init__(self, manager: SoftwareManager, pkg: PackageView = None):
- super(ShowScreenshots, self).__init__()
+ def __init__(self, i18n: I18n, manager: SoftwareManager, pkg: PackageView = None):
+ super(ShowScreenshots, self).__init__(i18n=i18n)
self.pkg = pkg
self.manager = manager
@@ -1035,8 +1046,8 @@ def run(self):
class IgnorePackageUpdates(AsyncAction):
- def __init__(self, manager: SoftwareManager, pkg: PackageView = None):
- super(IgnorePackageUpdates, self).__init__()
+ def __init__(self, i18n: I18n, manager: SoftwareManager, pkg: PackageView = None):
+ super(IgnorePackageUpdates, self).__init__(i18n=i18n)
self.pkg = pkg
self.manager = manager
diff --git a/bauh/view/qt/window.py b/bauh/view/qt/window.py
index 3847934bf..1b3273e3b 100755
--- a/bauh/view/qt/window.py
+++ b/bauh/view/qt/window.py
@@ -331,18 +331,18 @@ def __init__(self, i18n: I18n, icon_cache: MemoryCache, manager: SoftwareManager
internet_checker=context.internet_checker,
screen_width=screen_size.width()),
finished_call=self._finish_upgrade_selected)
- self.thread_refresh = self._bind_async_action(RefreshApps(self.manager), finished_call=self._finish_refresh_packages, only_finished=True)
+ self.thread_refresh = self._bind_async_action(RefreshApps(i18n, self.manager), finished_call=self._finish_refresh_packages, only_finished=True)
self.thread_uninstall = self._bind_async_action(UninstallPackage(self.manager, self.icon_cache, self.i18n), finished_call=self._finish_uninstall)
- self.thread_show_info = self._bind_async_action(ShowPackageInfo(self.manager), finished_call=self._finish_show_info)
+ self.thread_show_info = self._bind_async_action(ShowPackageInfo(i18n, self.manager), finished_call=self._finish_show_info)
self.thread_show_history = self._bind_async_action(ShowPackageHistory(self.manager, self.i18n), finished_call=self._finish_show_history)
- self.thread_search = self._bind_async_action(SearchPackages(self.manager), finished_call=self._finish_search, only_finished=True)
+ self.thread_search = self._bind_async_action(SearchPackages(i18n, self.manager), finished_call=self._finish_search, only_finished=True)
self.thread_downgrade = self._bind_async_action(DowngradePackage(self.manager, self.i18n), finished_call=self._finish_downgrade)
- self.thread_suggestions = self._bind_async_action(FindSuggestions(man=self.manager), finished_call=self._finish_load_suggestions, only_finished=True)
- self.thread_launch = self._bind_async_action(LaunchPackage(self.manager), finished_call=self._finish_launch_package, only_finished=False)
+ self.thread_suggestions = self._bind_async_action(FindSuggestions(i18n=i18n, man=self.manager), finished_call=self._finish_load_suggestions, only_finished=True)
+ self.thread_launch = self._bind_async_action(LaunchPackage(i18n, self.manager), finished_call=self._finish_launch_package, only_finished=False)
self.thread_custom_action = self._bind_async_action(CustomAction(manager=self.manager, i18n=self.i18n), finished_call=self._finish_execute_custom_action)
- self.thread_screenshots = self._bind_async_action(ShowScreenshots(self.manager), finished_call=self._finish_show_screenshots)
+ self.thread_screenshots = self._bind_async_action(ShowScreenshots(i18n, self.manager), finished_call=self._finish_show_screenshots)
- self.thread_apply_filters = ApplyFilters()
+ self.thread_apply_filters = ApplyFilters(i18n)
self.thread_apply_filters.signal_finished.connect(self._finish_apply_filters)
self.thread_apply_filters.signal_table.connect(self._update_table_and_upgrades)
self.signal_table_update.connect(self.thread_apply_filters.stop_waiting)
@@ -358,7 +358,7 @@ def __init__(self, i18n: I18n, icon_cache: MemoryCache, manager: SoftwareManager
self.thread_notify_pkgs_ready.signal_finished.connect(self._update_state_when_pkgs_ready)
self.signal_stop_notifying.connect(self.thread_notify_pkgs_ready.stop_working)
- self.thread_ignore_updates = IgnorePackageUpdates(manager=self.manager)
+ self.thread_ignore_updates = IgnorePackageUpdates(i18n=i18n, manager=self.manager)
self._bind_async_action(self.thread_ignore_updates, finished_call=self.finish_ignore_updates)
self.thread_reload = StartAsyncAction(delay_in_milis=5)
@@ -469,7 +469,7 @@ def _register_groups(self):
*common_filters)
self.comp_manager.register_group(GROUP_UPPER_BAR, False,
- CHECK_APPS, CHECK_UPDATES, COMBO_CATEGORIES, COMBO_TYPES, INP_NAME,
+ CHECK_APPS, CHECK_UPDATES, CHECK_INSTALLED, COMBO_CATEGORIES, COMBO_TYPES, INP_NAME,
BT_INSTALLED, BT_SUGGESTIONS, BT_REFRESH, BT_UPGRADE)
self.comp_manager.register_group(GROUP_LOWER_BTS, False, BT_SUGGESTIONS, BT_THEMES, BT_CUSTOM_ACTIONS, BT_SETTINGS, BT_ABOUT)
@@ -559,7 +559,8 @@ def _ask_confirmation(self, msg: dict):
window_cancel=msg['window_cancel'],
confirmation_button=msg.get('confirmation_button', True),
min_width=msg.get('min_width'),
- min_height=msg.get('min_height'))
+ min_height=msg.get('min_height'),
+ max_width=msg.get('max_width'))
diag.ask()
res = diag.confirmed
self.thread_animate_progress.animate()
@@ -659,7 +660,7 @@ def _update_state_when_pkgs_ready(self):
self._reorganize()
def _update_package_data(self, idx: int):
- if self.table_apps.isEnabled():
+ if self.table_apps.isEnabled() and self.pkgs is not None and 0 <= idx < len(self.pkgs):
pkg = self.pkgs[idx]
pkg.status = PackageViewStatus.READY
self.table_apps.update_package(pkg)
@@ -805,6 +806,7 @@ def _finish_uninstall(self, res: dict):
self.update_custom_actions()
self._show_console_checkbox_if_output()
+ self._update_installed_filter()
self.begin_apply_filters()
notify_tray()
else:
@@ -932,9 +934,6 @@ def update_pkgs(self, new_pkgs: Optional[List[SoftwarePackage]], as_installed: b
pkgs_info = commons.new_pkgs_info()
filters = self._gen_filters(ignore_updates=ignore_updates)
- if not keep_filters:
- self._change_checkbox(self.check_installed, False, 'filter_installed', trigger=False)
-
if new_pkgs is not None:
old_installed = None
@@ -988,6 +987,9 @@ def update_pkgs(self, new_pkgs: Optional[List[SoftwarePackage]], as_installed: b
self.pkgs_installed = pkgs_info['pkgs']
self.pkgs = pkgs_info['pkgs_displayed']
+ self._update_installed_filter(installed_available=pkgs_info['installed'] > 0,
+ keep_state=keep_filters,
+ hide=as_installed)
self._update_table(pkgs_info=pkgs_info)
if new_pkgs:
@@ -1007,6 +1009,27 @@ def update_pkgs(self, new_pkgs: Optional[List[SoftwarePackage]], as_installed: b
return True
+ def _update_installed_filter(self, keep_state: bool = True, hide: bool = False, installed_available: Optional[bool] = None):
+ if installed_available is not None:
+ has_installed = installed_available
+ elif self.pkgs_available == self.pkgs_installed: # it means the "installed" view is loaded
+ has_installed = False
+ else:
+ has_installed = False
+ if self.pkgs_available:
+ for p in self.pkgs_available:
+ if p.model.installed:
+ has_installed = True
+ break
+
+ if not keep_state or not has_installed:
+ self._change_checkbox(self.check_installed, False, 'filter_installed', trigger=False)
+
+ if hide:
+ self.comp_manager.set_component_visible(CHECK_INSTALLED, False)
+ else:
+ self.comp_manager.set_component_visible(CHECK_INSTALLED, has_installed)
+
def _apply_filters(self, pkgs_info: dict, ignore_updates: bool):
pkgs_info['pkgs_displayed'] = []
filters = self._gen_filters(ignore_updates=ignore_updates)
@@ -1438,6 +1461,7 @@ def _finish_install(self, res: dict):
self.pkgs_installed.insert(idx, PackageView(model, self.i18n))
self.update_custom_actions()
+ self._update_installed_filter(installed_available=True, keep_state=True)
self.table_apps.change_headers_policy(policy=QHeaderView.Stretch, maximized=self._maximized)
self.table_apps.change_headers_policy(policy=QHeaderView.ResizeToContents, maximized=self._maximized)
self._resize(accept_lower_width=False)
diff --git a/bauh/view/resources/locale/ca b/bauh/view/resources/locale/ca
index 87b6267e8..53ce2f99d 100644
--- a/bauh/view/resources/locale/ca
+++ b/bauh/view/resources/locale/ca
@@ -45,7 +45,7 @@ Ukraine=Ukraine
United Kingdom=United Kingdom
United States=United States
action.backup.error.create=It was not possible to generate a new system copy
-action.backup.error.delete=It was not possible to delete the old system copies
+action.backup.error.delete=It was not possible to delete all the old system copies
action.backup.error.proceed=Proceed anyway ?
action.backup.invalid_mode=Invalid backup mode
action.backup.msg=Do you want to generate a system copy before proceeding ?
@@ -211,6 +211,11 @@ core.config.backup.mode.incremental=Incremental
core.config.backup.mode.incremental.tip=A new system backup will be generated containing only the changed files since the latest copy.
core.config.backup.mode.only_one=Single
core.config.backup.mode.only_one.tip=Only one system backup will be kept. Pre-existing backups will be erased.
+core.config.backup.remove_method=Remove
+core.config.backup.remove_method.self=Only generated
+core.config.backup.remove_method.self.tip=It removes only the self generated backups
+core.config.backup.remove_method.all=All
+core.config.backup.remove_method.all.tip=It removes all existing backups on the disc
core.config.backup.uninstall=Before uninstalling
core.config.backup.upgrade=Before upgrading
core.config.boot.load_apps=Load apps after startup
@@ -406,6 +411,7 @@ publisher=proveïdor
publisher.verified=verificat
repository=dipòsit
screenshots.bt_back.label=anterior
+screenshots.bt_close=Tancar
screenshots.bt_next.label=següent
screenshots.download.no_content=No hi ha contingut a mostrar
screenshots.image.loading=Loading
diff --git a/bauh/view/resources/locale/de b/bauh/view/resources/locale/de
index aac43a58d..2b414ca2c 100644
--- a/bauh/view/resources/locale/de
+++ b/bauh/view/resources/locale/de
@@ -45,7 +45,7 @@ Ukraine=Ukraine
United Kingdom=United Kingdom
United States=United States
action.backup.error.create=It was not possible to generate a new system copy
-action.backup.error.delete=It was not possible to delete the old system copies
+action.backup.error.delete=It was not possible to delete all the old system copies
action.backup.error.proceed=Proceed anyway ?
action.backup.invalid_mode=Invalid backup mode
action.backup.msg=Do you want to generate a system copy before proceeding ?
@@ -210,6 +210,11 @@ core.config.backup.mode.incremental=Incremental
core.config.backup.mode.incremental.tip=A new system backup will be generated containing only the changed files since the latest copy.
core.config.backup.mode.only_one=Single
core.config.backup.mode.only_one.tip=Only one system backup will be kept. Pre-existing backups will be erased.
+core.config.backup.remove_method=Remove
+core.config.backup.remove_method.self=Only generated
+core.config.backup.remove_method.self.tip=It removes only the self generated backups
+core.config.backup.remove_method.all=All
+core.config.backup.remove_method.all.tip=It removes all existing backups on the disc
core.config.backup.uninstall=Before uninstalling
core.config.backup.upgrade=Before upgrading
core.config.boot.load_apps=Load apps after startup
@@ -405,6 +410,7 @@ publisher=Herausgeber
publisher.verified=Geprüft
repository=Repository
screenshots.bt_back.label=Zurück
+screenshots.bt_close=Schließen
screenshots.bt_next.label=Fortfahren
screenshots.download.no_content=Kein Inhalt
screenshots.download.no_response=Screenshot nicht gefunden
diff --git a/bauh/view/resources/locale/en b/bauh/view/resources/locale/en
index bb4e98935..5c04ac3d7 100644
--- a/bauh/view/resources/locale/en
+++ b/bauh/view/resources/locale/en
@@ -45,7 +45,7 @@ Ukraine=Ukraine
United Kingdom=United Kingdom
United States=United States
action.backup.error.create=It was not possible to generate a new system copy
-action.backup.error.delete=It was not possible to delete the old system copies
+action.backup.error.delete=It was not possible to delete all the old system copies
action.backup.error.proceed=Proceed anyway ?
action.backup.invalid_mode=Invalid backup mode
action.backup.msg=Do you want to generate a system copy before proceeding ?
@@ -211,6 +211,11 @@ core.config.backup.mode.incremental=Incremental
core.config.backup.mode.only_one.tip=Only one system backup will be kept. Pre-existing backups will be erased.
core.config.backup.mode.only_one=Single
core.config.backup.mode=Mode
+core.config.backup.remove_method=Remove
+core.config.backup.remove_method.self=Only generated
+core.config.backup.remove_method.self.tip=It removes only the self generated backups
+core.config.backup.remove_method.all=All
+core.config.backup.remove_method.all.tip=It removes all existing backups on the disc
core.config.backup.uninstall=Before uninstalling
core.config.backup.upgrade=Before upgrading
core.config.backup=Enabled
@@ -408,6 +413,7 @@ publisher.verified=verified
publisher=publisher
repository=repository
screenshots.bt_back.label=previous
+screenshots.bt_close=Close
screenshots.bt_next.label=next
screenshots.download.no_content=No content to display
screenshots.download.no_response=Image not found
diff --git a/bauh/view/resources/locale/es b/bauh/view/resources/locale/es
index 6278a5423..87ddf223b 100644
--- a/bauh/view/resources/locale/es
+++ b/bauh/view/resources/locale/es
@@ -45,7 +45,7 @@ Ukraine=Ucrania
United Kingdom=Reino Unido
United States=Estados Unidos
action.backup.error.create=No fue posible generar una nueva copia de seguridad
-action.backup.error.delete=No fue posible eliminar las copias de seguridad antiguas del sistema
+action.backup.error.delete=No fue posible eliminar todas las copias de seguridad antiguas del sistema
action.backup.error.proceed=¿Continuar de todos modos?
action.backup.invalid_mode=Modo de copia de seguridad inválido
action.backup.msg=¿Desea generar una copia de seguridad del sistema antes de continuar?
@@ -211,6 +211,11 @@ core.config.backup.mode.incremental=Incremental
core.config.backup.mode.incremental.tip=Se generará una nueva copia de seguridad del sistema que contenga solo los archivos modificados desde la última copia.
core.config.backup.mode.only_one=Única
core.config.backup.mode.only_one.tip=Solo se guardará una copia de seguridad del sistema. Las copias preexistentes serán borradas.
+core.config.backup.remove_method=Eliminar
+core.config.backup.remove_method.self=Solo generadas
+core.config.backup.remove_method.self.tip=Elimina solo las copias generadas por la aplicación
+core.config.backup.remove_method.all=Todas
+core.config.backup.remove_method.all.tip=Elimina todas las copias de seguridad existentes en el disco
core.config.backup.uninstall=Antes de desinstalar
core.config.backup.upgrade=Antes de actualizar
core.config.boot.load_apps=Cargar aplicaciones al inicio
@@ -408,6 +413,7 @@ publisher.verified=verificado
removing=removing
repository=repositorio
screenshots.bt_back.label=anterior
+screenshots.bt_close=Cerrar
screenshots.bt_next.label=siguiente
screenshots.download.no_content=No hay contenido para mostrar
screenshots.download.no_response=No se encontró la imagen
diff --git a/bauh/view/resources/locale/fr b/bauh/view/resources/locale/fr
index 1aa701fea..9525573f5 100644
--- a/bauh/view/resources/locale/fr
+++ b/bauh/view/resources/locale/fr
@@ -209,6 +209,11 @@ core.config.backup.mode.incremental=Incrémental
core.config.backup.mode.only_one.tip=Une seule sauvegarde sera conservée. Les précédantes seront écrasées
core.config.backup.mode.only_one=Seul
core.config.backup.mode=Mode
+core.config.backup.remove_method=Remove
+core.config.backup.remove_method.self=Only generated
+core.config.backup.remove_method.self.tip=It removes only the self generated backups
+core.config.backup.remove_method.all=All
+core.config.backup.remove_method.all.tip=It removes all existing backups on the disc
core.config.backup.uninstall=Avant de désinstaller
core.config.backup.upgrade=Avant de mettre à jour
core.config.backup=Activé
@@ -402,6 +407,7 @@ publisher.verified=vérifié
publisher=publier
repository=dépôt
screenshots.bt_back.label=précédant
+screenshots.bt_close=Fermer
screenshots.bt_next.label=suivant
screenshots.download.no_content=Rien à afficher
screenshots.download.no_response=Pas d'image
diff --git a/bauh/view/resources/locale/it b/bauh/view/resources/locale/it
index be84a0d18..db6a75c85 100644
--- a/bauh/view/resources/locale/it
+++ b/bauh/view/resources/locale/it
@@ -45,7 +45,7 @@ Ukraine=Ukraine
United Kingdom=United Kingdom
United States=United States
action.backup.error.create=It was not possible to generate a new system copy
-action.backup.error.delete=It was not possible to delete the old system copies
+action.backup.error.delete=It was not possible to delete all the old system copies
action.backup.error.proceed=Proceed anyway ?
action.backup.invalid_mode=Invalid backup mode
action.backup.msg=Do you want to generate a system copy before proceeding ?
@@ -210,6 +210,11 @@ core.config.backup.mode.incremental=Incremental
core.config.backup.mode.incremental.tip=A new system backup will be generated containing only the changed files since the latest copy.
core.config.backup.mode.only_one=Single
core.config.backup.mode.only_one.tip=Only one system backup will be kept. Pre-existing backups will be erased.
+core.config.backup.remove_method=Remove
+core.config.backup.remove_method.self=Only generated
+core.config.backup.remove_method.self.tip=It removes only the self generated backups
+core.config.backup.remove_method.all=All
+core.config.backup.remove_method.all.tip=It removes all existing backups on the disc
core.config.backup.uninstall=Before uninstalling
core.config.backup.upgrade=Before upgrading
core.config.boot.load_apps=Load apps after startup
@@ -408,6 +413,7 @@ publisher=editore
publisher.verified=verificato
repository=deposito
screenshots.bt_back.label=precedente
+screenshots.bt_close=Chiudere
screenshots.bt_next.label=prossimo
screenshots.download.no_content=Nessun contenuto da visualizzare
screenshots.download.no_response=Immagine non trovata
diff --git a/bauh/view/resources/locale/pt b/bauh/view/resources/locale/pt
index 2ff8566ad..d48a94037 100644
--- a/bauh/view/resources/locale/pt
+++ b/bauh/view/resources/locale/pt
@@ -45,7 +45,7 @@ Ukraine=Ucrânia
United Kingdom=Reino Unido
United States=Estados Unidos
action.backup.error.create=Não foi possível gerar uma nova cópia de segurança do sistema
-action.backup.error.delete=Não foi possível remover as cópias de segurança antigas do sistema
+action.backup.error.delete=Não foi possível remover todas as cópias de segurança antigas do sistema
action.backup.error.proceed=Continuar mesmo assim ?
action.backup.invalid_mode=Modo de cópia de segurança inválido
action.backup.msg=Você deseja gerar uma cópia de segurança do sistema antes de proceder ?
@@ -209,6 +209,11 @@ core.config.backup.mode.incremental=Incremental
core.config.backup.mode.only_one.tip=Somente uma cópia de segurança do sistema será mantida. Pré-existentes serão apagadas
core.config.backup.mode.only_one=Única
core.config.backup.mode=Modo
+core.config.backup.remove_method=Remover
+core.config.backup.remove_method.self=Somente geradas
+core.config.backup.remove_method.self.tip=Remove somente as cópia geradas pela aplicação
+core.config.backup.remove_method.all=Todas
+core.config.backup.remove_method.all.tip=Remove todas as cópias existentes no disco
core.config.backup.uninstall=Antes de desinstalar
core.config.backup.upgrade=Antes de atualizar
core.config.backup=Habilitada
@@ -406,6 +411,7 @@ publisher.verified=verificado
publisher=publicador
repository=repositório
screenshots.bt_back.label=anterior
+screenshots.bt_close=Fechar
screenshots.bt_next.label=próxima
screenshots.download.no_content=Sem conteúdo para exibir
screenshots.download.no_response=Imagem não encontrada
diff --git a/bauh/view/resources/locale/ru b/bauh/view/resources/locale/ru
index d9a834e7d..b975dfc49 100644
--- a/bauh/view/resources/locale/ru
+++ b/bauh/view/resources/locale/ru
@@ -45,7 +45,7 @@ Ukraine=Украина
United Kingdom=Великобритания
United States=Соединённые Штаты Америки
action.backup.error.create=It was not possible to generate a new system copy
-action.backup.error.delete=It was not possible to delete the old system copies
+action.backup.error.delete=It was not possible to delete all the old system copies
action.backup.error.proceed=Proceed anyway ?
action.backup.invalid_mode=Invalid backup mode
action.backup.msg=Do you want to generate a system copy before proceeding ?
@@ -210,6 +210,11 @@ core.config.backup.mode.incremental=Incremental
core.config.backup.mode.incremental.tip=A new system backup will be generated containing only the changed files since the latest copy.
core.config.backup.mode.only_one=Single
core.config.backup.mode.only_one.tip=Only one system backup will be kept. Pre-existing backups will be erased.
+core.config.backup.remove_method=Remove
+core.config.backup.remove_method.self=Only generated
+core.config.backup.remove_method.self.tip=It removes only the self generated backups
+core.config.backup.remove_method.all=All
+core.config.backup.remove_method.all.tip=It removes all existing backups on the disc
core.config.backup.uninstall=Before uninstalling
core.config.backup.upgrade=Before upgrading
core.config.boot.load_apps=Load apps after startup
@@ -405,6 +410,7 @@ publisher=издатель
publisher.verified=Проверенный
repository=Репозиторий
screenshots.bt_back.label=Предыдущий
+screenshots.bt_close=закрыть
screenshots.bt_next.label=Следующий
screenshots.download.no_content=Нет материалов для отображени
screenshots.download.no_response=Изображение не найдено
diff --git a/bauh/view/resources/locale/tr b/bauh/view/resources/locale/tr
index f8e92e3a5..1dce6f53b 100644
--- a/bauh/view/resources/locale/tr
+++ b/bauh/view/resources/locale/tr
@@ -209,6 +209,11 @@ core.config.backup.mode.incremental=Artan
core.config.backup.mode.only_one.tip=Yalnızca bir sistem yedeklemesi tutulur. Önceden var olan yedeklemeler silinecek.
core.config.backup.mode.only_one=Tekil
core.config.backup.mode=Mod
+core.config.backup.remove_method=Remove
+core.config.backup.remove_method.self=Only generated
+core.config.backup.remove_method.self.tip=It removes only the self generated backups
+core.config.backup.remove_method.all=All
+core.config.backup.remove_method.all.tip=It removes all existing backups on the disc
core.config.backup.uninstall=Önce kaldırılıyor
core.config.backup.upgrade=Önce yükseltiliyor
core.config.backup=Etkin
@@ -405,6 +410,7 @@ publisher.verified=doğrula
publisher=yayımcı
repository=depo
screenshots.bt_back.label=önceki
+screenshots.bt_close=Kapatmak
screenshots.bt_next.label=sonraki
screenshots.download.no_content=Görüntülenecek içerik yok
screenshots.download.no_response=İmaj bulunamadı
diff --git a/bauh/view/resources/style/default/default.qss b/bauh/view/resources/style/default/default.qss
index 0f2cb3130..6c71d578b 100644
--- a/bauh/view/resources/style/default/default.qss
+++ b/bauh/view/resources/style/default/default.qss
@@ -391,6 +391,10 @@ ScreenshotsDialog QAbstractButton[control = "true"] {
min-width: 100px;
}
+ScreenshotsDialog QPushButton#close {
+ min-width: 25px;
+}
+
SettingsWindow TabGroupQt#settings {
min-width: 400px;
}
diff --git a/tests/gems/debian/test_aptitude.py b/tests/gems/debian/test_aptitude.py
index 37ba53854..a8a5a93a3 100644
--- a/tests/gems/debian/test_aptitude.py
+++ b/tests/gems/debian/test_aptitude.py
@@ -35,8 +35,7 @@ def test_search__must_return_installed_and_not_installed_packages_with_updates(s
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=''))
+ execute.assert_called_once_with(f"aptitude search {query} -q -F '%p^%v^%V^%m^%s^%d' --disable-columns", shell=True)
exp = [
DebianPackage(name='gimp-cbmplugs', version='1.2.2-1build1', latest_version='1.2.2-1build1',
diff --git a/tests/gems/web/__init__.py b/tests/gems/web/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/gems/web/test_controller.py b/tests/gems/web/test_controller.py
new file mode 100644
index 000000000..442ffe248
--- /dev/null
+++ b/tests/gems/web/test_controller.py
@@ -0,0 +1,47 @@
+from unittest import TestCase
+from unittest.mock import Mock, patch
+
+from bauh.gems.web.controller import DEFAULT_LANGUAGE_HEADER
+from bauh.gems.web.controller import WebApplicationManager
+
+
+class ControllerTest(TestCase):
+
+ def test_DEFAULT_LANGUAGE_HEADER(self):
+ self.assertEqual('en-US, en', DEFAULT_LANGUAGE_HEADER)
+
+
+class WebApplicationManagerTest(TestCase):
+
+ def setUp(self):
+ self.manager = WebApplicationManager(context=Mock())
+
+ @patch('locale.getdefaultlocale', side_effect=Exception)
+ def test_get_accept_language_header__must_return_default_locale_when_exception_raised(self, getdefaultlocale: Mock):
+ returned = self.manager.get_accept_language_header()
+ self.assertEqual(DEFAULT_LANGUAGE_HEADER, returned)
+ getdefaultlocale.assert_called_once()
+
+ @patch('locale.getdefaultlocale', return_value=None)
+ def test_get_accept_language_header__must_return_default_locale_when_no_locale_is_returned(self, getdefaultlocale: Mock):
+ returned = self.manager.get_accept_language_header()
+ self.assertEqual(DEFAULT_LANGUAGE_HEADER, returned)
+ getdefaultlocale.assert_called_once()
+
+ @patch('locale.getdefaultlocale', return_value=['es_AR'])
+ def test_get_accept_language_header__must_return_the_system_locale_without_underscore_plus_default_locale(self, getdefaultlocale: Mock):
+ returned = self.manager.get_accept_language_header()
+ self.assertEqual(f'es-AR, es, {DEFAULT_LANGUAGE_HEADER}', returned)
+ getdefaultlocale.assert_called_once()
+
+ @patch('locale.getdefaultlocale', return_value=['es'])
+ def test_get_accept_language_header__must_return_the_simple_system_locale_plus_default_locale(self, getdefaultlocale: Mock):
+ returned = self.manager.get_accept_language_header()
+ self.assertEqual(f'es, {DEFAULT_LANGUAGE_HEADER}', returned)
+ getdefaultlocale.assert_called_once()
+
+ @patch('locale.getdefaultlocale', return_value=['en_IN'])
+ def test_get_accept_language_header__must_not_concatenate_default_locale_if_system_locale_has_it(self, getdefaultlocale: Mock):
+ returned = self.manager.get_accept_language_header()
+ self.assertEqual(f'en-IN, en', returned)
+ getdefaultlocale.assert_called_once()