This commit is contained in:
Vinícius Moreira
2022-11-05 15:07:04 -03:00
committed by GitHub
24 changed files with 321 additions and 115 deletions

View File

@@ -4,6 +4,36 @@ 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.4] 2022-11-05
### Improvements
- Arch
- replaced some system calls by Python calls
### Fixes
- AppImage
- some desktop entries not being displayed on the desktop environment menu (requires the AppImage to be reinstalled) [#287](https://github.com/vinifmor/bauh/issues/287)
- Arch
- not detecting some package replacements when upgrading due to conflict information having logic operators (e.g: `at-spi2-core: 2.44.1 -> 2.46.0` should replace the installed `at-spi2-atk: 2.38`)
- not considering some conflict expressions when retrieving upgrade requirements (it could lead to a system breakage depending on the conflict)
- Debian
- not properly handling packages with names ending with `:i386` [#298](https://github.com/vinifmor/bauh/issues/298)
- Packaging
- AppImage: download certificate issue [#280](https://github.com/vinifmor/bauh/issues/280)
- GUI
- initialization panel size not based on the current display device size
- management panel:
- not fully maximizing
- not maximizing based on the current display device size (for multiple display setups)
- not respecting a minimal/maximum width based on the current display device size (for multiple display setup)
- dialogs not respecting the current display device width
- not readjusting size after maximizing/minimizing
### Localization (i18n)
- Italian ([@albanobattistella](https://github.com/albanobattistella), [@luca-digrazia](https://github.com/luca-digrazia))
- Turkish ([@agahemir](https://github.com/agahemir))
## [0.10.3] 2022-05-30
### Features

View File

@@ -554,6 +554,7 @@ the `view` code is only attached to them (it does not know how the `gems` handle
- Separate modules for each packaging technology
- Memory and performance improvements
- Improve user experience
- The current development changes can be checked [here](https://github.com/vinifmor/bauh/blob/staging/CHANGELOG.md)
#### <a name="donations">Donations</a>
- You can support this project through [ko-fi](https://ko-fi.com/vinifmor).

View File

@@ -1,4 +1,4 @@
__version__ = '0.10.3'
__version__ = '0.10.4'
__app_name__ = 'bauh'
import os

View File

@@ -528,13 +528,13 @@ class AppImageManager(SoftwareManager, SettingsController):
finally:
releases_con.close()
def _find_desktop_file(self, folder: str) -> str:
def _find_desktop_file(self, folder: str) -> Optional[str]:
for r, d, files in os.walk(folder):
for f in files:
if f.endswith('.desktop'):
return f
def _find_icon_file(self, folder: str) -> str:
def _find_icon_file(self, folder: str) -> Optional[str]:
for f in glob.glob(folder + ('/**' if not folder.endswith('/') else '**'), recursive=True):
if RE_ICON_ENDS_WITH.match(f):
return f

View File

@@ -1,15 +1,16 @@
import os
import re
from typing import Optional
RE_DESKTOP_EXEC = re.compile(r'(Exec\s*=(.+)(\n?))')
RE_DESKTOP_EXEC = re.compile(r'(\n?\s*\w*Exec\s*=(.+))')
RE_MANY_SPACES = re.compile(r'\s+')
def find_appimage_file(folder: str) -> str:
def find_appimage_file(folder: str) -> Optional[str]:
for r, d, files in os.walk(folder):
for f in files:
if f.lower().endswith('.appimage'):
return '{}/{}'.format(folder, f)
return f'{folder}/{f}'
def replace_desktop_entry_exec_command(desktop_entry: str, appname: str, file_path: str) -> str:
@@ -23,6 +24,11 @@ def replace_desktop_entry_exec_command(desktop_entry: str, appname: str, file_pa
for exec_groups in execs:
full_match = exec_groups[0]
if full_match.strip().startswith("TryExec"): # TryExec cause issues in some DE to display the app icon
final_entry = final_entry.replace(full_match, "")
continue
cmd = RE_MANY_SPACES.sub(' ', exec_groups[1].strip())
if cmd:
words = cmd.split(' ')
@@ -30,12 +36,12 @@ def replace_desktop_entry_exec_command(desktop_entry: str, appname: str, file_pa
for idx in range(len(words)):
if words[idx].lower() == treated_name:
words[idx] = '"{}"'.format(file_path)
words[idx] = f'"{file_path}"'
changed = True
break
if not changed:
words = ['"{}"'.format(file_path)]
words = [f'"{file_path}"']
final_entry = final_entry.replace(full_match, full_match.replace(exec_groups[1], ' '.join(words)))

View File

@@ -2,7 +2,7 @@ import re
import traceback
from logging import Logger
from threading import Thread
from typing import Set, List, Tuple, Dict, Iterable, Optional, Generator
from typing import Set, List, Tuple, Dict, Iterable, Optional, Generator, Pattern
from bauh.api.abstract.handler import ProcessWatcher
from bauh.gems.arch import pacman, message, sorting, confirmation
@@ -14,12 +14,20 @@ from bauh.view.util.translation import I18n
class DependenciesAnalyser:
_re_dep_operator: Optional[Pattern] = None
def __init__(self, aur_client: AURClient, i18n: I18n, logger: Logger):
self.aur_client = aur_client
self.i18n = i18n
self.re_dep_operator = re.compile(r'([<>=]+)')
self._log = logger
@classmethod
def re_dep_operator(cls) -> Pattern:
if not cls._re_dep_operator:
cls._re_dep_operator = re.compile(r'([<>=]+)')
return cls._re_dep_operator
def _fill_repository(self, name: str, output: List[Tuple[str, str]]):
repository = pacman.read_repository_from_info(name)
@@ -225,7 +233,7 @@ class DependenciesAnalyser:
raise Exception(f"Could not retrieve information from providers: "
f"{', '.join(data_not_found)}")
split_informed_dep = self.re_dep_operator.split(dep_exp)
split_informed_dep = self.re_dep_operator().split(dep_exp)
version_required = split_informed_dep[2]
exp_op = split_informed_dep[1] if split_informed_dep[1] != '=' else '=='
@@ -237,7 +245,7 @@ class DependenciesAnalyser:
info = missing_providers_data[p]
for provided_exp in info['p']:
split_dep = self.re_dep_operator.split(provided_exp)
split_dep = self.re_dep_operator().split(provided_exp)
if len(split_dep) == 3 and split_dep[0] == dep_name:
version_provided = split_dep[2]
@@ -253,7 +261,7 @@ class DependenciesAnalyser:
return
else:
for _, dep_data in self.aur_client.gen_updates_data((dep_name,)):
split_informed_dep = self.re_dep_operator.split(dep_exp)
split_informed_dep = self.re_dep_operator().split(dep_exp)
version_required = split_informed_dep[2]
exp_op = split_informed_dep[1].strip()
@@ -270,7 +278,7 @@ class DependenciesAnalyser:
if dep_name == dep_exp:
version_required, exp_op = None, None
else:
split_informed_dep = self.re_dep_operator.split(dep_exp)
split_informed_dep = self.re_dep_operator().split(dep_exp)
version_required = split_informed_dep[2]
exp_op = split_informed_dep[1] if split_informed_dep[1] != '=' else '=='
@@ -405,7 +413,7 @@ class DependenciesAnalyser:
if dep in pkgs_data:
continue
if dep not in provided_map:
dep_split = self.re_dep_operator.split(dep)
dep_split = self.re_dep_operator().split(dep)
dep_name = dep_split[0].strip()
if dep_name not in deps_checked:

View File

@@ -5,7 +5,7 @@ import shutil
import traceback
from io import StringIO
from threading import Thread
from typing import List, Set, Tuple, Dict, Iterable, Optional, Any
from typing import List, Set, Tuple, Dict, Iterable, Optional, Any, Pattern, Collection
from colorama import Fore
@@ -26,6 +26,7 @@ RE_REMOVE_TRANSITIVE_DEPS = re.compile(r'removing\s([\w\-_]+)\s.+required\sby\s(
RE_AVAILABLE_MIRRORS = re.compile(r'.+\s+OK\s+.+\s+(\d+:\d+)\s+.+(http.+)')
RE_PACMAN_SYNC_FIRST = re.compile(r'SyncFirst\s*=\s*(.+)')
RE_DESKTOP_FILES = re.compile(r'\n?([\w\-_]+)\s+(/usr/share/.+\.desktop)')
RE_IGNORED_PACKAGES: Optional[Pattern] = None
def is_available() -> bool:
@@ -234,22 +235,27 @@ def sign_key(key: str, root_password: Optional[str]) -> SystemProcess:
def list_ignored_packages(config_path: str = '/etc/pacman.conf') -> Set[str]:
ignored = set()
try:
pacman_conf = new_subprocess(['cat', config_path])
grep = new_subprocess(['grep', '-Eo', r'\s*#*\s*ignorepkg\s*=\s*.+'], stdin=pacman_conf.stdout)
for o in grep.stdout:
if o:
line = o.decode().strip()
with open(config_path, 'r') as f:
file_content = f.read()
if not line.startswith('#'):
ignored.add(line.split('=')[1].strip())
global RE_IGNORED_PACKAGES
pacman_conf.terminate()
grep.terminate()
return ignored
except:
if not RE_IGNORED_PACKAGES:
RE_IGNORED_PACKAGES = re.compile(r'[\s#]*ignorepkg\s*=\s*.+', re.IGNORECASE)
for raw_line in RE_IGNORED_PACKAGES.findall(file_content):
line = raw_line.strip()
if line and not line.startswith("#"):
ignored.add(line.split("=")[1].strip())
except (FileNotFoundError, OSError):
pass
except Exception:
traceback.print_exc()
return ignored
return ignored
def check_missing(names: Set[str]) -> Set[str]:
@@ -1075,3 +1081,18 @@ def map_available_packages() -> Optional[Dict[str, Any]]:
'r': package_data[0].strip(),
'i': len(package_data) == 4 and 'installed' in package_data[3]}
return res
def map_installed(pkgs: Optional[Collection[str]] = None) -> Optional[Dict[str, str]]:
output = run_cmd(f"pacman -Q {' '.join({*pkgs} if pkgs else '')}".strip(), print_error=False)
if output:
res = dict()
for raw_line in output.split("\n"):
line = raw_line.strip()
if line:
pkg_version = line.split(" ")
if len(pkg_version) == 2:
res[pkg_version[0]] = pkg_version[1]
return res

View File

@@ -22,7 +22,7 @@ class UpdateRequirementsContext:
aur_to_update: Dict[str, ArchPackage], repo_to_install: Dict[str, ArchPackage],
aur_to_install: Dict[str, ArchPackage], to_install: Dict[str, ArchPackage],
pkgs_data: Dict[str, dict], cannot_upgrade: Dict[str, UpgradeRequirement],
to_remove: Dict[str, UpgradeRequirement], installed_names: Set[str], provided_map: Dict[str, Set[str]],
to_remove: Dict[str, UpgradeRequirement], installed_names: Dict[str, str], provided_map: Dict[str, Set[str]],
aur_index: Set[str], arch_config: dict, remote_provided_map: Dict[str, Set[str]], remote_repo_map: Dict[str, str],
root_password: Optional[str], aur_supported: bool):
self.to_update = to_update
@@ -33,7 +33,7 @@ class UpdateRequirementsContext:
self.pkgs_data = pkgs_data
self.cannot_upgrade = cannot_upgrade
self.root_password = root_password
self.installed_names = installed_names
self.installed = installed_names
self.provided_map = provided_map
self.to_remove = to_remove
self.to_install = to_install
@@ -149,12 +149,21 @@ class UpdatesSummarizer:
for p, data in context.pkgs_data.items():
if data['c']:
for c in data['c']:
if c and c != p and c in context.installed_names:
# source = provided_map[c]
root_conflict[c] = p
if c:
name_op_exp = DependenciesAnalyser.re_dep_operator().split(c)
conflict_name = name_op_exp[0]
if (p, c) in root_conflict.items():
mutual_conflicts[c] = p
if conflict_name != p:
conflict_version = context.installed.get(conflict_name)
if conflict_version:
if len(name_op_exp) == 1 or match_required_version(conflict_version,
name_op_exp[1],
name_op_exp[2]):
root_conflict[conflict_name] = p
if (p, conflict_name) in root_conflict.items():
mutual_conflicts[conflict_name] = p
if mutual_conflicts:
for pkg1, pkg2 in mutual_conflicts.items():
@@ -278,8 +287,8 @@ class UpdatesSummarizer:
ti = time.time()
self.logger.info("Filling provided names")
if not context.installed_names:
context.installed_names = pacman.list_installed_names()
if not context.installed:
context.installed = pacman.map_installed()
installed_to_ignore = set()
@@ -305,8 +314,8 @@ class UpdatesSummarizer:
if len(split_provided) > 1 and split_provided[0] != p:
pacman.fill_provided_map(split_provided[0], pkgname, context.provided_map)
if installed_to_ignore: # filling the provided names of the installed
installed_to_query = context.installed_names.difference(installed_to_ignore)
if context.installed and installed_to_ignore: # filling the provided names of the installed
installed_to_query = {*context.installed}.difference(installed_to_ignore)
if installed_to_query:
context.provided_map.update(pacman.map_provided(remote=False, pkgs=installed_to_query))
@@ -374,7 +383,7 @@ class UpdatesSummarizer:
remote_repo_map = pacman.map_repositories()
context = UpdateRequirementsContext(to_update={}, repo_to_update={}, aur_to_update={}, repo_to_install={},
aur_to_install={}, to_install={}, pkgs_data={}, cannot_upgrade={},
to_remove={}, installed_names=set(), provided_map={}, aur_index=set(),
to_remove={}, installed_names=dict(), provided_map={}, aur_index=set(),
arch_config=arch_config, root_password=root_password,
remote_provided_map=remote_provided_map, remote_repo_map=remote_repo_map,
aur_supported=self.aur_supported)
@@ -604,7 +613,7 @@ class UpdatesSummarizer:
for r in reqs:
if r in transaction_pkgs:
reqs_in_transaction.add(r)
elif r in context.installed_names:
elif r in context.installed:
reqs_not_in_transaction.add(r)
if not reqs_not_in_transaction and not reqs_in_transaction:

View File

@@ -241,7 +241,7 @@ class Aptitude:
@property
def re_transaction_pkg(self) -> Pattern:
if self._re_transaction_pkg is None:
self._re_transaction_pkg = re.compile(r'([a-zA-Z0-9\-_@~.+]+)({\w+})?\s*\[([a-zA-Z0-9\-_@~.+:]+)'
self._re_transaction_pkg = re.compile(r'([a-zA-Z0-9\-_@~.+:]+)({\w+})?\s*\[([a-zA-Z0-9\-_@~.+:]+)'
r'(\s+->\s+([a-zA-Z0-9\-_@~.+:]+))?](\s*<([\-+]?[0-9.,]+\s+\w+)>)?')
return self._re_transaction_pkg

View File

@@ -76,7 +76,6 @@ def new_manage_panel(app_args: Namespace, app_config: dict, logger: logging.Logg
manage_window = ManageWindow(i18n=i18n,
manager=manager,
icon_cache=icon_cache,
screen_size=screen_size,
config=app_config,
context=context,
http_client=http_client,
@@ -84,8 +83,7 @@ def new_manage_panel(app_args: Namespace, app_config: dict, logger: logging.Logg
force_suggestions=force_suggestions,
logger=logger)
prepare = PreparePanel(screen_size=screen_size,
context=context,
prepare = PreparePanel(context=context,
manager=manager,
i18n=i18n,
manage_window=manage_window,

View File

@@ -86,7 +86,7 @@ class AdaptableFileDownloader(FileDownloader):
return SimpleProcess(cmd=cmd, cwd=cwd, root_password=root_password)
def _get_wget_process(self, url: str, output_path: str, cwd: str, root_password: Optional[str]) -> SimpleProcess:
cmd = ['wget', url, '-c', '--retry-connrefused', '-t', '10', '--no-config', '-nc']
cmd = ['wget', url, '-c', '--retry-connrefused', '-t', '10', '-nc']
if not self.check_ssl:
cmd.append('--no-check-certificate')

View File

@@ -15,6 +15,7 @@ from bauh.api.abstract.model import PackageStatus, CustomSoftwareAction
from bauh.commons.html import strip_html, bold
from bauh.view.qt.components import IconButton, QCustomMenuAction, QCustomToolbar
from bauh.view.qt.dialog import ConfirmationDialog
from bauh.view.qt.qt_utils import get_current_screen_geometry
from bauh.view.qt.view_model import PackageView
from bauh.view.util.translation import I18n
@@ -68,9 +69,8 @@ class PackagesTable(QTableWidget):
COL_NUMBER = 9
DEFAULT_ICON_SIZE = QSize(16, 16)
def __init__(self, parent: QWidget, icon_cache: MemoryCache, download_icons: bool, screen_width: int):
def __init__(self, parent: QWidget, icon_cache: MemoryCache, download_icons: bool):
super(PackagesTable, self).__init__()
self.screen_width = screen_width
self.setObjectName('table_packages')
self.setParent(parent)
self.window = parent
@@ -180,15 +180,16 @@ class PackagesTable(QTableWidget):
action=custom_action)
def refresh(self, pkg: PackageView):
self._update_row(pkg, update_check_enabled=False, change_update_col=False)
screen_width = get_current_screen_geometry(self.parent()).width()
self._update_row(pkg, screen_width, update_check_enabled=False, change_update_col=False)
def update_package(self, pkg: PackageView, change_update_col: bool = False):
def update_package(self, pkg: PackageView, screen_width: int, change_update_col: bool = False):
if self.download_icons and pkg.model.icon_url:
icon_request = QNetworkRequest(QUrl(pkg.model.icon_url))
icon_request.setAttribute(QNetworkRequest.FollowRedirectsAttribute, True)
self.network_man.get(icon_request)
self._update_row(pkg, change_update_col=change_update_col)
self._update_row(pkg, screen_width, change_update_col=change_update_col)
def _uninstall(self, pkg: PackageView):
if ConfirmationDialog(title=self.i18n['manage_window.apps_table.row.actions.uninstall.popup.title'],
@@ -255,6 +256,7 @@ class PackagesTable(QTableWidget):
self.setEnabled(True)
if pkgs:
screen_width = get_current_screen_geometry(self.parent()).width()
self.setColumnCount(self.COL_NUMBER if update_check_enabled else self.COL_NUMBER - 1)
self.setRowCount(len(pkgs))
@@ -266,16 +268,17 @@ class PackagesTable(QTableWidget):
icon_request.setAttribute(QNetworkRequest.FollowRedirectsAttribute, True)
self.network_man.get(icon_request)
self._update_row(pkg, update_check_enabled)
self._update_row(pkg, screen_width, update_check_enabled)
self.scrollToTop()
def _update_row(self, pkg: PackageView, update_check_enabled: bool = True, change_update_col: bool = True):
def _update_row(self, pkg: PackageView, screen_width: int,
update_check_enabled: bool = True, change_update_col: bool = True):
self._set_col_icon(0, pkg)
self._set_col_name(1, pkg)
self._set_col_version(2, pkg)
self._set_col_description(3, pkg)
self._set_col_publisher(4, pkg)
self._set_col_name(1, pkg, screen_width)
self._set_col_version(2, pkg, screen_width)
self._set_col_description(3, pkg, screen_width)
self._set_col_publisher(4, pkg, screen_width)
self._set_col_type(5, pkg)
self._set_col_installed(6, pkg)
self._set_col_actions(7, pkg)
@@ -355,7 +358,7 @@ class PackagesTable(QTableWidget):
col_type_icon.setToolTip(icon_data['tip'])
self.setCellWidget(pkg.table_index, col, col_type_icon)
def _set_col_version(self, col: int, pkg: PackageView):
def _set_col_version(self, col: int, pkg: PackageView, screen_width: int):
label_version = QLabel(str(pkg.model.version if pkg.model.version else '?'))
label_version.setObjectName('app_version')
label_version.setAlignment(Qt.AlignCenter)
@@ -383,7 +386,7 @@ class PackagesTable(QTableWidget):
f"{self.i18n['version.latest']}: {pkg.model.latest_version})"
label_version.setText(f"{label_version.text()} > {pkg.model.latest_version}")
if label_version.sizeHint().width() / self.screen_width > 0.22:
if label_version.sizeHint().width() / screen_width > 0.22:
label_version.setText(pkg.model.latest_version)
item.setToolTip(tooltip)
@@ -426,7 +429,7 @@ class PackagesTable(QTableWidget):
self._update_icon(col_icon, icon)
self.setCellWidget(pkg.table_index, col, col_icon)
def _set_col_name(self, col: int, pkg: PackageView):
def _set_col_name(self, col: int, pkg: PackageView, screen_width: int):
col_name = QLabel()
col_name.setObjectName('app_name')
col_name.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Preferred)
@@ -439,7 +442,7 @@ class PackagesTable(QTableWidget):
col_name.setToolTip(self.i18n['app.name'].lower())
col_name.setText(name)
screen_perc = col_name.sizeHint().width() / self.screen_width
screen_perc = col_name.sizeHint().width() / screen_width
if screen_perc > 0.15:
max_chars = int(len(name) * 0.15 / screen_perc) - 3
@@ -454,7 +457,7 @@ class PackagesTable(QTableWidget):
sizes = icon.availableSizes()
return sizes[-1] if sizes else self.DEFAULT_ICON_SIZE
def _set_col_description(self, col: int, pkg: PackageView):
def _set_col_description(self, col: int, pkg: PackageView, screen_width: int):
item = QLabel()
item.setObjectName('app_description')
@@ -468,7 +471,7 @@ class PackagesTable(QTableWidget):
item.setText(desc)
current_width_perc = item.sizeHint().width() / self.screen_width
current_width_perc = item.sizeHint().width() / screen_width
if current_width_perc > 0.18:
max_width = int(len(desc) * 0.18 / current_width_perc) - 3
desc = desc[0:max_width] + '...'
@@ -479,7 +482,7 @@ class PackagesTable(QTableWidget):
self.setCellWidget(pkg.table_index, col, item)
def _set_col_publisher(self, col: int, pkg: PackageView):
def _set_col_publisher(self, col: int, pkg: PackageView, screen_width: int):
item = QToolBar()
publisher = pkg.model.get_publisher()
@@ -494,7 +497,7 @@ class PackagesTable(QTableWidget):
if publisher:
lb_name.setText(publisher)
screen_perc = lb_name.sizeHint().width() / self.screen_width
screen_perc = lb_name.sizeHint().width() / screen_width
if screen_perc > 0.12:
max_chars = int(len(publisher) * 0.12 / screen_perc) - 3

View File

@@ -1,6 +1,6 @@
from collections.abc import Iterable
from PyQt5.QtCore import QSize, Qt
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QIcon, QCursor
from PyQt5.QtWidgets import QDialog, QVBoxLayout, QGroupBox, \
QLineEdit, QLabel, QGridLayout, QPushButton, QPlainTextEdit, QScrollArea, QFrame, QWidget, QSizePolicy, \
@@ -8,6 +8,7 @@ from PyQt5.QtWidgets import QDialog, QVBoxLayout, QGroupBox, \
from bauh.api.abstract.cache import MemoryCache
from bauh.view.qt.components import new_spacer
from bauh.view.qt.qt_utils import get_current_screen_geometry
from bauh.view.util.translation import I18n
IGNORED_ATTRS = {'name', '__app__'}
@@ -15,10 +16,9 @@ IGNORED_ATTRS = {'name', '__app__'}
class InfoDialog(QDialog):
def __init__(self, pkg_info: dict, icon_cache: MemoryCache, i18n: I18n, screen_size: QSize):
def __init__(self, pkg_info: dict, icon_cache: MemoryCache, i18n: I18n):
super(InfoDialog, self).__init__()
self.setWindowTitle(str(pkg_info['__app__']))
self.screen_size = screen_size
self.i18n = i18n
layout = QVBoxLayout()
self.setLayout(layout)
@@ -108,7 +108,9 @@ class InfoDialog(QDialog):
lower_container.layout().addWidget(self.bt_close)
layout.addWidget(lower_container)
self.setMinimumWidth(int(self.gbox_info.sizeHint().width() * 1.2))
self.setMaximumHeight(int(screen_size.height() * 0.8))
screen_height = get_current_screen_geometry().height()
self.setMaximumHeight(int(screen_height * 0.8))
self.adjustSize()
def _gen_show_button(self, idx: int, val):

View File

@@ -15,7 +15,7 @@ from bauh.api.abstract.controller import SoftwareManager, SoftwareAction
from bauh.api.abstract.handler import TaskManager
from bauh.api import user
from bauh.view.qt.components import new_spacer, QCustomToolbar
from bauh.view.qt.qt_utils import centralize
from bauh.view.qt.qt_utils import centralize, get_current_screen_geometry
from bauh.view.qt.root import RootDialog
from bauh.view.qt.thread import AnimateProgress
from bauh.view.util.translation import I18n
@@ -145,7 +145,7 @@ class PreparePanel(QWidget, TaskManager):
signal_status = pyqtSignal(int)
signal_password_response = pyqtSignal(bool, str)
def __init__(self, context: ApplicationContext, manager: SoftwareManager, screen_size: QSize,
def __init__(self, context: ApplicationContext, manager: SoftwareManager,
i18n: I18n, manage_window: QWidget, app_config: dict, force_suggestions: bool = False):
super(PreparePanel, self).__init__(flags=Qt.CustomizeWindowHint | Qt.WindowTitleHint)
self.i18n = i18n
@@ -153,9 +153,7 @@ class PreparePanel(QWidget, TaskManager):
self.app_config = app_config
self.manage_window = manage_window
self.setWindowTitle('{} ({})'.format(__app_name__, self.i18n['prepare_panel.title.start'].lower()))
self.setMinimumWidth(int(screen_size.width() * 0.5))
self.setMinimumHeight(int(screen_size.height() * 0.35))
self.setMaximumHeight(int(screen_size.height() * 0.95))
self.setLayout(QVBoxLayout())
self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
self.manager = manager
@@ -292,6 +290,10 @@ class PreparePanel(QWidget, TaskManager):
super(PreparePanel, self).show()
self.prepare_thread.start()
centralize(self)
screen_size = get_current_screen_geometry()
self.setMinimumWidth(int(screen_size.width() * 0.5))
self.setMinimumHeight(int(screen_size.height() * 0.35))
self.setMaximumHeight(int(screen_size.height() * 0.95))
def start(self, tasks: int):
self.started_at = time.time()

View File

@@ -1,16 +1,18 @@
from PyQt5.QtCore import Qt
from typing import Optional, Union
from PyQt5.QtCore import Qt, QRect, QPoint
from PyQt5.QtGui import QIcon, QPixmap
from PyQt5.QtWidgets import QWidget, QApplication
from PyQt5.QtWidgets import QWidget, QApplication, QDesktopWidget
from bauh.view.util import resource
desktop: Optional[QDesktopWidget] = None
def centralize(widget: QWidget):
geo = widget.frameGeometry()
screen = QApplication.desktop().screenNumber(QApplication.desktop().cursor().pos())
center_point = QApplication.desktop().screenGeometry(screen).center()
geo.moveCenter(center_point)
widget.move(geo.topLeft())
screen_geometry = get_current_screen_geometry()
widget.frameGeometry().moveCenter(screen_geometry.center())
widget.move(widget.frameGeometry().topLeft())
def load_icon(path: str, width: int, height: int = None) -> QIcon:
@@ -27,3 +29,13 @@ def measure_based_on_width(percent: float) -> int:
def measure_based_on_height(percent: float) -> int:
return round(percent * QApplication.primaryScreen().size().height())
def get_current_screen_geometry(source_widget: Optional[Union[QWidget, QPoint]] = None) -> QRect:
global desktop
if not desktop:
desktop = QDesktopWidget()
current_screen_idx = desktop.screenNumber(source_widget if source_widget else desktop.cursor().pos())
return desktop.screen(current_screen_idx).geometry()

View File

@@ -29,6 +29,7 @@ from bauh.view.core import timeshift
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.qt_utils import get_current_screen_geometry
from bauh.view.qt.view_model import PackageView, PackageViewStatus
from bauh.view.util.translation import I18n
@@ -231,12 +232,12 @@ class UpgradeSelected(AsyncAction):
SUMMARY_FILE = UPGRADE_LOGS_DIR + '/{}_summary.txt'
def __init__(self, manager: SoftwareManager, internet_checker: InternetChecker, i18n: I18n,
screen_width: int, pkgs: List[PackageView] = None):
parent_widget: QWidget, pkgs: List[PackageView] = None):
super(UpgradeSelected, self).__init__(i18n=i18n)
self.pkgs = pkgs
self.manager = manager
self.internet_checker = internet_checker
self.screen_width = screen_width
self._parent_widget = parent_widget
def _req_as_option(self, req: UpgradeRequirement, tooltip: bool = True, custom_tooltip: str = None, required_size: bool = True, display_sizes: bool = True,
positive_size_symbol: bool = False) -> InputOption:
@@ -495,9 +496,10 @@ class UpgradeSelected(AsyncAction):
comps.insert(0, TextComponent(f'{disc_size_text} ({download_size_text})', size=14))
comps.insert(1, TextComponent(''))
screen_width = get_current_screen_geometry(self._parent_widget).width()
if not self.request_confirmation(title=self.i18n['action.update.summary'].capitalize(), body='', components=comps,
confirmation_label=self.i18n['proceed'].capitalize(), deny_label=self.i18n['cancel'].capitalize(),
confirmation_button=can_upgrade, min_width=int(0.45 * self.screen_width)):
confirmation_button=can_upgrade, min_width=int(0.45 * screen_width)):
self.notify_finished({'success': success, 'updated': updated, 'types': updated_types, 'id': None})
self.pkgs = None
return

View File

@@ -2,12 +2,11 @@ import logging
import operator
import os.path
import time
import traceback
from pathlib import Path
from typing import List, Type, Set, Tuple, Optional
from PyQt5.QtCore import QEvent, Qt, pyqtSignal, QSize
from PyQt5.QtGui import QIcon, QWindowStateChangeEvent, QCursor
from PyQt5.QtCore import QEvent, Qt, pyqtSignal, QRect
from PyQt5.QtGui import QIcon, QWindowStateChangeEvent, QCursor, QMoveEvent
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QCheckBox, QHeaderView, QToolBar, \
QLabel, QPlainTextEdit, QProgressBar, QPushButton, QComboBox, QApplication, QListView, QSizePolicy, \
QMenu, QHBoxLayout
@@ -34,6 +33,7 @@ from bauh.view.qt.components import new_spacer, IconButton, QtComponentsManager,
from bauh.view.qt.dialog import ConfirmationDialog
from bauh.view.qt.history import HistoryDialog
from bauh.view.qt.info import InfoDialog
from bauh.view.qt.qt_utils import get_current_screen_geometry
from bauh.view.qt.root import RootDialog
from bauh.view.qt.screenshots import ScreenshotsDialog
from bauh.view.qt.settings import SettingsWindow
@@ -96,7 +96,7 @@ class ManageWindow(QWidget):
signal_table_update = pyqtSignal()
signal_stop_notifying = pyqtSignal()
def __init__(self, i18n: I18n, icon_cache: MemoryCache, manager: SoftwareManager, screen_size: QSize, config: dict,
def __init__(self, i18n: I18n, icon_cache: MemoryCache, manager: SoftwareManager, config: dict,
context: ApplicationContext, http_client: HttpClient, logger: logging.Logger, icon: QIcon,
force_suggestions: bool = False):
super(ManageWindow, self).__init__()
@@ -112,7 +112,6 @@ class ManageWindow(QWidget):
self.pkgs_installed = [] # cached installed packages
self.display_limit = config['ui']['table']['max_displayed']
self.icon_cache = icon_cache
self.screen_size = screen_size
self.config = config
self.context = context
self.http_client = http_client
@@ -279,8 +278,7 @@ class ManageWindow(QWidget):
self.table_container.layout().setContentsMargins(0, 0, 0, 0)
self.table_apps = PackagesTable(self, self.icon_cache,
download_icons=bool(self.config['download']['icons']),
screen_width=int(screen_size.width()))
download_icons=bool(self.config['download']['icons']))
self.table_apps.change_headers_policy()
self.table_container.layout().addWidget(self.table_apps)
@@ -333,7 +331,7 @@ class ManageWindow(QWidget):
self.thread_update = self._bind_async_action(UpgradeSelected(manager=self.manager, i18n=self.i18n,
internet_checker=context.internet_checker,
screen_width=screen_size.width()),
parent_widget=self),
finished_call=self._finish_upgrade_selected)
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)
@@ -433,8 +431,6 @@ class ManageWindow(QWidget):
self.container_progress.add_space()
self.layout.addWidget(self.container_progress)
qt_utils.centralize(self)
self.filter_only_apps = True
self.type_filter = self.any_type_filter
self.category_filter = self.any_category_filter
@@ -457,11 +453,8 @@ class ManageWindow(QWidget):
self.thread_load_installed = NotifyInstalledLoaded()
self.thread_load_installed.signal_loaded.connect(self._finish_loading_installed)
self.setMinimumHeight(int(screen_size.height() * 0.5))
self.setMaximumHeight(int(screen_size.height()))
self.setMinimumWidth(int(screen_size.width() * 0.5))
self.setMaximumWidth(int(screen_size.width() - screen_size.width() * 0.015))
self._register_groups()
self._screen_geometry: Optional[QRect] = None
def _register_groups(self):
common_filters = (CHECK_APPS, CHECK_UPDATES, COMBO_CATEGORIES, COMBO_TYPES, INP_NAME)
@@ -597,6 +590,8 @@ class ManageWindow(QWidget):
self.thread_warnings.start()
qt_utils.centralize(self)
self._screen_geometry = get_current_screen_geometry()
self._update_size_limits()
def verify_warnings(self):
self.thread_warnings.start()
@@ -671,7 +666,8 @@ class ManageWindow(QWidget):
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)
screen_width = get_current_screen_geometry(self).width()
self.table_apps.update_package(pkg, screen_width=screen_width)
def _reload_categories(self):
categories = set()
@@ -687,11 +683,33 @@ class ManageWindow(QWidget):
if categories:
self._update_categories(categories, keep_selected=True)
def _update_size_limits(self):
self.setMinimumHeight(int(self._screen_geometry.height() * 0.5))
self.setMinimumWidth(int(self._screen_geometry.width() * 0.5))
self.setMaximumWidth(int(self._screen_geometry.width()))
def changeEvent(self, e: QEvent):
if isinstance(e, QWindowStateChangeEvent):
self._maximized = self.isMaximized()
self.table_apps.change_headers_policy(maximized=self._maximized)
if not self._maximized:
self._reorganize()
self.adjustSize()
def event(self, e: QEvent) -> bool:
res = super(ManageWindow, self).event(e)
if self.isVisible() and e.type() == 216: # drop event
current_geometry = get_current_screen_geometry()
if current_geometry != self._screen_geometry: # only if the display device has changed
self._screen_geometry = current_geometry
self._update_size_limits()
self._reorganize()
self.adjustSize()
return res
def _handle_console(self, checked: bool):
if checked:
self.textarea_details.show()
@@ -780,6 +798,7 @@ class ManageWindow(QWidget):
util.notify_user('{} ({}) {}'.format(src_pkg.model.name, src_pkg.model.get_type(), self.i18n['uninstalled']))
if res['removed']:
screen_width = get_current_screen_geometry(self).width()
for list_idx, pkg_list in enumerate((self.pkgs_available, self.pkgs, self.pkgs_installed)):
if pkg_list:
removed_idxs = []
@@ -796,7 +815,9 @@ class ManageWindow(QWidget):
removed_idxs.append(pkgv_idx)
if self.search_performed and list_idx == 1: # only for displayed
self.table_apps.update_package(pkgv, change_update_col=True)
self.table_apps.update_package(pkgv,
screen_width=screen_width,
change_update_col=True)
break # as the model has been found, stops the loop
@@ -1286,8 +1307,7 @@ class ManageWindow(QWidget):
if pkg_info:
if len(pkg_info) > 1:
dialog_info = InfoDialog(pkg_info=pkg_info, icon_cache=self.icon_cache,
i18n=self.i18n, screen_size=self.screen_size)
dialog_info = InfoDialog(pkg_info=pkg_info, icon_cache=self.icon_cache, i18n=self.i18n)
dialog_info.exec_()
else:
dialog.show_message(title=self.i18n['warning'].capitalize(),
@@ -1462,10 +1482,11 @@ class ManageWindow(QWidget):
self.pkgs_available.insert(new_idx, data[2])
# updating the respective table rows:
screen_width = get_current_screen_geometry(self).width()
for displayed in self.pkgs:
for model in models_updated:
if displayed.model == model:
self.table_apps.update_package(displayed, change_update_col=True)
self.table_apps.update_package(displayed, screen_width=screen_width, change_update_col=True)
self.update_bt_upgrade()
@@ -1565,7 +1586,8 @@ class ManageWindow(QWidget):
self.settings_window.handle_display()
else:
self.settings_window = SettingsWindow(manager=self.manager, i18n=self.i18n, window=self)
self.settings_window.setMinimumWidth(int(self.screen_size.width() / 4))
screen_width = get_current_screen_geometry(self).width()
self.settings_window.setMinimumWidth(int(screen_width / 4))
self.settings_window.resize(self.size())
self.settings_window.adjustSize()
qt_utils.centralize(self.settings_window)
@@ -1628,10 +1650,11 @@ class ManageWindow(QWidget):
self._update_table_indexes()
self.update_bt_upgrade()
else:
screen_width = get_current_screen_geometry(self).width()
for pkg in self.pkgs:
if pkg == res['pkg']:
pkg.update_model(res['pkg'].model)
self.table_apps.update_package(pkg, change_update_col=not any([self.search_performed, self.suggestions_requested]))
self.table_apps.update_package(pkg, screen_width=screen_width, change_update_col=not any([self.search_performed, self.suggestions_requested]))
self.update_bt_upgrade()
break

View File

@@ -0,0 +1,22 @@
FROM ubuntu:20.04
ARG bauh_commit
ENV BAUH_VERSION=$bauh_commit
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && \
apt-get upgrade && \
apt-get install python3-pip python3-setuptools python3-wheel wget fuse binutils coreutils desktop-file-utils fakeroot patchelf squashfs-tools strace zsync libgdk-pixbuf2.0-dev gtk-update-icon-cache -y && \
mkdir /build && cd /build && \
wget https://github.com/AppImageCrafters/appimage-builder/releases/download/v0.9.2/appimage-builder-0.9.2-35e3eab-x86_64.AppImage -O appimage-builder && \
wget https://github.com/AppImage/AppImageKit/releases/download/13/appimagetool-x86_64.AppImage -O appimage-tool && \
chmod +x appimage-tool && \
chmod +x appimage-builder && \
mv /build/appimage-builder /usr/local/bin/appimage-builder && \
mv /build/appimage-tool /usr/local/bin/appimage-tool
WORKDIR /build
COPY AppImageBuilder.yml /build
CMD [ "appimage-builder", "--skip-tests"]

3
linux_dist/appimage/build.sh Executable file
View File

@@ -0,0 +1,3 @@
#!/bin/bash
docker build -t bauh-appimage --build-arg bauh_commit=$BAUH_COMMIT .
docker run --cap-add=SYS_ADMIN --device /dev/fuse --mount type=bind,source="$(pwd)",target=/build bauh-appimage

View File

@@ -1,4 +1,3 @@
import time
from unittest import TestCase
from bauh.commons.util import size_to_byte, sanitize_command_input

View File

@@ -8,7 +8,7 @@ class GetHumanSizeStrTest(TestCase):
def setUp(self):
try:
locale.setlocale(locale.LC_NUMERIC, None)
locale.setlocale(locale.LC_NUMERIC, "C")
except:
print("Error: could not set locale.LC_NUMERIC to None")

View File

@@ -79,6 +79,45 @@ class TestUtil(TestCase):
self.assertEqual(expected, res)
def test_replace_desktop_entry_exec_command__exec_as_the_first_field(self):
desktop_entry = """
Exec=myapp %f
Name=MyApp
Icon=MyApp
"""
res = replace_desktop_entry_exec_command(desktop_entry=desktop_entry,
appname='myapp',
file_path='/path/to/myapp.appimage')
expected = """
Exec="/path/to/myapp.appimage" %f
Name=MyApp
Icon=MyApp
"""
self.assertEqual(expected, res)
def test_replace_desktop_entry_exec_command__try_exec_as_the_first_field(self):
desktop_entry = """
TryExec=MyApp
Exec=myapp %f
Name=MyApp
Icon=MyApp
"""
res = replace_desktop_entry_exec_command(desktop_entry=desktop_entry,
appname='myapp',
file_path='/path/to/myapp.appimage')
expected = """
Exec="/path/to/myapp.appimage" %f
Name=MyApp
Icon=MyApp
"""
self.assertEqual(expected, res)
def test_replace_desktop_entry_exec_command__only_one_exec_field_with_spaces_and_params(self):
desktop_entry = """
Name=MyApp
@@ -136,7 +175,7 @@ class TestUtil(TestCase):
self.assertEqual(expected, res)
def test_replace_desktop_entry_exec_command__only_one_tryexec_field_with_spaces_and_params(self):
def test_replace_desktop_entry_exec_command__must_remove_try_exec_field(self):
desktop_entry = """
Name=MyApp
Icon=MyApp
@@ -150,12 +189,11 @@ class TestUtil(TestCase):
expected = """
Name=MyApp
Icon=MyApp
TryExec ="/path/to/myapp.appimage" %f --a
"""
self.assertEqual(expected, res)
def test_replace_desktop_entry_exec_command__exec_and_tryexec_fields(self):
def test_replace_desktop_entry_exec_command__must_replace_exec_and_remove_tryexec_fields(self):
desktop_entry = """
Name=MyApp
Icon=MyApp
@@ -171,14 +209,13 @@ class TestUtil(TestCase):
expected = """
Name=MyApp
Icon=MyApp
TryExec ="/path/to/myapp.appimage" %f
Exec="/path/to/myapp.appimage" --a
Terminal=false
"""
self.assertEqual(expected, res)
def test_replace_desktop_entry_exec_command__exec_and_tryexec_fields_with_envvars_and_params(self):
def test_replace_desktop_entry_exec_command__exec_field_with_envvars_and_params(self):
desktop_entry = """
Name=MyApp
Icon=MyApp
@@ -194,7 +231,6 @@ class TestUtil(TestCase):
expected = """
Name=MyApp
Icon=MyApp
TryExec=__MY_VAR=1 "/path/to/myapp.appimage" %f
Exec=NEW_VAR=abc "/path/to/myapp.appimage" --a
Terminal=false
"""
@@ -228,7 +264,6 @@ Name=RPCS3
GenericName=PlayStation 3 Emulator
Comment=An open-source PlayStation 3 emulator/debugger written in C++.
Icon=rpcs3
TryExec="/path/to/rpcs3.appimage"
Exec="/path/to/rpcs3.appimage" %f
Terminal=false
Categories=Game;Emulator;

View File

@@ -98,4 +98,4 @@ Include = /etc/pacman.d/mirrorlist
#Server = file:///home/custompkgs
ignorepkg=google-chrome
# ignorepkg=abc
ignorepkg = firefox
IgnorePkg = firefox

View File

@@ -166,3 +166,33 @@ Description: GNU C compiler
]
self.assertEqual([p.__dict__ for p in expected], [p.__dict__ for p in returned])
def test_map_transaction_output__it_should_map_i386_packages(self):
output = "\nThe following NEW packages will be installed:\n" \
" gcc-12-base:i386{a} [12.1.0-2distro~22.04] <+272 kB> glib-networking:i386{a} [2.72.0-1] <+242 kB>\n" \
"\nThe following packages will be REMOVED:\n" \
" celluloid{a} [0.21-linux+distro] <-1066 kB> libpcre3:i386{a} [2:8.39-13distro0.22.04.1] <-714 kB>"
transaction = self.aptitude.map_transaction_output(output)
to_install = {
DebianPackage(name="gcc-12-base:i386",
version="12.1.0-2distro~22.04",
latest_version="12.1.0-2distro~22.04",
transaction_size=272000.0
),
DebianPackage(name="glib-networking:i386", version="2.72.0-1",
latest_version="2.72.0-1", transaction_size=242000.0)
}
self.assertEqual(to_install, {*transaction.to_install})
to_remove = {
DebianPackage(name="celluloid",
version="0.21-linux+distro",
latest_version="0.21-linux+distro",
transaction_size=-1066000.0
),
DebianPackage(name="libpcre3:i386", version="2:8.39-13distro0.22.04.1",
latest_version="2:8.39-13distro0.22.04.1", transaction_size=-714000.0)
}
self.assertEqual(to_remove, {*transaction.to_remove})