From 9451e04f1c9e06c99b5f6313c1429ab4a5f7670d Mon Sep 17 00:00:00 2001 From: Karim Date: Fri, 19 Jun 2026 18:41:02 +0200 Subject: [PATCH] Fix [tanin] AUR rebuild names: eww-git, tiramisu-git (not eww/tiramisu) Co-Authored-By: Claude Opus 4.8 --- packaging/build-tanin-repo.sh | 2 +- packaging/finish-tanin-repo.sh | 4 +- packaging/taninux/pkg/taninux/.BUILDINFO | 1168 +++++++++++++++++ packaging/taninux/pkg/taninux/.MTREE | Bin 0 -> 15399 bytes packaging/taninux/pkg/taninux/.PKGINFO | 33 + packaging/taninux/pkg/taninux/usr/bin/taninux | 8 + .../taninux/pkg/taninux/usr/bin/taninux-gtk | 8 + packaging/taninux/pkg/taninux/usr/bin/thub | 8 + .../taninux/pkg/taninux/usr/bin/tsettings | 8 + .../taninux-0.2.0.dist-info/METADATA | 9 + .../taninux-0.2.0.dist-info/RECORD | 144 ++ .../taninux-0.2.0.dist-info/WHEEL | 4 + .../taninux-0.2.0.dist-info/entry_points.txt | 5 + .../taninux-0.2.0.dist-info/licenses/LICENSE | 232 ++++ .../site-packages/taninux/__init__.py | 1 + .../site-packages/taninux/__main__.py | 3 + .../python3.14/site-packages/taninux/app.py | 192 +++ .../site-packages/taninux/bundles/__init__.py | 0 .../site-packages/taninux/bundles/builtins.py | 148 +++ .../site-packages/taninux/bundles/loader.py | 67 + .../site-packages/taninux/bundles/pillar.py | 13 + .../site-packages/taninux/bundles/screen.py | 247 ++++ .../python3.14/site-packages/taninux/cli.py | 837 ++++++++++++ .../site-packages/taninux/core/__init__.py | 0 .../taninux/core/accessibility.py | 52 + .../site-packages/taninux/core/account.py | 121 ++ .../site-packages/taninux/core/appinfo.py | 145 ++ .../site-packages/taninux/core/audio.py | 140 ++ .../site-packages/taninux/core/avatar.py | 64 + .../site-packages/taninux/core/backup.py | 63 + .../site-packages/taninux/core/bluetooth.py | 99 ++ .../site-packages/taninux/core/bootloader.py | 142 ++ .../site-packages/taninux/core/catalog.py | 110 ++ .../site-packages/taninux/core/compositor.py | 37 + .../site-packages/taninux/core/confirm.py | 89 ++ .../site-packages/taninux/core/defaultapps.py | 112 ++ .../site-packages/taninux/core/display.py | 291 ++++ .../site-packages/taninux/core/firewall.py | 45 + .../taninux/core/localization.py | 117 ++ .../site-packages/taninux/core/lock.py | 28 + .../site-packages/taninux/core/network.py | 211 +++ .../site-packages/taninux/core/nextcloud.py | 135 ++ .../taninux/core/package_index.py | 81 ++ .../site-packages/taninux/core/packages.py | 46 + .../site-packages/taninux/core/pacman.py | 29 + .../site-packages/taninux/core/panel.py | 169 +++ .../site-packages/taninux/core/paths.py | 34 + .../site-packages/taninux/core/pkg.py | 154 +++ .../site-packages/taninux/core/power.py | 271 ++++ .../site-packages/taninux/core/printers.py | 125 ++ .../site-packages/taninux/core/runner.py | 51 + .../site-packages/taninux/core/search.py | 152 +++ .../site-packages/taninux/core/shares.py | 153 +++ .../site-packages/taninux/core/snapshot.py | 137 ++ .../site-packages/taninux/core/state.py | 69 + .../site-packages/taninux/core/storage.py | 73 ++ .../site-packages/taninux/core/streaming.py | 169 +++ .../site-packages/taninux/core/sudo.py | 45 + .../site-packages/taninux/core/sudo_modal.py | 78 ++ .../site-packages/taninux/core/sysinfo.py | 274 ++++ .../site-packages/taninux/core/theme.py | 29 + .../site-packages/taninux/core/trash.py | 128 ++ .../site-packages/taninux/core/wallpaper.py | 135 ++ .../site-packages/taninux/flatpak/__init__.py | 0 .../site-packages/taninux/flatpak/pillar.py | 13 + .../site-packages/taninux/flatpak/screen.py | 341 +++++ .../site-packages/taninux/gui/README.md | 86 ++ .../site-packages/taninux/gui/__init__.py | 10 + .../site-packages/taninux/gui/__main__.py | 3 + .../site-packages/taninux/gui/accents.py | 36 + .../site-packages/taninux/gui/app.py | 54 + .../site-packages/taninux/gui/config.py | 44 + .../site-packages/taninux/gui/confirm.py | 42 + .../site-packages/taninux/gui/console.py | 123 ++ .../site-packages/taninux/gui/desktop.py | 256 ++++ .../site-packages/taninux/gui/eww.py | 33 + .../taninux/gui/helper/taninux-helper | 198 +++ .../taninux/gui/pages/__init__.py | 149 +++ .../site-packages/taninux/gui/pages/about.py | 66 + .../taninux/gui/pages/accessibility.py | 35 + .../taninux/gui/pages/appdetail.py | 203 +++ .../site-packages/taninux/gui/pages/audio.py | 197 +++ .../site-packages/taninux/gui/pages/backup.py | 219 ++++ .../taninux/gui/pages/bluetooth.py | 173 +++ .../taninux/gui/pages/bundles.py | 140 ++ .../taninux/gui/pages/dashboard.py | 281 ++++ .../taninux/gui/pages/defaultapps.py | 48 + .../taninux/gui/pages/display.py | 212 +++ .../taninux/gui/pages/firewall.py | 50 + .../taninux/gui/pages/hub_home.py | 130 ++ .../site-packages/taninux/gui/pages/kernel.py | 138 ++ .../site-packages/taninux/gui/pages/locale.py | 247 ++++ .../site-packages/taninux/gui/pages/lock.py | 39 + .../taninux/gui/pages/maintain.py | 394 ++++++ .../taninux/gui/pages/network.py | 194 +++ .../site-packages/taninux/gui/pages/panel.py | 199 +++ .../site-packages/taninux/gui/pages/power.py | 213 +++ .../taninux/gui/pages/printers.py | 195 +++ .../taninux/gui/pages/settings.py | 233 ++++ .../site-packages/taninux/gui/pages/shares.py | 282 ++++ .../taninux/gui/pages/storage.py | 80 ++ .../site-packages/taninux/gui/pages/store.py | 158 +++ .../site-packages/taninux/gui/pages/update.py | 195 +++ .../site-packages/taninux/gui/pages/users.py | 253 ++++ .../site-packages/taninux/gui/pages/vpn.py | 130 ++ .../taninux/gui/pages/wallpaper.py | 312 +++++ .../polkit/ch.gabrielevarano.Taninux.policy | 31 + .../site-packages/taninux/gui/process.py | 105 ++ .../site-packages/taninux/gui/style.py | 271 ++++ .../site-packages/taninux/gui/window.py | 144 ++ .../site-packages/taninux/health/__init__.py | 0 .../site-packages/taninux/health/pillar.py | 13 + .../site-packages/taninux/health/screen.py | 248 ++++ .../site-packages/taninux/kernel/__init__.py | 0 .../site-packages/taninux/kernel/pillar.py | 13 + .../site-packages/taninux/kernel/screen.py | 506 +++++++ .../taninux/maintain/__init__.py | 0 .../site-packages/taninux/maintain/pillar.py | 13 + .../taninux/maintain/scanners/__init__.py | 113 ++ .../taninux/maintain/scanners/_util.py | 36 + .../taninux/maintain/scanners/aur.py | 86 ++ .../taninux/maintain/scanners/base.py | 11 + .../taninux/maintain/scanners/dotfolders.py | 140 ++ .../taninux/maintain/scanners/flatpak.py | 167 +++ .../taninux/maintain/scanners/pacman.py | 218 +++ .../taninux/maintain/scanners/system.py | 229 ++++ .../site-packages/taninux/maintain/screen.py | 565 ++++++++ .../site-packages/taninux/maintain/types.py | 76 ++ .../taninux/overview/__init__.py | 0 .../site-packages/taninux/overview/pillar.py | 13 + .../site-packages/taninux/overview/screen.py | 351 +++++ .../taninux/packages/__init__.py | 0 .../site-packages/taninux/packages/pillar.py | 13 + .../site-packages/taninux/packages/screen.py | 324 +++++ .../site-packages/taninux/pillars/__init__.py | 33 + .../site-packages/taninux/pillars/base.py | 23 + .../site-packages/taninux/suggest/__init__.py | 0 .../site-packages/taninux/suggest/pillar.py | 13 + .../site-packages/taninux/suggest/rules.py | 160 +++ .../site-packages/taninux/suggest/screen.py | 176 +++ .../site-packages/taninux/update/__init__.py | 0 .../site-packages/taninux/update/pillar.py | 13 + .../site-packages/taninux/update/screen.py | 426 ++++++ .../taninux/update/sources/__init__.py | 23 + .../taninux/update/sources/aur.py | 48 + .../taninux/update/sources/flatpak.py | 66 + .../taninux/update/sources/fwupd.py | 38 + .../taninux/update/sources/pacman.py | 34 + .../site-packages/taninux/update/types.py | 14 + .../taninux/usr/lib/taninux/taninux-helper | 198 +++ ...ch.gabrielevarano.Taninux.Settings.desktop | 10 + ...gabrielevarano.Taninux.SoftwareHub.desktop | 10 + .../actions/ch.gabrielevarano.Taninux.policy | 31 + .../taninux/taninux-0.2.0-1-any.pkg.tar.zst | Bin 0 -> 556033 bytes 154 files changed, 18947 insertions(+), 3 deletions(-) create mode 100644 packaging/taninux/pkg/taninux/.BUILDINFO create mode 100644 packaging/taninux/pkg/taninux/.MTREE create mode 100644 packaging/taninux/pkg/taninux/.PKGINFO create mode 100755 packaging/taninux/pkg/taninux/usr/bin/taninux create mode 100755 packaging/taninux/pkg/taninux/usr/bin/taninux-gtk create mode 100755 packaging/taninux/pkg/taninux/usr/bin/thub create mode 100755 packaging/taninux/pkg/taninux/usr/bin/tsettings create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux-0.2.0.dist-info/METADATA create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux-0.2.0.dist-info/RECORD create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux-0.2.0.dist-info/WHEEL create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux-0.2.0.dist-info/entry_points.txt create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux-0.2.0.dist-info/licenses/LICENSE create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/__init__.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/__main__.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/app.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/bundles/__init__.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/bundles/builtins.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/bundles/loader.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/bundles/pillar.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/bundles/screen.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/cli.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/__init__.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/accessibility.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/account.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/appinfo.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/audio.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/avatar.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/backup.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/bluetooth.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/bootloader.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/catalog.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/compositor.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/confirm.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/defaultapps.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/display.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/firewall.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/localization.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/lock.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/network.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/nextcloud.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/package_index.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/packages.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/pacman.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/panel.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/paths.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/pkg.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/power.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/printers.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/runner.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/search.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/shares.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/snapshot.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/state.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/storage.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/streaming.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/sudo.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/sudo_modal.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/sysinfo.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/theme.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/trash.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/wallpaper.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/flatpak/__init__.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/flatpak/pillar.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/flatpak/screen.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/README.md create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/__init__.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/__main__.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/accents.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/app.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/config.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/confirm.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/console.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/desktop.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/eww.py create mode 100755 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/helper/taninux-helper create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/__init__.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/about.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/accessibility.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/appdetail.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/audio.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/backup.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/bluetooth.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/bundles.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/dashboard.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/defaultapps.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/display.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/firewall.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/hub_home.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/kernel.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/locale.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/lock.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/maintain.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/network.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/panel.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/power.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/printers.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/settings.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/shares.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/storage.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/store.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/update.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/users.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/vpn.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/wallpaper.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/polkit/ch.gabrielevarano.Taninux.policy create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/process.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/style.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/window.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/health/__init__.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/health/pillar.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/health/screen.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/kernel/__init__.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/kernel/pillar.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/kernel/screen.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/maintain/__init__.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/maintain/pillar.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/maintain/scanners/__init__.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/maintain/scanners/_util.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/maintain/scanners/aur.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/maintain/scanners/base.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/maintain/scanners/dotfolders.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/maintain/scanners/flatpak.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/maintain/scanners/pacman.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/maintain/scanners/system.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/maintain/screen.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/maintain/types.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/overview/__init__.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/overview/pillar.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/overview/screen.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/packages/__init__.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/packages/pillar.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/packages/screen.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/pillars/__init__.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/pillars/base.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/suggest/__init__.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/suggest/pillar.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/suggest/rules.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/suggest/screen.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/update/__init__.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/update/pillar.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/update/screen.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/update/sources/__init__.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/update/sources/aur.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/update/sources/flatpak.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/update/sources/fwupd.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/update/sources/pacman.py create mode 100644 packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/update/types.py create mode 100755 packaging/taninux/pkg/taninux/usr/lib/taninux/taninux-helper create mode 100644 packaging/taninux/pkg/taninux/usr/share/applications/ch.gabrielevarano.Taninux.Settings.desktop create mode 100644 packaging/taninux/pkg/taninux/usr/share/applications/ch.gabrielevarano.Taninux.SoftwareHub.desktop create mode 100644 packaging/taninux/pkg/taninux/usr/share/polkit-1/actions/ch.gabrielevarano.Taninux.policy create mode 100644 packaging/taninux/taninux-0.2.0-1-any.pkg.tar.zst diff --git a/packaging/build-tanin-repo.sh b/packaging/build-tanin-repo.sh index a06590b..a7561d9 100755 --- a/packaging/build-tanin-repo.sh +++ b/packaging/build-tanin-repo.sh @@ -17,7 +17,7 @@ mkdir -p "$OUT" OWN_SIMPLE=(tanin-greet tanin-libadwaita tanin-setup tanin-eww tanin-desktop) OWN_BUILD=(taninux) -AUR_REBUILD=(eww tiramisu waypaper) +AUR_REBUILD=(eww-git tiramisu-git waypaper) build() { local p="$1" diff --git a/packaging/finish-tanin-repo.sh b/packaging/finish-tanin-repo.sh index b48336b..dad8974 100755 --- a/packaging/finish-tanin-repo.sh +++ b/packaging/finish-tanin-repo.sh @@ -16,9 +16,9 @@ sudo pacman -S --needed --noconfirm python-build python-installer python-hatchli ( cd "$HERE/taninux" && makepkg -sf --noconfirm ) \ && cp "$HERE/taninux/"*.pkg.tar.zst "$OUT/" && echo " ok" || echo " FAILED (taninux)" -echo "==> AUR rebuilds: eww tiramisu waypaper" +echo "==> AUR rebuilds: eww-git tiramisu-git waypaper" tmp="$(mktemp -d)" -for p in eww tiramisu waypaper; do +for p in eww-git tiramisu-git waypaper; do echo " -- $p" if git clone --depth=1 "https://aur.archlinux.org/$p.git" "$tmp/$p" >/dev/null 2>&1 \ && ( cd "$tmp/$p" && makepkg -sf --noconfirm ); then diff --git a/packaging/taninux/pkg/taninux/.BUILDINFO b/packaging/taninux/pkg/taninux/.BUILDINFO new file mode 100644 index 0000000..8ff662f --- /dev/null +++ b/packaging/taninux/pkg/taninux/.BUILDINFO @@ -0,0 +1,1168 @@ +format = 2 +pkgname = taninux +pkgbase = taninux +pkgver = 0.2.0-1 +pkgarch = any +pkgbuild_sha256sum = 0034394998debf800f45a42786b415250875a99801c2403bf95d29d00aa2ffa9 +packager = Unknown Packager +builddate = 1781886675 +builddir = /home/karim/projects/taninux/packaging/taninux +startdir = /home/karim/projects/taninux/packaging/taninux +buildtool = makepkg +buildtoolver = 7.1.0 +buildenv = !distcc +buildenv = color +buildenv = !ccache +buildenv = check +buildenv = !sign +options = strip +options = docs +options = !libtool +options = !staticlibs +options = emptydirs +options = zipman +options = purge +options = debug +options = lto +installed = 7zip-26.01-1-x86_64 +installed = aalib-1.4rc5-19-x86_64 +installed = abseil-cpp-20260526.0-2-x86_64 +installed = accountsservice-26.13.3-1-x86_64 +installed = acl-2.3.2-2-x86_64 +installed = ada-3.4.4-1-x86_64 +installed = adw-gtk-theme-6.5-1-any +installed = adwaita-cursors-50.0-1-any +installed = adwaita-fonts-50.0-1-any +installed = adwaita-icon-theme-50.0-1-any +installed = adwaita-icon-theme-legacy-46.2-3-any +installed = alembic-1.8.11-2-x86_64 +installed = alsa-card-profiles-1:1.6.7-1-x86_64 +installed = alsa-lib-1.2.16.1-1-x86_64 +installed = alsa-plugins-1:1.2.12-5-x86_64 +installed = alsa-topology-conf-1.2.5.1-4-any +installed = alsa-ucm-conf-1.2.16.1-1-any +installed = alsa-utils-1.2.16-1-x86_64 +installed = amd-ucode-20260519-1-any +installed = ananicy-cpp-1.2.0-1-x86_64 +installed = antimicrox-3.6.1-1-x86_64 +installed = aom-3.14.1-1-x86_64 +installed = appstream-1.1.3-1-x86_64 +installed = aquamarine-0.12.1-1-x86_64 +installed = arch-update-3.20.2-1-any +installed = archlinux-contrib-20260611-1-any +installed = archlinux-keyring-20260612-1-any +installed = argon2-20190702-6-x86_64 +installed = at-spi2-core-2.60.4-1-x86_64 +installed = atkmm-2.28.5-2-x86_64 +installed = attr-2.5.2-2-x86_64 +installed = audit-4.1.4-2-x86_64 +installed = autoconf-2.73-1-any +installed = automake-1.18.1-1-any +installed = avahi-1:0.9rc4-1-x86_64 +installed = awesome-terminal-fonts-1.1.0-5-any +installed = ayatana-ido-0.10.4-1-x86_64 +installed = base-3-3-any +installed = base-devel-1-2-any +installed = bash-5.3.15-1-x86_64 +installed = bats-1.13.0-1-any +installed = binutils-2.46+r70+g155188ea10a7-1-x86_64 +installed = bison-3.8.2-8-x86_64 +installed = blas-3.12.1-2-x86_64 +installed = blender-17:5.1.2-1-x86_64 +installed = blosc-1.21.6-2-x86_64 +installed = bluez-5.86-6-x86_64 +installed = bluez-libs-5.86-6-x86_64 +installed = bluez-utils-5.86-6-x86_64 +installed = boost-libs-1.91.0-1-x86_64 +installed = botan-3.12.0-1-x86_64 +installed = brotli-1.2.0-1-x86_64 +installed = btrfs-progs-7.0-1-x86_64 +installed = bubblewrap-0.11.2-1-x86_64 +installed = bzip2-1.0.8-6-x86_64 +installed = c-ares-1.34.6-1-x86_64 +installed = ca-certificates-20240618-1-any +installed = ca-certificates-mozilla-3.125-1-x86_64 +installed = ca-certificates-utils-20240618-1-any +installed = cabextract-1.11-2-x86_64 +installed = cage-0.3.0-1-x86_64 +installed = cairo-1.18.4-1-x86_64 +installed = cairomm-1.14.6-1-x86_64 +installed = cairomm-1.16-1.18.1-1-x86_64 +installed = capstone-5.0.9-1-x86_64 +installed = cblas-3.12.1-2-x86_64 +installed = cdparanoia-10.2-9-x86_64 +installed = ceres-solver-2.2.0-5-x86_64 +installed = chatbox-bin-1.21.0-1-x86_64 +installed = cifs-utils-7.5-1-x86_64 +installed = clang-22.1.6-1-x86_64 +installed = claude-code-2.1.177-1-x86_64 +installed = cliphist-1:0.7.0-2-x86_64 +installed = clucene-2.3.3.4-17-x86_64 +installed = cmake-4.3.4-1-x86_64 +installed = code-1.124.2-1-x86_64 +installed = compiler-rt-22.1.6-1-x86_64 +installed = composefs-1.0.8-1-x86_64 +installed = coreutils-9.11-1-x86_64 +installed = cpio-2.15-3-x86_64 +installed = cppdap-1.58.0-3-x86_64 +installed = cracklib-2.10.3-1-x86_64 +installed = cronie-1.7.2-2-x86_64 +installed = cryptsetup-2.8.6-1-x86_64 +installed = cups-2:2.4.19-1-x86_64 +installed = cups-filters-2.0.1-2-x86_64 +installed = curl-8.20.0-7-x86_64 +installed = dav1d-1.5.3-1-x86_64 +installed = db5.3-5.3.28-7-x86_64 +installed = dbus-1.16.2-1-x86_64 +installed = dbus-broker-37-3-x86_64 +installed = dbus-broker-units-37-3-x86_64 +installed = dbus-glib-0.114-1-x86_64 +installed = dbus-units-37-3-x86_64 +installed = dconf-0.49.0-1-x86_64 +installed = debugedit-5.3-1-x86_64 +installed = default-cursors-3-1-any +installed = desktop-file-utils-0.28-1-x86_64 +installed = device-mapper-2.03.41-1-x86_64 +installed = dialog-1:1.3_20260107-1-x86_64 +installed = diffutils-3.12-2-x86_64 +installed = djvulibre-3.5.30.1-1-x86_64 +installed = dmidecode-3.7-1-x86_64 +installed = dnsmasq-2.93-1-x86_64 +installed = dos2unix-7.5.6-1-x86_64 +installed = dosfstools-4.2-5-x86_64 +installed = double-conversion-3.4.0-1-x86_64 +installed = draco-1.5.7-2-x86_64 +installed = dtc-1:1.7.2-1-x86_64 +installed = duktape-2.7.0-7-x86_64 +installed = e2fsprogs-1.47.4-1-x86_64 +installed = editorconfig-core-c-0.12.11-1-x86_64 +installed = edk2-ovmf-202605-1-any +installed = efibootmgr-18-4-x86_64 +installed = efivar-39-2-x86_64 +installed = electron39-39.8.10-1-x86_64 +installed = electron42-42.3.0-1-x86_64 +installed = elfutils-0.195-1-x86_64 +installed = embree-4.4.1-1-x86_64 +installed = enchant-2.8.15-2-x86_64 +installed = epson-inkjet-printer-escpr2-1.2.39-1-x86_64 +installed = eww-git-0.6.0.r89.g865cf63-1-x86_64 +installed = exempi-2.6.6-3-x86_64 +installed = exiv2-0.28.8-2-x86_64 +installed = expat-2.8.1-1-x86_64 +installed = extra-cmake-modules-6.27.0-1-any +installed = fakeroot-1:1.37.2-2-x86_64 +installed = fd-10.4.2-2-x86_64 +installed = ffmpeg-2:8.1.2-1-x86_64 +installed = fftw-3.3.11-1-x86_64 +installed = file-5.48-1-x86_64 +installed = filesystem-2025.10.12-1-any +installed = findutils-4.10.0-3-x86_64 +installed = flac-1.5.0-1-x86_64 +installed = flatpak-1:1.18.0-1-x86_64 +installed = flex-2.6.4-6-x86_64 +installed = fmt-12.2.0-1-x86_64 +installed = font-manager-0.9.4-3-x86_64 +installed = fontconfig-2:2.18.1-1-x86_64 +installed = freeglut-3.8.0-1-x86_64 +installed = freetype2-2.14.3-1-x86_64 +installed = fribidi-1.0.16-2-x86_64 +installed = fuse-common-3.18.2-1-x86_64 +installed = fuse2-2.9.9-5-x86_64 +installed = fuse3-3.18.2-1-x86_64 +installed = gamemode-1.8.2-2-x86_64 +installed = gawk-5.4.0-1-x86_64 +installed = gc-8.2.12-1-x86_64 +installed = gcc-16.1.1+r12+g301eb08fa2c5-1-x86_64 +installed = gcc-libs-16.1.1+r12+g301eb08fa2c5-1-x86_64 +installed = gcr-3.41.2-2-x86_64 +installed = gcr-4-4.4.0.1-1-x86_64 +installed = gdb-17.2-1-x86_64 +installed = gdb-common-17.2-1-x86_64 +installed = gdbm-1.26-2-x86_64 +installed = gdk-pixbuf2-2.44.6-2-x86_64 +installed = gedit-50.0-3-x86_64 +installed = geoclue-2.8.1-1-x86_64 +installed = geocode-glib-3.26.4-6-x86_64 +installed = gettext-1.0-2-x86_64 +installed = gexiv2-0.16.0-2-x86_64 +installed = gflags-2.2.2-6-x86_64 +installed = gfxstream-0.1.2-2-x86_64 +installed = ghostscript-10.07.1-1-x86_64 +installed = giflib-6.1.3-1-x86_64 +installed = git-2.54.0-1-x86_64 +installed = glew-2.3.1-1-x86_64 +installed = glfw-1:3.4-1-x86_64 +installed = glib-networking-1:2.80.1-1-x86_64 +installed = glib2-2.88.1-1-x86_64 +installed = glibc-2.43+r22+g8362e8ce10b2-2-x86_64 +installed = glibmm-2.66.8-2-x86_64 +installed = glibmm-2.68-2.88.0-2-x86_64 +installed = glm-1.0.3-1-x86_64 +installed = glslang-1:1.4.350.0-1-x86_64 +installed = glu-9.0.3-3-x86_64 +installed = glycin-2.1.1-1-x86_64 +installed = glycin-gtk4-2.1.1-1-x86_64 +installed = gmp-6.3.0-3-x86_64 +installed = gnome-autoar-0.4.5-1-x86_64 +installed = gnome-clocks-50.0-2-x86_64 +installed = gnome-desktop-4-1:44.5-1-x86_64 +installed = gnome-desktop-common-1:44.5-1-x86_64 +installed = gnome-disk-utility-46.1-2-x86_64 +installed = gnome-keyring-1:50.0-1-x86_64 +installed = gnome-text-editor-50.1-1-x86_64 +installed = gnu-free-fonts-20120503-9-any +installed = gnulib-l10n-20241231-1-any +installed = gnupg-2.4.9-1-x86_64 +installed = gnutls-3.8.13-2-x86_64 +installed = go-2:1.26.4-1-x86_64 +installed = gobject-introspection-runtime-1.86.0-2-x86_64 +installed = google-glog-0.7.1-2-x86_64 +installed = gperftools-2.18.1-1-x86_64 +installed = gpgme-2.1.0-1-x86_64 +installed = gpgmepp-2.1.0-1-x86_64 +installed = gpm-1.20.7.r38.ge82d1a6-6-x86_64 +installed = gpsd-3.27.5-1-x86_64 +installed = graphene-1.10.8-2-x86_64 +installed = graphite-1:1.3.15-1-x86_64 +installed = greetd-0.10.3-2-x86_64 +installed = greetd-agreety-0.10.3-2-x86_64 +installed = greetd-regreet-0.4.0-1-x86_64 +installed = greetd-tuigreet-0.9.1-2-x86_64 +installed = grep-3.12-2-x86_64 +installed = grim-1.5.0-2-x86_64 +installed = groff-1.24.1-1-x86_64 +installed = grub-2:2.14-1-x86_64 +installed = gsettings-desktop-schemas-50.1-1-any +installed = gsettings-system-schemas-50.1-1-any +installed = gsm-1.0.24-1-x86_64 +installed = gspell-1.14.3-1-x86_64 +installed = gssdp-1.6.5-1-x86_64 +installed = gst-plugins-bad-libs-1.28.4-1-x86_64 +installed = gst-plugins-base-1.28.4-1-x86_64 +installed = gst-plugins-base-libs-1.28.4-1-x86_64 +installed = gst-plugins-good-1.28.4-1-x86_64 +installed = gstreamer-1.28.4-1-x86_64 +installed = gtest-1.17.0-2-x86_64 +installed = gtk-layer-shell-0.10.1-1-x86_64 +installed = gtk-update-icon-cache-1:4.22.4-1-x86_64 +installed = gtk-vnc-1.5.0-1-x86_64 +installed = gtk3-1:3.24.52-1-x86_64 +installed = gtk4-1:4.22.4-1-x86_64 +installed = gtkmm-4.0-4.22.0-2-x86_64 +installed = gtkmm3-3.24.11-1-x86_64 +installed = gtksourceview4-4.8.4-2-x86_64 +installed = gtksourceview5-5.20.0-1-x86_64 +installed = guile-3.0.11-1-x86_64 +installed = gumbo-parser-0.13.2-1-x86_64 +installed = gupnp-1:1.6.10-1-x86_64 +installed = gupnp-dlna-0.12.0-5-x86_64 +installed = gupnp-igd-1.6.0-2-x86_64 +installed = gvfs-1.60.0-2-x86_64 +installed = gweather-locations-2026.2-1-x86_64 +installed = gzip-1.14-2-x86_64 +installed = harfbuzz-14.2.1-1-x86_64 +installed = harfbuzz-icu-14.2.1-1-x86_64 +installed = hdf5-2.1.1-1-x86_64 +installed = heroic-games-launcher-bin-2.22.0-1-x86_64 +installed = hicolor-icon-theme-0.18-1-any +installed = hidapi-0.15.0-1-x86_64 +installed = highway-1.4.0-1-x86_64 +installed = htmlq-0.4.0-3-x86_64 +installed = hugo-0.163.1-1-x86_64 +installed = hunspell-1.7.3-1-x86_64 +installed = hwdata-0.408-1-any +installed = hwloc-2.14.0-1-x86_64 +installed = hyphen-2.8.9-1-x86_64 +installed = hyprcursor-0.1.13-6-x86_64 +installed = hyprgraphics-0.5.1-2-x86_64 +installed = hyprland-0.55.4-1-x86_64 +installed = hyprland-guiutils-0.2.1-6-x86_64 +installed = hyprlang-0.6.8-4-x86_64 +installed = hyprshot-1.3.0-4-any +installed = hyprtoolkit-0.5.4-2-x86_64 +installed = hyprutils-0.13.1-1-x86_64 +installed = hyprwayland-scanner-0.4.6-1-x86_64 +installed = hyprwire-0.3.1-2-x86_64 +installed = iana-etc-20260530-1-any +installed = icu-78.3-1-x86_64 +installed = ijs-0.35-7-x86_64 +installed = imagemagick-7.1.2.25-1-x86_64 +installed = imath-3.2.2-6-x86_64 +installed = imlib2-1.12.6-1-x86_64 +installed = iniparser-4.2.6-2-x86_64 +installed = intel-oneapi-common-2026.0.0_235-1-any +installed = intel-oneapi-compiler-dpcpp-cpp-runtime-libs-2026.0.0_947-2-x86_64 +installed = intel-oneapi-compiler-shared-runtime-libs-2026.0.0_947-1-x86_64 +installed = intel-oneapi-openmp-2026.0.0_947-1-x86_64 +installed = intel-oneapi-tbb-2023.0.0_724-1-x86_64 +installed = intel-oneapi-tcm-1.5.0_489-1-x86_64 +installed = intel-oneapi-umf-1.1.0_340-2-x86_64 +installed = inter-font-4.1-1-any +installed = iproute2-7.1.0-1-x86_64 +installed = iptables-1:1.8.13-1-x86_64 +installed = iputils-20250605-1-x86_64 +installed = iso-codes-4.20.1-1-any +installed = jansson-2.15.0-1-x86_64 +installed = jbig2dec-0.20-2-x86_64 +installed = jbigkit-2.1-8-x86_64 +installed = jemalloc-1:5.3.1-2-x86_64 +installed = jq-1.8.1-3-x86_64 +installed = json-c-0.18-2-x86_64 +installed = json-glib-1.10.8-1-x86_64 +installed = jsoncpp-1.9.6-3-x86_64 +installed = karchive-6.27.0-1-x86_64 +installed = kbd-2.10.0-1-x86_64 +installed = kddockwidgets-2.4.0-3-x86_64 +installed = kdsingleapplication-1.2.1-1-x86_64 +installed = keyutils-1.6.3-4-x86_64 +installed = kguiaddons-6.27.0-1-x86_64 +installed = kitty-0.47.1-1-x86_64 +installed = kitty-shell-integration-0.47.1-1-x86_64 +installed = kitty-terminfo-0.47.1-1-x86_64 +installed = kmod-34.2-1-x86_64 +installed = krb5-1.22.2-1-x86_64 +installed = l-smash-2.14.5-4-x86_64 +installed = lact-0.9.0-1-x86_64 +installed = lame-3.101.r6531-1-x86_64 +installed = lapack-3.12.1-2-x86_64 +installed = lcms2-2.19.1-1-x86_64 +installed = ldb-2:4.24.3-1-x86_64 +installed = leancrypto-1.7.2-1-x86_64 +installed = level-zero-loader-1.28.2-1-x86_64 +installed = lib32-alsa-lib-1.2.16.1-1-x86_64 +installed = lib32-alsa-plugins-1.2.12-1-x86_64 +installed = lib32-brotli-1.1.0-1-x86_64 +installed = lib32-bzip2-1.0.8-4-x86_64 +installed = lib32-curl-8.20.0-7-x86_64 +installed = lib32-dbus-1.16.2-1-x86_64 +installed = lib32-e2fsprogs-1.47.4-1-x86_64 +installed = lib32-expat-2.8.1-1-x86_64 +installed = lib32-fontconfig-2:2.18.1-1-x86_64 +installed = lib32-freetype2-2.14.3-1-x86_64 +installed = lib32-gcc-libs-16.1.1+r12+g301eb08fa2c5-1-x86_64 +installed = lib32-glib2-2.88.1-1-x86_64 +installed = lib32-glibc-2.43+r22+g8362e8ce10b2-2-x86_64 +installed = lib32-gmp-6.3.0-2-x86_64 +installed = lib32-gnutls-3.8.13-3-x86_64 +installed = lib32-icu-78.3-1-x86_64 +installed = lib32-json-c-0.18-2-x86_64 +installed = lib32-keyutils-1.6.3-4-x86_64 +installed = lib32-krb5-1.22.2-1-x86_64 +installed = lib32-libdisplay-info-0.3.0-1-x86_64 +installed = lib32-libdrm-2.4.134-1-x86_64 +installed = lib32-libelf-0.195-1-x86_64 +installed = lib32-libffi-3.5.2-1-x86_64 +installed = lib32-libgcrypt-1.12.2-1-x86_64 +installed = lib32-libglvnd-1.7.0-1-x86_64 +installed = lib32-libgpg-error-1.61-1-x86_64 +installed = lib32-libidn2-2.3.8-1-x86_64 +installed = lib32-libldap-2.6.13-1-x86_64 +installed = lib32-libnghttp2-1.69.0-1-x86_64 +installed = lib32-libnghttp3-1.16.0-1-x86_64 +installed = lib32-libngtcp2-1.23.0-1-x86_64 +installed = lib32-libnm-1.56.1-1-x86_64 +installed = lib32-libpciaccess-0.19-1-x86_64 +installed = lib32-libpipewire-1:1.6.7-1-x86_64 +installed = lib32-libpng-1.6.58-1-x86_64 +installed = lib32-libpsl-0.21.5-1-x86_64 +installed = lib32-libssh2-1.11.1-1-x86_64 +installed = lib32-libtasn1-4.21.0-1-x86_64 +installed = lib32-libunistring-1.4.2-1-x86_64 +installed = lib32-libva-2.22.0-1-x86_64 +installed = lib32-libx11-1.8.13-1-x86_64 +installed = lib32-libxau-1.0.12-1-x86_64 +installed = lib32-libxcb-1.17.0-1-x86_64 +installed = lib32-libxcrypt-4.5.2-1-x86_64 +installed = lib32-libxcrypt-compat-4.5.2-1-x86_64 +installed = lib32-libxdmcp-1.1.5-1-x86_64 +installed = lib32-libxext-1.3.7-1-x86_64 +installed = lib32-libxfixes-6.0.1-2-x86_64 +installed = lib32-libxinerama-1.1.6-1-x86_64 +installed = lib32-libxml2-2.15.3-1-x86_64 +installed = lib32-libxshmfence-1.3.3-1-x86_64 +installed = lib32-libxss-1.2.5-1-x86_64 +installed = lib32-libxxf86vm-1.1.5-2-x86_64 +installed = lib32-llvm-libs-1:22.1.6-1-x86_64 +installed = lib32-lm_sensors-1:3.6.2-2-x86_64 +installed = lib32-mesa-1:26.1.3-2-x86_64 +installed = lib32-ncurses-6.6-2-x86_64 +installed = lib32-nettle-4.0-2-x86_64 +installed = lib32-nspr-4.39-1-x86_64 +installed = lib32-nss-3.125-1-x86_64 +installed = lib32-openssl-1:3.6.3-1-x86_64 +installed = lib32-p11-kit-0.26.2-1-x86_64 +installed = lib32-pcre2-10.47-1-x86_64 +installed = lib32-pipewire-1:1.6.7-1-x86_64 +installed = lib32-spirv-tools-1:1.4.350.0-1-x86_64 +installed = lib32-sqlite-3.53.2-1-x86_64 +installed = lib32-systemd-260.2-1-x86_64 +installed = lib32-util-linux-2.42.2-1-x86_64 +installed = lib32-vulkan-icd-loader-1.4.350.0-1-x86_64 +installed = lib32-vulkan-mesa-implicit-layers-1:26.1.3-2-x86_64 +installed = lib32-vulkan-radeon-1:26.1.3-2-x86_64 +installed = lib32-wayland-1.25.0-1-x86_64 +installed = lib32-xcb-util-keysyms-0.4.1-2-x86_64 +installed = lib32-xz-5.8.3-1-x86_64 +installed = lib32-zlib-1.3.2-1-x86_64 +installed = lib32-zstd-1.5.7-2-x86_64 +installed = libabw-0.1.4-1-x86_64 +installed = libadwaita-1:1.9.1-1-x86_64 +installed = libaec-1.1.7-1-x86_64 +installed = libaemu-0.1.2-5-x86_64 +installed = libaio-0.3.113-4-x86_64 +installed = libappindicator-12.10.1-1-x86_64 +installed = libarchive-3.8.7-1-x86_64 +installed = libasan-16.1.1+r12+g301eb08fa2c5-1-x86_64 +installed = libass-0.17.4-2-x86_64 +installed = libassuan-3.0.0-1-x86_64 +installed = libasyncns-1:0.8+r3+g68cd5af-3-x86_64 +installed = libatasmart-0.19-8-x86_64 +installed = libatomic-16.1.1+r12+g301eb08fa2c5-1-x86_64 +installed = libatomic_ops-7.8.2-2-x86_64 +installed = libavc1394-0.5.4-7-x86_64 +installed = libavif-1.4.2-1-x86_64 +installed = libayatana-appindicator-0.5.94-1-x86_64 +installed = libayatana-indicator-0.9.5-1-x86_64 +installed = libb2-0.98.1-3-x86_64 +installed = libb64-1.2.1-5-x86_64 +installed = libbacktrace-1.0.git1-1-x86_64 +installed = libblake3-1.8.4-1-x86_64 +installed = libblockdev-3.5.0-2-x86_64 +installed = libblockdev-crypto-3.5.0-2-x86_64 +installed = libblockdev-fs-3.5.0-2-x86_64 +installed = libblockdev-loop-3.5.0-2-x86_64 +installed = libblockdev-mdraid-3.5.0-2-x86_64 +installed = libblockdev-nvme-3.5.0-2-x86_64 +installed = libblockdev-part-3.5.0-2-x86_64 +installed = libblockdev-smart-3.5.0-2-x86_64 +installed = libblockdev-swap-3.5.0-2-x86_64 +installed = libbluray-1.4.1-1-x86_64 +installed = libbpf-1.7.0-1-x86_64 +installed = libbs2b-3.1.0-10-x86_64 +installed = libbsd-0.12.2-2-x86_64 +installed = libburn-1.5.8-1-x86_64 +installed = libbytesize-2.12-3-x86_64 +installed = libcaca-0.99.beta20-7-x86_64 +installed = libcacard-2.8.1-1-x86_64 +installed = libcanberra-1:0.30+r2+gc0620e4-6-x86_64 +installed = libcap-2.78-1-x86_64 +installed = libcap-ng-0.9.3-1-x86_64 +installed = libcbor-0.14.0-1-x86_64 +installed = libcdio-2.3.0-1-x86_64 +installed = libcdio-paranoia-10.2+2.0.2-2-x86_64 +installed = libcdr-0.1.9-1-x86_64 +installed = libcloudproviders-0.4.0-1-x86_64 +installed = libcmis-0.6.3-1-x86_64 +installed = libcolord-1.4.8-1-x86_64 +installed = libcue-2.3.0-2-x86_64 +installed = libcups-2:2.4.19-1-x86_64 +installed = libcupsfilters-2.1.1-4-x86_64 +installed = libdaemon-0.14-6-x86_64 +installed = libdatrie-0.2.14-1-x86_64 +installed = libdbusmenu-glib-18.10.20180917-1-x86_64 +installed = libdbusmenu-gtk3-18.10.20180917-1-x86_64 +installed = libde265-1.1.1-1-x86_64 +installed = libdecor-0.2.5-1-x86_64 +installed = libdeflate-1.25-1-x86_64 +installed = libdisplay-info-0.3.0-1-x86_64 +installed = libdovi-3.3.2-1-x86_64 +installed = libdrm-2.4.134-1-x86_64 +installed = libdv-1.0.0-12-x86_64 +installed = libdvdnav-7.0.0-1-x86_64 +installed = libdvdread-7.0.1-1-x86_64 +installed = libe-book-0.1.3-20-x86_64 +installed = libebur128-1.2.6-2-x86_64 +installed = libedit-20260512_3.1-1-x86_64 +installed = libei-1.6.0-1-x86_64 +installed = libelf-0.195-1-x86_64 +installed = libepoxy-1.5.10-3-x86_64 +installed = libepubgen-0.1.1-6-x86_64 +installed = libetonyek-0.1.13-2-x86_64 +installed = libevdev-1.13.6-1-x86_64 +installed = libevent-2.1.12-5-x86_64 +installed = libexif-0.6.26-1-x86_64 +installed = libexttextcat-3.4.8-1-x86_64 +installed = libfdk-aac-2.0.3-2-x86_64 +installed = libffi-3.5.2-1-x86_64 +installed = libfontenc-1.1.9-1-x86_64 +installed = libfreeaptx-0.2.2-1-x86_64 +installed = libfreehand-0.1.3-1-x86_64 +installed = libfyaml-0.9.6-2-x86_64 +installed = libgcc-16.1.1+r12+g301eb08fa2c5-1-x86_64 +installed = libgcrypt-1.12.2-1-x86_64 +installed = libgedit-amtk-5.10.0-1-x86_64 +installed = libgedit-gfls-1:0.4.1-1-x86_64 +installed = libgedit-gtksourceview-299.7.0-1-x86_64 +installed = libgedit-tepl-6.14.0-2-x86_64 +installed = libgee-0.20.8-1-x86_64 +installed = libgfortran-16.1.1+r12+g301eb08fa2c5-1-x86_64 +installed = libgirepository-1.86.0-2-x86_64 +installed = libglvnd-1.7.0-3-x86_64 +installed = libgnomekbd-1:3.28.1-2-x86_64 +installed = libgomp-16.1.1+r12+g301eb08fa2c5-1-x86_64 +installed = libgpg-error-1.61-1-x86_64 +installed = libgsf-1.14.58-1-x86_64 +installed = libgudev-238-3-x86_64 +installed = libgweather-4-4.6.0-1-x86_64 +installed = libgxps-0.3.2-5-x86_64 +installed = libhandy-1.8.3-2-x86_64 +installed = libharu-2.4.6-1-x86_64 +installed = libheif-1.23.0-1-x86_64 +installed = libice-1.1.2-1-x86_64 +installed = libidn-1.44-1-x86_64 +installed = libidn2-2.3.8-1-x86_64 +installed = libiec61883-1.2.0-9-x86_64 +installed = libimagequant-4.4.1-2-x86_64 +installed = libimobiledevice-1.4.0-2-x86_64 +installed = libimobiledevice-glue-1.3.2-1-x86_64 +installed = libinih-62-2-x86_64 +installed = libinput-1.31.3-1-x86_64 +installed = libiptcdata-1.0.5-5-x86_64 +installed = libisl-0.27-1-x86_64 +installed = libisoburn-1.5.8.2-1-x86_64 +installed = libisofs-1.5.8.2-1-x86_64 +installed = libixion-0.20.0-7-x86_64 +installed = libjpeg-turbo-3.1.4.1-1-x86_64 +installed = libjxl-0.11.2-2-x86_64 +installed = libksba-1.8.0-1-x86_64 +installed = liblangtag-0.6.8-1-x86_64 +installed = liblc3-1.1.3-2-x86_64 +installed = libldac-2.0.2.3-3-x86_64 +installed = libldap-2.6.13-1-x86_64 +installed = libliftoff-0.5.0-1-x86_64 +installed = liblqr-0.4.3-1-x86_64 +installed = liblsan-16.1.1+r12+g301eb08fa2c5-1-x86_64 +installed = libmakepkg-dropins-20-1-any +installed = libmalcontent-0.14.0-4-x86_64 +installed = libmanette-0.2.13-2-x86_64 +installed = libmd-1.2.0-1-x86_64 +installed = libmm-glib-1.24.2-1-x86_64 +installed = libmnl-1.0.5-2-x86_64 +installed = libmodplug-0.8.9.0-7-x86_64 +installed = libmpc-1.4.1-1-x86_64 +installed = libmpdclient-2.23-1-x86_64 +installed = libmspub-0.1.5-1-x86_64 +installed = libmwaw-0.3.22-4-x86_64 +installed = libmysofa-1.3.4-1-x86_64 +installed = libnatpmp-20230423-3-x86_64 +installed = libnautilus-extension-50.2.2-1-x86_64 +installed = libnbd-1.24.2-2-x86_64 +installed = libndp-1.9-1-x86_64 +installed = libnetfilter_conntrack-1.1.1-1-x86_64 +installed = libnewt-0.52.25-2-x86_64 +installed = libnfnetlink-1.0.2-2-x86_64 +installed = libnfs-6.0.2-5-x86_64 +installed = libnftnl-1.3.1-1-x86_64 +installed = libnghttp2-1.69.0-1-x86_64 +installed = libnghttp3-1.16.0-1-x86_64 +installed = libngtcp2-1.23.0-1-x86_64 +installed = libnice-0.1.23-1-x86_64 +installed = libnl-3.12.0-1-x86_64 +installed = libnm-1.56.1-1-x86_64 +installed = libnma-1.10.6-3-x86_64 +installed = libnma-common-1.10.6-3-x86_64 +installed = libnotify-0.8.8-1-x86_64 +installed = libnsl-2.0.1-2-x86_64 +installed = libnumbertext-1.0.11-3-x86_64 +installed = libnvme-1.16.2-1-x86_64 +installed = libobjc-16.1.1+r12+g301eb08fa2c5-1-x86_64 +installed = libodfgen-0.1.8-5-x86_64 +installed = libogg-1.3.6-1-x86_64 +installed = libopenmpt-0.8.7-1-x86_64 +installed = liborcus-0.21.0-6-x86_64 +installed = libosinfo-1.12.0-3-x86_64 +installed = libp11-0.4.18-1-x86_64 +installed = libp11-kit-0.26.2-1-x86_64 +installed = libpagemaker-0.0.4-5-x86_64 +installed = libpaper-2.2.8-1-x86_64 +installed = libpcap-1.10.6-1-x86_64 +installed = libpciaccess-0.19-1-x86_64 +installed = libpeas-1.38.1-1-x86_64 +installed = libpgm-5.3.128-4-x86_64 +installed = libpipewire-1:1.6.7-1-x86_64 +installed = libplacebo-7.360.1-2-x86_64 +installed = libplist-2.7.0-3-x86_64 +installed = libpng-1.6.58-1-x86_64 +installed = libportal-0.10.0-1-x86_64 +installed = libportal-gtk4-0.10.0-1-x86_64 +installed = libppd-2.1.1-2-x86_64 +installed = libproxy-0.5.12-1-x86_64 +installed = libpsl-0.21.5-2-x86_64 +installed = libpulse-17.0+r98+gb096704c0-1-x86_64 +installed = libpwquality-1.4.5-7-x86_64 +installed = libqalculate-5.11.0-1-x86_64 +installed = libquadmath-16.1.1+r12+g301eb08fa2c5-1-x86_64 +installed = libqxp-0.0.3-1-x86_64 +installed = libraqm-0.10.5-1-x86_64 +installed = libraw-0.22.1-1-x86_64 +installed = libraw1394-2.1.2-4-x86_64 +installed = libre-trainsim-bin-0.8-1-x86_64 +installed = libreoffice-fresh-26.2.4-2-x86_64 +installed = librevenge-0.0.5-4-x86_64 +installed = librewolf-bin-1:151.0.4_1-1-x86_64 +installed = librsvg-2:2.62.3-1-x86_64 +installed = librsync-1:2.3.4-2-x86_64 +installed = libsamplerate-0.2.2-3-x86_64 +installed = libsasl-2.1.28-5-x86_64 +installed = libseccomp-2.6.0-1-x86_64 +installed = libsecret-0.21.7-1-x86_64 +installed = libshout-1:2.4.6-5-x86_64 +installed = libsigc++-2.12.2-1-x86_64 +installed = libsigc++-3.0-3.8.1-1-x86_64 +installed = libslirp-4.9.3-1-x86_64 +installed = libsm-1.2.6-1-x86_64 +installed = libsndfile-1.2.2-4-x86_64 +installed = libsodium-1.0.22-1-x86_64 +installed = libsoup3-3.6.6-2-x86_64 +installed = libsoxr-0.1.3-4-x86_64 +installed = libspelling-0.4.10-1-x86_64 +installed = libspnav-1.2-1-x86_64 +installed = libssh-0.12.0-1-x86_64 +installed = libssh2-1.11.1-1-x86_64 +installed = libstaroffice-0.0.7-5-x86_64 +installed = libstdc++-16.1.1+r12+g301eb08fa2c5-1-x86_64 +installed = libstemmer-3.1.1-1-x86_64 +installed = libsysprof-capture-50.0-2-x86_64 +installed = libtasn1-4.21.0-1-x86_64 +installed = libtatsu-1.0.5-1-x86_64 +installed = libteam-1.32-3-x86_64 +installed = libthai-0.1.30-1-x86_64 +installed = libtheora-1.2.0-1-x86_64 +installed = libtiff-4.7.1-2-x86_64 +installed = libtirpc-1.3.7-1-x86_64 +installed = libtommath-1.3.0-2-x86_64 +installed = libtool-2.6.1-1-x86_64 +installed = libtpms-0.10.2-1-x86_64 +installed = libtraceevent-1:1.9.0-1-x86_64 +installed = libtracefs-1.8.3-1-x86_64 +installed = libtsan-16.1.1+r12+g301eb08fa2c5-1-x86_64 +installed = libubsan-16.1.1+r12+g301eb08fa2c5-1-x86_64 +installed = libunibreak-7.0-1-x86_64 +installed = libunistring-1.4.2-1-x86_64 +installed = libunwind-1.8.2-1-x86_64 +installed = liburing-2.14-1-x86_64 +installed = libusb-1.0.30-1-x86_64 +installed = libusbmuxd-2.1.1-2-x86_64 +installed = libuv-1.52.1-1-x86_64 +installed = libva-2.23.0-1-x86_64 +installed = libvdpau-1.5-4-x86_64 +installed = libverto-0.3.2-6-x86_64 +installed = libvirt-1:12.4.0-1-x86_64 +installed = libvirt-glib-5.0.0-3-x86_64 +installed = libvirt-python-1:12.4.0-1-x86_64 +installed = libvisio-0.1.11-1-x86_64 +installed = libvorbis-1.3.7-4-x86_64 +installed = libvpl-2.16.0-2-x86_64 +installed = libvpx-1.16.0-3-x86_64 +installed = libwacom-2.19.0-1-x86_64 +installed = libwbclient-2:4.24.3-1-x86_64 +installed = libwebp-1.6.0-2-x86_64 +installed = libwireplumber-0.5.15-1-x86_64 +installed = libwpd-0.10.3-6-x86_64 +installed = libwps-0.4.14-4-x86_64 +installed = libx11-1.8.13-1-x86_64 +installed = libxau-1.0.12-1-x86_64 +installed = libxcb-1.17.0-1-x86_64 +installed = libxcomposite-0.4.7-1-x86_64 +installed = libxcrypt-4.5.2-1-x86_64 +installed = libxcrypt-compat-4.5.2-1-x86_64 +installed = libxcursor-1.2.3-1-x86_64 +installed = libxcvt-0.1.3-1-x86_64 +installed = libxdamage-1.1.7-1-x86_64 +installed = libxdmcp-1.1.5-2-x86_64 +installed = libxdp-1.6.3-1-x86_64 +installed = libxext-1.3.7-1-x86_64 +installed = libxfixes-6.0.2-1-x86_64 +installed = libxfont2-2.0.7-1-x86_64 +installed = libxft-2.3.9-1-x86_64 +installed = libxi-1.8.3-1-x86_64 +installed = libxinerama-1.1.6-1-x86_64 +installed = libxkbcommon-1.13.2-1-x86_64 +installed = libxkbcommon-x11-1.13.2-1-x86_64 +installed = libxkbfile-1.2.0-1-x86_64 +installed = libxklavier-5.4-7-x86_64 +installed = libxml2-2.15.3-1-x86_64 +installed = libxmlb-0.3.27-1-x86_64 +installed = libxrandr-1.5.5-1-x86_64 +installed = libxrender-0.9.12-1-x86_64 +installed = libxshmfence-1.3.3-1-x86_64 +installed = libxslt-1.1.45-2-x86_64 +installed = libxss-1.2.5-1-x86_64 +installed = libxt-1.3.1-1-x86_64 +installed = libxtst-1.2.5-1-x86_64 +installed = libxv-1.0.13-1-x86_64 +installed = libxxf86vm-1.1.7-1-x86_64 +installed = libyaml-0.2.5-3-x86_64 +installed = libyuv-r2426+464c51a03-1-x86_64 +installed = libzip-1.11.4-1-x86_64 +installed = libzmf-0.0.2-20-x86_64 +installed = licenses-20240728-1-any +installed = lilv-0.28.0-1-x86_64 +installed = linux-api-headers-7.1-1-x86_64 +installed = linux-firmware-20260519-1-any +installed = linux-firmware-amdgpu-20260519-1-any +installed = linux-firmware-atheros-20260519-1-any +installed = linux-firmware-broadcom-20260519-1-any +installed = linux-firmware-cirrus-20260519-1-any +installed = linux-firmware-intel-20260519-1-any +installed = linux-firmware-mediatek-20260519-1-any +installed = linux-firmware-nvidia-20260519-1-any +installed = linux-firmware-other-20260519-1-any +installed = linux-firmware-radeon-20260519-1-any +installed = linux-firmware-realtek-20260519-1-any +installed = linux-firmware-whence-20260519-1-any +installed = linux-zen-7.0.12.zen1-1-x86_64 +installed = linux-zen-headers-7.0.12.zen1-1-x86_64 +installed = litehtml0.9-0.9-2-x86_64 +installed = littlenavmap-bin-3.0.18-1-x86_64 +installed = lld-22.1.6-1-x86_64 +installed = llvm-22.1.6-1-x86_64 +installed = llvm-libs-22.1.6-1-x86_64 +installed = lm_sensors-1:3.6.2-1-x86_64 +installed = lmdb-0.9.35-1-x86_64 +installed = localsearch-3.11.1-1-x86_64 +installed = log4cplus-2.1.2-1-x86_64 +installed = loupe-50.0-1-x86_64 +installed = lpsolve-5.5.2.14-1-x86_64 +installed = lsb-release-2.0.r55.a25a4fc-1-any +installed = lsof-4.99.6-1-x86_64 +installed = lua-5.5.0-2-x86_64 +installed = lua54-5.4.8-6-x86_64 +installed = lv2-1.18.10-2-x86_64 +installed = lz4-1:1.10.0-2-x86_64 +installed = lzo-2.10-5-x86_64 +installed = m4-1.4.21-2-x86_64 +installed = mailcap-2.1.54-2-any +installed = make-4.4.1-3-x86_64 +installed = manifold-3.4.1-1-x86_64 +installed = materialx-1.39.4-5-x86_64 +installed = md4c-0.5.3-1-x86_64 +installed = mdadm-4.6-2-x86_64 +installed = mesa-1:26.1.3-2-x86_64 +installed = meson-1.11.1-3-any +installed = mingw-w64-binutils-2.46.0-1-x86_64 +installed = mingw-w64-crt-14.0.0-1-any +installed = mingw-w64-gcc-16.1.0-1-x86_64 +installed = mingw-w64-headers-14.0.0-1-any +installed = mingw-w64-winpthreads-14.0.0-1-any +installed = miniupnpc-2.3.3-3-x86_64 +installed = minizip-1:1.3.2-3-x86_64 +installed = minizip-ng-4.2.1-1-x86_64 +installed = mkinitcpio-41-4-any +installed = mkinitcpio-busybox-1.36.1-1-x86_64 +installed = mobile-broadband-provider-info-20251101-1-any +installed = mpdecimal-4.0.1-3-x86_64 +installed = mpfr-4.2.2-1-x86_64 +installed = mpg123-1.33.5-1-x86_64 +installed = mtdev-1.1.7-1-x86_64 +installed = mtools-1:4.0.49-1-x86_64 +installed = muparser-2.3.5-2-x86_64 +installed = nano-9.0-1-x86_64 +installed = nanobind-2.13.0-1-any +installed = nautilus-50.2.2-1-x86_64 +installed = ncurses-6.6-2-x86_64 +installed = ndctl-84-1-x86_64 +installed = neon-0.37.1-1-x86_64 +installed = nettle-4.0-1-x86_64 +installed = network-manager-applet-1.36.0-2-x86_64 +installed = networkmanager-1.56.1-1-x86_64 +installed = nextcloud-client-2:33.0.5-1-x86_64 +installed = nftables-1:1.1.6-3-x86_64 +installed = ninja-1.13.2-3-x86_64 +installed = niri-26.04-1-x86_64 +installed = nm-connection-editor-1.36.0-2-x86_64 +installed = node-gyp-13.0.0-1-any +installed = nodejs-26.2.0-1-x86_64 +installed = nodejs-nopt-10.0.1-1-any +installed = noto-fonts-emoji-1:2.051-1-any +installed = npm-11.16.0-1-any +installed = npth-1.8-1-x86_64 +installed = nspr-4.39-1-x86_64 +installed = nss-3.125-1-x86_64 +installed = numactl-2.0.19-1-x86_64 +installed = nwg-look-1.1.1-3-x86_64 +installed = obsidian-1.12.7-2-x86_64 +installed = ocl-icd-2.3.4-1-x86_64 +installed = onetbb-2023.0.0-1-x86_64 +installed = oniguruma-6.9.10-1-x86_64 +installed = openal-1.25.2-1-x86_64 +installed = opencolorio-2.5.1-1-x86_64 +installed = opencore-amr-0.1.6-2-x86_64 +installed = openexr-3.4.12-3-x86_64 +installed = openh264-2.6.0-2-x86_64 +installed = openimagedenoise-2.4.1-2-x86_64 +installed = openimageio-3.1.12.1-3-x86_64 +installed = openjpeg2-2.5.4-1-x86_64 +installed = openjph-0.29.0-1-x86_64 +installed = openpgl-0.7.1-1-x86_64 +installed = openpomodoro-0.3.0-2-x86_64 +installed = openshadinglanguage-1.15.3.0-1-x86_64 +installed = openssh-10.3p1-1-x86_64 +installed = openssl-3.6.3-1-x86_64 +installed = opensubdiv-3.7.0-1-x86_64 +installed = openvdb-13.0.0-1-x86_64 +installed = openxr-1.1.60-1-x86_64 +installed = opus-1.6.1-1-x86_64 +installed = orc-0.4.42-1-x86_64 +installed = osinfo-db-20251212-1-any +installed = ostree-2025.7-3-x86_64 +installed = otf-font-awesome-7.2.0-1-any +installed = p11-kit-0.26.2-1-x86_64 +installed = pacman-7.1.0.r9.g54d9411-2-x86_64 +installed = pacman-contrib-1.13.1-1-x86_64 +installed = pacman-mirrorlist-20260610-1-any +installed = pahole-1:1.31-2-x86_64 +installed = pam-1.7.2-2-x86_64 +installed = pambase-20260616-1-any +installed = pango-1:1.57.1-1-x86_64 +installed = pangomm-2.46.4-3-x86_64 +installed = pangomm-2.48-2.56.1-2-x86_64 +installed = papers-50.2-1-x86_64 +installed = papirus-folders-1.14.0-1-any +installed = papirus-icon-theme-20250501-1-any +installed = parallel-20260522-1-any +installed = parted-3.7-1-x86_64 +installed = paru-2.1.0-2-x86_64 +installed = patch-2.8-1-x86_64 +installed = pavucontrol-1:6.2-1-x86_64 +installed = pciutils-3.15.0-1-x86_64 +installed = pcre-8.45-4-x86_64 +installed = pcre2-10.47-1-x86_64 +installed = pcsclite-2.5.1-1-x86_64 +installed = pcsx2-git-2.7.419.r0.ge0566546c6-1-x86_64 +installed = perl-5.42.2-1-x86_64 +installed = perl-error-0.17030-3-any +installed = perl-mailtools-2.22-3-any +installed = perl-timedate-2.35-1-any +installed = pfetch-1.11.0-1-any +installed = phodav-3.0-4-x86_64 +installed = pinentry-1.3.2-2-x86_64 +installed = pipewire-1:1.6.7-1-x86_64 +installed = pipewire-audio-1:1.6.7-1-x86_64 +installed = pipewire-jack-1:1.6.7-1-x86_64 +installed = pipewire-pulse-1:1.6.7-1-x86_64 +installed = pipewire-session-manager-1:1.6.7-1-x86_64 +installed = pixman-0.46.4-1-x86_64 +installed = pkgconf-2.5.1-1-x86_64 +installed = playerctl-2.4.1-5-x86_64 +installed = plutosvg-0.0.8-1-x86_64 +installed = plutovg-git-1.3.3+12.r311.20260606.dd73459-1-x86_64 +installed = polkit-127-3-x86_64 +installed = polkit-gnome-0.105-12-x86_64 +installed = poppler-26.06.0-1-x86_64 +installed = poppler-data-0.4.12-2-any +installed = poppler-glib-26.06.0-1-x86_64 +installed = popt-1.19-2-x86_64 +installed = portaudio-1:19.7.0-4-x86_64 +installed = potrace-1.16-5-x86_64 +installed = pps-tools-1.0.3-2-x86_64 +installed = procps-ng-4.0.6-2-x86_64 +installed = psmisc-23.7-2-x86_64 +installed = ptex-2.5.2-1-x86_64 +installed = pugixml-1.16-1-x86_64 +installed = pyalpm-0.11.1-1-x86_64 +installed = pybind11-3.0.4-1-any +installed = pyside6-6.11.1-1-x86_64 +installed = pystring-1.2.0-1-x86_64 +installed = python-3.14.5-1-x86_64 +installed = python-autocommand-2.2.2-9-any +installed = python-build-1.4.3-1-any +installed = python-cairo-1.29.0-2-x86_64 +installed = python-certifi-2026.06.17-1-any +installed = python-charset-normalizer-3.4.7-1-x86_64 +installed = python-editables-0.6-1-any +installed = python-gobject-3.56.3-1-x86_64 +installed = python-hatchling-1.30.1-1-any +installed = python-idna-3.18-1-any +installed = python-imageio-2.37.3-1-any +installed = python-imageio-ffmpeg-0.6.0-2-any +installed = python-installer-1.0.0-1-any +installed = python-jaraco.collections-5.1.0-3-any +installed = python-jaraco.context-6.1.2-1-any +installed = python-jaraco.functools-4.1.0-3-any +installed = python-jaraco.text-4.0.0-4-any +installed = python-jinja-1:3.1.6-3-any +installed = python-markupsafe-3.0.3-1-x86_64 +installed = python-more-itertools-11.1.0-1-any +installed = python-numpy-2.4.6-1-x86_64 +installed = python-opengl-3.1.10-3-any +installed = python-packaging-26.2-1-any +installed = python-pathspec-1.1.1-1-any +installed = python-pillow-12.2.0-1-x86_64 +installed = python-pkg_resources-81.0.0-1-any +installed = python-platformdirs-4.10.0-1-any +installed = python-pluggy-1.6.0-3-any +installed = python-pyproject-hooks-1.2.0-6-any +installed = python-pyqt6-6.11.0-2-x86_64 +installed = python-pyqt6-sip-13.11.1-1-x86_64 +installed = python-requests-2.34.2-1-any +installed = python-screeninfo-0.8.1-2-any +installed = python-setproctitle-1.3.7-2-x86_64 +installed = python-setuptools-1:82.0.1-1-any +installed = python-tqdm-4.68.2-1-any +installed = python-trove-classifiers-2026.6.1.19-1-any +installed = python-typing_extensions-4.15.0-3-any +installed = python-urllib3-2.7.0-1-any +installed = python-wheel-0.47.0-1-any +installed = qemu-audio-alsa-11.0.1-1-x86_64 +installed = qemu-audio-dbus-11.0.1-1-x86_64 +installed = qemu-audio-jack-11.0.1-1-x86_64 +installed = qemu-audio-oss-11.0.1-1-x86_64 +installed = qemu-audio-pa-11.0.1-1-x86_64 +installed = qemu-audio-pipewire-11.0.1-1-x86_64 +installed = qemu-audio-sdl-11.0.1-1-x86_64 +installed = qemu-audio-spice-11.0.1-1-x86_64 +installed = qemu-base-11.0.1-1-x86_64 +installed = qemu-block-curl-11.0.1-1-x86_64 +installed = qemu-block-dmg-11.0.1-1-x86_64 +installed = qemu-block-nfs-11.0.1-1-x86_64 +installed = qemu-block-ssh-11.0.1-1-x86_64 +installed = qemu-chardev-spice-11.0.1-1-x86_64 +installed = qemu-common-11.0.1-1-x86_64 +installed = qemu-desktop-11.0.1-1-x86_64 +installed = qemu-hw-display-qxl-11.0.1-1-x86_64 +installed = qemu-hw-display-virtio-gpu-11.0.1-1-x86_64 +installed = qemu-hw-display-virtio-gpu-gl-11.0.1-1-x86_64 +installed = qemu-hw-display-virtio-gpu-pci-11.0.1-1-x86_64 +installed = qemu-hw-display-virtio-gpu-pci-gl-11.0.1-1-x86_64 +installed = qemu-hw-display-virtio-gpu-pci-rutabaga-11.0.1-1-x86_64 +installed = qemu-hw-display-virtio-gpu-rutabaga-11.0.1-1-x86_64 +installed = qemu-hw-display-virtio-vga-11.0.1-1-x86_64 +installed = qemu-hw-display-virtio-vga-gl-11.0.1-1-x86_64 +installed = qemu-hw-display-virtio-vga-rutabaga-11.0.1-1-x86_64 +installed = qemu-hw-uefi-vars-11.0.1-1-x86_64 +installed = qemu-hw-usb-host-11.0.1-1-x86_64 +installed = qemu-hw-usb-redirect-11.0.1-1-x86_64 +installed = qemu-hw-usb-smartcard-11.0.1-1-x86_64 +installed = qemu-img-11.0.1-1-x86_64 +installed = qemu-system-x86-11.0.1-1-x86_64 +installed = qemu-system-x86-firmware-11.0.1-1-x86_64 +installed = qemu-ui-curses-11.0.1-1-x86_64 +installed = qemu-ui-dbus-11.0.1-1-x86_64 +installed = qemu-ui-egl-headless-11.0.1-1-x86_64 +installed = qemu-ui-gtk-11.0.1-1-x86_64 +installed = qemu-ui-opengl-11.0.1-1-x86_64 +installed = qemu-ui-sdl-11.0.1-1-x86_64 +installed = qemu-ui-spice-app-11.0.1-1-x86_64 +installed = qemu-ui-spice-core-11.0.1-1-x86_64 +installed = qemu-vhost-user-gpu-11.0.1-1-x86_64 +installed = qpdf-12.3.2-2-x86_64 +installed = qt5-base-5.15.19+kde+r96-1-x86_64 +installed = qt5-declarative-5.15.19+kde+r23-1-x86_64 +installed = qt5-svg-5.15.19+kde+r5-1-x86_64 +installed = qt5-translations-5.15.19-1-any +installed = qt6-5compat-6.11.1-1-x86_64 +installed = qt6-base-6.11.1-1-x86_64 +installed = qt6-declarative-6.11.1-3-x86_64 +installed = qt6-positioning-6.11.1-1-x86_64 +installed = qt6-shadertools-6.11.1-1-x86_64 +installed = qt6-svg-6.11.1-1-x86_64 +installed = qt6-tools-6.11.1-3-x86_64 +installed = qt6-translations-6.11.1-1-any +installed = qt6-wayland-6.11.1-1-x86_64 +installed = qt6-webchannel-6.11.1-1-x86_64 +installed = qt6-webengine-6.11.1-4-x86_64 +installed = qt6-websockets-6.11.1-1-x86_64 +installed = qtkeychain-qt6-0.16.0-1-x86_64 +installed = rapidyaml-0.11.1-2-x86_64 +installed = raptor-2.0.16-9-x86_64 +installed = rasqal-1:0.9.33-9-x86_64 +installed = rav1e-0.8.1-2-x86_64 +installed = rdma-core-63.0-2-x86_64 +installed = re2-2:2025.11.05-5-x86_64 +installed = readline-8.3.003-1-x86_64 +installed = redland-1:1.0.17-10-x86_64 +installed = reflector-2023-5-any +installed = retroarch-1.22.2-1-x86_64 +installed = rhash-1.4.6-1-x86_64 +installed = ripgrep-15.1.0-4-x86_64 +installed = robin-map-1.4.1-1-x86_64 +installed = rofi-2.0.0-1-x86_64 +installed = rofi-calc-2.5.1-1-x86_64 +installed = rofi-themes-collection-git-1:r28.43ec2f5-1-any +installed = rpcs3-bin-0.0.41.19433-1-x86_64 +installed = rsync-3.4.4-1-x86_64 +installed = rtkit-0.14-1-x86_64 +installed = rubberband-4.0.0-2-x86_64 +installed = run-parts-5.23.2-1-x86_64 +installed = rutabaga-ffi-0.1.75-1-x86_64 +installed = sbc-2.2-1-x86_64 +installed = scdoc-1.11.4-1-x86_64 +installed = scenefx0.4-0.4.1-2-x86_64 +installed = sdbus-cpp-2.3.1-1-x86_64 +installed = sdl2-compat-2.32.70-1-x86_64 +installed = sdl2_image-2.8.12-1-x86_64 +installed = sdl3-3.4.10-1-x86_64 +installed = seabios-1.17.0-2-any +installed = seatd-0.9.3-1-x86_64 +installed = sed-4.10-1-x86_64 +installed = semver-7.8.4-1-any +installed = serd-0.32.10-1-x86_64 +installed = shaderc-2026.2-1-x86_64 +installed = shadow-4.19.4.arch1-1-x86_64 +installed = shared-mime-info-2.4-3-x86_64 +installed = shiboken6-6.11.1-1-x86_64 +installed = signal-desktop-8.15.0-1-x86_64 +installed = simdjson-1:4.6.4-1-x86_64 +installed = slang-2.3.3-4-x86_64 +installed = slurp-1.5.0-2-x86_64 +installed = smbclient-2:4.24.3-1-x86_64 +installed = snappy-1.2.2-3-x86_64 +installed = sndio-1.10.0-1-x86_64 +installed = sord-0.16.22-1-x86_64 +installed = sound-theme-freedesktop-0.8-6-any +installed = source-highlight-3.1.9-18-x86_64 +installed = spandsp-0.0.6-7-x86_64 +installed = spdlog-1.17.0-2-x86_64 +installed = speex-1.2.1-2-x86_64 +installed = speexdsp-1.2.1-2-x86_64 +installed = spice-0.16.0-2-x86_64 +installed = spice-gtk-0.42-5-x86_64 +installed = spice-protocol-0.14.5-1-any +installed = spirv-tools-1:1.4.350.0-1-x86_64 +installed = sqlite-3.53.2-1-x86_64 +installed = sratom-0.6.22-1-x86_64 +installed = srt-1.5.5-1-x86_64 +installed = startup-notification-0.12-9-x86_64 +installed = steam-1.0.0.85-7-x86_64 +installed = steam-devices-1.0.0.85-7-x86_64 +installed = sudo-1.9.17.p2-6-x86_64 +installed = suitesparse-7.12.2-2-x86_64 +installed = svt-av1-4.1.0-1-x86_64 +installed = swaybg-1.2.2-1-x86_64 +installed = swayfx-0.5.3-4-x86_64 +installed = swig-4.4.1-1-x86_64 +installed = swtpm-0.10.1-2-x86_64 +installed = systemd-260.2-2-x86_64 +installed = systemd-libs-260.2-2-x86_64 +installed = systemd-sysvcompat-260.2-2-x86_64 +installed = taglib-2.3-1-x86_64 +installed = talloc-2.4.4-1-x86_64 +installed = tar-1.35-2-x86_64 +installed = tdb-1.4.15-1-x86_64 +installed = tevent-1:0.17.1-2-x86_64 +installed = texinfo-7.3-1-x86_64 +installed = thunderbird-151.0.1-1-x86_64 +installed = thunderbird-gnome-theme-git-r95.c61b897-1-any +installed = thunderbird-i18n-de-151.0.1-1-x86_64 +installed = thunderbird-ublock-origin-1.70.0-1-any +installed = timeshift-25.12.4-1-x86_64 +installed = timeshift-autosnap-0.10.0-1-any +installed = tinysparql-3.11.1-1-x86_64 +installed = tomlplusplus-3.4.0-2-x86_64 +installed = totem-pl-parser-3.26.7-1-x86_64 +installed = tpm2-tss-4.1.3-1-x86_64 +installed = transmission-gtk-4.1.1-1-x86_64 +installed = tslib-1.24-1-x86_64 +installed = ttc-iosevka-34.6.1-1-any +installed = ttc-iosevka-aile-34.6.1-1-any +installed = ttc-iosevka-curly-34.6.1-1-any +installed = ttc-iosevka-curly-slab-34.6.1-1-any +installed = ttc-iosevka-etoile-34.6.1-1-any +installed = ttf-iosevka-nerd-3.4.0-2-any +installed = ttf-iosevkaterm-nerd-3.4.0-2-any +installed = ttf-liberation-2.1.5-2-any +installed = ttf-nerd-fonts-symbols-3.4.0-1-any +installed = ttf-nerd-fonts-symbols-common-3.4.0-1-any +installed = twolame-0.4.0-4-x86_64 +installed = tzdata-2026b-1-x86_64 +installed = uchardet-0.0.8-4-x86_64 +installed = udisks2-2.11.1-2-x86_64 +installed = unzip-6.0-23-x86_64 +installed = upower-1.91.2-1-x86_64 +installed = usbredir-0.15.0-1-x86_64 +installed = usbutils-019-1-x86_64 +installed = usd-26.05-3-x86_64 +installed = util-linux-2.42.2-1-x86_64 +installed = util-linux-libs-2.42.2-1-x86_64 +installed = uv-0.11.22-1-x86_64 +installed = v4l-utils-1.32.0-2-x86_64 +installed = vapoursynth-77-1-x86_64 +installed = vde2-2.3.3-8-x86_64 +installed = vid.stab-1.1.1-2-x86_64 +installed = vim-9.2.0670-1-x86_64 +installed = vim-runtime-9.2.0670-1-x86_64 +installed = virglrenderer-1.3.0-2-x86_64 +installed = virt-install-5.1.0-3-any +installed = virt-manager-5.1.0-3-any +installed = virtiofsd-1.13.3-1-x86_64 +installed = vmaf-3.1.0-1-x86_64 +installed = volume_key-0.3.12-12-x86_64 +installed = vte-common-0.84.0-1-x86_64 +installed = vte3-0.84.0-1-x86_64 +installed = vulkan-headers-1:1.4.350.0-1-any +installed = vulkan-icd-loader-1.4.350.0-1-x86_64 +installed = vulkan-mesa-implicit-layers-1:26.1.3-2-x86_64 +installed = vulkan-radeon-1:26.1.3-2-x86_64 +installed = vulkan-tools-1.4.350.0-1-x86_64 +installed = wavpack-5.9.0-1-x86_64 +installed = waybar-0.15.0-2-x86_64 +installed = wayland-1.25.0-1-x86_64 +installed = wayland-protocols-1.49-1-any +installed = waypaper-2.8-1-any +installed = webkit2gtk-4.1-2.52.4-1-x86_64 +installed = webkitgtk-6.0-2.52.4-1-x86_64 +installed = webrtc-audio-processing-1-1.3-5-x86_64 +installed = wget-1.25.0-5-x86_64 +installed = which-2.25-1-x86_64 +installed = wine-11.11-1-x86_64 +installed = winetricks-20260125-2-any +installed = wireplumber-0.5.15-1-x86_64 +installed = wl-clipboard-1:2.3.0-1-x86_64 +installed = wlroots0.19-0.19.3-1-x86_64 +installed = wlroots0.20-0.20.1-1-x86_64 +installed = wlsunset-0.4.0-1-x86_64 +installed = woff2-1.0.2-6-x86_64 +installed = wolfssl-5.9.1-1-x86_64 +installed = wpa_supplicant-2:2.11-5-x86_64 +installed = x264-3:0.165.r3222.b35605a-2-x86_64 +installed = x265-4.1-1-x86_64 +installed = xapp-3.2.2-1-x86_64 +installed = xapp-symbolic-icons-1.1.0-1-any +installed = xcb-imdkit-1.0.9-1-x86_64 +installed = xcb-proto-1.17.0-4-any +installed = xcb-util-0.4.1-2-x86_64 +installed = xcb-util-cursor-0.1.6-1-x86_64 +installed = xcb-util-errors-1.0.1-2-x86_64 +installed = xcb-util-image-0.4.1-3-x86_64 +installed = xcb-util-keysyms-0.4.1-5-x86_64 +installed = xcb-util-renderutil-0.3.10-2-x86_64 +installed = xcb-util-wm-0.4.2-2-x86_64 +installed = xcur2png-0.7.1-9-x86_64 +installed = xdg-dbus-proxy-0.1.7-1-x86_64 +installed = xdg-desktop-portal-1.22.1-1-x86_64 +installed = xdg-desktop-portal-gtk-1.15.3-1-x86_64 +installed = xdg-desktop-portal-hyprland-1.3.12-2-x86_64 +installed = xdg-terminal-exec-0.14.0-1-any +installed = xdg-user-dirs-0.20-1-x86_64 +installed = xdg-user-dirs-gtk-0.16-1-x86_64 +installed = xdg-utils-1.2.1-2-any +installed = xdotool-4.20260303.1-1-x86_64 +installed = xenia-canary-bin-02d2cb5-2-x86_64 +installed = xenia-edge-bin-20260614125312.ce9fb29-1-x86_64 +installed = xkeyboard-config-2.47-1-any +installed = xmlsec-1.3.11-1-x86_64 +installed = xorg-fonts-encodings-1.1.0-2-any +installed = xorg-server-common-21.1.23-1-x86_64 +installed = xorg-setxkbmap-1.3.5-1-x86_64 +installed = xorg-xkbcomp-1.5.0-1-x86_64 +installed = xorg-xprop-1.2.8-1-x86_64 +installed = xorg-xrandr-1.5.4-1-x86_64 +installed = xorg-xwayland-24.1.12-1-x86_64 +installed = xorgproto-2025.1-1-any +installed = xvidcore-1.3.7-3-x86_64 +installed = xxhash-0.8.3-1-x86_64 +installed = xz-5.8.3-1-x86_64 +installed = yaml-cpp-0.9.0-1-x86_64 +installed = yazi-26.5.6-4-x86_64 +installed = zenity-4.2.2-1-x86_64 +installed = zeromq-4.3.5-3-x86_64 +installed = zimg-3.0.6-1-x86_64 +installed = zint-2.16.0-2-x86_64 +installed = zix-0.8.2-1-x86_64 +installed = zlib-1:1.3.2-3-x86_64 +installed = zlib-ng-2.3.3-1-x86_64 +installed = zram-generator-1.2.1-1-x86_64 +installed = zsh-5.9.1-1-x86_64 +installed = zstd-1.5.7-3-x86_64 +installed = zxing-cpp-3.0.2-1-x86_64 diff --git a/packaging/taninux/pkg/taninux/.MTREE b/packaging/taninux/pkg/taninux/.MTREE new file mode 100644 index 0000000000000000000000000000000000000000..fcd18b2fc138c0baad75ed112d825d580fb16bc7 GIT binary patch literal 15399 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.11 +Requires-Dist: textual>=0.86 +Provides-Extra: gui +Requires-Dist: pygobject>=3.50; (sys_platform == 'linux') and extra == 'gui' diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux-0.2.0.dist-info/RECORD b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux-0.2.0.dist-info/RECORD new file mode 100644 index 0000000..dbf0698 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux-0.2.0.dist-info/RECORD @@ -0,0 +1,144 @@ +../../../bin/taninux,sha256=EazMj8nYdwUA2FX1l7QoCaPTkVWapFfjFGcEZCxhIrA,209 +taninux-0.2.0.dist-info/METADATA,sha256=XD3zJwHDMe9igtaV3aWRihX-6oNUDE_QTSwBCwRFFQ8,349 +taninux-0.2.0.dist-info/RECORD,, +taninux-0.2.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87 +taninux-0.2.0.dist-info/entry_points.txt,sha256=a8OoA6CDS8mfUAEGXzS4OMI7W7KWNOpFqcqvKOqV7ZM,154 +taninux-0.2.0.dist-info/licenses/LICENSE,sha256=-5gWaMGKJ54oX8TYP7oeg2zITdTapzyWl9PP0tispuA,34674 +../../../bin/taninux-gtk,sha256=qVixIhGEFtuAtrhLWU-PAw2zyr2cMPSvQ-Z6-uO_Gzs,213 +taninux/__init__.py,sha256=Zn1KFblwuFHiDRdRAiRnDBRkbPttWh44jKa5zG2ov0E,22 +taninux/__main__.py,sha256=8yGxX4NhckPZsL2XmkmWw_t3hwocE4oOb9yvKKvl3cE,55 +taninux/app.py,sha256=iJeV-F80aLmGFCM0mmpqWkLiTnZWLtX-e1Ljdxo-6Uk,5285 +taninux/bundles/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +taninux/bundles/builtins.py,sha256=CXSetbLIsjXi_p8sMN3RGJD8nThQzS-9tbT_YeG2w8E,3936 +taninux/bundles/loader.py,sha256=ZtUEjyj130uUA78Kd62LBQFumUcFQ2g6Hub-MyAjOy4,1891 +taninux/bundles/pillar.py,sha256=aALPWIWw_d19FBUzKlwgPwNWQKAHeLDHVnQ63TatRmY,258 +taninux/bundles/screen.py,sha256=6exvk4HRYk0qSDYG_XG-2N-5qf-5i3XrSDfAE-YbTus,8603 +taninux/cli.py,sha256=xyAuZVY7kYSVf9ODkta9ZKGu64mZ2Ww-_4IJUuQpdUA,27737 +taninux/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +taninux/core/accessibility.py,sha256=laf7-c8D4OwYm9XHX8x_b9VXOgbs8eWHNbHYaz30s_k,1411 +taninux/core/account.py,sha256=JU14ExIQ4q_R2tGiO8syRUiuU72TWqcsLVV8QgDQ-Hw,3771 +taninux/core/appinfo.py,sha256=57faxBWrUvfReAGxngAURiljDU-KmVlqB_HPQPYZJj8,4819 +taninux/core/audio.py,sha256=mufj-cdaeACgR-zcxvzlh6biwmiWRtsD4HkrskDvDEE,4256 +taninux/core/avatar.py,sha256=e1Szn7KM8Z0bi__g2odm6WKJiaGkKffVTLnjZ7b-jVA,1942 +taninux/core/backup.py,sha256=USYELzHQGPqnJ3_qFQUXOuwWWRzwakzbgbF06Dc-NnU,2048 +taninux/core/bluetooth.py,sha256=TOg5WzeCl7X7PZ0KD8aSJQZAL9PS0cZzcMPsFROOKKY,2789 +taninux/core/bootloader.py,sha256=wtrQ6wUZkhelWX5Iw4_LwlydrELrS4ZnH8j5Kfo53pM,5339 +taninux/core/catalog.py,sha256=GwyWIrkuVP2emzKQjGWPQYIOTazyrBbYADoZlPERaWY,5576 +taninux/core/compositor.py,sha256=Zt2JeWO6eNC6q-zHblSSfUyJpZKRYFPkqJcxiK2XVAM,1090 +taninux/core/confirm.py,sha256=ICUvK01bi-0jpRW2lQz4zOoScZEKbI1m7I29f57DBBk,2518 +taninux/core/defaultapps.py,sha256=C-0_wcUCUrJiSZsbb1xbxBDGdaNZdy6ugBoKEZ8rqB4,3956 +taninux/core/display.py,sha256=s0IzBDxewwqw1jwtGfIijhsuAUG6mscgh2hfYwPpjAQ,9390 +taninux/core/firewall.py,sha256=0qf5iHMDUs3mjmThSJlXUDrJswteuaziUkCq0g29Frs,1201 +taninux/core/localization.py,sha256=8hMJb167JN7gVxiO025jU19kMZDJIb5CBWMiGvgtwQo,4146 +taninux/core/lock.py,sha256=kGaoHRKiZcNFChGa_J7A0NhQSh7LMun2m_zsHCjIEvc,684 +taninux/core/network.py,sha256=gLjaUXSTs9XRJnLapDwcXbdKVHTJ2-G--xAJ3vlX5mU,6327 +taninux/core/nextcloud.py,sha256=Ez6M-4_sI1KhkeoiYnZNuVxahG-B_XiaOxJJrF8R5EA,3952 +taninux/core/package_index.py,sha256=pHJmt1ua5k6krHFyyoxi8ZdFIjD80O0vsGNaeuN0_ok,2656 +taninux/core/packages.py,sha256=0SwtI5WmU6JJwk17Es4pCMkRcd4M7zL_bOfYJMnVVik,1230 +taninux/core/pacman.py,sha256=lIn7rvZxZN1LfzwD5j5rRDZgusSmvGvBm-bQc06VUao,813 +taninux/core/panel.py,sha256=1Gp9JhREXmN1Kdd1oxS5wojRk0LlMPVxswM-8Pa95hY,5381 +taninux/core/paths.py,sha256=F7i4rWlPeS6YnsQntzShmm2T-qubsN5ff0rpEwiNiag,712 +taninux/core/pkg.py,sha256=jtxtoC3kOmz-edu-Jn6d7Vn5_pu4i5UAFGgwq8S0uCE,4568 +taninux/core/power.py,sha256=hbmIlH2qxOnTSjdH0IE4LoZMnARfo-8Py9XEAXe-moE,8979 +taninux/core/printers.py,sha256=DzPVLiMg6Kr56jRDgZUAqcV4dtnpgBCxNvB1R0z0n9A,3814 +taninux/core/runner.py,sha256=hKuJXkyoW0iD5UGE5ylxPRjyp_jtEdZbeY43xOJ8UTs,1547 +taninux/core/search.py,sha256=p83NEtFAD9qcMZW1JL8G4fb3tCxqcYlZkEDJhsEbXVo,5165 +taninux/core/shares.py,sha256=Pcq82CJoQ_YWajGW-UhvhKFXWEQCrpLzTuDEid1lIzE,4624 +taninux/core/snapshot.py,sha256=9gQkT_gB2xj2VKLsCM_SvHFUh5SRcpG7vtUxbRM-9b4,3884 +taninux/core/state.py,sha256=-TwV-u68zVYC9D2svQh521839lJJRN5C1Qbmtzg-RHY,1875 +taninux/core/storage.py,sha256=xyDhhGe6LJVKp8Az1SuBVjWY-g64Cdp6zsTXkfHhxhc,2190 +taninux/core/streaming.py,sha256=tL8v17lfqM_SJMWYjh1ATLehL8kfO9EYxgS9ebUgjhw,5731 +taninux/core/sudo.py,sha256=tIY6JAtwUCVczSX63tytccw31qYg1zQ1mmMC0QNslGk,1341 +taninux/core/sudo_modal.py,sha256=iOdsC1MDx8nDVL5fxwCNvjkxBH9ScciGDF4m7m5wz3c,2222 +taninux/core/sysinfo.py,sha256=hEazy-fZdt7J88ReClFG7iTYMKd_NnqvBfyTzQ8_L48,7594 +taninux/core/theme.py,sha256=fgd2Lo3ryeTXnWbI9iNh-GDuLWGR6rUSiyULU_2lRIs,1069 +taninux/core/trash.py,sha256=yThXpdeGVjH6_pSMFGkv5l4ExfY85Pz1nY4dfYkIy5o,3537 +taninux/core/wallpaper.py,sha256=S3v_BzFYIbz_qqhmShFh103UAn5eribhFhauLlYtMZY,4134 +taninux/flatpak/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +taninux/flatpak/pillar.py,sha256=Io0dKiboFu5EMixbYjT-T-0OXOecb2bP0Mi2q0SDGrE,251 +taninux/flatpak/screen.py,sha256=kpySJ5F-08RolQCwuSpLpTXSfGXlxzCfAjjcDchaBBQ,10842 +taninux/gui/README.md,sha256=X6n2y-FNlu8TPTlRWp-usASjocuvvWJ1UrZXgn45bQU,3496 +taninux/gui/__init__.py,sha256=ybAnNTWZ7EbAi6dJ3HKJRL5RkkRaA2Hx1dt99GRSS2w,396 +taninux/gui/__main__.py,sha256=jfT07opWgNkuL3XiJU45GcQNCsi326MWIe51odh2rSQ,59 +taninux/gui/accents.py,sha256=x0U6HdIEIhSZzbrACLIXhNL-uR2IiOedVtlb2-iDb9o,1011 +taninux/gui/app.py,sha256=KlOBzVTNIdZAzzfLjiP-8rjQVjwIae9tvjvoedtlCMU,1775 +taninux/gui/config.py,sha256=k8VXfMvevQ-NEOYjm3rTtmgdcs-cj02na_LUAirl5SE,1240 +taninux/gui/confirm.py,sha256=ShkFsYiQFr0lq79jBHvVo9N76mLWuW7iEPTzrnZxEZc,1410 +taninux/gui/console.py,sha256=vEIZcmxDorOwXdrDiQdNkz1cRdPF10vCR7DTHCSXaCc,4391 +taninux/gui/desktop.py,sha256=8b0So96mzSvPlfyAy4SOxc3HL5TuWaLbiE54N5cv0rg,8336 +taninux/gui/eww.py,sha256=qXHvMJx5TSRpirMSS2tP5CuOMLolhPD6vUlDCwxaypE,1175 +taninux/gui/helper/taninux-helper,sha256=3ivVllI4UeV8gzWDyLrYUXsWwZKfZ4dLjNrJuZ4ukIY,7172 +taninux/gui/pages/__init__.py,sha256=h7fzAyVZtifwQ7vfUgApWm9b3Br5VzQwrmVwhNFcsCA,6220 +taninux/gui/pages/about.py,sha256=9YYiV9zKpJmoB-wN6MQ_EbHyZ4kIfmfNxDtK3vk-b5M,1837 +taninux/gui/pages/accessibility.py,sha256=W4O23XwjozpVNzAxomj1i4fdl8Y6M2yVQsZvm23a4j4,1195 +taninux/gui/pages/appdetail.py,sha256=y6oqtQQlSqJ2VrthpLbsDjPnyGzFc524NEn1FqdItgA,7093 +taninux/gui/pages/audio.py,sha256=t45im3l3Hfp-gu55krwwa0sqUEaHPlYPqQ1lNkHOkt4,6129 +taninux/gui/pages/backup.py,sha256=IjztvLOUd5dJLLmRBhDTcdLRQCLbczuYljFbcIAHyPs,6792 +taninux/gui/pages/bluetooth.py,sha256=kWL8i4n8TCIgSzd5Qq3haxYGvJQ8NRnshlKWwiWToco,6197 +taninux/gui/pages/bundles.py,sha256=IAxg0wx-97LP4xguDee1L5oSzPKh8p-sM9b8K-Kp-H4,4774 +taninux/gui/pages/dashboard.py,sha256=0f-0Zvq_PoumqE_TJG0LVFIhWmsWxDYaTm6yXHsIL84,9496 +taninux/gui/pages/defaultapps.py,sha256=Rg91RtJEnOMi0nPnhe1DGESZB2N63huCDntLO6zwbJk,1481 +taninux/gui/pages/display.py,sha256=iovVXT058FkGKuHsvEIOsnV_aShu7RMXEiw5ldeWyxk,7024 +taninux/gui/pages/firewall.py,sha256=hJoOJS3KvrwYbVy-PC9R8WjmpebQSH45ToCTC0XWuXM,1530 +taninux/gui/pages/hub_home.py,sha256=ijjX5lu4dxh333Qr7ZC_2wRHjce7Pc-lkgo95I2w1jY,4508 +taninux/gui/pages/kernel.py,sha256=nrsVAMUkC89VkYwTcD8SVYJrEJ16gZSWgwA5DlruavM,5168 +taninux/gui/pages/locale.py,sha256=8DEbHFkbNgBXUNiFX5S28-q-RYO_4-xW8uHGE9Lq5RI,7993 +taninux/gui/pages/lock.py,sha256=P7vDu8e07mhMCayLssIErDSuj5eQI8mZsYYCy2XcNto,1185 +taninux/gui/pages/maintain.py,sha256=frkP7W5ilnHWY5ODp0wHcNtPbKYP-2PsUIxfqZG_Jrk,14839 +taninux/gui/pages/network.py,sha256=PVMIi9T4nHX4I3NcXFLly6jVWPbF1DD6PPXPP4nCfOg,6744 +taninux/gui/pages/panel.py,sha256=rTsKPsYV5l86GLYSBYt8AFzRY68QDgqpw4avXFtOwZ4,5930 +taninux/gui/pages/power.py,sha256=i3v4QCVB99-yJHpl2q31D2GNOlqv7HW9hkVI-Ur8vTk,6884 +taninux/gui/pages/printers.py,sha256=1Smm2SSqztfTJA9vbu426_jAjWAGB4a4ilUnxbc-qs8,5961 +taninux/gui/pages/settings.py,sha256=LJgtejider4GlFZM_t4BACxwNvicVOu6WQ6AMdP-zgM,7679 +taninux/gui/pages/shares.py,sha256=xJR9yZkjAeauvbWfYSFfMdqTLq-_504Et3Yy2PfZU00,10392 +taninux/gui/pages/storage.py,sha256=CZ3uU5cvoAoheIRXdaxAFlJackvFJCRqcPS3mtqpzaQ,2605 +taninux/gui/pages/store.py,sha256=D57KOhz6a1vJHwSVokAVCsUf86HMPUF9Vr_Qo9xXKrk,5297 +taninux/gui/pages/update.py,sha256=fIo8PoIS5Emd9CEi-melxReZOzlRG7WZJm4krm3o284,6245 +taninux/gui/pages/users.py,sha256=ROY6lS273tGYItY2zPyWp94bW5ESBxF90-eIkuyBlIU,7999 +taninux/gui/pages/vpn.py,sha256=0I8lGopdhG9T7d25IE-tKTUKbXqZFqoy9que9Tk0RLY,4391 +taninux/gui/pages/wallpaper.py,sha256=OwBupe30UoqlIpZoBApLPWc1As1N0aUqeEIxOG7TQ3g,11202 +taninux/gui/polkit/ch.gabrielevarano.Taninux.policy,sha256=FWDXgh4YtXJ-oRYh__c3flJoezTG0C9bM9UdRxP2uqM,1366 +taninux/gui/process.py,sha256=Oc6Yvm-ADStEUuRt6-ZxB1ATq3RCuQtDetkUexhhdbU,3850 +taninux/gui/style.py,sha256=VIhJzOWAAe5nNbLqVD1n7Pm526QwzGLVYhQVmcD7d2c,10179 +taninux/gui/window.py,sha256=_2YuuI89Isf4503--9TnlskNJ2tzEwg4wo3r1fT2_VA,5476 +taninux/health/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +taninux/health/pillar.py,sha256=TnD-oFRA_0cfEQ-CyV53QQWyj1ZG_LFWDFnIiI3QUp4,258 +taninux/health/screen.py,sha256=dh9yRpHwQwMgHW-K7K5hV19VJSKd0pWrASjV0YfRfuM,9293 +taninux/kernel/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +taninux/kernel/pillar.py,sha256=Yjd-_xxm2eq4teUfETzE6p-1S20t6swLteflYnSoNG0,256 +taninux/kernel/screen.py,sha256=KmksMHH1K8J0Pi6Qu5UfFLX-m21h7F69PJndBj7VFIQ,18564 +taninux/maintain/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +taninux/maintain/pillar.py,sha256=Br8K6Zo0SejRyR2WSTH-KwegIhuajLbqRGzxUDoKaZM,253 +taninux/maintain/scanners/__init__.py,sha256=tHph4nEMgf5-pzAQPp4ujAuoTw2bV_YLhW-wEO0T0bo,3257 +taninux/maintain/scanners/_util.py,sha256=WcNBG3uyBpxiSjB31JdMO08lZCit3erDOpopUzvWCd4,1018 +taninux/maintain/scanners/aur.py,sha256=tjBrzwvGySpdmNb67ga2GajG5KHtB2F7Mppb8VLg14w,2789 +taninux/maintain/scanners/base.py,sha256=QTCyeVo4eSITMoWqXsqmFd2NGfgNEX02ys_gD4tW72I,191 +taninux/maintain/scanners/dotfolders.py,sha256=RKZJl-4a9US8NjuoMpnmmViBAJfN0AeyHF7tJz7kwx0,4862 +taninux/maintain/scanners/flatpak.py,sha256=y1XNi2rNwjmUcSmUsNuEWgz1GJRdacCz481zVRUIFdE,5742 +taninux/maintain/scanners/pacman.py,sha256=-Efq8cKqLX6CNPfB3ziaT4yjr0NxgMaXL0yJQvrt4Ig,7965 +taninux/maintain/scanners/system.py,sha256=cO5E-zeKiuquNYQzxNsRt0S-WEUN71vOYqBm8vvcyLI,7945 +taninux/maintain/screen.py,sha256=vXkSMAx77xvuOgWLu_IMS8x6XIMnHJViouIIzj84GMI,19193 +taninux/maintain/types.py,sha256=eGNDf-daRLUXAktJhO-qSkAoMbiL8pVbNXB7x3GEFBI,1772 +taninux/overview/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +taninux/overview/pillar.py,sha256=TuB436V05t_xWXc-qv5dCbAQsTwNwPrUtHWVkitHops,255 +taninux/overview/screen.py,sha256=XiVEl3Cj_UfF3793wz4CX4xHlpRYTgpeqzcvMaG562E,12398 +taninux/packages/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +taninux/packages/pillar.py,sha256=CdX75cFGRqS32j3Z2rOE12rxfLqM4Hs9IFBmnBerXDg,258 +taninux/packages/screen.py,sha256=BzSiAy6aw8MT3krm2j_ZqCoUqo2OgfagNgNwg8LgUTU,10452 +taninux/pillars/__init__.py,sha256=HAalRrRLJBgF50GUtq_sJuUbkTp1V7x-Fm1lBKUn15g,794 +taninux/pillars/base.py,sha256=cL0yWYaRTEvt5qIU1qZt4Q0kuopSgE8LzRkdx5IEjhQ,878 +taninux/suggest/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +taninux/suggest/pillar.py,sha256=_fVkV3mBgetx9uxbJMvZJToQk6L0qWEq09co0n91UFk,261 +taninux/suggest/rules.py,sha256=7QCvrB4efDI5wUCvGza0KDD2IqXH7C4k_bu0_Ffhx-g,4800 +taninux/suggest/screen.py,sha256=qpTSFg2q4PvnaDBCPPkvqg6Ja0TmfwVqhHRMmth0t-Y,5963 +taninux/update/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +taninux/update/pillar.py,sha256=Z9cdgUHbs8KRAmvVsXcGjwCcDAcxpMbgGEMmjihdgfA,267 +taninux/update/screen.py,sha256=-mEZvhTEx5zpCAWiau0b3I8WvNPC9guqLmf3TE0-l5o,15523 +taninux/update/sources/__init__.py,sha256=nRVW3ilMZ5U6aowjEPmKf2-lf2vfCE9HrHYKDNS8S6A,515 +taninux/update/sources/aur.py,sha256=42hu33RLvEN8l9EYzPqE-rX0RmCYtCqXWG9nSq2nqqE,1285 +taninux/update/sources/flatpak.py,sha256=4F1-QrIv2a3sIJQe7DKA7PQDPCEf_1KpwIPCTgLwldw,2027 +taninux/update/sources/fwupd.py,sha256=e3e6g0olO4BJJrI8ith9mgHFKUrdWLptC2YAHqgp3BI,1150 +taninux/update/sources/pacman.py,sha256=95cFbaK7xnUGJvxFB5l2fw0Tu3BrU0wCLT75A3vMtps,1065 +taninux/update/types.py,sha256=jAxtOjCABznmDBLNkCx5p4Uv7v0LgPw9m56wPj0fVoI,515 +../../../bin/thub,sha256=B3XLYTBUObruzl0lW2AWgAKkXpJuJkrSIGN0LtVe2jU,221 +../../../bin/tsettings,sha256=nkajkGpJqiDiwrjYXzr9zuxXD5Xo6tAokgCad6kKpiE,231 diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux-0.2.0.dist-info/WHEEL b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux-0.2.0.dist-info/WHEEL new file mode 100644 index 0000000..bd84226 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux-0.2.0.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: hatchling 1.30.1 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux-0.2.0.dist-info/entry_points.txt b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux-0.2.0.dist-info/entry_points.txt new file mode 100644 index 0000000..983cf1b --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux-0.2.0.dist-info/entry_points.txt @@ -0,0 +1,5 @@ +[console_scripts] +taninux = taninux.cli:main +taninux-gtk = taninux.gui.app:main +thub = taninux.gui.app:main_hub +tsettings = taninux.gui.app:main_settings diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux-0.2.0.dist-info/licenses/LICENSE b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux-0.2.0.dist-info/licenses/LICENSE new file mode 100644 index 0000000..f6cdd22 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux-0.2.0.dist-info/licenses/LICENSE @@ -0,0 +1,232 @@ +GNU GENERAL PUBLIC LICENSE +Version 3, 29 June 2007 + +Copyright © 2007 Free Software Foundation, Inc. + +Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. + +Preamble + +The GNU General Public License is a free, copyleft license for software and other kinds of works. + +The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. + +When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. + +To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. + +For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. + +Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. + +For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. + +Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. + +Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. + +The precise terms and conditions for copying, distribution and modification follow. + +TERMS AND CONDITIONS + +0. Definitions. + +“This License” refers to version 3 of the GNU General Public License. + +“Copyright” also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. + +“The Program” refers to any copyrightable work licensed under this License. Each licensee is addressed as “you”. “Licensees” and “recipients” may be individuals or organizations. + +To “modify” a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a “modified version” of the earlier work or a work “based on” the earlier work. + +A “covered work” means either the unmodified Program or a work based on the Program. + +To “propagate” a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. + +To “convey” a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. + +An interactive user interface displays “Appropriate Legal Notices” to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. + +1. Source Code. +The “source code” for a work means the preferred form of the work for making modifications to it. “Object code” means any non-source form of a work. + +A “Standard Interface” means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. + +The “System Libraries” of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A “Major Component”, in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. + +The “Corresponding Source” for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. + +The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. + +The Corresponding Source for a work in source code form is that same work. + +2. Basic Permissions. +All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. + +You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. + +3. Protecting Users' Legal Rights From Anti-Circumvention Law. +No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. + +When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. + +4. Conveying Verbatim Copies. +You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. + +5. Conveying Modified Source Versions. +You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to “keep intact all notices”. + + c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. + +A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an “aggregate” if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. + +6. Conveying Non-Source Forms. +You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: + + a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. + + d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. + +A “User Product” is either (1) a “consumer product”, which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, “normally used” refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. + +“Installation Information” for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. + +If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). + +The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. + +Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. + +7. Additional Terms. +“Additional permissions” are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. + +Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or authors of the material; or + + e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. + +All other non-permissive additional terms are considered “further restrictions” within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. + +8. Termination. +You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). + +However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. + +Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. + +Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. + +9. Acceptance Not Required for Having Copies. +You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. + +10. Automatic Licensing of Downstream Recipients. +Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. + +An “entity transaction” is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. + +11. Patents. +A “contributor” is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's “contributor version”. + +A contributor's “essential patent claims” are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, “control” includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. + +In the following three paragraphs, a “patent license” is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To “grant” such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. + +If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. “Knowingly relying” means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. + +A patent license is “discriminatory” if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. + +12. No Surrender of Others' Freedom. +If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. + +13. Use with the GNU Affero General Public License. +Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. + +14. Revised Versions of this License. +The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License “or any later version” applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. + +If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. + +Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. + +15. Disclaimer of Warranty. +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. Limitation of Liability. +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +17. Interpretation of Sections 15 and 16. +If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. + +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the “copyright” line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + +If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an “about box”. + +You should also get your employer (if you work as a programmer) or school, if any, to sign a “copyright disclaimer” for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . + +The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/__init__.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/__init__.py new file mode 100644 index 0000000..d3ec452 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/__init__.py @@ -0,0 +1 @@ +__version__ = "0.2.0" diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/__main__.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/__main__.py new file mode 100644 index 0000000..1210ce6 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/__main__.py @@ -0,0 +1,3 @@ +from taninux.cli import main + +raise SystemExit(main()) diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/app.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/app.py new file mode 100644 index 0000000..25be30c --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/app.py @@ -0,0 +1,192 @@ +"""TANINUX — Top-Level App. Sidebar links, ContentSwitcher rechts.""" +from __future__ import annotations + +from textual.app import App, ComposeResult +from textual.binding import Binding +from textual.containers import Horizontal, Vertical +from textual.reactive import reactive +from textual.theme import Theme +from textual.widgets import ContentSwitcher, Footer, Header, Static + +from taninux import __version__ +from taninux.core.theme import ( + BASE1, BASE2, BASE3, BASE4, BASE5, + CHIKYU, FG_ALT, RED, SHINKAI, TAKE, UME, YORU, YUKI, +) +from taninux.pillars import PILLARS, by_key + + +MOUNTAIN_THEME = Theme( + name="mountain-fuji", + background=YORU, + surface=YORU, + panel=YORU, + foreground=YUKI, + primary=UME, + secondary=SHINKAI, + accent=BASE5, + success=TAKE, + warning=CHIKYU, + error=RED, + dark=True, + variables={ + "footer-key-foreground": BASE5, + "footer-key-background": YORU, + "footer-description-foreground": BASE4, + "footer-description-background": YORU, + "footer-foreground": BASE4, + "footer-background": YORU, + "header-background": YORU, + "header-foreground": UME, + "border": BASE2, + "border-blurred": BASE2, + "primary-darken-1": BASE2, + "primary-darken-2": BASE1, + }, +) + + +class Sidebar(Vertical): + """Linke Pillar-Liste mit dem aktiv markierten Eintrag.""" + + DEFAULT_CSS = f""" + Sidebar {{ + width: 22; + background: {YORU}; + padding: 1 1 1 2; + border-right: vkey {BASE2}; + }} + Sidebar Static {{ + background: {YORU}; + }} + Sidebar #s-brand {{ + height: 2; + padding-bottom: 1; + color: {UME}; + }} + Sidebar #s-version {{ + height: 1; + color: {BASE4}; + }} + Sidebar .pillar-row {{ + height: 2; + padding: 0 0 0 0; + }} + """ + + current: reactive[str] = reactive("") + + def compose(self) -> ComposeResult: + yield Static(f"[{UME}]▲[/] [{UME}]TANINUX[/]", id="s-brand", markup=True) + yield Static(f"[{BASE4}]v{__version__} · fuji[/]", id="s-version", markup=True) + yield Static("", id="s-gap") + for p in PILLARS: + yield Static("", id=f"row-{p.key}", classes="pillar-row", markup=True) + + def watch_current(self, _old: str, new: str) -> None: + self._refresh_view() + + def on_mount(self) -> None: + self._refresh_view() + + def _refresh_view(self) -> None: + for p in PILLARS: + row = self.query_one(f"#row-{p.key}", Static) + is_active = (self.current == p.key) + key_color = UME if is_active else BASE4 + label_color = YUKI if is_active else BASE5 + tagline_color = BASE5 if is_active else BASE4 + marker = "▶" if is_active else " " + row.update( + f"[{key_color}]{marker} {p.icon} {p.label}[/]\n" + f"[{tagline_color}] {p.tagline}[/]" + ) + + +class TaninuxApp(App): + """TANINUX — Linux maintenance & management TUI.""" + + CSS = f""" + Screen {{ + background: {YORU}; + color: {YUKI}; + layout: vertical; + }} + Header {{ + background: {YORU}; + color: {UME}; + }} + HeaderIcon {{ + display: none; + }} + #root {{ + height: 1fr; + background: {YORU}; + }} + #content {{ + width: 1fr; + background: {YORU}; + }} + Footer {{ + background: {YORU}; + color: {BASE4}; + }} + FooterKey {{ + background: {YORU}; + color: {BASE4}; + }} + FooterKey > .footer-key--key {{ + background: {YORU}; + color: {BASE5}; + }} + FooterKey > .footer-key--description {{ + background: {YORU}; + color: {BASE4}; + }} + """ + + BINDINGS = [ + Binding("o", "switch('o')", "overview"), + Binding("p", "switch('p')", "packages"), + Binding("m", "switch('m')", "maintain"), + Binding("u", "switch('u')", "update"), + Binding("b", "switch('b')", "bundles"), + Binding("s", "switch('s')", "suggest"), + Binding("f", "switch('f')", "flatpak"), + Binding("k", "switch('k')", "kernel"), + Binding("h", "switch('h')", "health"), + Binding("q", "quit", "quit"), + ] + + TITLE = "▲ TANINUX" + SUB_TITLE = "fuji · linux maintenance" + + def __init__(self) -> None: + super().__init__() + self.register_theme(MOUNTAIN_THEME) + self.theme = "mountain-fuji" + self.current_pillar: str = PILLARS[0].key + + def compose(self) -> ComposeResult: + yield Header(show_clock=False, icon="") + with Horizontal(id="root"): + yield Sidebar(id="sidebar") + with ContentSwitcher(initial=PILLARS[0].key, id="content"): + for p in PILLARS: + yield p.factory(id=p.key) + yield Footer() + + def on_mount(self) -> None: + sidebar = self.query_one(Sidebar) + sidebar.current = self.current_pillar + + def action_switch(self, key: str) -> None: + if by_key(key) is None: + return + self.current_pillar = key + self.query_one("#content", ContentSwitcher).current = key + self.query_one(Sidebar).current = key + + +def run() -> None: + TaninuxApp().run() diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/bundles/__init__.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/bundles/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/bundles/builtins.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/bundles/builtins.py new file mode 100644 index 0000000..7433ba1 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/bundles/builtins.py @@ -0,0 +1,148 @@ +"""Eingebaute Paket-Bundles — deklarative Paket-Sets nach Use-Case. + +Built-ins können nicht editiert werden; User-Bundles in +~/.local/share/taninux/bundles/.toml ergänzen sie (TODO).""" +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass(frozen=True) +class Bundle: + id: str + label: str + tagline: str + packages: tuple[str, ...] + tags: tuple[str, ...] = () + + +BUILTIN_BUNDLES: list[Bundle] = [ + Bundle( + id="cli-essentials", + label="CLI Essentials", + tagline="modern terminal tools", + packages=( + "bat", "eza", "fd", "ripgrep", "fzf", "zoxide", + "dust", "bottom", "starship", "tldr", + ), + tags=("cli",), + ), + Bundle( + id="rust-dev", + label="Rust Development", + tagline="rust toolchain + LSP + qol", + packages=( + "rust", "rust-analyzer", "sccache", "cargo-audit", + "cargo-watch", "cargo-edit", + ), + tags=("dev", "rust"), + ), + Bundle( + id="python-dev", + label="Python Development", + tagline="python + uv + ruff + ipython", + packages=( + "python", "uv", "ruff", "ipython", + "python-pip", "python-lsp-server", + ), + tags=("dev", "python"), + ), + Bundle( + id="web-dev", + label="Web Development", + tagline="node + typescript + pnpm", + packages=( + "nodejs", "npm", "pnpm", "typescript", + "yarn", + ), + tags=("dev", "web"), + ), + Bundle( + id="go-dev", + label="Go Development", + tagline="go toolchain + LSP", + packages=( + "go", "gopls", "delve", + ), + tags=("dev", "go"), + ), + Bundle( + id="gaming", + label="Gaming", + tagline="steam + qol for proton/wine games", + packages=( + "steam", "gamemode", "mangohud", "lutris", + "wine", "wine-mono", "wine-gecko", + "winetricks", "vulkan-tools", + ), + tags=("gaming",), + ), + Bundle( + id="multimedia", + label="Multimedia", + tagline="audio/video player, editor, recorder", + packages=( + "mpv", "ffmpeg", "obs-studio", + "kdenlive", "audacity", "imagemagick", + ), + tags=("multimedia",), + ), + Bundle( + id="niri", + label="niri Desktop", + tagline="niri scrolling compositor + eww bar/dock + launcher (wayland)", + packages=( + "niri", "eww", "swaylock", "swayidle", "wlsunset", + "waypaper", "swaybg", "wl-clipboard", "grim", "slurp", + "kitty", + ), + tags=("wm", "wayland"), + ), + Bundle( + id="security", + label="Security Tooling", + tagline="audit + scan + monitoring", + packages=( + "nmap", "arch-audit", + "rkhunter", "lynis", "wireshark-qt", + ), + tags=("security",), + ), + Bundle( + id="containers", + label="Containers", + tagline="docker + buildah + podman + compose", + packages=( + "podman", "buildah", "skopeo", + "podman-compose", "docker", "docker-compose", + ), + tags=("dev", "containers"), + ), + Bundle( + id="virtualization", + label="Virtualization", + tagline="KVM/QEMU/libvirt stack", + packages=( + "qemu-full", "libvirt", "virt-manager", + "edk2-ovmf", "dnsmasq", "bridge-utils", + ), + tags=("virt",), + ), + Bundle( + id="creative", + label="Creative Tools", + tagline="design + 3d + illustration", + packages=( + "blender", "krita", "inkscape", "gimp", + "darktable", + ), + tags=("creative",), + ), +] + + +def by_id(bid: str) -> Bundle | None: + for b in BUILTIN_BUNDLES: + if b.id == bid: + return b + return None diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/bundles/loader.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/bundles/loader.py new file mode 100644 index 0000000..968a820 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/bundles/loader.py @@ -0,0 +1,67 @@ +"""User-Bundle Loader. + +Lädt zusätzlich zu den BUILTIN_BUNDLES alle TOML-Files aus +~/.local/share/taninux/bundles/.toml. + +Format: + label = "My Setup" + tagline = "alles was ich nach einer neuinstallation brauche" + tags = ["personal"] + packages = [ + "neovim", + "git", + "tmux", + ] +""" +from __future__ import annotations + +import tomllib +from pathlib import Path + +from taninux.bundles.builtins import Bundle, BUILTIN_BUNDLES +from taninux.core.paths import bundles_dir + + +def load_user_bundles() -> list[Bundle]: + bundles_path = bundles_dir() + out: list[Bundle] = [] + for p in sorted(bundles_path.glob("*.toml")): + try: + data = tomllib.loads(p.read_text()) + except (OSError, tomllib.TOMLDecodeError): + continue + if "packages" not in data: + continue + out.append(Bundle( + id=p.stem, + label=data.get("label") or p.stem, + tagline=data.get("tagline", ""), + packages=tuple(data["packages"]), + tags=tuple(data.get("tags", ())), + )) + return out + + +def all_bundles() -> list[Bundle]: + return [*BUILTIN_BUNDLES, *load_user_bundles()] + + +def save_user_bundle(bundle_id: str, label: str, tagline: str, + packages: list[str], tags: list[str] | None = None) -> Path: + """Schreibt ein User-Bundle als TOML.""" + safe_id = "".join(c if c.isalnum() or c in "-_" else "_" for c in bundle_id) + target = bundles_dir() / f"{safe_id}.toml" + + lines = [ + f'label = "{label}"', + f'tagline = "{tagline}"', + ] + if tags: + tags_str = ", ".join(f'"{t}"' for t in tags) + lines.append(f"tags = [{tags_str}]") + lines.append("packages = [") + for p in packages: + lines.append(f' "{p}",') + lines.append("]") + target.write_text("\n".join(lines) + "\n") + return target diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/bundles/pillar.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/bundles/pillar.py new file mode 100644 index 0000000..8703761 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/bundles/pillar.py @@ -0,0 +1,13 @@ +from __future__ import annotations + +from taninux.bundles.screen import BundlesView +from taninux.pillars.base import Pillar + + +BUNDLES = Pillar( + key="b", + label="bundles", + tagline="deklarative paket-sets", + icon="◆", + factory=BundlesView, +) diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/bundles/screen.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/bundles/screen.py new file mode 100644 index 0000000..b1704fc --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/bundles/screen.py @@ -0,0 +1,247 @@ +"""Bundles-Pillar — deklarative Paket-Sets installieren/checken.""" +from __future__ import annotations + +from dataclasses import dataclass + +from textual.app import ComposeResult +from textual.binding import Binding +from textual.containers import VerticalScroll +from textual.widgets import DataTable, Static + +from taninux.bundles.builtins import Bundle +from taninux.bundles.loader import all_bundles +from taninux.core import pacman as pac +from taninux.core import sysinfo +from taninux.core import theme as T +from taninux.core.confirm import ConfirmModal +from taninux.core.runner import Command, run_interactive + + +@dataclass +class BundleStatus: + bundle: Bundle + installed: list[str] + missing: list[str] + + @property + def state(self) -> str: + if not self.installed: + return "none" + if not self.missing: + return "satisfied" + return "partial" + + +STATE_GLYPH = { + "satisfied": ("●", T.TAKE), + "partial": ("◐", T.CHIKYU), + "none": ("○", T.BASE4), +} +STATE_LABEL = { + "satisfied": "vollständig", + "partial": "teilweise", + "none": "fehlt", +} + + +def status_for(bundle: Bundle, installed: frozenset[str]) -> BundleStatus: + inst = [p for p in bundle.packages if p in installed] + miss = [p for p in bundle.packages if p not in installed] + return BundleStatus(bundle=bundle, installed=inst, missing=miss) + + +class BundlesView(VerticalScroll): + DEFAULT_CSS = f""" + BundlesView {{ + background: {T.YORU}; + padding: 1 2; + scrollbar-color: {T.BASE2} {T.YORU}; + scrollbar-background: {T.YORU}; + scrollbar-corner-color: {T.YORU}; + scrollbar-size-vertical: 1; + }} + BundlesView Static {{ + background: {T.YORU}; + margin-bottom: 1; + }} + BundlesView #b-table {{ + height: auto; + background: {T.YORU}; + color: {T.FG_ALT}; + border: none; + overflow-x: hidden; + }} + BundlesView #b-table:focus {{ + border: none; + }} + BundlesView DataTable > .datatable--header {{ + background: {T.YORU}; + color: {T.SHINKAI}; + }} + BundlesView DataTable > .datatable--cursor {{ + background: {T.BASE1}; + color: {T.YUKI}; + }} + BundlesView DataTable > .datatable--even-row {{ + background: {T.YORU}; + }} + BundlesView DataTable > .datatable--odd-row {{ + background: {T.YORU}; + }} + """ + + BINDINGS = [ + Binding("r", "rescan", "rescan"), + Binding("i", "install", "fehlende installieren"), + Binding("j", "cursor_down", "down", show=False), + Binding("k", "cursor_up", "up", show=False), + ] + + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + self.statuses: list[BundleStatus] = [] + + def compose(self) -> ComposeResult: + yield Static("", id="b-intro", markup=True) + yield DataTable(id="b-table", cursor_type="row", zebra_stripes=False) + yield Static("", id="b-detail", markup=True) + yield Static("", id="b-flash", markup=True) + + def on_mount(self) -> None: + t = self.query_one("#b-table", DataTable) + t.add_column("", width=2, key="glyph") + t.add_column("bundle", width=22, key="name") + t.add_column("status", width=14, key="status") + t.add_column("fortschritt", width=18, key="progress") + t.add_column("beschreibung", key="desc") + self.action_rescan() + + def action_rescan(self) -> None: + pac.invalidate() + installed = pac.installed_packages() + self.statuses = [status_for(b, installed) for b in all_bundles()] + # Sortierung: partial first (in-progress), dann none (zum vorschlagen), dann satisfied + order = {"partial": 0, "none": 1, "satisfied": 2} + self.statuses.sort(key=lambda s: (order[s.state], s.bundle.label)) + self._refresh_view() + self._flash(f"{len(self.statuses)} bundles geprüft") + + def action_install(self) -> None: + cur = self._current() + if cur is None: + return + if not cur.missing: + self._flash(f"{cur.bundle.label} bereits vollständig", color=T.TAKE) + return + body = ( + f"bundle [{cur.bundle.label}] vervollständigen?\n\n" + f"fehlende pakete ({len(cur.missing)}):\n" + f" {', '.join(cur.missing)}" + ) + self.app.push_screen( + ConfirmModal(f"install {cur.bundle.id}?", body, danger=False), + lambda ok: self._after_install(ok, cur), + ) + + def _after_install(self, ok: bool | None, cur: BundleStatus) -> None: + if not ok: + self._flash("abgebrochen") + return + helper = "paru" if sysinfo.has_binary("paru") else ( + "yay" if sysinfo.has_binary("yay") else "sudo pacman" + ) + argv = helper.split() + ["-S", "--needed", *cur.missing] + codes = run_interactive( + self.app, + [Command( + label=f"install {len(cur.missing)} pakete für [{cur.bundle.label}]", + argv=argv, + )], + ) + success = codes and codes[0] == 0 + self._flash( + f"{cur.bundle.label} installiert" if success else "install fehlgeschlagen", + color=T.TAKE if success else T.RED, + ) + self.action_rescan() + + def action_cursor_down(self) -> None: + self.query_one("#b-table", DataTable).action_cursor_down() + + def action_cursor_up(self) -> None: + self.query_one("#b-table", DataTable).action_cursor_up() + + def _current(self) -> BundleStatus | None: + t = self.query_one("#b-table", DataTable) + r = t.cursor_row + if r is None or r < 0 or r >= len(self.statuses): + return None + return self.statuses[r] + + def _flash(self, msg: str, color: str | None = None) -> None: + color = color or T.BASE4 + self.query_one("#b-flash", Static).update(f"[{color}]{msg}[/]") + + def _refresh_view(self) -> None: + total = len(self.statuses) + sat = sum(1 for s in self.statuses if s.state == "satisfied") + par = sum(1 for s in self.statuses if s.state == "partial") + non = sum(1 for s in self.statuses if s.state == "none") + intro = ( + f"[{T.UME}]◆ bundles[/]\n" + f"[{T.BASE5}]deklarative paket-sets · " + f"[{T.TAKE}]●[/] {sat} vollständig " + f"[{T.CHIKYU}]◐[/] {par} teilweise " + f"[{T.BASE4}]○[/] {non} fehlend[/]\n" + f"[{T.BASE5}]`i` installiert die fehlenden pakete des ausgewählten bundles[/]" + ) + self.query_one("#b-intro", Static).update(intro) + + t = self.query_one("#b-table", DataTable) + t.clear() + for s in self.statuses: + glyph, color = STATE_GLYPH[s.state] + label = STATE_LABEL[s.state] + total_p = len(s.bundle.packages) + done = len(s.installed) + # Progress bar als unicode block-chars + ratio = done / total_p if total_p else 0 + blocks = int(ratio * 10) + bar = "█" * blocks + "░" * (10 - blocks) + t.add_row( + f"[{color}]{glyph}[/]", + f"[{T.YUKI}]{s.bundle.label}[/]", + f"[{color}]{label}[/]", + f"[{color}]{bar}[/] [{T.BASE5}]{done}/{total_p}[/]", + f"[{T.BASE5}]{s.bundle.tagline}[/]", + ) + + # Detail: zeige fehlende Pakete des aktuellen Bundles + if self.statuses: + self._render_detail(self.statuses[0]) + + def _render_detail(self, s: BundleStatus) -> None: + lines = [ + f"[{T.SHINKAI}]{s.bundle.label}[/] [{T.BASE5}]{s.bundle.tagline}[/]", + ] + if s.bundle.tags: + lines.append(f"[{T.BASE4}]tags:[/] {', '.join(s.bundle.tags)}") + lines += [ + "", + f"[{T.SHINKAI}]installiert ({len(s.installed)})[/]", + ] + for p in s.installed: + lines.append(f" [{T.TAKE}]●[/] [{T.YUKI}]{p}[/]") + if s.missing: + lines += [ + "", + f"[{T.SHINKAI}]fehlt ({len(s.missing)})[/]", + ] + for p in s.missing: + lines.append(f" [{T.BASE4}]○[/] [{T.BASE5}]{p}[/]") + self.query_one("#b-detail", Static).update("\n".join(lines)) + + def on_data_table_row_highlighted(self, event: DataTable.RowHighlighted) -> None: + if event.cursor_row is None or event.cursor_row >= len(self.statuses): + return + self._render_detail(self.statuses[event.cursor_row]) diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/cli.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/cli.py new file mode 100644 index 0000000..2122ca4 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/cli.py @@ -0,0 +1,837 @@ +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + + +def _human_size(n: int) -> str: + f = float(n) + for unit in ("B", "K", "M", "G", "T"): + if f < 1024 or unit == "T": + return f"{f:.0f}{unit}" if unit == "B" else f"{f:.1f}{unit}" + f /= 1024 + return f"{f:.1f}T" + + +def _format_days(d: int) -> str: + if d == 0: + return "heute" + if d < 30: + return f"{d}d" + if d < 365: + return f"{d // 30}mo" + return f"{d // 365}y" + + +def _console(): + from rich.console import Console + return Console() + + +# ---------------------------------------------------------------- scan + + +def _scan(category: str) -> int: + from rich.table import Table + + from taninux.maintain.scanners import SCANNERS, by_id + from taninux.core import theme as T + from taninux.maintain.types import ( + CLASS_COLOR, CLASS_GLYPH, CLASS_LABEL, Classification, + ) + + console = _console() + specs = SCANNERS if category == "all" else [s for s in [by_id(category)] if s] + if not specs: + console.print(f" [{T.RED}]unknown category: {category}[/]") + return 2 + + home = str(Path.home()) + for spec in specs: + scanner = spec.factory() + findings = scanner.scan() + reclaim = sum( + f.size_bytes for f in findings + if f.classification in (Classification.SAFE, Classification.LIKELY_ORPHAN) + ) + + console.print() + console.print( + f" [{T.UME}]▲ {spec.label}[/] " + f"[{T.YUKI}]{len(findings)}[/] [{T.BASE5}]einträge[/] " + f"[{T.BASE3}]·[/] " + f"[{T.TAKE}]{_human_size(reclaim)}[/] [{T.BASE5}]potentiell wegwerfbar[/]" + ) + console.print() + if not findings: + console.print(f" [{T.BASE4}]keine einträge.[/]") + continue + + table = Table( + show_header=True, + header_style=T.SHINKAI, + border_style=T.BASE2, + pad_edge=False, + box=None, + ) + table.add_column("", width=2) + table.add_column("pfad", no_wrap=True) + table.add_column("grösse", justify="right") + table.add_column("idle", justify="right") + table.add_column("status") + + for f in findings: + color = CLASS_COLOR[f.classification] + s = str(f.path) + path = s[6:] if s.startswith("pkg://") else s.replace(home, "~", 1) + size = "—" if not f.size_bytes else _human_size(f.size_bytes) + days = "—" if f.last_access_days is None else _format_days(f.last_access_days) + table.add_row( + f"[{color}]{CLASS_GLYPH[f.classification]}[/]", + f"[{T.FG_ALT}]{path}[/]", + f"[{T.CHIKYU}]{size}[/]" if f.size_bytes else f"[{T.BASE3}]—[/]", + f"[{T.BASE5}]{days}[/]", + f"[{color}]{CLASS_LABEL[f.classification]}[/]", + ) + console.print(table) + return 0 + + +# ---------------------------------------------------------------- trash + + +def _trash_list() -> int: + from rich.table import Table + + from taninux.core.trash import list_trash + from taninux.core import theme as T + + console = _console() + entries = list_trash() + if not entries: + console.print(f" [{T.BASE4}]trash ist leer.[/]") + return 0 + + total = sum(e.size_bytes for e in entries) + console.print() + console.print( + f" [{T.UME}]▲ trash[/] " + f"[{T.YUKI}]{len(entries)}[/] [{T.BASE5}]einträge[/] " + f"[{T.BASE3}]·[/] " + f"[{T.CHIKYU}]{_human_size(total)}[/]" + ) + console.print() + + table = Table( + show_header=True, + header_style=T.SHINKAI, + border_style=T.BASE2, + pad_edge=False, + box=None, + ) + table.add_column("id") + table.add_column("origin", no_wrap=True) + table.add_column("grösse", justify="right") + table.add_column("gelöscht am") + + home = str(Path.home()) + for e in entries: + origin = str(e.original).replace(home, "~", 1) + table.add_row( + f"[{T.BASE5}]{e.id}[/]", + f"[{T.FG_ALT}]{origin}[/]", + f"[{T.CHIKYU}]{_human_size(e.size_bytes)}[/]", + f"[{T.BASE5}]{e.trashed_at}[/]", + ) + console.print(table) + return 0 + + +def _trash_restore(slot_id: str) -> int: + from taninux.core.trash import restore + from taninux.core import theme as T + + console = _console() + try: + original = restore(slot_id) + except FileNotFoundError: + console.print(f" [{T.RED}]kein eintrag: {slot_id}[/]") + return 2 + except FileExistsError as e: + console.print(f" [{T.RED}]{e}[/]") + return 2 + console.print(f" [{T.TAKE}]wiederhergestellt:[/] [{T.YUKI}]{original}[/]") + return 0 + + +def _trash_purge(slot_id: str | None) -> int: + from taninux.core.trash import list_trash, purge + from taninux.core import theme as T + + console = _console() + if slot_id: + try: + purge(slot_id) + except FileNotFoundError: + console.print(f" [{T.RED}]kein eintrag: {slot_id}[/]") + return 2 + console.print(f" [{T.BASE5}]gelöscht:[/] [{T.YUKI}]{slot_id}[/]") + return 0 + + entries = list_trash() + if not entries: + console.print(f" [{T.BASE4}]trash bereits leer.[/]") + return 0 + total = sum(e.size_bytes for e in entries) + console.print( + f" [{T.RED}]purge {len(entries)} einträge[/] " + f"[{T.BASE4}]({_human_size(total)})?[/] " + f"[{T.BASE5}]bestätige mit:[/] taninux trash purge --all --yes" + ) + return 1 + + +def _trash_purge_all(confirmed: bool) -> int: + from taninux.core.trash import list_trash, purge + from taninux.core import theme as T + + console = _console() + entries = list_trash() + if not entries: + console.print(f" [{T.BASE4}]trash bereits leer.[/]") + return 0 + if not confirmed: + return _trash_purge(None) + for e in entries: + purge(e.id) + console.print(f" [{T.BASE5}]{len(entries)} einträge endgültig gelöscht.[/]") + return 0 + + +# ---------------------------------------------------------------- update + + +def _update_list() -> int: + from collections import Counter + from rich.table import Table + + from taninux.core import theme as T + from taninux.update.sources import fetch_all + + console = _console() + console.print(f"\n [{T.BASE4}]scanne pacman + AUR + flatpak + fwupd…[/]") + items = fetch_all() + + if not items: + console.print(f"\n [{T.TAKE}]system ist aktuell.[/]\n") + return 0 + + total_size = sum(i.size_bytes for i in items) + counts = Counter(i.source for i in items) + console.print() + console.print( + f" [{T.UME}]↻ updates[/] " + f"[{T.YUKI}]{len(items)}[/] [{T.BASE5}]verfügbar[/]" + + (f" [{T.BASE3}]·[/] [{T.CHIKYU}]{_human_size(total_size)}[/]" if total_size else "") + ) + parts = [] + for src in ("pacman", "aur", "flatpak", "fwupd"): + n = counts.get(src, 0) + if n: + parts.append(f"[{T.BASE5}]{src} {n}[/]") + if parts: + console.print(f" {' · '.join(parts)}") + console.print() + + order = {"pacman": 0, "aur": 1, "flatpak": 2, "fwupd": 3} + items.sort(key=lambda i: (order.get(i.source, 9), i.name)) + + table = Table( + show_header=True, + header_style=T.SHINKAI, + border_style=T.BASE2, + pad_edge=False, + box=None, + ) + table.add_column("quelle", width=9) + table.add_column("paket") + table.add_column("aktuell") + table.add_column("→", width=1) + table.add_column("neu") + table.add_column("grösse", justify="right") + + for i in items: + size = "—" if not i.size_bytes else _human_size(i.size_bytes) + table.add_row( + f"[{T.BASE5}]{i.source}[/]", + f"[{T.YUKI}]{i.name}[/]", + f"[{T.BASE4}]{i.current_version}[/]", + f"[{T.BASE3}]→[/]", + f"[{T.TAKE}]{i.new_version}[/]", + f"[{T.CHIKYU}]{size}[/]" if i.size_bytes else f"[{T.BASE3}]—[/]", + ) + console.print(table) + console.print() + + cmds = [] + if counts.get("pacman") or counts.get("aur"): + cmds.append("paru -Syu") + if counts.get("flatpak"): + cmds.append("flatpak update -y") + if counts.get("fwupd"): + cmds.append("sudo fwupdmgr update") + if cmds: + console.print(f" [{T.SHINKAI}]ausführen:[/]") + for c in cmds: + console.print(f" [{T.CHIKYU}]{c}[/]") + console.print() + return 0 + + +def _update_execute() -> int: + """Headless: führt alle pending updates aus mit --noconfirm.""" + import subprocess + + from taninux.core import sysinfo + from taninux.core import theme as T + from taninux.update.sources import fetch_all + from collections import Counter + + console = _console() + console.print(f"\n [{T.UME}]↻ scanne updates…[/]") + items = fetch_all() + if not items: + console.print(f" [{T.TAKE}]system ist aktuell.[/]\n") + return 0 + counts = Counter(i.source for i in items) + console.print(f" [{T.YUKI}]{len(items)}[/] [{T.BASE5}]updates verfügbar[/]") + console.print() + + cmds: list[list[str]] = [] + if counts.get("pacman") or counts.get("aur"): + helper = "paru" if sysinfo.has_binary("paru") else ( + "yay" if sysinfo.has_binary("yay") else None + ) + if helper: + cmds.append([ + helper, "-Syu", + "--noconfirm", "--sudoloop", + "--skipreview", "--removemake", "--cleanafter", + "--batchinstall", + ]) + elif counts.get("pacman"): + cmds.append(["sudo", "pacman", "-Syu", "--noconfirm"]) + if counts.get("flatpak"): + cmds.append(["flatpak", "update", "--noninteractive"]) + if counts.get("fwupd"): + cmds.append(["sudo", "fwupdmgr", "update", "--assume-yes", "--no-reboot-check"]) + + rc_total = 0 + for argv in cmds: + console.print(f" [{T.SHINKAI}]$ {' '.join(argv)}[/]") + rc = subprocess.run(argv, check=False).returncode + if rc != 0: + console.print(f" [{T.RED}]exit {rc}[/]\n") + rc_total = rc + else: + console.print(f" [{T.TAKE}]ok[/]\n") + return rc_total + + +# ---------------------------------------------------------------- health + + +def _health_summary() -> int: + from taninux.core import sysinfo + from taninux.core import theme as T + + console = _console() + mem = sysinfo.memory() + total = mem.get("MemTotal", 0) + avail = mem.get("MemAvailable", 0) + used = total - avail + la = sysinfo.load_avg() + ncpu = sysinfo.cpu_count() + failed = sysinfo.systemctl_failed() + errs_today = sysinfo.journal_errors_today() + boot = sysinfo.boot_time_seconds() + + console.print() + console.print(f" [{T.UME}]◈ {sysinfo.hostname()}[/] [{T.BASE5}]{sysinfo.distro_pretty()} · {sysinfo.kernel_release()}[/]") + console.print() + console.print(f" [{T.SHINKAI}]uptime[/] [{T.YUKI}]{int(sysinfo.uptime_seconds() // 3600)}h[/]") + if boot: + console.print(f" [{T.SHINKAI}]boot[/] [{T.YUKI}]{boot:.1f}s[/]") + console.print(f" [{T.SHINKAI}]load[/] [{T.YUKI}]{la[0]:.2f}[/] [{T.BASE5}]{la[1]:.2f} {la[2]:.2f} ({ncpu} cpus)[/]") + if total: + console.print(f" [{T.SHINKAI}]memory[/] [{T.YUKI}]{used/1024**3:.1f}G[/] [{T.BASE5}]/[/] [{T.YUKI}]{total/1024**3:.1f}G[/] [{T.BASE4}]({used*100/total:.0f}%)[/]") + console.print() + + for g in sysinfo.gpus(): + console.print(f" [{T.SHINKAI}]gpu[/] [{T.YUKI}]{g.vendor}[/] [{T.BASE5}]·[/] [{T.BASE5}]{g.model[:55]}[/] [{T.CHIKYU}]{g.driver or '—'}[/]") + console.print(f" [{T.SHINKAI}]audio[/] [{T.YUKI}]{sysinfo.audio_server()}[/]") + + console.print() + failed_color = T.TAKE if not failed else T.RED + console.print(f" [{failed_color}]●[/] [{T.YUKI}]{len(failed)}[/] [{T.BASE5}]failed services[/]") + for u in failed[:5]: + console.print(f" [{T.RED}]{u}[/]") + err_color = T.TAKE if errs_today == 0 else (T.CHIKYU if errs_today < 20 else T.RED) + err_label = f"{errs_today}" if errs_today >= 0 else "?" + console.print(f" [{err_color}]●[/] [{T.YUKI}]{err_label}[/] [{T.BASE5}]errors heute (journalctl -p 3)[/]") + console.print() + return 0 + + +# ---------------------------------------------------------------- suggest + + +def _suggest_list() -> int: + import subprocess + from rich.table import Table + + from taninux.core import theme as T + from taninux.suggest.rules import all_suggestions + + console = _console() + installed = { + l for l in subprocess.run( + ["pacman", "-Qq"], capture_output=True, text=True, check=False, + ).stdout.splitlines() if l + } + suggestions = sorted( + all_suggestions(installed), + key=lambda s: (s.priority, s.category), + ) + + console.print() + console.print( + f" [{T.UME}]✦ suggest[/] " + f"[{T.YUKI}]{len(suggestions)}[/] [{T.BASE5}]empfehlungen[/]" + ) + console.print() + if not suggestions: + console.print(f" [{T.TAKE}]nichts zu empfehlen — solides setup.[/]\n") + return 0 + + table = Table( + show_header=True, + header_style=T.SHINKAI, + border_style=T.BASE2, + pad_edge=False, + box=None, + ) + table.add_column("kat", width=8) + table.add_column("anlass") + table.add_column("pakete") + table.add_column("pri", width=6) + for s in suggestions: + pri_color = {1: T.RED, 2: T.CHIKYU, 3: T.BASE5}.get(s.priority, T.BASE5) + pri_label = {1: "hoch", 2: "mittel", 3: "nice"}.get(s.priority, str(s.priority)) + table.add_row( + f"[{T.SHINKAI}]{s.category}[/]", + f"[{T.YUKI}]{s.reason}[/]", + f"[{T.BASE5}]{', '.join(s.packages)}[/]", + f"[{pri_color}]{pri_label}[/]", + ) + console.print(table) + console.print() + return 0 + + +# ---------------------------------------------------------------- snapshot + + +def _snapshot_save(label: str) -> int: + from taninux.core import snapshot as snap + from taninux.core import theme as T + + console = _console() + s = snap.take_snapshot(label=label) + console.print() + console.print( + f" [{T.TAKE}]snapshot[/] [{T.YUKI}]{s.id}[/] " + f"[{T.BASE5}]({s.label})[/]" + ) + console.print( + f" [{T.BASE5}]explicit[/] [{T.YUKI}]{len(s.explicit)}[/] " + f"[{T.BASE5}]aur[/] [{T.YUKI}]{len(s.aur)}[/] " + f"[{T.BASE5}]deps[/] [{T.YUKI}]{len(s.deps)}[/]" + ) + console.print(f" [{T.BASE4}]→ {s.path}[/]") + console.print() + return 0 + + +def _snapshot_list() -> int: + from rich.table import Table + + from taninux.core import snapshot as snap + from taninux.core import theme as T + + console = _console() + snaps = snap.list_snapshots() + if not snaps: + console.print(f" [{T.BASE4}]keine snapshots — speichere mit:[/] taninux snapshot save") + return 0 + + table = Table( + show_header=True, + header_style=T.SHINKAI, + border_style=T.BASE2, + pad_edge=False, + box=None, + ) + table.add_column("id") + table.add_column("zeitpunkt") + table.add_column("label") + table.add_column("explicit", justify="right") + table.add_column("aur", justify="right") + table.add_column("deps", justify="right") + for s in snaps: + table.add_row( + f"[{T.YUKI}]{s.id}[/]", + f"[{T.BASE5}]{s.timestamp}[/]", + f"[{T.BASE5}]{s.label}[/]", + f"[{T.CHIKYU}]{len(s.explicit)}[/]", + f"[{T.CHIKYU}]{len(s.aur)}[/]", + f"[{T.BASE4}]{len(s.deps)}[/]", + ) + console.print() + console.print(table) + console.print() + return 0 + + +def _snapshot_diff(from_id: str | None, to_id: str | None) -> int: + from taninux.core import snapshot as snap + from taninux.core import theme as T + + console = _console() + if from_id is None: + old = snap.latest() + if old is None: + console.print(f" [{T.BASE4}]keine snapshots vorhanden — speichere mit:[/] taninux snapshot save") + return 1 + else: + old = snap.load(from_id) + if old is None: + console.print(f" [{T.RED}]kein snapshot mit id: {from_id}[/]") + return 2 + + if to_id is None: + new = snap.current_snapshot() + else: + new_loaded = snap.load(to_id) + if new_loaded is None: + console.print(f" [{T.RED}]kein snapshot mit id: {to_id}[/]") + return 2 + new = new_loaded + + d = snap.diff(old, new) + console.print() + console.print( + f" [{T.UME}]drift[/] " + f"[{T.BASE5}]{old.id} ({old.label})[/] [{T.BASE3}]→[/] " + f"[{T.BASE5}]{new.id} ({new.label})[/]" + ) + console.print() + + total_added = len(d.added_explicit) + len(d.added_aur) + len(d.added_deps) + total_removed = len(d.removed_explicit) + len(d.removed_aur) + len(d.removed_deps) + if total_added == 0 and total_removed == 0: + console.print(f" [{T.TAKE}]keine änderungen seit {old.timestamp}.[/]") + console.print() + return 0 + + def render_block(title: str, added: list[str], removed: list[str]) -> None: + if not added and not removed: + return + console.print(f" [{T.SHINKAI}]{title}[/]") + for p in added: + console.print(f" [{T.TAKE}]+[/] [{T.YUKI}]{p}[/]") + for p in removed: + console.print(f" [{T.RED}]-[/] [{T.BASE5}]{p}[/]") + console.print() + + render_block("explicit", d.added_explicit, d.removed_explicit) + render_block("aur", d.added_aur, d.removed_aur) + render_block("deps", d.added_deps, d.removed_deps) + + console.print( + f" [{T.BASE4}]gesamt:[/] " + f"[{T.TAKE}]+{total_added}[/] " + f"[{T.RED}]-{total_removed}[/]" + ) + console.print() + return 0 + + +# ---------------------------------------------------------------- bundles + + +def _bundles_list() -> int: + from rich.table import Table + + from taninux.bundles.loader import all_bundles + from taninux.core import pacman as pac + from taninux.core import theme as T + + console = _console() + installed = pac.installed_packages() + bundles = all_bundles() + + console.print() + console.print(f" [{T.UME}]◆ bundles[/] [{T.YUKI}]{len(bundles)}[/] [{T.BASE5}]bundles[/]") + console.print() + + table = Table( + show_header=True, + header_style=T.SHINKAI, + border_style=T.BASE2, + pad_edge=False, + box=None, + ) + table.add_column("", width=2) + table.add_column("id", width=22) + table.add_column("status") + table.add_column("fortschritt") + table.add_column("tagline") + + for b in bundles: + inst = sum(1 for p in b.packages if p in installed) + total = len(b.packages) + state = "satisfied" if inst == total else ("partial" if inst else "none") + glyph, color = { + "satisfied": ("●", T.TAKE), + "partial": ("◐", T.CHIKYU), + "none": ("○", T.BASE4), + }[state] + ratio = inst / total if total else 0 + bar = "█" * int(ratio * 10) + "░" * (10 - int(ratio * 10)) + table.add_row( + f"[{color}]{glyph}[/]", + f"[{T.YUKI}]{b.id}[/]", + f"[{color}]{state}[/]", + f"[{color}]{bar}[/] [{T.BASE5}]{inst}/{total}[/]", + f"[{T.BASE5}]{b.tagline}[/]", + ) + console.print(table) + console.print() + return 0 + + +def _bundles_export(bundle_id: str, label: str | None, tagline: str) -> int: + import subprocess + from taninux.bundles.loader import save_user_bundle + from taninux.core import theme as T + + console = _console() + try: + r = subprocess.run( + ["pacman", "-Qenq"], + capture_output=True, text=True, check=False, timeout=15, + ) + except (FileNotFoundError, subprocess.TimeoutExpired): + console.print(f" [{T.RED}]pacman nicht verfügbar[/]") + return 2 + pkgs = sorted(l.strip() for l in r.stdout.splitlines() if l.strip()) + if not pkgs: + console.print(f" [{T.RED}]keine pakete gefunden[/]") + return 2 + + target = save_user_bundle( + bundle_id=bundle_id, + label=label or bundle_id, + tagline=tagline, + packages=pkgs, + tags=["exported"], + ) + console.print() + console.print( + f" [{T.TAKE}]exportiert:[/] [{T.YUKI}]{target}[/] " + f"[{T.BASE5}]{len(pkgs)} pakete[/]" + ) + console.print( + f" [{T.BASE4}]restore via:[/] taninux bundles install {bundle_id}" + f" [{T.BASE4}](in TUI: bundles tab → cursor auf '{bundle_id}' → i)[/]" + ) + console.print() + return 0 + + +# ---------------------------------------------------------------- whitelist + + +def _whitelist_list() -> int: + from taninux.core.state import State + from taninux.core import theme as T + + console = _console() + state = State.load() + if not state.whitelist and not state.ignored: + console.print(f" [{T.BASE4}]whitelist und ignore-list sind leer.[/]") + return 0 + + home = str(Path.home()) + if state.whitelist: + console.print(f"\n [{T.SHINKAI}]whitelist[/]") + for p in sorted(state.whitelist): + console.print(f" [{T.YUKI}]{str(p).replace(home, '~', 1)}[/]") + if state.ignored: + console.print(f"\n [{T.BASE4}]ignored[/]") + for p in sorted(state.ignored): + console.print(f" [{T.BASE5}]{str(p).replace(home, '~', 1)}[/]") + return 0 + + +def _whitelist_add(path: str) -> int: + from taninux.core.state import State + from taninux.core import theme as T + + console = _console() + p = Path(path).expanduser().resolve() + state = State.load() + state.whitelist_add(p) + console.print(f" [{T.SHINKAI}]+ whitelist:[/] [{T.YUKI}]{p}[/]") + return 0 + + +def _whitelist_remove(path: str) -> int: + from taninux.core.state import State + from taninux.core import theme as T + + console = _console() + p = Path(path).expanduser().resolve() + state = State.load() + state.whitelist_remove(p) + console.print(f" [{T.BASE5}]- whitelist:[/] [{T.YUKI}]{p}[/]") + return 0 + + +# ---------------------------------------------------------------- entry + + +def main() -> int: + parser = argparse.ArgumentParser( + prog="taninux", + description="Mountain-themed Arch Linux maintenance TUI.", + ) + sub = parser.add_subparsers(dest="cmd") + + sub.add_parser("tui", help="Open the interactive TUI (default)") + p_scan = sub.add_parser("scan", help="Print findings without the TUI") + p_scan.add_argument( + "--category", + choices=[ + "all", "dotfolders", "orphans", "pacnew", "cache", + "flatpak-runtimes", "flatpak-data", "aur-cache", + "journal", "coredumps", "kernels", + ], + default="all", + help="Scanner-Kategorie (default: all)", + ) + + p_trash = sub.add_parser("trash", help="Trash management") + p_trash_sub = p_trash.add_subparsers(dest="trash_cmd") + p_trash_sub.add_parser("list", help="List trashed entries") + p_restore = p_trash_sub.add_parser("restore", help="Restore an entry") + p_restore.add_argument("id", help="Trash entry id") + p_purge = p_trash_sub.add_parser("purge", help="Permanently delete trash") + p_purge.add_argument("id", nargs="?", help="Trash entry id (or omit for --all)") + p_purge.add_argument("--all", action="store_true", help="Purge everything") + p_purge.add_argument("--yes", action="store_true", help="Skip confirmation") + + p_update = sub.add_parser("update", help="List pending updates from all sources") + p_update.add_argument("--execute", "-y", action="store_true", + help="Updates direkt ausführen (paru/flatpak/fwupdmgr, --noconfirm)") + sub.add_parser("health", help="System health summary (headless)") + sub.add_parser("suggest", help="System-aware package suggestions") + + p_snap = sub.add_parser("snapshot", help="Pacman state snapshots / drift") + p_snap_sub = p_snap.add_subparsers(dest="snap_cmd") + p_s_save = p_snap_sub.add_parser("save", help="Aktuellen State speichern") + p_s_save.add_argument("--label", default="manual", help="Optionales Label") + p_snap_sub.add_parser("list", help="Alle Snapshots zeigen") + p_s_diff = p_snap_sub.add_parser("diff", help="Diff zwischen Snapshot und jetzt (oder zweier Snapshots)") + p_s_diff.add_argument("from_id", nargs="?", help="Snapshot-ID (default: latest)") + p_s_diff.add_argument("to_id", nargs="?", help="Zweite Snapshot-ID (default: current)") + + p_bundles = sub.add_parser("bundles", help="Bundle management") + p_bundles_sub = p_bundles.add_subparsers(dest="bundles_cmd") + p_bundles_sub.add_parser("list", help="Liste alle bundles (builtin + user)") + p_b_export = p_bundles_sub.add_parser("export", help="Aktuelles System als Bundle speichern") + p_b_export.add_argument("id", help="Bundle-ID (= dateiname ohne .toml)") + p_b_export.add_argument("--label", default=None, help="Display-Name") + p_b_export.add_argument("--tagline", default="exported by taninux", help="Kurze Beschreibung") + p_b_export.add_argument("--explicit-only", action="store_true", default=True, + help="Nur pacman -Qe (explizit installierte, ohne deps)") + + p_wl = sub.add_parser("whitelist", help="Whitelist management") + p_wl_sub = p_wl.add_subparsers(dest="wl_cmd") + p_wl_sub.add_parser("list", help="Show whitelist + ignore-list") + p_add = p_wl_sub.add_parser("add", help="Add a path to the whitelist") + p_add.add_argument("path") + p_rm = p_wl_sub.add_parser("remove", help="Remove a path from the whitelist") + p_rm.add_argument("path") + + args = parser.parse_args() + + if args.cmd in (None, "tui"): + from taninux.app import run + run() + return 0 + if args.cmd == "scan": + return _scan(args.category) + if args.cmd == "update": + if args.execute: + return _update_execute() + return _update_list() + if args.cmd == "health": + return _health_summary() + if args.cmd == "suggest": + return _suggest_list() + if args.cmd == "bundles": + if args.bundles_cmd == "list": + return _bundles_list() + if args.bundles_cmd == "export": + return _bundles_export(args.id, args.label, args.tagline) + p_bundles.print_help() + return 1 + if args.cmd == "snapshot": + if args.snap_cmd == "save": + return _snapshot_save(args.label) + if args.snap_cmd == "list": + return _snapshot_list() + if args.snap_cmd == "diff": + return _snapshot_diff(args.from_id, args.to_id) + p_snap.print_help() + return 1 + if args.cmd == "trash": + if args.trash_cmd == "list": + return _trash_list() + if args.trash_cmd == "restore": + return _trash_restore(args.id) + if args.trash_cmd == "purge": + if args.all: + return _trash_purge_all(args.yes) + return _trash_purge(args.id) + p_trash.print_help() + return 1 + if args.cmd == "whitelist": + if args.wl_cmd == "list": + return _whitelist_list() + if args.wl_cmd == "add": + return _whitelist_add(args.path) + if args.wl_cmd == "remove": + return _whitelist_remove(args.path) + p_wl.print_help() + return 1 + + parser.print_help() + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/__init__.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/accessibility.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/accessibility.py new file mode 100644 index 0000000..e40c017 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/accessibility.py @@ -0,0 +1,52 @@ +"""Accessibility-Schalter über gsettings (user-level, kein root).""" +from __future__ import annotations + +import subprocess + +_IFACE = "org.gnome.desktop.interface" +_A11Y = "org.gnome.desktop.a11y.interface" +_BIG_TEXT = 1.25 + + +def _get(schema: str, key: str) -> str: + try: + r = subprocess.run(["gsettings", "get", schema, key], + capture_output=True, text=True, check=False, timeout=5) + return r.stdout.strip() + except (FileNotFoundError, subprocess.TimeoutExpired): + return "" + + +def _set(schema: str, key: str, value: str) -> None: + try: + subprocess.run(["gsettings", "set", schema, key, value], + check=False, timeout=5) + except (FileNotFoundError, subprocess.TimeoutExpired): + pass + + +def large_text() -> bool: + try: + return float(_get(_IFACE, "text-scaling-factor") or "1") > 1.1 + except ValueError: + return False + + +def set_large_text(on: bool) -> None: + _set(_IFACE, "text-scaling-factor", str(_BIG_TEXT if on else 1.0)) + + +def high_contrast() -> bool: + return _get(_A11Y, "high-contrast") == "true" + + +def set_high_contrast(on: bool) -> None: + _set(_A11Y, "high-contrast", "true" if on else "false") + + +def animations() -> bool: + return _get(_IFACE, "enable-animations") != "false" + + +def set_animations(on: bool) -> None: + _set(_IFACE, "enable-animations", "true" if on else "false") diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/account.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/account.py new file mode 100644 index 0000000..3f6c829 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/account.py @@ -0,0 +1,121 @@ +"""Eigenes Benutzerkonto. UI-frei. + +Konto-Infos aus pwd/grp. Passwort-Änderung des eigenen Users über `passwd`, +das interaktiv ist — wir treiben es über ein PTY (kein root, kein Terminal). +""" +from __future__ import annotations + +import grp +import os +import pwd +import select +import time +from dataclasses import dataclass, field + + +@dataclass +class Account: + username: str + fullname: str + admin: bool # in 'wheel' + groups: list[str] = field(default_factory=list) + + +def current() -> Account: + info = pwd.getpwuid(os.getuid()) + fullname = info.pw_gecos.split(",")[0].strip() or info.pw_name + gids = os.getgrouplist(info.pw_name, info.pw_gid) + names = [] + for gid in gids: + try: + names.append(grp.getgrgid(gid).gr_name) + except KeyError: + pass + return Account(info.pw_name, fullname, "wheel" in names, sorted(names)) + + +def _groups_of(name: str, gid: int) -> list[str]: + names = [] + for g in os.getgrouplist(name, gid): + try: + names.append(grp.getgrgid(g).gr_name) + except KeyError: + pass + return names + + +def list_users() -> list[Account]: + """Menschliche Konten (UID 1000–64999, ohne nobody).""" + me = os.getuid() + out: list[Account] = [] + for info in pwd.getpwall(): + if 1000 <= info.pw_uid < 65000 and info.pw_name != "nobody": + groups = _groups_of(info.pw_name, info.pw_gid) + fullname = info.pw_gecos.split(",")[0].strip() or info.pw_name + acc = Account(info.pw_name, fullname, "wheel" in groups, sorted(groups)) + out.append(acc) + out.sort(key=lambda a: a.username != pwd.getpwuid(me).pw_name) # eigener zuerst + return out + + +def add_user_cmds(username: str, fullname: str, admin: bool) -> list[list[str]]: + """Befehls-Queue: Konto anlegen, optional zu wheel, Passwort-Reset erzwingen.""" + cmds = [["useradd", "-m", "-c", fullname or username, username]] + if admin: + cmds.append(["gpasswd", "-a", username, "wheel"]) + cmds.append(["passwd", "-e", username]) # neuer User setzt PW beim 1. Login + return cmds + + +def delete_user_argv(username: str) -> list[str]: + return ["userdel", "-r", username] + + +def change_password(current_pw: str, new_pw: str, timeout: float = 15.0) -> tuple[bool, str]: + """Eigenes Passwort via passwd über ein PTY ändern. (ok, message).""" + import pty + + pid, fd = pty.fork() + if pid == 0: # child + try: + os.execvp("passwd", ["passwd"]) + except OSError: + os._exit(127) + + answers = [current_pw, new_pw, new_pw] + sent = 0 + out = b"" + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + r, _, _ = select.select([fd], [], [], 0.4) + if fd not in r: + continue + try: + chunk = os.read(fd, 4096) + except OSError: + break + if not chunk: + break + out += chunk + tail = chunk.decode(errors="ignore").rstrip() + if tail.endswith(":") and sent < len(answers): + os.write(fd, (answers[sent] + "\n").encode()) + sent += 1 + + try: + _, status = os.waitpid(pid, 0) + except OSError: + status = 1 + ok = os.WIFEXITED(status) and os.WEXITSTATUS(status) == 0 + text = out.decode(errors="ignore") + msg = "Password changed." if ok else _error_hint(text) + return ok, msg + + +def _error_hint(text: str) -> str: + low = text.lower() + if "authentication" in low or "incorrect" in low: + return "Current password is incorrect." + if "too short" in low or "dictionary" in low or "simplistic" in low or "bad" in low: + return "New password is too weak." + return "Could not change the password." diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/appinfo.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/appinfo.py new file mode 100644 index 0000000..82b1686 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/appinfo.py @@ -0,0 +1,145 @@ +"""App-Detail-Metadaten + Quellen-Probing. UI-frei. + +Holt Beschreibung/Version/Größe/Lizenz für ein Paket aus pacman/paru/flatpak +und ermittelt, aus welchen Quellen (Repo · AUR · Flatpak) es installierbar ist. +Cross-Source-Mapping ist namensbasiert (Repo/AUR teilen oft den Paketnamen); +einen echten AppStream-Mapping gibt es bewusst nicht. +""" +from __future__ import annotations + +import shutil +import subprocess +from dataclasses import dataclass + +from taninux.core import pacman + +SOURCE_LABEL = { + "repo": "Official repository", + "aur": "AUR (built from source)", + "flatpak": "Flatpak (Flathub)", +} + + +@dataclass +class Detail: + source: str # 'repo' | 'aur' | 'flatpak' + pkg: str # pacman-Name bzw. Flatpak-App-ID + version: str = "" + summary: str = "" + description: str = "" + size: str = "" # Download-Größe + installed_size: str = "" + license: str = "" + url: str = "" + remote: str = "flathub" + available: bool = False + installed: bool = False + + +def _run(argv: list[str], timeout: int = 20): + try: + return subprocess.run(argv, capture_output=True, text=True, + check=False, timeout=timeout) + except (FileNotFoundError, subprocess.TimeoutExpired): + return None + + +def _parse_kv(text: str) -> dict[str, str]: + out: dict[str, str] = {} + for line in text.splitlines(): + if ":" not in line: + continue + key, _, val = line.partition(":") + key = key.strip().lower() + if key and key not in out: + out[key] = val.strip() + return out + + +def _pacman_detail(name: str, *, aur: bool) -> Detail | None: + helper = shutil.which("paru") or shutil.which("yay") + argv = [helper, "-Si", name] if (aur and helper) else ["pacman", "-Si", name] + r = _run(argv) + if r is None or r.returncode != 0 or not r.stdout.strip(): + return None + kv = _parse_kv(r.stdout) + # paru -Si markiert AUR-Pakete mit "Repository: aur"; sonst ist's Repo. + source = "aur" if kv.get("repository") == "aur" else "repo" + return Detail( + source=source, + pkg=name, + version=kv.get("version", ""), + summary=kv.get("description", ""), + description=kv.get("description", ""), + size=kv.get("download size", ""), + installed_size=kv.get("installed size", ""), + license=kv.get("licenses", ""), + url=kv.get("url", ""), + available=True, + installed=pacman.is_installed(name), + ) + + +def _flatpak_detail(app_id: str, remote: str = "flathub") -> Detail | None: + if not shutil.which("flatpak"): + return None + r = _run(["flatpak", "remote-info", remote, app_id]) + if r is None or r.returncode != 0 or not r.stdout.strip(): + return None + kv = _parse_kv(r.stdout) + summary = "" + first = r.stdout.strip().splitlines()[0].strip() + if " - " in first: + summary = first.split(" - ", 1)[1].strip() + installed = False + chk = _run(["flatpak", "info", app_id], timeout=8) + if chk is not None and chk.returncode == 0: + installed = True + return Detail( + source="flatpak", + pkg=app_id, + version=kv.get("version", ""), + summary=summary, + description=summary, + size=kv.get("download size", ""), + installed_size=kv.get("installed size", ""), + license=kv.get("license", ""), + url="", + remote=remote, + available=True, + installed=installed, + ) + + +def candidates(*, repo_name: str | None = None, flatpak_id: str | None = None, + remote: str = "flathub", include_aur: bool = False) -> list[Detail]: + """Verfügbare Installationsquellen für eine App (blockierend → Worker).""" + out: list[Detail] = [] + if repo_name: + d = _pacman_detail(repo_name, aur=False) + if d and d.source == "repo": + out.append(d) + if include_aur: + a = _pacman_detail(repo_name, aur=True) + if a and a.source == "aur": # nur echte AUR-Treffer + out.append(a) + if flatpak_id: + f = _flatpak_detail(flatpak_id, remote) + if f: + out.append(f) + return out + + +def install_argv(d: Detail) -> tuple[list[str], bool]: + if d.source == "flatpak": + return ["flatpak", "install", "--assumeyes", d.remote, d.pkg], False + if d.source == "aur": + helper = shutil.which("paru") or shutil.which("yay") or "paru" + return [helper, "--sudo", "pkexec", "-S", "--noconfirm", d.pkg], False + return ["pacman", "-S", "--needed", "--noconfirm", d.pkg], True + + +def remove_argv(d: Detail) -> tuple[list[str], bool]: + if d.source == "flatpak": + return ["flatpak", "uninstall", "--assumeyes", d.pkg], False + return ["pacman", "-Rns", "--noconfirm", d.pkg], True diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/audio.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/audio.py new file mode 100644 index 0000000..dd9dfd6 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/audio.py @@ -0,0 +1,140 @@ +"""Audio-Steuerung via pactl (PulseAudio/PipeWire). UI-frei. + +Genug für eine pavucontrol-artige Geräte+Lautstärke-Steuerung: Sinks/Sources +auflisten, Default setzen, Volume/Mute lesen+setzen. Alles User-Level (kein +root). `kind` ist "sink" (Ausgabe) oder "source" (Eingabe). +""" +from __future__ import annotations + +import re +import subprocess +from dataclasses import dataclass + +_PCT = re.compile(r"(\d+)%") + + +@dataclass +class Device: + name: str # interner pactl-Name (für set-default etc.) + description: str # menschlicher Name (fürs UI) + + +@dataclass +class Stream: + index: str # sink-input-Index (für set-sink-input-* etc.) + app: str # App-Name fürs UI + volume: int + muted: bool + + +def _pactl(args: list[str], timeout: int = 4) -> str: + try: + r = subprocess.run( + ["pactl", *args], + capture_output=True, text=True, check=False, timeout=timeout, + ) + return r.stdout if r.returncode == 0 else "" + except (FileNotFoundError, subprocess.TimeoutExpired): + return "" + + +def available() -> bool: + return bool(_pactl(["info"])) + + +def list_devices(kind: str, include_monitors: bool = False) -> list[Device]: + """Parst `pactl list sinks|sources` für Name + Description je Block.""" + out = _pactl(["list", f"{kind}s"]) + devices: list[Device] = [] + name: str | None = None + for line in out.splitlines(): + if line and not line[0].isspace(): # Block-Header "Sink #N" + name = None + continue + s = line.strip() + if s.startswith("Name:") and name is None: + name = s.split(":", 1)[1].strip() + elif s.startswith("Description:") and name is not None: + desc = s.split(":", 1)[1].strip() + if include_monitors or not name.endswith(".monitor"): + devices.append(Device(name, desc)) + name = None + return devices + + +def get_default(kind: str) -> str: + return _pactl([f"get-default-{kind}"]).strip() + + +def get_volume(kind: str, name: str) -> int: + m = _PCT.search(_pactl([f"get-{kind}-volume", name])) + return int(m.group(1)) if m else 0 + + +def get_muted(kind: str, name: str) -> bool: + return "yes" in _pactl([f"get-{kind}-mute", name]).lower() + + +def set_default(kind: str, name: str) -> None: + _pactl([f"set-default-{kind}", name]) + + +def set_volume(kind: str, name: str, pct: int) -> None: + _pactl([f"set-{kind}-volume", name, f"{int(pct)}%"]) + + +def set_mute(kind: str, name: str, muted: bool) -> None: + _pactl([f"set-{kind}-mute", name, "1" if muted else "0"]) + + +_APP_NAME = re.compile(r'application\.name\s*=\s*"([^"]*)"') +_MEDIA_NAME = re.compile(r'media\.name\s*=\s*"([^"]*)"') + + +def playback_streams() -> list[Stream]: + """Parst `pactl list sink-inputs` zu Stream je App. Robust/leer bei Fehler.""" + out = _pactl(["list", "sink-inputs"]) + streams: list[Stream] = [] + index: str | None = None + muted = False + volume = 0 + app = "" + have_vol = False + + def flush() -> None: + if index is not None: + streams.append(Stream(index, app or f"Stream #{index}", volume, muted)) + + for line in out.splitlines(): + if line and not line[0].isspace(): # Block-Header "Sink Input #N" + flush() + m = re.search(r"#(\d+)", line) + index = m.group(1) if m else None + muted, volume, app, have_vol = False, 0, "", False + continue + s = line.strip() + if s.startswith("Mute:"): + muted = "yes" in s.lower() + elif s.startswith("Volume:") and not have_vol: + m = _PCT.search(s) + if m: + volume = int(m.group(1)) + have_vol = True + else: + m = _APP_NAME.search(s) + if m and not app: + app = m.group(1) + elif not app: + m = _MEDIA_NAME.search(s) + if m: + app = m.group(1) + flush() + return streams + + +def set_stream_volume(index: str, pct: int) -> None: + _pactl(["set-sink-input-volume", str(index), f"{int(pct)}%"]) + + +def set_stream_mute(index: str, muted: bool) -> None: + _pactl(["set-sink-input-mute", str(index), "1" if muted else "0"]) diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/avatar.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/avatar.py new file mode 100644 index 0000000..598db37 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/avatar.py @@ -0,0 +1,64 @@ +"""Profilbild des eigenen Users. UI-frei, kein root. + +Schreibt ~/.face (von vielen Greetern/Apps gelesen) und setzt zusätzlich das +AccountsService-Icon (Greeter/Lockscreen) — Letzteres über D-Bus, das für die +EIGENEN Userdaten per polkit ohne Admin erlaubt ist. +""" +from __future__ import annotations + +import getpass +import shutil +import subprocess +from pathlib import Path + + +def _face() -> Path: + return Path.home() / ".face" + + +def current_path() -> str | None: + asv = Path("/var/lib/AccountsService/icons") / getpass.getuser() + if asv.is_file(): + return str(asv) + if _face().is_file(): + return str(_face()) + return None + + +def _user_object_path() -> str | None: + try: + r = subprocess.run( + ["gdbus", "call", "--system", "--dest", "org.freedesktop.Accounts", + "--object-path", "/org/freedesktop/Accounts", + "--method", "org.freedesktop.Accounts.FindUserByName", getpass.getuser()], + capture_output=True, text=True, check=False, timeout=8, + ) + except (FileNotFoundError, subprocess.TimeoutExpired): + return None + # Ausgabe: (objectpath '/org/freedesktop/Accounts/User1000',) + s = r.stdout + if "'" in s: + return s.split("'")[1] + return None + + +def set_own(image_path: str) -> bool: + """Profilbild setzen. image_path sollte bereits skaliert sein (PNG).""" + ok = False + try: + shutil.copyfile(image_path, _face()) + ok = True + except OSError: + pass + obj = _user_object_path() + if obj: + try: + subprocess.run( + ["gdbus", "call", "--system", "--dest", "org.freedesktop.Accounts", + "--object-path", obj, + "--method", "org.freedesktop.Accounts.User.SetIconFile", image_path], + check=False, timeout=8, + ) + except (FileNotFoundError, subprocess.TimeoutExpired): + pass + return ok diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/backup.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/backup.py new file mode 100644 index 0000000..998ea66 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/backup.py @@ -0,0 +1,63 @@ +"""Backup-Engine — Time-Machine-artige Snapshots über Timeshift (CLI). + +UI-frei: liefert nur argv-Listen + parst die Timeshift-Ausgabe. Timeshift +braucht für ALLES root (auch `--list`) → das Frontend schleust die argv durch +den privilegierten Helper (pkexec). +""" +from __future__ import annotations + +import re +import shutil +from dataclasses import dataclass + +# Zeitstempel-Name eines Snapshots, z.B. "2024-01-02_00-00-01". +_NAME_RE = re.compile(r"\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}") + + +@dataclass +class Snapshot: + name: str # Zeitstempel, z.B. "2024-01-02_00-00-01" + tags: str # Buchstaben-Tags (O/B/H/D/W/M) + description: str # freier Kommentar + + +def available() -> bool: + return shutil.which("timeshift") is not None + + +def list_argv() -> list[str]: + return ["timeshift", "--list"] + + +def create_argv(comment: str) -> list[str]: + return ["timeshift", "--create", "--comments", comment or "TANINUX", "--scripted"] + + +def delete_argv(name: str) -> list[str]: + return ["timeshift", "--delete", "--snapshot", name, "--yes"] + + +def restore_argv(name: str) -> list[str]: + # Sehr destruktiv: stellt das System wieder her, ggf. Reboot nötig. + return ["timeshift", "--restore", "--snapshot", name, "--yes"] + + +def parse_list(text: str) -> list[Snapshot]: + """Parst die Ausgabe von `timeshift --list`. + + BTRFS-Format: nach einer Trennlinie ("----") Zeilen wie + `0 > 2024-01-02_00-00-01 O mein kommentar` + Robust per Regex auf den Zeitstempel; der Rest danach ist tags+description. + Header- und Trennzeilen werden ignoriert. + """ + snapshots: list[Snapshot] = [] + for line in text.splitlines(): + match = _NAME_RE.search(line) + if match is None: + continue + name = match.group(0) + rest = line[match.end():].split(maxsplit=1) + tags = rest[0] if rest else "" + description = rest[1].strip() if len(rest) > 1 else "" + snapshots.append(Snapshot(name=name, tags=tags, description=description)) + return snapshots diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/bluetooth.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/bluetooth.py new file mode 100644 index 0000000..718ed6d --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/bluetooth.py @@ -0,0 +1,99 @@ +"""Bluetooth via bluetoothctl (BlueZ). UI-frei. + +Power an/aus, Geräte (paired/connected/discovered), connect/disconnect, scan. +bluetoothctl-Einzelkommandos laufen one-shot. Für den aktiven User ohne root. +Schreib-Aktionen liefern argv (Aufrufer führt via ProcessRunner aus). +""" +from __future__ import annotations + +import subprocess +from dataclasses import dataclass + + +def _run(args: list[str], timeout: int = 6) -> str: + try: + r = subprocess.run(["bluetoothctl", *args], capture_output=True, + text=True, check=False, timeout=timeout) + return r.stdout if r.returncode == 0 else r.stdout + except (FileNotFoundError, subprocess.TimeoutExpired): + return "" + + +def available() -> bool: + return bool(_run(["show"])) + + +def powered() -> bool: + for line in _run(["show"]).splitlines(): + if "Powered:" in line: + return line.split(":", 1)[1].strip() == "yes" + return False + + +def set_power_argv(on: bool) -> list[str]: + return ["bluetoothctl", "power", "on" if on else "off"] + + +def scan_argv(seconds: int = 8) -> list[str]: + return ["bluetoothctl", "--timeout", str(seconds), "scan", "on"] + + +@dataclass +class BtDevice: + mac: str + name: str + paired: bool + connected: bool + trusted: bool = False + + +def _macs(args: list[str]) -> set[str]: + macs: set[str] = set() + for line in _run(args).splitlines(): + parts = line.split() + if len(parts) >= 2 and parts[0] == "Device": + macs.add(parts[1]) + return macs + + +def devices() -> list[BtDevice]: + paired = _macs(["devices", "Paired"]) + connected = _macs(["devices", "Connected"]) + trusted = _macs(["devices", "Trusted"]) + out: list[BtDevice] = [] + seen: set[str] = set() + for line in _run(["devices"]).splitlines(): + parts = line.split(maxsplit=2) + if len(parts) >= 2 and parts[0] == "Device": + mac = parts[1] + if mac in seen: + continue + seen.add(mac) + name = parts[2] if len(parts) > 2 else mac + out.append(BtDevice( + mac=mac, name=name, + paired=mac in paired, connected=mac in connected, + trusted=mac in trusted, + )) + # Verbundene/gepairte zuerst + return sorted(out, key=lambda d: (not d.connected, not d.paired, d.name.lower())) + + +def connect_argv(mac: str) -> list[str]: + return ["bluetoothctl", "connect", mac] + + +def disconnect_argv(mac: str) -> list[str]: + return ["bluetoothctl", "disconnect", mac] + + +def pair_argv(mac: str) -> list[str]: + return ["bluetoothctl", "pair", mac] + + +def trust_argv(mac: str) -> list[str]: + return ["bluetoothctl", "trust", mac] + + +def remove_argv(mac: str) -> list[str]: + return ["bluetoothctl", "remove", mac] diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/bootloader.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/bootloader.py new file mode 100644 index 0000000..671ca7b --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/bootloader.py @@ -0,0 +1,142 @@ +"""Bootloader-Detection + Default-Entry Management. + +Unterstützt: +- systemd-boot (via /boot/loader/entries/ + bootctl) +- GRUB (info-only — grub-set-default ist fragil, command zeigen) +""" +from __future__ import annotations + +import re +import subprocess +from dataclasses import dataclass +from pathlib import Path + + +@dataclass +class BootloaderInfo: + kind: str # "systemd-boot" / "grub" / "unknown" + config_path: Path | None + default_entry: str | None # bei systemd-boot der entry-name (z.B. "arch.conf") + entries: list[Path] # alle .conf entries (nur systemd-boot) + + +def detect() -> BootloaderInfo: + # systemd-boot + loader_conf = Path("/boot/loader/loader.conf") + entries_dir = Path("/boot/loader/entries") + if loader_conf.exists() and entries_dir.is_dir(): + default = None + try: + for line in loader_conf.read_text().splitlines(): + line = line.strip() + if line.startswith("default"): + parts = line.split(maxsplit=1) + if len(parts) == 2: + default = parts[1].strip() + break + except OSError: + pass + # bootctl status liefert oft den effektiven default (falls "default" in loader.conf einen pattern hat) + if not default or "*" in default: + try: + r = subprocess.run( + ["bootctl", "list", "--json=short"], + capture_output=True, text=True, check=False, timeout=5, + ) + if r.returncode == 0: + import json + for entry in json.loads(r.stdout): + if entry.get("isDefault"): + default = entry.get("id", default) + break + except (FileNotFoundError, subprocess.TimeoutExpired, ValueError): + pass + return BootloaderInfo( + kind="systemd-boot", + config_path=loader_conf, + default_entry=default, + entries=sorted(entries_dir.glob("*.conf")), + ) + + # GRUB + grub_cfg = Path("/boot/grub/grub.cfg") + if grub_cfg.exists(): + default = None + try: + etcdefault = Path("/etc/default/grub").read_text() + m = re.search(r'^GRUB_DEFAULT\s*=\s*"?([^"\n]+)"?', etcdefault, re.MULTILINE) + if m: + default = m.group(1).strip() + except OSError: + pass + return BootloaderInfo( + kind="grub", + config_path=grub_cfg, + default_entry=default, + entries=[], + ) + + return BootloaderInfo(kind="unknown", config_path=None, + default_entry=None, entries=[]) + + +def entry_for_kernel(bl: BootloaderInfo, kernel_pkg: str) -> Path | None: + """Findet das systemd-boot .conf entry für ein gegebenes Kernel-Paket. + + Match-Strategie: entry enthält `linux /vmlinuz-` wo suffix mit + `kernel_pkg` korrespondiert. `linux` → `/vmlinuz-linux`, + `linux-zen` → `/vmlinuz-linux-zen`.""" + if bl.kind != "systemd-boot": + return None + target = f"vmlinuz-{kernel_pkg}" + for entry in bl.entries: + try: + content = entry.read_text() + except OSError: + continue + # Match exakt: vmlinuz-linux-zen, NICHT vmlinuz-linux wenn pkg=linux-zen + for line in content.splitlines(): + line = line.strip() + if line.startswith("linux"): + # Format: "linux /vmlinuz-linux-zen" + parts = line.split() + if len(parts) >= 2: + path = parts[1].rstrip("/").split("/")[-1] + if path == target: + return entry + return None + + +def is_default(bl: BootloaderInfo, entry: Path) -> bool: + if bl.default_entry is None: + return False + return bl.default_entry == entry.name or bl.default_entry == entry.stem + + +def set_default_argv(bl: BootloaderInfo, entry) -> list[str] | None: + """ARGV zum Setzen des Default-Boot-Entry. + + core eskaliert NICHT selbst — gibt nur die Befehlsliste zurück, die der + (GTK-)Aufruferprivilegiert (pkexec) ausführt. Passt zum Muster „command zeigen". + + - systemd-boot: `bootctl set-default ` (entry-id = .conf-Dateiname). + `entry` darf ein str, eine Path oder None sein. + - GRUB: None — `grub-set-default` ist fragil (Index vs. Menüpfad, danach + `grub-mkconfig` nötig); bewusst nicht automatisiert. + """ + if bl.kind != "systemd-boot" or entry is None: + return None + # entry-id für bootctl ist der Dateiname des .conf-Eintrags + entry_id = entry.name if isinstance(entry, Path) else str(entry) + return ["bootctl", "set-default", entry_id] + + +def prune_argv(keep: int) -> list[str]: + """ARGV zum Aufräumen alter zwischengespeicherter Pakete via `paccache`. + + Meint den pacman-Cache (/var/cache/pacman/pkg) — darin liegen alte Versionen + aller Pakete, inkl. alter Kernel. `paccache -rk ` entfernt alle bis auf + die `keep` jüngsten Versionen pro Paket; idiomatischer Arch-Weg. + core eskaliert nicht selbst — der Aufrufer führt das privilegiert aus. + """ + return ["paccache", "-rk", str(max(0, int(keep)))] diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/catalog.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/catalog.py new file mode 100644 index 0000000..22a5541 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/catalog.py @@ -0,0 +1,110 @@ +"""Kuratierter App-Katalog für Endnutzer (GNOME-Software-Stil). UI-frei. + +Beliebte Desktop-Apps, die ein normaler User typisch braucht — gruppiert nach +Kategorie. Quelle ist entweder ein Arch-Repo-Paket (via paru) oder Flathub. +Empfohlen wird, was noch nicht installiert ist. +""" +from __future__ import annotations + +import shutil +import subprocess +from dataclasses import dataclass + + +@dataclass(frozen=True) +class App: + id: str # stabiler Key + name: str + summary: str + category: str + source: str # 'repo' | 'flatpak' + pkg: str # pacman-Name bzw. Flatpak-App-ID + icon: str # bevorzugter Icon-Name (Fallback = Kategorie-Icon) + + +CATEGORY_ORDER = [ + "Internet", "Office", "Communication", "Multimedia", + "Graphics", "Games", "Productivity", "Utilities", +] + +CATEGORY_ICON = { + "Internet": "web-browser-symbolic", + "Office": "x-office-document-symbolic", + "Communication": "mail-send-symbolic", + "Multimedia": "multimedia-player-symbolic", + "Graphics": "applications-graphics-symbolic", + "Games": "applications-games-symbolic", + "Productivity": "view-list-symbolic", + "Utilities": "applications-utilities-symbolic", +} + +CATALOG: list[App] = [ + # Internet + App("firefox", "Firefox", "Fast, private web browser", "Internet", "repo", "firefox", "firefox"), + App("chromium", "Chromium", "Open-source web browser", "Internet", "repo", "chromium", "chromium"), + App("qbittorrent", "qBittorrent", "Lightweight BitTorrent client", "Internet", "repo", "qbittorrent", "qbittorrent"), + # Office + App("libreoffice", "LibreOffice", "Word processor, spreadsheets and slides", "Office", "repo", "libreoffice-fresh", "libreoffice-startcenter"), + App("okular", "Okular", "View PDFs, comics and e-books", "Office", "repo", "okular", "okular"), + # Communication + App("thunderbird", "Thunderbird", "Email, calendar and contacts", "Communication", "repo", "thunderbird", "thunderbird"), + App("signal", "Signal", "Private encrypted messaging", "Communication", "repo", "signal-desktop", "signal-desktop"), + App("telegram", "Telegram", "Cloud-based messaging", "Communication", "repo", "telegram-desktop", "telegram"), + App("discord", "Discord", "Voice and text chat for communities", "Communication", "repo", "discord", "discord"), + # Multimedia + App("vlc", "VLC", "Plays nearly any audio or video file", "Multimedia", "repo", "vlc", "vlc"), + App("spotify", "Spotify", "Music streaming", "Multimedia", "flatpak", "com.spotify.Client", "com.spotify.Client"), + App("obs", "OBS Studio", "Screen recording and live streaming", "Multimedia", "repo", "obs-studio", "com.obsproject.Studio"), + App("kdenlive", "Kdenlive", "Non-linear video editor", "Multimedia", "repo", "kdenlive", "kdenlive"), + # Graphics + App("gimp", "GIMP", "Powerful image editor", "Graphics", "repo", "gimp", "gimp"), + App("inkscape", "Inkscape", "Vector graphics editor", "Graphics", "repo", "inkscape", "org.inkscape.Inkscape"), + App("krita", "Krita", "Digital painting studio", "Graphics", "repo", "krita", "krita"), + App("blender", "Blender", "3D modelling and animation", "Graphics", "repo", "blender", "blender"), + # Games + App("steam", "Steam", "The biggest PC game store", "Games", "repo", "steam", "steam"), + App("lutris", "Lutris", "Manage and run games from anywhere", "Games", "repo", "lutris", "lutris"), + App("heroic", "Heroic", "Epic, GOG and Amazon games launcher", "Games", "flatpak", "com.heroicgameslauncher.hgl", "com.heroicgameslauncher.hgl"), + # Productivity + App("obsidian", "Obsidian", "Markdown notes and knowledge base", "Productivity", "flatpak", "md.obsidian.Obsidian", "md.obsidian.Obsidian"), + App("keepassxc", "KeePassXC", "Secure offline password manager", "Productivity", "repo", "keepassxc", "keepassxc"), + # Utilities + App("bottles", "Bottles", "Run Windows software easily", "Utilities", "flatpak", "com.usebottles.bottles", "com.usebottles.bottles"), + App("flatseal", "Flatseal", "Manage Flatpak app permissions", "Utilities", "flatpak", "com.github.tchx84.Flatseal", "com.github.tchx84.Flatseal"), +] + + +def _flatpak_installed_ids() -> set[str]: + if not shutil.which("flatpak"): + return set() + try: + r = subprocess.run( + ["flatpak", "list", "--app", "--columns=application"], + capture_output=True, text=True, check=False, timeout=10, + ) + except (FileNotFoundError, subprocess.TimeoutExpired): + return set() + return {ln.strip() for ln in r.stdout.splitlines() if ln.strip()} + + +def is_installed(app: App, pacman_set: set[str], flatpak_ids: set[str]) -> bool: + if app.source == "flatpak": + return app.pkg in flatpak_ids + return app.pkg in pacman_set + + +def recommended(pacman_set: set[str], flatpak_ids: set[str] | None = None) -> list[App]: + """Noch nicht installierte Katalog-Apps, in Kategorie-Reihenfolge.""" + if flatpak_ids is None: + flatpak_ids = _flatpak_installed_ids() + out = [a for a in CATALOG if not is_installed(a, pacman_set, flatpak_ids)] + out.sort(key=lambda a: (CATEGORY_ORDER.index(a.category) + if a.category in CATEGORY_ORDER else 99, a.name.lower())) + return out + + +def install_argv(app: App) -> tuple[list[str], bool]: + """(argv, privileged). Katalog-Apps sind Repo (via Helper) oder Flatpak.""" + if app.source == "flatpak": + return ["flatpak", "install", "--assumeyes", "flathub", app.pkg], False + return ["pacman", "-S", "--needed", "--noconfirm", app.pkg], True diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/compositor.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/compositor.py new file mode 100644 index 0000000..18d7393 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/compositor.py @@ -0,0 +1,37 @@ +"""Compositor — TANINUX ist niri-only. UI-frei. + +Früher unterschied dieses Modul Hyprland vs Sway/SwayFX; seit der Umstellung +auf **niri** gibt es nur noch einen Compositor. Das Modul bleibt als dünne +Schicht erhalten, damit die Session-Erkennung („läuft niri überhaupt?") an +EINER Stelle liegt — genutzt von der Display-Seite (niri-IPC verfügbar?). +""" +from __future__ import annotations + +import os +import subprocess + +NIRI = "niri" +UNKNOWN = "unknown" + + +def detect() -> str: + # niri exportiert NIRI_SOCKET in jede Session — das ist der zuverlässige Weg. + if os.environ.get("NIRI_SOCKET"): + return NIRI + # Fallback: laufender Prozess (z.B. wenn aus anderem Env gestartet). + try: + r = subprocess.run(["pgrep", "-x", "niri"], + capture_output=True, timeout=3) + if r.returncode == 0: + return NIRI + except (FileNotFoundError, subprocess.TimeoutExpired): + pass + return UNKNOWN + + +def is_niri() -> bool: + return detect() == NIRI + + +def pretty() -> str: + return "niri" if is_niri() else "unknown" diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/confirm.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/confirm.py new file mode 100644 index 0000000..b40dffb --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/confirm.py @@ -0,0 +1,89 @@ +"""Keyboard-driven confirmation modal im Mountain-Stil.""" +from __future__ import annotations + +from textual.app import ComposeResult +from textual.binding import Binding +from textual.containers import Vertical +from textual.screen import ModalScreen +from textual.widgets import Static + +from taninux.core import theme as T + + +def _escape_markup(s: str) -> str: + """Escape Textual/rich markup brackets — wir wollen body als plain text.""" + return s.replace("[", "\\[").replace("]", "\\]") + + +class ConfirmModal(ModalScreen[bool]): + """Yes/No-Modal — `y` bestätigt, `n`/`esc` bricht ab.""" + + BINDINGS = [ + Binding("y", "confirm", "ja"), + Binding("n", "cancel", "nein"), + Binding("enter", "confirm", "ja", show=False), + Binding("escape", "cancel", "abbruch"), + ] + + CSS = f""" + ConfirmModal {{ + align: center middle; + background: rgba(0, 0, 0, 0.6); + }} + ConfirmModal #dialog {{ + width: 80; + height: auto; + max-height: 90%; + padding: 1 2; + background: {T.YORU}; + border: round {T.BASE3}; + }} + ConfirmModal #c-title {{ + height: 1; + background: {T.YORU}; + }} + ConfirmModal #c-body {{ + height: auto; + margin-top: 1; + background: {T.YORU}; + color: {T.YUKI}; + }} + ConfirmModal #c-hint {{ + height: 1; + margin-top: 1; + background: {T.YORU}; + color: {T.BASE4}; + }} + """ + + def __init__(self, title: str, body: str, danger: bool = False) -> None: + super().__init__() + self._title = title + self._body = body + self._danger = danger + + def compose(self) -> ComposeResult: + title_color = T.RED if self._danger else T.UME + with Vertical(id="dialog"): + yield Static( + f"[{title_color}]{self._title}[/]", + id="c-title", + markup=True, + ) + # Body: markup escaped — sonst crashen brackets aus argv-output + yield Static( + _escape_markup(self._body), + id="c-body", + ) + yield Static( + f"[{T.BASE5}]y[/]/[{T.BASE5}]enter[/] [{T.BASE4}]bestätigen[/] " + f"[{T.BASE5}]n[/]/[{T.BASE5}]esc[/] [{T.BASE4}]abbrechen[/]", + id="c-hint", + markup=True, + ) + + def action_confirm(self) -> None: + self.dismiss(True) + + def action_cancel(self) -> None: + self.dismiss(False) diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/defaultapps.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/defaultapps.py new file mode 100644 index 0000000..feda5b9 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/defaultapps.py @@ -0,0 +1,112 @@ +"""Standard-Anwendungen pro Kategorie. UI-frei, user-level (kein root). + +Liest/setzt die MIME-Default-Apps über `gio mime` + `xdg-mime`. Anzeigename und +Icon kommen aus den .desktop-Dateien. +""" +from __future__ import annotations + +import os +import subprocess +from dataclasses import dataclass +from pathlib import Path + +# (Label, repräsentativer MIME-Typ, Fallback-Icon) +CATEGORIES = [ + ("Web browser", "x-scheme-handler/https", "web-browser-symbolic"), + ("Email", "x-scheme-handler/mailto", "mail-send-symbolic"), + ("Files", "inode/directory", "folder-symbolic"), + ("Images", "image/png", "image-x-generic-symbolic"), + ("Audio", "audio/mpeg", "audio-x-generic-symbolic"), + ("Video", "video/mp4", "video-x-generic-symbolic"), + ("PDF documents", "application/pdf", "x-office-document-symbolic"), +] + +_DIRS = [ + Path.home() / ".local/share/applications", + Path.home() / ".local/share/flatpak/exports/share/applications", + Path("/var/lib/flatpak/exports/share/applications"), + Path("/usr/share/applications"), +] + + +@dataclass +class App: + id: str # desktop file id, z.B. firefox.desktop + name: str + icon: str + + +def _find_desktop(desktop_id: str) -> Path | None: + for d in _DIRS: + p = d / desktop_id + if p.is_file(): + return p + return None + + +def _meta(desktop_id: str) -> App: + name, icon = desktop_id.removesuffix(".desktop"), "" + path = _find_desktop(desktop_id) + if path: + try: + in_entry = False + for line in path.read_text().splitlines(): + s = line.strip() + if s.startswith("["): + in_entry = s == "[Desktop Entry]" + elif in_entry and s.startswith("Name=") and name == desktop_id.removesuffix(".desktop"): + name = s[5:].strip() + elif in_entry and s.startswith("Icon="): + icon = s[5:].strip() + except OSError: + pass + return App(id=desktop_id, name=name, icon=icon) + + +def _gio_mime(mime: str) -> tuple[str | None, list[str]]: + env = {**os.environ, "LC_ALL": "C"} + try: + r = subprocess.run(["gio", "mime", mime], capture_output=True, text=True, + check=False, timeout=8, env=env) + except (FileNotFoundError, subprocess.TimeoutExpired): + return None, [] + default: str | None = None + recommended: list[str] = [] + section = None + for line in r.stdout.splitlines(): + if line.startswith("Default application"): + default = line.split(":", 1)[1].strip() or None + elif line.startswith("Recommended applications"): + section = "rec" + elif line.startswith("Registered applications"): + section = "reg" if not recommended else section + elif line.startswith("\t") or line.startswith(" "): + app = line.strip() + if app.endswith(".desktop") and app not in recommended and section in ("rec", "reg"): + recommended.append(app) + return default, recommended + + +def apps_for(mime: str) -> tuple[App | None, list[App]]: + """(aktueller Default, verfügbare Apps) für einen MIME-Typ.""" + default_id, ids = _gio_mime(mime) + apps = [_meta(i) for i in ids] + current = _meta(default_id) if default_id else None + return current, apps + + +def set_default(desktop_id: str, mime: str) -> bool: + ok = True + try: + subprocess.run(["xdg-mime", "default", desktop_id, mime], + check=False, timeout=8) + except (FileNotFoundError, subprocess.TimeoutExpired): + ok = False + if mime.startswith("x-scheme-handler/http"): + # Browser zusätzlich als System-Default registrieren. + try: + subprocess.run(["xdg-settings", "set", "default-web-browser", desktop_id], + check=False, timeout=8) + except (FileNotFoundError, subprocess.TimeoutExpired): + pass + return ok diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/display.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/display.py new file mode 100644 index 0000000..852fb5e --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/display.py @@ -0,0 +1,291 @@ +"""Display / Output-Einstellungen — niri-only über native IPC. UI-frei. + +Liest Monitore via `niri msg --json outputs` und normalisiert sie auf ein +`Output`-Dataclass. Apply erzeugt das passende argv (`niri msg output …`). + +Bewusst NUR live: `niri msg output` ändert die Konfiguration temporär, nichts +wird in die niri-Config (config.kdl) geschrieben. Nach einem Reload/Login sind +die Änderungen wieder weg. +""" +from __future__ import annotations + +import json +import re +import shutil +import subprocess +from dataclasses import dataclass, field +from pathlib import Path + +# Eigener TANINUX-User-Service für wlsunset (analog swayidle in core/power.py). +# Der niri-Autostart (spawn-at-startup) wird NICHT angefasst. +WLSUNSET_UNIT = ( + Path.home() / ".config" / "systemd" / "user" / "taninux-wlsunset.service" +) + + +@dataclass +class Output: + name: str + make: str + model: str + width: int + height: int + refresh_hz: float + x: int + y: int + scale: float + transform: str # "normal" / "90" / "180" / "270" + enabled: bool + modes: list[tuple[int, int, float]] = field(default_factory=list) # (w, h, hz) + + +# -- Lesen ---------------------------------------------------------------- + +def _run_json(argv: list[str]): + try: + r = subprocess.run(argv, capture_output=True, text=True, timeout=5) + except (FileNotFoundError, subprocess.TimeoutExpired): + return None + if r.returncode != 0 or not r.stdout.strip(): + return None + try: + return json.loads(r.stdout) + except json.JSONDecodeError: + return None + + +def _norm_transform(value) -> str: + """niri-Transform → "normal"/"90"/"180"/"270". + + niri liefert z.B. "Normal", "_90", "Flipped", "Flipped90". Die UI bietet + nur die vier rechtwinkligen Rotationen; flipped-* fallen auf "normal" / + deren Rotationsanteil zurück. + """ + s = str(value).lower().replace("flipped", "").strip("_-") + return s if s in ("90", "180", "270") else "normal" + + +def outputs() -> list[Output]: + # `niri msg --json outputs` liefert ein dict {name: output}. + data = _run_json(["niri", "msg", "--json", "outputs"]) + if not isinstance(data, dict): + return [] + result: list[Output] = [] + for name, o in sorted(data.items()): + modes: list[tuple[int, int, float]] = [] + for md in o.get("modes", []) or []: + modes.append(( + int(md.get("width", 0)), + int(md.get("height", 0)), + int(md.get("refresh_rate", 0)) / 1000.0, # niri: mHz -> Hz + )) + # logical ist null, wenn der Output deaktiviert ist. + logical = o.get("logical") or None + enabled = logical is not None + + # current_mode ist ein Index in modes (null wenn aus / unbekannt). + cur_idx = o.get("current_mode") + if isinstance(cur_idx, int) and 0 <= cur_idx < len(modes): + cw, ch, chz = modes[cur_idx] + elif modes: + cw, ch, chz = modes[0] + else: + cw, ch, chz = 0, 0, 0.0 + + result.append(Output( + name=name, + make=o.get("make", "") or "", + model=o.get("model", "") or "", + width=cw, + height=ch, + refresh_hz=chz, + x=int(logical.get("x", 0)) if logical else 0, + y=int(logical.get("y", 0)) if logical else 0, + scale=float(logical.get("scale", 1.0)) if logical else 1.0, + transform=_norm_transform(logical.get("transform", "normal")) if logical else "normal", + enabled=enabled, + modes=modes, + )) + return result + + +# -- Apply ---------------------------------------------------------------- + +def _fmt_hz(hz: float) -> str: + """Refresh kompakt: '143.973' statt '143.97300'. Ganze Zahlen ohne Punkt. + + Drei Nachkommastellen, weil niri die Modes mit mHz-Präzision führt + (z.B. 59.951) und den Mode-String sonst nicht trifft. + """ + s = f"{hz:.3f}".rstrip("0").rstrip(".") + return s or "0" + + +def _fmt_scale(scale: float) -> str: + s = f"{scale:.2f}".rstrip("0").rstrip(".") + return s or "1" + + +def apply_argv( + out: Output, + *, + mode: tuple[int, int, float] | None = None, + scale: float | None = None, + transform: str | None = None, + enabled: bool | None = None, +) -> list[str]: + """argv für GENAU eine Änderung an `out` via `niri msg output`. + Unprivilegiert ausführbar. Genau einer der Keyword-Parameter sollte gesetzt sein. + + niri ändert die Output-Config temporär (nicht persistent) — passt zur + bewussten „nur live"-Semantik dieses Moduls. + """ + base = ["niri", "msg", "output", out.name] + if enabled is not None: + return base + (["on"] if enabled else ["off"]) + if mode is not None: + w, h, hz = mode + return base + ["mode", f"{w}x{h}@{_fmt_hz(hz)}"] + if scale is not None: + return base + ["scale", _fmt_scale(scale)] + if transform is not None: + return base + ["transform", transform] + return base + + +# -- Night Light / Farbtemperatur (wlsunset) ------------------------------ +# +# TANINUX verwaltet einen EIGENEN systemd-User-Service +# taninux-wlsunset.service (analog zum swayidle-Ansatz in core/power.py). +# wlsunset hat keine Config-Datei — alle Parameter stehen in den ExecStart-Args. +# Karims Compositor-Autostart bleibt unberührt. + +# Defaults = Karims Werte aus dem Autostart. +_NL_DEFAULT_DAY = 6500 +_NL_DEFAULT_NIGHT = 4000 +_NL_DEFAULT_LAT = 47.0 +_NL_DEFAULT_LON = 8.3 + + +@dataclass +class NightLight: + enabled: bool + day_temp: int + night_temp: int + lat: float + lon: float + + +def wlsunset_installed() -> bool: + return shutil.which("wlsunset") is not None + + +def night_light() -> NightLight: + """Liest die aktuellen Night-Light-Werte aus der TANINUX-Unit. + + enabled = Unit existiert (wir verwalten genau diesen Service). Die + Parameter kommen aus den ExecStart-Args: -t , -T , + -l , -L . Fehlt etwas, greifen die Defaults. Robust gegen + fehlende/kaputte Unit. + """ + day = _NL_DEFAULT_DAY + night = _NL_DEFAULT_NIGHT + lat = _NL_DEFAULT_LAT + lon = _NL_DEFAULT_LON + enabled = WLSUNSET_UNIT.is_file() + if enabled: + try: + text = WLSUNSET_UNIT.read_text(errors="ignore") + except OSError: + text = "" + m = re.search(r"^\s*ExecStart\s*=\s*(.+)$", text, re.MULTILINE) + exec_line = m.group(1) if m else "" + + def _int_arg(flag: str, fallback: int) -> int: + mm = re.search(rf"(?:^|\s){re.escape(flag)}\s+(\d+)", exec_line) + return int(mm.group(1)) if mm else fallback + + def _float_arg(flag: str, fallback: float) -> float: + mm = re.search( + rf"(?:^|\s){re.escape(flag)}\s+(-?[\d.]+)", exec_line + ) + try: + return float(mm.group(1)) if mm else fallback + except ValueError: + return fallback + + night = _int_arg("-t", night) + day = _int_arg("-T", day) + lat = _float_arg("-l", lat) + lon = _float_arg("-L", lon) + return NightLight( + enabled=enabled, day_temp=day, night_temp=night, lat=lat, lon=lon + ) + + +def _fmt_coord(v: float) -> str: + """Koordinate kompakt: '47' statt '47.0', '8.3' bleibt '8.3'.""" + s = f"{v:.4f}".rstrip("0").rstrip(".") + return s or "0" + + +def write_night_light( + day_temp: int, night_temp: int, lat: float, lon: float +) -> Path: + """Schreibt die TANINUX-verwaltete wlsunset-User-Unit. Alle Parameter + landen in der ExecStart-Zeile. Gibt den geschriebenen Pfad zurück.""" + wlsunset = shutil.which("wlsunset") or "/usr/bin/wlsunset" + exec_start = ( + f"{wlsunset} -l {_fmt_coord(lat)} -L {_fmt_coord(lon)} " + f"-t {int(night_temp)} -T {int(day_temp)}" + ) + lines = [ + "[Unit]", + "Description=TANINUX-managed wlsunset (night light)", + "Documentation=man:wlsunset(1)", + "PartOf=graphical-session.target", + "", + "[Service]", + "Type=simple", + f"ExecStart={exec_start}", + "Restart=on-failure", + "", + "[Install]", + "WantedBy=default.target", + "", + ] + WLSUNSET_UNIT.parent.mkdir(parents=True, exist_ok=True) + WLSUNSET_UNIT.write_text("\n".join(lines)) + return WLSUNSET_UNIT + + +def apply_night_light(enable: bool) -> bool: + """User-Service anwenden (best-effort, unprivilegiert). + + enable → daemon-reload + enable --now + restart. + disable → disable --now. True wenn die Befehle abgesetzt wurden. + """ + try: + subprocess.run( + ["systemctl", "--user", "daemon-reload"], + check=False, timeout=8, + ) + if enable: + subprocess.run( + ["systemctl", "--user", "enable", "--now", + "taninux-wlsunset.service"], + check=False, timeout=8, + ) + subprocess.run( + ["systemctl", "--user", "restart", "taninux-wlsunset.service"], + check=False, timeout=8, + ) + else: + subprocess.run( + ["systemctl", "--user", "disable", "--now", + "taninux-wlsunset.service"], + check=False, timeout=8, + ) + return True + except (FileNotFoundError, subprocess.TimeoutExpired): + return False diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/firewall.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/firewall.py new file mode 100644 index 0000000..3231220 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/firewall.py @@ -0,0 +1,45 @@ +"""Firewall — einfacher An/Aus-Status. UI-frei. Bevorzugt ufw, sonst firewalld. + +Status ist ohne root lesbar (systemctl is-active); Umschalten läuft privilegiert +über den Helper. +""" +from __future__ import annotations + +import shutil +import subprocess + + +def backend() -> str | None: + if shutil.which("ufw"): + return "ufw" + if shutil.which("firewall-cmd"): + return "firewalld" + return None + + +def installed() -> bool: + return backend() is not None + + +def _service() -> str: + return "firewalld" if backend() == "firewalld" else "ufw" + + +def enabled() -> bool: + try: + r = subprocess.run(["systemctl", "is-active", _service()], + capture_output=True, text=True, check=False, timeout=5) + return r.stdout.strip() == "active" + except (FileNotFoundError, subprocess.TimeoutExpired): + return False + + +def set_enabled_argv(on: bool) -> list[str]: + if backend() == "ufw": + return ["ufw", "enable"] if on else ["ufw", "disable"] + action = "enable" if on else "disable" + return ["systemctl", action, "--now", "firewalld"] + + +def install_argv() -> list[str]: + return ["pacman", "-S", "--needed", "--noconfirm", "ufw"] diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/localization.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/localization.py new file mode 100644 index 0000000..ea878fe --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/localization.py @@ -0,0 +1,117 @@ +"""Locale + Tastatur-Layout via localectl (systemd). + +UI-frei — von TUI und GTK-Frontend nutzbar. Lese-Funktionen laufen +unprivilegiert; die Schreib-Aktionen liefern nur das ARGV (wie bei +core.bootloader), das der Aufrufer privilegiert über den Helper ausführt. +core eskaliert NIE selbst. +""" +from __future__ import annotations + +import subprocess +from dataclasses import dataclass + + +def _run(args: list[str], timeout: int = 5) -> str: + try: + r = subprocess.run( + args, capture_output=True, text=True, check=False, timeout=timeout, + ) + return r.stdout if r.returncode == 0 else "" + except (FileNotFoundError, subprocess.TimeoutExpired): + return "" + + +@dataclass +class LocaleStatus: + locale: str # LANG-Wert, z.B. "de_DE.UTF-8" + vc_keymap: str # Konsolen-Keymap, z.B. "de-latin1" + x11_layout: str # XKB-Layout, z.B. "de" + x11_variant: str # XKB-Variante, z.B. "nodeadkeys" + + +def status() -> LocaleStatus: + """Parst `localectl status`.""" + out = _run(["localectl", "status"]) + fields = {"locale": "", "vc_keymap": "", "x11_layout": "", "x11_variant": ""} + for line in out.splitlines(): + line = line.strip() + if line.startswith("System Locale:"): + val = line.split(":", 1)[1].strip() + # "LANG=de_DE.UTF-8" (evtl. mehrere, durch Whitespace getrennt) + for part in val.split(): + if part.startswith("LANG="): + fields["locale"] = part.split("=", 1)[1] + elif line.startswith("VC Keymap:"): + fields["vc_keymap"] = line.split(":", 1)[1].strip() + elif line.startswith("X11 Layout:"): + fields["x11_layout"] = line.split(":", 1)[1].strip() + elif line.startswith("X11 Variant:"): + fields["x11_variant"] = line.split(":", 1)[1].strip() + return LocaleStatus(**fields) + + +def list_locales() -> list[str]: + """Verfügbare (generierte) Locales. Fallback auf `locale -a`.""" + out = _run(["localectl", "list-locales"]) + if not out: + out = _run(["locale", "-a"]) + return [l.strip() for l in out.splitlines() if l.strip()] + + +def current_timezone() -> str: + """Aktuelle System-Zeitzone, z.B. "Europe/Zurich". '' bei Fehler.""" + return _run(["timedatectl", "show", "-p", "Timezone", "--value"]).strip() + + +def list_timezones() -> list[str]: + """Alle bekannten Zeitzonen (timedatectl list-timezones).""" + out = _run(["timedatectl", "list-timezones"]) + return [l.strip() for l in out.splitlines() if l.strip()] + + +def ntp_enabled() -> bool: + """Ob NTP-Zeitsynchronisation (systemd-timesyncd o.ä.) aktiv ist.""" + return _run(["timedatectl", "show", "-p", "NTP", "--value"]).strip() == "yes" + + +def local_time() -> str: + """Kurze, lesbare lokale Zeit inkl. Zeitzonen-Kürzel (reiner Read).""" + return _run(["date", "+%Y-%m-%d %H:%M %Z"]).strip() + + +def list_x11_layouts() -> list[str]: + out = _run(["localectl", "list-x11-keymap-layouts"]) + return [l.strip() for l in out.splitlines() if l.strip()] + + +def list_x11_variants(layout: str) -> list[str]: + """XKB-Varianten für ein Layout (z.B. nodeadkeys, dvorak). '' = Standard.""" + out = _run(["localectl", "list-x11-keymap-variants", layout]) + return [""] + [l.strip() for l in out.splitlines() if l.strip()] + + +# ---- Schreib-Aktionen: nur ARGV liefern (Aufrufer führt privilegiert aus) ---- + + +def set_locale_argv(lang: str) -> list[str]: + return ["localectl", "set-locale", f"LANG={lang}"] + + +def set_x11_keymap_argv(layout: str, variant: str = "") -> list[str]: + # Syntax: set-x11-keymap LAYOUT [MODEL [VARIANT [OPTIONS]]]. + # Variante ohne Model -> leeres Model als Platzhalter durchreichen. + if variant: + return ["localectl", "set-x11-keymap", layout, "", variant] + return ["localectl", "set-x11-keymap", layout] + + +def set_keymap_argv(keymap: str) -> list[str]: + return ["localectl", "set-keymap", keymap] + + +def set_timezone_argv(tz: str) -> list[str]: + return ["timedatectl", "set-timezone", tz] + + +def set_ntp_argv(on: bool) -> list[str]: + return ["timedatectl", "set-ntp", "true" if on else "false"] diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/lock.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/lock.py new file mode 100644 index 0000000..3693f23 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/lock.py @@ -0,0 +1,28 @@ +"""Screen lock. UI-frei. niri-only — Locker via swaylock (ext-session-lock).""" +from __future__ import annotations + +import shutil + +# swaylock spricht das ext-session-lock-Protokoll, das niri implementiert; +# gtklock als Fallback. (hyprlock entfällt — Hyprland-only.) +_LOCKERS = ("swaylock", "gtklock") + + +def locker() -> str | None: + for name in _LOCKERS: + if shutil.which(name): + return name + return None + + +def preferred() -> str: + return "swaylock" + + +def lock_now_argv() -> list[str] | None: + name = locker() + return [name] if name else None + + +def install_argv() -> list[str]: + return ["pacman", "-S", "--needed", "--noconfirm", preferred()] diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/network.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/network.py new file mode 100644 index 0000000..48dbbae --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/network.py @@ -0,0 +1,211 @@ +"""Netzwerk via nmcli (NetworkManager). UI-frei. + +WiFi an/aus, Status, verfügbare Netze, Verbinden. Für den aktiven User ohne +root (NetworkManager-polkit erlaubt network-control). Schreib-Aktionen liefern +argv (der Aufrufer führt sie via ProcessRunner aus, mit Live-Output). +""" +from __future__ import annotations + +import subprocess +from dataclasses import dataclass + + +def _run(args: list[str], timeout: int = 8) -> str: + try: + r = subprocess.run(["nmcli", *args], capture_output=True, text=True, + check=False, timeout=timeout) + return r.stdout if r.returncode == 0 else "" + except (FileNotFoundError, subprocess.TimeoutExpired): + return "" + + +def available() -> bool: + return bool(_run(["-t", "-f", "RUNNING", "general"])) + + +def _split(line: str) -> list[str]: + """nmcli -t Felder splitten, escaped '\\:' respektieren.""" + out: list[str] = [] + cur = "" + i = 0 + while i < len(line): + c = line[i] + if c == "\\" and i + 1 < len(line): + cur += line[i + 1] + i += 2 + continue + if c == ":": + out.append(cur) + cur = "" + i += 1 + continue + cur += c + i += 1 + out.append(cur) + return out + + +@dataclass +class WifiNet: + ssid: str + signal: int # 0..100 + security: str # "" = offen + in_use: bool + + +def wifi_enabled() -> bool: + return _run(["-t", "-f", "WIFI", "radio"]).strip() == "enabled" + + +def set_wifi_argv(on: bool) -> list[str]: + return ["nmcli", "radio", "wifi", "on" if on else "off"] + + +def active_summary() -> str: + """Kurzbeschreibung der aktiven Verbindung(en).""" + out = _run(["-t", "-f", "NAME,TYPE,DEVICE", "connection", "show", "--active"]) + parts = [] + for line in out.splitlines(): + f = _split(line) + if len(f) >= 3 and f[1] != "loopback": + parts.append(f"{f[0]} ({f[2]})") + return ", ".join(parts) if parts else "Not connected" + + +def wifi_networks(rescan: bool = False) -> list[WifiNet]: + # rescan=False -> NetworkManagers Cache (instant); True -> aktiver Scan (langsam). + out = _run(["-t", "-f", "IN-USE,SSID,SIGNAL,SECURITY", "device", "wifi", "list", + "--rescan", "yes" if rescan else "no"]) + best: dict[str, WifiNet] = {} + for line in out.splitlines(): + f = _split(line) + if len(f) < 4: + continue + in_use, ssid, sig, sec = f[0], f[1], f[2], f[3] + if not ssid: + continue + try: + signal = int(sig) + except ValueError: + signal = 0 + net = WifiNet(ssid=ssid, signal=signal, security=sec.strip(), + in_use=in_use.strip() == "*") + cur = best.get(ssid) + if cur is None or net.signal > cur.signal or net.in_use: + best[ssid] = net + return sorted(best.values(), key=lambda n: (not n.in_use, -n.signal)) + + +def connect_argv(ssid: str, password: str = "") -> list[str]: + argv = ["nmcli", "device", "wifi", "connect", ssid] + if password: + argv += ["password", password] + return argv + + +def saved_connections() -> set[str]: + """Namen gespeicherter NetworkManager-Verbindungen.""" + out = _run(["-t", "-f", "NAME", "connection", "show"]) + names: set[str] = set() + for line in out.splitlines(): + f = _split(line) + if f and f[0]: + names.add(f[0]) + return names + + +def wifi_device() -> str: + """Erstes Wi-Fi-Device (z.B. 'wlp3s0'), sonst leer.""" + out = _run(["-t", "-f", "DEVICE,TYPE", "device"]) + for line in out.splitlines(): + f = _split(line) + if len(f) >= 2 and f[1].strip() == "wifi": + return f[0] + return "" + + +def disconnect_argv() -> list[str]: + """Disconnect des aktiven Wi-Fi via Device. Leer, wenn kein Device.""" + dev = wifi_device() + if not dev: + return [] + return ["nmcli", "device", "disconnect", dev] + + +def forget_argv(ssid: str) -> list[str]: + """Gespeicherte Verbindung löschen.""" + return ["nmcli", "connection", "delete", ssid] + + +# ----------------------------------------------------------------- VPN + +@dataclass +class Vpn: + name: str + vtype: str # 'vpn' | 'wireguard' + active: bool + + +def vpn_connections() -> list[Vpn]: + active = set() + for line in _run(["-t", "-f", "NAME", "connection", "show", "--active"]).splitlines(): + if line.strip(): + active.add(line.strip()) + out: list[Vpn] = [] + for line in _run(["-t", "-f", "NAME,TYPE", "connection", "show"]).splitlines(): + parts = _split(line) + if len(parts) >= 2 and parts[1] in ("vpn", "wireguard"): + out.append(Vpn(parts[0], parts[1], parts[0] in active)) + return out + + +def vpn_up_argv(name: str) -> list[str]: + return ["nmcli", "connection", "up", name] + + +def vpn_down_argv(name: str) -> list[str]: + return ["nmcli", "connection", "down", name] + + +def vpn_import_argv(path: str) -> list[str] | None: + low = path.lower() + if low.endswith(".ovpn"): + vtype = "openvpn" + elif low.endswith(".conf"): + vtype = "wireguard" + else: + return None + return ["nmcli", "connection", "import", "type", vtype, "file", path] + + +# ----------------------------------------------------------------- DNS + +DNS_PRESETS = { + "Automatic": "", + "Cloudflare": "1.1.1.1 1.0.0.1", + "Google": "8.8.8.8 8.8.4.4", + "Quad9": "9.9.9.9 149.112.112.112", +} + + +def primary_connection() -> str | None: + for line in _run(["-t", "-f", "NAME,TYPE", "connection", "show", "--active"]).splitlines(): + parts = _split(line) + if len(parts) >= 2 and parts[1] in ("802-11-wireless", "802-3-ethernet"): + return parts[0] + return None + + +def get_dns(name: str) -> str: + return _run(["-g", "ipv4.dns", "connection", "show", name]).strip().replace("|", " ") + + +def set_dns_cmds(name: str, servers: str) -> list[list[str]]: + """modify + reaktivieren. servers='' = automatisch (DHCP).""" + if servers: + modify = ["nmcli", "connection", "modify", name, + "ipv4.dns", servers, "ipv4.ignore-auto-dns", "yes"] + else: + modify = ["nmcli", "connection", "modify", name, + "ipv4.dns", "", "ipv4.ignore-auto-dns", "no"] + return [modify, ["nmcli", "connection", "up", name]] diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/nextcloud.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/nextcloud.py new file mode 100644 index 0000000..c19cde6 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/nextcloud.py @@ -0,0 +1,135 @@ +"""Nextcloud — bindet den vorhandenen Desktop-Client ein. UI-frei. + +Liest die Konten aus `~/.config/Nextcloud/nextcloud.cfg` (INI, Keys im Abschnitt +`[Accounts]` mit Index-Präfix wie `0\\url`, `0\\dav_user`, `0\\displayName`), +startet den Client, verwaltet den Autostart und liefert die WebDAV-URI, um ein +Konto als Nautilus-Bookmark zu hinterlegen. Alles User-Level, kein root. +""" +from __future__ import annotations + +import configparser +import os +import shutil +import subprocess +from dataclasses import dataclass + + +def _config_home() -> str: + return os.environ.get("XDG_CONFIG_HOME") or os.path.expanduser("~/.config") + + +def _cfg_path() -> str: + return os.path.join(_config_home(), "Nextcloud", "nextcloud.cfg") + + +def _autostart_path() -> str: + return os.path.join(_config_home(), "autostart", "Nextcloud.desktop") + + +def client_installed() -> bool: + return shutil.which("nextcloud") is not None + + +@dataclass +class Account: + name: str + url: str + dav_user: str + + +def accounts() -> list[Account]: + """Konten aus nextcloud.cfg. Robust: leere Liste bei Fehler/fehlender Datei.""" + cp = configparser.ConfigParser() + cp.optionxform = str # Groß-/Kleinschreibung der Keys erhalten + try: + cp.read(_cfg_path(), encoding="utf-8") + except (OSError, configparser.Error): + return [] + + if not cp.has_section("Accounts"): + return [] + + # Keys nach führendem Index gruppieren: "0\url" → idx "0", key "url". + grouped: dict[str, dict[str, str]] = {} + for key, value in cp.items("Accounts"): + if "\\" not in key: + continue + idx, sub = key.split("\\", 1) + grouped.setdefault(idx, {})[sub] = value + + out: list[Account] = [] + for idx in sorted(grouped): + data = grouped[idx] + url = data.get("url", "").strip() + dav_user = data.get("dav_user", "").strip() + name = data.get("displayName", "").strip() or dav_user or url + if not url and not dav_user: + continue + out.append(Account(name=name, url=url, dav_user=dav_user)) + return out + + +def webdav_uri_from(server_url: str, user: str) -> str: + """GVfs-WebDAV-URI (davs:// = HTTPS) aus Server-URL + User bauen — ganz + ohne Client. Nimmt rohe Eingaben (mit/ohne https://, mit/ohne Slash).""" + host = server_url.strip() + for prefix in ("https://", "http://"): + if host.startswith(prefix): + host = host[len(prefix):] + break + host = host.rstrip("/") + return f"davs://{host}/remote.php/dav/files/{user.strip()}/" + + +def webdav_uri(acc: Account) -> str: + """WebDAV-URI für ein erkanntes Client-Konto (Komfort).""" + return webdav_uri_from(acc.url, acc.dav_user) + + +def autostart_enabled() -> bool: + path = _autostart_path() + if not os.path.exists(path): + return False + try: + with open(path, encoding="utf-8") as fh: + for line in fh: + if line.strip().lower() == "hidden=true": + return False + except OSError: + return False + return True + + +_DESKTOP_ENTRY = ( + "[Desktop Entry]\n" + "Type=Application\n" + "Name=Nextcloud\n" + "Exec=nextcloud --background\n" + "Icon=Nextcloud\n" + "Terminal=false\n" + "X-GNOME-Autostart-enabled=true\n" +) + + +def set_autostart(on: bool) -> None: + """Autostart über eine .desktop-Datei in ~/.config/autostart steuern.""" + path = _autostart_path() + if on: + os.makedirs(os.path.dirname(path), exist_ok=True) + with open(path, "w", encoding="utf-8") as fh: + fh.write(_DESKTOP_ENTRY) + else: + try: + os.remove(path) + except FileNotFoundError: + pass + except OSError: + pass + + +def launch() -> None: + """Client starten, ohne zu blockieren. Fehler werden geschluckt.""" + try: + subprocess.Popen(["nextcloud"], start_new_session=True) + except (OSError, subprocess.SubprocessError): + pass diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/package_index.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/package_index.py new file mode 100644 index 0000000..821ed4d --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/package_index.py @@ -0,0 +1,81 @@ +"""Index aller pacman-bekannten Namen, gegen die Dotfolder gematcht werden.""" +from __future__ import annotations + +import re +import subprocess +from dataclasses import dataclass, field +from functools import lru_cache +from pathlib import Path + +# Pfade, deren Basename ein guter Hint auf einen App-Namen ist +_BIN_PREFIXES = ("/usr/bin/", "/usr/local/bin/", "/usr/lib/") + + +def _normalize(name: str) -> str: + """App-Namen für Fuzzy-Match normalisieren: lowercase, ohne -_. Trennzeichen.""" + return re.sub(r"[-_.\s]", "", name.lower()) + + +@dataclass +class PackageIndex: + """Set von normalisierten Tokens (Paketnamen + Binary-Namen), + gegen die ein Dotfolder gematcht werden kann.""" + + tokens: set[str] = field(default_factory=set) + package_names: set[str] = field(default_factory=set) + + def matches(self, dirname: str) -> str | None: + """Gibt den Original-Paketnamen zurück wenn ein Match gefunden wird.""" + norm = _normalize(dirname) + if not norm: + return None + if norm in self.tokens: + for pkg in self.package_names: + if _normalize(pkg) == norm: + return pkg + return norm + # Substring-Match (eine Richtung) - z.B. "discord-canary" matcht "discord" + for pkg in self.package_names: + npkg = _normalize(pkg) + if not npkg or len(npkg) < 3: + continue + if npkg in norm or norm in npkg: + return pkg + return None + + +@lru_cache(maxsize=1) +def build_index() -> PackageIndex: + """Index einmalig bauen, danach gecached. Liest pacman -Ql.""" + idx = PackageIndex() + try: + result = subprocess.run( + ["pacman", "-Ql"], + capture_output=True, + text=True, + check=True, + timeout=30, + ) + except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError): + return idx + + for line in result.stdout.splitlines(): + # Format: " " + parts = line.split(" ", 1) + if len(parts) != 2: + continue + pkg, path = parts + idx.package_names.add(pkg) + idx.tokens.add(_normalize(pkg)) + + # Auch Binary-Basenames als Tokens (z.B. "code" für "code-oss") + for prefix in _BIN_PREFIXES: + if path.startswith(prefix): + tail = path[len(prefix):].rstrip("/") + if "/" in tail: + continue # Subverzeichnisse überspringen, nur direkte Binaries + if tail: + idx.tokens.add(_normalize(tail)) + break + + return idx diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/packages.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/packages.py new file mode 100644 index 0000000..7243a7a --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/packages.py @@ -0,0 +1,46 @@ +"""Paket-Zähler — pacman + flatpak. UI-frei. + +Schnelle Counts fürs Dashboard. pacman-Queries sind billig; flatpak kann etwas +dauern, daher im Aufrufer am besten async laden. +""" +from __future__ import annotations + +import shutil +import subprocess + +from taninux.core.pacman import installed_packages + + +def _count(args: list[str], timeout: int = 8) -> int: + try: + r = subprocess.run(args, capture_output=True, text=True, + check=False, timeout=timeout) + if r.returncode != 0: + return 0 + return sum(1 for line in r.stdout.splitlines() if line.strip()) + except (FileNotFoundError, subprocess.TimeoutExpired): + return 0 + + +def pacman_total() -> int: + return len(installed_packages()) + + +def pacman_explicit() -> int: + return _count(["pacman", "-Qeq"]) + + +def pacman_foreign() -> int: + """AUR / nicht-Repo-Pakete.""" + return _count(["pacman", "-Qmq"]) + + +def flatpak_installed() -> bool: + return shutil.which("flatpak") is not None + + +def flatpak_apps() -> int: + """Anzahl installierter Flatpak-Apps; -1 wenn flatpak fehlt.""" + if not flatpak_installed(): + return -1 + return _count(["flatpak", "list", "--app", "--columns=application"]) diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/pacman.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/pacman.py new file mode 100644 index 0000000..14a734c --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/pacman.py @@ -0,0 +1,29 @@ +"""Shared pacman queries mit Cache-Invalidation. + +Zentral statt in jedem Pillar einen Subprocess-Call. Cache wird gecleart +nach jedem write-Aktion (install/remove).""" +from __future__ import annotations + +import subprocess +from functools import lru_cache + + +@lru_cache(maxsize=1) +def installed_packages() -> frozenset[str]: + try: + r = subprocess.run( + ["pacman", "-Qq"], + capture_output=True, text=True, check=False, timeout=10, + ) + except (FileNotFoundError, subprocess.TimeoutExpired): + return frozenset() + return frozenset(l.strip() for l in r.stdout.splitlines() if l.strip()) + + +def is_installed(name: str) -> bool: + return name in installed_packages() + + +def invalidate() -> None: + """Nach pacman -S/-R aufrufen.""" + installed_packages.cache_clear() diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/panel.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/panel.py new file mode 100644 index 0000000..133d3f8 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/panel.py @@ -0,0 +1,169 @@ +"""Panel & Dock — schreibt panel.json + dock-pins, die eww konsumiert. UI-frei. + +Vertrag: docs/eww-panel-dock-integration.md. TANINUX fasst ~/eww NIE direkt an; +es schreibt nur die Config-Dateien und feuert optional den eww-Apply-Hook +(scripts/panelcfg.sh apply) — das ist ein eww-BEREITGESTELLTES Skript, kein +Edit. eww zieht sonst per Watcher (≤2 s) nach. +""" +from __future__ import annotations + +import json +import os +import subprocess +from dataclasses import dataclass +from pathlib import Path + +from taninux.core.paths import data_dir + +MODULES = ("music", "sys", "updates", "net", "bt", "vol") + +_DEFAULTS = { + "version": 1, + "dock": {"autohide": True, "icon_size": 42, "hide_delay": 1.2}, + "bar": { + "clock_format": "%H:%M %A, %d.%m.%Y", + "modules": {m: True for m in MODULES}, + }, + # Update-Scope: gilt für Hub UND den eww-Update-Zähler. AUR ist Opt-in + # (braucht paru/base-devel) — Default Repo + Flatpak. + "updates": {"include_aur": False}, +} + + +def _panel_path() -> Path: + return data_dir() / "panel.json" + + +def _pins_path() -> Path: + base = os.environ.get("XDG_CONFIG_HOME") or str(Path.home() / ".config") + return Path(base) / "eww" / "dock-pins" + + +def _apply_script() -> Path: + base = os.environ.get("XDG_CONFIG_HOME") or str(Path.home() / ".config") + return Path(base) / "eww" / "scripts" / "panelcfg.sh" + + +# ----------------------------------------------------------- panel.json + +def load() -> dict: + """panel.json lesen, mit Defaults auffüllen (defensiv).""" + cfg = json.loads(json.dumps(_DEFAULTS)) # deep copy + try: + data = json.loads(_panel_path().read_text()) + except (OSError, json.JSONDecodeError): + return cfg + if isinstance(data.get("dock"), dict): + cfg["dock"].update({k: v for k, v in data["dock"].items() + if k in cfg["dock"]}) + bar = data.get("bar") + if isinstance(bar, dict): + if isinstance(bar.get("clock_format"), str): + cfg["bar"]["clock_format"] = bar["clock_format"] + if isinstance(bar.get("modules"), dict): + for m in MODULES: + if m in bar["modules"]: + cfg["bar"]["modules"][m] = bool(bar["modules"][m]) + upd = data.get("updates") + if isinstance(upd, dict) and "include_aur" in upd: + cfg["updates"]["include_aur"] = bool(upd["include_aur"]) + return cfg + + +def save(cfg: dict, *, apply: bool = True) -> None: + """panel.json schreiben (mit Clamping) und optional eww anwenden lassen.""" + dock = cfg.get("dock", {}) + try: + icon = int(dock.get("icon_size", 42)) + except (TypeError, ValueError): + icon = 42 + try: + delay = float(dock.get("hide_delay", 1.2)) + except (TypeError, ValueError): + delay = 1.2 + out = { + "version": 1, + "dock": { + "autohide": bool(dock.get("autohide", True)), + "icon_size": max(24, min(64, icon)), + "hide_delay": max(0.2, min(5.0, round(delay, 1))), + }, + "bar": { + "clock_format": str(cfg.get("bar", {}).get("clock_format") + or _DEFAULTS["bar"]["clock_format"]), + "modules": { + m: bool(cfg.get("bar", {}).get("modules", {}).get(m, True)) + for m in MODULES + }, + }, + "updates": { + "include_aur": bool(cfg.get("updates", {}).get("include_aur", False)), + }, + } + p = _panel_path() + try: + p.write_text(json.dumps(out, indent=2)) + except OSError: + return + if apply: + apply_now() + + +def include_aur() -> bool: + """Ob AUR im Update-/Such-Scope ist (Opt-in). Single source: panel.json.""" + return bool(load().get("updates", {}).get("include_aur", False)) + + +def set_include_aur(value: bool) -> None: + """AUR-Scope umschalten — panel.json aktualisieren + eww anwenden lassen.""" + cfg = load() + cfg.setdefault("updates", {})["include_aur"] = bool(value) + save(cfg, apply=True) + + +def apply_now() -> None: + """eww-Apply-Hook feuern (best-effort). Skript gehört eww, wird nur ausgeführt.""" + script = _apply_script() + if not script.exists(): + return + try: + subprocess.run([str(script), "apply"], check=False, timeout=5) + except (FileNotFoundError, subprocess.TimeoutExpired, PermissionError): + pass + + +# ----------------------------------------------------------- dock-pins + +@dataclass +class Pin: + exec: str + wmclass: str + icon: str + + +def load_pins() -> list[Pin]: + """dock-pins lesen — Zeilen `exec|class|icon`.""" + pins: list[Pin] = [] + try: + text = _pins_path().read_text() + except OSError: + return pins + for line in text.splitlines(): + line = line.strip() + if not line or line.startswith("#"): + continue + parts = line.split("|") + if len(parts) >= 3: + pins.append(Pin(parts[0].strip(), parts[1].strip(), parts[2].strip())) + return pins + + +def save_pins(pins: list[Pin]) -> None: + """dock-pins komplett neu schreiben (idempotent; eww pollt alle 2 s).""" + lines = [f"{p.exec}|{p.wmclass}|{p.icon}" for p in pins] + path = _pins_path() + try: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text("\n".join(lines) + ("\n" if lines else "")) + except OSError: + pass diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/paths.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/paths.py new file mode 100644 index 0000000..80a677a --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/paths.py @@ -0,0 +1,34 @@ +"""XDG-konforme Pfade für taninux.""" +from __future__ import annotations + +import os +from pathlib import Path + + +def data_dir() -> Path: + base = os.environ.get("XDG_DATA_HOME") or str(Path.home() / ".local" / "share") + d = Path(base) / "taninux" + d.mkdir(parents=True, exist_ok=True) + return d + + +def trash_dir() -> Path: + d = data_dir() / "trash" + d.mkdir(parents=True, exist_ok=True) + return d + + +def state_file() -> Path: + return data_dir() / "state.json" + + +def bundles_dir() -> Path: + d = data_dir() / "bundles" + d.mkdir(parents=True, exist_ok=True) + return d + + +def snapshots_dir() -> Path: + d = data_dir() / "snapshots" + d.mkdir(parents=True, exist_ok=True) + return d diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/pkg.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/pkg.py new file mode 100644 index 0000000..6d13cee --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/pkg.py @@ -0,0 +1,154 @@ +"""Pacman-Pakete-Metadaten (name/version/size/reason/repo/desc). + +Bevorzugt `expac` für Geschwindigkeit (parse alle 6k pakete in <500ms). +Fallback auf `pacman -Qi` falls expac nicht installiert (~5s).""" +from __future__ import annotations + +import re +import shutil +import subprocess +from dataclasses import dataclass + + +_SIZE_RE = re.compile(r"([\d.]+)\s*([KMGT]i?B|B)") +_UNITS = { + "B": 1, "KiB": 1024, "MiB": 1024**2, "GiB": 1024**3, "TiB": 1024**4, + "KB": 1000, "MB": 1000**2, "GB": 1000**3, "TB": 1000**4, +} + + +def _parse_size(s: str) -> int: + m = _SIZE_RE.search(s) + if not m: + return 0 + val = float(m.group(1)) + return int(val * _UNITS.get(m.group(2), 1)) + + +@dataclass +class Package: + name: str + version: str + size_bytes: int + reason: str # 'explicit' / 'dependency' + repo: str # 'core' / 'extra' / 'multilib' / 'aur' (foreign) / 'local' + desc: str = "" + is_orphan: bool = False + + +def _foreign_set() -> set[str]: + try: + r = subprocess.run( + ["pacman", "-Qmq"], + capture_output=True, text=True, check=False, timeout=10, + ) + return {l.strip() for l in r.stdout.splitlines() if l.strip()} + except (FileNotFoundError, subprocess.TimeoutExpired): + return set() + + +def _orphan_set() -> set[str]: + try: + r = subprocess.run( + ["pacman", "-Qdtq"], + capture_output=True, text=True, check=False, timeout=10, + ) + return {l.strip() for l in r.stdout.splitlines() if l.strip()} + except (FileNotFoundError, subprocess.TimeoutExpired): + return set() + + +def _via_expac() -> list[Package]: + try: + r = subprocess.run( + ["expac", "-Q", "%n\t%v\t%m\t%w\t%r\t%d"], + capture_output=True, text=True, check=False, timeout=30, + ) + except (FileNotFoundError, subprocess.TimeoutExpired): + return [] + out: list[Package] = [] + for line in r.stdout.splitlines(): + parts = line.split("\t") + if len(parts) < 5: + continue + try: + size = int(parts[2]) + except ValueError: + size = 0 + out.append(Package( + name=parts[0], + version=parts[1], + size_bytes=size, + reason=parts[3] or "explicit", + repo=parts[4] or "local", + desc=parts[5] if len(parts) > 5 else "", + )) + return out + + +def _via_pacman() -> list[Package]: + try: + r = subprocess.run( + ["pacman", "-Qi"], + capture_output=True, text=True, check=False, timeout=60, + ) + except (FileNotFoundError, subprocess.TimeoutExpired): + return [] + out: list[Package] = [] + cur: dict[str, object] = {} + for line in r.stdout.splitlines(): + if not line.strip(): + if cur.get("name"): + out.append(Package( + name=str(cur["name"]), + version=str(cur.get("version", "")), + size_bytes=int(cur.get("size", 0)), + reason=str(cur.get("reason", "explicit")), + repo=str(cur.get("repo", "local")), + desc=str(cur.get("desc", "")), + )) + cur = {} + continue + if ":" not in line: + continue + key, _, val = line.partition(":") + key = key.strip().lower() + val = val.strip() + if key == "name": + cur["name"] = val + elif key == "version": + cur["version"] = val + elif key == "installed size": + cur["size"] = _parse_size(val) + elif key == "install reason": + cur["reason"] = "explicit" if "explicit" in val.lower() else "dependency" + elif key == "repository": + cur["repo"] = val + elif key == "description": + cur["desc"] = val + if cur.get("name"): + out.append(Package( + name=str(cur["name"]), + version=str(cur.get("version", "")), + size_bytes=int(cur.get("size", 0)), + reason=str(cur.get("reason", "explicit")), + repo=str(cur.get("repo", "local")), + desc=str(cur.get("desc", "")), + )) + return out + + +def all_installed() -> list[Package]: + if shutil.which("expac"): + pkgs = _via_expac() + else: + pkgs = _via_pacman() + + foreign = _foreign_set() + orphans = _orphan_set() + for p in pkgs: + if p.name in foreign: + p.repo = "aur" + if p.name in orphans: + p.is_orphan = True + return pkgs diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/power.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/power.py new file mode 100644 index 0000000..1a4343c --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/power.py @@ -0,0 +1,271 @@ +"""Power: Akku-Info (read) + Idle-Timeouts (niri-only über swayidle). + +Akku/AC kommen aus /sys/class/power_supply (reiner Read). Idle-Timeouts +(Bildschirm aus / Suspend) verwaltet TANINUX über swayidle: niri implementiert +das ext-idle-notify-Protokoll, daher ist swayidle der passende Idle-Daemon. +Verwaltet wird ein eigener taninux-swayidle.service (swayidle hat KEINE +Config-Datei — alles steht in den ExecStart-Args); der Bildschirm wird per +`niri msg action power-off-monitors` ab- und wieder angeschaltet. + +Die niri-Config (config.kdl) wird nicht angefasst; Autostart läuft über den +systemd-User-Service. +""" +from __future__ import annotations + +import re +import shutil +import subprocess +from dataclasses import dataclass +from pathlib import Path + +_PS = Path("/sys/class/power_supply") +SWAYIDLE_UNIT = ( + Path.home() / ".config" / "systemd" / "user" / "taninux-swayidle.service" +) + + +# ----------------------------------------------------------- Akku / AC + +@dataclass +class Battery: + name: str + capacity: int # Prozent, -1 = unbekannt + status: str # Charging / Discharging / Full / ? + + +def _read(p: Path) -> str: + try: + return p.read_text().strip() + except OSError: + return "" + + +def batteries() -> list[Battery]: + out: list[Battery] = [] + if not _PS.is_dir(): + return out + for d in sorted(_PS.iterdir()): + if _read(d / "type") == "Battery": + cap = _read(d / "capacity") + out.append(Battery( + name=d.name, + capacity=int(cap) if cap.isdigit() else -1, + status=_read(d / "status") or "?", + )) + return out + + +def ac_online() -> bool | None: + """True/False wenn ein AC-Adapter gefunden wird, sonst None.""" + if not _PS.is_dir(): + return None + for d in sorted(_PS.iterdir()): + if _read(d / "type") == "Mains" or d.name.startswith(("AC", "ADP")): + v = _read(d / "online") + if v in ("0", "1"): + return v == "1" + return None + + +# --------------------------------------------------- Energy-Mode (PPD) + +def _out(args: list[str], timeout: int = 5) -> str: + try: + r = subprocess.run(args, capture_output=True, text=True, + check=False, timeout=timeout) + return r.stdout if r.returncode == 0 else "" + except (FileNotFoundError, subprocess.TimeoutExpired): + return "" + + +def ppd_installed() -> bool: + return shutil.which("powerprofilesctl") is not None + + +def power_profiles() -> list[str]: + """Verfügbare Profile aus `powerprofilesctl list` (z.B. performance, + balanced, power-saver). Leer wenn Daemon nicht läuft.""" + profs: list[str] = [] + for line in _out(["powerprofilesctl", "list"]).splitlines(): + s = line.strip() + if s.startswith("* "): # aktiv markiert + s = s[2:] + if re.fullmatch(r"[a-z][a-z-]*:", s): # reine "name:"-Zeile + profs.append(s[:-1]) + return profs + + +def current_profile() -> str: + return _out(["powerprofilesctl", "get"]).strip() + + +def set_profile(name: str) -> None: + try: + subprocess.run(["powerprofilesctl", "set", name], + check=False, timeout=5) + except (FileNotFoundError, subprocess.TimeoutExpired): + pass + + +# ------------------------------------------------- Bildschirmhelligkeit + +def brightness_available() -> bool: + """True, wenn brightnessctl da ist UND es ein Backlight-Gerät gibt. + Desktops ohne Backlight → False (Gruppe wird dann weggelassen).""" + if shutil.which("brightnessctl") is None: + return False + # `-c backlight info` gibt rc 0 nur bei vorhandenem Backlight. + try: + r = subprocess.run( + ["brightnessctl", "-c", "backlight", "info"], + capture_output=True, text=True, check=False, timeout=5, + ) + if r.returncode == 0: + return True + except (FileNotFoundError, subprocess.TimeoutExpired): + return False + # Fallback: device-Liste nach class 'backlight' durchsuchen. + return "class 'backlight'" in _out(["brightnessctl", "-l"]) + + +def brightness_percent() -> int: + """Aktuelle Helligkeit in Prozent. 0 bei Fehler.""" + # machine-readable: device,class,current,percent,max — Feld 4 ist "NN%". + line = _out(["brightnessctl", "-m", "-c", "backlight", "info"]).strip() + if line: + fields = line.splitlines()[0].split(",") + if len(fields) >= 4: + pct = fields[3].rstrip("%") + if pct.isdigit(): + return int(pct) + # Fallback: aus get/max berechnen. + cur = _out(["brightnessctl", "-c", "backlight", "get"]).strip() + mx = _out(["brightnessctl", "-c", "backlight", "max"]).strip() + if cur.isdigit() and mx.isdigit() and int(mx) > 0: + return round(int(cur) * 100 / int(mx)) + return 0 + + +def set_brightness_argv(pct: int) -> list[str]: + """argv zum Setzen der Helligkeit (min 1 %, nie 0).""" + return ["brightnessctl", "-c", "backlight", "set", + f"{max(1, min(100, int(pct)))}%"] + + +# ----------------------------------------------------------- swayidle (niri) + +def swayidle_installed() -> bool: + return shutil.which("swayidle") is not None + + +def _read_idle_swayidle() -> dict[str, int]: + """Liest screen_off/suspend (Sekunden) aus der ExecStart-Zeile des + taninux-swayidle.service. swayidle hat keine Config-Datei — die Timeouts + stehen als `timeout ''`-Paare in den Args. 0 = aus.""" + res = {"screen_off": 0, "suspend": 0} + if not SWAYIDLE_UNIT.is_file(): + return res + text = SWAYIDLE_UNIT.read_text(errors="ignore") + m = re.search(r"^\s*ExecStart\s*=\s*(.+)$", text, re.MULTILINE) + if not m: + return res + exec_line = m.group(1) + # Jedes timeout-Paar: timeout '' — der cmd entscheidet das Ziel. + for tm in re.finditer(r"timeout\s+(\d+)\s+(['\"])(.*?)\2", exec_line): + sec = int(tm.group(1)) + cmd = tm.group(3).lower() + if "power-off-monitors" in cmd or "dpms" in cmd: + res["screen_off"] = sec + elif "suspend" in cmd: + res["suspend"] = sec + return res + + +def _write_idle_swayidle(screen_off: int, suspend: int) -> Path: + """Schreibt die TANINUX-verwaltete swayidle-User-Unit. Nur Listener mit + Wert >0 kommen in die ExecStart-Zeile. Der Bildschirm wird per niri-Action + (DPMS) ab-/angeschaltet; die niri-Config bleibt unberührt.""" + swayidle = shutil.which("swayidle") or "/usr/bin/swayidle" + parts = [swayidle, "-w"] + if screen_off > 0: + parts += [ + "timeout", str(screen_off), + "'niri msg action power-off-monitors'", + "resume", "'niri msg action power-on-monitors'", + ] + if suspend > 0: + parts += ["timeout", str(suspend), "'systemctl suspend'"] + exec_start = " ".join(parts) + + lines = [ + "[Unit]", + "Description=TANINUX-managed swayidle (idle timeouts)", + "Documentation=man:swayidle(1)", + "PartOf=graphical-session.target", + "", + "[Service]", + "Type=simple", + f"ExecStart={exec_start}", + "Restart=on-failure", + "", + "[Install]", + "WantedBy=default.target", + "", + ] + SWAYIDLE_UNIT.parent.mkdir(parents=True, exist_ok=True) + SWAYIDLE_UNIT.write_text("\n".join(lines)) + return SWAYIDLE_UNIT + + +def apply_swayidle() -> bool: + """daemon-reload + User-Service enablen/(re)starten. True wenn ausgeführt.""" + if not swayidle_installed(): + return False + try: + subprocess.run( + ["systemctl", "--user", "daemon-reload"], + check=False, timeout=8, + ) + subprocess.run( + ["systemctl", "--user", "enable", "--now", + "taninux-swayidle.service"], + check=False, timeout=8, + ) + subprocess.run( + ["systemctl", "--user", "restart", "taninux-swayidle.service"], + check=False, timeout=8, + ) + return True + except (FileNotFoundError, subprocess.TimeoutExpired): + return False + + +# -------------------------------------------------- Idle (niri-only) + +def idle_daemon_name() -> str: + """Idle-Daemon für niri — swayidle (spricht ext-idle-notify).""" + return "swayidle" + + +def idle_pkg() -> str: + """Paketname für pacman — identisch zum Daemon-Namen.""" + return idle_daemon_name() + + +def idle_daemon_installed() -> bool: + return swayidle_installed() + + +def read_idle_timeouts() -> dict[str, int]: + """screen_off/suspend (Sekunden) aus der swayidle-Unit lesen. 0 = aus.""" + return _read_idle_swayidle() + + +def write_idle_config(screen_off: int, suspend: int) -> Path: + """swayidle-Unit schreiben. 0 = Listener weglassen. Gibt den Pfad zurück.""" + return _write_idle_swayidle(screen_off, suspend) + + +def apply_idle() -> bool: + """Idle-Daemon anwenden (best-effort, unprivilegiert).""" + return apply_swayidle() diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/printers.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/printers.py new file mode 100644 index 0000000..15d11e4 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/printers.py @@ -0,0 +1,125 @@ +"""Printers: CUPS-Druckerverwaltung (read + argv für Schreib-Aktionen). + +Read kommt aus den CUPS-Clienttools (lpstat). Schreib-Aktionen werden NICHT +hier ausgeführt — wir liefern nur das argv, das der GUI-Aufrufer (privilegiert +via Helper oder unprivilegiert) selbst startet. So bleibt das Modul UI-frei. + +Drucker HINZUFÜGEN (Treiberauswahl) ist bewusst nicht enthalten — das gehört +in ein dediziertes System-Tool. +""" +from __future__ import annotations + +import re +import shutil +import subprocess +from dataclasses import dataclass + + +def available() -> bool: + """True, wenn die CUPS-Clienttools (lpstat) vorhanden sind.""" + return shutil.which("lpstat") is not None + + +@dataclass +class Printer: + name: str + info: str # Location o.ä., leer wenn unbekannt + state: str # "idle" / "printing" / "stopped" + enabled: bool + is_default: bool + + +def _run(argv: list[str]) -> str: + """Führt ein read-only Kommando aus, gibt stdout zurück (leer bei Fehler).""" + try: + out = subprocess.run( + argv, capture_output=True, text=True, timeout=10, check=False + ) + except (OSError, subprocess.SubprocessError): + return "" + return out.stdout + + +def _info_map() -> dict[str, str]: + """Location pro Drucker aus `lpstat -l -p` (optional, best effort).""" + info: dict[str, str] = {} + name: str | None = None + for line in _run(["lpstat", "-l", "-p"]).splitlines(): + m = re.match(r"printer (\S+) ", line) + if m: + name = m.group(1) + continue + stripped = line.strip() + if name and stripped.lower().startswith("location:"): + info[name] = stripped.split(":", 1)[1].strip() + return info + + +def printers() -> list[Printer]: + """Parst `lpstat -p -d`. + + Zeilen sehen so aus: + printer Brother is idle. enabled since … + printer HP_LaserJet disabled since … - (stopped, …) + system default destination: Brother + """ + out = _run(["lpstat", "-p", "-d"]) + default = "" + result: list[Printer] = [] + + for line in out.splitlines(): + m = re.match(r"system default destination:\s*(\S+)", line) + if m: + default = m.group(1) + continue + m = re.match(r"printer (\S+) (?:is )?(\S+)", line) + if not m: + continue + name = m.group(1) + word = m.group(2).rstrip(".").lower() + if word == "disabled": + state, enabled = "stopped", False + elif word == "printing": + state, enabled = "printing", True + else: + state, enabled = "idle", True + result.append(Printer(name=name, info="", state=state, + enabled=enabled, is_default=False)) + + locations = _info_map() + for p in result: + p.info = locations.get(p.name, "") + p.is_default = (p.name == default) + return result + + +def queue() -> list[str]: + """Job-Zeilen aus `lpstat -o`. Leere Liste, wenn nichts in der Queue.""" + return [ln for ln in _run(["lpstat", "-o"]).splitlines() if ln.strip()] + + +# ----------------------------------------------------------- argv (Aufrufer führt aus) + +def set_default_argv(name: str) -> list[str]: + """Default-Drucker setzen (root → via Helper).""" + return ["lpadmin", "-d", name] + + +def enable_argv(name: str) -> list[str]: + """Drucker freigeben (root → via Helper).""" + return ["cupsenable", name] + + +def disable_argv(name: str) -> list[str]: + """Drucker anhalten (root → via Helper).""" + return ["cupsdisable", name] + + +def remove_argv(name: str) -> list[str]: + """Drucker entfernen (root → via Helper).""" + return ["lpadmin", "-x", name] + + +def cancel_all_argv() -> list[str]: + """Gesamte Druck-Queue leeren (unprivilegiert).""" + return ["cancel", "-a"] diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/runner.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/runner.py new file mode 100644 index 0000000..e67cfeb --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/runner.py @@ -0,0 +1,51 @@ +"""Subprocess-Runner: suspendet die TUI und führt Befehle interaktiv aus. + +So kann pacman/paru/sudo seinen eigenen Passwort-Prompt + Progress +direkt im Terminal anzeigen, ohne dass wir parsen müssen. +""" +from __future__ import annotations + +import subprocess +from dataclasses import dataclass + +from textual.app import App + + +@dataclass +class Command: + label: str + argv: list[str] + + +def run_interactive(app: App, commands: list[Command]) -> list[int]: + """Suspendet die App, führt jeden Befehl nacheinander aus, kehrt zurück. + + Returns: liste der exit codes (in Reihenfolge der Befehle). + """ + codes: list[int] = [] + with app.suspend(): + print("\n" + "═" * 64) + print(f" TANINUX — {len(commands)} befehl(e)") + print("═" * 64) + for cmd in commands: + print() + print(f"┌── {cmd.label} " + "─" * max(0, 56 - len(cmd.label))) + print(f"│ $ {' '.join(cmd.argv)}") + print("└" + "─" * 63) + try: + rc = subprocess.run(cmd.argv, check=False).returncode + except KeyboardInterrupt: + print("\n[abgebrochen]") + codes.append(130) + break + except FileNotFoundError as e: + print(f"[fehler: {e}]") + rc = 127 + codes.append(rc) + print() + print("═" * 64) + try: + input(" ENTER zum zurück zu TANINUX … ") + except (EOFError, KeyboardInterrupt): + pass + return codes diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/search.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/search.py new file mode 100644 index 0000000..d53f1da --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/search.py @@ -0,0 +1,152 @@ +"""Paket-Suche über Repo + AUR (paru) und Flatpak. UI-frei. + +`paru -Ss` deckt offizielle Repos UND AUR in einem Aufruf ab; Flatpak wird +separat durchsucht. Ergebnisse werden zu einem einheitlichen `Result` normiert. + +Install/Remove laufen als User: paru eskaliert den pacman-Teil selbst über +`--sudo pkexec` (grafischer Polkit-Dialog, kein TTY nötig); Flatpak braucht +ohnehin kein root. +""" +from __future__ import annotations + +import re +import shutil +import subprocess +from dataclasses import dataclass, field + +MAX_RESULTS = 60 + +_HEAD = re.compile(r"^(?P[\w.-]+)/(?P\S+)\s+(?P\S+)(?P.*)$") + + +@dataclass +class Result: + source: str # 'repo' | 'aur' | 'flatpak' + name: str # Paketname (Repo/AUR) bzw. Flatpak-App-ID + version: str + description: str = "" + installed: bool = False + repo: str = "" # Repo-Name bzw. Flatpak-Remote + extra: dict[str, str] = field(default_factory=dict) + + +def _helper() -> str | None: + for h in ("paru", "yay"): + if shutil.which(h): + return h + return None + + +def _search_pacman(query: str, include_aur: bool) -> list[Result]: + # AUR nur via paru/yay; ohne AUR plain pacman (kein paru/base-devel nötig). + helper = _helper() if include_aur else None + argv = [helper, "-Ss", query] if helper else ["pacman", "-Ss", query] + try: + r = subprocess.run( + argv, capture_output=True, text=True, check=False, timeout=45, + ) + except (FileNotFoundError, subprocess.TimeoutExpired): + return [] + if r.returncode not in (0, 1): + return [] + + out: list[Result] = [] + cur: Result | None = None + for line in r.stdout.splitlines(): + if line[:1].isspace(): + if cur is not None and not cur.description: + cur.description = line.strip() + continue + m = _HEAD.match(line) + if not m: + continue + repo = m.group("repo") + cur = Result( + source="aur" if repo == "aur" else "repo", + name=m.group("name"), + version=m.group("ver"), + installed="[installed" in m.group("rest").lower(), + repo=repo, + ) + out.append(cur) + return out + + +def _flatpak_installed_ids() -> set[str]: + try: + r = subprocess.run( + ["flatpak", "list", "--app", "--columns=application"], + capture_output=True, text=True, check=False, timeout=10, + ) + except (FileNotFoundError, subprocess.TimeoutExpired): + return set() + return {ln.strip() for ln in r.stdout.splitlines() if ln.strip()} + + +def _search_flatpak(query: str) -> list[Result]: + if not shutil.which("flatpak"): + return [] + try: + r = subprocess.run( + ["flatpak", "search", + "--columns=application,version,name,description,remotes", query], + capture_output=True, text=True, check=False, timeout=30, + ) + except (FileNotFoundError, subprocess.TimeoutExpired): + return [] + if r.returncode != 0: + return [] + + installed = _flatpak_installed_ids() + out: list[Result] = [] + for line in r.stdout.splitlines(): + s = line.strip() + if not s or s.lower().startswith("application"): + continue + parts = s.split("\t") + if len(parts) < 1: + continue + app = parts[0] + ver = parts[1] if len(parts) > 1 and parts[1] != "" else "?" + name = parts[2] if len(parts) > 2 else app + desc = parts[3] if len(parts) > 3 else "" + remote = parts[4].split(",")[0] if len(parts) > 4 and parts[4] else "flathub" + out.append(Result( + source="flatpak", + name=app, + version=ver, + description=desc if desc and desc != name else "", + installed=app in installed, + repo=remote, + extra={"title": name}, + )) + return out + + +def search(query: str, include_aur: bool | None = None) -> list[Result]: + """Repo (+AUR falls Opt-in) und Flatpak durchsuchen, zusammengeführt.""" + if include_aur is None: + from taninux.core import panel + include_aur = panel.include_aur() + query = query.strip() + if len(query) < 2: + return [] + results = _search_pacman(query, include_aur) + _search_flatpak(query) + return results[:MAX_RESULTS] + + +def install_argv(item: Result) -> tuple[list[str], bool]: + """(argv, privileged). Repo läuft über den Helper (pacman), AUR über paru.""" + if item.source == "flatpak": + return ["flatpak", "install", "--assumeyes", item.repo, item.name], False + if item.source == "aur": + helper = _helper() or "paru" + return [helper, "--sudo", "pkexec", "-S", "--noconfirm", item.name], False + return ["pacman", "-S", "--needed", "--noconfirm", item.name], True + + +def remove_argv(item: Result) -> tuple[list[str], bool]: + """(argv, privileged). Entfernen baut nie — Repo+AUR über den Helper.""" + if item.source == "flatpak": + return ["flatpak", "uninstall", "--assumeyes", item.name], False + return ["pacman", "-Rns", "--noconfirm", item.name], True diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/shares.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/shares.py new file mode 100644 index 0000000..f59477f --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/shares.py @@ -0,0 +1,153 @@ +"""Network Shares — Remote-Bookmarks für den Dateimanager (Nautilus). UI-frei. + +Verwaltet Remote-Verbindungen (SFTP/FTP/SMB/WebDAV/NFS) als GTK-3-Bookmarks +in `~/.config/gtk-3.0/bookmarks` plus `gio mount` zum Verbinden. Alles +User-Level: GVfs läuft im Session-Bus, kein root. Lokale `file://`-Bookmarks +werden NIE angefasst — nur Remote-Zeilen (scheme in REMOTE_SCHEMES). + +SMB/NFS brauchen die GVfs-Backends `gvfs-smb`/`gvfs-nfs` (eigene Pakete). +""" +from __future__ import annotations + +import os +import subprocess +from dataclasses import dataclass + +REMOTE_SCHEMES = {"sftp", "ftp", "smb", "dav", "davs", "nfs"} + + +def _config_home() -> str: + return os.environ.get("XDG_CONFIG_HOME") or os.path.expanduser("~/.config") + + +def bookmarks_path() -> str: + return os.path.join(_config_home(), "gtk-3.0", "bookmarks") + + +@dataclass +class Bookmark: + uri: str + label: str + scheme: str + + +def _scheme(uri: str) -> str: + return uri.split("://", 1)[0].lower() if "://" in uri else "" + + +def _read_lines() -> list[str]: + try: + with open(bookmarks_path(), encoding="utf-8") as fh: + return [ln.rstrip("\n") for ln in fh] + except OSError: + return [] + + +def list_remote() -> list[Bookmark]: + """Alle Remote-Bookmarks (scheme in REMOTE_SCHEMES). file:// bleibt außen vor.""" + out: list[Bookmark] = [] + for line in _read_lines(): + line = line.strip() + if not line: + continue + parts = line.split(maxsplit=1) + uri = parts[0] + label = parts[1] if len(parts) > 1 else uri + sch = _scheme(uri) + if sch in REMOTE_SCHEMES: + out.append(Bookmark(uri=uri, label=label, scheme=sch)) + return out + + +def build_uri(scheme: str, host: str, path: str = "", + user: str = "", port: str = "") -> str: + """Baut eine GVfs-URI, z. B. `sftp://user@host:2222/srv`. + + user/port optional. path wird mit führendem „/" normalisiert. + SMB-Konvention: `smb://host/share` (share = path ohne führenden Slash-Zwang, + wird aber ebenfalls über den Slash drangehängt). + """ + scheme = scheme.strip().lower() + host = host.strip() + user = user.strip() + port = port.strip() + path = path.strip() + + authority = host + if user: + authority = f"{user}@{authority}" + if port: + authority = f"{authority}:{port}" + + uri = f"{scheme}://{authority}" + if path: + if not path.startswith("/"): + path = "/" + path + uri += path + return uri + + +def add_bookmark(uri: str, label: str) -> None: + """Remote-Bookmark hinzufügen (idempotent). Alle anderen Zeilen — inkl. + lokaler file://-Bookmarks — bleiben unverändert erhalten.""" + uri = uri.strip() + if not uri: + return + lines = _read_lines() + # Vorhandenen Eintrag mit derselben URI entfernen (Duplikate vermeiden). + kept = [ln for ln in lines if ln.strip().split(maxsplit=1)[:1] != [uri]] + entry = uri if not label.strip() else f"{uri} {label.strip()}" + kept.append(entry) + _write_lines(kept) + + +def remove_bookmark(uri: str) -> None: + """Entfernt genau die Bookmark-Zeile(n) mit dieser URI. Alles andere bleibt.""" + uri = uri.strip() + lines = _read_lines() + kept = [ln for ln in lines if ln.strip().split(maxsplit=1)[:1] != [uri]] + _write_lines(kept) + + +def _write_lines(lines: list[str]) -> None: + path = bookmarks_path() + os.makedirs(os.path.dirname(path), exist_ok=True) + # Leere Zeilen am Ende vermeiden, genau ein abschließendes Newline. + body = "\n".join(ln for ln in lines if ln.strip() != "") + with open(path, "w", encoding="utf-8") as fh: + fh.write(body + ("\n" if body else "")) + + +def mount_argv(uri: str) -> list[str]: + return ["gio", "mount", uri] + + +def unmount_argv(uri: str) -> list[str]: + return ["gio", "mount", "-u", uri] + + +def _pkg_installed(name: str) -> bool: + """True, wenn das Pacman-Paket installiert ist (rc 0). GVfs-Backends sind + Libs ohne eigenes Binary, deshalb Paketabfrage statt shutil.which.""" + try: + r = subprocess.run(["pacman", "-Q", name], capture_output=True, + text=True, timeout=6, check=False) + return r.returncode == 0 + except (OSError, subprocess.SubprocessError): + return False + + +def smb_available() -> bool: + return _pkg_installed("gvfs-smb") + + +def nfs_available() -> bool: + return _pkg_installed("gvfs-nfs") + + +def install_smb_argv() -> list[str]: + return ["pacman", "-S", "--needed", "--noconfirm", "gvfs-smb"] + + +def install_nfs_argv() -> list[str]: + return ["pacman", "-S", "--needed", "--noconfirm", "gvfs-nfs"] diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/snapshot.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/snapshot.py new file mode 100644 index 0000000..c1d4f33 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/snapshot.py @@ -0,0 +1,137 @@ +"""Snapshot/Drift: speichere den Paket-Stand und vergleiche über Zeit. + +Snapshots leben unter ~/.local/share/taninux/snapshots/.json mit: + - timestamp + - explicit: pacman -Qenq (explicit installiert, native repos) + - aur: pacman -Qmq (foreign — meist AUR) + - deps: pacman -Qdq (als dependency installiert) +""" +from __future__ import annotations + +import json +import subprocess +from dataclasses import dataclass, field +from datetime import datetime +from pathlib import Path + +from taninux.core.paths import snapshots_dir + + +@dataclass +class Snapshot: + id: str # e.g. '20260526T195430' + path: Path + timestamp: str + label: str + explicit: set[str] = field(default_factory=set) + aur: set[str] = field(default_factory=set) + deps: set[str] = field(default_factory=set) + + @property + def all_pkgs(self) -> set[str]: + return self.explicit | self.aur | self.deps + + +def _pacman_qq(args: list[str]) -> set[str]: + try: + r = subprocess.run( + ["pacman", *args], + capture_output=True, text=True, check=False, timeout=10, + ) + except (FileNotFoundError, subprocess.TimeoutExpired): + return set() + return {l.strip() for l in r.stdout.splitlines() if l.strip()} + + +def take_snapshot(label: str | None = None) -> Snapshot: + ts = datetime.now().strftime("%Y%m%dT%H%M%S") + iso = datetime.now().isoformat(timespec="seconds") + snapshot_id = ts + path = snapshots_dir() / f"{snapshot_id}.json" + explicit = _pacman_qq(["-Qenq"]) + aur = _pacman_qq(["-Qmq"]) + deps = _pacman_qq(["-Qdq"]) + payload = { + "id": snapshot_id, + "label": label or "manual", + "timestamp": iso, + "explicit": sorted(explicit), + "aur": sorted(aur), + "deps": sorted(deps), + } + path.write_text(json.dumps(payload, indent=2)) + return Snapshot( + id=snapshot_id, + path=path, + timestamp=iso, + label=payload["label"], + explicit=explicit, + aur=aur, + deps=deps, + ) + + +def list_snapshots() -> list[Snapshot]: + out: list[Snapshot] = [] + for p in sorted(snapshots_dir().glob("*.json")): + try: + data = json.loads(p.read_text()) + except (OSError, json.JSONDecodeError): + continue + out.append(Snapshot( + id=data.get("id", p.stem), + path=p, + timestamp=data.get("timestamp", ""), + label=data.get("label", ""), + explicit=set(data.get("explicit", [])), + aur=set(data.get("aur", [])), + deps=set(data.get("deps", [])), + )) + return out + + +def load(snapshot_id: str) -> Snapshot | None: + for s in list_snapshots(): + if s.id == snapshot_id: + return s + return None + + +@dataclass +class Diff: + added_explicit: list[str] + removed_explicit: list[str] + added_aur: list[str] + removed_aur: list[str] + added_deps: list[str] + removed_deps: list[str] + + +def current_snapshot() -> Snapshot: + return Snapshot( + id="current", + path=Path("/dev/null"), + timestamp=datetime.now().isoformat(timespec="seconds"), + label="now", + explicit=_pacman_qq(["-Qenq"]), + aur=_pacman_qq(["-Qmq"]), + deps=_pacman_qq(["-Qdq"]), + ) + + +def diff(old: Snapshot, new: Snapshot) -> Diff: + def sorted_list(s: set[str]) -> list[str]: + return sorted(s) + return Diff( + added_explicit=sorted_list(new.explicit - old.explicit), + removed_explicit=sorted_list(old.explicit - new.explicit), + added_aur=sorted_list(new.aur - old.aur), + removed_aur=sorted_list(old.aur - new.aur), + added_deps=sorted_list(new.deps - old.deps), + removed_deps=sorted_list(old.deps - new.deps), + ) + + +def latest() -> Snapshot | None: + snaps = list_snapshots() + return snaps[-1] if snaps else None diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/state.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/state.py new file mode 100644 index 0000000..88b4258 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/state.py @@ -0,0 +1,69 @@ +"""Persistenter User-State: Whitelist (immer behalten) + Ignored (nicht mehr anzeigen).""" +from __future__ import annotations + +import json +from dataclasses import dataclass, field +from pathlib import Path + +from taninux.core.paths import state_file + + +@dataclass +class State: + whitelist: set[Path] = field(default_factory=set) + ignored: set[Path] = field(default_factory=set) + + # ---- load / save ---- + + @classmethod + def load(cls) -> "State": + f = state_file() + if not f.exists(): + return cls() + try: + data = json.loads(f.read_text()) + except (json.JSONDecodeError, OSError): + return cls() + return cls( + whitelist={Path(p) for p in data.get("whitelist", [])}, + ignored={Path(p) for p in data.get("ignored", [])}, + ) + + def save(self) -> None: + f = state_file() + payload = { + "version": 1, + "whitelist": sorted(str(p) for p in self.whitelist), + "ignored": sorted(str(p) for p in self.ignored), + } + f.write_text(json.dumps(payload, indent=2)) + + # ---- queries ---- + + def is_whitelisted(self, p: Path) -> bool: + return p in self.whitelist + + def is_ignored(self, p: Path) -> bool: + return p in self.ignored + + # ---- mutations (auto-save) ---- + + def whitelist_add(self, p: Path) -> None: + self.whitelist.add(p) + self.ignored.discard(p) + self.save() + + def whitelist_remove(self, p: Path) -> None: + if p in self.whitelist: + self.whitelist.discard(p) + self.save() + + def ignore_add(self, p: Path) -> None: + self.ignored.add(p) + self.whitelist.discard(p) + self.save() + + def ignore_remove(self, p: Path) -> None: + if p in self.ignored: + self.ignored.discard(p) + self.save() diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/storage.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/storage.py new file mode 100644 index 0000000..de01e52 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/storage.py @@ -0,0 +1,73 @@ +"""Laufwerke und Belegung. UI-frei. Lesen via lsblk; mount/eject via udisksctl +(user-level, polkit-backed — kein root-Helper nötig).""" +from __future__ import annotations + +import json +import subprocess +from dataclasses import dataclass + + +@dataclass +class Volume: + dev: str # /dev/sdX1 + size: int # bytes + fstype: str + label: str + mountpoint: str + removable: bool + used: int # bytes (0 wenn nicht gemountet) + use_pct: int # 0..100 + + +def _lsblk() -> list[dict]: + try: + r = subprocess.run( + ["lsblk", "-J", "-b", "-o", + "NAME,PATH,SIZE,TYPE,MOUNTPOINT,FSTYPE,LABEL,RM,FSUSED,FSUSE%"], + capture_output=True, text=True, check=False, timeout=8, + ) + return json.loads(r.stdout).get("blockdevices", []) + except (FileNotFoundError, subprocess.TimeoutExpired, json.JSONDecodeError): + return [] + + +def _walk(node: dict, removable: bool, out: list[Volume]) -> None: + rm = removable or node.get("rm", False) + if (node.get("fstype") and node.get("fstype") != "swap" + and node.get("type") in ("part", "crypt", "lvm", "disk")): + pct = node.get("fsuse%") or "" + try: + use_pct = int(str(pct).rstrip("%")) if pct else 0 + except ValueError: + use_pct = 0 + out.append(Volume( + dev=node.get("path", node.get("name", "")), + size=int(node.get("size") or 0), + fstype=node.get("fstype", ""), + label=node.get("label") or "", + mountpoint=node.get("mountpoint") or "", + removable=bool(rm), + used=int(node.get("fsused") or 0), + use_pct=use_pct, + )) + for child in node.get("children", []): + _walk(child, rm, out) + + +def volumes() -> list[Volume]: + out: list[Volume] = [] + for dev in _lsblk(): + _walk(dev, False, out) + return out + + +def mount_argv(dev: str) -> list[str]: + return ["udisksctl", "mount", "-b", dev] + + +def unmount_argv(dev: str) -> list[str]: + return ["udisksctl", "unmount", "-b", dev] + + +def eject_argv(dev: str) -> list[str]: + return ["udisksctl", "power-off", "-b", dev] diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/streaming.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/streaming.py new file mode 100644 index 0000000..b8f8fe7 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/streaming.py @@ -0,0 +1,169 @@ +"""Streaming-Runner: führt Subprozesse aus, streamt stdout in einen RichLog +innerhalb eines Modals. Kein TUI-suspend — der user sieht alles live.""" +from __future__ import annotations + +import asyncio +import shlex +from dataclasses import dataclass + +from textual.app import ComposeResult +from textual.binding import Binding +from textual.containers import Vertical +from textual.screen import ModalScreen +from textual.widgets import RichLog, Static + +from taninux.core import theme as T + + +@dataclass +class StreamCommand: + label: str + argv: list[str] + + +class RunnerScreen(ModalScreen[list[int]]): + """Modal das Commands sequenziell ausführt + stdout/stderr streamt. + + Returns list[int] — ein exit-code pro Command in Reihenfolge.""" + + BINDINGS = [ + Binding("ctrl+c", "cancel", "abbruch"), + Binding("escape", "close", "schliessen", show=True), + Binding("q", "close", "quit", show=False), + ] + + CSS = f""" + RunnerScreen {{ + align: center middle; + background: rgba(0, 0, 0, 0.85); + }} + RunnerScreen #runner-dialog {{ + width: 90%; + height: 85%; + padding: 1 2; + background: {T.YORU}; + border: round {T.BASE3}; + }} + RunnerScreen #runner-title {{ + height: 1; + background: {T.YORU}; + color: {T.UME}; + }} + RunnerScreen #runner-log {{ + height: 1fr; + background: {T.YORU}; + color: {T.FG_ALT}; + border: none; + scrollbar-color: {T.BASE2} {T.YORU}; + scrollbar-background: {T.YORU}; + scrollbar-corner-color: {T.YORU}; + scrollbar-size-vertical: 1; + }} + RunnerScreen #runner-hint {{ + height: 1; + background: {T.YORU}; + color: {T.BASE4}; + margin-top: 1; + }} + """ + + def __init__(self, commands: list[StreamCommand]) -> None: + super().__init__() + self.commands = commands + self.codes: list[int] = [] + self.done = False + self._current_proc: asyncio.subprocess.Process | None = None + self._cancelled = False + + def compose(self) -> ComposeResult: + with Vertical(id="runner-dialog"): + yield Static("", id="runner-title", markup=True) + yield RichLog(id="runner-log", highlight=False, markup=True, wrap=True) + yield Static("", id="runner-hint", markup=True) + + def on_mount(self) -> None: + self._set_title(f"running {len(self.commands)} command(s)…") + self._set_hint(f"[{T.BASE5}]ctrl+c[/] [{T.BASE4}]abbruch[/]") + self.run_worker(self._run_all(), exclusive=True) + + def _set_title(self, msg: str) -> None: + self.query_one("#runner-title", Static).update(f"[{T.UME}]▲ {msg}[/]") + + def _set_hint(self, msg: str) -> None: + self.query_one("#runner-hint", Static).update(msg) + + def _log(self, line: str) -> None: + self.query_one("#runner-log", RichLog).write(line) + + async def _run_all(self) -> None: + log = self.query_one("#runner-log", RichLog) + for i, cmd in enumerate(self.commands, 1): + if self._cancelled: + self._log(f"[{T.RED}]abgebrochen[/]") + break + log.write("") + log.write(f"[{T.SHINKAI}]┌── [{i}/{len(self.commands)}] {cmd.label}[/]") + log.write(f"[{T.BASE5}]│ $ {shlex.join(cmd.argv)}[/]") + log.write(f"[{T.BASE3}]└────────────────[/]") + code = await self._run_one(cmd) + self.codes.append(code) + color = T.TAKE if code == 0 else T.RED + log.write(f"[{color}]→ exit {code}[/]") + + self.done = True + self._current_proc = None + ok = sum(1 for c in self.codes if c == 0) + if self._cancelled: + self._set_title(f"abgebrochen ({ok}/{len(self.codes)} ok)") + elif ok == len(self.codes): + self._set_title(f"fertig — {ok}/{len(self.codes)} ok") + else: + self._set_title(f"fertig mit fehlern — {ok}/{len(self.codes)} ok") + self._set_hint( + f"[{T.BASE5}]ESC[/] [{T.BASE4}]schliessen + zurück[/]" + ) + + async def _run_one(self, cmd: StreamCommand) -> int: + try: + self._current_proc = await asyncio.create_subprocess_exec( + *cmd.argv, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.STDOUT, + stdin=asyncio.subprocess.DEVNULL, + ) + except FileNotFoundError as e: + self._log(f"[{T.RED}]fehler: {e}[/]") + return 127 + except Exception as e: # noqa: BLE001 + self._log(f"[{T.RED}]start fehlgeschlagen: {e}[/]") + return 1 + + assert self._current_proc.stdout is not None + try: + async for raw in self._current_proc.stdout: + line = raw.decode("utf-8", errors="replace").rstrip("\n").rstrip("\r") + if line: + # Escape rich markup chars so program output isn't parsed + self._log(line.replace("[", "\\[")) + except asyncio.CancelledError: + self._current_proc.terminate() + raise + + return await self._current_proc.wait() + + def action_cancel(self) -> None: + if self.done: + self.dismiss(self.codes) + return + self._cancelled = True + if self._current_proc is not None: + try: + self._current_proc.terminate() + except ProcessLookupError: + pass + self._set_title("abbruch läuft…") + + def action_close(self) -> None: + if self.done: + self.dismiss(self.codes) + # Wenn noch running: ignore (use ctrl+c) diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/sudo.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/sudo.py new file mode 100644 index 0000000..7ccd013 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/sudo.py @@ -0,0 +1,45 @@ +"""Sudo-Helper: Password-Cache via `sudo -v`.""" +from __future__ import annotations + +import subprocess + + +def is_cached() -> bool: + """Prüft ob sudo-credentials noch im 5-min cache stehen.""" + try: + r = subprocess.run( + ["sudo", "-n", "-v"], + capture_output=True, check=False, timeout=5, + ) + return r.returncode == 0 + except (FileNotFoundError, subprocess.TimeoutExpired): + return False + + +def cache(password: str) -> bool: + """Validiert Passwort + cached sudo-Credentials. + + Wir nutzen `sudo -S -v -p ''` (lies pw von stdin, nur cache update, + leerer prompt). Returns True wenn akzeptiert.""" + try: + r = subprocess.run( + ["sudo", "-S", "-v", "-p", ""], + input=password + "\n", + capture_output=True, text=True, check=False, timeout=10, + ) + return r.returncode == 0 + except (FileNotFoundError, subprocess.TimeoutExpired): + return False + + +def keep_alive() -> None: + """Verlängert den sudo cache nochmal, falls noch gültig. + + Praktisch in laufenden Updates die mehrere Minuten dauern können.""" + try: + subprocess.run( + ["sudo", "-n", "-v"], + capture_output=True, check=False, timeout=5, + ) + except (FileNotFoundError, subprocess.TimeoutExpired): + pass diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/sudo_modal.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/sudo_modal.py new file mode 100644 index 0000000..dad65f6 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/sudo_modal.py @@ -0,0 +1,78 @@ +"""Sudo-Passwort-Modal — fragt das Passwort einmal ab und gibt es zurück.""" +from __future__ import annotations + +from textual.app import ComposeResult +from textual.binding import Binding +from textual.containers import Vertical +from textual.screen import ModalScreen +from textual.widgets import Input, Static + +from taninux.core import theme as T + + +class SudoModal(ModalScreen[str | None]): + """Fragt sudo-passwort. Liefert das Passwort oder None bei abbruch. + + Eingabe ist gemaskt. Das Passwort wird NICHT geloggt oder gespeichert — + nur an `sudo -S -v` weitergereicht zum cachen.""" + + BINDINGS = [ + Binding("escape", "cancel", "abbruch"), + ] + + CSS = f""" + SudoModal {{ + align: center middle; + background: rgba(0, 0, 0, 0.6); + }} + SudoModal #sudo-dialog {{ + width: auto; + max-width: 70; + height: auto; + padding: 1 3; + background: {T.YORU}; + border: round {T.BASE3}; + }} + SudoModal Static {{ + background: {T.YORU}; + margin-bottom: 1; + }} + SudoModal Input {{ + background: {T.BASE1}; + color: {T.YUKI}; + border: round {T.BASE2}; + }} + SudoModal Input:focus {{ + border: round {T.UME}; + }} + SudoModal #sudo-hint {{ + margin-top: 1; + margin-bottom: 0; + color: {T.BASE4}; + }} + """ + + def compose(self) -> ComposeResult: + with Vertical(id="sudo-dialog"): + yield Static(f"[{T.UME}]sudo passwort[/]", markup=True) + yield Static( + f"[{T.BASE5}]wird via sudo -S -v gecached (5 min ttl), " + f"dann updates laufen im TUI.[/]", + markup=True, + ) + yield Input(password=True, id="sudo-pw") + yield Static( + f"[{T.BASE5}]ENTER[/] [{T.BASE4}]bestätigen[/] " + f"[{T.BASE5}]ESC[/] [{T.BASE4}]abbrechen[/]", + id="sudo-hint", + markup=True, + ) + + def on_mount(self) -> None: + self.query_one(Input).focus() + + def on_input_submitted(self, event: Input.Submitted) -> None: + self.dismiss(event.value) + + def action_cancel(self) -> None: + self.dismiss(None) diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/sysinfo.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/sysinfo.py new file mode 100644 index 0000000..fcdc457 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/sysinfo.py @@ -0,0 +1,274 @@ +"""Geteilte System-Info-Erkennung — genutzt von health, suggest, kernel.""" +from __future__ import annotations + +import os +import platform +import re +import shutil +import subprocess +from dataclasses import dataclass, field +from pathlib import Path + + +# ----------------------------------------------------------------- base + + +def hostname() -> str: + return platform.node() or "?" + + +def kernel_release() -> str: + return platform.release() + + +def distro_pretty() -> str: + try: + for line in Path("/etc/os-release").read_text().splitlines(): + if line.startswith("PRETTY_NAME="): + return line.split("=", 1)[1].strip().strip('"') + except OSError: + pass + return "?" + + +def uptime_seconds() -> float: + try: + with open("/proc/uptime") as f: + return float(f.read().split()[0]) + except OSError: + return 0.0 + + +def load_avg() -> tuple[float, float, float]: + try: + return os.getloadavg() + except OSError: + return (0.0, 0.0, 0.0) + + +def cpu_model() -> str: + try: + with open("/proc/cpuinfo") as f: + for line in f: + if line.startswith("model name"): + return line.split(":", 1)[1].strip() + except OSError: + pass + return "Unknown CPU" + + +def cpu_count() -> int: + return os.cpu_count() or 1 + + +def memory() -> dict[str, int]: + """MemTotal/MemAvailable/SwapTotal/SwapFree etc in Bytes.""" + out: dict[str, int] = {} + try: + with open("/proc/meminfo") as f: + for line in f: + parts = line.split() + if len(parts) >= 2: + key = parts[0].rstrip(":") + try: + out[key] = int(parts[1]) * 1024 + except ValueError: + continue + except OSError: + pass + return out + + +def boot_time_seconds() -> float | None: + """systemd-analyze boot duration.""" + try: + r = subprocess.run( + ["systemd-analyze", "time"], + capture_output=True, text=True, check=False, timeout=5, + ) + m = re.search(r"=\s*([\d.]+)s", r.stdout) + if m: + return float(m.group(1)) + except (FileNotFoundError, subprocess.TimeoutExpired): + return None + return None + + +# ----------------------------------------------------------------- hardware + + +@dataclass +class GPU: + vendor: str # "nvidia" / "amd" / "intel" / "unknown" + model: str + pci_slot: str + driver: str | None = None + + +def _detect_gpu_vendor(text: str) -> str: + t = text.lower() + if "nvidia" in t: + return "nvidia" + if "amd" in t or "radeon" in t or "ati " in t: + return "amd" + if "intel" in t: + return "intel" + return "unknown" + + +def gpus() -> list[GPU]: + """Parse `lspci -k` für VGA/3D/Display controller.""" + try: + r = subprocess.run( + ["lspci", "-k"], + capture_output=True, text=True, check=False, timeout=5, + ) + except (FileNotFoundError, subprocess.TimeoutExpired): + return [] + found: list[GPU] = [] + current: GPU | None = None + # lspci -k Format: jede Top-Level-Zeile beginnt mit "BB:DD.F" (bus:device.function) + # Sub-Zeilen ("Kernel driver…", "Kernel modules…") sind eingerückt. + for line in r.stdout.splitlines(): + # Neue Top-Level-Zeile? + if line and not line.startswith((" ", "\t")) and ":" in line.split()[0]: + # Reset: alte GPU bekommt keine driver-zeile mehr von hier + current = None + if any(k in line for k in ( + "VGA compatible controller", + "3D controller", + "Display controller", + )): + parts = line.split(":", 2) + if len(parts) >= 3: + pci = parts[0].strip() + model = parts[2].strip() + current = GPU( + vendor=_detect_gpu_vendor(model), + model=model, + pci_slot=pci, + driver=None, + ) + found.append(current) + elif current and line.lstrip().startswith("Kernel driver in use:"): + current.driver = line.split(":", 1)[1].strip() + return found + + +def audio_server() -> str: + """pipewire / pulse / ? (best-effort heuristic).""" + uid = os.getuid() + runtime = Path(f"/run/user/{uid}") + if (runtime / "pipewire-0").exists(): + return "pipewire" + if (runtime / "pulse").exists(): + return "pulse" + return "?" + + +@dataclass +class NetIface: + name: str + state: str # "UP" / "DOWN" / "UNKNOWN" + addrs: list[str] = field(default_factory=list) + + +def network_interfaces() -> list[NetIface]: + try: + r = subprocess.run( + ["ip", "-br", "addr"], + capture_output=True, text=True, check=False, timeout=5, + ) + except (FileNotFoundError, subprocess.TimeoutExpired): + return [] + out: list[NetIface] = [] + for line in r.stdout.splitlines(): + parts = line.split() + if len(parts) < 2: + continue + name = parts[0] + state = parts[1] + addrs = parts[2:] if len(parts) > 2 else [] + out.append(NetIface(name=name, state=state, addrs=addrs)) + return out + + +@dataclass +class Disk: + name: str + size_bytes: int + model: str + rotational: bool + + +def disks() -> list[Disk]: + try: + r = subprocess.run( + ["lsblk", "-d", "-b", "-n", "-o", "NAME,SIZE,ROTA,MODEL"], + capture_output=True, text=True, check=False, timeout=5, + ) + except (FileNotFoundError, subprocess.TimeoutExpired): + return [] + out: list[Disk] = [] + for line in r.stdout.splitlines(): + parts = line.split(None, 3) + if len(parts) < 3: + continue + try: + size = int(parts[1]) + except ValueError: + continue + out.append(Disk( + name=parts[0], + size_bytes=size, + rotational=parts[2] == "1", + model=parts[3].strip() if len(parts) > 3 else "", + )) + return out + + +# ----------------------------------------------------------------- services / logs + + +def systemctl_failed() -> list[str]: + try: + r = subprocess.run( + ["systemctl", "list-units", "--failed", "--no-legend", "--plain"], + capture_output=True, text=True, check=False, timeout=5, + ) + except (FileNotFoundError, subprocess.TimeoutExpired): + return [] + return [ + line.split()[0] + for line in r.stdout.splitlines() + if line.strip() + ] + + +def journal_errors_today() -> int: + try: + r = subprocess.run( + ["journalctl", "-p", "3", "--since", "today", "-q", "--no-pager"], + capture_output=True, text=True, check=False, timeout=10, + ) + return len([l for l in r.stdout.splitlines() if l.strip()]) + except (FileNotFoundError, subprocess.TimeoutExpired): + return -1 + + +def journal_errors_boot() -> int: + try: + r = subprocess.run( + ["journalctl", "-p", "3", "-b", "-q", "--no-pager"], + capture_output=True, text=True, check=False, timeout=10, + ) + return len([l for l in r.stdout.splitlines() if l.strip()]) + except (FileNotFoundError, subprocess.TimeoutExpired): + return -1 + + +# ----------------------------------------------------------------- binaries + + +def has_binary(name: str) -> bool: + return shutil.which(name) is not None diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/theme.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/theme.py new file mode 100644 index 0000000..e737bfc --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/theme.py @@ -0,0 +1,29 @@ +"""Mountain (Fuji) Palette. + +Reine Farben-Konstanten, keine textual-Dependency — damit auch CLI-Code +und Scanner ohne installed textual importieren können. +Das Textual-`Theme`-Objekt lebt in `taninux.app.MOUNTAIN_THEME`. +""" +from __future__ import annotations + +# ---- Mountain Fuji Palette (adaptiert von mountain-theme/Mountain) ---- + +# Backgrounds +YORU = "#0f0f0f" # bg +BASE0 = "#191919" # subtle alt bg +BASE1 = "#262626" # hover / selection / modeline +BASE2 = "#393939" # divider +BASE3 = "#4c4c4c" # muted border / comment + +# Foregrounds +YUKI = "#f0f0f0" # fg +FG_ALT = "#e7e7e7" +BASE4 = "#767676" # secondary text +BASE5 = "#a0a0a0" # tertiary text + +# Accents — bewusst reduziert auf 4 muted hues + ein gedämpftes Rot +UME = "#8f8aac" # violet — title / active / primary +TAKE = "#8aac8b" # green — safe / success +SHINKAI = "#8a98ac" # blue — section headers / secondary +CHIKYU = "#aca98a" # yellow — sizes / warning +RED = "#ac8a8c" # red — destructive (gedämpft) diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/trash.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/trash.py new file mode 100644 index 0000000..7df69d2 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/trash.py @@ -0,0 +1,128 @@ +"""Safe-Trash: Pfade wandern in einen Timestamp-Slot mit Manifest, statt rm. + +Restore und Purge sind vom User aus CLI/TUI ansteuerbar. +""" +from __future__ import annotations + +import json +import os +import shutil +from dataclasses import dataclass +from datetime import datetime +from pathlib import Path + +from taninux.core.paths import trash_dir + + +@dataclass +class TrashEntry: + id: str # slot directory name + slot: Path # absolute path of the slot + original: Path # where it came from + trashed_at: str # ISO timestamp + size_bytes: int + + +def _slot_for(name: str) -> Path: + ts = datetime.now().strftime("%Y%m%dT%H%M%S") + base = trash_dir() / f"{ts}-{name}" + slot = base + n = 1 + while slot.exists(): + slot = trash_dir().joinpath(f"{ts}-{name}-{n}") + n += 1 + return slot + + +def _dir_size(p: Path) -> int: + if p.is_symlink(): + return 0 + if p.is_file(): + try: + return p.stat(follow_symlinks=False).st_size + except OSError: + return 0 + if not p.is_dir(): + return 0 + total = 0 + for root, _dirs, files in os.walk(p, followlinks=False): + for f in files: + try: + total += os.stat(os.path.join(root, f), follow_symlinks=False).st_size + except OSError: + continue + return total + + +def move_to_trash(path: Path) -> TrashEntry: + """Atomically move `path` into a new trash slot. Returns the entry.""" + if not path.exists(): + raise FileNotFoundError(path) + + size = _dir_size(path) + slot = _slot_for(path.name) + slot.mkdir(parents=True) + + payload_dir = slot / path.name + shutil.move(str(path), str(payload_dir)) + + manifest = { + "original": str(path), + "trashed_at": datetime.now().isoformat(timespec="seconds"), + "size_bytes": size, + "payload": path.name, + } + (slot / "manifest.json").write_text(json.dumps(manifest, indent=2)) + + return TrashEntry( + id=slot.name, + slot=slot, + original=path, + trashed_at=manifest["trashed_at"], + size_bytes=size, + ) + + +def list_trash() -> list[TrashEntry]: + entries: list[TrashEntry] = [] + for slot in sorted(trash_dir().iterdir()): + if not slot.is_dir(): + continue + mf = slot / "manifest.json" + if not mf.exists(): + continue + try: + data = json.loads(mf.read_text()) + except (json.JSONDecodeError, OSError): + continue + entries.append(TrashEntry( + id=slot.name, + slot=slot, + original=Path(data["original"]), + trashed_at=data.get("trashed_at", ""), + size_bytes=int(data.get("size_bytes", 0)), + )) + return entries + + +def restore(slot_id: str) -> Path: + slot = trash_dir() / slot_id + if not slot.is_dir(): + raise FileNotFoundError(slot_id) + manifest = json.loads((slot / "manifest.json").read_text()) + original = Path(manifest["original"]) + payload = slot / manifest.get("payload", original.name) + if original.exists(): + raise FileExistsError(f"original path already exists: {original}") + original.parent.mkdir(parents=True, exist_ok=True) + shutil.move(str(payload), str(original)) + (slot / "manifest.json").unlink() + slot.rmdir() + return original + + +def purge(slot_id: str) -> None: + slot = trash_dir() / slot_id + if not slot.is_dir(): + raise FileNotFoundError(slot_id) + shutil.rmtree(slot) diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/wallpaper.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/wallpaper.py new file mode 100644 index 0000000..a69e781 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/core/wallpaper.py @@ -0,0 +1,135 @@ +"""Wallpaper-Verwaltung über waypaper (Backend swaybg). UI-frei. + +Liest die waypaper-Config (~/.config/waypaper/config.ini), liefert den +Hintergrundordner, das aktuelle Bild, erkannte Monitore und die flache Liste +der Bilddateien. Setzen läuft über die waypaper-CLI (argv für ProcessRunner). + +WICHTIG: Dieses Modul lädt NIEMALS Bilddaten — nur Pfade. Das Skalieren / +Rendern der Thumbnails passiert ausschließlich in der GUI (RAM-kontrolliert). +""" +from __future__ import annotations + +import configparser +import os +import shutil +from pathlib import Path + +_EXTS = {".jpg", ".jpeg", ".png", ".webp", ".bmp"} + + +def _config_path() -> Path: + base = os.environ.get("XDG_CONFIG_HOME") or os.path.expanduser("~/.config") + return Path(base) / "waypaper" / "config.ini" + + +def _expand(value: str) -> str: + """`~` und Umgebungs-Tilde expandieren, Whitespace strippen.""" + return os.path.expanduser(value.strip()) + + +def config() -> dict[str, str]: + """Liest [Settings] aus der waypaper-Config. Defensiv: leeres dict bei Fehler. + + `folder`/`wallpaper` werden bei der weiteren Auswertung expandiert; hier + bleiben die Rohwerte (mehrzeilige Werte wie `monitors`/`wallpaper` erhalten + ihre Zeilenumbrüche). + """ + path = _config_path() + if not path.exists(): + return {} + parser = configparser.ConfigParser() + try: + parser.read(path, encoding="utf-8") + except (configparser.Error, OSError): + return {} + if not parser.has_section("Settings"): + return {} + out: dict[str, str] = {} + for key in ("folder", "wallpaper", "backend", "monitors"): + if parser.has_option("Settings", key): + out[key] = parser.get("Settings", key) + return out + + +def wallpaper_dir() -> Path: + """Expandierter Hintergrundordner; Fallback ~/Pictures.""" + folder = config().get("folder", "").strip() + if folder: + return Path(_expand(folder)) + return Path(os.path.expanduser("~/Pictures")) + + +def set_folder(path: str) -> None: + """Hintergrundordner in waypapers config.ini setzen (andere Keys bleiben).""" + cfg_path = _config_path() + parser = configparser.ConfigParser() + try: + if cfg_path.exists(): + parser.read(cfg_path, encoding="utf-8") + except (configparser.Error, OSError): + pass + if not parser.has_section("Settings"): + parser.add_section("Settings") + parser.set("Settings", "folder", path) + try: + cfg_path.parent.mkdir(parents=True, exist_ok=True) + with cfg_path.open("w", encoding="utf-8") as f: + parser.write(f) + except OSError: + pass + + +def current() -> str: + """Aktuell gesetztes Wallpaper (expandiert) oder "" wenn keiner. + + waypaper kann pro Monitor ein Bild speichern (mehrzeiliger Wert); wir nehmen + die erste nicht-leere Zeile als „das" aktuelle Bild. + """ + raw = config().get("wallpaper", "") + for line in raw.splitlines(): + line = line.strip() + if line: + return _expand(line) + return "" + + +def monitors() -> list[str]: + """["All"] + erkannte Monitornamen aus dem (mehrzeiligen) monitors-Block.""" + out = ["All"] + raw = config().get("monitors", "") + for line in raw.splitlines(): + name = line.strip() + if name and name != "All" and name not in out: + out.append(name) + return out + + +def list_wallpapers() -> list[Path]: + """Bilddateien im wallpaper_dir (flach, case-insensitive), nach Name sortiert. + + Lädt KEINE Bilddaten — gibt nur Pfade zurück. + """ + base = wallpaper_dir() + try: + entries = [ + p for p in base.iterdir() + if p.is_file() and p.suffix.lower() in _EXTS + ] + except OSError: + return [] + return sorted(entries, key=lambda p: p.name.lower()) + + +def set_argv(path, monitor: str = "All") -> list[str]: + argv = ["waypaper", "--wallpaper", str(path)] + if monitor and monitor != "All": + argv += ["--monitor", monitor] + return argv + + +def random_argv() -> list[str]: + return ["waypaper", "--random"] + + +def available() -> bool: + return shutil.which("waypaper") is not None diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/flatpak/__init__.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/flatpak/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/flatpak/pillar.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/flatpak/pillar.py new file mode 100644 index 0000000..440793d --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/flatpak/pillar.py @@ -0,0 +1,13 @@ +from __future__ import annotations + +from taninux.flatpak.screen import FlatpakView +from taninux.pillars.base import Pillar + + +FLATPAK = Pillar( + key="f", + label="flatpak", + tagline="apps & runtimes", + icon="●", + factory=FlatpakView, +) diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/flatpak/screen.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/flatpak/screen.py new file mode 100644 index 0000000..547f3f3 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/flatpak/screen.py @@ -0,0 +1,341 @@ +"""Flatpak-Pillar: installierte Apps + Runtimes verwalten.""" +from __future__ import annotations + +import subprocess +from dataclasses import dataclass + +from textual import work +from textual.app import ComposeResult +from textual.binding import Binding +from textual.containers import Vertical +from textual.widgets import DataTable, Static, Tab, Tabs + +from taninux.core import theme as T +from taninux.core.confirm import ConfirmModal +from taninux.core.runner import Command, run_interactive +from taninux.maintain.scanners.pacman import _parse_size + + +@dataclass +class FlatpakItem: + ref: str # full ref like 'org.gnome.Calculator/x86_64/stable' + app_id: str # 'org.gnome.Calculator' + branch: str + arch: str + version: str + size_bytes: int + origin: str # remote name like 'flathub' + kind: str # 'app' or 'runtime' + + +def _flatpak_available() -> bool: + try: + subprocess.run(["flatpak", "--version"], + capture_output=True, check=False, timeout=5) + return True + except (FileNotFoundError, subprocess.TimeoutExpired): + return False + + +def _list_kind(kind: str) -> list[FlatpakItem]: + if not _flatpak_available(): + return [] + flag = "--app" if kind == "app" else "--runtime" + try: + r = subprocess.run( + ["flatpak", "list", flag, + "--columns=application,branch,arch,version,size,origin"], + capture_output=True, text=True, check=False, timeout=10, + ) + except (FileNotFoundError, subprocess.TimeoutExpired): + return [] + items: list[FlatpakItem] = [] + for line in r.stdout.splitlines(): + s = line.strip() + if not s or s.startswith("Application"): + continue + parts = s.split("\t") + if len(parts) < 3: + continue + app_id = parts[0] + branch = parts[1] + arch = parts[2] + version = parts[3] if len(parts) > 3 else "" + size_str = parts[4] if len(parts) > 4 else "" + origin = parts[5] if len(parts) > 5 else "" + items.append(FlatpakItem( + ref=f"{app_id}/{arch}/{branch}", + app_id=app_id, + branch=branch, + arch=arch, + version=version, + size_bytes=_parse_size(size_str) if size_str else 0, + origin=origin, + kind=kind, + )) + return items + + +def _human_size(n: int) -> str: + f = float(n) + for unit in ("B", "K", "M", "G", "T"): + if f < 1024 or unit == "T": + return f"{f:.0f}{unit}" if unit == "B" else f"{f:.1f}{unit}" + f /= 1024 + return f"{f:.1f}T" + + +class FlatpakView(Vertical): + """Flatpak Apps + Runtimes verwalten (list, sizes, remove).""" + + DEFAULT_CSS = f""" + FlatpakView {{ + background: {T.YORU}; + color: {T.YUKI}; + layout: vertical; + }} + FlatpakView Tabs {{ + background: {T.YORU}; + height: 3; + }} + FlatpakView Tab {{ + background: {T.YORU}; + color: {T.BASE4}; + padding: 0 2; + }} + FlatpakView Tab.-active {{ + color: {T.UME}; + }} + FlatpakView Underline {{ + background: {T.YORU}; + color: {T.BASE2}; + }} + FlatpakView Underline > .underline--bar {{ + background: {T.YORU}; + color: {T.UME}; + }} + FlatpakView #f-stats {{ + height: 1; + padding: 0 2; + background: {T.YORU}; + color: {T.BASE5}; + }} + FlatpakView #f-table {{ + height: 1fr; + background: {T.YORU}; + color: {T.FG_ALT}; + border: none; + overflow-x: hidden; + scrollbar-size-vertical: 1; + scrollbar-color: {T.BASE2} {T.YORU}; + scrollbar-background: {T.YORU}; + scrollbar-corner-color: {T.YORU}; + }} + FlatpakView #f-table:focus {{ + border: none; + }} + FlatpakView DataTable > .datatable--header {{ + background: {T.YORU}; + color: {T.SHINKAI}; + }} + FlatpakView DataTable > .datatable--cursor {{ + background: {T.BASE1}; + color: {T.YUKI}; + }} + FlatpakView DataTable > .datatable--hover {{ + background: {T.YORU}; + }} + FlatpakView DataTable > .datatable--even-row {{ + background: {T.YORU}; + }} + FlatpakView DataTable > .datatable--odd-row {{ + background: {T.YORU}; + }} + FlatpakView #f-flash {{ + height: 1; + padding: 0 2; + background: {T.YORU}; + color: {T.BASE4}; + }} + """ + + BINDINGS = [ + Binding("r", "rescan", "rescan"), + Binding("x", "remove", "entfernen"), + Binding("u", "update_all", "alle updaten", show=False), + Binding("ctrl+a", "remove_unused", "unused runtimes"), + Binding("j", "cursor_down", "down", show=False), + Binding("k", "cursor_up", "up", show=False), + ] + + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + self.current_tab = "app" + self.items_by_tab: dict[str, list[FlatpakItem]] = {"app": [], "runtime": []} + + def compose(self) -> ComposeResult: + yield Tabs( + Tab("apps", id="app"), + Tab("runtimes", id="runtime"), + id="f-tabs", + ) + yield Static("", id="f-stats", markup=True) + yield DataTable(id="f-table", cursor_type="row", zebra_stripes=False) + yield Static("", id="f-flash", markup=True) + + def on_mount(self) -> None: + if not _flatpak_available(): + self._flash("flatpak ist nicht installiert", color=T.RED) + return + t = self.query_one("#f-table", DataTable) + t.add_column("app-id", key="appid") + t.add_column("branch", width=10, key="branch") + t.add_column("version", width=18, key="version") + t.add_column("grösse", width=10, key="size") + t.add_column("origin", width=14, key="origin") + + # ---- actions ---- + + def action_rescan(self) -> None: + self._flash(f"scanne flatpak {self.current_tab}s…") + self._rescan_worker(self.current_tab) + + @work(thread=True, exclusive=True, group="flatpak-scan") + def _rescan_worker(self, kind: str) -> None: + items = _list_kind(kind) + self.app.call_from_thread(self._after_rescan, kind, items) + + def _after_rescan(self, kind: str, items: list[FlatpakItem]) -> None: + self.items_by_tab[kind] = items + if kind == self.current_tab: + self._refresh_view() + self._flash(f"{len(items)} {kind}s") + + def action_remove(self) -> None: + cur = self._current() + if cur is None: + return + body = ( + f"flatpak entfernen?\n\n" + f" {cur.app_id}\n" + f" branch: {cur.branch}\n" + f" grösse: {_human_size(cur.size_bytes)}" + ) + self.app.push_screen( + ConfirmModal(f"uninstall {cur.app_id}?", body, danger=True), + lambda ok: self._after_remove(ok, cur), + ) + + def _after_remove(self, ok: bool | None, item: FlatpakItem) -> None: + if not ok: + self._flash("abgebrochen") + return + codes = run_interactive( + self.app, + [Command( + label=f"uninstall {item.app_id}", + argv=["flatpak", "uninstall", "-y", item.app_id], + )], + ) + success = codes and codes[0] == 0 + self._flash( + f"{item.app_id} entfernt" if success else "uninstall fehlgeschlagen", + color=T.TAKE if success else T.RED, + ) + self.action_rescan() + + def action_remove_unused(self) -> None: + body = "alle unused runtimes via `flatpak uninstall --unused -y` entfernen?" + self.app.push_screen( + ConfirmModal("unused runtimes weg?", body, danger=False), + self._after_remove_unused, + ) + + def _after_remove_unused(self, ok: bool | None) -> None: + if not ok: + self._flash("abgebrochen") + return + codes = run_interactive( + self.app, + [Command( + label="flatpak uninstall --unused", + argv=["flatpak", "uninstall", "--unused", "-y"], + )], + ) + success = codes and codes[0] == 0 + self._flash( + "unused runtimes weg" if success else "fehlgeschlagen", + color=T.TAKE if success else T.RED, + ) + self.action_rescan() + + def action_update_all(self) -> None: + codes = run_interactive( + self.app, + [Command(label="flatpak update", argv=["flatpak", "update", "-y"])], + ) + success = codes and codes[0] == 0 + self._flash("update fertig" if success else "update fehlgeschlagen", + color=T.TAKE if success else T.RED) + self.action_rescan() + + def action_cursor_down(self) -> None: + self.query_one("#f-table", DataTable).action_cursor_down() + + def action_cursor_up(self) -> None: + self.query_one("#f-table", DataTable).action_cursor_up() + + # ---- helpers ---- + + @property + def items(self) -> list[FlatpakItem]: + return self.items_by_tab.get(self.current_tab, []) + + def _current(self) -> FlatpakItem | None: + t = self.query_one("#f-table", DataTable) + r = t.cursor_row + if r is None or r < 0 or r >= len(self.items): + return None + return self.items[r] + + def _flash(self, msg: str, color: str | None = None) -> None: + color = color or T.BASE4 + self.query_one("#f-flash", Static).update(f"[{color}]{msg}[/]") + + def _refresh_view(self) -> None: + items = self.items + t = self.query_one("#f-table", DataTable) + t.clear() + + total_size = sum(i.size_bytes for i in items) + kind_label = "apps" if self.current_tab == "app" else "runtimes" + stats = ( + f"[{T.YUKI}]{len(items)}[/] [{T.BASE5}]{kind_label}[/]" + + (f" [{T.BASE3}]·[/] [{T.CHIKYU}]{_human_size(total_size)}[/] [{T.BASE5}]installiert[/]" + if total_size else "") + ) + self.query_one("#f-stats", Static).update(stats) + + for i in items: + size_styled = ( + f"[{T.CHIKYU}]{_human_size(i.size_bytes)}[/]" + if i.size_bytes else f"[{T.BASE3}]—[/]" + ) + t.add_row( + f"[{T.YUKI}]{i.app_id}[/]", + f"[{T.BASE5}]{i.branch}[/]", + f"[{T.BASE5}]{i.version}[/]", + size_styled, + f"[{T.BASE5}]{i.origin}[/]", + ) + + # ---- events ---- + + def on_tabs_tab_activated(self, event: Tabs.TabActivated) -> None: + if event.tab is None: + return + self.current_tab = event.tab.id + if not self.items_by_tab.get(self.current_tab): + self.action_rescan() + else: + self._refresh_view() diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/README.md b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/README.md new file mode 100644 index 0000000..457a178 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/README.md @@ -0,0 +1,86 @@ +# TANINUX GTK-Frontend + +GTK4 + libadwaita Desktop-App neben der Textual-TUI. Teilt sich die +`taninux.core`-Engine (sysinfo / pacman / bootloader / snapshot …). + +## Start + +```sh +# System-Abhängigkeiten (empfohlen statt pip-Build von PyGObject): +sudo pacman -S python-gobject gtk4 libadwaita + +# starten +python -m taninux.gui +# oder nach `uv pip install -e .`: +taninux-gtk +``` + +## Architektur + +``` +gui/ + app.py Adw.Application + main() + window.py Adw.NavigationSplitView: Sidebar (Pillar-Liste) + Content-Stack + process.py async Gio.Subprocess-Runner (non-blocking), pkexec für root + console.py LogConsole + RunnerCard — Button → privilegierter Befehl → Live-Output + pages/ + __init__.py PAGES-Registry (key, label, icon, builder) — Pendant zur PILLARS-Liste + overview.py read-only, komplett aus core.sysinfo + update.py `pacman -Syu` via pkexec, Live-Stream (Async-Showcase) + kernel.py installierte Kernel + bootloader, linux-lts via pkexec installieren + settings.py SwitchRow/ComboRow/SpinRow/ColorScheme — das libadwaita-Vokabular +``` + +Neue Seite = Modul in `pages/` mit `build() -> Gtk.Widget` + ein Eintrag in +`PAGES`. Genau wie ein neuer Pillar in der TUI. + +## Privilege Escalation — dedizierter Helper + +`process.ProcessRunner(... privileged=True)` führt root-Befehle **nicht** mehr +als `pkexec pacman …` (= beliebiges root) aus, sondern schleust das argv durch +einen dedizierten Helper: + +``` +pkexec /usr/lib/taninux/taninux-helper +``` + +Der Helper (`helper/taninux-helper`) validiert das argv gegen eine feste +**Allowlist** und lehnt alles andere mit Exit ≠ 0 ab. Erlaubt sind nur: + +| Kommando | Operationen | erlaubte Flags | +|------------|----------------------------------------------|---------------------------------------------| +| `pacman` | `-S` `-Sy` `-Su` `-Syu` `-R` `-Rs` `-Rns` | `--noconfirm` `--needed` `--noprogressbar` | +| `bootctl` | `set-default` `set-oneshot` | — | +| `paccache` | `-r` `-rk1` `-rk0` `-ruk0` | `-k` `0`–`3` `--remove` `--uninstalled` | + +Operanden (Paketnamen, Boot-Einträge) sind frei — Tokens mit führendem `-`, +die nicht oben stehen, werden abgelehnt. Es läuft KEINE Shell, der Helper +`execvp()`t exakt das übergebene argv. Die argv-Schnittstelle nach aussen +(`RunnerCard(label, ["pacman","-Syu"], privileged=True)`) bleibt unverändert. + +Findet `process.py` `/usr/lib/taninux/taninux-helper` nicht, fällt es auf den +Helper im Repo zurück (`gui/helper/taninux-helper`) — praktisch für Dev ohne +Installation. + +### Installation (Helper + Policy) + +```sh +sudo install -Dm755 src/taninux/gui/helper/taninux-helper \ + /usr/lib/taninux/taninux-helper +sudo cp src/taninux/gui/polkit/ch.gabrielevarano.Taninux.policy \ + /usr/share/polkit-1/actions/ +``` + +Die Policy (`action id = ch.gabrielevarano.taninux.maintain`, +`exec.path = /usr/lib/taninux/taninux-helper`, `allow_active = auth_admin_keep`) +sorgt für **einmal Passwort pro Session**, danach gecached. + +### polkit-Auth-Agent + +Damit pkexec **ohne** Terminal nach dem Passwort fragen kann, muss ein +polkit-Auth-Agent laufen. Auf wlroots/eww-Setups ist der oft nicht automatisch +da — starte einen in deiner Autostart-Config: + +- `polkit-gnome` Agent, oder +- `lxqt-policykit` (`/usr/lib/lxqt-policykit/lxqt-policykit-agent`), oder +- `mate-polkit` / `hyprpolkitagent` diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/__init__.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/__init__.py new file mode 100644 index 0000000..78a6fb3 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/__init__.py @@ -0,0 +1,10 @@ +"""TANINUX GTK-Frontend (GTK4 + libadwaita). + +Zweites Frontend neben der Textual-TUI — teilt sich dieselbe `taninux.core`- +Engine (sysinfo / pacman / bootloader / snapshot …). Die TUI bleibt für +Terminal/SSH, das GTK-Frontend ist die Desktop-Verwaltungs-App, die sich in +ein GTK-Setup (eww-Panel + Dock) einfügt. +""" +from __future__ import annotations + +APP_ID = "ch.gabrielevarano.Taninux" diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/__main__.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/__main__.py new file mode 100644 index 0000000..d02cbf4 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/__main__.py @@ -0,0 +1,3 @@ +from taninux.gui.app import main + +raise SystemExit(main()) diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/accents.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/accents.py new file mode 100644 index 0000000..bb598eb --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/accents.py @@ -0,0 +1,36 @@ +"""Fuji-Akzent-Definitionen — reine Daten, KEINE GTK-Abhängigkeit. + +Bewusst GTK-frei, damit sowohl das GTK-Frontend (gui.style) als auch der +Persistenz-Writer (gui.config) und externe Consumer (z.B. die eww-Bar, die +gui.json liest) dieselbe Quelle nutzen. Hex-Werte kommen aus core.theme, +damit GTK-App und TUI identisch sind. +""" +from __future__ import annotations + +from dataclasses import dataclass + +from taninux.core import theme as T + + +@dataclass(frozen=True) +class Accent: + key: str + label: str + hex: str + + +# Reihenfolge = Anzeige-Reihenfolge der Swatches. +FUJI_ACCENTS: list[Accent] = [ + Accent("ume", "Ume · violet", T.UME), + Accent("shinkai", "Shinkai · blue", T.SHINKAI), + Accent("take", "Take · green", T.TAKE), + Accent("chikyu", "Chikyū · yellow", T.CHIKYU), + Accent("beni", "Beni · red", T.RED), +] + +_BY_KEY = {a.key: a for a in FUJI_ACCENTS} +DEFAULT_ACCENT = FUJI_ACCENTS[0].key + + +def by_key(key: str) -> Accent: + return _BY_KEY.get(key, _BY_KEY[DEFAULT_ACCENT]) diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/app.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/app.py new file mode 100644 index 0000000..118f150 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/app.py @@ -0,0 +1,54 @@ +"""Adw.Application-Einstiegspunkte fürs GTK-Frontend. + +Zwei Apps auf derselben Codebasis (teilen core/ + die GUI-Bausteine): + + tsettings System Settings — Kernel/Connectivity/Hardware/… + thub Software Hub — Pakete suchen/installieren, Updates… + +Jede ist nur ein dünner Shell, der seine Sektionsliste an MainWindow gibt. +""" +from __future__ import annotations + +import sys + +import gi + +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") +from gi.repository import Adw, Gio # noqa: E402 + +from taninux.gui import APP_ID, style +from taninux.gui.pages import HUB_SECTIONS, SETTINGS_SECTIONS +from taninux.gui.window import MainWindow + + +class _ShellApp(Adw.Application): + def __init__(self, app_id: str, title: str, sections) -> None: + super().__init__(application_id=app_id, flags=Gio.ApplicationFlags.DEFAULT_FLAGS) + self._title = title + self._sections = sections + self._window: MainWindow | None = None + + def do_activate(self) -> None: + if self._window is None: + style.apply_persisted() # gespeichertes Farbschema + Fuji-Akzent + self._window = MainWindow(self, self._sections, self._title) + self._window.present() + + +def main_settings(argv: list[str] | None = None) -> int: + app = _ShellApp(f"{APP_ID}.Settings", "System Settings", SETTINGS_SECTIONS) + return app.run(argv if argv is not None else sys.argv) + + +def main_hub(argv: list[str] | None = None) -> int: + app = _ShellApp(f"{APP_ID}.SoftwareHub", "Software Hub", HUB_SECTIONS) + return app.run(argv if argv is not None else sys.argv) + + +# Rückwärtskompatibel: `taninux-gtk` / `python -m taninux.gui` -> System Settings. +main = main_settings + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/config.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/config.py new file mode 100644 index 0000000..8686b2c --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/config.py @@ -0,0 +1,44 @@ +"""Persistenter Appearance-State fürs GTK-Frontend. + +Eigene kleine JSON neben dem TUI-State (core.state ist whitelist-fokussiert, +das hier ist reine GUI-Präferenz). Liegt in der XDG-data-dir von taninux. +""" +from __future__ import annotations + +import json + +from taninux.core.paths import data_dir +from taninux.gui.accents import by_key + +_DEFAULTS = {"color_scheme": "system", "accent": "ume"} + + +def _path(): + return data_dir() / "gui.json" + + +def load() -> dict: + try: + data = json.loads(_path().read_text()) + except (json.JSONDecodeError, OSError): + return dict(_DEFAULTS) + return {**_DEFAULTS, **{k: data[k] for k in _DEFAULTS if k in data}} + + +def save(color_scheme: str, accent: str) -> None: + # accent_hex/-label gleich mit auflösen, damit externe Consumer (eww-Bar) + # nur diese eine JSON lesen müssen — kein Key→Hex-Mapping nachbauen. + acc = by_key(accent) + try: + _path().write_text(json.dumps( + { + "version": 1, + "color_scheme": color_scheme, + "accent": accent, + "accent_hex": acc.hex, + "accent_label": acc.label, + }, + indent=2, + )) + except OSError: + pass diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/confirm.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/confirm.py new file mode 100644 index 0000000..fc1d976 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/confirm.py @@ -0,0 +1,42 @@ +"""Wiederverwendbare Bestätigungs-Dialoge fürs GTK-Frontend. + +Vor destruktiven Aktionen (Pakete entfernen, Cache leeren, Bootloader ändern) +zeigen die Seiten einen `Adw.AlertDialog` und rufen den Callback nur bei +„Fortfahren". So liegt das Bestätigungsmuster an EINER Stelle. +""" +from __future__ import annotations + +from typing import Callable + +import gi + +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") +from gi.repository import Adw, Gtk # noqa: E402 + + +def confirm( + parent: Gtk.Widget, + heading: str, + body: str, + on_confirm: Callable[[], None], +) -> None: + """Zeigt einen Bestätigungs-Dialog. Ruft `on_confirm()`, wenn der Nutzer + „Fortfahren" wählt; bei „Abbrechen" passiert nichts. + + `parent` ist irgendein Widget im Fenster — der Dialog hängt sich an dessen + Root (Gtk.Window) an. Nicht-blockierend (libadwaita-Async-Pattern). + """ + dialog = Adw.AlertDialog(heading=heading, body=body) + dialog.add_response("cancel", "Cancel") + dialog.add_response("confirm", "Continue") + dialog.set_response_appearance("confirm", Adw.ResponseAppearance.DESTRUCTIVE) + dialog.set_default_response("cancel") + dialog.set_close_response("cancel") + + def _on_response(_dlg: Adw.AlertDialog, response: str) -> None: + if response == "confirm": + on_confirm() + + dialog.connect("response", _on_response) + dialog.present(parent) diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/console.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/console.py new file mode 100644 index 0000000..47526e7 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/console.py @@ -0,0 +1,123 @@ +"""Scrollbare Monospace-Log-Konsole + Run-Button — wiederverwendbar. + +Kapselt das Muster „Button drücken → privilegierten Befehl async ausführen → +Output live mitlesen". Genutzt von der Update- und Kernel-Seite. +""" +from __future__ import annotations + +import gi + +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") +from gi.repository import Adw, Gtk # noqa: E402 + +from taninux.gui import confirm as confirm_mod +from taninux.gui.process import ProcessRunner + + +class LogConsole(Gtk.Box): + """TextView in einem ScrolledWindow, das automatisch nach unten scrollt.""" + + def __init__(self) -> None: + super().__init__(orientation=Gtk.Orientation.VERTICAL) + self.add_css_class("card") + + self._view = Gtk.TextView(editable=False, cursor_visible=False, monospace=True) + self._view.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) + self._view.set_left_margin(10) + self._view.set_right_margin(10) + self._view.set_top_margin(8) + self._view.set_bottom_margin(8) + self._buffer = self._view.get_buffer() + + # Höhe auf ~5 Zeilen begrenzt (vorher 220px = riesiges Feld), scrollbar. + scroller = Gtk.ScrolledWindow(hexpand=True) + scroller.set_min_content_height(112) + scroller.set_max_content_height(112) + scroller.set_propagate_natural_height(False) + scroller.set_child(self._view) + self.set_vexpand(False) + self.append(scroller) + + def clear(self) -> None: + self._buffer.set_text("") + + def append_line(self, text: str) -> None: + end = self._buffer.get_end_iter() + self._buffer.insert(end, text + "\n") + # ans Ende scrollen + mark = self._buffer.create_mark(None, self._buffer.get_end_iter(), False) + self._view.scroll_mark_onscreen(mark) + self._buffer.delete_mark(mark) + + +def collapsible_log(console: LogConsole, *, title: str = "Log") -> Gtk.Expander: + """Konsole in einem zugeklappten, dezenten „Log"-Disclosure.""" + exp = Gtk.Expander(label=title) + exp.set_margin_top(6) + exp.set_child(console) + return exp + + +class RunnerCard(Gtk.Box): + """Konsole + Action-Button, verdrahtet mit einem ProcessRunner.""" + + def __init__( + self, + button_label: str, + argv: list[str], + *, + privileged: bool = True, + confirm: tuple[str, str] | None = None, + ) -> None: + super().__init__(orientation=Gtk.Orientation.VERTICAL, spacing=12) + self._argv = argv + self._privileged = privileged + self._confirm = confirm + self._runner = ProcessRunner() + self._runner.connect("line", lambda _r, line: self._console.append_line(line)) + self._runner.connect("done", self._on_done) + + self._button = Gtk.Button(label=button_label) + self._button.add_css_class("suggested-action") + self._button.add_css_class("pill") + self._button.set_halign(Gtk.Align.START) + self._button.connect("clicked", self._on_click) + self.append(self._button) + + self._console = LogConsole() + self.append(collapsible_log(self._console)) + + def _run(self) -> None: + self._button.set_sensitive(False) + self._console.clear() + self._runner.run(self._argv, privileged=self._privileged) + + def _on_click(self, _button: Gtk.Button) -> None: + if self._confirm is not None: + heading, body = self._confirm + confirm_mod.confirm(self._button, heading, body, self._run) + else: + self._run() + + def _on_done(self, _runner: ProcessRunner, code: int) -> None: + self._console.append_line("") + self._console.append_line( + f"✔ done (exit {code})" if code == 0 else f"✘ exit {code}" + ) + self._button.set_sensitive(True) + + +def log_expander(console: LogConsole, *, title: str = "Log", + expanded: bool = False) -> Adw.PreferencesGroup: + """Verpackt eine LogConsole in eine einklappbare „Log"-Zeile (Pfeil). + + Standardmäßig zugeklappt; aufgeklappt zeigt sie die auf ~5 Zeilen + begrenzte, scrollbare Konsole. Rückgabe = PreferencesGroup (für page.add()). + Ersetzt das frühere „große schwarze Log-Feld" auf jeder Seite. + """ + group = Adw.PreferencesGroup() + exp = collapsible_log(console, title=title) + exp.set_expanded(expanded) + group.add(exp) + return group diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/desktop.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/desktop.py new file mode 100644 index 0000000..546cc8d --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/desktop.py @@ -0,0 +1,256 @@ +"""Desktop-Look: GTK3-Theme, Icon-Theme, System-Font — wie nwg-look. + +Alles User-Level (kein root). Robuster Doppel-Weg wie nwg-look: wir setzen +sowohl die gsettings-Keys (org.gnome.desktop.interface) ALS AUCH die +~/.config/gtk-{3,4}.0/settings.ini — so greift es unter GNOME (gsettings) +genauso wie unter niri (settings.ini), ohne laufenden Daemon. + +Hinweis: libadwaita-Apps ignorieren das GTK-*Theme* bewusst (nur GTK3/alte +GTK4-Apps folgen). Icon-Theme und Font gelten dagegen auch für libadwaita. +""" +from __future__ import annotations + +import configparser +import subprocess +from pathlib import Path + +_IFACE = "org.gnome.desktop.interface" +_SKIP_ICONS = {"default", "hicolor", "locolor"} + + +# ------------------------------------------------------------- discovery + +def _theme_dirs() -> list[Path]: + return [ + Path("/usr/share/themes"), + Path.home() / ".themes", + Path.home() / ".local/share/themes", + ] + + +def _icon_dirs() -> list[Path]: + return [ + Path("/usr/share/icons"), + Path.home() / ".local/share/icons", + Path.home() / ".icons", + ] + + +def list_gtk_themes() -> list[str]: + """Theme-Verzeichnisse mit gtk-3.0/ oder gtk-4.0/ Unterordner.""" + names: set[str] = set() + for d in _theme_dirs(): + if not d.is_dir(): + continue + for t in d.iterdir(): + if t.is_dir() and any((t / s).is_dir() for s in ("gtk-3.0", "gtk-4.0")): + names.add(t.name) + return sorted(names, key=str.lower) + + +def list_icon_themes() -> list[str]: + """Echte Icon-Themes (index.theme mit nicht-leerem Directories=).""" + names: set[str] = set() + for d in _icon_dirs(): + if not d.is_dir(): + continue + for t in d.iterdir(): + if t.name in _SKIP_ICONS: + continue + idx = t / "index.theme" + if not idx.is_file(): + continue + try: + text = idx.read_text(errors="ignore") + except OSError: + continue + for line in text.splitlines(): + s = line.strip() + if s.startswith("Directories=") and s.split("=", 1)[1].strip(): + names.add(t.name) + break + return sorted(names, key=str.lower) + + +def list_cursor_themes() -> list[str]: + """Cursor-Themes = Verzeichnisse mit einem cursors/ Unterordner.""" + names: set[str] = set() + for d in _icon_dirs(): + if not d.is_dir(): + continue + for t in d.iterdir(): + if t.is_dir() and (t / "cursors").is_dir(): + names.add(t.name) + return sorted(names, key=str.lower) + + +# ------------------------------------------------------------- read state + +def _gget(key: str) -> str: + try: + r = subprocess.run( + ["gsettings", "get", _IFACE, key], + capture_output=True, text=True, check=False, timeout=5, + ) + except (FileNotFoundError, subprocess.TimeoutExpired): + return "" + return r.stdout.strip().strip("'\"") if r.returncode == 0 else "" + + +def current() -> dict[str, str]: + return { + "gtk_theme": _gget("gtk-theme"), + "icon_theme": _gget("icon-theme"), + "font": _gget("font-name"), + "cursor_theme": _gget("cursor-theme"), + "cursor_size": _gget("cursor-size"), + } + + +# ------------------------------------------------------------- apply + +def _gset(key: str, value: str) -> None: + try: + subprocess.run( + ["gsettings", "set", _IFACE, key, value], + check=False, timeout=5, + ) + except (FileNotFoundError, subprocess.TimeoutExpired): + pass + + +def _write_ini(key: str, value: str) -> None: + """Schreibt [Settings] key=value in gtk-3.0 und gtk-4.0 settings.ini.""" + for sub in ("gtk-3.0", "gtk-4.0"): + d = Path.home() / ".config" / sub + d.mkdir(parents=True, exist_ok=True) + ini = d / "settings.ini" + cp = configparser.ConfigParser() + cp.optionxform = str # Key-Case erhalten + if ini.exists(): + try: + cp.read(ini) + except configparser.Error: + pass + if "Settings" not in cp: + cp["Settings"] = {} + cp["Settings"][key] = value + try: + with ini.open("w") as f: + cp.write(f) + except OSError: + pass + + +def apply_gtk_theme(name: str) -> None: + _gset("gtk-theme", name) + _write_ini("gtk-theme-name", name) + + +def apply_icon_theme(name: str) -> None: + _gset("icon-theme", name) + _write_ini("gtk-icon-theme-name", name) + + +def apply_font(name: str) -> None: + _gset("font-name", name) + _write_ini("gtk-font-name", name) + + +def apply_cursor_theme(name: str) -> None: + _gset("cursor-theme", name) + _write_ini("gtk-cursor-theme-name", name) + + +def apply_cursor_size(size: int) -> None: + _gset("cursor-size", str(size)) + _write_ini("gtk-cursor-theme-size", str(size)) + + +# ------------------------------------------------------------- titlebar buttons +# +# button-layout (Schema org.gnome.desktop.wm.preferences) bestimmt, welche +# Knöpfe in der Titlebar erscheinen und auf welcher Seite. Format ":", +# Buttons komma-getrennt aus {minimize,maximize,close,appmenu,icon}. +# Beispiele: ":minimize,maximize,close" (alle rechts), "close:" (nur Close links). +# libadwaita respektiert diesen Key (auch diese App). + +_WM = "org.gnome.desktop.wm.preferences" + + +def _wm_get(key: str) -> str: + try: + r = subprocess.run( + ["gsettings", "get", _WM, key], + capture_output=True, text=True, check=False, timeout=5, + ) + except (FileNotFoundError, subprocess.TimeoutExpired): + return "" + return r.stdout.strip().strip("'\"") if r.returncode == 0 else "" + + +def _wm_set(key: str, value: str) -> None: + try: + subprocess.run( + ["gsettings", "set", _WM, key, value], + check=False, timeout=5, + ) + except (FileNotFoundError, subprocess.TimeoutExpired): + pass + + +def _read_ini(key: str) -> str: + """Liest [Settings] key aus gtk-4.0 (bevorzugt) bzw. gtk-3.0 settings.ini.""" + for sub in ("gtk-4.0", "gtk-3.0"): + ini = Path.home() / ".config" / sub / "settings.ini" + if not ini.is_file(): + continue + cp = configparser.ConfigParser() + cp.optionxform = str + try: + cp.read(ini) + except configparser.Error: + continue + if cp.has_option("Settings", key): + return cp.get("Settings", key) + return "" + + +def _parse_layout(raw: str) -> dict: + left_part, _, right_part = raw.partition(":") + left = {b.strip() for b in left_part.split(",") if b.strip()} + right = {b.strip() for b in right_part.split(",") if b.strip()} + wanted = ("minimize", "maximize", "close") + side = "left" if sum(b in left for b in wanted) > sum(b in right for b in wanted) else "right" + present = left | right + return { + "minimize": "minimize" in present, + "maximize": "maximize" in present, + "close": "close" in present, + "side": side, + } + + +def window_buttons() -> dict: + """Effektives Decoration-Layout → {minimize, maximize, close, side}. + + Quelle ist `gtk-decoration-layout` aus settings.ini (das, was GTK wirklich + nutzt — der gnome-gsettings-Key allein greift auf portal-losen Sessions + NICHT). Wenn noch nichts gesetzt: GTK-Default (alle drei, rechts). + """ + raw = _read_ini("gtk-decoration-layout") + if not raw or ":" not in raw: + return {"minimize": True, "maximize": True, "close": True, "side": "right"} + return _parse_layout(raw) + + +def set_window_buttons(minimize: bool, maximize: bool, close: bool, side: str) -> None: + """Schreibt das Decoration-Layout. WIRKSAM ist `gtk-decoration-layout` in + settings.ini (greift bei NEU geöffneten Fenstern). Zusätzlich der + gnome-gsettings-Key, falls ein Settings-Portal ihn live übersetzt.""" + joined = ",".join( + b for b, on in (("minimize", minimize), ("maximize", maximize), ("close", close)) if on + ) + value = joined + ":" if side == "left" else ":" + joined + _write_ini("gtk-decoration-layout", value) # wirksam (settings.ini) + _wm_set("button-layout", value) # zusätzlich (gsettings) diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/eww.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/eww.py new file mode 100644 index 0000000..b8f87b7 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/eww.py @@ -0,0 +1,33 @@ +"""eww-Reload — Bar nimmt neuen Akzent sofort über gui.json auf. + +Best-effort: re-evaluiert eww-Config (liest ihrerseits gui.json), verändert +eww selbst NICHT direkt. Das eww-Setup gehört Karims separater Config in ~/eww. +Reines stdlib (subprocess), keine GTK-Importe. +""" +from __future__ import annotations + +import subprocess + + +def reload_if_running() -> bool: + """Löst `eww reload` aus, falls der eww-Daemon läuft. Gibt True zurück, + wenn `eww reload` ausgeführt wurde, sonst False. Best-effort: fängt + FileNotFoundError/TimeoutExpired ab und crasht nie.""" + try: + running = subprocess.run( + ["pgrep", "-x", "eww"], check=False, timeout=5, + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, + ) + except (FileNotFoundError, subprocess.TimeoutExpired): + return False + if running.returncode != 0: + return False # Daemon läuft nicht + + try: + subprocess.run( + ["eww", "reload"], check=False, timeout=5, + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, + ) + except (FileNotFoundError, subprocess.TimeoutExpired): + return False + return True diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/helper/taninux-helper b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/helper/taninux-helper new file mode 100755 index 0000000..dc8daa0 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/helper/taninux-helper @@ -0,0 +1,198 @@ +#!/usr/bin/env python3 +"""TANINUX root-Helper — wird via pkexec mit erhöhten Rechten gestartet. + +Installationspfad: /usr/lib/taninux/taninux-helper +(passend zur polkit-Action ch.gabrielevarano.taninux.maintain, deren +exec.path genau auf diesen Pfad zeigt.) + +Idee: Statt `pkexec pacman …` direkt laufen zu lassen (= beliebiges root!), +reicht das GTK-Frontend sein argv durch DIESEN Helper. Der prüft das Kommando +gegen eine feste ALLOWLIST und lehnt alles andere ab. So bleibt die +argv-basierte Schnittstelle (`["pacman","-Syu"]`, `["bootctl","set-default",…]`) +nach aussen unverändert, aber root kann nur noch die erlaubten Operationen. + +Aufruf: + taninux-helper [args…] → validieren + ausführen (exec) + taninux-helper --check [..] → nur validieren, Exit 0/2 (für Tests) + +Keine externen Deps — reines Stdlib, damit der Helper auch ohne installiertes +taninux-Paket aus dem Repo heraus läuft. +""" +from __future__ import annotations + +import os +import sys + +# ALLOWLIST: erste argv-Komponente → erlaubte Operationen. +# +# Pro Kommando definieren wir, welche der nachfolgenden Tokens als Sub-/Flag +# zulässig sind. Tokens, die mit "-" beginnen, müssen in "flags" stehen +# (oder einer der erlaubten Operatoren sein). Nicht-Flag-Tokens gelten als +# Operanden (Paketnamen, Boot-Einträge) und werden NICHT eingeschränkt — +# pacman/bootctl validieren die selbst, und sie können kein anderes Programm +# starten. +# +# Wichtig: KEINE Shell, kein eval. Wir führen via os.execvp() exakt das +# übergebene argv aus, ohne Interpretation durch eine Shell. +ALLOWLIST: dict[str, dict[str, set[str]]] = { + # Pakete (de)installieren + Vollupgrade. --noconfirm erlaubt, damit das + # GUI-Frontend nicht in einem TTY-Prompt hängen bleibt. + "pacman": { + "ops": {"-S", "-Syu", "-Su", "-Sy", "-R", "-Rs", "-Rns"}, + "flags": {"--noconfirm", "--needed", "--noprogressbar"}, + }, + # Firewall an/aus (ufw). Nur enable/disable — keine Regel-Manipulation. + "ufw": { + "ops": {"enable", "disable"}, + "flags": set(), + }, + # Benutzerverwaltung. useradd bewusst NUR mit -m/-c (KEIN -G/-u/-p/-o → + # keine Gruppen-/UID-Injektion); Admin-Rechte laufen separat über gpasswd + # (zu/aus wheel). passwd nur -e (Reset beim 1. Login erzwingen, kein Setzen). + "useradd": { + "ops": set(), + "flags": {"-m", "-c"}, + }, + "userdel": { + "ops": set(), + "flags": {"-r"}, + }, + "gpasswd": { + "ops": {"-a", "-d"}, + "flags": set(), + }, + "passwd": { + "ops": {"-e"}, + "flags": set(), + }, + # Bootloader-Default / einmaliger Boot (systemd-boot). + "bootctl": { + "ops": {"set-default", "set-oneshot"}, + "flags": set(), + }, + # Paket-Cache aufräumen. + "paccache": { + "ops": {"-r", "-rk1", "-rk0", "-ruk0"}, + "flags": {"-k", "0", "1", "2", "3", "--remove", "--uninstalled"}, + }, + # System-Locale + Tastatur-Layout (systemd). Operanden (LANG=…, Layout, + # Variante) sind frei; localectl validiert sie selbst. + "localectl": { + "ops": {"set-locale", "set-keymap", "set-x11-keymap"}, + "flags": set(), + }, + # Zeitzone + NTP (systemd-timesyncd). Operanden (Zeitzone, true/false) + # sind freie Operanden; timedatectl validiert sie selbst. set-time o.ä. + # ist bewusst NICHT erlaubt. + "timedatectl": { + "ops": {"set-timezone", "set-ntp", "set-local-rtc"}, + "flags": set(), + }, + # Dienste verwalten (z.B. power-profiles-daemon enablen). Unit-Namen sind + # freie Operanden; nur diese Verben sind erlaubt — kein mask/edit/o.ä. + "systemctl": { + "ops": {"enable", "disable", "start", "stop", "restart", "reenable"}, + "flags": {"--now"}, + }, + # CUPS-Druckerverwaltung. lpadmin: Default setzen (-d) / Drucker entfernen + # (-x); -p/-E nur als Flags zugelassen (kein Hinzufügen mit Treiber). + "lpadmin": { + "ops": {"-d", "-x"}, + "flags": {"-p", "-E"}, + }, + # Drucker an-/abschalten. KEINE Op — nur ein Operand (der Druckername). + "cupsenable": { + "ops": set(), + "flags": set(), + }, + "cupsdisable": { + "ops": set(), + "flags": set(), + }, + # Journal aufräumen (vacuum). Nur feste, ungefährliche Vacuum-Flags. + "journalctl": { + "ops": set(), + "flags": {"--vacuum-time=2weeks", "--vacuum-size=200M"}, + }, + # Snapshots (Timeshift). Braucht für ALLES root, auch --list. Snapshot- + # Namen/Kommentare sind freie Operanden (kein "-" am Anfang). + "timeshift": { + "ops": {"--list", "--create", "--delete", "--delete-all", "--restore", "--check"}, + "flags": {"--snapshot", "--comments", "--yes", "--scripted", "--tags"}, + }, +} + + +class Rejected(Exception): + """Argv hat die Allowlist nicht passiert.""" + + +def validate(argv: list[str]) -> list[str]: + """Prüft argv gegen die ALLOWLIST. Gibt das (unveränderte) argv zurück + oder wirft Rejected mit einer Begründung.""" + if not argv: + raise Rejected("leeres kommando") + + cmd = argv[0] + rule = ALLOWLIST.get(cmd) + if rule is None: + raise Rejected(f"kommando nicht erlaubt: {cmd!r}") + + allowed_ops = rule["ops"] + allowed_flags = rule["flags"] + saw_op = False + + for tok in argv[1:]: + if tok in allowed_ops: + saw_op = True + continue + if tok in allowed_flags: + continue + if tok.startswith("-"): + # ein Flag/Operator, der weder Operation noch erlaubtes Flag ist + raise Rejected(f"option nicht erlaubt für {cmd}: {tok!r}") + # ein Operand (Paketname, Boot-Eintrag) — keine Optionen-Injektion + # möglich, da kein "-" am Anfang. Operanden sind frei. + + # Kommandos mit erlaubten Operationen müssen eine davon nennen. Kommandos + # OHNE Ops (z.B. cupsenable/cupsdisable: nur Operand = Druckername) sind + # auch ohne Op gültig. + if allowed_ops and not saw_op: + raise Rejected(f"keine erlaubte operation für {cmd} angegeben") + + # gpasswd darf NUR die wheel-Gruppe verwalten (Admin-Toggle) — keine + # beliebige Gruppen-Mitgliedschaft (z.B. root). + if cmd == "gpasswd" and (not argv or argv[-1] != "wheel"): + raise Rejected("gpasswd nur für die wheel-Gruppe erlaubt") + + return argv + + +def main(raw_argv: list[str]) -> int: + check_only = False + args = raw_argv[1:] + if args and args[0] == "--check": + check_only = True + args = args[1:] + + try: + argv = validate(args) + except Rejected as err: + print(f"taninux-helper: abgelehnt: {err}", file=sys.stderr) + return 2 + + if check_only: + print(f"taninux-helper: ok: {' '.join(argv)}") + return 0 + + # erlaubtes Kommando ausführen — execvp ersetzt den Helper-Prozess, + # damit pkexec den echten Exit-Code des Kommandos durchreicht. + try: + os.execvp(argv[0], argv) + except OSError as err: # Programm nicht gefunden o.ä. + print(f"taninux-helper: exec fehlgeschlagen: {err}", file=sys.stderr) + return 127 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/__init__.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/__init__.py new file mode 100644 index 0000000..f014d7d --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/__init__.py @@ -0,0 +1,149 @@ +"""Seiten-Registry fürs GTK-Frontend. + +Seiten sind in logische SECTIONS gruppiert (Sidebar zeigt Überschriften). +PAGES ist die flache Ableitung (für Stack-Lookup / Navigation). +Neue Seite = Modul in pages/ mit build() + Eintrag in einer Section unten. +""" +from __future__ import annotations + +from dataclasses import dataclass +from typing import Callable + +from gi.repository import Gtk + +from taninux.gui.pages import ( + about, + accessibility, + audio, + backup, + bluetooth, + bundles, + dashboard, + defaultapps, + display, + firewall, + hub_home, + kernel, + locale, + lock, + maintain, + network, + panel, + power, + printers, + settings, + shares, + storage, + store, + update, + users, + vpn, +) + + +@dataclass(frozen=True) +class Page: + key: str + label: str + icon: str + build: Callable[[], Gtk.Widget] + keywords: tuple[str, ...] = () + + +@dataclass(frozen=True) +class Section: + title: str | None # None = keine Überschrift (Home) + pages: list[Page] + + +# -- System Settings — Hardware/System/Personalisierung -------------------- +SETTINGS_SECTIONS: list[Section] = [ + Section(None, [ + Page("dashboard", "Dashboard", "go-home-symbolic", dashboard.build, + ("overview", "status", "summary", "system", "home")), + ]), + Section("System", [ + Page("about", "About", "help-about-symbolic", about.build, + ("about", "system", "info", "version", "hardware", "cpu", "memory")), + Page("kernel", "Kernel", "application-x-firmware-symbolic", kernel.build, + ("kernel", "boot", "bootloader", "lts", "prune", "headers")), + Page("backup", "Timeshift", "drive-multidisk-symbolic", backup.build, + ("backup", "backups", "snapshot", "timeshift", "restore", "btrfs")), + Page("storage", "Storage", "drive-harddisk-symbolic", storage.build, + ("storage", "disk", "drive", "mount", "usb", "space", "eject")), + Page("users", "Users", "system-users-symbolic", users.build, + ("users", "account", "password", "login", "name")), + Page("defaultapps", "Default Apps", "emblem-default-symbolic", defaultapps.build, + ("default", "apps", "browser", "mail", "open with", "associations")), + ]), + Section("Connectivity", [ + Page("network", "Network", "network-wireless-symbolic", network.build, + ("network", "wifi", "wi-fi", "internet", "connection", "ethernet")), + Page("vpn", "VPN", "network-vpn-symbolic", vpn.build, + ("vpn", "wireguard", "openvpn", "dns", "privacy", "tunnel")), + Page("bluetooth", "Bluetooth", "bluetooth-symbolic", bluetooth.build, + ("bluetooth", "pair", "devices", "headset")), + Page("shares", "Network Shares", "folder-remote-symbolic", shares.build, + ("shares", "smb", "samba", "sftp", "ftp", "webdav", "nextcloud", + "nfs", "mount", "nautilus", "cloud")), + ]), + Section("Hardware", [ + Page("display", "Display", "video-display-symbolic", display.build, + ("display", "monitor", "resolution", "scale", "refresh", "rotation", + "night light", "color temperature")), + Page("audio", "Audio", "audio-volume-high-symbolic", audio.build, + ("audio", "sound", "volume", "output", "input", "microphone", + "per-app", "pavucontrol")), + Page("power", "Power", "battery-full-symbolic", power.build, + ("power", "battery", "energy", "profile", "brightness", "suspend", + "idle", "screen off", "sleep")), + Page("printers", "Printers", "printer-symbolic", printers.build, + ("printer", "cups", "print")), + ]), + Section("Privacy & Security", [ + Page("firewall", "Firewall", "security-high-symbolic", firewall.build, + ("firewall", "ufw", "security", "incoming", "block", "ports")), + Page("lock", "Screen Lock", "system-lock-screen-symbolic", lock.build, + ("lock", "screen", "lockscreen", "security", "password")), + ]), + Section("Personalization", [ + Page("panel", "Panel & Dock", "view-dual-symbolic", panel.build, + ("panel", "dock", "bar", "eww", "taskbar")), + Page("locale", "Region & Time", "input-keyboard-symbolic", locale.build, + ("region", "time", "timezone", "date", "language", "locale", + "keyboard", "layout", "ntp")), + Page("settings", "Appearance", "emblem-system-symbolic", settings.build, + ("appearance", "theme", "accent", "color scheme", "dark", "light", + "icons", "font", "cursor", "wallpaper", "window buttons", "gtk")), + Page("accessibility", "Accessibility", "preferences-desktop-accessibility-symbolic", + accessibility.build, + ("accessibility", "a11y", "large text", "contrast", "animations", "zoom")), + ]), +] + +# -- Software Hub — Paketverwaltung ---------------------------------------- +HUB_SECTIONS: list[Section] = [ + Section(None, [ + Page("home", "Home", "go-home-symbolic", hub_home.build, + ("home", "recommended", "discover", "suggestions", "featured")), + Page("store", "Browse", "system-search-symbolic", store.build, + ("search", "install", "remove", "packages", "pacman", "aur", + "flatpak", "software", "apps")), + ]), + Section("Packages", [ + Page("update", "Updates", "software-update-available-symbolic", update.build, + ("update", "upgrade", "pacman", "flatpak", "aur", "packages")), + Page("maintain", "Cleanup", "user-trash-symbolic", maintain.build, + ("cleanup", "clean", "cache", "orphans", "trash", "disk space", + "journal", "coredumps")), + Page("bundles", "Bundles", "application-x-addon-symbolic", bundles.build, + ("bundles", "packages", "install", "recommended", "suggestions", + "dev", "gaming")), + ]), +] + +# Default-Sektionen = System Settings (Rückwärtskompatibilität). +SECTIONS: list[Section] = SETTINGS_SECTIONS + +# Flache Liste über BEIDE Apps (für _select / Lookup / Tooling). +PAGES: list[Page] = [p for s in (SETTINGS_SECTIONS + HUB_SECTIONS) for p in s.pages] diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/about.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/about.py new file mode 100644 index 0000000..213dd77 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/about.py @@ -0,0 +1,66 @@ +"""About — Überblick über System und Hardware (read-only).""" +from __future__ import annotations + +import gi + +gi.require_version("Adw", "1") +from gi.repository import Adw, Gtk # noqa: E402 + +from taninux.core import sysinfo + + +def _ver() -> str: + try: + from importlib.metadata import version + return version("taninux") + except Exception: # noqa: BLE001 + return "0.2.0" + + +def _row(title: str, value: str) -> Adw.ActionRow: + row = Adw.ActionRow(title=title) + lbl = Gtk.Label(label=value, xalign=1.0, wrap=True) + lbl.add_css_class("dim-label") + lbl.set_selectable(True) + row.add_suffix(lbl) + return row + + +def _fmt_gib(b: int) -> str: + return f"{b / 1024**3:.1f} GiB" if b else "—" + + +def _uptime(sec: float) -> str: + m = int(sec // 60) + d, h, m = m // 1440, (m % 1440) // 60, m % 60 + if d: + return f"{d}d {h}h {m}m" + return f"{h}h {m}m" if h else f"{m}m" + + +def build() -> Gtk.Widget: + page = Adw.PreferencesPage() + mem = sysinfo.memory() + gpus = sysinfo.gpus() + + g_sys = Adw.PreferencesGroup(title="System") + g_sys.add(_row("Device", sysinfo.hostname())) + g_sys.add(_row("Operating system", sysinfo.distro_pretty())) + g_sys.add(_row("Kernel", sysinfo.kernel_release())) + g_sys.add(_row("TANINUX", _ver())) + page.add(g_sys) + + g_hw = Adw.PreferencesGroup(title="Hardware") + g_hw.add(_row("Processor", f"{sysinfo.cpu_model()} · {sysinfo.cpu_count()} cores")) + g_hw.add(_row("Memory", _fmt_gib(mem.get("MemTotal", 0)))) + if gpus: + for i, gpu in enumerate(gpus): + title = "Graphics" if i == 0 else "" + g_hw.add(_row(title, gpu.model)) + page.add(g_hw) + + g_st = Adw.PreferencesGroup(title="Status") + g_st.add(_row("Uptime", _uptime(sysinfo.uptime_seconds()))) + page.add(g_st) + + return page diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/accessibility.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/accessibility.py new file mode 100644 index 0000000..d1ee2d0 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/accessibility.py @@ -0,0 +1,35 @@ +"""Accessibility — Lesbarkeit und Bewegung (gsettings, user-level).""" +from __future__ import annotations + +import gi + +gi.require_version("Adw", "1") +from gi.repository import Adw, Gtk # noqa: E402 + +from taninux.core import accessibility as a11y + + +def _switch(title: str, subtitle: str, getter, setter) -> Adw.SwitchRow: + row = Adw.SwitchRow(title=title, subtitle=subtitle) + row.set_active(getter()) + row.connect("notify::active", lambda r, _p: setter(r.get_active())) + return row + + +def build() -> Gtk.Widget: + page = Adw.PreferencesPage() + + g_see = Adw.PreferencesGroup(title="Seeing") + g_see.add(_switch("Large text", "Increase the size of text", + a11y.large_text, a11y.set_large_text)) + g_see.add(_switch("High contrast", "Make windows and buttons stand out", + a11y.high_contrast, a11y.set_high_contrast)) + page.add(g_see) + + g_motion = Adw.PreferencesGroup(title="Motion") + g_motion.add(_switch("Reduce animations", "Minimise motion across the system", + lambda: not a11y.animations(), + lambda on: a11y.set_animations(not on))) + page.add(g_motion) + + return page diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/appdetail.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/appdetail.py new file mode 100644 index 0000000..101e17d --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/appdetail.py @@ -0,0 +1,203 @@ +"""App-Detailansicht (Adw.Dialog) im GNOME-Software-Stil. + +Zeigt Icon · Name · Beschreibung · Version/Größe/Lizenz/Website und lässt die +Installationsquelle wählen (Repo · Flatpak · AUR falls aktiviert). Quellen und +Metadaten kommen async aus core.appinfo. +""" +from __future__ import annotations + +import threading + +import gi + +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") +from gi.repository import Adw, Gdk, GLib, Gtk # noqa: E402 + +from taninux.core import appinfo, panel +from taninux.gui.console import LogConsole, log_expander +from taninux.gui.process import ProcessRunner + + +def _icon_name(name: str, fallback: str = "package-x-generic-symbolic") -> str: + theme = Gtk.IconTheme.get_for_display(Gdk.Display.get_default()) + return name if (theme and theme.has_icon(name)) else fallback + + +def open_app_detail(parent: Gtk.Widget, *, title: str, icon: str, + repo_name: str | None = None, flatpak_id: str | None = None, + remote: str = "flathub", on_changed=None) -> None: + dialog = Adw.Dialog() + dialog.set_title(title) + dialog.set_content_width(540) + dialog.set_content_height(640) + + console = LogConsole() + runner = ProcessRunner() + runner.connect("line", lambda _r, line: console.append_line(line)) + + cand: list[appinfo.Detail] = [] + + page = Adw.PreferencesPage() + tv = Adw.ToolbarView() + tv.add_top_bar(Adw.HeaderBar()) + tv.set_content(page) + dialog.set_child(tv) + + # -- Kopf: Icon + Name + Summary -------------------------------------- + hg = Adw.PreferencesGroup() + head = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=16) + head.set_margin_top(8) + head.set_margin_bottom(8) + img = Gtk.Image.new_from_icon_name(_icon_name(icon)) + img.set_pixel_size(72) + head.append(img) + tbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4) + tbox.set_valign(Gtk.Align.CENTER) + name_lbl = Gtk.Label(label=title, xalign=0.0) + name_lbl.add_css_class("title-1") + summary_lbl = Gtk.Label(label="", xalign=0.0, wrap=True) + summary_lbl.add_css_class("dim-label") + tbox.append(name_lbl) + tbox.append(summary_lbl) + head.append(tbox) + hg.add(head) + page.add(hg) + + # -- Quelle + Aktion -------------------------------------------------- + sg = Adw.PreferencesGroup(title="Install") + source_combo = Adw.ComboRow(title="Source") + sg.add(source_combo) + action_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10) + action_box.set_margin_top(10) + action_btn = Gtk.Button(label="Install") + action_btn.add_css_class("suggested-action") + action_btn.add_css_class("pill") + action_btn.set_sensitive(False) + spinner = Adw.Spinner() + spinner.set_valign(Gtk.Align.CENTER) + spinner.set_visible(False) + action_box.append(action_btn) + action_box.append(spinner) + sg.add(action_box) + page.add(sg) + + # -- Details ---------------------------------------------------------- + dg = Adw.PreferencesGroup(title="Details") + page.add(dg) + detail_rows: list[Gtk.Widget] = [] + + page.add(log_expander(console)) + + def _selected() -> appinfo.Detail | None: + idx = source_combo.get_selected() + return cand[idx] if 0 <= idx < len(cand) else None + + def _static_row(label: str, value: str, *, link: bool = False) -> Gtk.Widget: + row = Adw.ActionRow(title=label) + if link: + btn = Gtk.LinkButton(uri=value, label=value) + btn.set_valign(Gtk.Align.CENTER) + row.add_suffix(btn) + else: + lab = Gtk.Label(label=value, xalign=1.0, wrap=True) + lab.add_css_class("dim-label") + lab.set_selectable(True) + row.add_suffix(lab) + return row + + def _show_details(d: appinfo.Detail) -> None: + for r in detail_rows: + dg.remove(r) + detail_rows.clear() + summary_lbl.set_label(d.summary or "") + fields = [ + ("Version", d.version), + ("Download size", d.size), + ("Installed size", d.installed_size), + ("License", d.license), + ] + for lab, val in fields: + if val: + r = _static_row(lab, val) + dg.add(r) + detail_rows.append(r) + if d.url: + r = _static_row("Website", d.url, link=True) + dg.add(r) + detail_rows.append(r) + if d.installed: + action_btn.set_label("Remove") + action_btn.remove_css_class("suggested-action") + action_btn.add_css_class("destructive-action") + else: + action_btn.set_label("Install") + action_btn.remove_css_class("destructive-action") + action_btn.add_css_class("suggested-action") + action_btn.set_sensitive(True) + + def _on_source(_combo, _pspec=None) -> None: + d = _selected() + if d: + _show_details(d) + + source_combo.connect("notify::selected", _on_source) + + def _on_action(_b) -> None: + d = _selected() + if not d: + return + argv, priv = (appinfo.remove_argv if d.installed else appinfo.install_argv)(d) + action_btn.set_sensitive(False) + spinner.set_visible(True) + console.clear() + runner.run(argv, privileged=priv) + + action_btn.connect("clicked", _on_action) + + def on_done(_r, code: int) -> None: + console.append_line("✔ done" if code == 0 else f"✘ exit {code}") + spinner.set_visible(False) + if callable(on_changed): + on_changed() + _load() # Quellen/Installed-Status neu ermitteln + + runner.connect("done", on_done) + + def _populate(found: list[appinfo.Detail]) -> None: + cand.clear() + cand.extend(found) + if not found: + source_combo.set_model(Gtk.StringList.new(["Not available"])) + source_combo.set_sensitive(False) + action_btn.set_sensitive(False) + summary_lbl.set_label("This app could not be found in any enabled source.") + return + source_combo.set_sensitive(True) + labels = [] + for d in found: + lab = appinfo.SOURCE_LABEL.get(d.source, d.source) + labels.append(f"{lab} · {d.version}" if d.version else lab) + source_combo.set_model(Gtk.StringList.new(labels)) + # bevorzugt eine bereits installierte Quelle vorwählen + sel = next((i for i, d in enumerate(found) if d.installed), 0) + source_combo.set_selected(sel) + _show_details(found[sel]) + + def _load() -> None: + include_aur = panel.include_aur() + + def worker() -> None: + try: + found = appinfo.candidates( + repo_name=repo_name, flatpak_id=flatpak_id, + remote=remote, include_aur=include_aur, + ) + except Exception: # noqa: BLE001 + found = [] + GLib.idle_add(lambda: (_populate(found), False)[1]) + + threading.Thread(target=worker, daemon=True).start() + + _load() + dialog.present(parent) diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/audio.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/audio.py new file mode 100644 index 0000000..7c23880 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/audio.py @@ -0,0 +1,197 @@ +"""Audio — pavucontrol-artige Steuerung (Output/Input) über core.audio (pactl). + +Pro Richtung: Geräte-Auswahl (= setzt Default), Lautstärke-Slider, Mute. +Alles User-Level, kein root. Live-Updates von aussen (anderes Tool ändert +Volume) holt man per Refresh — eine pactl-subscribe-Schleife wäre der nächste +Ausbau. +""" +from __future__ import annotations + +import gi + +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") +from gi.repository import Adw, Gtk # noqa: E402 + +from taninux.core import audio + + +def build() -> Gtk.Widget: + page = Adw.PreferencesPage() + if not audio.available(): + group = Adw.PreferencesGroup(title="Audio") + group.add(Adw.ActionRow( + title="No audio server", + subtitle="pactl found no PulseAudio/PipeWire server.", + )) + page.add(group) + return page + + page.add(_audio_group( + "sink", "Output", + "audio-volume-high-symbolic", "audio-volume-muted-symbolic", + )) + page.add(_audio_group( + "source", "Input", + "audio-input-microphone-symbolic", "microphone-disabled-symbolic", + )) + page.add(_apps_group()) + return page + + +def _apps_group() -> Adw.PreferencesGroup: + """Per-App-Lautstärke der gerade abspielenden Sink-Inputs.""" + group = Adw.PreferencesGroup(title="Applications") + + refresh = Gtk.Button(icon_name="view-refresh-symbolic") + refresh.add_css_class("flat") + refresh.set_valign(Gtk.Align.CENTER) + refresh.set_tooltip_text("Refresh") + group.set_header_suffix(refresh) + + rows: list[Gtk.Widget] = [] + + def populate() -> None: + for r in rows: + group.remove(r) + rows.clear() + + streams = audio.playback_streams() + if not streams: + row = Adw.ActionRow(title="No apps are playing audio") + group.add(row) + rows.append(row) + return + + for st in streams: + rows.append(_stream_row(st)) + group.add(rows[-1]) + + refresh.connect("clicked", lambda _b: populate()) + populate() + return group + + +def _stream_row(st: audio.Stream) -> Adw.ActionRow: + icon_on = "audio-volume-high-symbolic" + icon_off = "audio-volume-muted-symbolic" + row = Adw.ActionRow(title=st.app) + + adj = Gtk.Adjustment(value=0, lower=0, upper=150, step_increment=1, + page_increment=5) + scale = Gtk.Scale(orientation=Gtk.Orientation.HORIZONTAL, adjustment=adj) + scale.set_hexpand(True) + scale.set_size_request(200, -1) + scale.set_draw_value(False) + scale.set_valign(Gtk.Align.CENTER) + scale.add_mark(100, Gtk.PositionType.BOTTOM, None) + + mute_btn = Gtk.ToggleButton() + mute_btn.add_css_class("flat") + mute_btn.set_valign(Gtk.Align.CENTER) + + row.add_suffix(scale) + row.add_suffix(mute_btn) + + # -- Initialwerte VOR connect setzen (kein Initial-Fire) ----------- + adj.set_value(min(st.volume, 150)) + mute_btn.set_active(st.muted) + mute_btn.set_icon_name(icon_off if st.muted else icon_on) + + def on_vol(a: Gtk.Adjustment) -> None: + audio.set_stream_volume(st.index, int(a.get_value())) + + def on_mute(b: Gtk.ToggleButton) -> None: + m = b.get_active() + audio.set_stream_mute(st.index, m) + b.set_icon_name(icon_off if m else icon_on) + + adj.connect("value-changed", on_vol) + mute_btn.connect("toggled", on_mute) + return row + + +def _audio_group(kind: str, title: str, icon_on: str, icon_off: str) -> Adw.PreferencesGroup: + devices = audio.list_devices(kind) + group = Adw.PreferencesGroup(title=title) + + if not devices: + group.add(Adw.ActionRow(title="No devices found")) + return group + + names = [d.name for d in devices] + descs = [d.description for d in devices] + default = audio.get_default(kind) + state = {"name": default if default in names else names[0]} + + # -- Geräte-Auswahl (= Default setzen) ---------------------------- + combo = Adw.ComboRow(title="Device") + combo.set_model(Gtk.StringList.new(descs)) + combo.set_selected(names.index(state["name"])) + + # -- Lautstärke-Zeile: Slider + % + Mute -------------------------- + vol_row = Adw.ActionRow(title="Volume") + adj = Gtk.Adjustment(value=0, lower=0, upper=150, step_increment=1, + page_increment=5) + scale = Gtk.Scale(orientation=Gtk.Orientation.HORIZONTAL, adjustment=adj) + scale.set_hexpand(True) + scale.set_size_request(220, -1) + scale.set_draw_value(False) + scale.set_valign(Gtk.Align.CENTER) + scale.add_mark(100, Gtk.PositionType.BOTTOM, None) # 100%-Markierung + + pct = Gtk.Label() + pct.add_css_class("dim-label") + pct.add_css_class("numeric") + pct.set_width_chars(4) + + mute_btn = Gtk.ToggleButton() + mute_btn.add_css_class("flat") + mute_btn.set_valign(Gtk.Align.CENTER) + + vol_row.add_suffix(scale) + vol_row.add_suffix(pct) + vol_row.add_suffix(mute_btn) + + guard = {"sync": False} + + def load(name: str) -> None: + guard["sync"] = True + v = audio.get_volume(kind, name) + m = audio.get_muted(kind, name) + adj.set_value(min(v, 150)) + pct.set_label(f"{v}%") + mute_btn.set_active(m) + mute_btn.set_icon_name(icon_off if m else icon_on) + guard["sync"] = False + + load(state["name"]) + + def on_combo(c: Adw.ComboRow, _p) -> None: + i = c.get_selected() + if 0 <= i < len(names): + state["name"] = names[i] + audio.set_default(kind, names[i]) + load(names[i]) + + def on_vol(a: Gtk.Adjustment) -> None: + if guard["sync"]: + return + v = int(a.get_value()) + pct.set_label(f"{v}%") + audio.set_volume(kind, state["name"], v) + + def on_mute(b: Gtk.ToggleButton) -> None: + if guard["sync"]: + return + m = b.get_active() + audio.set_mute(kind, state["name"], m) + b.set_icon_name(icon_off if m else icon_on) + + combo.connect("notify::selected", on_combo) + adj.connect("value-changed", on_vol) + mute_btn.connect("toggled", on_mute) + + group.add(combo) + group.add(vol_row) + return group diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/backup.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/backup.py new file mode 100644 index 0000000..dfd9f3f --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/backup.py @@ -0,0 +1,219 @@ +"""Backups-Seite — Time-Machine-artige Snapshots über Timeshift. + +ALLE Timeshift-Aufrufe (auch --list) brauchen root → laufen privilegiert via +pkexec durch den taninux-helper. Restore ist sehr destruktiv (stellt das System +wieder her, ggf. Reboot) → mit Bestätigungsdialog. +""" +from __future__ import annotations + +import gi + +gi.require_version("Adw", "1") +from gi.repository import Adw, Gtk # noqa: E402 + +from taninux.core import backup +from taninux.gui import confirm +from taninux.gui.console import LogConsole, log_expander +from taninux.gui.process import ProcessRunner + + +def build() -> Gtk.Widget: + page = Adw.PreferencesPage() + + if not backup.available(): + _build_install(page) + return page + + # Gemeinsame Konsole + Action-Runner (create/delete/restore). Nach jeder + # Aktion wird die Snapshot-Liste neu geladen. + console = LogConsole() + action_runner = ProcessRunner() + action_runner.connect("line", lambda _r, line: console.append_line(line)) + + # Snapshots-Gruppe (wird in-place neu befüllt nach jeder Aktion). + g_snaps = Adw.PreferencesGroup( + title="Snapshots", + description="Restore reverts the system and may require a reboot.", + ) + rows: list[Gtk.Widget] = [] + + def reload_snapshots() -> None: + _load_snapshots(g_snaps, rows, console, action_runner, reload_snapshots) + + def on_action_done(_r: ProcessRunner, code: int) -> None: + console.append_line("") + console.append_line("✔ done (exit 0)" if code == 0 else f"✘ exit {code}") + reload_snapshots() + + action_runner.connect("done", on_action_done) + + # -- Create-Gruppe ------------------------------------------------ + page.add(_create_group(console, action_runner)) + + # -- Snapshots-Gruppe (initial laden) ----------------------------- + page.add(g_snaps) + reload_snapshots() + + # -- Log-Gruppe --------------------------------------------------- + page.add(log_expander(console)) + + return page + + +def _build_install(page: Adw.PreferencesPage) -> None: + """Fallback-UI, wenn timeshift nicht installiert ist.""" + from taninux.gui.console import RunnerCard + + group = Adw.PreferencesGroup( + title="Timeshift not installed", + description="Backups use Timeshift for system snapshots.", + ) + card = RunnerCard( + "Install timeshift", + ["pacman", "-S", "--needed", "--noconfirm", "timeshift"], + privileged=True, + ) + box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12) + box.set_margin_top(6) + box.append(card) + group.add(box) + page.add(group) + + +def _create_group(console: LogConsole, runner: ProcessRunner) -> Adw.PreferencesGroup: + """EntryRow für Kommentar + Button zum Erstellen eines Snapshots.""" + group = Adw.PreferencesGroup(title="Create snapshot") + + entry = Adw.EntryRow(title="Comment") + group.add(entry) + + button = Gtk.Button(label="Create snapshot") + button.add_css_class("suggested-action") + button.add_css_class("pill") + button.set_halign(Gtk.Align.START) + + def on_click(_b: Gtk.Button) -> None: + if runner.running: + return + console.clear() + runner.run(backup.create_argv(entry.get_text()), privileged=True) + + button.connect("clicked", on_click) + + box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12) + box.set_margin_top(6) + box.append(button) + group.add(box) + return group + + +def _load_snapshots( + group: Adw.PreferencesGroup, + rows: list[Gtk.Widget], + console: LogConsole, + action_runner: ProcessRunner, + reload: "callable", +) -> None: + """Lädt `timeshift --list` privilegiert über einen EIGENEN Runner, sammelt + die Ausgabe in einem Buffer und baut bei `done` die Zeilen neu auf.""" + # bestehende Zeilen entfernen + for row in rows: + group.remove(row) + rows.clear() + + spinner_row = Adw.ActionRow(title="Loading snapshots…") + spinner = Gtk.Spinner() + spinner.start() + spinner_row.add_prefix(spinner) + group.add(spinner_row) + rows.append(spinner_row) + + buffer: list[str] = [] + list_runner = ProcessRunner() + list_runner.connect("line", lambda _r, line: buffer.append(line)) + + def on_done(_r: ProcessRunner, code: int) -> None: + spinner.stop() + group.remove(spinner_row) + rows.remove(spinner_row) + + if code != 0: + err_row = Adw.ActionRow(title="Could not load snapshots") + group.add(err_row) + rows.append(err_row) + return + + snapshots = backup.parse_list("\n".join(buffer)) + if not snapshots: + empty_row = Adw.ActionRow(title="No snapshots yet") + group.add(empty_row) + rows.append(empty_row) + return + + for snap in snapshots: + subtitle = " ".join(p for p in (snap.tags, snap.description) if p) + row = Adw.ActionRow(title=snap.name, subtitle=subtitle or None) + row.add_suffix(_restore_button(snap.name, console, action_runner, reload)) + row.add_suffix(_delete_button(snap.name, console, action_runner, reload)) + group.add(row) + rows.append(row) + + list_runner.connect("done", on_done) + list_runner.run(backup.list_argv(), privileged=True) + + +def _restore_button( + name: str, + console: LogConsole, + runner: ProcessRunner, + reload: "callable", +) -> Gtk.Button: + button = Gtk.Button(label="Restore") + button.add_css_class("destructive-action") + button.set_valign(Gtk.Align.CENTER) + + def do_run() -> None: + if runner.running: + return + console.clear() + runner.run(backup.restore_argv(name), privileged=True) + + def on_click(_b: Gtk.Button) -> None: + confirm.confirm( + button, + "Restore this snapshot?", + f"This reverts the entire system to {name}. All changes since then " + "will be lost and a reboot may be required. Runs as root via pkexec.", + do_run, + ) + + button.connect("clicked", on_click) + return button + + +def _delete_button( + name: str, + console: LogConsole, + runner: ProcessRunner, + reload: "callable", +) -> Gtk.Button: + button = Gtk.Button(label="Delete") + button.add_css_class("destructive-action") + button.set_valign(Gtk.Align.CENTER) + + def do_run() -> None: + if runner.running: + return + console.clear() + runner.run(backup.delete_argv(name), privileged=True) + + def on_click(_b: Gtk.Button) -> None: + confirm.confirm( + button, + "Delete this snapshot?", + f"Snapshot {name} will be permanently removed. Runs as root via pkexec.", + do_run, + ) + + button.connect("clicked", on_click) + return button diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/bluetooth.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/bluetooth.py new file mode 100644 index 0000000..5885e41 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/bluetooth.py @@ -0,0 +1,173 @@ +"""Bluetooth — über core.bluetooth (bluetoothctl / BlueZ). + +Power-Toggle, Scan (timed), Geräteliste mit Connect/Disconnect. User-Level, +kein root. Live-Output im Log. +""" +from __future__ import annotations + +import threading + +import gi + +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") +from gi.repository import Adw, GLib, Gtk # noqa: E402 + +from taninux.core import bluetooth +from taninux.gui import confirm +from taninux.gui.console import LogConsole, log_expander +from taninux.gui.process import ProcessRunner + + +def build() -> Gtk.Widget: + page = Adw.PreferencesPage() + + if not bluetooth.available(): + g = Adw.PreferencesGroup(title="Bluetooth") + g.add(Adw.ActionRow( + title="No Bluetooth controller", + subtitle="bluetoothctl found no adapter / BlueZ not running.", + )) + page.add(g) + return page + + console = LogConsole() + runner = ProcessRunner() + runner.connect("line", lambda _r, ln: console.append_line(ln)) + + # -- Power + Scan -------------------------------------------------- + g_ctrl = Adw.PreferencesGroup(title="Bluetooth") + power_sw = Adw.SwitchRow(title="Bluetooth", subtitle="Power the adapter") + power_sw.set_active(bluetooth.powered()) + + def on_power(sw: Adw.SwitchRow, _p) -> None: + runner.run(bluetooth.set_power_argv(sw.get_active())) + + power_sw.connect("notify::active", on_power) + g_ctrl.add(power_sw) + page.add(g_ctrl) + + # -- Geräte (async) ------------------------------------------------ + g_dev = Adw.PreferencesGroup( + title="Devices", + description="Pairing PIN-protected devices may need a Bluetooth agent " + "and can fail here — check the log.", + ) + scan_btn = Gtk.Button(icon_name="view-refresh-symbolic") + scan_btn.add_css_class("flat") + scan_btn.set_valign(Gtk.Align.CENTER) + scan_btn.set_tooltip_text("Scan for devices (8 s)") + g_dev.set_header_suffix(scan_btn) + page.add(g_dev) + + rows: list[Gtk.Widget] = [] + # Nach erfolgreichem Pair automatisch Trust nachschieben. Da der Runner + # nur EINEN Prozess gleichzeitig führt, wird die Folge-Aktion erst im + # nächsten `done` ausgeführt. Liste = [argv, …], FIFO. + pending: list[list[str]] = [] + + def _btn(label: str, *, css: str | None = None) -> Gtk.Button: + b = Gtk.Button(label=label) + b.add_css_class("flat") + if css: + b.add_css_class(css) + b.set_valign(Gtk.Align.CENTER) + return b + + def _clear() -> None: + for r in rows: + g_dev.remove(r) + rows.clear() + + def _render(devs: list[bluetooth.BtDevice]) -> None: + _clear() + if not devs: + empty = Adw.ActionRow(title="No devices", + subtitle="Hit rescan to discover nearby devices.") + g_dev.add(empty) + rows.append(empty) + return + for d in devs: + sub = "connected" if d.connected else ("paired" if d.paired else "available") + if d.paired and d.trusted: + sub += " · trusted" + row = Adw.ActionRow(title=d.name, subtitle=sub) + row.add_prefix(Gtk.Image.new_from_icon_name("bluetooth-symbolic")) + + if not d.paired: + # Unbekanntes Gerät: Pair (und danach automatisch Trust). + pair = _btn("Pair", css="suggested-action") + pair.connect("clicked", lambda _b, m=d.mac: _pair(m)) + row.add_suffix(pair) + else: + if d.connected: + primary = _btn("Disconnect") + primary.connect("clicked", + lambda _b, m=d.mac: _run(bluetooth.disconnect_argv(m))) + else: + primary = _btn("Connect", css="suggested-action") + primary.connect("clicked", + lambda _b, m=d.mac: _run(bluetooth.connect_argv(m))) + row.add_suffix(primary) + + forget = _btn("Forget", css="destructive-action") + forget.connect("clicked", + lambda _b, m=d.mac, n=d.name: _forget(m, n)) + row.add_suffix(forget) + + g_dev.add(row) + rows.append(row) + + def _run(argv: list[str]) -> None: + console.clear() + runner.run(argv) + + def _pair(mac: str) -> None: + # Pair zuerst; Trust wird nach erfolgreichem Pair nachgereicht. + pending.append(bluetooth.trust_argv(mac)) + _run(bluetooth.pair_argv(mac)) + + def _forget(mac: str, name: str) -> None: + confirm.confirm( + page, + heading="Forget device?", + body=f"Remove the pairing for {name}. You will have to pair it again.", + on_confirm=lambda: _run(bluetooth.remove_argv(mac)), + ) + + def _load() -> None: + def worker() -> None: + devs = bluetooth.devices() + GLib.idle_add(lambda: (_render(devs), False)[1]) + threading.Thread(target=worker, daemon=True).start() + + def _scan() -> None: + _clear() + spin_row = Adw.ActionRow(title="Scanning … (8 s)") + sp = Adw.Spinner() + sp.set_valign(Gtk.Align.CENTER) + spin_row.add_prefix(sp) + g_dev.add(spin_row) + rows.append(spin_row) + console.clear() + runner.run(bluetooth.scan_argv(8)) + + scan_btn.connect("clicked", lambda _b: _scan()) + + def on_done(_r: ProcessRunner, code: int) -> None: + console.append_line("✔ done" if code == 0 else f"✘ exit {code}") + power_sw.set_active(bluetooth.powered()) + # Verkettete Folge-Aktion (z. B. Trust nach Pair) nur bei Erfolg. + if code == 0 and pending: + nxt = pending.pop(0) + runner.run(nxt) + return + pending.clear() # bei Fehler offene Kette verwerfen + _load() # Liste nach jeder Aktion (Scan/Connect) auffrischen + + runner.connect("done", on_done) + GLib.idle_add(lambda: (_load(), False)[1]) + + # -- Log ----------------------------------------------------------- + page.add(log_expander(console)) + return page diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/bundles.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/bundles.py new file mode 100644 index 0000000..1dda576 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/bundles.py @@ -0,0 +1,140 @@ +"""Bundles — deklarative Paket-Sets über bundles.loader. + +Pro Bundle eine ExpanderRow mit installiert/fehlend-Status pro Paket und einem +„Install missing"-Button (pacman -S --needed via Helper). Status wird hier +selbst berechnet (kein Import aus der Textual-screen.py). Nach jedem Lauf wird +neu berechnet. +""" +from __future__ import annotations + +import gi + +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") +from gi.repository import Adw, Gtk # noqa: E402 + +from taninux.bundles.loader import all_bundles +from taninux.core import pacman +from taninux.gui import confirm +from taninux.gui.console import LogConsole, log_expander +from taninux.gui.process import ProcessRunner +from taninux.suggest import rules + + +def build() -> Gtk.Widget: + page = Adw.PreferencesPage() + + console = LogConsole() + runner = ProcessRunner() + runner.connect("line", lambda _r, ln: console.append_line(ln)) + + g_rec = Adw.PreferencesGroup( + title="Recommended", + description="System-aware suggestions for your hardware and tools.", + ) + page.add(g_rec) + + g = Adw.PreferencesGroup(title="Bundles") + page.add(g) + + rows: list[Gtk.Widget] = [] + rec_rows: list[Gtk.Widget] = [] + + def _clear() -> None: + for r in rows: + g.remove(r) + rows.clear() + + def _install_pkgs(pkgs: list[str], parent: Gtk.Widget, title: str) -> None: + def do() -> None: + console.clear() + runner.run(["pacman", "-S", "--needed", "--noconfirm", *pkgs], privileged=True) + + confirm.confirm( + parent, f"Install {title}", + "Install via pacman: " + ", ".join(pkgs), do, + ) + + def _install_one(pkg: str, parent: Gtk.Widget) -> None: + _install_pkgs([pkg], parent, pkg) + + def _render() -> None: + _clear() + inst = pacman.installed_packages() + for b in all_bundles(): + n_inst = sum(1 for p in b.packages if p in inst) + total = len(b.packages) + complete = n_inst == total + + row = Adw.ExpanderRow(title=b.label, subtitle=b.tagline) + + # Count-Badge als Suffix (statt "Install missing"-Button). + badge = Gtk.Label(label=f"{n_inst}/{total}") + badge.add_css_class("numeric") + badge.add_css_class("success" if complete else "dim-label") + badge.add_css_class("caption-heading") + badge.set_valign(Gtk.Align.CENTER) + row.add_suffix(badge) + + # Pro Paket eine Zeile: installiert = dim-Tag, fehlend = Install-Button. + for pkg in b.packages: + pr = Adw.ActionRow(title=pkg) + if pkg in inst: + tag = Gtk.Label(label="installed") + tag.add_css_class("dim-label") + tag.add_css_class("caption") + tag.set_valign(Gtk.Align.CENTER) + pr.add_suffix(tag) + else: + ib = Gtk.Button(label="Install") + ib.add_css_class("suggested-action") + ib.set_valign(Gtk.Align.CENTER) + ib.connect("clicked", lambda _b, p=pkg: _install_one(p, _b)) + pr.add_suffix(ib) + row.add_row(pr) + + g.add(row) + rows.append(row) + + def _render_rec() -> None: + for r in rec_rows: + g_rec.remove(r) + rec_rows.clear() + sugg = rules.all_suggestions(set(pacman.installed_packages())) + if not sugg: + row = Adw.ActionRow(title="Nothing recommended", + subtitle="Your system looks complete.") + g_rec.add(row) + rec_rows.append(row) + return + for s in sorted(sugg, key=lambda x: x.priority): + row = Adw.ActionRow(title=s.title or s.reason, subtitle=s.reason) + btn = Gtk.Button(label="Install") + btn.add_css_class("suggested-action") + btn.set_valign(Gtk.Align.CENTER) + btn.set_tooltip_text(", ".join(s.packages)) + btn.connect( + "clicked", + lambda _b, p=list(s.packages), t=s.title or "packages": + _install_pkgs(p, _b, t), + ) + row.add_suffix(btn) + g_rec.add(row) + rec_rows.append(row) + + def _refresh() -> None: + _render_rec() + _render() + + def on_done(_r: ProcessRunner, code: int) -> None: + console.append_line("✔ done" if code == 0 else f"✘ exit {code}") + pacman.installed_packages.cache_clear() + _refresh() + + runner.connect("done", on_done) + + # -- Log ---------------------------------------------------------- + page.add(log_expander(console)) + + _refresh() + return page diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/dashboard.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/dashboard.py new file mode 100644 index 0000000..aa50db8 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/dashboard.py @@ -0,0 +1,281 @@ +"""Dashboard — Startseite als responsives Card-Grid. + +Kein abgespecktes Listen-Klon der System-Seite, sondern Karten mit großen +Werten, Level-Bars und Akzenten. Read-only aus den core-Modulen; die langsamen +Teile (Updates, Flatpak) laden async und füllen ihre Karten nach. +""" +from __future__ import annotations + +import json +import threading +from pathlib import Path + +import gi + +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") +from gi.repository import Adw, GLib, Gtk # noqa: E402 + +from taninux.core import audio, network, packages, power, sysinfo + + +def _bytes(n: int) -> str: + f = float(n) + for unit in ("B", "KiB", "MiB", "GiB", "TiB"): + if f < 1024 or unit == "TiB": + return f"{f:.0f} {unit}" if unit == "B" else f"{f:.1f} {unit}" + f /= 1024 + return f"{f:.1f} TiB" + + +def _uptime(secs: float) -> str: + s = int(secs) + d, s = divmod(s, 86400) + h, s = divmod(s, 3600) + m, _ = divmod(s, 60) + parts = [] + if d: + parts.append(f"{d}d") + if h: + parts.append(f"{h}h") + parts.append(f"{m}m") + return " ".join(parts) + + +# ----------------------------------------------------------- card builders + +def _shell(icon: str, title: str) -> tuple[Gtk.Widget, Gtk.Box]: + """Karten-Hülle (.card) + innerer Content-Box. Gibt (card, content).""" + card = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + card.add_css_class("card") + card.set_valign(Gtk.Align.START) + + inner = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) + inner.set_margin_top(16) + inner.set_margin_bottom(16) + inner.set_margin_start(16) + inner.set_margin_end(16) + + head = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8) + img = Gtk.Image.new_from_icon_name(icon) + img.add_css_class("dim-label") + head.append(img) + t = Gtk.Label(label=title, xalign=0.0) + t.add_css_class("caption-heading") + t.add_css_class("dim-label") + head.append(t) + inner.append(head) + + card.append(inner) + return card, inner + + +def _metric(icon: str, title: str, value: str, *, sub: str | None = None, + frac: float | None = None, value_css: str | None = None): + """Stat-Karte: großer Wert + optional Untertitel + Level-Bar. + Gibt (card, value_label, sub_label) — Labels für async-Updates.""" + card, inner = _shell(icon, title) + + v = Gtk.Label(label=value, xalign=0.0) + v.add_css_class("title-2") + if value_css: + v.add_css_class(value_css) + inner.append(v) + + s = Gtk.Label(label=sub or "", xalign=0.0) + s.add_css_class("dim-label") + s.add_css_class("caption") + s.set_wrap(True) + s.set_visible(bool(sub)) + inner.append(s) + + if frac is not None: + lb = Gtk.LevelBar() + lb.set_min_value(0.0) + lb.set_max_value(1.0) + lb.set_value(max(0.0, min(1.0, frac))) + lb.set_margin_top(6) + inner.append(lb) + + return card, v, s + + +def _info(icon: str, title: str, items: list[tuple[str, str]]) -> Gtk.Widget: + """Info-Karte mit mehreren Label/Wert-Zeilen.""" + card, inner = _shell(icon, title) + grid = Gtk.Grid() + grid.set_row_spacing(4) + grid.set_column_spacing(16) + grid.set_margin_top(2) + for r, (k, val) in enumerate(items): + kl = Gtk.Label(label=k, xalign=0.0) + kl.add_css_class("dim-label") + vl = Gtk.Label(label=val, xalign=1.0, hexpand=True) + vl.set_selectable(True) + vl.set_ellipsize(3) # END + grid.attach(kl, 0, r, 1, 1) + grid.attach(vl, 1, r, 1, 1) + inner.append(grid) + return card + + +def _timeshift_summary() -> tuple[str, str]: + """(value, sub) aus der world-readable Timeshift-Config — ohne root.""" + cfg = Path("/etc/timeshift/timeshift.json") + if not cfg.is_file(): + return "Not set up", "Timeshift not configured" + try: + data = json.loads(cfg.read_text()) + except (OSError, json.JSONDecodeError): + return "—", "config unreadable" + mode = "BTRFS" if data.get("btrfs_mode") == "true" else "rsync" + sched = [k[len("schedule_"):] for k, v in data.items() + if k.startswith("schedule_") and v == "true"] + sub = ("schedule: " + ", ".join(sched)) if sched else "manual snapshots only" + return mode, sub + + +def build() -> Gtk.Widget: + flow = Gtk.FlowBox() + flow.set_selection_mode(Gtk.SelectionMode.NONE) + flow.set_homogeneous(True) + flow.set_min_children_per_line(2) + flow.set_max_children_per_line(2) + flow.set_column_spacing(12) + flow.set_row_spacing(12) + flow.set_valign(Gtk.Align.START) + + # -- System (Info-Karte) ------------------------------------------ + flow.append(_info("computer-symbolic", "System", [ + ("Host", sysinfo.hostname()), + ("Distro", sysinfo.distro_pretty()), + ("Kernel", sysinfo.kernel_release()), + ("Uptime", _uptime(sysinfo.uptime_seconds())), + ])) + + # -- Memory ------------------------------------------------------- + mem = sysinfo.memory() + total = mem.get("MemTotal", 0) + used = total - mem.get("MemAvailable", 0) + if total: + flow.append(_metric( + "drive-harddisk-symbolic", "Memory", _bytes(used), + sub=f"of {_bytes(total)} used", frac=used / total, + )[0]) + + # -- CPU load ----------------------------------------------------- + la = sysinfo.load_avg() + cpus = sysinfo.cpu_count() + flow.append(_metric( + "utilities-system-monitor-symbolic", "CPU load", f"{la[0]:.2f}", + sub=f"{cpus} cores · 5m {la[1]:.2f} · 15m {la[2]:.2f}", + frac=la[0] / cpus if cpus else 0, + )[0]) + + # -- Updates (async) ---------------------------------------------- + upd_card, upd_v, upd_s = _metric( + "software-update-available-symbolic", "Updates", "…", + sub="checking", value_css="accent", + ) + flow.append(upd_card) + + # -- Packages (pacman sync, flatpak async) ------------------------ + pk_total = packages.pacman_total() + pkg_card, pkg_v, pkg_s = _metric( + "package-x-generic-symbolic", "Packages", str(pk_total), + sub=f"{packages.pacman_foreign()} AUR · {packages.pacman_explicit()} explicit · flatpak …", + ) + flow.append(pkg_card) + + # -- Backups (Timeshift, unprivilegiert aus config) --------------- + bk_val, bk_sub = _timeshift_summary() + flow.append(_metric("drive-multidisk-symbolic", "Backups", bk_val, sub=bk_sub)[0]) + + # -- Network ------------------------------------------------------ + flow.append(_metric( + "network-wireless-symbolic", "Network", + network.active_summary(), sub="NetworkManager", + )[0]) + + # -- Audio -------------------------------------------------------- + ds = audio.get_default("sink") + if ds: + vol = audio.get_volume("sink", ds) + muted = audio.get_muted("sink", ds) + name = next((d.description for d in audio.list_devices("sink") + if d.name == ds), ds) + flow.append(_metric( + "audio-volume-high-symbolic", "Audio", + "muted" if muted else f"{vol}%", sub=name, + )[0]) + + # -- Graphics ----------------------------------------------------- + gpus = sysinfo.gpus() + if gpus: + g0 = gpus[0] + sub = g0.model[:42] + (f" · {g0.driver}" if g0.driver else "") + flow.append(_metric( + "video-display-symbolic", "Graphics", g0.vendor.upper(), sub=sub, + )[0]) + + # -- Storage ------------------------------------------------------ + disks = sysinfo.disks() + if disks: + total = sum(d.size_bytes for d in disks) + flow.append(_metric( + "drive-multidisk-symbolic", "Storage", _bytes(total), + sub=f"{len(disks)} drive(s)", + )[0]) + + # -- Health ------------------------------------------------------- + failed = sysinfo.systemctl_failed() + je = sysinfo.journal_errors_boot() + if not failed: + h_sub = "no failed units" + (f" · {je} journal errors" if je > 0 else "") + else: + h_sub = ", ".join(failed) + flow.append(_metric( + "emblem-ok-symbolic" if not failed else "dialog-warning-symbolic", + "Health", + "Healthy" if not failed else f"{len(failed)} failed", + sub=h_sub, + value_css="success" if not failed else "error", + )[0]) + + # -- async: Updates + Flatpak nachladen --------------------------- + def worker() -> None: + fp = packages.flatpak_apps() + try: + from taninux.update.sources import pacman as upd_src + n = len(upd_src.fetch()) + except Exception: # pragma: no cover + n = -1 + + def deliver() -> bool: + if n < 0: + upd_v.set_label("—") + upd_s.set_visible(False) + elif n == 0: + upd_v.set_label("0") + upd_v.remove_css_class("accent") + upd_v.add_css_class("success") + upd_s.set_label("up to date") + else: + upd_v.set_label(str(n)) + upd_s.set_label("updates available") + fp_txt = "no flatpak" if fp < 0 else f"{fp} flatpak" + pkg_s.set_label( + f"{packages.pacman_foreign()} AUR · " + f"{packages.pacman_explicit()} explicit · {fp_txt}" + ) + return False + + GLib.idle_add(deliver) + + threading.Thread(target=worker, daemon=True).start() + + page = Adw.PreferencesPage() + grp = Adw.PreferencesGroup() + grp.add(flow) + page.add(grp) + return page diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/defaultapps.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/defaultapps.py new file mode 100644 index 0000000..cd1e759 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/defaultapps.py @@ -0,0 +1,48 @@ +"""Default apps — Standard-Anwendung pro Kategorie wählen (user-level).""" +from __future__ import annotations + +import gi + +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") +from gi.repository import Adw, Gdk, Gtk # noqa: E402 + +from taninux.core import defaultapps + + +def _icon(name: str, fallback: str) -> str: + theme = Gtk.IconTheme.get_for_display(Gdk.Display.get_default()) + return name if (name and theme and theme.has_icon(name)) else fallback + + +def build() -> Gtk.Widget: + page = Adw.PreferencesPage() + group = Adw.PreferencesGroup(title="Default applications") + page.add(group) + + for label, mime, fallback in defaultapps.CATEGORIES: + current, apps = defaultapps.apps_for(mime) + row = Adw.ComboRow(title=label) + row.add_prefix(Gtk.Image.new_from_icon_name(fallback)) + + if not apps: + row.set_subtitle("No app installed") + row.set_sensitive(False) + group.add(row) + continue + + names = [a.name for a in apps] + row.set_model(Gtk.StringList.new(names)) + if current: + sel = next((i for i, a in enumerate(apps) if a.id == current.id), 0) + row.set_selected(sel) + + def on_sel(r, _p, apps=apps, mime=mime) -> None: + i = r.get_selected() + if 0 <= i < len(apps): + defaultapps.set_default(apps[i].id, mime) + + row.connect("notify::selected", on_sel) + group.add(row) + + return page diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/display.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/display.py new file mode 100644 index 0000000..7a4dcbf --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/display.py @@ -0,0 +1,212 @@ +"""Display — Monitor-/Output-Einstellungen, niri-only über native IPC. + +Pro Output: An/Aus, Auflösung+Refresh, Scale, Rotation. Alles live über +core.display (`niri msg output`), unprivilegiert. Bewusst KEINE Persistenz in +die niri-Config (config.kdl). +""" +from __future__ import annotations + +import gi + +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") +from gi.repository import Adw, Gtk # noqa: E402 + +from taninux.core import compositor, display +from taninux.gui.console import RunnerCard +from taninux.gui.process import ProcessRunner + +# gängige Scale-Stufen +_SCALES = [1.0, 1.25, 1.5, 1.75, 2.0] +_ROTATIONS = ["normal", "90", "180", "270"] + + +def build() -> Gtk.Widget: + page = Adw.PreferencesPage() + + if compositor.detect() == compositor.UNKNOWN: + group = Adw.PreferencesGroup(title="Display") + group.add(Adw.ActionRow( + title="Display settings aren't available in this session", + )) + page.add(group) + return page + + outs = display.outputs() + if not outs: + group = Adw.PreferencesGroup(title="Display") + group.add(Adw.ActionRow(title="No monitors detected")) + page.add(group) + return page + + # Änderungen sind sofort am Bildschirm sichtbar (nicht persistent) — kein Log. + runner = ProcessRunner() + + def apply(**kwargs) -> None: + out = kwargs.pop("out") + runner.run(display.apply_argv(out, **kwargs), privileged=False) + + for out in outs: + page.add(_output_group(out, apply)) + + page.add(_night_light_group()) + return page + + +def _output_group(out: display.Output, apply) -> Adw.PreferencesGroup: + desc = f"{out.make} {out.model}".strip() or None + group = Adw.PreferencesGroup(title=out.name, description=desc) + + # -- Enabled ----------------------------------------------------------- + enable_row = Adw.SwitchRow(title="Enabled") + enable_row.set_active(out.enabled) + enable_row.connect( + "notify::active", + lambda r, _p: apply(out=out, enabled=r.get_active()), + ) + group.add(enable_row) + + # -- Resolution -------------------------------------------------------- + # eindeutige (w,h,hz) absteigend sortiert; aktueller Modus vorwählen. + seen: set[tuple[int, int, float]] = set() + modes: list[tuple[int, int, float]] = [] + for m in out.modes: + key = (m[0], m[1], round(m[2], 2)) + if key not in seen: + seen.add(key) + modes.append(m) + cur_mode = (out.width, out.height, out.refresh_hz) + cur_key = (out.width, out.height, round(out.refresh_hz, 2)) + if cur_key not in seen and out.width: + modes.insert(0, cur_mode) + modes.sort(key=lambda m: (m[0], m[1], m[2]), reverse=True) + + if modes: + labels = [f"{w}x{h} @ {hz:.2f}Hz" for (w, h, hz) in modes] + res_row = Adw.ComboRow(title="Resolution") + res_row.set_model(Gtk.StringList.new(labels)) + sel = next( + (i for i, m in enumerate(modes) + if (m[0], m[1], round(m[2], 2)) == cur_key), + 0, + ) + res_row.set_selected(sel) + # connect NACH set_selected, damit Initial nicht feuert + res_row.connect( + "notify::selected", + lambda r, _p: apply(out=out, mode=modes[r.get_selected()]), + ) + group.add(res_row) + + # -- Scale ------------------------------------------------------------- + scales = list(_SCALES) + if not any(abs(s - out.scale) < 1e-3 for s in scales): + scales.append(out.scale) + scales.sort() + scale_row = Adw.ComboRow(title="Scale") + scale_row.set_model(Gtk.StringList.new([f"{s:g}" for s in scales])) + sidx = next( + (i for i, s in enumerate(scales) if abs(s - out.scale) < 1e-3), 0, + ) + scale_row.set_selected(sidx) + scale_row.connect( + "notify::selected", + lambda r, _p: apply(out=out, scale=scales[r.get_selected()]), + ) + group.add(scale_row) + + # -- Rotation ---------------------------------------------------------- + rot_row = Adw.ComboRow(title="Rotation") + rot_row.set_model(Gtk.StringList.new(_ROTATIONS)) + ridx = _ROTATIONS.index(out.transform) if out.transform in _ROTATIONS else 0 + rot_row.set_selected(ridx) + rot_row.connect( + "notify::selected", + lambda r, _p: apply(out=out, transform=_ROTATIONS[r.get_selected()]), + ) + group.add(rot_row) + + return group + + +def _night_light_group() -> Adw.PreferencesGroup: + group = Adw.PreferencesGroup( + title="Night light", + description="Warm the screen colours in the evening.", + ) + + if not display.wlsunset_installed(): + group.add(Adw.ActionRow(title="Install to enable night light")) + group.add(RunnerCard( + "Install", + ["pacman", "-S", "--needed", "--noconfirm", "wlsunset"], + privileged=True, + )) + return group + + nl = display.night_light() + + enable_row = Adw.SwitchRow(title="Night light") + enable_row.set_active(nl.enabled) + group.add(enable_row) + + night_row = Adw.SpinRow.new_with_range(2500, 6500, 100) + night_row.set_title("Night temperature") + night_row.set_subtitle("Kelvin") + night_row.set_value(nl.night_temp) + group.add(night_row) + + day_row = Adw.SpinRow.new_with_range(3000, 6500, 100) + day_row.set_title("Day temperature") + day_row.set_subtitle("Kelvin") + day_row.set_value(nl.day_temp) + group.add(day_row) + + lat_row = Adw.EntryRow(title="Latitude") + lat_row.set_text(f"{nl.lat:g}") + group.add(lat_row) + + lon_row = Adw.EntryRow(title="Longitude") + lon_row.set_text(f"{nl.lon:g}") + group.add(lon_row) + + status = Gtk.Label(xalign=0.0, wrap=True) + status.add_css_class("dim-label") + + def _parse_float(text: str, fallback: float) -> float: + try: + return float(text.strip().replace(",", ".")) + except (ValueError, AttributeError): + return fallback + + def _on_apply(_btn: Gtk.Button) -> None: + lat = _parse_float(lat_row.get_text(), nl.lat) + lon = _parse_float(lon_row.get_text(), nl.lon) + path = display.write_night_light( + day_temp=int(day_row.get_value()), + night_temp=int(night_row.get_value()), + lat=lat, + lon=lon, + ) + enabled = enable_row.get_active() + ok = display.apply_night_light(enabled) + state = "enabled" if enabled else "disabled" + if ok: + status.set_text(f"Night light {state}. Service unit: {path}") + else: + status.set_text(f"Wrote {path}, but systemctl --user failed.") + + apply_btn = Gtk.Button(label="Apply") + apply_btn.add_css_class("suggested-action") + apply_btn.add_css_class("pill") + apply_btn.set_halign(Gtk.Align.START) + apply_btn.set_margin_top(6) + apply_btn.connect("clicked", _on_apply) + + box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) + box.set_margin_top(6) + box.append(apply_btn) + box.append(status) + group.add(box) + + return group diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/firewall.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/firewall.py new file mode 100644 index 0000000..17df9bf --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/firewall.py @@ -0,0 +1,50 @@ +"""Firewall — An/Aus (ufw/firewalld via Helper).""" +from __future__ import annotations + +import gi + +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") +from gi.repository import Adw, Gtk # noqa: E402 + +from taninux.core import firewall +from taninux.gui.console import RunnerCard +from taninux.gui.process import ProcessRunner + + +def build() -> Gtk.Widget: + page = Adw.PreferencesPage() + group = Adw.PreferencesGroup(title="Firewall") + page.add(group) + + if not firewall.installed(): + group.add(Adw.ActionRow(title="Install to turn on the firewall")) + box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12) + box.set_margin_top(6) + box.append(RunnerCard("Install", firewall.install_argv(), privileged=True)) + group.add(box) + return page + + runner = ProcessRunner() + status = Gtk.Label(xalign=0.0) + status.add_css_class("dim-label") + + row = Adw.SwitchRow( + title="Firewall", + subtitle="Block unsolicited incoming connections", + ) + row.set_active(firewall.enabled()) + + def on_toggle(r, _p) -> None: + runner.run(firewall.set_enabled_argv(r.get_active()), privileged=True) + + row.connect("notify::active", on_toggle) + runner.connect("done", lambda _r, code: status.set_label( + "✔ Updated" if code == 0 else f"✘ failed (exit {code})")) + group.add(row) + + box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) + box.set_margin_top(6) + box.append(status) + group.add(box) + return page diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/hub_home.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/hub_home.py new file mode 100644 index 0000000..e4675f5 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/hub_home.py @@ -0,0 +1,130 @@ +"""Home — App-Store-Startseite im GNOME-Software-Stil. + +Kuratierte Endnutzer-Apps (core/catalog), nach Kategorie gruppiert, als +klickbare Icon-Tiles. Klick öffnet die Detailansicht (Info + Quellenwahl + +Install). Async geladen. +""" +from __future__ import annotations + +import threading + +import gi + +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") +from gi.repository import Adw, Gdk, GLib, Gtk # noqa: E402 + +from taninux.core import catalog, pacman +from taninux.gui.pages.appdetail import open_app_detail + + +def build() -> Gtk.Widget: + page = Adw.PreferencesPage() + groups: list[Adw.PreferencesGroup] = [] + + intro = Adw.PreferencesGroup( + title="Recommended apps", + description="Popular apps for everyday use — click for details.", + ) + refresh = Gtk.Button(icon_name="view-refresh-symbolic") + refresh.add_css_class("flat") + refresh.set_valign(Gtk.Align.CENTER) + intro.set_header_suffix(refresh) + page.add(intro) + + def _icon_name(app: catalog.App) -> str: + theme = Gtk.IconTheme.get_for_display(Gdk.Display.get_default()) + if theme and theme.has_icon(app.icon): + return app.icon + return catalog.CATEGORY_ICON.get(app.category, "system-software-install-symbolic") + + def _open(app: catalog.App) -> None: + open_app_detail( + page, + title=app.name, + icon=app.icon, + repo_name=app.pkg if app.source == "repo" else None, + flatpak_id=app.pkg if app.source == "flatpak" else None, + on_changed=_load, + ) + + def _tile(app: catalog.App) -> Gtk.Widget: + tile = Gtk.Button() + tile.add_css_class("card") + tile.connect("clicked", lambda _b, a=app: _open(a)) + + row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=14) + row.set_margin_top(14) + row.set_margin_bottom(14) + row.set_margin_start(14) + row.set_margin_end(14) + icon = Gtk.Image.new_from_icon_name(_icon_name(app)) + icon.set_pixel_size(48) + row.append(icon) + text = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2, hexpand=True) + text.set_valign(Gtk.Align.CENTER) + name = Gtk.Label(label=app.name, xalign=0.0) + name.add_css_class("title-4") + summary = Gtk.Label(label=app.summary, xalign=0.0, wrap=True) + summary.add_css_class("dim-label") + summary.add_css_class("caption") + summary.set_lines(2) + summary.set_ellipsize(3) + text.append(name) + text.append(summary) + row.append(text) + tile.set_child(row) + return tile + + def _category_group(category: str, apps: list[catalog.App]) -> Adw.PreferencesGroup: + group = Adw.PreferencesGroup(title=category) + flow = Gtk.FlowBox() + flow.set_selection_mode(Gtk.SelectionMode.NONE) + flow.set_max_children_per_line(2) + flow.set_min_children_per_line(1) + flow.set_column_spacing(12) + flow.set_row_spacing(12) + flow.set_homogeneous(True) + for app in apps: + flow.append(_tile(app)) + group.add(flow) + return group + + def _render(apps: list[catalog.App]) -> None: + for g in groups: + page.remove(g) + groups.clear() + if not apps: + g = Adw.PreferencesGroup() + g.add(Adw.StatusPage( + icon_name="emblem-ok-symbolic", + title="You're all set", + description="All recommended apps are already installed.", + )) + page.add(g) + groups.append(g) + return + by_cat: dict[str, list[catalog.App]] = {} + for app in apps: + by_cat.setdefault(app.category, []).append(app) + for cat in catalog.CATEGORY_ORDER: + if cat in by_cat: + g = _category_group(cat, by_cat[cat]) + page.add(g) + groups.append(g) + + def _load() -> None: + def worker() -> None: + try: + pac = set(pacman.installed_packages()) + fp = catalog._flatpak_installed_ids() + apps = catalog.recommended(pac, fp) + except Exception: # noqa: BLE001 + apps = [] + GLib.idle_add(lambda: (_render(apps), False)[1]) + + threading.Thread(target=worker, daemon=True).start() + + refresh.connect("clicked", lambda _b: (pacman.invalidate(), _load())) + GLib.idle_add(lambda: (_load(), False)[1]) + return page diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/kernel.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/kernel.py new file mode 100644 index 0000000..1c9fcf9 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/kernel.py @@ -0,0 +1,138 @@ +"""Kernel — installierte Kernel, installierbare Alternativen, Bootloader.""" +from __future__ import annotations + +import gi + +gi.require_version("Adw", "1") +from gi.repository import Adw, GLib, Gtk # noqa: E402 + +from taninux.core import bootloader, pacman, sysinfo +from taninux.gui.console import LogConsole, log_expander +from taninux.gui.process import ProcessRunner + +# Paketname → Anzeigename +KNOWN_KERNELS: dict[str, str] = { + "linux": "Linux (stable)", + "linux-lts": "Linux LTS (longterm)", + "linux-zen": "Linux Zen (desktop-tuned)", + "linux-hardened": "Linux Hardened (security)", + "linux-rt": "Linux RT (realtime)", + "linux-rt-lts": "Linux RT LTS (realtime longterm)", +} + + +def _is_running(pkg: str, running: str) -> bool: + suffix = pkg.removeprefix("linux").lstrip("-") + tag = "-".join(running.split("-")[1:]) if "-" in running else "" + return (pkg == "linux" and not tag) or (bool(suffix) and suffix in running) + + +def build() -> Gtk.Widget: + page = Adw.PreferencesPage() + console = LogConsole() + runner = ProcessRunner() + runner.connect("line", lambda _r, line: console.append_line(line)) + groups: list[Adw.PreferencesGroup] = [] + log_group = log_expander(console) + + def _install(pkg: str, btn: Gtk.Button) -> None: + btn.set_sensitive(False) + btn.set_label("Installing…") + console.clear() + runner.run(["pacman", "-S", "--needed", "--noconfirm", pkg, f"{pkg}-headers"], + privileged=True) + + def _set_default(bl, combo: Adw.ComboRow, btn: Gtk.Button) -> None: + idx = combo.get_selected() + if not (0 <= idx < len(bl.entries)): + return + argv = bootloader.set_default_argv(bl, bl.entries[idx]) + if not argv: + console.append_line("⚠ not supported for this bootloader") + return + btn.set_sensitive(False) + console.clear() + runner.run(argv, privileged=True) + + def _render() -> None: + for g in groups: + page.remove(g) + groups.clear() + page.remove(log_group) + + installed = pacman.installed_packages() + running = sysinfo.kernel_release() + + g_inst = Adw.PreferencesGroup(title="Installed kernels", + description=f"Running: {running}") + any_inst = False + for pkg, label in KNOWN_KERNELS.items(): + if pkg in installed: + any_inst = True + row = Adw.ActionRow(title=label, subtitle=pkg) + if _is_running(pkg, running): + badge = Gtk.Label(label="active") + badge.add_css_class("success") + badge.add_css_class("caption-heading") + badge.set_valign(Gtk.Align.CENTER) + row.add_suffix(badge) + g_inst.add(row) + if not any_inst: + g_inst.add(Adw.ActionRow(title="No known kernel packages found")) + page.add(g_inst) + groups.append(g_inst) + + available = [(p, l) for p, l in KNOWN_KERNELS.items() if p not in installed] + if available: + g_avail = Adw.PreferencesGroup(title="Available to install") + for pkg, label in available: + row = Adw.ActionRow(title=label, subtitle=f"{pkg} · {pkg}-headers") + btn = Gtk.Button(label="Install") + btn.add_css_class("suggested-action") + btn.set_valign(Gtk.Align.CENTER) + btn.connect("clicked", lambda _b, p=pkg: _install(p, _b)) + row.add_suffix(btn) + g_avail.add(row) + page.add(g_avail) + groups.append(g_avail) + + bl = bootloader.detect() + g_boot = Adw.PreferencesGroup(title="Bootloader") + g_boot.add(_static_row("Type", bl.kind)) + g_boot.add(_static_row("Default entry", bl.default_entry or "—")) + if bl.kind == "systemd-boot" and bl.entries: + combo = Adw.ComboRow(title="Default boot entry", + model=Gtk.StringList.new([e.name for e in bl.entries])) + for i, e in enumerate(bl.entries): + if bootloader.is_default(bl, e): + combo.set_selected(i) + break + btn = Gtk.Button(label="Set") + btn.add_css_class("suggested-action") + btn.set_valign(Gtk.Align.CENTER) + btn.connect("clicked", lambda _b, c=combo: _set_default(bl, c, _b)) + combo.add_suffix(btn) + g_boot.add(combo) + page.add(g_boot) + groups.append(g_boot) + + page.add(log_group) + + def on_done(_r: ProcessRunner, code: int) -> None: + console.append_line("✔ done" if code == 0 else f"✘ exit {code}") + pacman.invalidate() + _render() + + runner.connect("done", on_done) + page.add(log_group) + GLib.idle_add(lambda: (_render(), False)[1]) + return page + + +def _static_row(title: str, value: str) -> Adw.ActionRow: + row = Adw.ActionRow(title=title) + label = Gtk.Label(label=value) + label.add_css_class("dim-label") + label.set_selectable(True) + row.add_suffix(label) + return row diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/locale.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/locale.py new file mode 100644 index 0000000..0de5c85 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/locale.py @@ -0,0 +1,247 @@ +"""Region & Input — System-Locale (Sprache) + Tastatur-Layout via localectl. + +Lese-Werte kommen aus core.localization (unprivilegiert). Das Setzen läuft +privilegiert über den Helper (localectl steht in dessen Allowlist). + +Ehrlicher Hinweis: niri nimmt sein aktives Tastaturlayout aus der EIGENEN +Config (config.kdl, input/keyboard). `localectl set-x11-keymap` setzt das +System-Default (Konsole, X11-Apps, XKB) — nicht zwingend das Live-Layout in +niri. Die Konsolen-Keymap (`set-keymap`) und das Locale gelten dagegen voll +system-weit. +""" +from __future__ import annotations + +import gi + +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") +from gi.repository import Adw, Gtk # noqa: E402 + +from taninux.core import localization +from taninux.gui import confirm +from taninux.gui.process import ProcessRunner + + +def build() -> Gtk.Widget: + page = Adw.PreferencesPage() + page.add(_language_group()) + page.add(_datetime_group()) + page.add(_keyboard_group()) + return page + + +# -- gemeinsamer Button+Konsole-Block (argv erst zur Klickzeit bekannt) ----- + +def _action_block(button_label: str): + """Liefert (box, button, runner, status). Caller verdrahtet button 'clicked'.""" + runner = ProcessRunner() + + button = Gtk.Button(label=button_label) + button.add_css_class("suggested-action") + button.add_css_class("pill") + button.set_halign(Gtk.Align.START) + + status = Gtk.Label(xalign=0.0, wrap=True) + status.add_css_class("dim-label") + + def on_done(_r: ProcessRunner, code: int) -> None: + status.set_label("✔ Applied" if code == 0 else f"✘ failed (exit {code})") + button.set_sensitive(True) + + runner.connect("done", on_done) + + box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8) + box.set_margin_top(6) + box.append(button) + box.append(status) + return box, button, runner, status + + +# -- Sprache -------------------------------------------------------------- + +def _language_group() -> Adw.PreferencesGroup: + st = localization.status() + locales = localization.list_locales() + + group = Adw.PreferencesGroup( + title="Language", + description="Applies on next login.", + ) + + combo = Adw.ComboRow(title="System language") + combo.set_model(Gtk.StringList.new(locales or ["(none generated)"])) + combo.set_enable_search(True) + if st.locale in locales: + combo.set_selected(locales.index(st.locale)) + group.add(combo) + + box, button, runner, _status = _action_block("Apply language") + + def on_click(_b: Gtk.Button) -> None: + idx = combo.get_selected() + if not locales or idx < 0 or idx >= len(locales): + return + locale = locales[idx] + + def do_run() -> None: + button.set_sensitive(False) + runner.run(localization.set_locale_argv(locale), privileged=True) + + confirm.confirm( + button, + "Change system language?", + f"Sets LANG={locale}. Applies on next login.", + do_run, + ) + + button.connect("clicked", on_click) + group.add(box) + return group + + +# -- Datum & Zeit --------------------------------------------------------- + +def _datetime_group() -> Adw.PreferencesGroup: + timezones = localization.list_timezones() + cur_tz = localization.current_timezone() + + group = Adw.PreferencesGroup(title="Date and Time") + + # Aktuelle lokale Zeit (read-only). + cur = Adw.ActionRow(title="Current") + cur_label = Gtk.Label(label=localization.local_time() or "—") + cur_label.add_css_class("dim-label") + cur_label.set_selectable(True) + cur.add_suffix(cur_label) + group.add(cur) + + # Zeitzone wählen + privilegiert anwenden. + tz_combo = Adw.ComboRow(title="Time zone") + tz_combo.set_model(Gtk.StringList.new(timezones or ["(none)"])) + tz_combo.set_enable_search(True) + if cur_tz in timezones: + tz_combo.set_selected(timezones.index(cur_tz)) + group.add(tz_combo) + + box, button, runner, status = _action_block("Apply time zone") + + def on_click(_b: Gtk.Button) -> None: + idx = tz_combo.get_selected() + if not timezones or idx < 0 or idx >= len(timezones): + return + tz = timezones[idx] + + def do_run() -> None: + button.set_sensitive(False) + runner.run(localization.set_timezone_argv(tz), privileged=True) + + confirm.confirm( + button, + "Change system time zone?", + f"Sets the system time zone to {tz}.", + do_run, + ) + + button.connect("clicked", on_click) + + # Automatische Zeit (NTP) — meldet in dieselbe Status-Zeile. + ntp_runner = ProcessRunner() + ntp_runner.connect( + "done", + lambda _r, code: status.set_label("✔ Applied" if code == 0 else f"✘ exit {code}"), + ) + + ntp_row = Adw.SwitchRow( + title="Automatic time (NTP)", + subtitle="Synchronize the clock over the network.", + ) + ntp_row.set_active(localization.ntp_enabled()) + + def on_ntp(_row: Adw.SwitchRow, _pspec) -> None: + ntp_runner.run(localization.set_ntp_argv(ntp_row.get_active()), privileged=True) + + ntp_row.connect("notify::active", on_ntp) + group.add(ntp_row) + + group.add(box) + return group + + +# -- Tastatur ------------------------------------------------------------- + +def _keyboard_group() -> Adw.PreferencesGroup: + st = localization.status() + layouts = localization.list_x11_layouts() + + group = Adw.PreferencesGroup(title="Keyboard") + + # Ist-Zustand sichtbar machen (gerade weil niri davon abweichen kann). + cur = Adw.ActionRow(title="Current (system)") + cur_val = f"{st.x11_layout or '—'}" + if st.x11_variant: + cur_val += f" · {st.x11_variant}" + cur_val += f" · console: {st.vc_keymap or '—'}" + cur_label = Gtk.Label(label=cur_val) + cur_label.add_css_class("dim-label") + cur_label.set_selectable(True) + cur.add_suffix(cur_label) + group.add(cur) + + layout_combo = Adw.ComboRow(title="Layout") + layout_combo.set_model(Gtk.StringList.new(layouts or ["(none)"])) + layout_combo.set_enable_search(True) + if st.x11_layout in layouts: + layout_combo.set_selected(layouts.index(st.x11_layout)) + group.add(layout_combo) + + variant_combo = Adw.ComboRow(title="Variant") + group.add(variant_combo) + + def _selected_layout() -> str: + idx = layout_combo.get_selected() + return layouts[idx] if layouts and 0 <= idx < len(layouts) else "" + + variants_state: dict[str, list[str]] = {"list": [""]} + + def _reload_variants(*_a) -> None: + layout = _selected_layout() + variants = localization.list_x11_variants(layout) if layout else [""] + variants_state["list"] = variants + # "" als "default" anzeigen + labels = ["default" if v == "" else v for v in variants] + variant_combo.set_model(Gtk.StringList.new(labels)) + # aktuelle Variante vorwählen, falls sie zum Layout passt + if st.x11_variant in variants: + variant_combo.set_selected(variants.index(st.x11_variant)) + else: + variant_combo.set_selected(0) + + layout_combo.connect("notify::selected", _reload_variants) + _reload_variants() + + box, button, runner, _status = _action_block("Apply layout") + + def on_click(_b: Gtk.Button) -> None: + layout = _selected_layout() + if not layout: + return + vidx = variant_combo.get_selected() + variants = variants_state["list"] + variant = variants[vidx] if 0 <= vidx < len(variants) else "" + + def do_run() -> None: + button.set_sensitive(False) + runner.run( + localization.set_x11_keymap_argv(layout, variant), privileged=True + ) + + confirm.confirm( + button, + "Change keyboard layout?", + f"Sets the system XKB layout to {layout}.", + do_run, + ) + + button.connect("clicked", on_click) + group.add(box) + return group diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/lock.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/lock.py new file mode 100644 index 0000000..d1bdc16 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/lock.py @@ -0,0 +1,39 @@ +"""Screen lock — jetzt sperren + Locker installieren.""" +from __future__ import annotations + +import gi + +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") +from gi.repository import Adw, Gtk # noqa: E402 + +from taninux.core import lock +from taninux.gui.console import RunnerCard +from taninux.gui.process import ProcessRunner + + +def build() -> Gtk.Widget: + page = Adw.PreferencesPage() + group = Adw.PreferencesGroup(title="Screen lock") + page.add(group) + + if not lock.locker(): + group.add(Adw.ActionRow(title="Install to enable screen locking")) + box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12) + box.set_margin_top(6) + box.append(RunnerCard("Install", lock.install_argv(), privileged=True)) + group.add(box) + return page + + runner = ProcessRunner() + row = Adw.ActionRow( + title="Lock now", + subtitle="Auto-lock timing is set under Power.", + ) + btn = Gtk.Button(label="Lock") + btn.add_css_class("pill") + btn.set_valign(Gtk.Align.CENTER) + btn.connect("clicked", lambda _b: runner.run(lock.lock_now_argv() or [])) + row.add_suffix(btn) + group.add(row) + return page diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/maintain.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/maintain.py new file mode 100644 index 0000000..dc8e625 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/maintain.py @@ -0,0 +1,394 @@ +"""Cleanup — System aufräumen über die Maintain-Scanner. + +Portiert die UI-freien Scanner (taninux.maintain) in eine libadwaita-Seite. +Scannen läuft async im Worker-Thread; Findings werden per GLib.idle_add +gerendert. Destruktive Aktionen gehen ausschliesslich über core.trash +(move-to-trash, kein echtes rm) — das Sicherheitsnetz der App. + +trash-Scanner (dotfolders, fp-data, aur-cache): User-Dateien → Trash/Keep. +view-Scanner (orphans, cache, …): nur anzeigen + Hinweis-Befehl zum Kopieren. +""" +from __future__ import annotations + +import threading + +import gi + +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") +from gi.repository import Adw, Gdk, GLib, Gtk # noqa: E402 + +from taninux.core import trash +from taninux.core.state import State +from taninux.gui import confirm +from taninux.gui.console import LogConsole, log_expander +from taninux.gui.process import ProcessRunner +from taninux.maintain.scanners import SCANNERS, ScannerSpec +from taninux.maintain.types import Classification, Finding + +# Klassifizierung → CSS-Klasse für den farbigen Punkt + englisches Label. +_DOT_CLASS = { + Classification.SAFE: "success", + Classification.LIKELY_ORPHAN: "warning", + Classification.UNKNOWN: "dim-label", + Classification.ACTIVE: "dim-label", + Classification.WHITELISTED: "accent", +} +_EN_LABEL = { + Classification.SAFE: "safe", + Classification.LIKELY_ORPHAN: "likely orphan", + Classification.UNKNOWN: "unknown", + Classification.WHITELISTED: "kept", + Classification.ACTIVE: "in use", +} +_RECLAIMABLE = (Classification.SAFE, Classification.LIKELY_ORPHAN) +_MAX_ROWS = 50 # pro Scanner, gegen UI-Overload + + +def _clean_argv(spec_id: str, findings: list[Finding]): + """Sicheres One-Click-Clean für view-Scanner → (argv, privileged) | None. + + Nur ungefährliche, nicht-interaktive Befehle. pacnew (interaktiver Merge) + und kernels (rm, gefährlich) bleiben bewusst Hinweis-only. + """ + if spec_id == "cache": + return ["paccache", "-ruk0"], True + if spec_id == "journal": + return ["journalctl", "--vacuum-time=2weeks"], True + if spec_id == "flatpak-runtimes": + return ["flatpak", "uninstall", "--unused", "--assumeyes"], False + if spec_id == "orphans": + pkgs = [f.matched_package or f.path.name for f in findings] + pkgs = [p for p in pkgs if p] + return (["pacman", "-Rns", "--noconfirm", *pkgs], True) if pkgs else None + return None + + +def _human(n: int) -> str: + """Bytes menschlich (B/KiB/MiB/GiB).""" + size = float(n) + for unit in ("B", "KiB", "MiB", "GiB", "TiB"): + if size < 1024 or unit == "TiB": + if unit == "B": + return f"{int(size)} {unit}" + return f"{size:.1f} {unit}" + size /= 1024 + return f"{n} B" # unerreichbar + + +def _dot(cls: Classification) -> Gtk.Label: + dot = Gtk.Label(label="●") + dot.add_css_class(_DOT_CLASS.get(cls, "dim-label")) + dot.set_valign(Gtk.Align.CENTER) + return dot + + +def _legend() -> Adw.ActionRow: + """Erklärt die farbigen Punkte.""" + row = Adw.ActionRow(title="Legend") + box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=14) + box.set_valign(Gtk.Align.CENTER) + for cls in (Classification.SAFE, Classification.LIKELY_ORPHAN, + Classification.UNKNOWN): + item = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5) + item.append(_dot(cls)) + lbl = Gtk.Label(label=_EN_LABEL[cls]) + lbl.add_css_class("dim-label") + lbl.add_css_class("caption") + item.append(lbl) + box.append(item) + row.add_suffix(box) + return row + + +def _flat_btn(label: str, cb) -> Gtk.Button: + btn = Gtk.Button(label=label) + btn.add_css_class("flat") + btn.set_valign(Gtk.Align.CENTER) + btn.connect("clicked", lambda _b: cb()) + return btn + + +def _fill_advice(advice: str, first: Finding | None) -> str: + """Platzhalter {pkg}/{path} mit dem ersten Finding füllen.""" + if first is None: + return advice + pkg = first.matched_package or first.path.name + return advice.replace("{pkg}", pkg).replace("{path}", str(first.path)) + + +def build() -> Gtk.Widget: + page = Adw.PreferencesPage() + + # -- Summary (oben) ----------------------------------------------- + g_summary = Adw.PreferencesGroup(title="Cleanup") + rescan_btn = Gtk.Button(icon_name="view-refresh-symbolic") + rescan_btn.add_css_class("flat") + rescan_btn.set_valign(Gtk.Align.CENTER) + rescan_btn.set_tooltip_text("Rescan") + g_summary.set_header_suffix(rescan_btn) + + summary_row = Adw.ActionRow(title="Reclaimable", subtitle="Scanning …") + summary_spin = Adw.Spinner() + summary_spin.set_valign(Gtk.Align.CENTER) + summary_row.add_prefix(summary_spin) + g_summary.add(summary_row) + g_summary.add(_legend()) + page.add(g_summary) + + console = LogConsole() + runner = ProcessRunner() + runner.connect("line", lambda _r, ln: console.append_line(ln)) + + # Dynamische Scanner-Gruppen (zwischen Summary und Trash) --------- + scanner_groups: list[Gtk.Widget] = [] + + # -- Trash-Gruppe (unten) ----------------------------------------- + g_trash = Adw.PreferencesGroup( + title="Trash", + description="Files moved here are recoverable, nothing is deleted.", + ) + trash_rows: list[Gtk.Widget] = [] + page.add(g_trash) + + # -- Log-Gruppe (ganz unten, für Clean-Befehle) ------------------- + g_log = log_expander(console) + page.add(g_log) + + def _clean_run(argv: list[str], priv: bool, label: str) -> None: + def go() -> None: + console.clear() + runner.run(argv, privileged=priv) + confirm.confirm(rescan_btn, f"Clean {label}", + "Run: " + " ".join(argv), go) + + # ---------------------------------------------------------------- + def _clipboard_set(text: str) -> None: + disp = Gdk.Display.get_default() + if disp is not None: + disp.get_clipboard().set(text) + + def _clear_scanner_groups() -> None: + for grp in scanner_groups: + page.remove(grp) + scanner_groups.clear() + + # -- Trash-Liste -------------------------------------------------- + def _refresh_trash() -> None: + for r in trash_rows: + g_trash.remove(r) + trash_rows.clear() + + entries = trash.list_trash() + if not entries: + empty = Adw.ActionRow( + title="Trash is empty", + subtitle="Trashed items will appear here.", + ) + g_trash.add(empty) + trash_rows.append(empty) + return + + for entry in entries: + row = Adw.ActionRow( + title=str(entry.original), + subtitle=f"{_human(entry.size_bytes)} · {entry.trashed_at}", + ) + + def _restore(eid: str = entry.id) -> None: + try: + trash.restore(eid) + except (FileExistsError, FileNotFoundError, OSError): + pass + _refresh_trash() + + def _purge(eid: str = entry.id, orig: str = str(entry.original)) -> None: + def do() -> None: + try: + trash.purge(eid) + except (FileNotFoundError, OSError): + pass + _refresh_trash() + confirm.confirm( + rescan_btn, + "Purge permanently", + f"Delete {orig} for good. This cannot be undone.", + do, + ) + + row.add_suffix(_flat_btn("Restore", _restore)) + purge_b = _flat_btn("Purge", _purge) + purge_b.add_css_class("destructive-action") + row.add_suffix(purge_b) + g_trash.add(row) + trash_rows.append(row) + + # -- eine Scanner-Gruppe rendern ---------------------------------- + def _build_group(spec: ScannerSpec, findings: list[Finding]) -> Gtk.Widget: + findings = sorted(findings, key=lambda f: f.sort_key) + reclaim = sum(f.size_bytes for f in findings + if f.classification in _RECLAIMABLE) + shown = findings[:_MAX_ROWS] + hidden = len(findings) - len(shown) + + desc = f"{len(findings)} findings · {_human(reclaim)} reclaimable" + if hidden > 0: + desc += f" · showing first {_MAX_ROWS}" + if spec.actions == "view" and spec.advice: + desc += "\n" + _fill_advice(spec.advice, findings[0] if findings else None) + + grp = Adw.PreferencesGroup(title=spec.label, description=desc) + + # view-Scanner: Header mit Copy-Button + (falls sicher) Clean-Button. + if spec.actions == "view": + hdr = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) + if spec.advice: + cmd = _fill_advice(spec.advice, findings[0] if findings else None) + copy = Gtk.Button(icon_name="edit-copy-symbolic") + copy.add_css_class("flat") + copy.set_valign(Gtk.Align.CENTER) + copy.set_tooltip_text("Copy command") + copy.connect("clicked", lambda _b, c=cmd: _clipboard_set(c)) + hdr.append(copy) + clean = _clean_argv(spec.id, findings) + if clean is not None: + argv, priv = clean + cb = Gtk.Button(label="Clean") + cb.add_css_class("destructive-action") + cb.set_valign(Gtk.Align.CENTER) + cb.connect("clicked", + lambda _b, a=argv, p=priv, lb=spec.label: _clean_run(a, p, lb)) + hdr.append(cb) + if hdr.get_first_child() is not None: + grp.set_header_suffix(hdr) + + # Findings in eine scrollbare ListBox: ~5 Zeilen sichtbar, dann scrollen. + listbox = Gtk.ListBox() + listbox.add_css_class("boxed-list") + listbox.set_selection_mode(Gtk.SelectionMode.NONE) + + for finding in shown: + cls = finding.classification + title = finding.matched_package or str(finding.path) + sub = f"{_EN_LABEL.get(cls, cls.value)} · {_human(finding.size_bytes)}" + if finding.reason: + sub += f" · {finding.reason}" + + row = Adw.ActionRow(title=title, subtitle=sub) + row.add_prefix(_dot(cls)) + + if spec.actions == "trash": + def _do_trash(f: Finding = finding, r: Adw.ActionRow = row, + lb: Gtk.ListBox = listbox) -> None: + def confirmed() -> None: + def worker() -> None: + ok = True + try: + trash.move_to_trash(f.path) + except (FileNotFoundError, OSError): + ok = False + GLib.idle_add(lambda: (_after_trash(r, lb, ok), False)[1]) + threading.Thread(target=worker, daemon=True).start() + confirm.confirm( + rescan_btn, + "Move to Trash", + f"Move {f.path} to the recoverable Trash.", + confirmed, + ) + + def _do_keep(f: Finding = finding, r: Adw.ActionRow = row, + lb: Gtk.ListBox = listbox) -> None: + State.load().whitelist_add(f.path) + lb.remove(r) + + row.add_suffix(_flat_btn("Keep", _do_keep)) + trash_b = _flat_btn("Trash", _do_trash) + trash_b.add_css_class("destructive-action") + row.add_suffix(trash_b) + + listbox.append(row) + + if hidden > 0: + more = Adw.ActionRow(title=f"… +{hidden} more") + more.add_css_class("dim-label") + listbox.append(more) + + visible = min(len(shown) + (1 if hidden else 0), 5) + h = visible * 56 + scroller = Gtk.ScrolledWindow() + scroller.set_min_content_height(h) + scroller.set_max_content_height(h) + scroller.set_child(listbox) + grp.add(scroller) + return grp + + def _after_trash(row: Adw.ActionRow, listbox: Gtk.ListBox, ok: bool) -> None: + if ok: + listbox.remove(row) + _refresh_trash() + + # -- Scan async --------------------------------------------------- + def _render(results: list[tuple[ScannerSpec, list[Finding]]]) -> None: + _clear_scanner_groups() + summary_spin.set_visible(False) + + # Trash + Log temporär entfernen, damit Scanner-Gruppen davor landen. + page.remove(g_trash) + page.remove(g_log) + total = 0 + for spec, findings in results: + if not findings: + continue + total += sum(f.size_bytes for f in findings + if f.classification in _RECLAIMABLE) + grp = _build_group(spec, findings) + page.add(grp) + scanner_groups.append(grp) + + page.add(g_trash) + page.add(g_log) + summary_row.set_subtitle(f"{_human(total)} can be reclaimed safely") + + def _scan() -> None: + _clear_scanner_groups() + summary_spin.set_visible(True) + summary_row.set_subtitle("Scanning …") + + def worker() -> None: + # State einmal laden, um whitelisted/ignored Findings auszublenden. + try: + state = State.load() + except Exception: # noqa: BLE001 — UI darf nie crashen + state = None + results: list[tuple[ScannerSpec, list[Finding]]] = [] + for spec in SCANNERS: + try: + findings = spec.factory().scan() + except Exception: # noqa: BLE001 — ein Scanner darf nicht alles killen + findings = [] + if state is not None: + try: + findings = [ + f for f in findings + if not state.is_whitelisted(f.path) + and not state.is_ignored(f.path) + ] + except OSError: + pass + results.append((spec, findings)) + GLib.idle_add(lambda: (_render(results), False)[1]) + + threading.Thread(target=worker, daemon=True).start() + + rescan_btn.connect("clicked", lambda _b: _scan()) + + def _on_clean_done(_r: ProcessRunner, code: int) -> None: + console.append_line("✔ done" if code == 0 else f"✘ exit {code}") + _refresh_trash() + _scan() + + runner.connect("done", _on_clean_done) + + _refresh_trash() + GLib.idle_add(lambda: (_scan(), False)[1]) + return page diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/network.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/network.py new file mode 100644 index 0000000..0070ca2 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/network.py @@ -0,0 +1,194 @@ +"""Network — Wi-Fi über core.network (nmcli). + +Radio an/aus, Status, verfügbare Netze (async geladen) mit Verbinden inkl. +Passwort-Dialog für gesicherte Netze. Alles User-Level (NetworkManager-polkit), +kein root. Live-Output im Connection-Log. +""" +from __future__ import annotations + +import threading + +import gi + +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") +from gi.repository import Adw, GLib, Gtk # noqa: E402 + +from taninux.core import network +from taninux.gui import confirm +from taninux.gui.console import LogConsole, log_expander +from taninux.gui.process import ProcessRunner + + +def _signal_icon(sig: int) -> str: + if sig >= 75: + return "network-wireless-signal-excellent-symbolic" + if sig >= 50: + return "network-wireless-signal-good-symbolic" + if sig >= 25: + return "network-wireless-signal-ok-symbolic" + return "network-wireless-signal-weak-symbolic" + + +def build() -> Gtk.Widget: + page = Adw.PreferencesPage() + + if not network.available(): + g = Adw.PreferencesGroup(title="Network") + g.add(Adw.ActionRow( + title="NetworkManager not available", + subtitle="nmcli found no running NetworkManager.", + )) + page.add(g) + return page + + console = LogConsole() + runner = ProcessRunner() + runner.connect("line", lambda _r, ln: console.append_line(ln)) + + # -- Wi-Fi radio + Status ----------------------------------------- + g_wifi = Adw.PreferencesGroup(title="Wi-Fi") + wifi_sw = Adw.SwitchRow(title="Wi-Fi", subtitle="Enable the wireless radio") + wifi_sw.set_active(network.wifi_enabled()) + status_row = Adw.ActionRow(title="Status") + status_lbl = Gtk.Label(label=network.active_summary()) + status_lbl.add_css_class("dim-label") + status_lbl.set_selectable(True) + status_row.add_suffix(status_lbl) + + def on_wifi(sw: Adw.SwitchRow, _p) -> None: + runner.run(network.set_wifi_argv(sw.get_active())) + + wifi_sw.connect("notify::active", on_wifi) + g_wifi.add(wifi_sw) + g_wifi.add(status_row) + page.add(g_wifi) + + # -- Verfügbare Netze (async) ------------------------------------- + g_nets = Adw.PreferencesGroup(title="Available networks") + refresh = Gtk.Button(icon_name="view-refresh-symbolic") + refresh.add_css_class("flat") + refresh.set_valign(Gtk.Align.CENTER) + refresh.set_tooltip_text("Rescan") + g_nets.set_header_suffix(refresh) + page.add(g_nets) + + rows: list[Gtk.Widget] = [] + + def _clear() -> None: + for r in rows: + g_nets.remove(r) + rows.clear() + + def _connect(net: network.WifiNet, saved: bool) -> None: + def do(pw: str) -> None: + console.clear() + runner.run(network.connect_argv(net.ssid, pw)) + # Gesichert, aber schon gespeichert -> ohne Passwort-Dialog verbinden. + if net.security and not saved: + _ask_password(refresh, net.ssid, do) + else: + do("") + + def _disconnect() -> None: + argv = network.disconnect_argv() + if not argv: + return + console.clear() + runner.run(argv) + + def _forget(net: network.WifiNet) -> None: + def do() -> None: + console.clear() + runner.run(network.forget_argv(net.ssid)) + confirm.confirm( + refresh, + f"Forget {net.ssid}", + "Delete this saved Wi-Fi connection.", + do, + ) + + def _btn(label: str, cb) -> Gtk.Button: + btn = Gtk.Button(label=label) + btn.add_css_class("flat") + btn.set_valign(Gtk.Align.CENTER) + btn.connect("clicked", lambda _b: cb()) + return btn + + def _render(nets: list[network.WifiNet], saved: set[str]) -> None: + _clear() + for net in nets: + row = Adw.ActionRow(title=net.ssid, + subtitle=net.security or "open") + row.add_prefix(Gtk.Image.new_from_icon_name(_signal_icon(net.signal))) + if net.security: + lock = Gtk.Image.new_from_icon_name("network-wireless-encrypted-symbolic") + lock.add_css_class("dim-label") + row.add_suffix(lock) + is_saved = net.ssid in saved + if net.in_use: + tag = Gtk.Label(label="connected") + tag.add_css_class("success") + tag.add_css_class("caption-heading") + row.add_suffix(tag) + row.add_suffix(_btn("Disconnect", _disconnect)) + else: + row.add_suffix(_btn("Connect", + lambda n=net, s=is_saved: _connect(n, s))) + if is_saved: + row.add_suffix(_btn("Forget", lambda n=net: _forget(n))) + g_nets.add(row) + rows.append(row) + + def _load(rescan: bool = False) -> None: + _clear() + spin_row = Adw.ActionRow(title="Scanning …" if rescan else "Loading …") + sp = Adw.Spinner() + sp.set_valign(Gtk.Align.CENTER) + spin_row.add_prefix(sp) + g_nets.add(spin_row) + rows.append(spin_row) + status_lbl.set_label(network.active_summary()) + + def worker() -> None: + nets = network.wifi_networks(rescan=rescan) + saved = network.saved_connections() + GLib.idle_add(lambda: (_render(nets, saved), False)[1]) + + threading.Thread(target=worker, daemon=True).start() + + refresh.connect("clicked", lambda _b: _load(rescan=True)) + GLib.idle_add(lambda: (_load(rescan=False), False)[1]) + + # -- Connection log ----------------------------------------------- + def on_done(_r: ProcessRunner, code: int) -> None: + status_lbl.set_label(network.active_summary()) + console.append_line("✔ done" if code == 0 else f"✘ exit {code}") + # Liste + Status nach jeder Aktion neu laden. + _load(rescan=False) + + runner.connect("done", on_done) + page.add(log_expander(console, title="Connection log")) + return page + + +def _ask_password(parent: Gtk.Widget, ssid: str, on_ok) -> None: + dialog = Adw.AlertDialog( + heading=f"Connect to {ssid}", + body="Enter the Wi-Fi password.", + ) + entry = Gtk.PasswordEntry(show_peek_icon=True) + entry.set_margin_top(8) + dialog.set_extra_child(entry) + dialog.add_response("cancel", "Cancel") + dialog.add_response("connect", "Connect") + dialog.set_response_appearance("connect", Adw.ResponseAppearance.SUGGESTED) + dialog.set_default_response("connect") + dialog.set_close_response("cancel") + + def on_resp(_d: Adw.AlertDialog, resp: str) -> None: + if resp == "connect": + on_ok(entry.get_text()) + + dialog.connect("response", on_resp) + dialog.present(parent) diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/panel.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/panel.py new file mode 100644 index 0000000..0a31697 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/panel.py @@ -0,0 +1,199 @@ +"""Panel & Dock — steuert die eww-Bar + das Dock über panel.json + dock-pins. + +Schreibt ausschließlich Config-Dateien (core.panel); eww zieht live nach. Fasst +~/eww NIE direkt an. Vertrag: docs/eww-panel-dock-integration.md. +""" +from __future__ import annotations + +import gi + +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") +from gi.repository import Adw, Gtk # noqa: E402 + +from taninux.core import panel + +_MODULE_LABELS = { + "music": "Now playing", + "sys": "System (CPU / RAM)", + "updates": "Updates", + "net": "Network", + "bt": "Bluetooth", + "vol": "Volume", +} + + +def build() -> Gtk.Widget: + page = Adw.PreferencesPage() + cfg = panel.load() + state = {"ready": False} + + def persist() -> None: + if state["ready"]: + panel.save(cfg) + + page.add(_topbar_group(cfg, persist)) + page.add(_dock_group(cfg, persist)) + page.add(_pins_group()) + + state["ready"] = True + return page + + +# -- Top bar -------------------------------------------------------------- + +def _topbar_group(cfg: dict, persist) -> Adw.PreferencesGroup: + group = Adw.PreferencesGroup( + title="Top bar", + description="Which modules the eww bar shows, and the clock format.", + ) + + clock = Adw.EntryRow(title="Clock format") + clock.set_text(cfg["bar"]["clock_format"]) + clock.set_show_apply_button(True) + + def on_clock(_e: Adw.EntryRow) -> None: + cfg["bar"]["clock_format"] = clock.get_text() + persist() + + clock.connect("apply", on_clock) + group.add(clock) + + mods = cfg["bar"]["modules"] + for key in panel.MODULES: + row = Adw.SwitchRow(title=_MODULE_LABELS.get(key, key)) + row.set_active(bool(mods.get(key, True))) + + def on_toggle(r: Adw.SwitchRow, _p, k: str = key) -> None: + cfg["bar"]["modules"][k] = r.get_active() + persist() + + row.connect("notify::active", on_toggle) + group.add(row) + + return group + + +# -- Dock ----------------------------------------------------------------- + +def _dock_group(cfg: dict, persist) -> Adw.PreferencesGroup: + group = Adw.PreferencesGroup( + title="Dock", + description="Auto-hide, icon size and hide delay. Applies live.", + ) + dock = cfg["dock"] + + autohide = Adw.SwitchRow( + title="Auto-hide", + subtitle="Hide the dock until you reach the screen edge", + ) + autohide.set_active(bool(dock.get("autohide", True))) + + def on_autohide(r: Adw.SwitchRow, _p) -> None: + dock["autohide"] = r.get_active() + persist() + + autohide.connect("notify::active", on_autohide) + group.add(autohide) + + size = Adw.SpinRow.new_with_range(24, 64, 1) + size.set_title("Icon size") + size.set_subtitle("Pixels (24–64)") + size.set_value(int(dock.get("icon_size", 42))) + + def on_size(r: Adw.SpinRow, _p) -> None: + dock["icon_size"] = int(r.get_value()) + persist() + + size.connect("notify::value", on_size) + group.add(size) + + delay_adj = Gtk.Adjustment(value=float(dock.get("hide_delay", 1.2)), + lower=0.2, upper=5.0, + step_increment=0.1, page_increment=0.5) + delay = Adw.SpinRow(title="Hide delay", adjustment=delay_adj, digits=1) + delay.set_subtitle("Seconds before the dock hides (0.2–5.0)") + + def on_delay(r: Adw.SpinRow, _p) -> None: + dock["hide_delay"] = round(r.get_value(), 1) + persist() + + delay.connect("notify::value", on_delay) + group.add(delay) + + return group + + +# -- Pinned apps (dock-pins) ---------------------------------------------- + +def _pins_group() -> Adw.PreferencesGroup: + group = Adw.PreferencesGroup( + title="Pinned apps", + description="Apps pinned to the dock (exec · class · icon).", + ) + + pins = panel.load_pins() + rows: list[Gtk.Widget] = [] + + def _save() -> None: + panel.save_pins(pins) + + def _refresh() -> None: + for r in rows: + group.remove(r) + rows.clear() + for i, pin in enumerate(pins): + row = Adw.ActionRow(title=pin.wmclass or pin.exec, subtitle=pin.exec) + row.add_prefix(Gtk.Image.new_from_icon_name(pin.icon or "application-x-executable")) + rm = Gtk.Button(icon_name="user-trash-symbolic") + rm.add_css_class("flat") + rm.set_valign(Gtk.Align.CENTER) + rm.connect("clicked", lambda _b, idx=i: (_remove(idx))) + row.add_suffix(rm) + group.add(row) + rows.append(row) + group.add(_add_row) + rows.append(_add_row) + + def _remove(idx: int) -> None: + if 0 <= idx < len(pins): + del pins[idx] + _save() + _refresh() + + # -- Add-Zeile (Expander mit 3 Feldern) -- + _add_row = Adw.ExpanderRow(title="Add app", subtitle="exec · class · icon") + e_exec = Adw.EntryRow(title="Exec (command)") + e_class = Adw.EntryRow(title="Window class (app_id / WM_CLASS)") + e_icon = Adw.EntryRow(title="Icon name (Papirus)") + _add_row.add_row(e_exec) + _add_row.add_row(e_class) + _add_row.add_row(e_icon) + add_btn_row = Adw.ActionRow() + add_btn = Gtk.Button(label="Add to dock") + add_btn.add_css_class("suggested-action") + add_btn.add_css_class("pill") + add_btn.set_halign(Gtk.Align.START) + add_btn.set_margin_top(4) + add_btn.set_margin_bottom(4) + add_btn_row.set_child(add_btn) + _add_row.add_row(add_btn_row) + + def _on_add(_b: Gtk.Button) -> None: + ex = e_exec.get_text().strip() + cl = e_class.get_text().strip() or ex + ic = e_icon.get_text().strip() or ex + if not ex: + return + pins.append(panel.Pin(ex, cl, ic)) + e_exec.set_text("") + e_class.set_text("") + e_icon.set_text("") + _add_row.set_expanded(False) + _save() + _refresh() + + add_btn.connect("clicked", _on_add) + + _refresh() + return group diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/power.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/power.py new file mode 100644 index 0000000..6f3ee24 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/power.py @@ -0,0 +1,213 @@ +"""Power — Akku-Info (falls vorhanden) + Idle-Timeouts (Bildschirm aus / Suspend). + +Akku/AC sind reiner Read aus core.power. Die Idle-Timeouts laufen niri-only +über swayidle (eigener taninux-swayidle.service, DPMS via niri-Action). Der +Service läuft als User-Service (kein root); die niri-Config wird nicht +angefasst. Fehlt swayidle, gibt's einen Install-Block (pacman via Helper). +""" +from __future__ import annotations + +import gi + +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") +from gi.repository import Adw, Gtk # noqa: E402 + +from taninux.core import power +from taninux.gui.console import RunnerCard +from taninux.gui.process import ProcessRunner + + +def build() -> Gtk.Widget: + page = Adw.PreferencesPage() + page.add(_power_source_group()) + if power.brightness_available(): + page.add(_brightness_group()) + page.add(_energy_mode_group()) + page.add(_idle_group()) + if not power.idle_daemon_installed(): + page.add(_install_group()) + return page + + +def _card_box(card: Gtk.Widget) -> Gtk.Box: + box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12) + box.set_margin_top(6) + box.append(card) + return box + + +def _energy_mode_group() -> Adw.PreferencesGroup: + group = Adw.PreferencesGroup( + title="Energy mode", + description="Most effective on laptops.", + ) + + if not power.ppd_installed(): + group.add(Adw.ActionRow(title="Install to enable energy modes")) + group.add(_card_box(RunnerCard( + "Install", + ["pacman", "-S", "--needed", "--noconfirm", "power-profiles-daemon"], + privileged=True, + ))) + return group + + profs = power.power_profiles() + if not profs: + group.add(Adw.ActionRow(title="Enable to use energy modes")) + group.add(_card_box(RunnerCard( + "Enable", + ["systemctl", "enable", "--now", "power-profiles-daemon"], + privileged=True, + ))) + return group + + cur = power.current_profile() + combo = Adw.ComboRow(title="Power profile") + combo.set_model(Gtk.StringList.new(profs)) + if cur in profs: + combo.set_selected(profs.index(cur)) + + def on_sel(c: Adw.ComboRow, _p) -> None: + i = c.get_selected() + if 0 <= i < len(profs): + power.set_profile(profs[i]) + + combo.connect("notify::selected", on_sel) + group.add(combo) + return group + + +def _power_source_group() -> Adw.PreferencesGroup: + group = Adw.PreferencesGroup(title="Power source") + bats = power.batteries() + if bats: + for b in bats: + row = Adw.ActionRow(title=b.name.upper(), subtitle=b.status) + cap = f"{b.capacity}%" if b.capacity >= 0 else "—" + if b.capacity >= 0: + level = Gtk.LevelBar.new_for_interval(0, 100) + level.set_value(b.capacity) + level.set_size_request(120, -1) + level.set_valign(Gtk.Align.CENTER) + row.add_suffix(level) + lbl = Gtk.Label(label=cap) + lbl.add_css_class("dim-label") + lbl.add_css_class("numeric") + row.add_suffix(lbl) + group.add(row) + else: + ac = power.ac_online() + sub = ( + "On AC power" if ac is True + else "No battery detected — desktop system" + ) + group.add(Adw.ActionRow(title="Battery", subtitle=sub)) + return group + + +def _brightness_group() -> Adw.PreferencesGroup: + group = Adw.PreferencesGroup(title="Display brightness") + row = Adw.ActionRow(title="Brightness") + + scale = Gtk.Scale.new_with_range(Gtk.Orientation.HORIZONTAL, 1, 100, 1) + scale.set_hexpand(True) + scale.set_size_request(220, -1) + scale.set_draw_value(False) + scale.set_valign(Gtk.Align.CENTER) + + pct_lbl = Gtk.Label() + pct_lbl.add_css_class("dim-label") + pct_lbl.add_css_class("numeric") + + # Startwert VOR connect setzen, sonst feuert es beim Aufbau ein set. + cur = power.brightness_percent() + scale.set_value(max(1, min(100, cur))) + pct_lbl.set_label(f"{cur}%") + + runner = ProcessRunner() + # Runner am Widget verankern, damit er nicht weg-gc't wird. + row._brightness_runner = runner # noqa: SLF001 + + def on_changed(s: Gtk.Scale) -> None: + val = int(s.get_value()) + pct_lbl.set_label(f"{val}%") + if not runner.running: + runner.run(power.set_brightness_argv(val), privileged=False) + + scale.connect("value-changed", on_changed) + + row.add_suffix(scale) + row.add_suffix(pct_lbl) + group.add(row) + return group + + +def _idle_group() -> Adw.PreferencesGroup: + t = power.read_idle_timeouts() + daemon = power.idle_daemon_name() + group = Adw.PreferencesGroup( + title="Idle · sleep", + description="Turn the screen off or suspend after inactivity. 0 = never.", + ) + + so_adj = Gtk.Adjustment(value=t["screen_off"] // 60, lower=0, upper=240, + step_increment=1, page_increment=5) + so = Adw.SpinRow(title="Screen off after", adjustment=so_adj) + so.set_subtitle("minutes (0 = never)") + group.add(so) + + su_adj = Gtk.Adjustment(value=t["suspend"] // 60, lower=0, upper=480, + step_increment=1, page_increment=5) + su = Adw.SpinRow(title="Suspend after", adjustment=su_adj) + su.set_subtitle("minutes (0 = never)") + group.add(su) + + btn = Gtk.Button(label="Apply idle settings") + btn.add_css_class("suggested-action") + btn.add_css_class("pill") + btn.set_halign(Gtk.Align.START) + status = Gtk.Label(xalign=0.0) + status.add_css_class("dim-label") + status.set_wrap(True) + + def on_apply(_b: Gtk.Button) -> None: + so_sec = int(so_adj.get_value()) * 60 + su_sec = int(su_adj.get_value()) * 60 + p = power.write_idle_config(so_sec, su_sec) + if power.idle_daemon_installed(): + ok = power.apply_idle() + status.set_label( + f"Saved → {p}. {daemon} " + + ("restarted." if ok else "service could not start.") + ) + else: + status.set_label( + f"Saved → {p}. Install {daemon} below to activate." + ) + + btn.connect("clicked", on_apply) + + box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8) + box.set_margin_top(6) + box.append(btn) + box.append(status) + group.add(box) + return group + + +def _install_group() -> Adw.PreferencesGroup: + pkg = power.idle_pkg() + group = Adw.PreferencesGroup( + title="Idle timers", + description="Install to enable the timers above.", + ) + card = RunnerCard( + "Install", ["pacman", "-S", "--needed", "--noconfirm", pkg], + privileged=True, + ) + box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12) + box.set_margin_top(6) + box.append(card) + group.add(box) + return group diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/printers.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/printers.py new file mode 100644 index 0000000..7d7e06a --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/printers.py @@ -0,0 +1,195 @@ +"""Printers — CUPS-Druckerverwaltung (libadwaita). + +Read kommt aus core.printers (lpstat). Schreib-Aktionen (Default setzen, +enable/disable, entfernen) liefern dort nur argv und laufen hier über einen +gemeinsamen ProcessRunner — privilegiert via Helper (lpadmin/cupsenable/ +cupsdisable) bzw. unprivilegiert (cancel -a für die Queue). + +Drucker HINZUFÜGEN (Treiberauswahl) ist NICHT enthalten — dafür ein +dediziertes System-Tool nutzen. +""" +from __future__ import annotations + +import gi + +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") +from gi.repository import Adw, Gtk # noqa: E402 + +from taninux.core import printers +from taninux.gui import confirm as confirm_mod +from taninux.gui.console import LogConsole, RunnerCard, log_expander +from taninux.gui.process import ProcessRunner + + +def build() -> Gtk.Widget: + page = Adw.PreferencesPage() + + if not printers.available(): + page.add(_unavailable_group()) + return page + + # Gemeinsame Konsole + Runner für alle Aktionen dieser Seite. + console = LogConsole() + runner = ProcessRunner() + runner.connect("line", lambda _r, line: console.append_line(line)) + + # Druckergruppe wird nach jeder Aktion neu befüllt (live). + printers_group = Adw.PreferencesGroup( + title="Printers", + description="Adding a printer (driver selection) isn't here — use a " + "dedicated tool for that.", + ) + queue_group = Adw.PreferencesGroup( + title="Print queue", + description="Pending jobs across all printers.", + ) + + def run(argv: list[str], *, privileged: bool) -> None: + console.clear() + runner.run(argv, privileged=privileged) + + def refresh() -> None: + _fill_printers(printers_group, run) + _fill_queue(queue_group, run) + + runner.connect("done", lambda _r, code: _on_done(console, code, refresh)) + + refresh() + page.add(printers_group) + page.add(queue_group) + page.add(log_expander(console, title="Output")) + return page + + +def _on_done(console: LogConsole, code: int, refresh) -> None: + console.append_line("") + console.append_line( + f"✔ done (exit {code})" if code == 0 else f"✘ exit {code}" + ) + refresh() + + +def _clear_group(group: Adw.PreferencesGroup) -> None: + """Entfernt alle bisher per _track() hinzugefügten Rows der Gruppe.""" + rows = getattr(group, "_tani_rows", None) + if not rows: + return + for row in list(rows): + group.remove(row) + rows.clear() + + +def _track(group: Adw.PreferencesGroup, row: Gtk.Widget) -> None: + rows = getattr(group, "_tani_rows", None) + if rows is None: + rows = [] + group._tani_rows = rows + group.add(row) + rows.append(row) + + +def _fill_printers(group: Adw.PreferencesGroup, run) -> None: + _clear_group(group) + plist = printers.printers() + if not plist: + _track(group, Adw.ActionRow( + title="No printers configured", + subtitle="Add one with a system printer tool.", + )) + return + + for p in plist: + subtitle = p.state + if p.is_default: + subtitle += " · default" + if p.info: + subtitle += f" · {p.info}" + row = Adw.ActionRow(title=p.name, subtitle=subtitle) + + if not p.is_default: + row.add_suffix(_btn( + "Set default", + lambda _b, n=p.name: run(printers.set_default_argv(n), + privileged=True), + )) + + if p.enabled: + row.add_suffix(_btn( + "Disable", + lambda _b, n=p.name: run(printers.disable_argv(n), + privileged=True), + )) + else: + row.add_suffix(_btn( + "Enable", + lambda _b, n=p.name: run(printers.enable_argv(n), + privileged=True), + suggested=True, + )) + + remove = _btn( + "Remove", + lambda _b, n=p.name: confirm_mod.confirm( + _b, "Remove printer?", + f"Permanently remove the printer “{n}” from CUPS.", + lambda n=n: run(printers.remove_argv(n), privileged=True), + ), + destructive=True, + ) + row.add_suffix(remove) + _track(group, row) + + +def _fill_queue(group: Adw.PreferencesGroup, run) -> None: + _clear_group(group) + jobs = printers.queue() + if not jobs: + _track(group, Adw.ActionRow( + title="Queue empty", subtitle="No pending print jobs." + )) + return + + for job in jobs: + _track(group, Adw.ActionRow(title=job)) + + clear_row = Adw.ActionRow(title="Clear queue", + subtitle="Cancel all pending jobs.") + clear_row.add_suffix(_btn( + "Clear queue", + lambda _b: run(printers.cancel_all_argv(), privileged=False), + destructive=True, + )) + _track(group, clear_row) + + +def _btn(label: str, on_click, *, suggested: bool = False, + destructive: bool = False) -> Gtk.Button: + btn = Gtk.Button(label=label) + btn.set_valign(Gtk.Align.CENTER) + if suggested: + btn.add_css_class("suggested-action") + if destructive: + btn.add_css_class("destructive-action") + btn.connect("clicked", on_click) + return btn + + +def _unavailable_group() -> Adw.PreferencesGroup: + group = Adw.PreferencesGroup( + title="Printing", + description="CUPS is not installed. Printer management is unavailable.", + ) + group.add(Adw.ActionRow( + title="CUPS not available", + subtitle="Install cups to manage printers.", + )) + box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12) + box.set_margin_top(6) + box.append(RunnerCard( + "Install CUPS", + ["pacman", "-S", "--needed", "--noconfirm", "cups"], + privileged=True, + )) + group.add(box) + return group diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/settings.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/settings.py new file mode 100644 index 0000000..19cec2d --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/settings.py @@ -0,0 +1,233 @@ +"""Appearance-Seite — Farbschema + Fuji-Akzent. + +Voll funktional und persistent (gui.config): Farbschema System/Light/Dark als +ComboRow, die Fuji-Palette als wählbare Swatches. Beide wirken sofort app-weit +über gui.style. +""" +from __future__ import annotations + +import gi + +gi.require_version("Adw", "1") +from gi.repository import Adw, Gtk, Pango # noqa: E402 + +from taninux.gui import config, desktop, eww, style +from taninux.gui.pages.wallpaper import wallpaper_group + +# Schema-Reihenfolge der ComboRow ↔ Persistenz-Keys +_SCHEME_LABELS = ["System", "Light", "Dark"] +_SCHEME_KEYS = ["system", "light", "dark"] + + +def build() -> Gtk.Widget: + page = Adw.PreferencesPage() + page.add(_appearance_group()) + page.add(wallpaper_group()) # sehr visuell -> direkt nach Color scheme/Accent + page.add(_window_buttons_group()) + page.add(_theme_icons_group()) + page.add(_font_group()) + page.add(_cursor_group()) + return page + + +# -- GTK-Theme + Icon-Theme (nwg-look-Stil, kein root) -------------------- + +def _combo(title: str, items: list[str], current_value: str, apply_fn) -> Adw.ComboRow: + """ComboRow, die bei Auswahl apply_fn(value) ruft. Initial-Set feuert nicht + (connect erst NACH set_selected).""" + opts = list(items) + if current_value and current_value not in opts: + opts.insert(0, current_value) # Ist-Wert immer wählbar halten + row = Adw.ComboRow(title=title) + row.set_model(Gtk.StringList.new(opts or ["(none)"])) + row.set_enable_search(True) + if current_value in opts: + row.set_selected(opts.index(current_value)) + + def on_sel(r: Adw.ComboRow, _p) -> None: + i = r.get_selected() + if 0 <= i < len(opts): + apply_fn(opts[i]) + + row.connect("notify::selected", on_sel) + return row + + +def _window_buttons_group() -> Adw.PreferencesGroup: + """Einzelne Titlebar-Knöpfe an/aus + Platzierung (wie GNOME Tweaks).""" + cur = desktop.window_buttons() + group = Adw.PreferencesGroup( + title="Window buttons", + description="Show or hide the titlebar buttons.", + ) + + state = {"ready": False} + + min_row = Adw.SwitchRow(title="Minimize") + max_row = Adw.SwitchRow(title="Maximize") + close_row = Adw.SwitchRow(title="Close") + place_row = Adw.ComboRow(title="Placement") + place_row.set_model(Gtk.StringList.new(["Right", "Left"])) + + def apply(*_a) -> None: + if not state["ready"]: + return + side = "left" if place_row.get_selected() == 1 else "right" + desktop.set_window_buttons( + min_row.get_active(), max_row.get_active(), close_row.get_active(), side, + ) + + # Startwerte ZUERST setzen, dann erst Signale verbinden (kein Initial-Feuern). + min_row.set_active(cur["minimize"]) + max_row.set_active(cur["maximize"]) + close_row.set_active(cur["close"]) + place_row.set_selected(1 if cur["side"] == "left" else 0) + + min_row.connect("notify::active", apply) + max_row.connect("notify::active", apply) + close_row.connect("notify::active", apply) + place_row.connect("notify::selected", apply) + + group.add(min_row) + group.add(max_row) + group.add(close_row) + group.add(place_row) + + state["ready"] = True + return group + + +def _theme_icons_group() -> Adw.PreferencesGroup: + cur = desktop.current() + group = Adw.PreferencesGroup( + title="Theme · icons", + description="Running apps may need a restart.", + ) + group.add(_combo( + "GTK theme", desktop.list_gtk_themes(), cur["gtk_theme"], + desktop.apply_gtk_theme, + )) + group.add(_combo( + "Icon theme", desktop.list_icon_themes(), cur["icon_theme"], + desktop.apply_icon_theme, + )) + return group + + +def _font_group() -> Adw.PreferencesGroup: + cur = desktop.current() + group = Adw.PreferencesGroup(title="Font") + row = Adw.ActionRow( + title="System font", + subtitle="Default interface font", + ) + dialog = Gtk.FontDialog() + fbtn = Gtk.FontDialogButton.new(dialog) + fbtn.set_valign(Gtk.Align.CENTER) + if cur["font"]: + fbtn.set_font_desc(Pango.FontDescription.from_string(cur["font"])) + + def on_font(btn: Gtk.FontDialogButton, _p) -> None: + desc = btn.get_font_desc() + if desc is not None: + desktop.apply_font(desc.to_string()) + + fbtn.connect("notify::font-desc", on_font) + row.add_suffix(fbtn) + group.add(row) + return group + + +def _cursor_group() -> Adw.PreferencesGroup: + cur = desktop.current() + group = Adw.PreferencesGroup( + title="Cursor", + description="Cursor theme and size.", + ) + group.add(_combo( + "Cursor theme", desktop.list_cursor_themes(), cur["cursor_theme"], + desktop.apply_cursor_theme, + )) + + try: + size = int(cur["cursor_size"]) + except (ValueError, TypeError): + size = 24 + adj = Gtk.Adjustment(value=size, lower=8, upper=128, step_increment=2, + page_increment=8) + spin = Adw.SpinRow(title="Cursor size", adjustment=adj) + spin.set_subtitle("Pixel") + # connect NACH dem Setzen des Startwerts -> Initial feuert nicht + adj.connect("value-changed", lambda a: desktop.apply_cursor_size(int(a.get_value()))) + group.add(spin) + return group + + +def _appearance_group() -> Adw.PreferencesGroup: + cfg = config.load() + group = Adw.PreferencesGroup( + title="Appearance", + description="Color scheme and Fuji accent — applied instantly, saved.", + ) + + # Guard: programmatisches Setzen der Startwerte soll nicht schon speichern. + state = {"ready": False} + + def persist() -> None: + if state["ready"]: + config.save(state["scheme"], state["accent"]) + style.apply(state["scheme"], state["accent"]) # diese App live + style.write_global_theme(state["scheme"], state["accent"]) # ALLE GTK3+GTK4-Apps + gsettings + eww.reload_if_running() # Bar live nachziehen + + state["scheme"] = cfg["color_scheme"] + state["accent"] = cfg["accent"] + + # -- Color scheme -------------------------------------------------- + scheme_row = Adw.ComboRow(title="Color scheme") + scheme_row.set_model(Gtk.StringList.new(_SCHEME_LABELS)) + cur = _SCHEME_KEYS.index(cfg["color_scheme"]) if cfg["color_scheme"] in _SCHEME_KEYS else 0 + scheme_row.set_selected(cur) + + def on_scheme(row: Adw.ComboRow, _param) -> None: + state["scheme"] = _SCHEME_KEYS[row.get_selected()] + persist() + + scheme_row.connect("notify::selected", on_scheme) + group.add(scheme_row) + + # -- Fuji accent (Swatch-Reihe) ----------------------------------- + accent_row = Adw.ActionRow( + title="Accent color", + subtitle="Fuji palette", + ) + swatches = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8) + swatches.set_valign(Gtk.Align.CENTER) + + leader: Gtk.ToggleButton | None = None + for acc in style.FUJI_ACCENTS: + btn = Gtk.ToggleButton() + btn.add_css_class("fuji-swatch") + btn.add_css_class(f"swatch-{acc.key}") + btn.set_tooltip_text(acc.label) + btn.set_valign(Gtk.Align.CENTER) + if leader is None: + leader = btn + else: + btn.set_group(leader) + if acc.key == cfg["accent"]: + btn.set_active(True) + + def on_toggle(b: Gtk.ToggleButton, key: str = acc.key) -> None: + if b.get_active(): + state["accent"] = key + persist() + + btn.connect("toggled", on_toggle) + swatches.append(btn) + + accent_row.add_suffix(swatches) + group.add(accent_row) + + state["ready"] = True + return group diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/shares.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/shares.py new file mode 100644 index 0000000..1c39329 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/shares.py @@ -0,0 +1,282 @@ +"""Network Shares — Remote-Verbindungen für Nautilus (SMB/SFTP/FTP/WebDAV/NFS). + +Schreibt Remote-Bookmarks über core.shares (GTK-Bookmarks, die Nautilus liest) +und verbindet via `gio mount` (unprivilegiert; GVfs fragt das Passwort selbst +via Keyring/Agent). Nextcloud ist hier als WebDAV-Preset integriert — kein +Sync-Client nötig (leichtgewichtig, Dateien on-demand). + +Felder passen sich pro Protokoll an, eine Live-Vorschau zeigt die Ziel-URI. +SMB/NFS brauchen gvfs-smb/gvfs-nfs; fehlen sie, gibt's einen Install-Block. +""" +from __future__ import annotations + +import gi + +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") +from gi.repository import Adw, Gtk # noqa: E402 + +from taninux.core import nextcloud as nc +from taninux.core import shares +from taninux.gui import confirm as confirm_mod +from taninux.gui.console import LogConsole, RunnerCard, log_expander +from taninux.gui.process import ProcessRunner + +# (Label, scheme). "nextcloud" ist ein Spezial-Scheme (baut den WebDAV-Pfad). +_PROTOCOLS: list[tuple[str, str]] = [ + ("SFTP", "sftp"), + ("SMB · Windows share", "smb"), + ("WebDAV", "davs"), + ("Nextcloud", "nextcloud"), + ("FTP", "ftp"), + ("NFS", "nfs"), +] + +# Pro Scheme: welche Felder sichtbar sind + ihr Titel. (title, visible) +_FIELDS: dict[str, dict[str, tuple[str, bool]]] = { + "sftp": {"host": ("Host", True), "path": ("Path (optional)", True), + "user": ("User (optional)", True), "port": ("Port (optional)", True)}, + "ftp": {"host": ("Host", True), "path": ("Path (optional)", True), + "user": ("User (optional)", True), "port": ("Port (optional)", True)}, + "smb": {"host": ("Host", True), "path": ("Share", True), + "user": ("User (optional)", True), "port": ("", False)}, + "davs": {"host": ("Host", True), "path": ("Path (optional)", True), + "user": ("User (optional)", True), "port": ("", False)}, + "nextcloud": {"host": ("Server · e.g. cloud.example.com", True), + "path": ("", False), "user": ("Username", True), + "port": ("", False)}, + "nfs": {"host": ("Host", True), "path": ("Export path", True), + "user": ("", False), "port": ("", False)}, +} + +_SCHEME_ICON = { + "smb": "folder-remote-symbolic", "sftp": "folder-remote-symbolic", + "ftp": "folder-remote-symbolic", "nfs": "folder-remote-symbolic", + "dav": "folder-cloud-symbolic", "davs": "folder-cloud-symbolic", +} + + +def build() -> Gtk.Widget: + page = Adw.PreferencesPage() + + console = LogConsole() + runner = ProcessRunner() + runner.connect("line", lambda _r, line: console.append_line(line)) + runner.connect("done", lambda _r, code: console.append_line( + "✔ done" if code == 0 else f"✘ exit {code}")) + + # ============================================================ Your shares + list_group = Adw.PreferencesGroup( + title="Your shares", + description="Saved remote locations — they appear in the file manager.", + ) + refresh_btn = Gtk.Button(icon_name="view-refresh-symbolic") + refresh_btn.add_css_class("flat") + refresh_btn.set_valign(Gtk.Align.CENTER) + refresh_btn.set_tooltip_text("Reload") + list_group.set_header_suffix(refresh_btn) + page.add(list_group) + + def _mount(uri: str) -> None: + console.clear() + runner.run(shares.mount_argv(uri)) + + def _remove(uri: str, label: str) -> None: + confirm_mod.confirm( + page, "Remove share?", + f"Remove “{label}”. The remote data is not deleted.", + lambda: (shares.remove_bookmark(uri), _fill_list()), + ) + + def _fill_list() -> None: + _clear_group(list_group) + bms = shares.list_remote() + if not bms: + _track(list_group, Adw.ActionRow( + title="No shares yet", + subtitle="Add one below — SFTP, SMB, WebDAV, Nextcloud, FTP or NFS.", + )) + return + for bm in bms: + row = Adw.ActionRow(title=bm.label or bm.uri, subtitle=bm.uri) + icon = _SCHEME_ICON.get(bm.scheme, "folder-remote-symbolic") + row.add_prefix(Gtk.Image.new_from_icon_name(icon)) + connect = _btn("Connect", suggested=True) + connect.connect("clicked", lambda _b, u=bm.uri: _mount(u)) + row.add_suffix(connect) + remove = _btn("Remove", destructive=True) + remove.connect("clicked", lambda _b, u=bm.uri, lb=bm.label: _remove(u, lb)) + row.add_suffix(remove) + _track(list_group, row) + + refresh_btn.connect("clicked", lambda _b: _fill_list()) + + # ============================================================ Add + add_group = Adw.PreferencesGroup( + title="Add a connection", + description="You'll be asked for the password on first connect.", + ) + + proto_row = Adw.ComboRow(title="Type") + proto_model = Gtk.StringList() + for label, _scheme in _PROTOCOLS: + proto_model.append(label) + proto_row.set_model(proto_model) + add_group.add(proto_row) + + host_row = Adw.EntryRow(title="Host") + path_row = Adw.EntryRow(title="Path") + user_row = Adw.EntryRow(title="User") + port_row = Adw.EntryRow(title="Port") + field_rows = {"host": host_row, "path": path_row, + "user": user_row, "port": port_row} + for r in field_rows.values(): + add_group.add(r) + + # Live-Vorschau der Ziel-URI. + preview = Adw.ActionRow(title="Will create") + preview_lbl = Gtk.Label(label="—") + preview_lbl.add_css_class("dim-label") + preview_lbl.set_selectable(True) + preview_lbl.set_ellipsize(3) # END + preview.add_suffix(preview_lbl) + add_group.add(preview) + + add_btn = Gtk.Button(label="Add") + add_btn.add_css_class("suggested-action") + add_btn.set_valign(Gtk.Align.CENTER) + add_row = Adw.ActionRow(title="Add to file manager") + add_row.add_suffix(add_btn) + add_group.add(add_row) + page.add(add_group) + + def _scheme() -> str: + return _PROTOCOLS[proto_row.get_selected()][1] + + def _current_uri() -> str: + host = host_row.get_text().strip() + if not host: + return "" + scheme = _scheme() + if scheme == "nextcloud": + u = user_row.get_text().strip() + return nc.webdav_uri_from(host, u) if u else "" + return shares.build_uri( + scheme, host, path=path_row.get_text(), + user=user_row.get_text(), port=port_row.get_text(), + ) + + def _update_preview(*_a) -> None: + preview_lbl.set_label(_current_uri() or "—") + + def _sync_fields(*_a) -> None: + conf = _FIELDS[_scheme()] + for key, row in field_rows.items(): + title, visible = conf[key] + row.set_visible(visible) + if title: + row.set_title(title) + _update_preview() + + proto_row.connect("notify::selected", _sync_fields) + for r in field_rows.values(): + r.connect("changed", _update_preview) + + def _on_add(_b: Gtk.Button) -> None: + uri = _current_uri() + console.clear() + if not uri: + console.append_line("✘ fill in the required fields first") + return + label = host_row.get_text().strip() + shares.add_bookmark(uri, label=label) + for r in field_rows.values(): + r.set_text("") + console.append_line(f"✔ added {uri}") + _update_preview() + _fill_list() + + add_btn.connect("clicked", _on_add) + + # ============================================================ Nextcloud accounts + nc_accounts = nc.accounts() + if nc_accounts: + nc_group = Adw.PreferencesGroup( + title="Detected Nextcloud accounts", + description="From the Nextcloud client — add as WebDAV in one click.", + ) + for acc in nc_accounts: + row = Adw.ActionRow(title=acc.name, subtitle=acc.url) + row.add_prefix(Gtk.Image.new_from_icon_name("folder-cloud-symbolic")) + add = _btn("Add to Files", suggested=True) + + def _add_acc(_b, a=acc) -> None: + uri = nc.webdav_uri(a) + shares.add_bookmark(uri, label=a.name) + console.clear() + console.append_line(f"✔ added {uri}") + _fill_list() + + add.connect("clicked", _add_acc) + row.add_suffix(add) + nc_group.add(row) + page.add(nc_group) + + # ============================================================ Backends + missing: list[tuple[str, str, list[str]]] = [] + if not shares.smb_available(): + missing.append(("SMB · Windows shares", "gvfs-smb", shares.install_smb_argv())) + if not shares.nfs_available(): + missing.append(("NFS shares", "gvfs-nfs", shares.install_nfs_argv())) + if missing: + be_group = Adw.PreferencesGroup( + title="More connection types", + description="SMB and NFS need an extra component.", + ) + for label, pkg, argv in missing: + be_group.add(Adw.ActionRow( + title=label, subtitle="Install to enable this connection type.")) + box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12) + box.set_margin_top(6) + box.append(RunnerCard("Install", argv, privileged=True)) + be_group.add(box) + page.add(be_group) + + # ============================================================ Log + page.add(log_expander(console)) + + _sync_fields() + _fill_list() + return page + + +# ----------------------------------------------------------- Helpers + +def _clear_group(group: Adw.PreferencesGroup) -> None: + rows = getattr(group, "_tani_rows", None) + if not rows: + return + for row in list(rows): + group.remove(row) + rows.clear() + + +def _track(group: Adw.PreferencesGroup, row: Gtk.Widget) -> None: + rows = getattr(group, "_tani_rows", None) + if rows is None: + rows = [] + group._tani_rows = rows + group.add(row) + rows.append(row) + + +def _btn(label: str, *, suggested: bool = False, + destructive: bool = False) -> Gtk.Button: + btn = Gtk.Button(label=label) + btn.add_css_class("flat") + btn.set_valign(Gtk.Align.CENTER) + if suggested: + btn.add_css_class("suggested-action") + if destructive: + btn.add_css_class("destructive-action") + return btn diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/storage.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/storage.py new file mode 100644 index 0000000..0e85a4c --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/storage.py @@ -0,0 +1,80 @@ +"""Storage — Laufwerke, Belegung, Wechseldatenträger (udisksctl).""" +from __future__ import annotations + +import gi + +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") +from gi.repository import Adw, GLib, Gtk # noqa: E402 + +from taninux.core import storage +from taninux.gui.process import ProcessRunner + + +def _gib(b: int) -> str: + if b >= 1024**3: + return f"{b / 1024**3:.0f} GiB" + return f"{b / 1024**2:.0f} MiB" if b else "—" + + +def build() -> Gtk.Widget: + page = Adw.PreferencesPage() + runner = ProcessRunner() + groups: list[Adw.PreferencesGroup] = [] + + def _row(vol: storage.Volume) -> Adw.ActionRow: + title = vol.label or vol.dev.split("/")[-1] + sub = vol.mountpoint or "Not mounted" + row = Adw.ActionRow(title=title, subtitle=f"{sub} · {_gib(vol.size)} · {vol.fstype}") + if vol.mountpoint and vol.use_pct: + level = Gtk.LevelBar.new_for_interval(0, 100) + level.set_value(vol.use_pct) + level.set_size_request(120, -1) + level.set_valign(Gtk.Align.CENTER) + row.add_suffix(level) + pct = Gtk.Label(label=f"{vol.use_pct}%") + pct.add_css_class("dim-label") + pct.add_css_class("numeric") + row.add_suffix(pct) + if vol.removable: + if vol.mountpoint: + row.add_suffix(_btn("Eject", lambda: runner.run(storage.eject_argv(vol.dev)))) + else: + row.add_suffix(_btn("Mount", lambda: runner.run(storage.mount_argv(vol.dev)))) + return row + + def _render() -> None: + for g in groups: + page.remove(g) + groups.clear() + vols = storage.volumes() + internal = [v for v in vols if not v.removable] + removable = [v for v in vols if v.removable] + + g_int = Adw.PreferencesGroup(title="This device") + if internal: + for v in internal: + g_int.add(_row(v)) + else: + g_int.add(Adw.ActionRow(title="No filesystems found")) + page.add(g_int) + groups.append(g_int) + + if removable: + g_rm = Adw.PreferencesGroup(title="Removable") + for v in removable: + g_rm.add(_row(v)) + page.add(g_rm) + groups.append(g_rm) + + runner.connect("done", lambda _r, _c: _render()) + GLib.idle_add(lambda: (_render(), False)[1]) + return page + + +def _btn(label: str, cb) -> Gtk.Button: + b = Gtk.Button(label=label) + b.add_css_class("flat") + b.set_valign(Gtk.Align.CENTER) + b.connect("clicked", lambda _b: cb()) + return b diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/store.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/store.py new file mode 100644 index 0000000..27e2d9f --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/store.py @@ -0,0 +1,158 @@ +"""Browse — Pakete suchen und installieren/entfernen. + +Default-Scope: offizielle Repos + Flatpak. AUR ist Opt-in (Schalter „Sources") +und braucht paru. Repo-Pakete laufen über den Helper (pacman), AUR über paru, +Flatpak als User. Async via core.search. +""" +from __future__ import annotations + +import threading + +import gi + +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") +from gi.repository import Adw, GLib, Gtk # noqa: E402 + +from taninux.core import panel +from taninux.core import search as pkgsearch +from taninux.gui.console import LogConsole, log_expander +from taninux.gui.pages.appdetail import open_app_detail +from taninux.gui.process import ProcessRunner + + +def build() -> Gtk.Widget: + page = Adw.PreferencesPage() + + console = LogConsole() + runner = ProcessRunner() + runner.connect("line", lambda _r, line: console.append_line(line)) + + token = {"n": 0} # gegen veraltete Such-Antworten + + g_sources = Adw.PreferencesGroup(title="Sources") + aur_row = Adw.SwitchRow( + title="Arch User Repository (AUR)", + subtitle="Community packages built from source — requires paru.", + ) + aur_row.set_active(panel.include_aur()) + g_sources.add(aur_row) + page.add(g_sources) + + g_search = Adw.PreferencesGroup() + entry = Gtk.SearchEntry() + entry.set_placeholder_text("Search apps and packages") + entry.set_hexpand(True) + g_search.add(entry) + page.add(g_search) + + g_results = Adw.PreferencesGroup(title="Results") + page.add(g_results) + rows: list[Gtk.Widget] = [] + + page.add(log_expander(console)) + + def _clear() -> None: + for r in rows: + g_results.remove(r) + rows.clear() + + def _placeholder(text: str) -> None: + _clear() + row = Adw.ActionRow(title=text) + g_results.add(row) + rows.append(row) + + def _act(item: pkgsearch.Result, btn: Gtk.Button) -> None: + argv, priv = (pkgsearch.remove_argv if item.installed else pkgsearch.install_argv)(item) + console.clear() + btn.set_sensitive(False) + runner.run(argv, privileged=priv) + + def _open(item: pkgsearch.Result) -> None: + flatpak = item.source == "flatpak" + open_app_detail( + page, + title=item.extra.get("title", item.name), + icon=item.name, + repo_name=None if flatpak else item.name, + flatpak_id=item.name if flatpak else None, + remote=item.repo or "flathub", + on_changed=lambda: _run_search(entry.get_text()), + ) + + def _render(items: list[pkgsearch.Result]) -> None: + _clear() + if not items: + _placeholder("No packages found") + return + g_results.set_title(f"Results ({len(items)})") + for item in items: + subtitle = f"{item.repo} · {item.version}" + if item.description: + subtitle += f" — {item.description}" + row = Adw.ActionRow(title=item.extra.get("title", item.name)) + row.set_subtitle(subtitle) + row.set_subtitle_lines(2) + row.set_activatable(True) + row.connect("activated", lambda _r, it=item: _open(it)) + tag = Gtk.Label(label=item.source) + tag.add_css_class("dim-label") + tag.add_css_class("caption") + tag.set_valign(Gtk.Align.CENTER) + row.add_prefix(tag) + btn = Gtk.Button(label="Remove" if item.installed else "Install") + btn.add_css_class("flat" if item.installed else "suggested-action") + btn.set_valign(Gtk.Align.CENTER) + btn.connect("clicked", lambda b, it=item: _act(it, b)) + row.add_suffix(btn) + g_results.add(row) + rows.append(row) + + def _run_search(query: str) -> None: + token["n"] += 1 + my = token["n"] + if len(query.strip()) < 2: + g_results.set_title("Results") + _clear() + return + _placeholder(f"Searching “{query}” …") + + def worker() -> None: + try: + items = pkgsearch.search(query) + except Exception: # noqa: BLE001 + items = [] + + def deliver() -> bool: + if my == token["n"]: + _render(items) + return False + + GLib.idle_add(deliver) + + threading.Thread(target=worker, daemon=True).start() + + def on_done(_r: ProcessRunner, code: int) -> None: + console.append_line("✔ done" if code == 0 else f"✘ exit {code}") + _run_search(entry.get_text()) # Status (installed/…) auffrischen + + debounce = {"id": 0} + + def _on_changed(e: Gtk.SearchEntry) -> None: + if debounce["id"]: + GLib.source_remove(debounce["id"]) + text = e.get_text() + debounce["id"] = GLib.timeout_add( + 400, lambda: (debounce.update(id=0), _run_search(text), False)[-1] + ) + + def _on_aur(row: Adw.SwitchRow, _pspec) -> None: + panel.set_include_aur(row.get_active()) + _run_search(entry.get_text()) + + runner.connect("done", on_done) + aur_row.connect("notify::active", _on_aur) + entry.connect("activate", lambda e: _run_search(e.get_text())) + entry.connect("search-changed", _on_changed) + return page diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/update.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/update.py new file mode 100644 index 0000000..d6faf36 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/update.py @@ -0,0 +1,195 @@ +"""Updates — Zähler (pacman/AUR/flatpak) + eine gemeinsame Paketliste. + +Alles auf einmal oder einzeln aktualisieren. pacman läuft privilegiert über +den Helper, flatpak/AUR als User. Quellen werden async geladen. +""" +from __future__ import annotations + +import threading + +import gi + +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") +from gi.repository import Adw, GLib, Gtk # noqa: E402 + +from taninux.core import panel +from taninux.gui.console import LogConsole, log_expander +from taninux.gui.process import ProcessRunner +from taninux.update.sources import aur, flatpak, pacman +from taninux.update.types import UpdateItem + + +def _one_argv(item: UpdateItem): + if item.source == "pacman": + return ["pacman", "-S", "--noconfirm", item.name], True + if item.source == "flatpak": + return ["flatpak", "update", "--assumeyes", item.name], False + if item.source == "aur": + return [item.extra.get("helper", "paru"), "-S", "--noconfirm", item.name], False + return None + + +def _all_queue(items: list[UpdateItem]): + """(argv, privileged)-Queue für „Update all".""" + out = [] + if any(i.source == "pacman" for i in items): + out.append((["pacman", "-Syu", "--noconfirm"], True)) + if any(i.source == "flatpak" for i in items): + out.append((["flatpak", "update", "--assumeyes"], False)) + aur_items = [i for i in items if i.source == "aur"] + if aur_items: + helper = aur_items[0].extra.get("helper", "paru") + out.append(([helper, "-Sua", "--noconfirm"], False)) + return out + + +def build() -> Gtk.Widget: + page = Adw.PreferencesPage() + + console = LogConsole() + runner = ProcessRunner() + runner.connect("line", lambda _r, line: console.append_line(line)) + + items: list[UpdateItem] = [] + pending: list = [] + + g_sum = Adw.PreferencesGroup(title="Updates") + refresh = Gtk.Button(icon_name="view-refresh-symbolic") + refresh.add_css_class("flat") + refresh.set_valign(Gtk.Align.CENTER) + g_sum.set_header_suffix(refresh) + + def _count_row(name: str): + row = Adw.ActionRow(title=name) + lbl = Gtk.Label(label="…") + lbl.add_css_class("numeric") + lbl.add_css_class("dim-label") + lbl.set_valign(Gtk.Align.CENTER) + row.add_suffix(lbl) + return row, lbl + + pac_row, pac_lbl = _count_row("Pacman") + aur_row, aur_lbl = _count_row("AUR") + fp_row, fp_lbl = _count_row("Flatpak") + for r in (pac_row, aur_row, fp_row): + g_sum.add(r) + + all_row = Adw.ActionRow(title="Update all") + all_btn = Gtk.Button(label="Update") + all_btn.add_css_class("suggested-action") + all_btn.add_css_class("pill") + all_btn.set_valign(Gtk.Align.CENTER) + all_btn.set_sensitive(False) + all_row.add_suffix(all_btn) + g_sum.add(all_row) + page.add(g_sum) + + g_list = Adw.PreferencesGroup(title="Packages") + page.add(g_list) + list_rows: list[Gtk.Widget] = [] + + page.add(log_expander(console)) + + def _start_next() -> None: + if pending: + argv, priv = pending.pop(0) + runner.run(argv, privileged=priv) + + def _update_one(item: UpdateItem) -> None: + a = _one_argv(item) + if not a: + return + console.clear() + runner.run(a[0], privileged=a[1]) + + def _update_all(_b: Gtk.Button) -> None: + pending.clear() + pending.extend(_all_queue(items)) + if not pending: + return + console.clear() + _start_next() + + all_btn.connect("clicked", _update_all) + + def _clear_list() -> None: + for r in list_rows: + g_list.remove(r) + list_rows.clear() + + def _render() -> None: + counts = {"pacman": 0, "aur": 0, "flatpak": 0} + for i in items: + if i.source in counts: + counts[i.source] += 1 + pac_lbl.set_label(str(counts["pacman"])) + aur_lbl.set_label(str(counts["aur"]) if panel.include_aur() else "off") + fp_lbl.set_label(str(counts["flatpak"])) + total = sum(counts.values()) + all_btn.set_sensitive(total > 0) + all_row.set_title(f"Update all ({total})" if total else "Update all") + + _clear_list() + if not items: + row = Adw.ActionRow(title="Everything up to date") + g_list.add(row) + list_rows.append(row) + return + order = {"pacman": 0, "aur": 1, "flatpak": 2} + for item in sorted(items, key=lambda i: (order.get(i.source, 9), i.name)): + row = Adw.ActionRow( + title=item.name, + subtitle=f"{item.current_version} → {item.new_version}", + ) + tag = Gtk.Label(label=item.source) + tag.add_css_class("dim-label") + tag.add_css_class("caption") + tag.set_valign(Gtk.Align.CENTER) + row.add_prefix(tag) + btn = Gtk.Button(label="Update") + btn.add_css_class("flat") + btn.set_valign(Gtk.Align.CENTER) + btn.connect("clicked", lambda _b, it=item: _update_one(it)) + row.add_suffix(btn) + g_list.add(row) + list_rows.append(row) + + def _load() -> None: + pac_lbl.set_label("…") + aur_lbl.set_label("…") + fp_lbl.set_label("…") + + fetchers = [pacman.fetch, flatpak.fetch] + if panel.include_aur(): + fetchers.insert(1, aur.fetch) + + def worker() -> None: + found: list[UpdateItem] = [] + for fetch in fetchers: + try: + found += fetch() + except Exception: # noqa: BLE001 + pass + + def deliver() -> bool: + items.clear() + items.extend(found) + _render() + return False + + GLib.idle_add(deliver) + + threading.Thread(target=worker, daemon=True).start() + + def on_done(_r: ProcessRunner, code: int) -> None: + console.append_line("✔ done" if code == 0 else f"✘ exit {code}") + if pending: + _start_next() + else: + _load() + + runner.connect("done", on_done) + refresh.connect("clicked", lambda _b: _load()) + GLib.idle_add(lambda: (_load(), False)[1]) + return page diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/users.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/users.py new file mode 100644 index 0000000..ae8e90e --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/users.py @@ -0,0 +1,253 @@ +"""Users — eigenes Konto (Info + Passwort) und weitere Benutzer verwalten.""" +from __future__ import annotations + +import re +import threading +from pathlib import Path + +import gi + +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") +gi.require_version("GdkPixbuf", "2.0") +from gi.repository import Adw, Gdk, GdkPixbuf, Gio, GLib, Gtk # noqa: E402 + +from taninux.core import account, avatar +from taninux.gui import confirm +from taninux.gui.process import ProcessRunner + +_NAME_RE = re.compile(r"^[a-z_][a-z0-9_-]*$") + + +def build() -> Gtk.Widget: + page = Adw.PreferencesPage() + me = account.current() + + runner = ProcessRunner() + queue: list = [] + others = Adw.PreferencesGroup(title="Other users") + rows: list[Gtk.Widget] = [] + + # -- eigenes Konto -------------------------------------------------- + g_info = Adw.PreferencesGroup(title="Account") + + av = Adw.Avatar(size=64, text=me.fullname, show_initials=True) + _load_avatar(av) + pic_row = Adw.ActionRow(title="Profile picture", subtitle="Click to change") + pic_row.add_prefix(av) + pic_row.set_activatable(True) + pic_row.connect("activated", lambda _r: _pick_avatar(page, av)) + g_info.add(pic_row) + + g_info.add(_row("Name", me.fullname)) + g_info.add(_row("Username", me.username)) + g_info.add(_row("Role", "Administrator" if me.admin else "Standard")) + page.add(g_info) + + page.add(_password_group()) + + # -- weitere Benutzer ---------------------------------------------- + add_btn = Gtk.Button(icon_name="list-add-symbolic") + add_btn.add_css_class("flat") + add_btn.set_valign(Gtk.Align.CENTER) + add_btn.set_tooltip_text("Add user") + others.set_header_suffix(add_btn) + page.add(others) + + def _run_queue() -> None: + if queue: + runner.run(queue.pop(0), privileged=True) + + def _start(cmds: list) -> None: + queue.clear() + queue.extend(cmds) + _run_queue() + + def _render() -> None: + for r in rows: + others.remove(r) + rows.clear() + rest = [u for u in account.list_users() if u.username != me.username] + if not rest: + row = Adw.ActionRow(title="No other users") + others.add(row) + rows.append(row) + return + for u in rest: + row = Adw.ActionRow( + title=u.fullname, + subtitle=f"{u.username} · {'Administrator' if u.admin else 'Standard'}", + ) + rm = Gtk.Button(icon_name="user-trash-symbolic") + rm.add_css_class("flat") + rm.set_valign(Gtk.Align.CENTER) + rm.connect("clicked", lambda _b, name=u.username: confirm.confirm( + page, f"Remove {name}?", + "Their account and home folder are deleted. This cannot be undone.", + lambda n=name: _start([account.delete_user_argv(n)]))) + row.add_suffix(rm) + others.add(row) + rows.append(row) + + add_btn.connect("clicked", lambda _b: _add_dialog(page, _start)) + + def on_done(_r: ProcessRunner, code: int) -> None: + if queue: + _run_queue() + else: + _render() + + runner.connect("done", on_done) + GLib.idle_add(lambda: (_render(), False)[1]) + return page + + +def _add_dialog(parent: Gtk.Widget, start) -> None: + dialog = Adw.Dialog() + dialog.set_title("Add user") + dialog.set_content_width(420) + + page = Adw.PreferencesPage() + tv = Adw.ToolbarView() + tv.add_top_bar(Adw.HeaderBar()) + tv.set_content(page) + dialog.set_child(tv) + + group = Adw.PreferencesGroup() + full = Adw.EntryRow(title="Full name") + user = Adw.EntryRow(title="Username") + admin = Adw.SwitchRow(title="Administrator", + subtitle="Can install software and change system settings") + group.add(full) + group.add(user) + group.add(admin) + page.add(group) + + status = Gtk.Label(xalign=0.0, wrap=True) + status.add_css_class("dim-label") + create = Gtk.Button(label="Create user") + create.add_css_class("suggested-action") + create.add_css_class("pill") + create.set_halign(Gtk.Align.START) + create.set_margin_top(6) + + # Username aus Vollname vorschlagen + def _suggest(_e) -> None: + if not user.get_text(): + user.set_text(re.sub(r"[^a-z0-9_-]", "", full.get_text().lower().split(" ")[0])) + full.connect("changed", _suggest) + + def on_create(_b) -> None: + name = user.get_text().strip() + if not _NAME_RE.match(name): + status.set_label("Username must be lowercase letters, digits, - or _.") + return + start(account.add_user_cmds(name, full.get_text().strip(), admin.get_active())) + dialog.close() + + create.connect("clicked", on_create) + box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8) + box.set_margin_top(6) + box.append(create) + box.append(status) + group.add(box) + + dialog.present(parent) + + +def _password_group() -> Adw.PreferencesGroup: + g_pw = Adw.PreferencesGroup(title="Password") + cur = Adw.PasswordEntryRow(title="Current password") + new = Adw.PasswordEntryRow(title="New password") + confirm_row = Adw.PasswordEntryRow(title="Confirm new password") + g_pw.add(cur) + g_pw.add(new) + g_pw.add(confirm_row) + + btn = Gtk.Button(label="Change password") + btn.add_css_class("suggested-action") + btn.add_css_class("pill") + btn.set_halign(Gtk.Align.START) + status = Gtk.Label(xalign=0.0, wrap=True) + status.add_css_class("dim-label") + + def on_click(_b) -> None: + c, n, cf = cur.get_text(), new.get_text(), confirm_row.get_text() + if not c or not n: + status.set_label("Enter your current and a new password.") + return + if n != cf: + status.set_label("New passwords don't match.") + return + btn.set_sensitive(False) + status.set_label("Changing…") + + def worker() -> None: + ok, msg = account.change_password(c, n) + + def done() -> bool: + status.set_label(("✔ " if ok else "✘ ") + msg) + btn.set_sensitive(True) + if ok: + for e in (cur, new, confirm_row): + e.set_text("") + return False + + GLib.idle_add(done) + + threading.Thread(target=worker, daemon=True).start() + + btn.connect("clicked", on_click) + box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8) + box.set_margin_top(6) + box.append(btn) + box.append(status) + g_pw.add(box) + return g_pw + + +def _load_avatar(av: Adw.Avatar) -> None: + path = avatar.current_path() + if path: + try: + av.set_custom_image(Gdk.Texture.new_from_filename(path)) + except GLib.Error: + pass + + +def _pick_avatar(parent: Gtk.Widget, av: Adw.Avatar) -> None: + dialog = Gtk.FileDialog(title="Choose a picture") + filt = Gtk.FileFilter() + filt.set_name("Images") + for p in ("*.png", "*.jpg", "*.jpeg", "*.webp", "*.gif"): + filt.add_pattern(p) + store = Gio.ListStore.new(Gtk.FileFilter) + store.append(filt) + dialog.set_filters(store) + + def done(d, res) -> None: + try: + f = d.open_finish(res) + except GLib.Error: + return + try: + pb = GdkPixbuf.Pixbuf.new_from_file_at_scale(f.get_path(), 256, 256, True) + out = Path.home() / ".cache" / "taninux" + out.mkdir(parents=True, exist_ok=True) + dst = out / "avatar.png" + pb.savev(str(dst), "png", [], []) + except (GLib.Error, OSError): + return + avatar.set_own(str(dst)) + _load_avatar(av) + + dialog.open(parent.get_root(), None, done) + + +def _row(title: str, value: str) -> Adw.ActionRow: + row = Adw.ActionRow(title=title) + lbl = Gtk.Label(label=value, xalign=1.0) + lbl.add_css_class("dim-label") + lbl.set_selectable(True) + row.add_suffix(lbl) + return row diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/vpn.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/vpn.py new file mode 100644 index 0000000..48e9107 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/vpn.py @@ -0,0 +1,130 @@ +"""VPN — NetworkManager-VPNs verbinden/importieren + DNS für die aktive +Verbindung. Alles über nmcli (user-level, polkit-backed).""" +from __future__ import annotations + +import gi + +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") +from gi.repository import Adw, Gio, GLib, Gtk # noqa: E402 + +from taninux.core import network +from taninux.gui.process import ProcessRunner + + +def build() -> Gtk.Widget: + page = Adw.PreferencesPage() + + if not network.available(): + g = Adw.PreferencesGroup(title="VPN") + g.add(Adw.ActionRow(title="NetworkManager is not running")) + page.add(g) + return page + + runner = ProcessRunner() + dns_runner = ProcessRunner() + dns_queue: list = [] + groups: list[Adw.PreferencesGroup] = [] + + # ---- VPN ---- + def _vpn_group() -> Adw.PreferencesGroup: + g = Adw.PreferencesGroup(title="VPN") + add_btn = Gtk.Button(icon_name="list-add-symbolic") + add_btn.add_css_class("flat") + add_btn.set_valign(Gtk.Align.CENTER) + add_btn.set_tooltip_text("Import .ovpn or WireGuard .conf") + add_btn.connect("clicked", lambda _b: _import()) + g.set_header_suffix(add_btn) + + vpns = network.vpn_connections() + if not vpns: + g.add(Adw.ActionRow(title="No VPN configured", + subtitle="Import a .ovpn or WireGuard .conf file")) + return g + for v in vpns: + row = Adw.ActionRow(title=v.name, subtitle=v.vtype) + sw = Gtk.Switch() + sw.set_active(v.active) + sw.set_valign(Gtk.Align.CENTER) + sw.connect("state-set", lambda _s, on, n=v.name: ( + runner.run(network.vpn_up_argv(n) if on else network.vpn_down_argv(n)), + False)[1]) + row.add_suffix(sw) + g.add(row) + return g + + def _import() -> None: + dialog = Gtk.FileDialog(title="Import VPN config") + filt = Gtk.FileFilter() + filt.set_name("VPN configs") + filt.add_pattern("*.ovpn") + filt.add_pattern("*.conf") + store = Gio.ListStore.new(Gtk.FileFilter) + store.append(filt) + dialog.set_filters(store) + + def done(d, res) -> None: + try: + f = d.open_finish(res) + except GLib.Error: + return + argv = network.vpn_import_argv(f.get_path()) + if argv: + runner.run(argv) + + dialog.open(page.get_root(), None, done) + + # ---- DNS ---- + def _dns_group() -> Adw.PreferencesGroup: + g = Adw.PreferencesGroup(title="DNS") + conn = network.primary_connection() + if not conn: + g.add(Adw.ActionRow(title="No active connection")) + return g + names = list(network.DNS_PRESETS.keys()) + ["Custom"] + row = Adw.ComboRow(title="DNS servers", subtitle=conn) + row.set_model(Gtk.StringList.new(names)) + cur = network.get_dns(conn) + sel = next((i for i, (_, v) in enumerate(network.DNS_PRESETS.items()) if v == cur), None) + if cur and sel is None: + sel = len(names) - 1 # Custom + row.set_selected(sel if sel is not None else 0) + g.add(row) + + custom = Adw.EntryRow(title="Custom servers") + custom.set_text(cur) + custom.set_visible(names[row.get_selected()] == "Custom") + g.add(custom) + + def apply(servers: str) -> None: + dns_queue.clear() + dns_queue.extend(network.set_dns_cmds(conn, servers)) + _dns_next() + + def on_sel(r, _p) -> None: + choice = names[r.get_selected()] + custom.set_visible(choice == "Custom") + if choice != "Custom": + apply(network.DNS_PRESETS[choice]) + + row.connect("notify::selected", on_sel) + custom.connect("apply", lambda e: apply(e.get_text().strip())) + return g + + def _dns_next() -> None: + if dns_queue: + dns_runner.run(dns_queue.pop(0)) + + dns_runner.connect("done", lambda _r, _c: _dns_next()) + + def _render() -> None: + for g in groups: + page.remove(g) + groups.clear() + for g in (_vpn_group(), _dns_group()): + page.add(g) + groups.append(g) + + runner.connect("done", lambda _r, _c: _render()) + GLib.idle_add(lambda: (_render(), False)[1]) + return page diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/wallpaper.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/wallpaper.py new file mode 100644 index 0000000..1106c06 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/pages/wallpaper.py @@ -0,0 +1,312 @@ +"""Wallpaper — Thumbnail-Grid, Klick setzt das Bild via waypaper (Backend swaybg). + +RAM-KRITISCH: Eine frühere Version hat ~30 GB belegt, weil Bilder voll geladen +wurden. Deshalb gilt hier strikt: + * Thumbnails NUR skaliert laden: `Pixbuf.new_from_file_at_scale(p, W, H, True)`. + NIE `Pixbuf.new_from_file` oder `Gtk.Picture.set_filename` (laden voll!). + * Aus dem skalierten Pixbuf eine `Gdk.Texture` machen, Pixbuf NICHT festhalten. + * CAP: max. die ersten 60 Bilder rendern (sortiert), Rest als „+N more"-Hinweis. + * Laden ASYNC in EINEM Worker-Thread: pro Bild Pixbuf im Thread skalieren, + dann via GLib.idle_add die Karte bauen + einhängen — graduell, nichts blockt. + +Diese Funktionalität lebt jetzt in `wallpaper_group()` und wird in die +Appearance-Seite eingefaltet. `build()` bleibt als dünner Wrapper bestehen. +""" +from __future__ import annotations + +import threading + +import gi + +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") +gi.require_version("GdkPixbuf", "2.0") +from gi.repository import Adw, Gdk, GdkPixbuf, Gio, GLib, Gtk # noqa: E402 + +from taninux.core import wallpaper +from taninux.gui.console import RunnerCard +from taninux.gui.process import ProcessRunner + +# RAM-Grenzen: kleine Thumbnails, harter Cap auf die Anzahl gerenderter Bilder. +THUMB_W = 220 +THUMB_H = 124 +MAX_THUMBS = 60 + +# Höhenbegrenzung des Grids als Row — verhindert, dass die Gruppe die ganze +# Appearance-Seite sprengt; der Grid scrollt intern. +GRID_MIN_H = 300 +GRID_MAX_H = 360 + + +def wallpaper_group() -> Adw.PreferencesGroup: + """Komplette Wallpaper-UI als eine PreferencesGroup (zum Einhängen in + Appearance). Enthält die Steuerzeile als Rows und den RAM-sicheren + Thumbnail-Grid in einem höhenbegrenzten, intern scrollenden Container.""" + if not wallpaper.available(): + return _missing_group() + + group = Adw.PreferencesGroup( + title="Wallpaper", + description="Click a thumbnail to set it.", + ) + + runner = ProcessRunner() + mons = wallpaper.monitors() + + # -- Steuerzeile: Monitor-Combo als ComboRow --------------------------- + monitor_row = Adw.ComboRow(title="Monitor") + monitor_row.set_model(Gtk.StringList.new(mons)) + monitor_row.set_selected(0) + group.add(monitor_row) + + # -- Steuerzeile: Aktionen als ActionRow mit Suffix-Buttons ------------ + actions_row = Adw.ActionRow(title="Wallpapers") + random_btn = Gtk.Button(label="Random") + random_btn.add_css_class("pill") + random_btn.set_valign(Gtk.Align.CENTER) + open_btn = Gtk.Button(label="Choose folder") + open_btn.add_css_class("pill") + open_btn.set_valign(Gtk.Align.CENTER) + refresh_btn = Gtk.Button(icon_name="view-refresh-symbolic") + refresh_btn.add_css_class("flat") + refresh_btn.set_valign(Gtk.Align.CENTER) + refresh_btn.set_tooltip_text("Reload thumbnails") + for b in (random_btn, open_btn, refresh_btn): + actions_row.add_suffix(b) + group.add(actions_row) + + # -- Status-/Spinner-Zeile --------------------------------------------- + status = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8) + status.set_halign(Gtk.Align.START) + status.set_margin_top(6) + status.set_margin_bottom(6) + spinner = Adw.Spinner() + spinner.set_valign(Gtk.Align.CENTER) + status_label = Gtk.Label(xalign=0) + status_label.add_css_class("dim-label") + status.append(spinner) + status.append(status_label) + + # -- Grid in höhenbegrenzter ScrolledWindow ---------------------------- + flow = Gtk.FlowBox() + flow.set_selection_mode(Gtk.SelectionMode.NONE) + flow.set_homogeneous(True) + flow.set_min_children_per_line(2) + flow.set_max_children_per_line(4) + flow.set_column_spacing(12) + flow.set_row_spacing(12) + flow.set_margin_top(6) + flow.set_margin_bottom(6) + flow.set_valign(Gtk.Align.START) + + scroller = Gtk.ScrolledWindow(hexpand=True) + scroller.set_min_content_height(GRID_MIN_H) + scroller.set_max_content_height(GRID_MAX_H) + scroller.set_propagate_natural_height(False) + scroller.set_child(flow) + + # Status + Grid zusammen in einer Row der Gruppe. + grid_holder = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) + grid_holder.set_margin_top(6) + grid_holder.append(status) + grid_holder.append(scroller) + group.add(grid_holder) + + # -- Aktionen ---------------------------------------------------------- + def _open_folder(_b: Gtk.Button) -> None: + from pathlib import Path + dialog = Gtk.FileDialog(title="Choose wallpaper folder") + cur = str(wallpaper.wallpaper_dir()) + try: + if Path(cur).is_dir(): + dialog.set_initial_folder(Gio.File.new_for_path(cur)) + except GLib.Error: + pass + + def done(d, res) -> None: + try: + f = d.select_folder_finish(res) + except GLib.Error: + return + if f and f.get_path(): + wallpaper.set_folder(f.get_path()) + _load() + + dialog.select_folder(_b.get_root(), None, done) + + def _random(_b: Gtk.Button) -> None: + runner.run(wallpaper.random_argv()) + + open_btn.connect("clicked", _open_folder) + random_btn.connect("clicked", _random) + + # Veränderlicher Zustand fürs Markieren der aktuellen Karte. + state = {"current": wallpaper.current(), "cards": {}} # path-str -> card + + def _selected_monitor() -> str: + return mons[monitor_row.get_selected()] + + def _set(path) -> None: + runner.run(wallpaper.set_argv(path, _selected_monitor())) + + def _mark_current() -> None: + cur = state["current"] + for p, card in state["cards"].items(): + if p == cur: + card.add_css_class("accent") + else: + card.remove_css_class("accent") + + def on_done(_r: ProcessRunner, code: int) -> None: + if code == 0: + # Leichtgewichtiger Reload des aktuellen Pfads (kein Bildladen). + state["current"] = wallpaper.current() + _mark_current() + + runner.connect("done", on_done) + + # -- ASYNC Thumbnail-Laden --------------------------------------------- + # Worker erzeugt skalierte Pixbufs für die ersten MAX_THUMBS Pfade; pro Bild + # baut idle_add die Karte (Picture aus Texture) und hängt sie ein. Der + # Pixbuf bleibt lokal -> wird nach Texture-Erzeugung freigegeben. + load_token = {"id": 0} + + def _clear_flow() -> None: + child = flow.get_first_child() + while child is not None: + nxt = child.get_next_sibling() + flow.remove(child) + child = nxt + state["cards"].clear() + + def _make_card(path, texture) -> Gtk.Widget: + card = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4) + card.add_css_class("card") + card.set_overflow(Gtk.Overflow.HIDDEN) + + pic = Gtk.Picture() + pic.set_content_fit(Gtk.ContentFit.COVER) + pic.set_size_request(THUMB_W, THUMB_H) + pic.set_paintable(texture) + card.append(pic) + + name = Gtk.Label(label=path.name, xalign=0) + name.set_ellipsize(3) # Pango.EllipsizeMode.END + name.add_css_class("caption") + name.set_margin_start(6) + name.set_margin_end(6) + name.set_margin_bottom(4) + card.append(name) + + # Klick = setzen. GestureClick auf der Karte. + click = Gtk.GestureClick() + click.connect("released", lambda *_a, p=path: _set(p)) + card.add_controller(click) + card.set_cursor(Gdk.Cursor.new_from_name("pointer", None)) + return card + + def _add_card(path, texture, token: int) -> bool: + if token != load_token["id"]: + return False # veralteter Ladevorgang + card = _make_card(path, texture) + flow.append(card) + state["cards"][str(path)] = card + if str(path) == state["current"]: + card.add_css_class("accent") + return False + + def _add_more(extra: int, token: int) -> bool: + if token != load_token["id"]: + return False + box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) + box.set_valign(Gtk.Align.CENTER) + box.set_halign(Gtk.Align.CENTER) + box.set_size_request(THUMB_W, THUMB_H) + lbl = Gtk.Label(label=f"… +{extra} more") + lbl.add_css_class("dim-label") + more_btn = Gtk.Button(label="Choose folder") + more_btn.add_css_class("pill") + more_btn.connect("clicked", _open_folder) + box.append(lbl) + box.append(more_btn) + flow.append(box) + return False + + def _finish(token: int) -> bool: + if token != load_token["id"]: + return False + spinner.set_visible(False) + return False + + def _worker(items, extra: int, token: int) -> None: + for path in items: + if token != load_token["id"]: + return + try: + # NUR skaliert laden — niemals voll! + pb = GdkPixbuf.Pixbuf.new_from_file_at_scale( + str(path), THUMB_W, THUMB_H, True + ) + except (GLib.Error, OSError): + continue + texture = Gdk.Texture.new_for_pixbuf(pb) + pb = None # Pixbuf freigeben, nur die Texture lebt weiter + GLib.idle_add(_add_card, path, texture, token) + if extra > 0: + GLib.idle_add(_add_more, extra, token) + GLib.idle_add(_finish, token) + + def _load() -> None: + load_token["id"] += 1 + token = load_token["id"] + _clear_flow() + state["current"] = wallpaper.current() + all_paths = wallpaper.list_wallpapers() + total = len(all_paths) + if total == 0: + spinner.set_visible(False) + status_label.set_text( + f"No images in {wallpaper.wallpaper_dir()}. " + "Use Open folder to add wallpapers." + ) + return + items = all_paths[:MAX_THUMBS] + extra = max(0, total - MAX_THUMBS) + spinner.set_visible(True) + shown = len(items) + status_label.set_text( + f"{shown} of {total} wallpapers" + if extra else f"{total} wallpapers" + ) + threading.Thread( + target=_worker, args=(items, extra, token), daemon=True + ).start() + + refresh_btn.connect("clicked", lambda _b: _load()) + GLib.idle_add(lambda: (_load(), False)[1]) + + return group + + +def build() -> Gtk.Widget: + """Dünner Wrapper: eigene PreferencesPage, die nur die wallpaper_group() + enthält. Bleibt erhalten, falls noch jemand build() aufruft.""" + page = Adw.PreferencesPage() + page.add(wallpaper_group()) + return page + + +def _missing_group() -> Adw.PreferencesGroup: + """Gruppe mit Install-Hinweis + RunnerCard, falls waypaper fehlt.""" + group = Adw.PreferencesGroup( + title="Wallpaper", + description="waypaper is not installed. Install it to manage wallpapers.", + ) + row = Adw.ActionRow() + row.set_child(RunnerCard( + "Install waypaper", + ["pacman", "-S", "--needed", "--noconfirm", "waypaper"], + privileged=True, + )) + group.add(row) + return group diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/polkit/ch.gabrielevarano.Taninux.policy b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/polkit/ch.gabrielevarano.Taninux.policy new file mode 100644 index 0000000..c1fc9cc --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/polkit/ch.gabrielevarano.Taninux.policy @@ -0,0 +1,31 @@ + + + + + TANINUX + https://gabrielevarano.ch + + + TANINUX-Systemwartung ausführen + Authentifizierung nötig, um Systempakete zu verwalten + + auth_admin + auth_admin + auth_admin_keep + + /usr/lib/taninux/taninux-helper + true + + diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/process.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/process.py new file mode 100644 index 0000000..78e314b --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/process.py @@ -0,0 +1,105 @@ +"""Async Subprocess-Runner fürs GTK-Frontend. + +Das GTK-Pendant zu `taninux.core.runner` / `streaming` (die an die Textual-App +hängen). Hier läuft alles non-blocking über `Gio.Subprocess`: stdout wird +zeilenweise im GLib-Mainloop gelesen, ohne den UI-Thread zu blockieren. + +Privilegierte Befehle (pacman -S/-R/-Syu, bootctl set-default) laufen NICHT mehr +direkt als `pkexec pacman …` (= beliebiges root), sondern werden durch den +dedizierten `taninux-helper` geschleust: `pkexec `. Der Helper +prüft das argv gegen eine feste Allowlist und lehnt alles andere ab. + +Damit das ohne Terminal-Passwortprompt funktioniert, braucht es einen laufenden +polkit-Auth-Agent **und** idealerweise die mitgelieferte Policy (siehe +gui/polkit/). Ohne Policy fragt pkexec generisch nach dem Passwort eines Admins. +""" +from __future__ import annotations + +import os + +import gi + +gi.require_version("Gtk", "4.0") +from gi.repository import Gio, GLib, GObject # noqa: E402 + +# Pfad zum root-Helper. Bevorzugt der Systempfad (so zeigt auch die +# polkit-Policy via exec.path dorthin). Fallback: der Helper im Repo, damit +# das Frontend ohne Installation (Dev / uneditierte Quelle) funktioniert. +_SYSTEM_HELPER = "/usr/lib/taninux/taninux-helper" +_REPO_HELPER = os.path.join(os.path.dirname(__file__), "helper", "taninux-helper") + + +def _helper_path() -> str: + return _SYSTEM_HELPER if os.path.exists(_SYSTEM_HELPER) else _REPO_HELPER + + +class ProcessRunner(GObject.Object): + """Führt einen Befehl async aus und emittiert Zeilen + Exit-Code. + + Signals: + line(str): eine Zeile stdout/stderr (ohne Newline) + done(int): Exit-Code, wenn der Prozess endet (-1 = Start fehlgeschlagen) + """ + + __gsignals__ = { + "line": (GObject.SignalFlags.RUN_FIRST, None, (str,)), + "done": (GObject.SignalFlags.RUN_FIRST, None, (int,)), + } + + def __init__(self) -> None: + super().__init__() + self._proc: Gio.Subprocess | None = None + + @property + def running(self) -> bool: + return self._proc is not None + + def run(self, argv: list[str], *, privileged: bool = False) -> None: + if self.running: + self.emit("line", "⚠ a process is already running") + return + + # privileged: argv durch pkexec + Helper schleusen (Helper validiert + # gegen Allowlist). Sonst direkt ausführen. + if privileged: + real_argv = ["pkexec", _helper_path(), *argv] + else: + real_argv = list(argv) + self.emit("line", f"$ {' '.join(real_argv)}") + + flags = Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_MERGE + try: + self._proc = Gio.Subprocess.new(real_argv, flags) + except GLib.Error as err: + self.emit("line", f"✘ failed to start process: {err.message}") + self.emit("done", -1) + return + + stdout = self._proc.get_stdout_pipe() + reader = Gio.DataInputStream.new(stdout) + self._read_next(reader) + self._proc.wait_async(None, self._on_wait) + + # -- intern ----------------------------------------------------------- + + def _read_next(self, reader: Gio.DataInputStream) -> None: + reader.read_line_async(GLib.PRIORITY_DEFAULT, None, self._on_line, None) + + def _on_line(self, reader: Gio.DataInputStream, result, _user) -> None: + try: + raw, _length = reader.read_line_finish_utf8(result) + except GLib.Error: + return + if raw is None: # EOF + return + self.emit("line", raw) + self._read_next(reader) + + def _on_wait(self, proc: Gio.Subprocess, result) -> None: + try: + proc.wait_finish(result) + code = proc.get_exit_status() + except GLib.Error: + code = -1 + self._proc = None + self.emit("done", code) diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/style.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/style.py new file mode 100644 index 0000000..7f49cb0 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/style.py @@ -0,0 +1,271 @@ +"""Theming: Fuji-Akzente + Light/Dark — über CSS-Recoloring. + +libadwaita ignoriert klassische GTK-Themes bewusst; Theming läuft daher über +das Überschreiben der benannten Adwaita-Farben (`@define-color accent_bg_color …`) +per CSS-Provider auf USER-Priorität. Die Akzent-Hues kommen 1:1 aus +`taninux.core.theme`, damit GTK-App und TUI dieselbe Palette teilen. +""" +from __future__ import annotations + +import os +import subprocess +from pathlib import Path + +import gi + +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") +from gi.repository import Adw, Gdk, Gtk # noqa: E402 + +# Akzent-Daten leben GTK-frei in gui.accents (geteilt mit config + eww-Consumer). +from taninux.core import theme as T # noqa: F401 (CSS nutzt @window_bg_color etc.) +from taninux.gui.accents import ( # noqa: F401 (re-export für settings.py) + DEFAULT_ACCENT, + FUJI_ACCENTS, + Accent, + by_key as accent, +) + +# Mapping Schema-Key → Adwaita ColorScheme +_SCHEMES = { + "system": Adw.ColorScheme.DEFAULT, + "light": Adw.ColorScheme.FORCE_LIGHT, + "dark": Adw.ColorScheme.FORCE_DARK, +} + +_provider: Gtk.CssProvider | None = None + + +def _contrast_fg(hex_color: str) -> str: + """Lesbarer Vordergrund (dunkel/weiß) für Text auf der Akzentfläche.""" + h = hex_color.lstrip("#") + r, g, b = (int(h[i:i + 2], 16) / 255 for i in (0, 2, 4)) + lum = 0.2126 * r + 0.7152 * g + 0.0588 * b + return T.YORU if lum > 0.6 else "#ffffff" + + +def _build_css(acc: Accent) -> bytes: + fg = _contrast_fg(acc.hex) + # Akzent-Override + Swatch-Styling (Swatches sind statisch, aber billig + # mitzubauen). Swatch = runder Farbknopf, aktiver bekommt einen Ring. + swatches = "\n".join( + f".swatch-{a.key} {{ background-color: {a.hex}; }}" + for a in FUJI_ACCENTS + ) + return f""" +@define-color accent_bg_color {acc.hex}; +@define-color accent_fg_color {fg}; +@define-color accent_color {acc.hex}; + +/* pill controls — all buttons & dropdowns are stadium pills */ +button, menubutton > button, dropdown, dropdown > button, +combobox button, splitbutton > button, entry, spinbutton, +.pill {{ border-radius: 999px; }} + +.fuji-swatch {{ + min-width: 26px; + min-height: 26px; + padding: 0; + border-radius: 50%; + box-shadow: inset 0 0 0 1px alpha(currentColor, 0.15); +}} +.fuji-swatch:checked {{ + box-shadow: inset 0 0 0 2px @window_bg_color, 0 0 0 2px @accent_bg_color; +}} +{swatches} +""".encode() + + +def apply(color_scheme: str, accent_key: str) -> None: + """Setzt Color-Scheme (StyleManager) + Akzent (CSS-Provider).""" + global _provider + + mgr = Adw.StyleManager.get_default() + mgr.set_color_scheme(_SCHEMES.get(color_scheme, Adw.ColorScheme.DEFAULT)) + + if _provider is None: + _provider = Gtk.CssProvider() + display = Gdk.Display.get_default() + if display is not None: + Gtk.StyleContext.add_provider_for_display( + display, _provider, Gtk.STYLE_PROVIDER_PRIORITY_USER + ) + _provider.load_from_data(_build_css(accent(accent_key))) + + +def apply_persisted() -> None: + """Beim Start aufrufen — lädt die gespeicherte Präferenz.""" + from taninux.gui import config + + cfg = config.load() + apply(cfg["color_scheme"], cfg["accent"]) + + +# -------------------------------------------------------------------------- +# Global theme (system-wide GTK4 + GTK3) — full dynamic Fuji recolour. +# -------------------------------------------------------------------------- +# Writes the picked Fuji accent + monochrome light/dark SURFACES to the global +# GTK4 (libadwaita) and GTK3 user CSS, and sets gsettings so EVERY app follows +# (not just this one). Dark base = #0f0f0f, light base = #f0f0f0; the accent is +# the chosen Fuji hue. All buttons & dropdowns are pills. GTK4 rides on the +# existing `@import "libadwaita.css"`; GTK3 rides on adw-gtk3. + +# Monochrome surfaces derived from the two base values (#0f0f0f / #f0f0f0). +_DARK_SURF = dict(bg="#0f0f0f", bg_alt="#191919", bg_hi="#262626", view="#141414", + card="#191919", headerbar="#0f0f0f", fg="#f0f0f0", + border="rgba(240,240,240,0.10)") +_LIGHT_SURF = dict(bg="#f0f0f0", bg_alt="#e4e4e4", bg_hi="#d6d6d6", view="#fafafa", + card="#ffffff", headerbar="#e8e8e8", fg="#0f0f0f", + border="rgba(15,15,15,0.12)") + +_PILLS_GTK4 = ( + "button, menubutton > button, dropdown, dropdown > button, combobox button,\n" + "splitbutton > button, entry, spinbutton, .pill { border-radius: 999px; }\n" + ".card, popover > contents, .osd { border-radius: 14px; }\n" +) +_PILLS_GTK3 = ( + "button, combobox button, combobox, entry, spinbutton,\n" + "notebook > header > tabs > tab { border-radius: 999px; }\n" +) + + +def _config_base() -> Path: + return Path(os.environ.get("XDG_CONFIG_HOME") or str(Path.home() / ".config")) + + +def _gtk4_config_dir() -> Path: + return _config_base() / "gtk-4.0" + + +def _resolve_dark(color_scheme: str) -> bool: + """True if the global theme should be dark. 'system' → read gsettings.""" + if color_scheme == "dark": + return True + if color_scheme == "light": + return False + try: + out = subprocess.run( + ["gsettings", "get", "org.gnome.desktop.interface", "color-scheme"], + capture_output=True, text=True, timeout=5, check=False, + ).stdout + except (FileNotFoundError, subprocess.TimeoutExpired): + return True + return "prefer-light" not in out + + +def _gtk4_full(acc: Accent, dark: bool) -> str: + p = _DARK_SURF if dark else _LIGHT_SURF + fg = _contrast_fg(acc.hex) + return f"""/* TANINUX-managed — global GTK4/libadwaita Fuji theme. Do not edit by hand. + {'dark' if dark else 'light'} · accent {acc.label} ({acc.hex}) */ +@define-color accent_color {acc.hex}; +@define-color accent_bg_color {acc.hex}; +@define-color accent_fg_color {fg}; +@define-color window_bg_color {p['bg']}; +@define-color window_fg_color {p['fg']}; +@define-color view_bg_color {p['view']}; +@define-color view_fg_color {p['fg']}; +@define-color headerbar_bg_color {p['headerbar']}; +@define-color headerbar_fg_color {p['fg']}; +@define-color headerbar_backdrop_color {p['bg']}; +@define-color headerbar_border_color {p['fg']}; +@define-color card_bg_color {p['card']}; +@define-color card_fg_color {p['fg']}; +@define-color dialog_bg_color {p['view']}; +@define-color dialog_fg_color {p['fg']}; +@define-color popover_bg_color {p['bg_alt']}; +@define-color popover_fg_color {p['fg']}; +@define-color sidebar_bg_color {p['bg_alt']}; +@define-color sidebar_fg_color {p['fg']}; +{_PILLS_GTK4}""" + + +def _gtk3_full(acc: Accent, dark: bool) -> str: + p = _DARK_SURF if dark else _LIGHT_SURF + fg = _contrast_fg(acc.hex) + return f"""/* TANINUX-managed — global GTK3 Fuji theme (rides adw-gtk3). Do not edit. */ +@define-color accent_color {acc.hex}; +@define-color accent_bg_color {acc.hex}; +@define-color accent_fg_color {fg}; +@define-color window_bg_color {p['bg']}; +@define-color window_fg_color {p['fg']}; +@define-color view_bg_color {p['view']}; +@define-color headerbar_bg_color {p['headerbar']}; +@define-color card_bg_color {p['card']}; +@define-color popover_bg_color {p['bg_alt']}; +@define-color theme_bg_color {p['bg']}; +@define-color theme_fg_color {p['fg']}; +@define-color theme_base_color {p['view']}; +@define-color theme_text_color {p['fg']}; +@define-color theme_selected_bg_color {acc.hex}; +@define-color theme_selected_fg_color {fg}; +@define-color borders {p['border']}; +{_PILLS_GTK3}/* The per-widget accent (sliders, tab underline, switches) is driven by the + GNOME accent-color gsetting (adw-gtk3 honours it and wins on specificity); + write_global_theme maps the Fuji hue to the nearest preset. */ +""" + + +# Fuji accent → nearest GNOME accent-color preset (for GTK3/adw-gtk3, which +# follows the gsetting; GTK4/libadwaita uses the exact Fuji hex from our css). +_ACCENT_PRESET = { + "ume": "purple", "shinkai": "blue", "take": "green", + "chikyu": "yellow", "beni": "red", +} + + +def _set_gsettings(dark: bool, system: bool, accent_key: str) -> None: + pairs = [ + ("color-scheme", "default" if system else ("prefer-dark" if dark else "prefer-light")), + ("gtk-theme", "adw-gtk3-dark" if dark else "adw-gtk3"), + ("accent-color", _ACCENT_PRESET.get(accent_key, "purple")), + ] + for key, val in pairs: + try: + subprocess.run( + ["gsettings", "set", "org.gnome.desktop.interface", key, val], + check=False, timeout=5, + ) + except (FileNotFoundError, subprocess.TimeoutExpired): + pass + + +def write_global_theme(color_scheme: str, accent_key: str) -> None: + """Write the full Fuji theme (GTK4 + GTK3) globally + sync gsettings. + + Drives EVERY app: surfaces follow light/dark (#f0f0f0 / #0f0f0f), the accent + is the chosen Fuji hue, all buttons & dropdowns are pills. + """ + acc = accent(accent_key) + dark = _resolve_dark(color_scheme) + + # GTK4 → ~/.config/gtk-4.0/libadwaita.css (imported by the adw-gtk3 gtk.css). + try: + g4 = _gtk4_config_dir() + g4.mkdir(parents=True, exist_ok=True) + (g4 / "libadwaita.css").write_text(_gtk4_full(acc, dark)) + tweaks = g4 / "libadwaita-tweaks.css" + if not tweaks.exists(): # silence the adw-gtk3 @import for it + tweaks.write_text("/* TANINUX (intentionally empty) */\n") + except OSError: + pass + + # GTK3 → ~/.config/gtk-3.0/gtk.css (overrides on top of adw-gtk3). + try: + g3 = _config_base() / "gtk-3.0" + g3.mkdir(parents=True, exist_ok=True) + dst = g3 / "gtk.css" + backup = g3 / "gtk.css.pre-tanin" + if dst.exists() and not dst.is_symlink() and not backup.exists(): + backup.write_text(dst.read_text()) # keep a real pre-existing file once + dst.write_text(_gtk3_full(acc, dark)) + except OSError: + pass + + _set_gsettings(dark, color_scheme == "system", accent_key) + + +def write_global_accent(accent_key: str) -> None: + """Back-compat shim — now writes the full theme for the saved scheme.""" + from taninux.gui import config + write_global_theme(config.load()["color_scheme"], accent_key) diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/window.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/window.py new file mode 100644 index 0000000..9a2554c --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/gui/window.py @@ -0,0 +1,144 @@ +"""Hauptfenster: Sidebar (Pillar-Liste) + Content-Stack. + +Adw.NavigationSplitView gibt das responsive Sidebar/Content-Layout, das man +von GNOME-Settings kennt — auf schmalen Fenstern klappt es automatisch zu +einer Drill-Down-Navigation um. +""" +from __future__ import annotations + +import gi + +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") +from gi.repository import Adw, Gtk # noqa: E402 + +from taninux.gui.pages import SECTIONS + + +class MainWindow(Adw.ApplicationWindow): + def __init__(self, app: Adw.Application, sections=None, title: str = "TANINUX") -> None: + super().__init__(application=app, title=title) + self.set_default_size(960, 680) + + self._sections = sections if sections is not None else SECTIONS + self._pages = [p for s in self._sections for p in s.pages] + self._app_title = title + self._stack = Gtk.Stack() + self._stack.set_transition_type(Gtk.StackTransitionType.CROSSFADE) + self._title = Adw.WindowTitle(title=title) + self._rows: dict[str, Gtk.ListBoxRow] = {} + self._query = "" + + split = Adw.NavigationSplitView() + split.set_sidebar(self._build_sidebar()) + split.set_content(self._build_content()) + self.set_content(split) + + # lazy: Seiten erst beim ersten Anzeigen bauen; Startseite vorwählen + self._built: set[str] = set() + if self._pages: + self._listbox.select_row(self._rows[self._pages[0].key]) + + # -- Sidebar ------------------------------------------------------- + + def _build_sidebar(self) -> Adw.NavigationPage: + self._listbox = Gtk.ListBox() + self._listbox.add_css_class("navigation-sidebar") + self._listbox.set_selection_mode(Gtk.SelectionMode.SINGLE) + self._listbox.connect("row-selected", self._on_row_selected) + self._listbox.set_filter_func(self._filter_row) + + for section in self._sections: + if section.title: + self._listbox.append(self._header_row(section.title)) + for page in section.pages: + row = Gtk.ListBoxRow() + row._page = page # type: ignore[attr-defined] + box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12) + box.set_margin_top(10) + box.set_margin_bottom(10) + box.set_margin_start(8) + box.set_margin_end(8) + box.append(Gtk.Image.new_from_icon_name(page.icon)) + box.append(Gtk.Label(label=page.label, xalign=0.0)) + row.set_child(box) + self._rows[page.key] = row + self._listbox.append(row) + + scroller = Gtk.ScrolledWindow(vexpand=True) + scroller.set_child(self._listbox) + + search = Gtk.SearchEntry() + search.set_placeholder_text("Search settings") + search.set_margin_top(6) + search.set_margin_bottom(6) + search.set_margin_start(6) + search.set_margin_end(6) + search.connect("search-changed", self._on_search) + + box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + box.append(search) + box.append(scroller) + + toolbar = Adw.ToolbarView() + toolbar.add_top_bar(Adw.HeaderBar()) + toolbar.set_content(box) + + nav = Adw.NavigationPage(title=self._app_title, child=toolbar) + nav.set_tag("sidebar") + return nav + + def _header_row(self, title: str) -> Gtk.ListBoxRow: + """Nicht-wählbare Sektions-Überschrift.""" + row = Gtk.ListBoxRow() + row.set_selectable(False) + row.set_activatable(False) + label = Gtk.Label(label=title, xalign=0.0) + label.add_css_class("dim-label") + label.add_css_class("caption-heading") + label.set_margin_top(14) + label.set_margin_bottom(2) + label.set_margin_start(12) + row.set_child(label) + return row + + # -- Content ------------------------------------------------------- + + def _build_content(self) -> Adw.NavigationPage: + toolbar = Adw.ToolbarView() + header = Adw.HeaderBar() + header.set_title_widget(self._title) + toolbar.add_top_bar(header) + toolbar.set_content(self._stack) + nav = Adw.NavigationPage(title="Content", child=toolbar) + nav.set_tag("content") + return nav + + # -- Suche --------------------------------------------------------- + + def _on_search(self, entry: Gtk.SearchEntry) -> None: + self._query = entry.get_text().strip().lower() + self._listbox.invalidate_filter() + + def _filter_row(self, row: Gtk.ListBoxRow) -> bool: + page = getattr(row, "_page", None) + if page is None: # Sektions-Header + return not self._query + if not self._query: + return True + hay = (page.label + " " + " ".join(page.keywords)).lower() + return self._query in hay + + # -- Navigation ---------------------------------------------------- + + def _on_row_selected(self, _box: Gtk.ListBox, row: Gtk.ListBoxRow | None) -> None: + page = getattr(row, "_page", None) if row is not None else None + if page is not None: + self._select(page) + + def _select(self, page) -> None: + if page.key not in self._built: + self._stack.add_named(page.build(), page.key) + self._built.add(page.key) + self._stack.set_visible_child_name(page.key) + self._title.set_title(page.label) diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/health/__init__.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/health/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/health/pillar.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/health/pillar.py new file mode 100644 index 0000000..750df06 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/health/pillar.py @@ -0,0 +1,13 @@ +from __future__ import annotations + +from taninux.health.screen import HealthView +from taninux.pillars.base import Pillar + + +HEALTH = Pillar( + key="h", + label="health", + tagline="diagnose · perf · drivers", + icon="◈", + factory=HealthView, +) diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/health/screen.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/health/screen.py new file mode 100644 index 0000000..3faf472 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/health/screen.py @@ -0,0 +1,248 @@ +"""Health-Pillar — Live-Dashboard mit System-Diagnose-Sektionen.""" +from __future__ import annotations + +from textual import work +from textual.app import ComposeResult +from textual.binding import Binding +from textual.containers import VerticalScroll +from textual.widgets import Static + +from taninux.core import sysinfo +from taninux.core import theme as T + + +def _human_size(n: int) -> str: + f = float(n) + for unit in ("B", "K", "M", "G", "T"): + if f < 1024 or unit == "T": + return f"{f:.0f}{unit}" if unit == "B" else f"{f:.1f}{unit}" + f /= 1024 + return f"{f:.1f}T" + + +def _human_duration(seconds: float) -> str: + s = int(seconds) + if s < 60: + return f"{s}s" + if s < 3600: + return f"{s // 60}m {s % 60}s" + h, rem = divmod(s, 3600) + if h < 24: + return f"{h}h {rem // 60}m" + d, h = divmod(h, 24) + return f"{d}d {h}h" + + +def _status_glyph(ok: bool, warn: bool = False) -> str: + if warn: + return f"[{T.CHIKYU}]◐[/]" + return f"[{T.TAKE}]●[/]" if ok else f"[{T.RED}]●[/]" + + +class HealthView(VerticalScroll): + """Live system health dashboard. Auto-refresh alle 10s.""" + + DEFAULT_CSS = f""" + HealthView {{ + background: {T.YORU}; + padding: 1 2; + scrollbar-color: {T.BASE2} {T.YORU}; + scrollbar-background: {T.YORU}; + scrollbar-corner-color: {T.YORU}; + scrollbar-size-vertical: 1; + }} + HealthView Static {{ + background: {T.YORU}; + margin-bottom: 1; + }} + """ + + BINDINGS = [ + Binding("r", "refresh", "refresh"), + ] + + def compose(self) -> ComposeResult: + yield Static("", id="h-system", markup=True) + yield Static("", id="h-perf", markup=True) + yield Static("", id="h-gpu", markup=True) + yield Static("", id="h-storage", markup=True) + yield Static("", id="h-net", markup=True) + yield Static("", id="h-services", markup=True) + yield Static("", id="h-logs", markup=True) + + def on_mount(self) -> None: + self._refresh_worker() + self.set_interval(10.0, self._refresh_worker) + + def action_refresh(self) -> None: + self._refresh_worker() + + @work(thread=True, exclusive=True, group="health-refresh") + def _refresh_worker(self) -> None: + # subprocess-heavy calls passieren im worker, render dann auf UI thread + self.app.call_from_thread(self._refresh_all) + + # ---- section builders ---- + + def _section(self, title: str) -> str: + return f"[{T.UME}]{title}[/]" + + def _refresh_all(self) -> None: + self._render_system() + self._render_perf() + self._render_gpu() + self._render_storage() + self._render_net() + self._render_services() + self._render_logs() + + def _render_system(self) -> None: + boot = sysinfo.boot_time_seconds() + up = sysinfo.uptime_seconds() + lines = [ + self._section("◈ system"), + f" [{T.SHINKAI}]host[/] [{T.YUKI}]{sysinfo.hostname()}[/]", + f" [{T.SHINKAI}]distro[/] [{T.YUKI}]{sysinfo.distro_pretty()}[/]", + f" [{T.SHINKAI}]kernel[/] [{T.YUKI}]{sysinfo.kernel_release()}[/]", + f" [{T.SHINKAI}]uptime[/] [{T.YUKI}]{_human_duration(up)}[/]", + ] + if boot is not None: + color = T.TAKE if boot < 30 else (T.CHIKYU if boot < 60 else T.RED) + lines.append(f" [{T.SHINKAI}]boot[/] [{color}]{boot:.1f}s[/]") + self.query_one("#h-system", Static).update("\n".join(lines)) + + def _render_perf(self) -> None: + mem = sysinfo.memory() + la = sysinfo.load_avg() + ncpu = sysinfo.cpu_count() + total = mem.get("MemTotal", 0) + avail = mem.get("MemAvailable", 0) + used = total - avail + used_pct = (used * 100 / total) if total else 0 + swap_total = mem.get("SwapTotal", 0) + swap_free = mem.get("SwapFree", 0) + swap_used = swap_total - swap_free + swap_pct = (swap_used * 100 / swap_total) if swap_total else 0 + + load_color = T.TAKE if la[0] < ncpu * 0.7 else (T.CHIKYU if la[0] < ncpu else T.RED) + mem_color = T.TAKE if used_pct < 60 else (T.CHIKYU if used_pct < 85 else T.RED) + swap_color = T.TAKE if swap_pct < 20 else (T.CHIKYU if swap_pct < 60 else T.RED) + + lines = [ + self._section("⊙ perf"), + f" [{T.SHINKAI}]load[/] " + f"[{load_color}]{la[0]:.2f}[/] " + f"[{T.BASE5}]{la[1]:.2f}[/] " + f"[{T.BASE5}]{la[2]:.2f}[/] " + f"[{T.BASE4}]({ncpu} cpus)[/]", + f" [{T.SHINKAI}]memory[/] " + f"[{mem_color}]{_human_size(used)}[/] [{T.BASE5}]/[/] " + f"[{T.YUKI}]{_human_size(total)}[/] " + f"[{T.BASE4}]({used_pct:.0f}%)[/]", + ] + if swap_total: + lines.append( + f" [{T.SHINKAI}]swap[/] " + f"[{swap_color}]{_human_size(swap_used)}[/] [{T.BASE5}]/[/] " + f"[{T.YUKI}]{_human_size(swap_total)}[/] " + f"[{T.BASE4}]({swap_pct:.0f}%)[/]" + ) + else: + lines.append(f" [{T.SHINKAI}]swap[/] [{T.BASE5}]nicht konfiguriert[/]") + self.query_one("#h-perf", Static).update("\n".join(lines)) + + def _render_gpu(self) -> None: + gpus = sysinfo.gpus() + lines = [self._section("▣ gpu")] + if not gpus: + lines.append(f" [{T.BASE5}](keine gpu erkannt)[/]") + else: + for g in gpus: + vendor_color = { + "nvidia": T.TAKE, + "amd": T.RED, + "intel": T.SHINKAI, + }.get(g.vendor, T.BASE4) + driver_part = ( + f"[{T.SHINKAI}]driver[/] [{T.CHIKYU}]{g.driver}[/]" + if g.driver else f"[{T.BASE4}](kein driver geladen)[/]" + ) + lines.append( + f" [{vendor_color}]{g.vendor:7s}[/] " + f"[{T.YUKI}]{g.model[:60]}[/]" + ) + lines.append(f" {driver_part}") + lines.append(f" [{T.SHINKAI}]audio[/] [{T.YUKI}]{sysinfo.audio_server()}[/]") + self.query_one("#h-gpu", Static).update("\n".join(lines)) + + def _render_storage(self) -> None: + disks = sysinfo.disks() + lines = [self._section("◇ storage")] + if not disks: + lines.append(f" [{T.BASE5}](keine disks)[/]") + else: + for d in disks: + kind = "hdd" if d.rotational else "ssd" + kind_color = T.CHIKYU if d.rotational else T.SHINKAI + lines.append( + f" [{kind_color}]{kind}[/] " + f"[{T.YUKI}]/dev/{d.name}[/] " + f"[{T.CHIKYU}]{_human_size(d.size_bytes)}[/] " + f"[{T.BASE5}]{d.model}[/]" + ) + self.query_one("#h-storage", Static).update("\n".join(lines)) + + def _render_net(self) -> None: + ifaces = sysinfo.network_interfaces() + lines = [self._section("⇆ netzwerk")] + if not ifaces: + lines.append(f" [{T.BASE5}](keine interfaces)[/]") + else: + for iface in ifaces: + if iface.name == "lo": + continue + state_color = { + "UP": T.TAKE, + "DOWN": T.BASE3, + "UNKNOWN": T.BASE4, + }.get(iface.state, T.BASE4) + addrs = " ".join(iface.addrs) if iface.addrs else "—" + lines.append( + f" [{state_color}]●[/] " + f"[{T.YUKI}]{iface.name:12s}[/] " + f"[{T.BASE5}]{iface.state:8s}[/] " + f"[{T.BASE4}]{addrs}[/]" + ) + self.query_one("#h-net", Static).update("\n".join(lines)) + + def _render_services(self) -> None: + failed = sysinfo.systemctl_failed() + lines = [self._section("◉ services")] + if not failed: + lines.append( + f" [{T.TAKE}]●[/] [{T.YUKI}]0[/] [{T.BASE5}]failed units[/]" + ) + else: + lines.append( + f" [{T.RED}]●[/] [{T.YUKI}]{len(failed)}[/] [{T.BASE5}]failed units[/]" + ) + for unit in failed[:10]: + lines.append(f" [{T.RED}]{unit}[/]") + if len(failed) > 10: + lines.append(f" [{T.BASE4}]… +{len(failed) - 10} mehr[/]") + self.query_one("#h-services", Static).update("\n".join(lines)) + + def _render_logs(self) -> None: + today = sysinfo.journal_errors_today() + boot = sysinfo.journal_errors_boot() + def label(n: int, txt: str) -> str: + if n < 0: + return f" [{T.BASE3}]?[/] [{T.BASE5}]{txt} (nicht verfügbar)[/]" + color = T.TAKE if n == 0 else (T.CHIKYU if n < 20 else T.RED) + return f" [{color}]●[/] [{T.YUKI}]{n}[/] [{T.BASE5}]{txt}[/]" + lines = [ + self._section("✎ logs"), + label(today, "errors heute (journalctl -p 3)"), + label(boot, "errors seit boot (journalctl -p 3 -b)"), + ] + self.query_one("#h-logs", Static).update("\n".join(lines)) diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/kernel/__init__.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/kernel/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/kernel/pillar.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/kernel/pillar.py new file mode 100644 index 0000000..623c343 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/kernel/pillar.py @@ -0,0 +1,13 @@ +from __future__ import annotations + +from taninux.kernel.screen import KernelView +from taninux.pillars.base import Pillar + + +KERNEL = Pillar( + key="k", + label="kernel", + tagline="liste · wechsel · prune", + icon="⊙", + factory=KernelView, +) diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/kernel/screen.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/kernel/screen.py new file mode 100644 index 0000000..d157678 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/kernel/screen.py @@ -0,0 +1,506 @@ +"""Kernel-Pillar — Liste verfügbarer Kernels, Status, Install/Switch, Boot-Default.""" +from __future__ import annotations + +import subprocess +from dataclasses import dataclass +from pathlib import Path + +from textual.app import ComposeResult +from textual.binding import Binding +from textual.containers import VerticalScroll +from textual.widgets import DataTable, Static + +from taninux.core import bootloader, pacman as pac, sudo, sysinfo +from taninux.core import theme as T +from taninux.core.confirm import ConfirmModal +from taninux.core.runner import Command, run_interactive +from taninux.core.streaming import RunnerScreen, StreamCommand +from taninux.core.sudo_modal import SudoModal + + +# Standard Arch-Kernel-Varianten +KERNEL_VARIANTS = [ + ("linux", "stable, latest mainline"), + ("linux-lts", "long-term support — stabilster"), + ("linux-zen", "performance + interactivity tweaks (gut für gaming/desktop)"), + ("linux-hardened", "security-fokus, härteren default-configs"), + ("linux-rt", "real-time scheduler (audio, robotics)"), + ("linux-rt-lts", "real-time + LTS"), +] + + +@dataclass +class KernelInfo: + name: str # 'linux-zen' + description: str + installed: bool + headers_installed: bool + version: str | None + is_running: bool + boot_entry: Path | None # systemd-boot .conf entry, falls match + is_default_boot: bool # ist der konfigurierte default für nächsten boot + + +def _installed_with_versions() -> dict[str, str]: + try: + r = subprocess.run( + ["pacman", "-Q"], + capture_output=True, text=True, check=False, timeout=10, + ) + except (FileNotFoundError, subprocess.TimeoutExpired): + return {} + out: dict[str, str] = {} + for line in r.stdout.splitlines(): + parts = line.split() + if len(parts) >= 2: + out[parts[0]] = parts[1] + return out + + +def _is_running(variant: str, running: str) -> bool: + if variant == "linux": + return not any(s in running for s in ("zen", "lts", "hardened", "rt")) + suffix = variant.removeprefix("linux-") + return suffix in running + + +def list_kernels() -> tuple[list[KernelInfo], bootloader.BootloaderInfo]: + installed = _installed_with_versions() + running = sysinfo.kernel_release() + bl = bootloader.detect() + out: list[KernelInfo] = [] + for name, desc in KERNEL_VARIANTS: + ver = installed.get(name) + headers_ver = installed.get(f"{name}-headers") + entry = bootloader.entry_for_kernel(bl, name) + out.append(KernelInfo( + name=name, + description=desc, + installed=ver is not None, + headers_installed=headers_ver is not None, + version=ver, + is_running=ver is not None and _is_running(name, running), + boot_entry=entry, + is_default_boot=entry is not None and bootloader.is_default(bl, entry), + )) + return out, bl + + +class KernelView(VerticalScroll): + DEFAULT_CSS = f""" + KernelView {{ + background: {T.YORU}; + padding: 1 2; + scrollbar-color: {T.BASE2} {T.YORU}; + scrollbar-background: {T.YORU}; + scrollbar-corner-color: {T.YORU}; + scrollbar-size-vertical: 1; + }} + KernelView Static {{ + background: {T.YORU}; + margin-bottom: 1; + }} + KernelView #k-table {{ + height: auto; + background: {T.YORU}; + color: {T.FG_ALT}; + border: none; + overflow-x: hidden; + }} + KernelView #k-table:focus {{ + border: none; + }} + KernelView DataTable > .datatable--header {{ + background: {T.YORU}; + color: {T.SHINKAI}; + }} + KernelView DataTable > .datatable--cursor {{ + background: {T.BASE1}; + color: {T.YUKI}; + }} + KernelView DataTable > .datatable--even-row {{ + background: {T.YORU}; + }} + KernelView DataTable > .datatable--odd-row {{ + background: {T.YORU}; + }} + """ + + BINDINGS = [ + Binding("r", "rescan", "rescan"), + Binding("i", "install", "install +headers"), + Binding("H", "install_headers", "headers only", show=False), + Binding("b", "set_default", "boot-default"), + Binding("x", "remove", "entfernen"), + Binding("j", "cursor_down", "down", show=False), + Binding("k", "cursor_up", "up", show=False), + ] + + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + self.kernels: list[KernelInfo] = [] + self.bl: bootloader.BootloaderInfo | None = None + + def compose(self) -> ComposeResult: + yield Static("", id="k-intro", markup=True) + yield DataTable(id="k-table", cursor_type="row", zebra_stripes=False) + yield Static("", id="k-detail", markup=True) + yield Static("", id="k-flash", markup=True) + + def on_mount(self) -> None: + t = self.query_one("#k-table", DataTable) + t.add_column("", width=2, key="glyph") + t.add_column("kernel", width=18, key="name") + t.add_column("status", width=12, key="status") + t.add_column("version", width=18, key="version") + t.add_column("hdr", width=4, key="headers") + t.add_column("boot", width=10, key="boot") + t.add_column("beschreibung", key="desc") + self.action_rescan() + + def action_rescan(self) -> None: + pac.invalidate() + self.kernels, self.bl = list_kernels() + self._refresh_view() + + # ---- install / install-headers / remove ---- + + def action_install(self) -> None: + cur = self._current() + if cur is None: + return + if cur.installed and cur.headers_installed: + self._flash("kernel + headers bereits installiert", color=T.BASE4) + return + pkgs = [] + if not cur.installed: + pkgs.append(cur.name) + if not cur.headers_installed: + pkgs.append(f"{cur.name}-headers") + body = ( + f"installieren?\n\n" + f" pakete: {', '.join(pkgs)}\n" + f" beschreibung: {cur.description}\n\n" + f"nach dem install:\n" + f" · mkinitcpio läuft automatisch via pacman-hook\n" + f" · bootloader-config wird {self._bootloader_hint()}" + ) + self.app.push_screen( + ConfirmModal(f"install {' + '.join(pkgs)}?", body, danger=False), + lambda ok: self._after_install(ok, pkgs, cur), + ) + + def action_install_headers(self) -> None: + cur = self._current() + if cur is None or not cur.installed: + self._flash("kernel nicht installiert", color=T.BASE4) + return + if cur.headers_installed: + self._flash("headers schon installiert", color=T.BASE4) + return + pkg = f"{cur.name}-headers" + self.app.push_screen( + ConfirmModal( + f"install {pkg}?", + f"nur headers für {cur.name} installieren?", + danger=False, + ), + lambda ok: self._after_install(ok, [pkg], cur), + ) + + def action_remove(self) -> None: + cur = self._current() + if cur is None or not cur.installed: + self._flash("nicht installiert", color=T.BASE4) + return + if cur.is_running: + self._flash( + "kannst den laufenden kernel nicht entfernen — wechsle erst per reboot", + color=T.RED, + ) + return + if cur.is_default_boot: + self._flash( + "kernel ist als boot-default gesetzt — wechsle erst (`b`)", + color=T.RED, + ) + return + pkgs = [cur.name] + if cur.headers_installed: + pkgs.append(f"{cur.name}-headers") + body = ( + f"entfernen?\n\n" + f" pakete: {', '.join(pkgs)}\n\n" + f" · sicherstellen dass ein anderer kernel installiert + bootbar ist\n" + f" · bootloader-config wird vom pacman-hook regeneriert" + ) + self.app.push_screen( + ConfirmModal(f"remove {cur.name}?", body, danger=True), + lambda ok: self._after_remove(ok, pkgs, cur), + ) + + # ---- boot default ---- + + def action_set_default(self) -> None: + cur = self._current() + if cur is None: + return + if self.bl is None or self.bl.kind == "unknown": + self._flash("bootloader unbekannt — siehe detail-pane", color=T.CHIKYU) + return + if not cur.installed: + self._flash("kernel nicht installiert — kann nicht booten", color=T.BASE4) + return + if cur.boot_entry is None: + if self.bl.kind == "systemd-boot": + self._flash( + f"kein boot-entry für {cur.name} gefunden in /boot/loader/entries/", + color=T.CHIKYU, + ) + else: + self._flash( + f"GRUB: manuell `sudo grub-set-default …` — siehe detail-pane", + color=T.CHIKYU, + ) + return + if cur.is_default_boot: + self._flash(f"{cur.name} ist schon boot-default", color=T.BASE4) + return + + entry_name = cur.boot_entry.name + body = ( + f"boot-default ändern?\n\n" + f" bootloader: {self.bl.kind}\n" + f" entry: {entry_name}\n" + f" kernel: {cur.name} ({cur.version})\n\n" + f" wird beim nächsten reboot gebootet." + ) + self.app.push_screen( + ConfirmModal(f"set boot-default → {cur.name}?", body, danger=False), + lambda ok: self._after_set_default(ok, cur, entry_name), + ) + + def _after_set_default(self, ok: bool | None, k: KernelInfo, entry_name: str) -> None: + if not ok: + self._flash("abgebrochen") + return + cmds = [StreamCommand( + label=f"set-default → {entry_name}", + argv=["sudo", "-n", "bootctl", "set-default", entry_name], + )] + self._run_with_sudo( + cmds, + on_done=lambda codes: self._after_set_default_done(codes, k, entry_name), + ) + + def _after_set_default_done(self, codes, k: KernelInfo, entry_name: str) -> None: + success = codes and codes[0] == 0 + self._flash( + f"boot-default → {k.name} ({entry_name})" if success + else "set-default fehlgeschlagen", + color=T.TAKE if success else T.RED, + ) + self.action_rescan() + + # ---- install/remove follow-up ---- + + def _after_install(self, ok: bool | None, pkgs: list[str], k: KernelInfo) -> None: + if not ok: + self._flash("abgebrochen") + return + helper = "paru" if sysinfo.has_binary("paru") else ( + "yay" if sysinfo.has_binary("yay") else None + ) + if helper: + argv = [ + helper, "-S", "--needed", + "--noconfirm", "--sudoloop", + "--skipreview", "--removemake", "--cleanafter", + *pkgs, + ] + else: + argv = ["sudo", "-n", "pacman", "-S", "--needed", "--noconfirm", *pkgs] + cmds = [StreamCommand(label=f"install {' + '.join(pkgs)}", argv=argv)] + self._run_with_sudo( + cmds, + on_done=lambda codes: self._after_install_done(codes, pkgs, k), + ) + + def _after_install_done(self, codes, pkgs, k: KernelInfo) -> None: + success = codes and codes[0] == 0 + if success: + msg = f"{', '.join(pkgs)} installiert" + if self.bl and self.bl.kind == "systemd-boot": + msg += " · systemd-boot regeneriert via hook" + elif self.bl and self.bl.kind == "grub": + msg += " · prüfe: sudo grub-mkconfig -o /boot/grub/grub.cfg" + self._flash(msg, color=T.TAKE) + else: + self._flash("install fehlgeschlagen — siehe runner-log", color=T.RED) + pac.invalidate() + self.action_rescan() + + def _after_remove(self, ok: bool | None, pkgs: list[str], k: KernelInfo) -> None: + if not ok: + self._flash("abgebrochen") + return + cmds = [StreamCommand( + label=f"remove {' + '.join(pkgs)}", + argv=["sudo", "-n", "pacman", "-Rns", "--noconfirm", *pkgs], + )] + self._run_with_sudo( + cmds, + on_done=lambda codes: self._after_remove_done(codes, k), + ) + + def _after_remove_done(self, codes, k: KernelInfo) -> None: + success = codes and codes[0] == 0 + pac.invalidate() + self._flash( + f"{k.name} entfernt" if success else f"remove fehlgeschlagen", + color=T.TAKE if success else T.RED, + ) + self.action_rescan() + + # ---- shared sudo + runner helper ---- + + def _run_with_sudo(self, cmds: list[StreamCommand], on_done) -> None: + """Stellt sicher dass sudo gecached ist, dann launched RunnerScreen.""" + needs_sudo = any( + c.argv[0] == "sudo" or c.argv[0] in ("paru", "yay") + for c in cmds + ) + if needs_sudo and not sudo.is_cached(): + self.app.push_screen( + SudoModal(), + lambda pw: self._sudo_then_run(pw, cmds, on_done), + ) + else: + self.app.push_screen(RunnerScreen(cmds), on_done) + + def _sudo_then_run(self, pw, cmds, on_done) -> None: + if not pw: + self._flash("kein passwort — abgebrochen") + return + if not sudo.cache(pw): + self._flash("falsches passwort", color=T.RED) + return + self.app.push_screen(RunnerScreen(cmds), on_done) + + # ---- nav ---- + + def action_cursor_down(self) -> None: + self.query_one("#k-table", DataTable).action_cursor_down() + + def action_cursor_up(self) -> None: + self.query_one("#k-table", DataTable).action_cursor_up() + + def _current(self) -> KernelInfo | None: + t = self.query_one("#k-table", DataTable) + r = t.cursor_row + if r is None or r < 0 or r >= len(self.kernels): + return None + return self.kernels[r] + + def _flash(self, msg: str, color: str | None = None) -> None: + color = color or T.BASE4 + self.query_one("#k-flash", Static).update(f"[{color}]{msg}[/]") + + def _bootloader_hint(self) -> str: + if self.bl is None or self.bl.kind == "unknown": + return "manuell regeneriert" + if self.bl.kind == "systemd-boot": + return "automatisch via pacman-hook regeneriert" + return "evt. via `sudo grub-mkconfig -o /boot/grub/grub.cfg` regeneriert" + + # ---- render ---- + + def _refresh_view(self) -> None: + running = sysinfo.kernel_release() + bl_kind = self.bl.kind if self.bl else "unknown" + bl_default = self.bl.default_entry if self.bl and self.bl.default_entry else "—" + + intro = ( + f"[{T.UME}]⊙ kernel[/]\n" + f"[{T.SHINKAI}]aktuell laufend[/] [{T.YUKI}]{running}[/]\n" + f"[{T.SHINKAI}]bootloader[/] [{T.YUKI}]{bl_kind}[/] " + f"[{T.BASE5}]default:[/] [{T.YUKI}]{bl_default}[/]\n" + f"[{T.BASE5}]`i` install +headers · `H` headers only · `b` boot-default · `x` entfernen[/]" + ) + self.query_one("#k-intro", Static).update(intro) + + t = self.query_one("#k-table", DataTable) + t.clear() + for k in self.kernels: + if k.is_running: + glyph = f"[{T.UME}]▲[/]" + status_str = f"[{T.UME}]laufend[/]" + elif k.installed: + glyph = f"[{T.TAKE}]●[/]" + status_str = f"[{T.TAKE}]installiert[/]" + else: + glyph = f"[{T.BASE3}]○[/]" + status_str = f"[{T.BASE4}]nicht inst.[/]" + ver_styled = f"[{T.CHIKYU}]{k.version}[/]" if k.version else f"[{T.BASE3}]—[/]" + + if not k.installed: + hdr = f"[{T.BASE3}]—[/]" + elif k.headers_installed: + hdr = f"[{T.TAKE}]✓[/]" + else: + hdr = f"[{T.CHIKYU}]✗[/]" + + if k.is_default_boot: + boot = f"[{T.UME}]default[/]" + elif k.boot_entry is not None: + boot = f"[{T.BASE5}]bootbar[/]" + elif k.installed and self.bl and self.bl.kind != "unknown": + boot = f"[{T.CHIKYU}]?[/]" + else: + boot = f"[{T.BASE3}]—[/]" + + t.add_row( + glyph, + f"[{T.YUKI}]{k.name}[/]", + status_str, + ver_styled, + hdr, + boot, + f"[{T.BASE5}]{k.description}[/]", + ) + + installed_count = sum(1 for k in self.kernels if k.installed) + with_headers = sum(1 for k in self.kernels if k.installed and k.headers_installed) + without_headers = installed_count - with_headers + + detail_lines = [ + f"[{T.SHINKAI}]übersicht[/]", + f" installiert: [{T.YUKI}]{installed_count}[/]", + f" headers ok: [{T.TAKE}]{with_headers}[/]" + + (f" · fehlt: [{T.CHIKYU}]{without_headers}[/]" if without_headers else ""), + ] + if self.bl and self.bl.kind == "systemd-boot": + detail_lines += [ + "", + f"[{T.SHINKAI}]systemd-boot[/]", + f" config: [{T.BASE5}]/boot/loader/loader.conf[/]", + f" default: [{T.YUKI}]{bl_default}[/]", + f" switch: [{T.CHIKYU}]sudo bootctl set-default [/]", + f" [{T.BASE5}](oder hier mit `b` auf die zeile)[/]", + ] + elif self.bl and self.bl.kind == "grub": + detail_lines += [ + "", + f"[{T.SHINKAI}]GRUB[/]", + f" config: [{T.BASE5}]/etc/default/grub + /boot/grub/grub.cfg[/]", + f" default: [{T.YUKI}]{bl_default}[/]", + f" switch: [{T.CHIKYU}]edit /etc/default/grub (GRUB_DEFAULT=…)[/]", + f" [{T.CHIKYU}]sudo grub-mkconfig -o /boot/grub/grub.cfg[/]", + ] + else: + detail_lines += [ + "", + f"[{T.SHINKAI}]bootloader[/]", + f" [{T.BASE5}]nicht erkannt — kein /boot/loader/loader.conf oder /boot/grub/grub.cfg[/]", + ] + self.query_one("#k-detail", Static).update("\n".join(detail_lines)) diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/maintain/__init__.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/maintain/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/maintain/pillar.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/maintain/pillar.py new file mode 100644 index 0000000..ca7e26b --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/maintain/pillar.py @@ -0,0 +1,13 @@ +from __future__ import annotations + +from taninux.maintain.screen import MaintainView +from taninux.pillars.base import Pillar + + +MAINTAIN = Pillar( + key="m", + label="maintain", + tagline="scan & clean", + icon="▣", + factory=MaintainView, +) diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/maintain/scanners/__init__.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/maintain/scanners/__init__.py new file mode 100644 index 0000000..86cb98a --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/maintain/scanners/__init__.py @@ -0,0 +1,113 @@ +"""Scanner-Registry: jeder Eintrag wird zu einem Tab in der TUI.""" +from __future__ import annotations + +from dataclasses import dataclass +from typing import Callable + +from taninux.maintain.scanners.aur import AurCacheScanner +from taninux.maintain.scanners.base import Scanner +from taninux.maintain.scanners.dotfolders import DotfoldersScanner +from taninux.maintain.scanners.flatpak import FlatpakDataScanner, FlatpakRuntimesScanner +from taninux.maintain.scanners.pacman import ( + PacmanCacheScanner, + PacmanOrphansScanner, + PacnewScanner, +) +from taninux.maintain.scanners.system import ( + SystemCoredumpScanner, + SystemJournalScanner, + SystemKernelsScanner, +) + + +@dataclass(frozen=True) +class ScannerSpec: + id: str + label: str + factory: Callable[[], Scanner] + # Welche TUI-Aktionen sind im jeweiligen Tab sinnvoll? + # - 'trash': → move-to-trash der User-File (dotfolders) + # - 'view': → nur anzeigen + Hinweis-Command im Detail-Pane + actions: str = "view" + advice: str = "" + + +SCANNERS: list[ScannerSpec] = [ + ScannerSpec( + id="dotfolders", + label="dotfolders", + factory=DotfoldersScanner, + actions="trash", + advice="", + ), + ScannerSpec( + id="orphans", + label="orphans", + factory=PacmanOrphansScanner, + actions="view", + advice="entfernen mit: sudo pacman -Rns {pkg}", + ), + ScannerSpec( + id="pacnew", + label="pacnew", + factory=PacnewScanner, + actions="view", + advice="review/merge mit: sudo pacdiff (oder: sudo $EDITOR {path})", + ), + ScannerSpec( + id="cache", + label="cache", + factory=PacmanCacheScanner, + actions="view", + advice="cleanup mit: sudo paccache -ruk0 (entfernt uninstalled + alte Versionen)", + ), + ScannerSpec( + id="flatpak-runtimes", + label="fp-runtimes", + factory=FlatpakRuntimesScanner, + actions="view", + advice="cleanup mit: flatpak uninstall --unused (löst Extension-Ketten korrekt auf)", + ), + ScannerSpec( + id="flatpak-data", + label="fp-data", + factory=FlatpakDataScanner, + actions="trash", + advice="", + ), + ScannerSpec( + id="aur-cache", + label="aur-cache", + factory=AurCacheScanner, + actions="trash", + advice="", + ), + ScannerSpec( + id="journal", + label="journal", + factory=SystemJournalScanner, + actions="view", + advice="vacuum: sudo journalctl --vacuum-time=2weeks (oder --vacuum-size=200M)", + ), + ScannerSpec( + id="coredumps", + label="coredumps", + factory=SystemCoredumpScanner, + actions="view", + advice="cleanup: sudo coredumpctl --since=2weeksago delete (oder: sudo rm /var/lib/systemd/coredump/*)", + ), + ScannerSpec( + id="kernels", + label="kernels", + factory=SystemKernelsScanner, + actions="view", + advice="entfernen: sudo rm -r {path} (vorsicht: nur wenn das System nicht mehr von diesem Kernel bootet!)", + ), +] + + +def by_id(scanner_id: str) -> ScannerSpec | None: + for s in SCANNERS: + if s.id == scanner_id: + return s + return None diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/maintain/scanners/_util.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/maintain/scanners/_util.py new file mode 100644 index 0000000..9f9aaa7 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/maintain/scanners/_util.py @@ -0,0 +1,36 @@ +"""Geteilte Helfer für Scanner-Module.""" +from __future__ import annotations + +import os +import time +from pathlib import Path + + +def dir_size(path: Path) -> int: + """Rekursive Grösse in Bytes. Symlinks werden nicht gefolgt.""" + if path.is_symlink(): + return 0 + if path.is_file(): + try: + return path.stat(follow_symlinks=False).st_size + except OSError: + return 0 + if not path.is_dir(): + return 0 + total = 0 + for root, _dirs, files in os.walk(path, followlinks=False, onerror=lambda _e: None): + for f in files: + try: + total += os.stat(os.path.join(root, f), follow_symlinks=False).st_size + except OSError: + continue + return total + + +def last_access_days(path: Path) -> int | None: + """Tage seit letzter Modifikation (mtime). None wenn nicht lesbar.""" + try: + mtime = path.stat().st_mtime + return int((time.time() - mtime) / 86400) + except OSError: + return None diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/maintain/scanners/aur.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/maintain/scanners/aur.py new file mode 100644 index 0000000..5def4f2 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/maintain/scanners/aur.py @@ -0,0 +1,86 @@ +"""AUR-Scanner: paru clone-cache (~/.cache/paru/clone//). + +Die installierten AUR-Pakete tauchen via `pacman -Qq` auf (AUR ist nur +ein Build-System, die Pakete landen ganz normal in der lokalen DB). +Ein Clone in `~/.cache/paru/` ohne entsprechendes installiertes Paket +ist also verwaist. +""" +from __future__ import annotations + +import os +import subprocess +from pathlib import Path + +from taninux.maintain.scanners._util import dir_size, last_access_days +from taninux.maintain.types import Category, Classification, Finding + + +PARU_CACHE = Path.home() / ".cache" / "paru" + + +def _pacman_installed() -> set[str]: + try: + r = subprocess.run( + ["pacman", "-Qq"], + capture_output=True, text=True, check=False, timeout=10, + ) + except (FileNotFoundError, subprocess.TimeoutExpired): + return set() + return {l.strip() for l in r.stdout.splitlines() if l.strip()} + + +class AurCacheScanner: + name = "aur-cache" + + def scan(self) -> list[Finding]: + clone_root = PARU_CACHE / "clone" + if not clone_root.is_dir(): + return [] + installed = _pacman_installed() + + findings: list[Finding] = [] + try: + entries = list(os.scandir(clone_root)) + except (OSError, PermissionError): + return [] + + for entry in entries: + try: + if not entry.is_dir(follow_symlinks=False): + continue + except OSError: + continue + pkg = entry.name + path = Path(entry.path) + size = dir_size(path) + days = last_access_days(path) + is_installed = pkg in installed + + if not is_installed: + cls = Classification.SAFE + reason = f"AUR-Paket {pkg} nicht installiert — Clone verwaist" + elif days is not None and days > 180: + cls = Classification.LIKELY_ORPHAN + reason = f"AUR-Clone von {pkg} seit {days}d unverändert" + else: + cls = Classification.ACTIVE + reason = f"AUR-Clone von {pkg} — installiert, möglicher Rebuild-Source" + + # Active wird in der Übersicht meist nicht gesized + if cls == Classification.ACTIVE: + size_to_show = 0 + else: + size_to_show = size + + findings.append(Finding( + path=path, + category=Category.AUR, + classification=cls, + size_bytes=size_to_show, + last_access_days=days, + reason=reason, + matched_package=pkg, + extra={"kind": "paru-clone", "pkg": pkg}, + )) + findings.sort(key=lambda f: f.sort_key) + return findings diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/maintain/scanners/base.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/maintain/scanners/base.py new file mode 100644 index 0000000..5c03648 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/maintain/scanners/base.py @@ -0,0 +1,11 @@ +from __future__ import annotations + +from typing import Protocol + +from taninux.maintain.types import Finding + + +class Scanner(Protocol): + name: str + + def scan(self) -> list[Finding]: ... diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/maintain/scanners/dotfolders.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/maintain/scanners/dotfolders.py new file mode 100644 index 0000000..01cb7f1 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/maintain/scanners/dotfolders.py @@ -0,0 +1,140 @@ +"""Scannt XDG-Dirs nach Foldern ohne klaren Paket-Owner.""" +from __future__ import annotations + +import os +import time +from pathlib import Path + +from taninux.core.package_index import build_index +from taninux.core.state import State +from taninux.maintain.types import Category, Classification, Finding + +# XDG-Dirs, die wir scannen. Reihenfolge = Anzeige-Reihenfolge. +XDG_DIRS = [ + Path.home() / ".config", + Path.home() / ".local" / "share", + Path.home() / ".cache", + Path.home() / ".local" / "state", +] + +# Schwellwerte (Tage) +SAFE_DAYS = 90 +LIKELY_DAYS = 30 + +# Folders die nie als Findings erscheinen sollen (system-managed) +HARD_EXCLUDE = { + "systemd", "dconf", "pulse", "pipewire", "wireplumber", + "fontconfig", "gtk-3.0", "gtk-4.0", "Trash", "mimeapps.list", + "user-dirs.dirs", "user-dirs.locale", +} + +# Pfade die andere Scanner besser kennen — hier ausblenden um Duplikate zu vermeiden +OWNED_BY_OTHER_SCANNERS = { + Path.home() / ".cache" / "paru", # aur-cache + Path.home() / ".cache" / "yay", # aur-cache (fallback) + Path.home() / ".var", # flatpak-data (theoretisch — XDG-Dirs treffen das nicht, aber sicherheitshalber) +} + + +def _dir_size(path: Path) -> int: + """Rekursive Grösse in Bytes. Symlinks werden nicht gefolgt.""" + total = 0 + try: + for entry in os.scandir(path): + try: + if entry.is_symlink(): + continue + if entry.is_file(follow_symlinks=False): + total += entry.stat(follow_symlinks=False).st_size + elif entry.is_dir(follow_symlinks=False): + total += _dir_size(Path(entry.path)) + except (OSError, PermissionError): + continue + except (OSError, PermissionError): + pass + return total + + +def _last_access_days(path: Path) -> int | None: + """Tage seit letzter Modifikation (mtime). None wenn nicht lesbar.""" + try: + mtime = path.stat().st_mtime + return int((time.time() - mtime) / 86400) + except OSError: + return None + + +def _classify(matched_pkg: str | None, days: int | None) -> tuple[Classification, str]: + if matched_pkg: + return Classification.ACTIVE, f"Paket installiert: {matched_pkg}" + if days is None: + return Classification.UNKNOWN, "Zugriffszeit unbekannt" + if days >= SAFE_DAYS: + return Classification.SAFE, f"Kein Paket, {days} Tage inaktiv" + if days >= LIKELY_DAYS: + return Classification.LIKELY_ORPHAN, f"Kein Paket, {days} Tage inaktiv" + return Classification.UNKNOWN, f"Kein Paket, aber nur {days} Tage inaktiv" + + +class DotfoldersScanner: + name = "dotfolders" + + def __init__(self, state: State | None = None) -> None: + self.state = state if state is not None else State.load() + + def scan(self) -> list[Finding]: + idx = build_index() + findings: list[Finding] = [] + + for base in XDG_DIRS: + if not base.is_dir(): + continue + try: + entries = list(os.scandir(base)) + except (OSError, PermissionError): + continue + + for entry in entries: + name = entry.name + if name.startswith(".") or name in HARD_EXCLUDE: + continue + path = Path(entry.path) + if path in OWNED_BY_OTHER_SCANNERS: + continue + if self.state.is_ignored(path): + continue + + matched = idx.matches(name) + days = _last_access_days(path) + + if self.state.is_whitelisted(path): + classification = Classification.WHITELISTED + reason = "Auf der Whitelist" + else: + classification, reason = _classify(matched, days) + + # Size nur für Cleanup-Kandidaten berechnen — ACTIVE/WHITELISTED + # können riesig sein (Steam-Library!) und das Tool ist nicht + # dafür da, installierte Apps zu vermessen. + if classification in (Classification.ACTIVE, Classification.WHITELISTED): + size = 0 + elif path.is_dir(): + size = _dir_size(path) + else: + try: + size = path.stat().st_size + except OSError: + size = 0 + + findings.append(Finding( + path=path, + category=Category.DOTFOLDERS, + classification=classification, + size_bytes=size, + last_access_days=days, + reason=reason, + matched_package=matched, + )) + + findings.sort(key=lambda f: f.sort_key) + return findings diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/maintain/scanners/flatpak.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/maintain/scanners/flatpak.py new file mode 100644 index 0000000..16f01f2 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/maintain/scanners/flatpak.py @@ -0,0 +1,167 @@ +"""Flatpak-Scanner: unused runtimes + leftover ~/.var/app data.""" +from __future__ import annotations + +import os +import subprocess +from pathlib import Path + +from taninux.maintain.scanners._util import dir_size, last_access_days +from taninux.maintain.scanners.pacman import _parse_size # gleiche Logik +from taninux.maintain.types import Category, Classification, Finding + + +def _flatpak_available() -> bool: + try: + subprocess.run( + ["flatpak", "--version"], + capture_output=True, check=False, timeout=5, + ) + return True + except (FileNotFoundError, subprocess.TimeoutExpired): + return False + + +def _installed_app_ids() -> set[str]: + try: + r = subprocess.run( + ["flatpak", "list", "--app", "--columns=application"], + capture_output=True, text=True, check=False, timeout=10, + ) + except (FileNotFoundError, subprocess.TimeoutExpired): + return set() + return { + line.strip() + for line in r.stdout.splitlines() + if line.strip() and line.strip() != "Application ID" + } + + +def _app_runtime_refs() -> set[str]: + """Welche Runtime-Refs werden von Apps benötigt? + + Refs haben Format `//`.""" + try: + r = subprocess.run( + ["flatpak", "list", "--app", "--columns=runtime"], + capture_output=True, text=True, check=False, timeout=10, + ) + except (FileNotFoundError, subprocess.TimeoutExpired): + return set() + refs: set[str] = set() + for line in r.stdout.splitlines(): + s = line.strip() + if not s or s == "Runtime": + continue + # Manche Outputs prefixen mit "runtime/" — abschneiden + if s.startswith("runtime/"): + s = s[len("runtime/"):] + refs.add(s) + return refs + + +# ----------------------------------------------------------------- runtimes + + +class FlatpakRuntimesScanner: + """Installierte Runtimes ohne direkten App-Bezug. + + Heuristik (nicht so präzise wie `flatpak uninstall --unused`, das auch + Extension-Ketten berücksichtigt) — Detail-Pane verweist auf den + kanonischen Befehl.""" + name = "flatpak-runtimes" + + def scan(self) -> list[Finding]: + if not _flatpak_available(): + return [] + + try: + r = subprocess.run( + ["flatpak", "list", "--runtime", + "--columns=application,branch,arch,size"], + capture_output=True, text=True, check=False, timeout=10, + ) + except (FileNotFoundError, subprocess.TimeoutExpired): + return [] + + used = _app_runtime_refs() + # used_app_ids enthält die Basis-IDs der genutzten Runtimes, ohne arch/branch + used_app_ids = {ref.split("/", 1)[0] for ref in used} + + findings: list[Finding] = [] + for line in r.stdout.splitlines(): + s = line.strip() + if not s or s.startswith("Application"): + continue + parts = s.split("\t") + if len(parts) < 3: + continue + app_id, branch, arch = parts[0], parts[1], parts[2] + size_str = parts[3] if len(parts) > 3 else "" + ref = f"{app_id}/{arch}/{branch}" + # Direkt-Match auf vollständigen Ref + if ref in used: + continue + # Substring-Match auf Application-ID, fängt Extensions ein + # (z.B. wenn org.freedesktop.Platform.GL.nvidia-... existiert + # weil org.freedesktop.Platform genutzt wird) + if any(app_id.startswith(u) for u in used_app_ids): + continue + findings.append(Finding( + path=Path(f"flatpak://{ref}"), + category=Category.FLATPAK, + classification=Classification.LIKELY_ORPHAN, + size_bytes=_parse_size(size_str) if size_str else 0, + last_access_days=None, + reason="Runtime hat keinen direkten App-Bezug", + matched_package=app_id, + extra={"kind": "runtime", "ref": ref, "branch": branch, "arch": arch}, + )) + findings.sort(key=lambda f: -f.size_bytes) + return findings + + +# ----------------------------------------------------------------- leftover data + + +class FlatpakDataScanner: + """`~/.var/app//` für Flatpaks die nicht (mehr) installiert sind.""" + name = "flatpak-data" + + def scan(self) -> list[Finding]: + var_app = Path.home() / ".var" / "app" + if not var_app.is_dir(): + return [] + if not _flatpak_available(): + return [] + + installed = _installed_app_ids() + findings: list[Finding] = [] + try: + entries = list(os.scandir(var_app)) + except (OSError, PermissionError): + return [] + + for entry in entries: + try: + if not entry.is_dir(follow_symlinks=False): + continue + except OSError: + continue + app_id = entry.name + if app_id in installed: + continue + path = Path(entry.path) + size = dir_size(path) + days = last_access_days(path) + findings.append(Finding( + path=path, + category=Category.FLATPAK, + classification=Classification.SAFE, + size_bytes=size, + last_access_days=days, + reason=f"Flatpak {app_id} nicht installiert — Daten verwaist", + matched_package=app_id, + extra={"kind": "leftover-data", "app_id": app_id}, + )) + findings.sort(key=lambda f: f.sort_key) + return findings diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/maintain/scanners/pacman.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/maintain/scanners/pacman.py new file mode 100644 index 0000000..ec51530 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/maintain/scanners/pacman.py @@ -0,0 +1,218 @@ +"""Pacman-Scanner: orphans, .pacnew/.pacsave Configs, /var/cache/pacman/pkg.""" +from __future__ import annotations + +import os +import re +import subprocess +from pathlib import Path + +from taninux.maintain.types import Category, Classification, Finding + + +_SIZE_RE = re.compile(r"([\d.]+)\s*(\w+)") +_SIZE_UNITS = { + "B": 1, "KiB": 1024, "MiB": 1024**2, "GiB": 1024**3, "TiB": 1024**4, + "K": 1024, "M": 1024**2, "G": 1024**3, "T": 1024**4, + "KB": 1000, "MB": 1000**2, "GB": 1000**3, "TB": 1000**4, +} + + +def _parse_size(s: str) -> int: + m = _SIZE_RE.search(s) + if not m: + return 0 + val = float(m.group(1)) + unit = m.group(2) + return int(val * _SIZE_UNITS.get(unit, 1)) + + +# ----------------------------------------------------------------- orphans + + +class PacmanOrphansScanner: + """`pacman -Qdt` — als Dependency installiert, hat aber keinen Owner mehr.""" + name = "orphans" + + def scan(self) -> list[Finding]: + try: + r = subprocess.run( + ["pacman", "-Qdtq"], + capture_output=True, text=True, check=False, timeout=10, + ) + except (FileNotFoundError, subprocess.TimeoutExpired): + return [] + pkgs = [p.strip() for p in r.stdout.splitlines() if p.strip()] + if not pkgs: + return [] + + sizes = self._installed_sizes(pkgs) + findings: list[Finding] = [] + for pkg in pkgs: + findings.append(Finding( + path=Path(f"pkg://{pkg}"), + category=Category.PACMAN, + classification=Classification.LIKELY_ORPHAN, + size_bytes=sizes.get(pkg, 0), + last_access_days=None, + reason="Als Dependency installiert, hat keinen Owner mehr", + matched_package=pkg, + extra={"kind": "orphan", "pkg": pkg}, + )) + findings.sort(key=lambda f: -f.size_bytes) + return findings + + @staticmethod + def _installed_sizes(pkgs: list[str]) -> dict[str, int]: + try: + r = subprocess.run( + ["pacman", "-Qi", *pkgs], + capture_output=True, text=True, check=False, timeout=30, + ) + except (FileNotFoundError, subprocess.TimeoutExpired): + return {} + sizes: dict[str, int] = {} + current: str | None = None + for line in r.stdout.splitlines(): + if line.startswith("Name") and ":" in line: + current = line.split(":", 1)[1].strip() + elif current and line.startswith("Installed Size") and ":" in line: + sizes[current] = _parse_size(line.split(":", 1)[1]) + return sizes + + +# ----------------------------------------------------------------- pacnew/pacsave + + +class PacnewScanner: + """`.pacnew` (neue Default-Configs vom Update) + `.pacsave` (Backups bei Removal).""" + name = "pacnew" + + def scan(self) -> list[Finding]: + findings: list[Finding] = [] + for root in (Path("/etc"), Path("/usr/share")): + if not root.is_dir(): + continue + for dirpath, _dirs, files in os.walk( + root, followlinks=False, onerror=lambda _e: None + ): + for fn in files: + if not (fn.endswith(".pacnew") or fn.endswith(".pacsave")): + continue + p = Path(dirpath) / fn + try: + size = p.stat(follow_symlinks=False).st_size + except OSError: + continue + kind = "pacnew" if fn.endswith(".pacnew") else "pacsave" + reason = ( + "Neue Default-Config vom Update — mit pacdiff mergen" + if kind == "pacnew" + else "Backup der alten Config nach Paket-Entfernung" + ) + findings.append(Finding( + path=p, + category=Category.PACMAN, + classification=Classification.UNKNOWN, + size_bytes=size, + last_access_days=None, + reason=reason, + extra={"kind": kind}, + )) + findings.sort(key=lambda f: str(f.path)) + return findings + + +# ----------------------------------------------------------------- pacman cache + + +class PacmanCacheScanner: + """`/var/cache/pacman/pkg/` — ältere Paket-Versionen + Pakete die nicht mehr installiert sind.""" + name = "cache" + + def scan(self) -> list[Finding]: + cache_dir = Path("/var/cache/pacman/pkg") + if not cache_dir.is_dir(): + return [] + + installed = self._installed_packages() + + # Files nach Paket-Name gruppieren + groups: dict[str, list[tuple[Path, int, float]]] = {} + try: + for entry in os.scandir(cache_dir): + if not entry.is_file(follow_symlinks=False): + continue + fn = entry.name + if not any(fn.endswith(ext) for ext in ( + ".pkg.tar.zst", ".pkg.tar.xz", ".pkg.tar.gz" + )): + continue + if fn.endswith(".sig"): + continue + pkg_name = self._extract_pkg_name(fn) + if not pkg_name: + continue + try: + st = entry.stat(follow_symlinks=False) + except OSError: + continue + groups.setdefault(pkg_name, []).append( + (Path(entry.path), st.st_size, st.st_mtime) + ) + except (OSError, PermissionError): + return [] + + findings: list[Finding] = [] + for pkg, items in groups.items(): + items.sort(key=lambda t: t[2], reverse=True) # newest first + is_installed = pkg in installed + for i, (path, size, _mtime) in enumerate(items): + # Neueste Version eines installierten Pakets → behalten als Reinstall-Quelle + if i == 0 and is_installed: + continue + if not is_installed: + cls = Classification.SAFE + reason = f"Paket {pkg} nicht installiert" + else: + cls = Classification.LIKELY_ORPHAN + reason = f"Ältere Version von {pkg} (neuere im Cache)" + findings.append(Finding( + path=path, + category=Category.PACMAN, + classification=cls, + size_bytes=size, + last_access_days=None, + reason=reason, + matched_package=pkg, + extra={"kind": "cache", "pkg": pkg}, + )) + findings.sort(key=lambda f: f.sort_key) + return findings + + @staticmethod + def _installed_packages() -> set[str]: + try: + r = subprocess.run( + ["pacman", "-Qq"], + capture_output=True, text=True, check=False, timeout=10, + ) + return {l.strip() for l in r.stdout.splitlines() if l.strip()} + except (FileNotFoundError, subprocess.TimeoutExpired): + return set() + + @staticmethod + def _extract_pkg_name(filename: str) -> str | None: + """`foo-bar-1.2.3-1-x86_64.pkg.tar.zst` → `foo-bar`""" + for ext in (".pkg.tar.zst", ".pkg.tar.xz", ".pkg.tar.gz"): + if filename.endswith(ext): + stem = filename[:-len(ext)] + break + else: + return None + # strip -, -, - + try: + stem = stem.rsplit("-", 1)[0] # arch + stem = stem.rsplit("-", 1)[0] # pkgrel + return stem.rsplit("-", 1)[0] # pkgver + except IndexError: + return None diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/maintain/scanners/system.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/maintain/scanners/system.py new file mode 100644 index 0000000..af5dd6a --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/maintain/scanners/system.py @@ -0,0 +1,229 @@ +"""System-wide Scanner: journal, coredumps, alte Kernel-Module.""" +from __future__ import annotations + +import json +import re +import subprocess +import time +from datetime import datetime, timezone +from pathlib import Path + +from taninux.maintain.scanners._util import dir_size +from taninux.maintain.types import Category, Classification, Finding + + +_SIZE_RE = re.compile(r"([\d.]+)\s*([KMGT])(?:iB)?", re.IGNORECASE) +_UNITS = {"K": 1024, "M": 1024**2, "G": 1024**3, "T": 1024**4} + + +def _parse_size_with_unit(s: str) -> int: + m = _SIZE_RE.search(s) + if not m: + return 0 + return int(float(m.group(1)) * _UNITS.get(m.group(2).upper(), 1)) + + +# ----------------------------------------------------------------- journal + + +class SystemJournalScanner: + """Gesamtgrösse des systemd-journal via `journalctl --disk-usage`.""" + name = "journal" + + def scan(self) -> list[Finding]: + try: + r = subprocess.run( + ["journalctl", "--disk-usage"], + capture_output=True, text=True, check=False, timeout=10, + ) + except (FileNotFoundError, subprocess.TimeoutExpired): + return [] + if r.returncode != 0: + return [] + + size = _parse_size_with_unit(r.stdout) + if size <= 0: + return [] + + if size > 2 * 1024**3: + cls = Classification.SAFE + reason = f"Journal nimmt {size/1024**3:.1f} GB ein — Vacuum dringend empfohlen" + elif size > 512 * 1024**2: + cls = Classification.LIKELY_ORPHAN + reason = f"Journal nimmt {size/1024**2:.0f} MB ein — Vacuum sinnvoll" + else: + cls = Classification.UNKNOWN + reason = f"Journal nimmt {size/1024**2:.0f} MB ein — unkritisch" + + return [Finding( + path=Path("journal://system"), + category=Category.SYSTEM, + classification=cls, + size_bytes=size, + last_access_days=None, + reason=reason, + matched_package="systemd-journal", + extra={"kind": "journal"}, + )] + + +# ----------------------------------------------------------------- coredumps + + +class SystemCoredumpScanner: + """Crash-Dumps via `coredumpctl`. Nutzt JSON-Output wenn verfügbar.""" + name = "coredumps" + + def scan(self) -> list[Finding]: + entries = self._list_via_json() + if entries is None: + entries = self._list_via_text() + if not entries: + return [] + + now = time.time() + findings: list[Finding] = [] + for e in entries: + ts = e.get("timestamp") + age_days = int((now - ts) / 86400) if ts else None + exe_name = Path(e.get("exe", "")).name or "?" + findings.append(Finding( + path=Path(f"coredump://{e.get('pid', '?')}/{exe_name}"), + category=Category.SYSTEM, + classification=Classification.LIKELY_ORPHAN, + size_bytes=e.get("size", 0), + last_access_days=age_days, + reason=f"Coredump von {exe_name} (sig {e.get('signal', '?')})", + matched_package=exe_name, + extra={"kind": "coredump", **e}, + )) + findings.sort(key=lambda f: -(f.size_bytes or 0)) + return findings + + def _list_via_json(self) -> list[dict] | None: + try: + r = subprocess.run( + ["coredumpctl", "--json=short", "list"], + capture_output=True, text=True, check=False, timeout=10, + ) + except (FileNotFoundError, subprocess.TimeoutExpired): + return None + if r.returncode != 0 or not r.stdout.strip(): + return None + try: + data = json.loads(r.stdout) + except json.JSONDecodeError: + return None + if not isinstance(data, list): + return None + entries: list[dict] = [] + for item in data: + # timestamp in microseconds since epoch + ts_us = item.get("time") + ts = ts_us / 1_000_000 if isinstance(ts_us, (int, float)) else None + # coredumpctl reports null/missing size when the corefile is gone + size_raw = item.get("size") + size = int(size_raw) if isinstance(size_raw, (int, float)) else 0 + entries.append({ + "pid": item.get("pid"), + "uid": item.get("uid"), + "signal": item.get("signal_name") or item.get("signal"), + "exe": item.get("exe", ""), + "size": size, + "timestamp": ts, + }) + return entries + + def _list_via_text(self) -> list[dict]: + """Fallback: parse `coredumpctl list` text output.""" + try: + r = subprocess.run( + ["coredumpctl", "list", "--no-pager"], + capture_output=True, text=True, check=False, timeout=10, + ) + except (FileNotFoundError, subprocess.TimeoutExpired): + return [] + if r.returncode not in (0, 1): # exit 1 = no dumps + return [] + entries: list[dict] = [] + # Header looks like: "TIME PID UID GID SIG COREFILE EXE SIZE" + for line in r.stdout.splitlines()[1:]: # skip header + parts = line.split() + if len(parts) < 7: + continue + try: + pid = int(parts[4]) + size = _parse_size_with_unit(parts[-1]) + exe = parts[-2] + sig = parts[7] if len(parts) > 7 else "?" + except (ValueError, IndexError): + continue + entries.append({ + "pid": pid, + "uid": None, + "signal": sig, + "exe": exe, + "size": size, + "timestamp": None, + }) + return entries + + +# ----------------------------------------------------------------- kernel modules + + +class SystemKernelsScanner: + """`/usr/lib/modules//` ohne pacman-Owner = orphaned Kernel-Module.""" + name = "kernels" + + def scan(self) -> list[Finding]: + modules_root = Path("/usr/lib/modules") + if not modules_root.is_dir(): + return [] + + kernel_dirs: list[Path] = [] + try: + for entry in modules_root.iterdir(): + if entry.is_symlink(): + continue + if entry.is_dir(): + kernel_dirs.append(entry) + except (OSError, PermissionError): + return [] + if not kernel_dirs: + return [] + + try: + r = subprocess.run( + ["pacman", "-Qo", *[str(d) for d in kernel_dirs]], + capture_output=True, text=True, check=False, timeout=30, + ) + except (FileNotFoundError, subprocess.TimeoutExpired): + return [] + + # pacman -Qo mit mehreren Args: stdout hat "X is owned by Y", + # stderr hat "error: No package owns Z" für Orphans. + owned: set[str] = set() + for line in r.stdout.splitlines(): + m = re.match(r"^(.+?)\s+is owned by ", line) + if m: + owned.add(m.group(1).rstrip("/")) + + findings: list[Finding] = [] + for d in kernel_dirs: + key = str(d).rstrip("/") + if key in owned: + continue + size = dir_size(d) + findings.append(Finding( + path=d, + category=Category.SYSTEM, + classification=Classification.SAFE, + size_bytes=size, + last_access_days=None, + reason="Kernel-Modul ohne Pacman-Eigentümer — Kernel-Paket weg, Module verwaist", + matched_package=None, + extra={"kind": "old-kernel-modules", "version": d.name}, + )) + findings.sort(key=lambda f: -f.size_bytes) + return findings diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/maintain/screen.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/maintain/screen.py new file mode 100644 index 0000000..933c71c --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/maintain/screen.py @@ -0,0 +1,565 @@ +"""Maintain-Pillar View — der bisherige archmaint-Code, jetzt als embeddable Widget.""" +from __future__ import annotations + +from collections import Counter +from pathlib import Path + +from textual import work +from textual.app import ComposeResult +from textual.binding import Binding +from textual.containers import Horizontal, Vertical +from textual.widgets import DataTable, Static, Tab, Tabs + +from taninux.core import theme as T +from taninux.core.confirm import ConfirmModal +from taninux.core.state import State +from taninux.core.trash import move_to_trash +from taninux.maintain.scanners import SCANNERS, ScannerSpec, by_id +from taninux.maintain.scanners.dotfolders import DotfoldersScanner +from taninux.maintain.types import ( + CLASS_COLOR, + CLASS_LABEL, + CLASSIFICATION_ORDER, + Classification, + Finding, + glyph, +) + + +def _human_size(n: int) -> str: + f = float(n) + for unit in ("B", "K", "M", "G", "T"): + if f < 1024 or unit == "T": + return f"{f:.0f}{unit}" if unit == "B" else f"{f:.1f}{unit}" + f /= 1024 + return f"{f:.1f}T" + + +def _format_days(d: int | None) -> str: + if d is None: + return "—" + if d == 0: + return "heute" + if d < 30: + return f"{d}d" + if d < 365: + return f"{d // 30}mo" + return f"{d // 365}y" + + +def _display_path(f: Finding) -> str: + s = str(f.path) + if s.startswith("pkg://"): + return s[6:] + home = str(Path.home()) + return "~" + s[len(home):] if s.startswith(home) else s + + +class MaintainView(Vertical): + """Maintain-Pillar als Widget — Tabs + Tabelle + Detail-Pane.""" + + DEFAULT_CSS = f""" + MaintainView {{ + background: {T.YORU}; + color: {T.YUKI}; + layout: vertical; + }} + + MaintainView Tabs {{ + background: {T.YORU}; + height: 3; + }} + MaintainView Tabs > #tabs-list {{ + background: {T.YORU}; + }} + MaintainView Tab {{ + background: {T.YORU}; + color: {T.BASE4}; + padding: 0 2; + }} + MaintainView Tab.-active {{ + color: {T.UME}; + background: {T.YORU}; + }} + MaintainView Underline {{ + background: {T.YORU}; + color: {T.BASE2}; + }} + MaintainView Underline > .underline--bar {{ + background: {T.YORU}; + color: {T.UME}; + }} + + MaintainView #m-stats {{ + height: 1; + padding: 0 2 0 2; + background: {T.YORU}; + color: {T.BASE5}; + }} + MaintainView #m-legend {{ + height: 1; + padding: 1 2 1 2; + background: {T.YORU}; + color: {T.BASE4}; + }} + MaintainView #m-main {{ + height: 1fr; + background: {T.YORU}; + }} + MaintainView #m-table {{ + width: 2fr; + background: {T.YORU}; + color: {T.FG_ALT}; + border: none; + padding-right: 2; + overflow-x: hidden; + scrollbar-size-vertical: 1; + scrollbar-color: {T.BASE2} {T.YORU}; + scrollbar-color-hover: {T.BASE3} {T.YORU}; + scrollbar-color-active: {T.BASE3} {T.YORU}; + scrollbar-background: {T.YORU}; + scrollbar-background-hover: {T.YORU}; + scrollbar-background-active: {T.YORU}; + scrollbar-corner-color: {T.YORU}; + }} + MaintainView #m-table:focus {{ + border: none; + }} + MaintainView DataTable > .datatable--header {{ + background: {T.YORU}; + color: {T.SHINKAI}; + }} + MaintainView DataTable > .datatable--cursor {{ + background: {T.BASE1}; + color: {T.YUKI}; + }} + MaintainView DataTable > .datatable--hover {{ + background: {T.YORU}; + }} + MaintainView DataTable > .datatable--even-row {{ + background: {T.YORU}; + }} + MaintainView DataTable > .datatable--odd-row {{ + background: {T.YORU}; + }} + MaintainView #m-detail {{ + width: 1fr; + padding: 1 2; + background: {T.YORU}; + color: {T.FG_ALT}; + }} + MaintainView #m-flash {{ + height: 1; + padding: 0 2; + background: {T.YORU}; + color: {T.BASE4}; + }} + """ + + BINDINGS = [ + Binding("space", "toggle_select", "select"), + Binding("d", "delete", "trash"), + Binding("w", "whitelist", "behalten"), + Binding("i", "ignore", "ignorieren"), + Binding("o", "open", "öffnen"), + Binding("r", "rescan", "rescan"), + Binding("R", "rescan_all", "rescan all", show=False), + Binding("j", "cursor_down", "down", show=False), + Binding("k", "cursor_up", "up", show=False), + Binding("g", "cursor_top", "top", show=False), + Binding("G", "cursor_bottom", "bottom", show=False), + Binding("ctrl+1", "select_tab('dotfolders')", "", show=False), + Binding("ctrl+2", "select_tab('orphans')", "", show=False), + Binding("ctrl+3", "select_tab('pacnew')", "", show=False), + Binding("ctrl+4", "select_tab('cache')", "", show=False), + Binding("ctrl+5", "select_tab('flatpak-runtimes')", "", show=False), + Binding("ctrl+6", "select_tab('flatpak-data')", "", show=False), + Binding("ctrl+7", "select_tab('aur-cache')", "", show=False), + Binding("ctrl+8", "select_tab('journal')", "", show=False), + Binding("ctrl+9", "select_tab('coredumps')", "", show=False), + Binding("ctrl+0", "select_tab('kernels')", "", show=False), + ] + + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + self.state = State.load() + self.current_tab: str = SCANNERS[0].id + self.findings_by_tab: dict[str, list[Finding]] = {} + self.selected: set[Path] = set() + + # ---- composition ---- + + def compose(self) -> ComposeResult: + yield Tabs(*[Tab(s.label, id=s.id) for s in SCANNERS], id="tabs") + yield Static("", id="m-stats", markup=True) + yield Static("", id="m-legend", markup=True) + with Horizontal(id="m-main"): + yield DataTable(id="m-table", cursor_type="row", zebra_stripes=False) + yield Static("", id="m-detail", markup=True) + yield Static("", id="m-flash", markup=True) + + def on_mount(self) -> None: + table = self.query_one("#m-table", DataTable) + table.add_column(" ", width=2, key="sel") + table.add_column("", width=2, key="icon") + table.add_column("pfad", key="path") + table.add_column("grösse", key="size") + table.add_column("idle", width=7, key="idle") + table.add_column("status", key="status") + + # ---- properties ---- + + @property + def findings(self) -> list[Finding]: + return self.findings_by_tab.get(self.current_tab, []) + + @property + def spec(self) -> ScannerSpec: + s = by_id(self.current_tab) + assert s is not None + return s + + def _current_finding(self) -> Finding | None: + table = self.query_one("#m-table", DataTable) + row = table.cursor_row + if row is None or row < 0 or row >= len(self.findings): + return None + return self.findings[row] + + def _targets(self) -> list[Finding]: + if self.selected: + return [f for f in self.findings if f.path in self.selected] + cur = self._current_finding() + return [cur] if cur else [] + + def _flash(self, msg: str, color: str | None = None) -> None: + color = color or T.BASE4 + self.query_one("#m-flash", Static).update(f"[{color}]{msg}[/]") + + def _make_scanner(self, spec: ScannerSpec): + if spec.id == "dotfolders": + return DotfoldersScanner(self.state) + return spec.factory() + + # ---- actions ---- + + def action_select_tab(self, tab_id: str) -> None: + self.query_one(Tabs).active = tab_id + + def action_rescan(self) -> None: + spec = self.spec + self._flash(f"scanne {spec.label}…") + if spec.id == "dotfolders": + self.state = State.load() + self._rescan_worker(spec.id) + + @work(thread=True, exclusive=True, group="maintain-scan") + def _rescan_worker(self, tab_id: str) -> None: + spec = by_id(tab_id) + if spec is None: + return + findings = self._make_scanner(spec).scan() + self.app.call_from_thread(self._after_rescan, tab_id, findings) + + def _after_rescan(self, tab_id: str, findings: list[Finding]) -> None: + self.findings_by_tab[tab_id] = findings + if tab_id == self.current_tab: + existing = {f.path for f in findings} + self.selected &= existing + self._refresh_view() + self._flash(f"{len(findings)} einträge in {self.spec.label}") + + def action_rescan_all(self) -> None: + self._flash("scanne alle kategorien…") + self.state = State.load() + self.selected.clear() + self._rescan_all_worker() + + @work(thread=True, exclusive=True, group="maintain-scan-all") + def _rescan_all_worker(self) -> None: + for spec in SCANNERS: + findings = self._make_scanner(spec).scan() + self.app.call_from_thread(self._after_rescan_all_one, spec.id, findings) + self.app.call_from_thread(self._flash, "alle kategorien gescannt") + + def _after_rescan_all_one(self, tab_id: str, findings: list[Finding]) -> None: + self.findings_by_tab[tab_id] = findings + if tab_id == self.current_tab: + self._refresh_view() + + def action_toggle_select(self) -> None: + cur = self._current_finding() + if cur is None: + return + if cur.path in self.selected: + self.selected.discard(cur.path) + else: + self.selected.add(cur.path) + self._render_keep_cursor() + self._render_summary() + + def action_whitelist(self) -> None: + if self.spec.actions != "trash": + self._flash(f"whitelist nicht verfügbar in {self.spec.label}", color=T.BASE4) + return + targets = self._targets() + if not targets: + return + for f in targets: + self.state.whitelist_add(f.path) + self.selected.clear() + self._flash(f"{len(targets)} auf whitelist gesetzt", color=T.SHINKAI) + self.action_rescan() + + def action_ignore(self) -> None: + if self.spec.actions != "trash": + self._flash(f"ignore nicht verfügbar in {self.spec.label}", color=T.BASE4) + return + targets = self._targets() + if not targets: + return + for f in targets: + self.state.ignore_add(f.path) + self.selected.clear() + self._flash(f"{len(targets)} ignoriert", color=T.BASE5) + self.action_rescan() + + def action_delete(self) -> None: + if self.spec.actions != "trash": + self._flash( + f"löschen in {self.spec.label} braucht sudo — siehe detail-pane", + color=T.CHIKYU, + ) + return + targets = self._targets() + if not targets: + return + total = sum(f.size_bytes for f in targets) + body = ( + f"{len(targets)} einträge ({_human_size(total)}) in den trash verschieben?\n" + f" ↩ wiederherstellbar via: taninux trash list / restore" + ) + self.app.push_screen( + ConfirmModal("löschen?", body, danger=True), + self._after_delete_confirm, + ) + + def _after_delete_confirm(self, confirmed: bool | None) -> None: + if not confirmed: + self._flash("abgebrochen") + return + targets = self._targets() + moved = 0 + bytes_moved = 0 + errors: list[str] = [] + for f in targets: + try: + entry = move_to_trash(f.path) + moved += 1 + bytes_moved += entry.size_bytes + except Exception as e: # noqa: BLE001 + errors.append(f"{f.path.name}: {e}") + self.selected.clear() + msg = f"{moved} einträge in trash ({_human_size(bytes_moved)})" + if errors: + msg += f" · {len(errors)} fehler" + self._flash(msg, color=T.TAKE if not errors else T.RED) + self.action_rescan() + + def action_open(self) -> None: + cur = self._current_finding() + if cur is None: + return + if str(cur.path).startswith("pkg://"): + self._flash("paket — kein pfad zum öffnen", color=T.BASE4) + return + import subprocess + try: + subprocess.Popen( + ["xdg-open", str(cur.path)], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + self._flash(f"öffne {_display_path(cur)}") + except FileNotFoundError: + self._flash("xdg-open nicht verfügbar", color=T.RED) + + def action_cursor_down(self) -> None: + self.query_one("#m-table", DataTable).action_cursor_down() + + def action_cursor_up(self) -> None: + self.query_one("#m-table", DataTable).action_cursor_up() + + def action_cursor_top(self) -> None: + table = self.query_one("#m-table", DataTable) + if table.row_count: + table.move_cursor(row=0) + + def action_cursor_bottom(self) -> None: + table = self.query_one("#m-table", DataTable) + if table.row_count: + table.move_cursor(row=table.row_count - 1) + + # ---- render ---- + + def _refresh_view(self) -> None: + self._render_table() + self._render_summary() + + def _render_keep_cursor(self) -> None: + table = self.query_one("#m-table", DataTable) + row = table.cursor_row + self._render_table() + if row is not None and 0 <= row < len(self.findings): + table.move_cursor(row=row) + + def _render_table(self) -> None: + table = self.query_one("#m-table", DataTable) + table.clear() + for f in self.findings: + color = CLASS_COLOR[f.classification] + label = CLASS_LABEL[f.classification] + sel_glyph = ( + f"[{T.UME}]■[/]" if f.path in self.selected + else f"[{T.BASE3}]·[/]" + ) + path = _display_path(f) + if "/" in path: + head, _, tail = path.rpartition("/") + path_styled = f"[{T.BASE4}]{head}/[/][{T.YUKI}]{tail}[/]" + else: + path_styled = f"[{T.YUKI}]{path}[/]" + size_str = _human_size(f.size_bytes) if f.size_bytes else "—" + size_styled = ( + f"[{T.CHIKYU}]{size_str}[/]" if f.size_bytes + else f"[{T.BASE3}]—[/]" + ) + idle_styled = f"[{T.BASE5}]{_format_days(f.last_access_days)}[/]" + status_styled = f"[{color}]{label}[/]" + table.add_row( + sel_glyph, + glyph(f.classification), + path_styled, + size_styled, + idle_styled, + status_styled, + ) + + if self.findings: + self._render_detail(self.findings[0]) + else: + self._render_empty() + + def _render_empty(self) -> None: + self.query_one("#m-detail", Static).update( + f"[{T.BASE4}]keine einträge in {self.spec.label}[/]\n\n" + f"[{T.BASE5}]drücke r zum rescan, oder ctrl+1…0 für anderen scanner[/]" + ) + + def _render_summary(self) -> None: + by_class: Counter[Classification] = Counter(f.classification for f in self.findings) + total_reclaim = sum( + f.size_bytes for f in self.findings + if f.classification in (Classification.SAFE, Classification.LIKELY_ORPHAN) + ) + if self.selected: + sel_bytes = sum( + f.size_bytes for f in self.findings if f.path in self.selected + ) + stats = ( + f"[{T.UME}]{len(self.selected)} ausgewählt[/]" + f" [{T.BASE3}]·[/] " + f"[{T.CHIKYU}]{_human_size(sel_bytes)}[/]" + f" [{T.BASE3}]·[/] " + f"[{T.BASE5}]{len(self.findings)} {self.spec.label} insgesamt[/]" + ) + else: + stats = ( + f"[{T.YUKI}]{len(self.findings)}[/] [{T.BASE5}]{self.spec.label}[/]" + f" [{T.BASE3}]·[/] " + f"[{T.TAKE}]{_human_size(total_reclaim)}[/] [{T.BASE5}]potentiell wegwerfbar[/]" + ) + self.query_one("#m-stats", Static).update(stats) + + legend_parts = [] + for cls in CLASSIFICATION_ORDER: + count = by_class.get(cls, 0) + if count == 0: + continue + legend_parts.append( + f"{glyph(cls)} [{T.BASE5}]{CLASS_LABEL[cls]}[/] " + f"[{T.BASE4}]{count}[/]" + ) + self.query_one("#m-legend", Static).update( + f" [{T.BASE3}]│[/] ".join(legend_parts) + ) + + def _render_detail(self, f: Finding) -> None: + color = CLASS_COLOR[f.classification] + label = CLASS_LABEL[f.classification] + gly = glyph(f.classification) + path_display = _display_path(f) + is_pkg = str(f.path).startswith("pkg://") + + lines = [ + f"{gly} [{color}]{label.upper()}[/]", + "", + ] + if is_pkg: + lines += [ + f"[{T.SHINKAI}]paket[/]", + f" [{T.YUKI}]{path_display}[/]", + ] + else: + lines += [ + f"[{T.SHINKAI}]pfad[/]", + f" [{T.YUKI}]{f.path}[/]", + ] + lines += [ + "", + f"[{T.SHINKAI}]grösse[/] [{T.CHIKYU}]{_human_size(f.size_bytes)}[/]", + ] + if f.last_access_days is not None: + lines.append( + f"[{T.SHINKAI}]idle[/] [{T.BASE5}]{_format_days(f.last_access_days)}[/]" + ) + lines.append( + f"[{T.SHINKAI}]kategorie[/] [{T.BASE5}]{self.spec.label}[/]" + ) + if f.matched_package and not is_pkg: + lines.append(f"[{T.SHINKAI}]paket[/] [{T.UME}]{f.matched_package}[/]") + lines += [ + "", + f"[{T.SHINKAI}]grund[/]", + f" [{T.BASE5}]{f.reason}[/]", + ] + if self.spec.advice: + advice = self.spec.advice.format( + pkg=f.matched_package or "", + path=str(f.path), + ) + lines += [ + "", + f"[{T.SHINKAI}]hinweis[/]", + f" [{T.CHIKYU}]{advice}[/]", + ] + if f.path in self.selected: + lines += ["", f"[{T.UME}]■ ausgewählt[/]"] + self.query_one("#m-detail", Static).update("\n".join(lines)) + + # ---- events ---- + + def on_tabs_tab_activated(self, event: Tabs.TabActivated) -> None: + if event.tab is None: + return + self.current_tab = event.tab.id + self.selected.clear() + if self.current_tab not in self.findings_by_tab: + self.action_rescan() + else: + self._refresh_view() + + def on_data_table_row_highlighted(self, event: DataTable.RowHighlighted) -> None: + if event.cursor_row is None or event.cursor_row >= len(self.findings): + return + self._render_detail(self.findings[event.cursor_row]) diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/maintain/types.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/maintain/types.py new file mode 100644 index 0000000..b3a240f --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/maintain/types.py @@ -0,0 +1,76 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from enum import Enum +from pathlib import Path + +from taninux.core import theme as T + + +class Category(str, Enum): + DOTFOLDERS = "dotfolders" + PACMAN = "pacman" + FLATPAK = "flatpak" + AUR = "aur" + SYSTEM = "system" + + +class Classification(str, Enum): + ACTIVE = "active" + WHITELISTED = "whitelisted" + UNKNOWN = "unknown" + LIKELY_ORPHAN = "likely" + SAFE = "safe" + + +CLASSIFICATION_ORDER = [ + Classification.SAFE, + Classification.LIKELY_ORPHAN, + Classification.UNKNOWN, + Classification.WHITELISTED, + Classification.ACTIVE, +] + +CLASS_COLOR = { + Classification.SAFE: T.TAKE, + Classification.LIKELY_ORPHAN: T.CHIKYU, + Classification.UNKNOWN: T.BASE4, + Classification.WHITELISTED: T.SHINKAI, + Classification.ACTIVE: T.UME, +} + +CLASS_GLYPH = { + Classification.SAFE: "●", + Classification.LIKELY_ORPHAN: "◐", + Classification.UNKNOWN: "○", + Classification.WHITELISTED: "◆", + Classification.ACTIVE: "▲", +} + +CLASS_LABEL = { + Classification.SAFE: "sicher", + Classification.LIKELY_ORPHAN: "vermutlich", + Classification.UNKNOWN: "unklar", + Classification.WHITELISTED: "behalten", + Classification.ACTIVE: "aktiv", +} + + +def glyph(cls: Classification) -> str: + return f"[{CLASS_COLOR[cls]}]{CLASS_GLYPH[cls]}[/]" + + +@dataclass +class Finding: + path: Path + category: Category + classification: Classification + size_bytes: int + last_access_days: int | None + reason: str + matched_package: str | None = None + extra: dict[str, str] = field(default_factory=dict) + + @property + def sort_key(self) -> tuple[int, int]: + return (CLASSIFICATION_ORDER.index(self.classification), -self.size_bytes) diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/overview/__init__.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/overview/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/overview/pillar.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/overview/pillar.py new file mode 100644 index 0000000..375b7d3 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/overview/pillar.py @@ -0,0 +1,13 @@ +from __future__ import annotations + +from taninux.overview.screen import OverviewView +from taninux.pillars.base import Pillar + + +OVERVIEW = Pillar( + key="o", + label="overview", + tagline="OS at-a-glance", + icon="◉", + factory=OverviewView, +) diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/overview/screen.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/overview/screen.py new file mode 100644 index 0000000..fa2bf0d --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/overview/screen.py @@ -0,0 +1,351 @@ +"""Overview/Dashboard — Landing-View mit OS at-a-glance.""" +from __future__ import annotations + +import os +import subprocess +from dataclasses import dataclass +from datetime import datetime +from pathlib import Path + +from textual import work +from textual.app import ComposeResult +from textual.binding import Binding +from textual.containers import VerticalScroll +from textual.widgets import Static + +from taninux.core import pacman as pac +from taninux.core import sysinfo +from taninux.core import theme as T +from taninux.core.paths import snapshots_dir, trash_dir + + +def _human_size(n: int) -> str: + f = float(n) + for unit in ("B", "K", "M", "G", "T"): + if f < 1024 or unit == "T": + return f"{f:.0f}{unit}" if unit == "B" else f"{f:.1f}{unit}" + f /= 1024 + return f"{f:.1f}T" + + +def _human_duration(seconds: float) -> str: + s = int(seconds) + if s < 60: + return f"{s}s" + if s < 3600: + return f"{s // 60}m" + h, rem = divmod(s, 3600) + if h < 24: + return f"{h}h {rem // 60}m" + d, h = divmod(h, 24) + return f"{d}d {h}h" + + +@dataclass +class PackageStats: + total: int + explicit: int + aur: int + deps: int + orphans: int + + +def _pacman_count(args: list[str]) -> int: + try: + r = subprocess.run( + ["pacman", *args], + capture_output=True, text=True, check=False, timeout=10, + ) + return len([l for l in r.stdout.splitlines() if l.strip()]) + except (FileNotFoundError, subprocess.TimeoutExpired): + return 0 + + +def _package_stats() -> PackageStats: + return PackageStats( + total=_pacman_count(["-Qq"]), + explicit=_pacman_count(["-Qenq"]), + aur=_pacman_count(["-Qmq"]), + deps=_pacman_count(["-Qdq"]), + orphans=_pacman_count(["-Qdtq"]), + ) + + +def _trash_stats() -> tuple[int, int]: + """(entries, total_bytes)""" + try: + entries = list(trash_dir().iterdir()) + except OSError: + return (0, 0) + count = 0 + total_size = 0 + for e in entries: + if not e.is_dir(): + continue + count += 1 + mf = e / "manifest.json" + if mf.exists(): + try: + import json + data = json.loads(mf.read_text()) + total_size += int(data.get("size_bytes", 0)) + except (OSError, ValueError): + pass + return (count, total_size) + + +def _last_snapshot() -> str | None: + try: + snaps = sorted(snapshots_dir().glob("*.json")) + except OSError: + return None + return snaps[-1].stem if snaps else None + + +def _flatpak_count() -> tuple[int, int]: + """(installed apps, installed runtimes)""" + try: + r1 = subprocess.run( + ["flatpak", "list", "--app", "--columns=application"], + capture_output=True, text=True, check=False, timeout=5, + ) + r2 = subprocess.run( + ["flatpak", "list", "--runtime", "--columns=application"], + capture_output=True, text=True, check=False, timeout=5, + ) + except (FileNotFoundError, subprocess.TimeoutExpired): + return (0, 0) + apps = len([l for l in r1.stdout.splitlines() if l.strip() and l.strip() != "Application ID"]) + rts = len([l for l in r2.stdout.splitlines() if l.strip() and l.strip() != "Application ID"]) + return (apps, rts) + + +class OverviewView(VerticalScroll): + """Landing-Dashboard: OS at-a-glance + quick metrics.""" + + DEFAULT_CSS = f""" + OverviewView {{ + background: {T.YORU}; + padding: 1 2; + scrollbar-color: {T.BASE2} {T.YORU}; + scrollbar-background: {T.YORU}; + scrollbar-corner-color: {T.YORU}; + scrollbar-size-vertical: 1; + }} + OverviewView Static {{ + background: {T.YORU}; + margin-bottom: 1; + }} + """ + + BINDINGS = [ + Binding("r", "refresh", "refresh"), + ] + + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + + def compose(self) -> ComposeResult: + yield Static("", id="o-banner", markup=True) + yield Static("", id="o-system", markup=True) + yield Static("", id="o-pkgs", markup=True) + yield Static("", id="o-updates", markup=True) + yield Static("", id="o-maintain", markup=True) + yield Static("", id="o-flatpak", markup=True) + yield Static("", id="o-recent", markup=True) + yield Static("", id="o-hints", markup=True) + + def on_mount(self) -> None: + self._refresh_fast() + self._refresh_slow() + self._refresh_maintain() + + def action_refresh(self) -> None: + pac.invalidate() + self._refresh_fast() + self._refresh_slow() + self._refresh_maintain() + + # ---- fast (synchronous, <100ms) ---- + + def _refresh_fast(self) -> None: + now = datetime.now() + date_str = now.strftime("%A · %d. %B %Y · %H:%M") + banner = ( + f"[{T.UME}]▲ TANINUX[/] " + f"[{T.BASE4}]·[/] " + f"[{T.SHINKAI}]{date_str}[/]" + ) + self.query_one("#o-banner", Static).update(banner) + + # System + up = sysinfo.uptime_seconds() + boot = sysinfo.boot_time_seconds() + gpus_list = sysinfo.gpus() + gpu_str = " · ".join( + f"[{T.YUKI}]{g.vendor}[/] [{T.BASE5}]({g.driver or '—'})[/]" + for g in gpus_list + ) or f"[{T.BASE5}]?[/]" + + lines = [ + f"[{T.UME}]◈ system[/]", + f" [{T.SHINKAI}]distro[/] [{T.YUKI}]{sysinfo.distro_pretty()}[/]", + f" [{T.SHINKAI}]kernel[/] [{T.YUKI}]{sysinfo.kernel_release()}[/]", + f" [{T.SHINKAI}]uptime[/] [{T.YUKI}]{_human_duration(up)}[/]" + + (f" [{T.BASE4}](boot {boot:.1f}s)[/]" if boot else ""), + f" [{T.SHINKAI}]gpu[/] {gpu_str}", + f" [{T.SHINKAI}]audio[/] [{T.YUKI}]{sysinfo.audio_server()}[/]", + ] + self.query_one("#o-system", Static).update("\n".join(lines)) + + # Packages + stats = _package_stats() + lines = [ + f"[{T.UME}]◆ pakete[/]", + f" [{T.SHINKAI}]total[/] [{T.YUKI}]{stats.total}[/]", + f" [{T.SHINKAI}]explicit[/] [{T.YUKI}]{stats.explicit}[/] [{T.BASE5}](native)[/] " + f"[{T.YUKI}]{stats.aur}[/] [{T.BASE5}](AUR)[/]", + f" [{T.SHINKAI}]deps[/] [{T.BASE5}]{stats.deps}[/]", + ] + if stats.orphans: + lines.append( + f" [{T.SHINKAI}]orphans[/] [{T.CHIKYU}]{stats.orphans}[/] " + f"[{T.BASE4}](pacman -Qdt — siehe maintain/orphans)[/]" + ) + else: + lines.append(f" [{T.SHINKAI}]orphans[/] [{T.TAKE}]0[/]") + self.query_one("#o-pkgs", Static).update("\n".join(lines)) + + # Flatpak + fp_apps, fp_runtimes = _flatpak_count() + if fp_apps or fp_runtimes: + lines = [ + f"[{T.UME}]● flatpak[/]", + f" [{T.SHINKAI}]apps[/] [{T.YUKI}]{fp_apps}[/] " + f"[{T.SHINKAI}]runtimes[/] [{T.YUKI}]{fp_runtimes}[/]", + ] + else: + lines = [ + f"[{T.UME}]● flatpak[/]", + f" [{T.BASE5}](nicht installiert / leer)[/]", + ] + self.query_one("#o-flatpak", Static).update("\n".join(lines)) + + # Recent + trash_count, trash_bytes = _trash_stats() + last_snap = _last_snapshot() + lines = [f"[{T.UME}]✎ recent[/]"] + if trash_count: + lines.append( + f" [{T.SHINKAI}]trash[/] [{T.CHIKYU}]{trash_count} einträge[/] " + f"[{T.BASE5}]({_human_size(trash_bytes)})[/] " + f"[{T.BASE4}]via: taninux trash list[/]" + ) + else: + lines.append(f" [{T.SHINKAI}]trash[/] [{T.BASE5}]leer[/]") + if last_snap: + lines.append(f" [{T.SHINKAI}]snapshot[/] [{T.YUKI}]{last_snap}[/] " + f"[{T.BASE4}](taninux snapshot diff)[/]") + else: + lines.append(f" [{T.SHINKAI}]snapshot[/] [{T.BASE5}]keine — speichere:[/] " + f"[{T.BASE4}]taninux snapshot save[/]") + self.query_one("#o-recent", Static).update("\n".join(lines)) + + # Hints + hints = ( + f"[{T.BASE4}]quick-keys:[/] " + f"[{T.BASE5}]m[/] maintain " + f"[{T.BASE5}]u[/] update " + f"[{T.BASE5}]b[/] bundles " + f"[{T.BASE5}]s[/] suggest " + f"[{T.BASE5}]f[/] flatpak " + f"[{T.BASE5}]k[/] kernel " + f"[{T.BASE5}]h[/] health" + ) + self.query_one("#o-hints", Static).update(hints) + + # Updates: zeige Placeholder bis worker fertig + self.query_one("#o-updates", Static).update( + f"[{T.UME}]↻ updates[/]\n [{T.BASE5}]scanne pacman/AUR/flatpak/fwupd…[/]" + ) + + # ---- slow (background) ---- + + @work(thread=True, exclusive=True, group="overview-updates") + def _refresh_slow(self) -> None: + from taninux.update.sources import fetch_all + items = fetch_all() + self.app.call_from_thread(self._render_updates, items) + + def _render_updates(self, items) -> None: + from collections import Counter + counts = Counter(i.source for i in items) + total = len(items) + if not items: + content = ( + f"[{T.UME}]↻ updates[/]\n" + f" [{T.TAKE}]✓ system ist aktuell[/]" + ) + else: + total_size = sum(i.size_bytes for i in items) + size_str = f" [{T.BASE4}]({_human_size(total_size)})[/]" if total_size else "" + parts = [] + for src in ("pacman", "aur", "flatpak", "fwupd"): + n = counts.get(src, 0) + if n: + color = T.UME if src == "pacman" else (T.CHIKYU if src == "aur" else (T.TAKE if src == "flatpak" else T.SHINKAI)) + parts.append(f"[{color}]{src} {n}[/]") + content = ( + f"[{T.UME}]↻ updates[/]\n" + f" [{T.SHINKAI}]pending[/] [{T.YUKI}]{total}[/]{size_str}\n" + f" [{T.SHINKAI}]quellen[/] " + " · ".join(parts) + "\n" + f" [{T.BASE4}]drücke u[/] [{T.BASE5}]→[/] [{T.BASE4}]e zum ausführen[/]" + ) + try: + self.query_one("#o-updates", Static).update(content) + except Exception: + pass + + @work(thread=True, exclusive=True, group="overview-maintain") + def _refresh_maintain(self) -> None: + # Schnelle Variante: nur cache + journal scannen (die sind <100ms) + from taninux.maintain.scanners.pacman import PacmanCacheScanner + from taninux.maintain.scanners.system import SystemJournalScanner + cache_findings = PacmanCacheScanner().scan() + journal_findings = SystemJournalScanner().scan() + cache_bytes = sum( + f.size_bytes for f in cache_findings + if f.classification.value in ("safe", "likely") + ) + journal_bytes = sum(f.size_bytes for f in journal_findings) + self.app.call_from_thread(self._render_maintain, cache_bytes, journal_bytes) + + def _render_maintain(self, cache_bytes: int, journal_bytes: int) -> None: + if cache_bytes or journal_bytes: + lines = [ + f"[{T.UME}]▣ maintain — quick wins[/]", + ] + if cache_bytes: + lines.append( + f" [{T.SHINKAI}]pacman cache[/] " + f"[{T.CHIKYU}]{_human_size(cache_bytes)}[/] " + f"[{T.BASE4}](sudo paccache -ruk0)[/]" + ) + if journal_bytes: + lines.append( + f" [{T.SHINKAI}]journal[/] " + f"[{T.CHIKYU}]{_human_size(journal_bytes)}[/] " + f"[{T.BASE4}](sudo journalctl --vacuum-time=2weeks)[/]" + ) + lines.append( + f" [{T.BASE4}]für full-scan: drücke m[/]" + ) + else: + lines = [ + f"[{T.UME}]▣ maintain — quick wins[/]", + f" [{T.TAKE}]✓ nichts auffälliges (full-scan via m)[/]", + ] + try: + self.query_one("#o-maintain", Static).update("\n".join(lines)) + except Exception: + pass diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/packages/__init__.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/packages/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/packages/pillar.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/packages/pillar.py new file mode 100644 index 0000000..2192f5a --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/packages/pillar.py @@ -0,0 +1,13 @@ +from __future__ import annotations + +from taninux.packages.screen import PackagesView +from taninux.pillars.base import Pillar + + +PACKAGES = Pillar( + key="p", + label="packages", + tagline="pacman übersicht", + icon="◆", + factory=PackagesView, +) diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/packages/screen.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/packages/screen.py new file mode 100644 index 0000000..6526842 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/packages/screen.py @@ -0,0 +1,324 @@ +"""Packages-Pillar — Übersicht aller installierten Pakete mit Filter-Tabs.""" +from __future__ import annotations + +from textual import work +from textual.app import ComposeResult +from textual.binding import Binding +from textual.containers import Vertical +from textual.widgets import DataTable, Static, Tab, Tabs + +from taninux.core import pacman as pac, pkg, sudo, sysinfo +from taninux.core import theme as T +from taninux.core.confirm import ConfirmModal +from taninux.core.pkg import Package +from taninux.core.streaming import RunnerScreen, StreamCommand +from taninux.core.sudo_modal import SudoModal + + +TABS = [ + ("all", "alle"), + ("explicit", "explicit"), + ("deps", "deps"), + ("orphans", "orphans"), + ("aur", "AUR"), +] + + +REPO_COLOR = { + "core": T.UME, + "extra": T.SHINKAI, + "multilib": T.CHIKYU, + "aur": T.CHIKYU, + "local": T.BASE4, +} + + +def _human_size(n: int) -> str: + f = float(n) + for unit in ("B", "K", "M", "G", "T"): + if f < 1024 or unit == "T": + return f"{f:.0f}{unit}" if unit == "B" else f"{f:.1f}{unit}" + f /= 1024 + return f"{f:.1f}T" + + +class PackagesView(Vertical): + """Pacman-Pakete: tabs für alle/explicit/deps/orphans/AUR.""" + + DEFAULT_CSS = f""" + PackagesView {{ + background: {T.YORU}; + color: {T.YUKI}; + layout: vertical; + }} + PackagesView Tabs {{ + background: {T.YORU}; + height: 3; + }} + PackagesView Tab {{ + background: {T.YORU}; + color: {T.BASE4}; + padding: 0 2; + }} + PackagesView Tab.-active {{ + color: {T.UME}; + }} + PackagesView Underline {{ + background: {T.YORU}; + color: {T.BASE2}; + }} + PackagesView Underline > .underline--bar {{ + background: {T.YORU}; + color: {T.UME}; + }} + PackagesView #p-stats {{ + height: 1; + padding: 0 2; + background: {T.YORU}; + color: {T.BASE5}; + }} + PackagesView #p-table {{ + height: 1fr; + background: {T.YORU}; + color: {T.FG_ALT}; + border: none; + overflow-x: hidden; + scrollbar-size-vertical: 1; + scrollbar-color: {T.BASE2} {T.YORU}; + scrollbar-background: {T.YORU}; + scrollbar-corner-color: {T.YORU}; + }} + PackagesView #p-table:focus {{ + border: none; + }} + PackagesView DataTable > .datatable--header {{ + background: {T.YORU}; + color: {T.SHINKAI}; + }} + PackagesView DataTable > .datatable--cursor {{ + background: {T.BASE1}; + color: {T.YUKI}; + }} + PackagesView DataTable > .datatable--hover {{ + background: {T.YORU}; + }} + PackagesView DataTable > .datatable--even-row {{ + background: {T.YORU}; + }} + PackagesView DataTable > .datatable--odd-row {{ + background: {T.YORU}; + }} + PackagesView #p-flash {{ + height: 1; + padding: 0 2; + background: {T.YORU}; + color: {T.BASE4}; + }} + """ + + BINDINGS = [ + Binding("r", "rescan", "rescan"), + Binding("x", "remove", "entfernen"), + Binding("X", "remove_orphans", "alle orphans"), + Binding("j", "cursor_down", "down", show=False), + Binding("k", "cursor_up", "up", show=False), + Binding("g", "cursor_top", "top", show=False), + Binding("G", "cursor_bottom", "bottom", show=False), + ] + + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + self.all_packages: list[Package] = [] + self.current_tab = "all" + self.filtered: list[Package] = [] + + def compose(self) -> ComposeResult: + yield Tabs(*[Tab(label, id=tab_id) for tab_id, label in TABS], id="p-tabs") + yield Static("", id="p-stats", markup=True) + yield DataTable(id="p-table", cursor_type="row", zebra_stripes=False) + yield Static("", id="p-flash", markup=True) + + def on_mount(self) -> None: + t = self.query_one("#p-table", DataTable) + t.add_column("name", width=30, key="name") + t.add_column("version", width=20, key="version") + t.add_column("grösse", width=10, key="size") + t.add_column("reason", width=10, key="reason") + t.add_column("repo", width=10, key="repo") + t.add_column("beschreibung", key="desc") + self.action_rescan() + + # ---- actions ---- + + def action_rescan(self) -> None: + self._flash("scanne pakete…") + self._scan_worker() + + @work(thread=True, exclusive=True, group="packages-scan") + def _scan_worker(self) -> None: + pkgs = pkg.all_installed() + self.app.call_from_thread(self._after_scan, pkgs) + + def _after_scan(self, pkgs: list[Package]) -> None: + self.all_packages = pkgs + self._apply_filter() + self._flash(f"{len(pkgs)} pakete installiert") + + def action_remove(self) -> None: + cur = self._current() + if cur is None: + return + body = ( + f"paket entfernen?\n\n" + f" {cur.name} {cur.version}\n" + f" {_human_size(cur.size_bytes)} reason: {cur.reason} repo: {cur.repo}\n\n" + f"command: sudo pacman -Rns {cur.name}" + ) + self.app.push_screen( + ConfirmModal(f"remove {cur.name}?", body, danger=True), + lambda ok: self._after_remove(ok, [cur.name]), + ) + + def action_remove_orphans(self) -> None: + orphans = [p for p in self.all_packages if p.is_orphan] + if not orphans: + self._flash("keine orphans", color=T.TAKE) + return + total = sum(p.size_bytes for p in orphans) + body = ( + f"alle orphans entfernen?\n\n" + f" {len(orphans)} pakete {_human_size(total)}\n\n" + f"command: sudo pacman -Rns $(pacman -Qdtq)" + ) + self.app.push_screen( + ConfirmModal( + f"remove {len(orphans)} orphans?", + body, danger=True, + ), + lambda ok: self._after_remove(ok, [p.name for p in orphans]), + ) + + def _after_remove(self, ok: bool | None, names: list[str]) -> None: + if not ok or not names: + self._flash("abgebrochen") + return + cmds = [StreamCommand( + label=f"remove {len(names)} paket{'e' if len(names) > 1 else ''}", + argv=["sudo", "-n", "pacman", "-Rns", "--noconfirm", *names], + )] + if not sudo.is_cached(): + self.app.push_screen( + SudoModal(), + lambda pw: self._sudo_then_run(pw, cmds), + ) + else: + self.app.push_screen(RunnerScreen(cmds), self._after_runner) + + def _sudo_then_run(self, pw, cmds): + if not pw: + self._flash("kein passwort — abgebrochen") + return + if not sudo.cache(pw): + self._flash("falsches passwort", color=T.RED) + return + self.app.push_screen(RunnerScreen(cmds), self._after_runner) + + def _after_runner(self, codes): + pac.invalidate() + if not codes: + self._flash("kein ergebnis") + else: + ok = sum(1 for c in codes if c == 0) + self._flash( + f"{ok}/{len(codes)} ok — rescan…", + color=T.TAKE if ok == len(codes) else T.CHIKYU, + ) + self.action_rescan() + + def action_cursor_down(self) -> None: + self.query_one("#p-table", DataTable).action_cursor_down() + + def action_cursor_up(self) -> None: + self.query_one("#p-table", DataTable).action_cursor_up() + + def action_cursor_top(self) -> None: + t = self.query_one("#p-table", DataTable) + if t.row_count: + t.move_cursor(row=0) + + def action_cursor_bottom(self) -> None: + t = self.query_one("#p-table", DataTable) + if t.row_count: + t.move_cursor(row=t.row_count - 1) + + # ---- helpers ---- + + def _current(self) -> Package | None: + t = self.query_one("#p-table", DataTable) + r = t.cursor_row + if r is None or r < 0 or r >= len(self.filtered): + return None + return self.filtered[r] + + def _flash(self, msg: str, color: str | None = None) -> None: + color = color or T.BASE4 + self.query_one("#p-flash", Static).update(f"[{color}]{msg}[/]") + + def _apply_filter(self) -> None: + all_pkgs = self.all_packages + tab = self.current_tab + if tab == "explicit": + f = [p for p in all_pkgs if p.reason == "explicit"] + elif tab == "deps": + f = [p for p in all_pkgs if p.reason == "dependency"] + elif tab == "orphans": + f = [p for p in all_pkgs if p.is_orphan] + elif tab == "aur": + f = [p for p in all_pkgs if p.repo == "aur"] + else: + f = list(all_pkgs) + # Sort by size desc + f.sort(key=lambda p: -p.size_bytes) + self.filtered = f + self._refresh_view() + + def _refresh_view(self) -> None: + t = self.query_one("#p-table", DataTable) + t.clear() + for p in self.filtered: + repo_color = REPO_COLOR.get(p.repo, T.BASE4) + reason_color = T.UME if p.reason == "explicit" else T.BASE5 + size_styled = ( + f"[{T.CHIKYU}]{_human_size(p.size_bytes)}[/]" + if p.size_bytes else f"[{T.BASE3}]—[/]" + ) + t.add_row( + f"[{T.YUKI}]{p.name}[/]", + f"[{T.BASE5}]{p.version}[/]", + size_styled, + f"[{reason_color}]{p.reason}[/]", + f"[{repo_color}]{p.repo}[/]", + f"[{T.BASE5}]{p.desc[:80]}[/]", + ) + + total_size = sum(p.size_bytes for p in self.filtered) + all_size = sum(p.size_bytes for p in self.all_packages) + stats_parts = [ + f"[{T.YUKI}]{len(self.filtered)}[/] [{T.BASE5}]pakete[/]", + f"[{T.CHIKYU}]{_human_size(total_size)}[/]", + ] + if self.current_tab != "all": + stats_parts.append( + f"[{T.BASE4}](von {len(self.all_packages)} · {_human_size(all_size)} total)[/]" + ) + self.query_one("#p-stats", Static).update( + " " + f" [{T.BASE3}]·[/] ".join(stats_parts) + ) + + # ---- events ---- + + def on_tabs_tab_activated(self, event: Tabs.TabActivated) -> None: + if event.tab is None: + return + self.current_tab = event.tab.id + self._apply_filter() diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/pillars/__init__.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/pillars/__init__.py new file mode 100644 index 0000000..0917fac --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/pillars/__init__.py @@ -0,0 +1,33 @@ +"""Pillar-Registry: alle Pillars in fester Reihenfolge.""" +from __future__ import annotations + +from taninux.bundles.pillar import BUNDLES +from taninux.flatpak.pillar import FLATPAK +from taninux.health.pillar import HEALTH +from taninux.kernel.pillar import KERNEL +from taninux.maintain.pillar import MAINTAIN +from taninux.overview.pillar import OVERVIEW +from taninux.packages.pillar import PACKAGES +from taninux.pillars.base import Pillar +from taninux.suggest.pillar import SUGGEST +from taninux.update.pillar import UPDATE + + +PILLARS: list[Pillar] = [ + OVERVIEW, + PACKAGES, + MAINTAIN, + UPDATE, + BUNDLES, + SUGGEST, + FLATPAK, + KERNEL, + HEALTH, +] + + +def by_key(key: str) -> Pillar | None: + for p in PILLARS: + if p.key == key: + return p + return None diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/pillars/base.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/pillars/base.py new file mode 100644 index 0000000..1752a73 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/pillars/base.py @@ -0,0 +1,23 @@ +"""Pillar-Abstraktion: jeder Pillar (maintain/update/kernel/suggest/health) +ist eine eigenständige Funktions-Säule der TANINUX-TUI.""" +from __future__ import annotations + +from dataclasses import dataclass +from typing import Callable + +from textual.widget import Widget + + +@dataclass(frozen=True) +class Pillar: + """Metadaten + Factory für ein Pillar. + + Der eigentliche Content (Widget-Tree) wird via `factory()` erzeugt und + in den ContentSwitcher der Top-Level-App eingehängt. Jeder Pillar erhält + automatisch eine ID = `key` für das Switching. + """ + key: str # Single-char nav key: 'm', 'u', 'k', 's', 'h' + label: str # Kurz-Label für Sidebar: 'maintain' + tagline: str # Einzeiler für Sidebar-Description + icon: str # Geometrisches Glyph: '▣', '↻', '⊙', '✦', '◈' + factory: Callable[[], Widget] diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/suggest/__init__.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/suggest/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/suggest/pillar.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/suggest/pillar.py new file mode 100644 index 0000000..98abd59 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/suggest/pillar.py @@ -0,0 +1,13 @@ +from __future__ import annotations + +from taninux.pillars.base import Pillar +from taninux.suggest.screen import SuggestView + + +SUGGEST = Pillar( + key="s", + label="suggest", + tagline="system-aware empfehlungen", + icon="✦", + factory=SuggestView, +) diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/suggest/rules.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/suggest/rules.py new file mode 100644 index 0000000..a77dd64 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/suggest/rules.py @@ -0,0 +1,160 @@ +"""Empfehlungs-Regeln: system-zustand → konkrete Pakete.""" +from __future__ import annotations + +from dataclasses import dataclass + +from taninux.core import sysinfo + + +@dataclass +class Suggestion: + category: str # 'gpu', 'dev', 'shell', 'audio' + reason: str # human-readable description (why), EN + packages: list[str] # what would be installed + priority: int = 1 # 1 high, 2 medium, 3 nice-to-have + title: str = "" # short title (what), EN + + +# ---- GPU ---- + +GPU_DRIVERS = { + "nvidia": { + "primary": ["nvidia", "nvidia-utils", "nvidia-settings"], + "open": ["nvidia-open", "nvidia-utils"], + "extras": ["cuda", "nvtop"], + }, + "amd": { + "primary": ["mesa", "vulkan-radeon", "libva-mesa-driver"], + "extras": ["radeontop", "lact"], + }, + "intel": { + "primary": ["mesa", "vulkan-intel", "intel-media-driver"], + "extras": ["intel-gpu-tools"], + }, +} + + +def suggest_gpu(installed: set[str]) -> list[Suggestion]: + out: list[Suggestion] = [] + for gpu in sysinfo.gpus(): + drivers = GPU_DRIVERS.get(gpu.vendor) + if not drivers: + continue + missing_primary = [p for p in drivers["primary"] if p not in installed] + if missing_primary: + out.append(Suggestion( + category="gpu", + title=f"{gpu.vendor.upper()} graphics drivers", + reason=f"{gpu.vendor.upper()} GPU detected ({gpu.model[:50]}) — primary drivers are missing", + packages=missing_primary, + priority=1, + )) + missing_extra = [p for p in drivers.get("extras", []) if p not in installed] + if missing_extra and not missing_primary: + out.append(Suggestion( + category="gpu", + title=f"{gpu.vendor.upper()} GPU extras", + reason="Optional tools for your GPU (monitoring, tuning)", + packages=missing_extra, + priority=3, + )) + return out + + +# ---- dev environments ---- + +DEV_ECOSYSTEMS = { + "rust": { + "trigger_binary": "cargo", + "trigger_pkg": "rust", + "missing": [ + ("rust-analyzer", "LSP für editor-integration"), + ("sccache", "compile cache, beschleunigt rebuilds"), + ("cargo-audit", "security-audit für deps"), + ], + }, + "go": { + "trigger_binary": "go", + "trigger_pkg": "go", + "missing": [ + ("gopls", "LSP für editor-integration"), + ("delve", "debugger"), + ], + }, + "node": { + "trigger_binary": "node", + "trigger_pkg": "nodejs", + "missing": [ + ("typescript", "TS compiler"), + ("pnpm", "schnellerer package-manager"), + ], + }, + "python": { + "trigger_binary": "python", + "trigger_pkg": "python", + "missing": [ + ("uv", "schneller dependency-manager"), + ("ruff", "schneller linter/formatter"), + ("ipython", "besseres REPL"), + ], + }, +} + + +def suggest_dev(installed: set[str]) -> list[Suggestion]: + out: list[Suggestion] = [] + for eco_name, eco in DEV_ECOSYSTEMS.items(): + trigger = eco["trigger_pkg"] in installed or sysinfo.has_binary(eco["trigger_binary"]) + if not trigger: + continue + missing = [ + (pkg, why) for pkg, why in eco["missing"] if pkg not in installed + ] + if missing: + out.append(Suggestion( + category="dev", + title=f"{eco_name.capitalize()} tooling", + reason=f"{eco_name} stack detected — complementary tools are missing", + packages=[p for p, _ in missing], + priority=2, + )) + return out + + +# ---- modern shell tooling ---- + +MODERN_CLI = [ + ("bat", "syntax-highlighted cat-ersatz"), + ("eza", "modernes ls mit icons + git-status"), + ("fd", "user-friendly find-ersatz"), + ("ripgrep", "schnelles grep"), + ("fzf", "fuzzy finder"), + ("zoxide", "smarter cd"), + ("dust", "rust-basiertes du"), + ("bottom", "moderner top/htop"), + ("starship", "shell-prompt"), +] + + +def suggest_shell(installed: set[str]) -> list[Suggestion]: + missing = [pkg for pkg, _ in MODERN_CLI if pkg not in installed] + if not missing: + return [] + return [Suggestion( + category="shell", + title="Modern CLI tools", + reason=f"{len(missing)} quality-of-life terminal tools are missing", + packages=missing, + priority=3, + )] + + +# ---- aggregate ---- + + +def all_suggestions(installed: set[str]) -> list[Suggestion]: + return [ + *suggest_gpu(installed), + *suggest_dev(installed), + *suggest_shell(installed), + ] diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/suggest/screen.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/suggest/screen.py new file mode 100644 index 0000000..dd40b5b --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/suggest/screen.py @@ -0,0 +1,176 @@ +"""Suggest-Pillar — system-aware Paket-Empfehlungen.""" +from __future__ import annotations + +import subprocess + +from textual.app import ComposeResult +from textual.binding import Binding +from textual.containers import VerticalScroll +from textual.widgets import DataTable, Static + +from taninux.core import pacman as pac +from taninux.core import theme as T +from taninux.core.confirm import ConfirmModal +from taninux.core.runner import Command, run_interactive +from taninux.suggest.rules import Suggestion, all_suggestions + + +CATEGORY_GLYPH = { + "gpu": ("▣", T.UME), + "dev": ("◆", T.SHINKAI), + "shell": ("●", T.TAKE), + "audio": ("◐", T.CHIKYU), +} + + +class SuggestView(VerticalScroll): + DEFAULT_CSS = f""" + SuggestView {{ + background: {T.YORU}; + padding: 1 2; + scrollbar-color: {T.BASE2} {T.YORU}; + scrollbar-background: {T.YORU}; + scrollbar-corner-color: {T.YORU}; + scrollbar-size-vertical: 1; + }} + SuggestView Static {{ + background: {T.YORU}; + margin-bottom: 1; + }} + SuggestView #s-table {{ + height: auto; + background: {T.YORU}; + color: {T.FG_ALT}; + border: none; + overflow-x: hidden; + }} + SuggestView #s-table:focus {{ + border: none; + }} + SuggestView DataTable > .datatable--header {{ + background: {T.YORU}; + color: {T.SHINKAI}; + }} + SuggestView DataTable > .datatable--cursor {{ + background: {T.BASE1}; + color: {T.YUKI}; + }} + SuggestView DataTable > .datatable--even-row {{ + background: {T.YORU}; + }} + SuggestView DataTable > .datatable--odd-row {{ + background: {T.YORU}; + }} + """ + + BINDINGS = [ + Binding("r", "rescan", "rescan"), + Binding("i", "install", "installieren"), + Binding("j", "cursor_down", "down", show=False), + Binding("k", "cursor_up", "up", show=False), + ] + + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + self.suggestions: list[Suggestion] = [] + + def compose(self) -> ComposeResult: + yield Static("", id="s-intro", markup=True) + yield DataTable(id="s-table", cursor_type="row", zebra_stripes=False) + yield Static("", id="s-flash", markup=True) + + def on_mount(self) -> None: + table = self.query_one("#s-table", DataTable) + table.add_column("", width=2, key="glyph") + table.add_column("kat", width=8, key="cat") + table.add_column("anlass", key="reason") + table.add_column("pakete", key="pkgs") + table.add_column("pri", width=4, key="pri") + self.action_rescan() + + def action_rescan(self) -> None: + self._flash("scanne …") + pac.invalidate() + installed = set(pac.installed_packages()) + self.suggestions = sorted( + all_suggestions(installed), + key=lambda s: (s.priority, s.category), + ) + self._refresh_view() + self._flash(f"{len(self.suggestions)} empfehlungen") + + def action_install(self) -> None: + cur = self._current() + if cur is None: + self._flash("nichts ausgewählt", color=T.BASE4) + return + body = ( + f"folgende pakete via paru -S installieren?\n\n" + f" {', '.join(cur.packages)}\n\n" + f"grund: {cur.reason}" + ) + self.app.push_screen( + ConfirmModal("installieren?", body, danger=False), + lambda ok: self._after_install(ok, cur), + ) + + def _after_install(self, confirmed: bool | None, s: Suggestion) -> None: + if not confirmed: + self._flash("abgebrochen") + return + from taninux.core import sysinfo + helper = "paru" if sysinfo.has_binary("paru") else ( + "yay" if sysinfo.has_binary("yay") else "sudo pacman" + ) + argv = helper.split() + ["-S", *s.packages] + codes = run_interactive( + self.app, + [Command(label=f"install {len(s.packages)} pakete", argv=argv)], + ) + ok = codes and codes[0] == 0 + pac.invalidate() + self._flash( + "installation ok — rescan…" if ok else "installation fehlgeschlagen", + color=T.TAKE if ok else T.RED, + ) + self.action_rescan() + + def action_cursor_down(self) -> None: + self.query_one("#s-table", DataTable).action_cursor_down() + + def action_cursor_up(self) -> None: + self.query_one("#s-table", DataTable).action_cursor_up() + + def _current(self) -> Suggestion | None: + t = self.query_one("#s-table", DataTable) + r = t.cursor_row + if r is None or r < 0 or r >= len(self.suggestions): + return None + return self.suggestions[r] + + def _flash(self, msg: str, color: str | None = None) -> None: + color = color or T.BASE4 + self.query_one("#s-flash", Static).update(f"[{color}]{msg}[/]") + + def _refresh_view(self) -> None: + intro = ( + f"[{T.UME}]✦ suggest[/]\n" + f"[{T.BASE5}]system-aware empfehlungen — `i` installiert die ausgewählte zeile via paru[/]" + ) + self.query_one("#s-intro", Static).update(intro) + + table = self.query_one("#s-table", DataTable) + table.clear() + for s in self.suggestions: + glyph, color = CATEGORY_GLYPH.get(s.category, ("·", T.BASE4)) + pri_color = {1: T.RED, 2: T.CHIKYU, 3: T.BASE5}.get(s.priority, T.BASE5) + pri_label = {1: "hoch", 2: "mittel", 3: "nice"}.get(s.priority, str(s.priority)) + pkg_count = len(s.packages) + pkgs_str = ", ".join(s.packages[:4]) + (f" +{pkg_count - 4}" if pkg_count > 4 else "") + table.add_row( + f"[{color}]{glyph}[/]", + f"[{color}]{s.category}[/]", + f"[{T.YUKI}]{s.reason}[/]", + f"[{T.BASE5}]{pkgs_str}[/]", + f"[{pri_color}]{pri_label}[/]", + ) diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/update/__init__.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/update/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/update/pillar.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/update/pillar.py new file mode 100644 index 0000000..bbaa067 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/update/pillar.py @@ -0,0 +1,13 @@ +from __future__ import annotations + +from taninux.pillars.base import Pillar +from taninux.update.screen import UpdateView + + +UPDATE = Pillar( + key="u", + label="update", + tagline="pacman · AUR · flatpak · firmware", + icon="↻", + factory=UpdateView, +) diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/update/screen.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/update/screen.py new file mode 100644 index 0000000..cf1f9db --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/update/screen.py @@ -0,0 +1,426 @@ +"""Update-Pillar View — listet pending updates aus allen Quellen.""" +from __future__ import annotations + +from collections import Counter + +from textual import work +from textual.app import ComposeResult +from textual.binding import Binding +from textual.containers import Vertical +from textual.widgets import DataTable, Static + +from taninux.core import sudo, theme as T +from taninux.core.confirm import ConfirmModal +from taninux.core.runner import Command, run_interactive +from taninux.core.streaming import RunnerScreen, StreamCommand +from taninux.core.sudo_modal import SudoModal +from taninux.update.sources import fetch_all +from taninux.update.types import UpdateItem + + +SOURCE_COLOR = { + "pacman": T.UME, # violet — system core + "aur": T.CHIKYU, # yellow — community + "flatpak": T.TAKE, # green — sandboxed + "fwupd": T.SHINKAI, # blue — firmware +} +SOURCE_GLYPH = { + "pacman": "▲", + "aur": "◆", + "flatpak": "●", + "fwupd": "◈", +} + + +def _human_size(n: int) -> str: + f = float(n) + for unit in ("B", "K", "M", "G", "T"): + if f < 1024 or unit == "T": + return f"{f:.0f}{unit}" if unit == "B" else f"{f:.1f}{unit}" + f /= 1024 + return f"{f:.1f}T" + + +class UpdateView(Vertical): + """Cross-source updater: pacman + AUR + flatpak + fwupd in einer View.""" + + DEFAULT_CSS = f""" + UpdateView {{ + background: {T.YORU}; + color: {T.YUKI}; + layout: vertical; + }} + UpdateView #u-stats {{ + height: 1; + padding: 1 2 0 2; + background: {T.YORU}; + color: {T.BASE5}; + }} + UpdateView #u-counts {{ + height: 1; + padding: 1 2 1 2; + background: {T.YORU}; + color: {T.BASE4}; + }} + UpdateView #u-table {{ + height: 1fr; + background: {T.YORU}; + color: {T.FG_ALT}; + border: none; + overflow-x: hidden; + scrollbar-size-vertical: 1; + scrollbar-color: {T.BASE2} {T.YORU}; + scrollbar-background: {T.YORU}; + scrollbar-corner-color: {T.YORU}; + }} + UpdateView #u-table:focus {{ + border: none; + }} + UpdateView DataTable > .datatable--header {{ + background: {T.YORU}; + color: {T.SHINKAI}; + }} + UpdateView DataTable > .datatable--cursor {{ + background: {T.BASE1}; + color: {T.YUKI}; + }} + UpdateView DataTable > .datatable--hover {{ + background: {T.YORU}; + }} + UpdateView DataTable > .datatable--even-row {{ + background: {T.YORU}; + }} + UpdateView DataTable > .datatable--odd-row {{ + background: {T.YORU}; + }} + UpdateView #u-advice {{ + height: auto; + padding: 1 2; + background: {T.YORU}; + color: {T.BASE4}; + }} + UpdateView #u-flash {{ + height: 1; + padding: 0 2; + background: {T.YORU}; + color: {T.BASE4}; + }} + """ + + BINDINGS = [ + Binding("e", "execute", "ausführen (TUI)"), + Binding("E", "execute_external", "ausführen (extern)", show=False), + Binding("r", "rescan", "rescan"), + Binding("j", "cursor_down", "down", show=False), + Binding("k", "cursor_up", "up", show=False), + Binding("g", "cursor_top", "top", show=False), + Binding("G", "cursor_bottom", "bottom", show=False), + ] + + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + self.items: list[UpdateItem] = [] + self._loaded = False + + def compose(self) -> ComposeResult: + yield Static("", id="u-stats", markup=True) + yield Static("", id="u-counts", markup=True) + yield DataTable(id="u-table", cursor_type="row", zebra_stripes=False) + yield Static("", id="u-advice", markup=True) + yield Static("", id="u-flash", markup=True) + + def on_mount(self) -> None: + table = self.query_one("#u-table", DataTable) + table.add_column("", width=2, key="glyph") + table.add_column("quelle", width=9, key="source") + table.add_column("paket", key="name") + table.add_column("aktuell", key="current") + table.add_column("→", width=1, key="arrow") + table.add_column("neu", key="new") + table.add_column("grösse", key="size") + self.action_rescan() + + # ---- actions ---- + + def action_rescan(self) -> None: + self._flash("scanne updates… (kann 30+ sek dauern)") + self._rescan_worker() + + @work(thread=True, exclusive=True, group="update-scan") + def _rescan_worker(self) -> None: + items = fetch_all() + self.app.call_from_thread(self._after_rescan, items) + + def _after_rescan(self, items: list[UpdateItem]) -> None: + self.items = items + self._loaded = True + self._refresh_view() + self._flash(f"{len(items)} updates verfügbar") + + def action_execute(self) -> None: + """Update INSIDE the TUI: sudo-prompt → streaming modal mit --noconfirm.""" + if not self.items: + self._flash("nichts zu tun — system aktuell", color=T.TAKE) + return + cmds = self._build_stream_commands() + if not cmds: + return + + body = ( + "die folgenden befehle werden im TUI ausgeführt (live output):\n\n" + + "\n".join(f" · {c.label}\n $ {' '.join(c.argv)}" for c in cmds) + + "\n\nhinweis: --noconfirm ist aktiv, alle prompts werden auto-bestätigt.\n" + + "für interaktiven modus mit prompts: Shift+E (suspendiert TUI)." + ) + self.app.push_screen( + ConfirmModal("updates im TUI ausführen?", body, danger=False), + lambda ok: self._after_execute_confirm(ok, cmds), + ) + + def action_execute_external(self) -> None: + """Klassischer suspend-modus für interaktive updates mit prompts.""" + if not self.items: + self._flash("nichts zu tun — system aktuell", color=T.TAKE) + return + cmds = self._build_commands() + if not cmds: + return + body = ( + "TUI wird suspendiert, befehle laufen interaktiv im terminal:\n\n" + + "\n".join(f" · {c.label}\n $ {' '.join(c.argv)}" for c in cmds) + ) + self.app.push_screen( + ConfirmModal("extern interaktiv ausführen?", body, danger=False), + lambda ok: self._after_external_confirm(ok, cmds), + ) + + def _build_commands(self) -> list[Command]: + """Interaktive Variante (mit prompts).""" + counts = Counter(i.source for i in self.items) + cmds: list[Command] = [] + if counts.get("pacman") or counts.get("aur"): + from taninux.core import sysinfo + helper = "paru" if sysinfo.has_binary("paru") else ( + "yay" if sysinfo.has_binary("yay") else None + ) + if helper: + cmds.append(Command( + label=f"system + AUR ({counts.get('pacman', 0)} pacman, {counts.get('aur', 0)} AUR)", + argv=[helper, "-Syu"], + )) + elif counts.get("pacman"): + cmds.append(Command( + label=f"system ({counts.get('pacman', 0)} pakete)", + argv=["sudo", "pacman", "-Syu"], + )) + if counts.get("flatpak"): + cmds.append(Command( + label=f"flatpak ({counts.get('flatpak')} apps)", + argv=["flatpak", "update", "-y"], + )) + if counts.get("fwupd"): + cmds.append(Command( + label=f"firmware ({counts.get('fwupd')} geräte)", + argv=["sudo", "fwupdmgr", "update"], + )) + return cmds + + def _build_stream_commands(self) -> list[StreamCommand]: + """Streaming-Variante mit aggressiven unattended-flags. + + paru flags: + --noconfirm keine [Y/n] prompts + --sudoloop hält sudo-cache am leben während build + --skipreview keine PKGBUILD-review prompts + --removemake cleanup nach build + --cleanafter src-dir cleanup + --batchinstall alle AUR pakete in einem pacman call + """ + counts = Counter(i.source for i in self.items) + out: list[StreamCommand] = [] + if counts.get("pacman") or counts.get("aur"): + from taninux.core import sysinfo + helper = "paru" if sysinfo.has_binary("paru") else ( + "yay" if sysinfo.has_binary("yay") else None + ) + if helper: + out.append(StreamCommand( + label=f"system + AUR via {helper}", + argv=[ + helper, "-Syu", + "--noconfirm", "--sudoloop", + "--skipreview", "--removemake", "--cleanafter", + "--batchinstall", + ], + )) + elif counts.get("pacman"): + out.append(StreamCommand( + label="system via pacman", + argv=["sudo", "-n", "pacman", "-Syu", "--noconfirm"], + )) + if counts.get("flatpak"): + out.append(StreamCommand( + label=f"flatpak ({counts.get('flatpak')} apps)", + argv=["flatpak", "update", "--noninteractive"], + )) + if counts.get("fwupd"): + out.append(StreamCommand( + label=f"firmware ({counts.get('fwupd')} geräte)", + argv=["sudo", "-n", "fwupdmgr", "update", + "--assume-yes", "--no-reboot-check"], + )) + return out + + def _after_execute_confirm(self, confirmed: bool | None, + cmds: list[StreamCommand]) -> None: + if not confirmed: + self._flash("abgebrochen") + return + # Brauchen wir sudo? + needs_sudo = any( + c.argv[0] == "sudo" or c.argv[0] in ("paru", "yay") + for c in cmds + ) + if needs_sudo and not sudo.is_cached(): + self.app.push_screen( + SudoModal(), + lambda pw: self._after_sudo_prompt(pw, cmds), + ) + else: + self._launch_runner(cmds) + + def _after_sudo_prompt(self, pw: str | None, + cmds: list[StreamCommand]) -> None: + if not pw: + self._flash("kein passwort — abgebrochen") + return + if not sudo.cache(pw): + self._flash("falsches passwort", color=T.RED) + return + self._launch_runner(cmds) + + def _launch_runner(self, cmds: list[StreamCommand]) -> None: + self.app.push_screen(RunnerScreen(cmds), self._after_runner) + + def _after_runner(self, codes: list[int] | None) -> None: + from taninux.core import pacman as pac + pac.invalidate() + if not codes: + self._flash("kein ergebnis") + else: + ok = sum(1 for c in codes if c == 0) + self._flash( + f"{ok}/{len(codes)} befehle ok — rescan…", + color=T.TAKE if ok == len(codes) else T.CHIKYU, + ) + self.action_rescan() + + def _after_external_confirm(self, confirmed: bool | None, + cmds: list[Command]) -> None: + if not confirmed: + self._flash("abgebrochen") + return + codes = run_interactive(self.app, cmds) + from taninux.core import pacman as pac + pac.invalidate() + ok = sum(1 for c in codes if c == 0) + self._flash( + f"{ok}/{len(codes)} befehle ok — rescan…", + color=T.TAKE if ok == len(codes) else T.CHIKYU, + ) + self.action_rescan() + + def action_cursor_down(self) -> None: + self.query_one("#u-table", DataTable).action_cursor_down() + + def action_cursor_up(self) -> None: + self.query_one("#u-table", DataTable).action_cursor_up() + + def action_cursor_top(self) -> None: + t = self.query_one("#u-table", DataTable) + if t.row_count: + t.move_cursor(row=0) + + def action_cursor_bottom(self) -> None: + t = self.query_one("#u-table", DataTable) + if t.row_count: + t.move_cursor(row=t.row_count - 1) + + # ---- helpers ---- + + def _flash(self, msg: str, color: str | None = None) -> None: + color = color or T.BASE4 + self.query_one("#u-flash", Static).update(f"[{color}]{msg}[/]") + + # ---- render ---- + + def _refresh_view(self) -> None: + table = self.query_one("#u-table", DataTable) + table.clear() + + # sort: pacman, aur, flatpak, fwupd + order = {"pacman": 0, "aur": 1, "flatpak": 2, "fwupd": 3} + items = sorted(self.items, key=lambda i: (order.get(i.source, 9), i.name)) + + for i in items: + c = SOURCE_COLOR.get(i.source, T.BASE5) + g = SOURCE_GLYPH.get(i.source, "·") + size = _human_size(i.size_bytes) if i.size_bytes else "—" + table.add_row( + f"[{c}]{g}[/]", + f"[{c}]{i.source}[/]", + f"[{T.YUKI}]{i.name}[/]", + f"[{T.BASE4}]{i.current_version}[/]", + f"[{T.BASE3}]→[/]", + f"[{T.TAKE}]{i.new_version}[/]", + f"[{T.CHIKYU}]{size}[/]" if i.size_bytes else f"[{T.BASE3}]—[/]", + ) + + # stats + total_size = sum(i.size_bytes for i in self.items) + if not self.items: + stats = f"[{T.TAKE}]system ist aktuell[/] [{T.BASE3}]·[/] [{T.BASE5}]keine pending updates[/]" + else: + stats = ( + f"[{T.YUKI}]{len(self.items)}[/] [{T.BASE5}]updates verfügbar[/]" + + (f" [{T.BASE3}]·[/] [{T.CHIKYU}]{_human_size(total_size)}[/] [{T.BASE5}]download[/]" + if total_size else "") + ) + self.query_one("#u-stats", Static).update(stats) + + # source counts legend + by_source = Counter(i.source for i in self.items) + parts = [] + for src in ("pacman", "aur", "flatpak", "fwupd"): + n = by_source.get(src, 0) + c = SOURCE_COLOR[src] + g = SOURCE_GLYPH[src] + count_color = T.BASE4 if n == 0 else T.BASE5 + parts.append(f"[{c}]{g}[/] [{count_color}]{src} {n}[/]") + self.query_one("#u-counts", Static).update(f" [{T.BASE3}]│[/] ".join(parts)) + + # advice — concrete commands + if not self.items: + self.query_one("#u-advice", Static).update("") + return + action_lines = [ + f"[{T.SHINKAI}]aktionen[/]", + f" [{T.UME}]e[/] [{T.YUKI}]alle {len(self.items)} updates im TUI installieren[/] " + f"[{T.BASE5}](streaming, --noconfirm)[/]", + f" [{T.BASE5}]E[/] [{T.BASE4}]extern interaktiv (suspend → terminal)[/]", + f" [{T.BASE5}]r[/] [{T.BASE4}]rescan[/]", + "", + f"[{T.SHINKAI}]oder shell[/]", + ] + sh_cmds: list[str] = [] + if by_source.get("pacman") or by_source.get("aur"): + sh_cmds.append("paru -Syu") + elif by_source.get("pacman"): + sh_cmds.append("sudo pacman -Syu") + if by_source.get("flatpak"): + sh_cmds.append("flatpak update -y") + if by_source.get("fwupd"): + sh_cmds.append("sudo fwupdmgr update") + for c in sh_cmds: + action_lines.append(f" [{T.CHIKYU}]{c}[/]") + self.query_one("#u-advice", Static).update("\n".join(action_lines)) diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/update/sources/__init__.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/update/sources/__init__.py new file mode 100644 index 0000000..ac35925 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/update/sources/__init__.py @@ -0,0 +1,23 @@ +"""Update-Sources Registry.""" +from __future__ import annotations + +from taninux.update.sources import aur, flatpak, fwupd, pacman +from taninux.update.types import UpdateItem + + +SOURCES = { + "pacman": pacman.fetch, + "aur": aur.fetch, + "flatpak": flatpak.fetch, + "fwupd": fwupd.fetch, +} + + +def fetch_all() -> list[UpdateItem]: + items: list[UpdateItem] = [] + for fn in SOURCES.values(): + try: + items += fn() + except Exception: # noqa: BLE001 + pass + return items diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/update/sources/aur.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/update/sources/aur.py new file mode 100644 index 0000000..70f24e9 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/update/sources/aur.py @@ -0,0 +1,48 @@ +"""AUR: pending updates via `paru -Qua` (oder yay als Fallback).""" +from __future__ import annotations + +import re +import shutil +import subprocess + +from taninux.update.types import UpdateItem + +_PARU_LINE = re.compile(r"^(\S+)\s+(\S+)\s+->\s+(\S+)") + + +def _helper() -> str | None: + for h in ("paru", "yay"): + if shutil.which(h): + return h + return None + + +def fetch() -> list[UpdateItem]: + helper = _helper() + if not helper: + return [] + try: + r = subprocess.run( + [helper, "-Qua"], + capture_output=True, text=True, check=False, timeout=60, + ) + except (FileNotFoundError, subprocess.TimeoutExpired): + return [] + if r.returncode not in (0, 1): + return [] + items: list[UpdateItem] = [] + for line in r.stdout.splitlines(): + line = line.strip() + if not line or line.startswith(":"): # paru status lines beginnen mit :: + continue + m = _PARU_LINE.match(line) + if m: + items.append(UpdateItem( + source="aur", + name=m.group(1), + current_version=m.group(2), + new_version=m.group(3), + size_bytes=0, + extra={"helper": helper}, + )) + return items diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/update/sources/flatpak.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/update/sources/flatpak.py new file mode 100644 index 0000000..c23abb5 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/update/sources/flatpak.py @@ -0,0 +1,66 @@ +"""Flatpak: pending updates via `flatpak remote-ls --updates`.""" +from __future__ import annotations + +import subprocess + +from taninux.update.types import UpdateItem + + +def _installed_versions() -> dict[str, str]: + try: + r = subprocess.run( + ["flatpak", "list", "--columns=application,version"], + capture_output=True, text=True, check=False, timeout=10, + ) + except (FileNotFoundError, subprocess.TimeoutExpired): + return {} + versions: dict[str, str] = {} + for line in r.stdout.splitlines(): + s = line.strip() + if not s or s.startswith("Application"): + continue + parts = s.split("\t") + if len(parts) >= 2: + versions[parts[0]] = parts[1] + return versions + + +def fetch() -> list[UpdateItem]: + try: + r = subprocess.run( + ["flatpak", "remote-ls", "--updates", + "--columns=application,version,download-size"], + capture_output=True, text=True, check=False, timeout=60, + ) + except (FileNotFoundError, subprocess.TimeoutExpired): + return [] + if r.returncode != 0: + return [] + + current = _installed_versions() + items: list[UpdateItem] = [] + for line in r.stdout.splitlines(): + s = line.strip() + if not s or s.startswith("Application"): + continue + parts = s.split("\t") + if len(parts) < 2: + continue + app, new_ver = parts[0], parts[1] + size = 0 + if len(parts) >= 3: + # download-size column is human-readable like "12.3 MB" + size_str = parts[2] + try: + from taninux.maintain.scanners.pacman import _parse_size + size = _parse_size(size_str) + except Exception: + size = 0 + items.append(UpdateItem( + source="flatpak", + name=app, + current_version=current.get(app, "?"), + new_version=new_ver, + size_bytes=size, + )) + return items diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/update/sources/fwupd.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/update/sources/fwupd.py new file mode 100644 index 0000000..33c2c04 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/update/sources/fwupd.py @@ -0,0 +1,38 @@ +"""Firmware: pending updates via `fwupdmgr get-updates --json`.""" +from __future__ import annotations + +import json +import subprocess + +from taninux.update.types import UpdateItem + + +def fetch() -> list[UpdateItem]: + try: + r = subprocess.run( + ["fwupdmgr", "get-updates", "--json"], + capture_output=True, text=True, check=False, timeout=30, + ) + except (FileNotFoundError, subprocess.TimeoutExpired): + return [] + if r.returncode != 0 or not r.stdout.strip(): + return [] + try: + data = json.loads(r.stdout) + except json.JSONDecodeError: + return [] + items: list[UpdateItem] = [] + for dev in data.get("Devices", []): + releases = dev.get("Releases", []) + if not releases: + continue + rel = releases[0] + items.append(UpdateItem( + source="fwupd", + name=dev.get("Name", "?"), + current_version=dev.get("Version", "?"), + new_version=rel.get("Version", "?"), + size_bytes=int(rel.get("Size", 0) or 0), + extra={"device_id": dev.get("DeviceId", "")}, + )) + return items diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/update/sources/pacman.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/update/sources/pacman.py new file mode 100644 index 0000000..d352fad --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/update/sources/pacman.py @@ -0,0 +1,34 @@ +"""Pacman: pending updates via `checkupdates` (aus pacman-contrib). + +`checkupdates` synct in ein temporäres DB ohne sudo zu brauchen und +listet was bei `pacman -Syu` aktualisiert würde.""" +from __future__ import annotations + +import subprocess + +from taninux.update.types import UpdateItem + + +def fetch() -> list[UpdateItem]: + try: + r = subprocess.run( + ["checkupdates"], + capture_output=True, text=True, check=False, timeout=60, + ) + except (FileNotFoundError, subprocess.TimeoutExpired): + return [] + # checkupdates exitet 2 wenn keine Updates pending — kein Fehler + if r.returncode not in (0, 2): + return [] + items: list[UpdateItem] = [] + for line in r.stdout.splitlines(): + parts = line.split() + if len(parts) >= 4 and parts[2] == "->": + items.append(UpdateItem( + source="pacman", + name=parts[0], + current_version=parts[1], + new_version=parts[3], + size_bytes=0, + )) + return items diff --git a/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/update/types.py b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/update/types.py new file mode 100644 index 0000000..6a810b4 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/python3.14/site-packages/taninux/update/types.py @@ -0,0 +1,14 @@ +from __future__ import annotations + +from dataclasses import dataclass, field + + +@dataclass +class UpdateItem: + """Ein verfügbares Update aus einer Quelle (pacman/AUR/flatpak/fwupd).""" + source: str # 'pacman', 'aur', 'flatpak', 'fwupd' + name: str # Paket- oder Geräte-Name + current_version: str # aktuelle Version + new_version: str # Ziel-Version + size_bytes: int = 0 # Download-Grösse falls bekannt + extra: dict[str, str] = field(default_factory=dict) diff --git a/packaging/taninux/pkg/taninux/usr/lib/taninux/taninux-helper b/packaging/taninux/pkg/taninux/usr/lib/taninux/taninux-helper new file mode 100755 index 0000000..dc8daa0 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/lib/taninux/taninux-helper @@ -0,0 +1,198 @@ +#!/usr/bin/env python3 +"""TANINUX root-Helper — wird via pkexec mit erhöhten Rechten gestartet. + +Installationspfad: /usr/lib/taninux/taninux-helper +(passend zur polkit-Action ch.gabrielevarano.taninux.maintain, deren +exec.path genau auf diesen Pfad zeigt.) + +Idee: Statt `pkexec pacman …` direkt laufen zu lassen (= beliebiges root!), +reicht das GTK-Frontend sein argv durch DIESEN Helper. Der prüft das Kommando +gegen eine feste ALLOWLIST und lehnt alles andere ab. So bleibt die +argv-basierte Schnittstelle (`["pacman","-Syu"]`, `["bootctl","set-default",…]`) +nach aussen unverändert, aber root kann nur noch die erlaubten Operationen. + +Aufruf: + taninux-helper [args…] → validieren + ausführen (exec) + taninux-helper --check [..] → nur validieren, Exit 0/2 (für Tests) + +Keine externen Deps — reines Stdlib, damit der Helper auch ohne installiertes +taninux-Paket aus dem Repo heraus läuft. +""" +from __future__ import annotations + +import os +import sys + +# ALLOWLIST: erste argv-Komponente → erlaubte Operationen. +# +# Pro Kommando definieren wir, welche der nachfolgenden Tokens als Sub-/Flag +# zulässig sind. Tokens, die mit "-" beginnen, müssen in "flags" stehen +# (oder einer der erlaubten Operatoren sein). Nicht-Flag-Tokens gelten als +# Operanden (Paketnamen, Boot-Einträge) und werden NICHT eingeschränkt — +# pacman/bootctl validieren die selbst, und sie können kein anderes Programm +# starten. +# +# Wichtig: KEINE Shell, kein eval. Wir führen via os.execvp() exakt das +# übergebene argv aus, ohne Interpretation durch eine Shell. +ALLOWLIST: dict[str, dict[str, set[str]]] = { + # Pakete (de)installieren + Vollupgrade. --noconfirm erlaubt, damit das + # GUI-Frontend nicht in einem TTY-Prompt hängen bleibt. + "pacman": { + "ops": {"-S", "-Syu", "-Su", "-Sy", "-R", "-Rs", "-Rns"}, + "flags": {"--noconfirm", "--needed", "--noprogressbar"}, + }, + # Firewall an/aus (ufw). Nur enable/disable — keine Regel-Manipulation. + "ufw": { + "ops": {"enable", "disable"}, + "flags": set(), + }, + # Benutzerverwaltung. useradd bewusst NUR mit -m/-c (KEIN -G/-u/-p/-o → + # keine Gruppen-/UID-Injektion); Admin-Rechte laufen separat über gpasswd + # (zu/aus wheel). passwd nur -e (Reset beim 1. Login erzwingen, kein Setzen). + "useradd": { + "ops": set(), + "flags": {"-m", "-c"}, + }, + "userdel": { + "ops": set(), + "flags": {"-r"}, + }, + "gpasswd": { + "ops": {"-a", "-d"}, + "flags": set(), + }, + "passwd": { + "ops": {"-e"}, + "flags": set(), + }, + # Bootloader-Default / einmaliger Boot (systemd-boot). + "bootctl": { + "ops": {"set-default", "set-oneshot"}, + "flags": set(), + }, + # Paket-Cache aufräumen. + "paccache": { + "ops": {"-r", "-rk1", "-rk0", "-ruk0"}, + "flags": {"-k", "0", "1", "2", "3", "--remove", "--uninstalled"}, + }, + # System-Locale + Tastatur-Layout (systemd). Operanden (LANG=…, Layout, + # Variante) sind frei; localectl validiert sie selbst. + "localectl": { + "ops": {"set-locale", "set-keymap", "set-x11-keymap"}, + "flags": set(), + }, + # Zeitzone + NTP (systemd-timesyncd). Operanden (Zeitzone, true/false) + # sind freie Operanden; timedatectl validiert sie selbst. set-time o.ä. + # ist bewusst NICHT erlaubt. + "timedatectl": { + "ops": {"set-timezone", "set-ntp", "set-local-rtc"}, + "flags": set(), + }, + # Dienste verwalten (z.B. power-profiles-daemon enablen). Unit-Namen sind + # freie Operanden; nur diese Verben sind erlaubt — kein mask/edit/o.ä. + "systemctl": { + "ops": {"enable", "disable", "start", "stop", "restart", "reenable"}, + "flags": {"--now"}, + }, + # CUPS-Druckerverwaltung. lpadmin: Default setzen (-d) / Drucker entfernen + # (-x); -p/-E nur als Flags zugelassen (kein Hinzufügen mit Treiber). + "lpadmin": { + "ops": {"-d", "-x"}, + "flags": {"-p", "-E"}, + }, + # Drucker an-/abschalten. KEINE Op — nur ein Operand (der Druckername). + "cupsenable": { + "ops": set(), + "flags": set(), + }, + "cupsdisable": { + "ops": set(), + "flags": set(), + }, + # Journal aufräumen (vacuum). Nur feste, ungefährliche Vacuum-Flags. + "journalctl": { + "ops": set(), + "flags": {"--vacuum-time=2weeks", "--vacuum-size=200M"}, + }, + # Snapshots (Timeshift). Braucht für ALLES root, auch --list. Snapshot- + # Namen/Kommentare sind freie Operanden (kein "-" am Anfang). + "timeshift": { + "ops": {"--list", "--create", "--delete", "--delete-all", "--restore", "--check"}, + "flags": {"--snapshot", "--comments", "--yes", "--scripted", "--tags"}, + }, +} + + +class Rejected(Exception): + """Argv hat die Allowlist nicht passiert.""" + + +def validate(argv: list[str]) -> list[str]: + """Prüft argv gegen die ALLOWLIST. Gibt das (unveränderte) argv zurück + oder wirft Rejected mit einer Begründung.""" + if not argv: + raise Rejected("leeres kommando") + + cmd = argv[0] + rule = ALLOWLIST.get(cmd) + if rule is None: + raise Rejected(f"kommando nicht erlaubt: {cmd!r}") + + allowed_ops = rule["ops"] + allowed_flags = rule["flags"] + saw_op = False + + for tok in argv[1:]: + if tok in allowed_ops: + saw_op = True + continue + if tok in allowed_flags: + continue + if tok.startswith("-"): + # ein Flag/Operator, der weder Operation noch erlaubtes Flag ist + raise Rejected(f"option nicht erlaubt für {cmd}: {tok!r}") + # ein Operand (Paketname, Boot-Eintrag) — keine Optionen-Injektion + # möglich, da kein "-" am Anfang. Operanden sind frei. + + # Kommandos mit erlaubten Operationen müssen eine davon nennen. Kommandos + # OHNE Ops (z.B. cupsenable/cupsdisable: nur Operand = Druckername) sind + # auch ohne Op gültig. + if allowed_ops and not saw_op: + raise Rejected(f"keine erlaubte operation für {cmd} angegeben") + + # gpasswd darf NUR die wheel-Gruppe verwalten (Admin-Toggle) — keine + # beliebige Gruppen-Mitgliedschaft (z.B. root). + if cmd == "gpasswd" and (not argv or argv[-1] != "wheel"): + raise Rejected("gpasswd nur für die wheel-Gruppe erlaubt") + + return argv + + +def main(raw_argv: list[str]) -> int: + check_only = False + args = raw_argv[1:] + if args and args[0] == "--check": + check_only = True + args = args[1:] + + try: + argv = validate(args) + except Rejected as err: + print(f"taninux-helper: abgelehnt: {err}", file=sys.stderr) + return 2 + + if check_only: + print(f"taninux-helper: ok: {' '.join(argv)}") + return 0 + + # erlaubtes Kommando ausführen — execvp ersetzt den Helper-Prozess, + # damit pkexec den echten Exit-Code des Kommandos durchreicht. + try: + os.execvp(argv[0], argv) + except OSError as err: # Programm nicht gefunden o.ä. + print(f"taninux-helper: exec fehlgeschlagen: {err}", file=sys.stderr) + return 127 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/packaging/taninux/pkg/taninux/usr/share/applications/ch.gabrielevarano.Taninux.Settings.desktop b/packaging/taninux/pkg/taninux/usr/share/applications/ch.gabrielevarano.Taninux.Settings.desktop new file mode 100644 index 0000000..23f47cf --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/share/applications/ch.gabrielevarano.Taninux.Settings.desktop @@ -0,0 +1,10 @@ +[Desktop Entry] +Type=Application +Name=System Settings +Comment=System, hardware and personalization settings +Exec=tsettings +Icon=preferences-system +Terminal=false +Categories=Settings;GTK; +Keywords=settings;system;kernel;display;audio;network;bluetooth;power; +StartupNotify=true diff --git a/packaging/taninux/pkg/taninux/usr/share/applications/ch.gabrielevarano.Taninux.SoftwareHub.desktop b/packaging/taninux/pkg/taninux/usr/share/applications/ch.gabrielevarano.Taninux.SoftwareHub.desktop new file mode 100644 index 0000000..f824c67 --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/share/applications/ch.gabrielevarano.Taninux.SoftwareHub.desktop @@ -0,0 +1,10 @@ +[Desktop Entry] +Type=Application +Name=Software Hub +Comment=Search, install and update packages (pacman, AUR, Flatpak) +Exec=thub +Icon=system-software-install +Terminal=false +Categories=System;PackageManager;GTK; +Keywords=software;packages;install;pacman;aur;flatpak;update; +StartupNotify=true diff --git a/packaging/taninux/pkg/taninux/usr/share/polkit-1/actions/ch.gabrielevarano.Taninux.policy b/packaging/taninux/pkg/taninux/usr/share/polkit-1/actions/ch.gabrielevarano.Taninux.policy new file mode 100644 index 0000000..c1fc9cc --- /dev/null +++ b/packaging/taninux/pkg/taninux/usr/share/polkit-1/actions/ch.gabrielevarano.Taninux.policy @@ -0,0 +1,31 @@ + + + + + TANINUX + https://gabrielevarano.ch + + + TANINUX-Systemwartung ausführen + Authentifizierung nötig, um Systempakete zu verwalten + + auth_admin + auth_admin + auth_admin_keep + + /usr/lib/taninux/taninux-helper + true + + diff --git a/packaging/taninux/taninux-0.2.0-1-any.pkg.tar.zst b/packaging/taninux/taninux-0.2.0-1-any.pkg.tar.zst new file mode 100644 index 0000000000000000000000000000000000000000..d5ac7130a926309172441ab5da42940e16171d00 GIT binary patch literal 556033 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≻1wcrY+NYr-#?!%xQe=%&V&g=@#f>8Y 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�Do(UxE4TxTf9f}n91_zrknjz7WJE9LgW`q!a z{^tI=Opk%#OD`_IUXLxch%lPTvsDgZw8|=-C8fW{=b?0tbCZo+`G#z3*d>y_E{$}) zwClg5>n!Wd!}0@-PP$3|lz$iE&k(LZER#!M9tfjX{%09tmtsF7m>f&~v-tVjO@EV5 z^DHyD&WT_*xL3@rKWzIzjCskio7iZDXEGhE~3Mf zV->J1EPx?W@F9BM3j9B=?qMXhy49WT?t!N(PV6SF8Cg$RNGR3Q?ct8Z)+f?(`xYWa zpusQedbur2ga^e|x7uQ7gWz9X;rzhAn&Rmq0x)Qg1X?(S+-yO1-YE(a`veVcwje|A zREWfG`Y}W6NsC7dC`*n(mH|dq&RZ=~K#UUy2N{XAYqbW`A*a=W*RW_9R{y^X52(t) ze*cd`!|52sG@O9&g{JLS&s9KRY%~sm!1$@Cd?5wOdS(j#Kh9FcFUFJ!u08y}Z5an) z%>Tzh|33rh{~rO13K|QwvN+Q-6R(Hh|KFAppr7bv(!EzK8`8(`<4eynyQ#^vmx>ZR zQBSrs zPZ}>NIf=#qh-qal>*4=*#4TH5g^Pm%7H36oh61+43NAwdTVln_QNWg10mRktIG*?U z|Gv5hoV&W!Biw8`fOw}LNbDvk4wMJlaxxQgv)wA*X$lhCr)82AlT`!7x$6^Af!{`? z2=FY(i*{O+vwc8hKT3W~Y1znn&G4duYms2FZ8%>$UOf1-r&fKNKAn`r^F$W}=fc+N zhc*Wd@TkGGP?g(3zkdH;l&Y0=(T2}#EdDUjNSgLhB8z5Y@dtNDfd2mm(6gV8(rBEO z_54K7Q1ncki&Vi9X3_tz(0Ll#dYEb0&401q7kSaQF1ae)B&@qgFE{xq|1`vZ-BjF1 zYHQE(DgQCVf5osC>4o&>j8-JJvq)_cCB_0JYs6~b)dQvR+E;s?G+wLeAQpv6q_Kv< zvmhny^u>9rj?~`mlEvH@5kSCp4PAN8?3ghD+ecX&p@GD4k^V}=fr5o*bfii2qYyAM}iuFDdsiLtP%FZzMR{_&(Dpf?-Q8*x&kuM zY;o1XhJUpR3d4dGIh9)AJ&zn<<1Eqqx%$CDgQ@Tc|Ei4Ud^O`3Hfb{Iivkb{fCvGz zmg-|g150Ua(x)CYwk;ccgPSdgiFaCp#3tcln|QX^(IPj~p@o%^g_2s37FIM^AgSFT zrp*D^;vjBw;5yJj=dhxULnTt_-g8w>_EQdp#94O#KNbD|PWu0A(EqQb-~Vr?l1Lj* z^#422|9_Hp#7|55+{AG+uV>HkUKmi(k- z0RP_(F~XAtU>9H=IOtOk8k?OP^8ZR@T{H?;RM2LAuUI+U)c@`0kxHyI^gJ75WnGMe zI;K!r7mYFy^vUlfXJDw!ac|&PK6zJSBitMKm0y#NWAu_^B^@Rkz4DvibY)zI3CVqe4m!!MxepZm%2!B&x2hlFW2lmsh;GX)>WguW<12)Y#gZk zBtChbCY&vDwCE}97$7D;VrqzCLCX*RL*};C;&c&p0fP$|fHPE?o81F-dMckVo;psT z96&mFoVpw}#m)9<>7dvqQf??AvsFTdi${l^El(0oAEz!yCAuQQfrF_mno207NkLml zL0mW>E=nLG$A}agXi~OlQn*x7s4SX8H&WvaMjD1%AV44@2MmRMz&=`HDvRce5>gmI zSRgJxVl%{O;94SF=uR8wO^OXSDO@E{7du?=P)VWkG-oaRd1`-JNQlkPj^mi6f$BaY77*%iL4eew2*-(YmU;C*gzAh3@C~T|7ukemIF3|e>Es7 z14{-BFrrJS?Wq!h6KphJIbJ$&H|p&J|EkiH#ts@hh)?&)bH3zw(b3|>)u=WF(5`4W zZn2kDVNIEi{UI_EmK{Q5Bq!u;rf3004iq|0bW~WAriGatGdf6e)V4Wt6T-p@WhTAB zLRA8)a3~c}o891M3&P@^_K?`M77AHVSt5EiiKovFA09kU_DQ><;jq2b+e`-^t6sC^ z;=~ru;xfQ6k`A+6oY?9$QWqz-UXVcMhtCW^SHF^}4>T^Jwy>hX0?+m@k(ayvke9oz zJjAp)m}qqnw>hve^%sD4P2jcn%q-HhYvSHg#i6ljreSyO^xm+$X-?Knblo&3Yxl1C zJsXkr|Nm&-j|xc(iNqo77AotZ@pjKN?1oYnscP8`r5q;P`zWm?1?&9U5ju4@sbfPVUa zSdrwArV=8Lu$yBMhfc}%c5jaLw+$o{d(ThASx6ivU-%Zn_w07DihhuSHAyt_d@RmU z!T;YagEdL?kjaUjsh(!4*l3)Q$)E{S1{4JUUshy(dLae0@>raM|Box;_7LQEQXI#@ znj}~cvAg6h&n^b;^85&31Q`psV1Nr8JWUhYvqWUumh7`*nAnP8f@?*Kgv%BQ7p@{G zi*1X=#&xq%VLU(dg2+hNufEmupa}nuqW=F8G%0EnG{<36EiAcH&`BHfaehJ(yRG7! z5H#z}{)BfC50FuRA)MjDXDG-8?e_WMIcjTn`zzvD-%$*`jJW(iGh|}LS&xj?qc`F@ zKaa$KA^acb|Jz_q65B(pvkdB(NpZ~fU^EWdyQg-$@L9p*0?25Fix&Nbix)k@qaz*{ zJ+r(0aV%m{SjFVtN8P8Wh^Oc1z1QDtWEvv5FTXFctjqWP(hi1JpJ%zrLPOlg&4G=` z@C|$8l4B#yAATC6R-W~Tf5r%wWgl2`b(}QWwND|E?c6>|3D>_PD}n0C%TdmD*^q#QpsetX2oIUlg{fa z4lA2fVWa+jp)%r_*(K_5` zot0(%TVChFToxO0eZ6h?@7{1BLWEdT$i!B+iUWP&3_5#G5>1~gA0Pi}&>7{t;qkBT zFruBV&|?K3Q_z$hDLimb66dWAg40feJ%T=9KxaE@^G_Jjo#@C659oc3N4};h-m21Izf{%g%Krs@?g`UGY6v*S;@N_ z4%c74<%gu$=eATvMS2mP_!dWsUd|0xF1I) z(8=AP^FbPg6$QgE>f1m}P6|V^1w+C$bdKb1@S*@AVyjzq(cxc>W$|L*$$-m(iQag| zdc=IN(EdEZ>6F7sN0XDsiSkN)&e&B92mJ29HcM*Moi0caJOw(jdw$5!npI{tjLfLA z=F5n5`J9nsh7OaF9oVavn$%a@^3}LpawI-|JnkFGQk8G8MIqCyNN=o z`6#=o<|BJ^BKn`ZpyA&?2aVKT$xEy!+L%S(wH^|m{dh!F~DTtS+FP%`qJa21ZM+Y?%6IL7zYzh>>e$h zR*M$bdb3DkAdvx=#fz3eVS2!j;AYSirGmQAAsj>okDX129Z^k4Ba;xevWj6~`2Ej2 z{m)_0|J;av{r*3SsVIef{eU2XFlG^~w@0scm2-w*YsBCco8+;-4pFY?lj=Agx%m#C^Zo^DJ@S?<#R$nfiZqadIjWUGD#{(cqb~ zE*fJ@_jcPFSbz1l%e;K9vbV_u`R^>{uO}dlnYgza=Kek~ibK;>={3{5)HK-!Ikr!>mlBQStu3%`@kdwW@B6GZRhvLm+7j+|JTO~yz# zy*xw8kn3zH8~(p?hH%|Cbv0=*qcpzrRiP)1?|c`H@9-lKi$bKb2Td#rrI54}?B(*x zu@Ue|VCJb4f#r@RkEth44Q%5?L7dnsBEa0ic?ib6{}O=5bdLlQfM7FfyxZ?V8!H<-$!T!>cIMPuyp6*HWt1{+DKp|YN( zipnTwvBt5+`~5!&&O>t(Xl2xKnN${MX6&A84C}b5I45fqOO9fMI&=Hr!exwJF1>ig zz1h8FU9P)0{QRxO-E6(qY4A+Ob6@Pr0up1acp`JB%)Zj7o6f#1Y77&3Y0tTCC{Q?YVkP80Q6hIrPfe$f! zkf9~3X#+e_AgJ6!_5lNWbk1l#T|K?TI9pXdi%zN>R63@dE1Wp|t3OdBEF>6UL~Wua zpg%o+eDFY*oF_D$uAF$RVqWRVYmY#4;2?MpJ=Yl4Lbpy1k9B*yzvN78ZyHceDd8M7 zRMPkk?iV-U07=Q27?jb=VKBxF*4iF|HRk`1;6Ei2s!k4%^?P$Ho7>8@6GHYeI zSS!o0R(`Fr3$y)YF0(6Nsk!g+L1{eqRo|1wb2)y37*yPz2)$ZrXitP>xf))$md6-^ z5pq?N+QUA;b`$i(GstPHQFCCM%?e>+*PKX&dIWDYQd~2hX$R zd7~N25sNiX*hy_K5Nd&t40wRBFnDFbh?anI=<7)`LX9xFCCAuGGpt){ealH)`N%+*b%1KDo{c$3L}fh$-a^;2aY@sJ5Y7Zye(}u8oSTZr_BOt5s3s&-dl%^> zd(W@Cu7}3s@TD9^v2i^Ui$-J}JHV<2@o|oq%(zxUH5HG+{|B)Q)+8P0r{mHHH9IAx z=HQKq0SUl>rXktV>7nDp2FlFA)G_6JEB*i%&@fDz|EiS5_g;U#73W6xKcJO1W@UQ| zYd8D*L7IXAWSR95Sri(LL)*=NMJvBAx=WbJV)6+il^&N!ac@%k!!eXso++JlcFCl# zOC_Bz$zp5`yRIh-om=cR}we0M!E7oZHTq~tjiCvF5MV^i}km|I;^iWukQ9X`2uoVAl6MWgwJXx zjo*A1jo%z#s+}ftHj>B=`XQqR87zSDW2rTiH6zO=t3>n%>jws&=ns0s84I#vwPteY zI90rW%L4kSK~^%1Eg1$^hhe`ScxB*OFG`RDBBb?dz)J^r)<}RyT~2l=sXf~Td~ui| zvDxSmbO_~v%6QH@W*)<*i$W6x94nkU8uh82I_2!S+a^F_S`Z_H79c)9RLGi<;75xs z!7vLdS_7*F3{%k=&=s7p3#%AdF<^iZ9npXhI&7RVEjpagPG#h5`N%#xVRsZ#BNSUk zD7adr)Sm5Hhp~SmtSRt>tCG?hBq#CEwRRrXp;aho1p?wL{8ziy7CZp|JOp3`TLmhc zs%Y1sal?|fr)X?V(cp@rVHcrnD53q7!q!s?T*DM>p$xkSgsKHpGMEA~0)(mc*6^}` z^X3u*l&+L;>}WeAWB)`}B=5Kui))Pk>I${aMB*y^udYk0{9j~UT9Z&%IzdgtUzVgn6i^d8~J1(p|*}tTtz7R zDFr(P8Mg3*E#V1VGfuE&z_4Mcu#bSiE06)hbO=B@T_I6+@H0(6(M^Tss<-h1JSTnljr;nQ`T2)D>1JF{w#x%Ou8i zi&0rv?JVFnqxx9Ez)~J)k&jgmmU5_&v&B^l95$HRV|5~v(p=Htz{zrGb?_Cs77A^# zab_ACX?l*`OAg;p{BG%4_PUqJrek{#jq6RE>t%bpx6~}f=5RkIA$-$wjUr8a-R?~z ziO0Fd>#mo{r{h}`*6u2$a;B3)B<-H1hDT+MgnI8 zvgvjUm&vBs{ZwNpBo2|r_r>xv;s1-lnxr32oCL;3Ws`Auq?bqsNf-q)_7c;4fMoP_6zj~Q$iepTf zcLG6G z;E|e%;M+JbaekFh2K@Z(Z&LcxP+HlY zAuhe-lMk&)Nq?GcHY3wtJOB_$`oph0vM#~o9bba!`qOe9hP(Br+4@Yb^=ybUxW8PU z^<*!{UVizPJPXfgm1iSHFUwwz;f!8>UxvN>O3G$@WVmh1_L&hYv$2nT*u~Iz%y-dv z%#4-4u-d_YtB&B_)@1|Z5aPtP0k!db^?2<7lmHVRl>|H;abmk!ETTQ?L_z^LCv8Su znVnYkfh87yv`$XXi1suED!&Mv2{US9JH&%^r)LK>zx^kle;Hh$> zzMdpE>d@5?jvGwP!T;jH7JIhJBFpEBX2b@X5iU0xGqM#k!j&>YCCMQI#Rd^5TuxCS z$`%lXtB10Z(%UJFoNc6In`Fhr5F(9TRQJNv^JWW@;+^i0*eHBD#-Zh@+VS5XkXyXi}gOIz* z10cv%Y1(flBAaA44)hIl6+Ju_nS}KaO7=92`G4t@#z9@rO|qN!11gb7rkMsJk3b-f zSMBh|L*~kxUAc|{mFtgNm0lkj42D&B(=;~ zS+p?+UrhJrzaJIQu}hF;8jVm*${`aFSYrZ#FmsL?N&$f|lWHzT)j%N3Od^p+Bym{- zfimW-hh^0fuPeA{3oZkKaC7?w>$0RsF4TAq40{Z_yK*Mg1 zRfIeOf%S42*TmzTjOSPf=>K(=Fu(W7cntMOeuW`f5=Lt255sai^He2%36|j`OJC_T z`DK;k^nVy&e}gj}ri5vL zdW#OCf9SuT?gPW3_xM(Ho;XF{=_vXMo=!JWqLaYobet}0c?Dm}SAU+{;s#eG3$zCD z{!3i>{as%9A)G_FP6Ajlh@q+mCFl?WXM_z25g|y32tg0w|6OKiD|i%l1e6Le!X5i)BD9Bd+|&U*^^0%cj6ooK|?WS z+&IoN-IPr7$OLPWREXoC3P+`2O_B;U$s2L3!V%e2q)Fb0Rk#sU;YL!=DURb&eJuvY%{51)H0jUQVpj)9Lkg<|U1^6Gab+EPVRTj?WMe z(oS8^O-(yHK14Bgs^U1_iP2xo*y;6lW~t$ECzi=%@l5h3n~7)Q86Tn;@k~4u&#>_6 z@k~53BO(XqX2!-HlhfPm-LFzc#SU= zq4Akjs0fX#xD^@U>uv}q9F{A>#BMM^#8LezxU>~Q(3#i%@)Muo!YK!IN+CB1KTw^>5LvTd>}zxWS%Fk zPRzjej86LN@ne}yU*g#m=6T}k#0qSBBITXhUaAai%jl%A9xxv*ltm}R=4RXQq%YPB zLmQ3Z39-?%fx`w4osH@;o62~mxJvN>wm;~kF+E~nX44rlZ11$Nq7&kqP332bs}v`& zDG8K!YVvNGyj-=HdIGjj=%lh3WHPi_Gd*T}u5hc-sgpV9(PbVa1Hnq`% zZ51uVIw3E!X^kh!Yzt+^Wt&k~UONCRq}YU{z@{eR*N$z{6`K$iP;5e6VB19}?dgP| zXrjP@0*7rzMS)FA_+Hy*lLm7Hbs#x+HX$ak=?Lo07Q|J+>7ilQrS_1 zta@(JnsY^sUG=QxsOiw%c1S`2$JRw-VjzjIaiokY+Nnuvr--p(Y9=Q{e?>N_i_Ipw zv1O=>ZOg^RWn!ZWS&_g=VQ{uk6b82igX7}is6SRkL_k|;haVrGZG!5G-K~_RHDa_3d zB4JlSQEpTf?ewIzQ^?pOY%O)jN+&0k$=Oj+ncS94j>{xRZDEz7COtNHK-e;#$Ezcb zob5@fFE{o?O5Dd)YiLvw+{>ymT)Pfo_flmo^~XwxCS7U<{p_C`jmn~(7FqSM*dm-o z0w_WNtlgo<3WQ9`BC}&aS!7!*GA`WgU)CkUv!jyGs4g@cTxeV)G$F19xDDu9iWVl@ zn@AiAldh3Un0#*|l`z@fOv0p$b|+)ElT(pc3zM(xP7aUB_fEp@Wb9^I*23AHgx$&4 z&9to9n||FRKTZMqpOc`Tt2#jQp#M1x|6Bw@+5OL@{`m|vtN=scQK9S}3W>xad<$!u zX;}-6o@k!hJ=gRw`9u%j`}h0j8K8^SUk$tKq|}l))5xaS4ZG>(Fvc7zheViJ-B9`$niBe6o6*%TUw?Y4u#hD6)7v_&e z5Kx8rtLeEoQ!!(DCL(2GB~v-mIL=T_b&-l0J2@3ulwkgICHgo&6=&vAJR6NXBmbNO z^!hW-OhXrOJsXEFZcO8Fc^pc;fIt}YI3H&o6XslFSjRPS&3A@CD495B#xpfbdYF`n zV`bD#7m0BlYk-E`_1siMLN&o|_V+Uyhq2CF90DH2OdwFkgi^-JW05vmVgCP2Cyg^yd-LB< zCJu^$6enNwn8l@%y_NM$Y;XP}N;R?f?0!Ph$@X4)ag^;*kc#OO~W>uWOB-)C_GH)-K$@o2@i&GV$)I!W>h;3iOvy8ZJA(El6)G%$|EVzJc( z{7J{HPdjCiv)La^P63)o z9M)iN$STthl8?OzzpPA{Ymv6I%x)2!stZBOU(G{m9WO!YYE{M*}>oX9F-e z9B4aeQP3IBryWl!SgC*j6%YW!cKGFL7@U!q5v)m~ zF^R`w4V-D@@u?USkIPwEgrW+{FqK2)k$ZD2xL2F;#Cj{gT!yLm`5TvA8^joxL`jLu zZ13g3vm7NB#Z0HuMIdDr;lp!Qz>4r0aY6-uf<=i0{vWr~vrWwG5-eLKSh$$a724y( zh;g<$1Cpdw)Y{?M#U|Ai6^^N9O-7ylF!^Bui9HkJY*hwLau!=~c{Vwq1plgy=qqa& zg@V@TQNza+6lTQ!v3k{BGa^dOk{WdcN9En9RnE9($2Bfpjf#?^s?caK(}9H*&`b)1 zlj^)oOT28Ac;N!VZROyv5#dc4jr}1gw0EnyA1_%%C<_Xe1-7Z2BQ$E!RS^ynm`b9} zVWgb_PjUv(wi`w{KhZ#m=olI>0Nn*g%k{~u{Y6>XU4V$y~WQtci>Ob7zd|9q+Y z1#6Oi#h94bX*rDJ1{|OUG1E{P>jcF>ijPH^;Qzxc|DOgru7|}Maqt5(Sd%1U(sMD0 zW7bbYWqbc~94-Lb{SoK)CLWhg^HUtpCQLq}gaUj7ZvZ+k(x3CE5%)OiE(7$AnR@Uy#e^0|thWJb}4*oyNQn^IVQbi@~{^vgWpZDnY z_k%QboM8Ze&~v>M&NYVh?dHECp6s>a)P9L%WT$h<^@oveE)uRk97DSGhg~FF3QMfk zISsA9NGsAKaimB3mKN!KE9@f0`U?pOhV;ByhrQjl1naN<>MM=gTlrC5Zn6bpTQ<2? zyI3IX51lBDyL`2N(zwfa(YVVLm+Ao(Tf5s^nb)N{QhQ#PEcTl5AOqVyP?YCy$8QI= zby_i5BC>E9WnA+6_N4H~84xfc(G*6ZC$(Xf=T# zm*qxZYBwNT_%MMU^yS&)09}phHf^G?3#}8T!IKPzKwKa+S|~_B zrG--hoK{H|ObjqGv}D<=*e$H2STLyt39^#8NnvhVFgGsUje67KU_HSDK$~!*hfM4( z02(S;WqfjP7ieXDEVefjzng|h_WtKlx8FaPI@*wOno1&txgR`=rfE)oHT=%ebb$pY z4juCmd2Dav5Ox#iV(jK8GABQI319>o5wK~1jgBHLlPuhm#*Py?drgYD2>PF=pjpm? zHIUq>luEEAl%YUPf*_?3LQJ8dq0InO6ktF|G#CtqqCu%9ti1yifDdPMNN6}h8Hpot z90g)DjFJ#T%pfu{Fat0%0E9>a0TIZx?fmCiWC$Z)^P6kU62%TtzL)K*IH0><(vt%b z!F=AsUNYMmPC%7*kh{C81EeT|g?xp*WwCyCYc;*OPqu~_7xJl~-Bu;v&^G=}XkVfk z9Ky8%gq+)_`=DPH-up{3`b+Z<0c_HQ>DkW^0vovo7+0H+9UVWCQZi-T`_i(10o%$pVAx z@oy#UL?Z{N>DWAA>0-m+Me$g6D;VG=`s>u|$Pz6XGwMNIes0AZr@#=Xf|Xh%il;v4 zU~`kCLt2o8z%Pp8?-Cj|`ghoqfC~%HNr0gVl_^0=f|p)iI}h00`DfXJ73*QcdG=?c zfcbVYEjRB@XC15BiW3kuep*NL_^rG5DjVnv+wMcQ3k+5sKDo#AWLSyWPV^&kPa!n2XYs{~ZkNN{@*@yK9exZ4Tl z*>^#N|6F!xHv=iBc(*SupY%Xy9(ck4TtK70=Kg3_Xvpg-s-gF2r;~?C^gW~gCnJ3+ z#t%@GQFBSG(qGFR(7d0&*|V2F&67cS{K%ZSv)jM0meTqml=~rms209&4?XCg0Mq&L z(a|@Ej{kU|-CLY`{lLT-uW_5Qa&FW)jVe~TFph4wY!!`ne<>UKzBA ztsH@1(9^PH`WCw|YLOrVIzmEsaOAR}qtO3@1x=xon=5Fy&L2)y2{Z_3zUe@(`E|C~ z2HJnl!!+yDC~~UPv#$H@*lEwgiNjqV{0Sud@FxNt`n3Qv0NP$qGK$}dPDe0929(mu zAKMbZ$daH^!YX1(8m8N$>i#LNOmXRe$Xj22;d^nV?S6UCmIVy6bJ;=BN9muQZi&Ws zFfjiiUQL=8K+2{+B3#nOl%6*zMFGoiFmk4KSPIe^zg-*7r|p93iJmGV>WF`lVl)>; z*{Vl|eMNhb{~*mtqR+UcA!6g89HkeRL)^!}_8C*;H9F$RS$E-Dz&A=m#ig%D30Y$7 ztPSZkMh0%ZEVk&j&KgKE#ViB>SlWiY?)in2JLY5BrRLmDKV_6~*idxGM`0j&*nE2d zPc`)Hc-Vw_ThHv>3&$Z^^*HQE+Xq6mVXr-`$n0dcy^z?jO`7v1J=Oj98|uuk9d`zs zK_GE*yB`(|o42?muSi_j5Ih2yfW3wh9J7(F|4T6=giTAZHU)ibf7Z9%)YLu(|1ZzX z-$-_hM2tWpd%g8_1LZxJsJb7j&BT-hQ{{?d`$&cP*t!pi<4?utqLl!}j&@F#Hfj~t zQB)LB%QQhtWCL@*c+1OT$QP~!IY7zz4~_fz>vQ$HbuN&kuWIAXa9d}q+L+>@gSPJP z3(Jgc!kw6nzt)jWNVe9NtRt<~&KzBcD=y{PSmM04Tv-c@lw}^wZ)Dyzb!#O!p6IO& zYlxHctQGU>x?ZGW|E{B;%8U<>bWxVp_K0H?YEw+$Z6_4t5s`b;ms82LO<`fKHe-)3 z!*vC-c(j#OX{K4Ik;kfSk$YhHwsju$j8JW1oSnbch`fx^)CwW@%9&ahh((%CzOps-Z^>cCDb zT-tpm_l_RjBr%PnEItd##LyKNlIaR?QGF{=Dz|js73ahyDwKN~*&=7IQlCEp8!XBz zJ+y2WY_@)sAv1~rpq|DI`MJaW`&T9T?d%aaI!3X7H#f~{i8W}NL>qGyJ+|tNW4}iH zTddd$JV1({icln07!9mB_BxhbGJ@BXVX=xqm#@3lQq!V#-|==)Bd_utl8R|LS8M# z6`(5%JB1hKK5U2BCRPyDWJCsKk-Q)4cOFLez4LSv286$&Ec>C(+_ROU9kZ2 zAMdXUsl@POzo=nw*a*Re=)8)ez^dLLli9(!F>AQtBea!>S61iNZ~uF`CU=L8_@crA zrT+80yQds~AT3c(`OPeU72l7`cdzsn+KY6oOGMs%^g@n+jlKqusgXyGw2n@ zYLM#ScDle8)>U?hm~bmQZ0<14*1<$yMaAM+e|A&Vv(2^ZhPlJG#|b!Jj*C#i(z^^- zjqJ(3)61i%aDv|FPcJCihFerTp9XzPc~*Q~jE8|dQ-}(b$fmxUw+;-?IAr>8ysCSu_68O)wq+()v6W- z5=;IzoY`B<>wcQp1&8EEDWmtVgOxXd#|3#17UEr?kDufYN@h>=I|}qaRw;X2K=v%U z%-s&|+QpmoB#H1n$v6#2W$jTz{ydOZI3G(ZVIG1K0JU~=;TAfATjGy4x!l>fCtMkC zj;0{E!T!OKB4#)G1B}{8OoD>OE(JccxrRFQ(DF$Kirgm@UGq>X-dW;9QN1}Sw+oW1 zYza5}c&V4D(x zvUb2u1EqYPHMs;E3qxzBla0ldC5t(xu1kHO8k^8L&QPT+m)xcN)}L?iCX(jZJ-o2P zJyP(3wItN!rV*2sd`q)m)!Em*e~PAtx|isCUk@ywTgpAH zsrp-v@Hm%LHR9H~k2fLk;Z_uGG~__^Hbt811P^Z+l@9fa^+*0iL2rVx3uwY!R9@92 z{+HhCMivCqhJ-R+d=Q+&;E{u+n4Hj^zIs8RST90np&5d}Ggx+u_x!#}RbF+>Ygwph zT&c)#ad-$|b3rNc{5w267a-cOIU-KHZ<#-zR#B+8k6-~11q24Z-wScD9|V8+-7@l6 z`s)*-KnRAv^Jq49Ig1XnlcboI6bsO@m3V+u<*(r^O8a^o`9)WEG#({@^b2=kQoEPC}yLD6(d z#5xnv@(PW&lv!kQ#rzn&st&z!igc#c$hGhQOypuZR2-rC*<$`PLigjY( z4l_ksRrqc$lmJlEf#*&e&WlKMou%YxYYKa{TR$Q8>7KiHtg&f+L4$()N!~+4bxRe? zeY*}jlm|}`2yByS&Im^l4;*@EAjAYAnDe?1RKH`mqxXQYZbW$K0xq>V&kaRd^Gl6B z(Q`#Wq}PX2>$)|D|3@Y0gjG4q=I7oR;>Vp{0HSQ3AgC3-p@w)M*&iqywNj;K8$Pq% zKY4{HPB%20o-%?OsfKTw@ux9ogz|`~(zSvPnt@PlrO;88qz$j+Ch6|%R5<=mf8Zg+-zZl~yW=`Lt}z&rB3oumTeU5)hem8` zE^UGWijS%K`I2=!DDFK{q;6I3xIGRJkUzf)-ST$)fI;&}OAKdRK9)Kf0?O-{N~#i+ZgdFdq&S>*H8zc+hS0 z2w%R#JEJ;yK5e@(NJXyxseH(TfGbvNH4;w5P#;Mp>aBulRUelM63-jW2?5)9hEM3B zy*wR7$QIUOz`%!}Z#YSbI(B^+%~sx|3jK1^i8K0u=AJ>=wTW$>uMise1F`SP`~}gP z38dOX$agBY&f9G?>6|GN8A66J^ooaWYot^gSnLRA5%3vbCVgZ~ZuO9ts$7&Yz;710 zh$-Z)FS}#t_n5e~#bH^u-$@MSQQADvv(6@g)r*)8F`|_%XPp(v?>R zgrb`?Bpvm{i)#lL|#3rAl zsOIDDW>3R|4RpwOTv5WL8z4$>pfqb};te7e`F;S6c7YIKn}2&=ihyCDDrCA{yP92( z;xc?@HK^%RC6P`gGu$X~J-Bk4*AXmkJRdxt+OLd{*L!BlraIw2&psf^?~&E?&4Mdyg;e{K zm|&j&Pw1~+SJ^QM3~M%vL9?g|0$zLw{G-nM| zUsG2FqFFswb6WriCy}-&tU9^tvcqVtxznxYsM9}68foh}_At{b^34dHsWxc^I#eoV z$P7&Xb4S%v~naH30goUeGLOb zVeIKg;&vW1)Urzf(D2%lw^l3ag_OliB835&U101JE1wHL)n-4MyFzK6Zegh9jx2%9 z#lOxRi9}28Ms6Pmq0U;ZAb`YI6v!Sc7_a&An=NbY^MO`t2UaV5)kKLs_^^S@(gDJk zh^1GotfR8=)uXfelmO&nN3F|JYLK;Fw~Bd4pG~4)%?TH+k~&}6gPB>@lQw(ErqCHhCYnk|kG2(8zQBpgCQ!DyEl5>qV4l&-nP zYD%ZM=qRYfVq+JSA(iv7xj?n`aPT#JM!e-RgMa*y>28TFSlM#oWIPA5OogMI=~~P3 ztM!(a6@KKF%{`&?h9-kw8RdRT%Xx-veU<N$SY4AI#vGHoz9$PBk{ra#$z;4=X^%A}pjJ@!k+js%QQcFmt6_qQuyY!Z@#HCcX zrYbEjtbg`Us?H)nTW<8S$juBEL(8pldMc%@o+X}sVPVYoNQB;)@#6H%@R5rLZ&l_B z5Rzp&_HOawu}KLNnJ_VmcBG~_N?Q1!Jn{V>i1WY5h)1WC>=ndgi%Vi<b%9Z+G{{yy2TYz7&zj@=-9*E7T9wJbaY zbJNZEWMH4ZKe8mK;QL^@v ztXOT5u~BosHnA2RT)r+9K{t`eH>J6(05S>U!7_r0Lu8%fh;I6KBY$xEqiWIgJM>4F zw=pgLpM_~{(o(7$O*F~l3AzY)<1LV&~g4_t8IO4PN)mY)iQJ&Wa;Qj2&Uo({ubboLNsj@uakhr)#cHFfftMQ`$Tl zrhm&Lmh;<8q>=m}+iR(AFO5QO^BJ}LN-4Jerx<$5Fm?IzDhg z-oUT>n)aFrRx6P!_m5Uj@cRucXy~?@->-f)xMYguClIs~(J{9y1AtqrPfa4cTNW65 zl$Wt)u?txkCu+B#3bVm6SIXsQomioK{k}1A4NjD-1xxH=;Wq&>SxJc{z;VS(b|#b4 zPa#qU^r>CpS}%kdB*MhCI(;p;Y%K)+7FI{jmK*M~n6|R3zi(V_ufx8DgiI7xwQx5s zW<5-zcadT*8a9Ct+(XczQs-9l@ds9?Z4Q5+(Ud+=2x5tuwB^AN6~i9W``PU;Mur(p zBie0glZ7J9+vLe=A2?T^7um;&yfkGgGi)0c3!lnybmrg`%|9(Yh|D;Jcc;2a@KcbM z?(%JLxie@pVJ5o}>6g)EnRL#A)OFq(EMTdD{DLaB-OYpqHUOS*Tz+(zBpR^ks%a`fzsO;A5W%om2I1ACXA0i+>F7h> zK_T@9cDcNWo8b+g5ItI(L`VY0>f4hhgl<3>bsx%8)uXzxWVcI7)aQPD4%lc$2{{m%eNswO8O4SMUyt&(;Ij+6p zJm;+k`6AWA8j%j=5L}~3gjoYUt)_4x@{fw#WAn2%)BatfC3a?5tfUZM-Hc`3NUpeGE=i(A`6s4cuo@ z`vl*hG~KAxi-X4*Y~gSC=1$e;3Iylm=p`4l5XhPuRkd)66eVXZksQlO>&X^sbI4#QpFB8C^TRd8t~Z1Sx!PhT%BAw zz|V(qnqpZP0f%^{dq>t}Bx3#S9y08x0LeAqNK=6W9V1fg4?rCZe~;I1t2}#yhZM#6 z_A{}5_lMdHhieY9{>bsPBXrI9nJ;RX-xWi7CAi*&Xa~Z7z)LHaxsukzh1HF@I|x8Y zvF6TDPlVCha(QQ`PI0L^^`{<*Cis}!;_>5VL~6XmJ$Z+j~42Ql#siJM1>zLHUSMu{bNx4EO?<6Sks)72g*; zpyd8~Q55sK!+4gG;$$dAN=DlvIU*Hy>GwvJCy|}!`bX z4LE7_+nOGS>V^ULXhdz@N)mAy8&?_{>T#u+V!k2FnA8A^q!CIZLDDVZ_nD~%Xb8q; zo0>zco!Pr7aKKInG3(O*HxSSIO@?2+kHj^FMF()o^Mf#>EZH(1`|qJa)!?0X06TAP zF6J!=3+#Hh!^t}WfRRks$LA?&nQlYmKv=ut^1y&sassUq<(K=*xGq2Wpc zo+ASKqzG0k2sa>E-cUQ&)6ux=e&AJCKz=3Jm42g z2+3~X$-G+BMU}JmofoB{a}X*9mIyou`64x!jE~EOS?xjIT+TlFaX*#tT&MbwvlC4S zH<_QUE0?;+5#`1gFYEY|EZPY%KF$7FQEr6hT0u+a&sIFO3S{&UIB@GlX=>8#B(kIg(MANFTp|zz0i&&i63-wv88*3wWG5g z8$R3P)q2?0vz)@(b$dDy)9TLS%QX&k?RWe?3j$O-^jujgA~`{&My$yWN7P4=+&J1w zbt1b17OVl1vMY2vt4|D>_gTbZW!Gj}C3&+d0@@BQ?#OPZ6U|7oOsPHOxejtSL)5P@ zEIP5ATFiwRUFO~M+8lQfVl(#Yhk(mxh+$V*t*y#bPc^Khb{K_DcLDViL{izUm7eqT z41&(E{XP%{dP)%|k7fEY>B=JOfg=)v#+uhBZe#Z^Q6<@_(5#%2crV4vpj1TX)>7L< z-EnPoF?lM~V4Z{O;c-E;vsg>(zmFq zO-7yWbW}px>7vmIqXuB;oNF~%4Nq$Mg(NE-Tq*C$Jth1-gw!an!`K-4(s+VpB2enYgL8eA`CL3^@eFIh3<=uNjrmLqOni%wO)DyBUYTP58+M`W6GH zZvWtGK~)k*KQ{weZ$iOG1iMn*)S?fazl;nE8|>$1i5XZ7rJ2xG{k7Ljy|?-jK*O2@ z9O!|voEN|Ky!Zoyu=`dZMu4vZ6~*5`eVZR*6=^Hy{3W1|vKYVZJBH~HY08@4xHU67 zejCHBI3wQ0;0{tFTY9!>v=O2Dq6*ZflVQv~js^^R!|K>5ACpjBWa!Q>&lr4%R$7-o z2=85Ym9(@2^d1uE?oG|lgIXv7PgNBl=}S{_rzq9la0du(1fzaH2QJ^d0`v4c$1AC( zGJf&L#r~`U{e~d7egzk>EypJH@-?*^W}`KEt66zznOg_7ji@8( zZCdH6+AAtM1O0HdD94EIgiCHchoSJ`()NL}I0RgJ!_yzId}`N(*}&g4d=Z#Q;LAQ? z^Lu0K4L#7KNh+q2rMrIFLe1KfJ;X;OEthj_#n=P zN2G|*SDn?zB^;hDJlR;gA%DFp%;f=v6;*Rsjy<=A^T*O?9=xCdyg;nwZ%Ei-mS z+uD`HdjOkQY|%hms4RR-FDL@!f}3^!)sO4*jWe&?*!U@!nIf7D-GAQVm?wQkYjZX8 zv~C&~JW;}i$U)#u`mZKhB+XdVep8mFnZyUQ_b;4#>vcj_`Q|S?m(s6cyyR7gX$DoT zFxYf=iThAi-fR+hXebKQz|TEvSM@yrsTHU3Tf1-91=4#xh3Va}%8SICTa2ljko^4C z%Tc3yI#yyC`aLfy!a?rnU&KL^JU!KV3LS?0yL{atKkiMc@>`Qt@C)Dk4hvC-s&f%K z*6#+Qqv;_FjFXU~NtSnQ{BWI#pqhqtJGqFqr(VokNGjy3DfJ+NUiVUG<#_wYJmp{Z zZVnefz?V2YJ;s=`!mp=xh5V!e2SG?qC!mNDj+)q|5(YZ3qbNG&RyOD;mfrJPOVM-4bRSVS~Z#7eR2cb(6iUuBLMi(%o7`9s{0vN(CN ztDX28F%|gm>9i-4|IywJSrdCUkf1$2=tLUzL1;P!u0437w9aY7Pf+WF#L&o&&T*h+ z@&y1u48e_DNM;(MTz*4`l_7yq4%1_HUV|Avewq5L zcu$H@vUy^6)6^$^%;|-pO_%z8C_A^S&l!X-qA1nFKa z3Gc|VwwRQa%LM^jUvJJ z1m+sG`&Gx~Zc6Ro0=K-jDfn$MV@Pd1SKM1-8GCw2_$*Y_kq-u-x|Tab4K6Cg4(?=# zu4szJG(Jq+22{|k-T`^%<-i2Sv)JAXcFXS8r%zW02DP-!IP`neJ)WQAFt8qy-~#VM zvwF8V*;k5TNI|SmD84bk?EsDW078c(L@m6X4bYTrAgbsAM_LLrBrYnh`teik5n@&g z`)`BYTq-;Wak3Z#L#4`XIv!6P%b@8PHth&lu)LSHRhN{NIXrmG&pGs(DOolH4fewl z|A9#p3f2iro>d$gdcYK3i-4{(%XpZrEQ#TqE<8znxtq%W^IH;0TQfTWF0 zp{IDL8BQ+Rj&EiKGlnJ9G>DysTsCtjD6S}G7W(WerPf(FrjN+t9dUfy5(ZJMO+N}w zs;W?>P^G^z#vokv>V00ylaQr4Nramrm=&Oft#{|CKoH}bH%%@gqG`&u+I4ZqTXf*_Btdd5nYgdS`p8Yq1QNyQa-Ppjg-Hpge$y80vGPEh`l={#t>7JnoFiCdrg8d%@Z%UNgD!Ic;qX z=dBFW@{DUAN`W?ohephtE@gO^nS_BV%oDlQ%pZQ|{^`c8=A}RBM2%Iq2W>O_^a#y6;(-l9Dtl5oJ0> zC?y4kYvbDTQIRpOiDsiryR}Tu@=?+3+VPTW%fn%u;@Y@H<1GWZG<;HA8#hj}jj?O@ z_^=3aX*uW`ardZb9^$u-9<~v8@1TwzJucU;$L-zQ7-4VkoiOb7!`ZOwTDZMuFZO14 zST~KfDZl^#vkq#&VkQO#CWHtOC~m{)J>G}#kTM%5ipSBoS;9fL(hD)mNlIEySj=)> zET@T>ud}aLDE$RJ-`OG_g-1^!HR#<_fOw+!?rj!Ze*ND2IgE#fO1@kB8>x#Ofw&r0d{uPhACHBpPRehY7L)*lXyMm}SbETe_5 zycoXvVmMJQ5|Qlnv%Y_C89t;sKCt{bpBVMxEzqBNyB)i(bNJX^{;R5T|})U2ho4N_Z*a6_)QenanTF6 zS-+NX8pD{5Vfbwu>-4YhJS;-Lf9v4zPJeMevg`P^*}wkVHU`r#2j>|?eEmka5&Bs_ zXmtJ_9BLB}9r9R&u z|9W8WxfMd-xl7S!T?JYLIrC|6+Q*qs`_;^+NEi$c&P1r20_pVR{Ct37=!Yk$hvx$n zvMT*pE~c-?sbmfsc-h7a!n?91e_q<6X~6vESm- zaZ-?2bX(k`I5?b*L~e^)%Sc7UU=PP}eb*P#b4|QwnwaFaxGnAoE~Munkr?Cx&2Sp` zTpNpIoBi(!ZH$L*i;Gl548GJck-M}cyDjc;NUV!&8$8FI>zQuxxt{6t*F+bh9GsP2 zI^aI8Ki@-RlIz=YQY>_3dR}^=i8rxE!97^Nke-ub;rfZf8qtFoQMlqsMY`y=xGk<6 zB+@$~`Gt%~USi?1+u|A-8Ed!2RWiCQF0Y0w$w3!BB+77G+!jDRZi`FL@lnxj8*38$ zrsX8}wvF%%SCWIi+cwrLuD-)s4ib}aIxbHSP7Jqw^ROs}b4aURjyX0s@N$!sX%fqO zOqjuP6(+=9xd$e^SRwrYD~%$oWxYS0htTi$b6ti-<1NF3^SS<&rQYlcr$TCq!l{ry z;Z(@=GsM#}POOdA@86*Kj)Sf5Ulutmf>vC4wq07rX&pU;wd`^boK3v^UP^G$a}6yA zYkA3?gR)H19O!}M+Vt$ZhqDod%k`7(M?PPC$HAY=fomI!h(;x7IT)Oe+!8ZZD=wL-7kBnH;Lgi;}w5Zi)#n+mb4RZUYNp%CZM zBpta~Wk^SE*bJn%n{|cscEdLH-fy)%kK?dl9}$VrzuFREKgDXDu|*_dFNqKUGh zc`b8n48ZB;c+>RbK?4c}IRGb`7jpn94B*K`mj~5EqaoYqvh6u3{kKie>U+NE>)-k8 za0*0UoW<0mMcIn;=LC9Sij3Z2=LdZj!+Irx)DNnuguL=P9_=>Rz4sTBI)y~GZ!I1A zvcetK;gM{le%YSorQs~!tuLH|w2mIPJsc91PC)kR-g`Ya=R4mKJ{IFisrLX%CFkJxA^;aEab2~9SckZyyd zrs8>=FY19qTHZ#EsA|6&QPq->U-JrS)(X^q*3H7}tqQ4dDukl9o*}+>bci?F77qiY5DUW2#6Tv*jLeJ-h?-S^^R>Q+ z=-NEi$~&~V&6U5{oc8ML)rTJB*I$s*gS@vImTbpFKl}D=l=c3ouL0~^wu?EO$p04W z_wSNC1MS!MFDjJY{?1`-BA|A7S)fb`5h4ag1k4B!G9XwRX1rf1BJ;Gh2ulxQqQhX< zgT%TEdcJcglKR+r_2Plb@3;HCcQp3;*r2`gUrOc6{kTD?m+2f3j zIm2V#JZ5-PG)^>6I$PedowY&7#tK8#rpqqlWC%zh66eZ!?DF>NYcqw7AMD4^WT`^Y zNV1edC+uKW9@uCRW46FCXArc&O$nKVO*C0wn^QL`^=%KSKR6G`MzPKMwwip;^^)t; zGf@EkyQG|W3J$}yaa&x-e-`&R+sy(95P(slN}5te5(Vn!F~i&0y>}D0|L4#7dKenh zJM4?SzkV_;@==lNmK0XNTum`dph-7{}J}iRhS$1!r6kPW9 z-U{p|F*vr8tbnypV2cH0Sr*JHp1%{g!f1?|!fWx20jj{RJ7L&kV2FCvqUQvNyyk0Y zbLAxON#Q`%e62T8WzE<6Rn6CEV9*)pCJeP+>#Xc3u-E)@4_(*4k;{x~(rFE;MF~j(I|@${9{Go^E80X61ro zwv_yEDQA)-IIET$^Mnc^x#P6+w7ccL+R|66>eZMdk}FOtPq|vI#H7?TljR~*a51@> zUTl7AX+U{V1toKBUz<6{7aP-L0wp<0cF>eIDcC%b*sO3Z^lbVg2pEu;8}mr%;Wp*r zR@H;F!K`p>knHjv8&oQ_O|#=W4yM28|7|3q?N49zC&{;+{+u2k7D2M_4-Ef%EUv!2 zm%^uq)srSXgL(=qzau38WwV#O@;mCOvVq<^j|s1YWM^kWEwq!+zxoq)c_RDaB3Xl} zMw3mJ>ufZTK%pX`!u0X*wa@{oNC!=+u5gp|Ng4#SuiPJdn0lIE?aoaBDmAsw60vrv zK(wUCkWb)QOgrEsS-Ps)uUu8VVZer;V1aPJPI16O(a2+=Xq;7`zUiR!H#e%!v2c16 zERYlI)CsoA$1-VJHXq^ed+$5y8v6d6Jp7NxQC@i*)#sp2V6DzJT`jZ|xwX35#PPtj zK(l~K3Mx-1D)e|Y06AFt+NoCQ#+En}-eez1z|_Dx`rPnPI}kuo>LlOV@~u_vZ0Zmh z4DHQW3{{DY;PRl{5(r+xCzdX5rd-@qk$`}t1hyG|Q8Dfk289VD2aFv+N}CRMm)~Gk zJ4`!Gvo=4OS*6DlvL7ehFZo3exxNMItNDL3nD)5nLE535>BgGx&U zqseOUt=>CRZOcJok@{J0jt<;}LT#(zG*f>odudr1508p$SvWkBjdJ+i;^$k((8I{* zV{stgiNEk4T0SZw)(g*BDrg=SSGd!*6OY%tkuyb3+z;zE3@SX8nfm` z4Gx*)h||auj+WD6d~d>>EJ3);6**Syz)|*GEBE!ixsqFc&`o{NRpCzfla0*9gw$FZ zT(ndxjw&ZmVF8K?Q0$gBE=w1ZWlPCaOG#CU8zpeaBu6-o&@@6*O_tYM8i*$(F(ozC zVtF3{$&Y=b-aASoNF+*!j%;J`^^3(A{WXF-2{rp+uZ)h$hKhk=(M_@Fs%~Vh$;D`+ zHWr~DiFkhW9U6*#8FlY@pbk8Nx{Y%stz{t{2iNc46dm}_+Q4-=ZQ#1D60eG{xU$RD z0WH_Ek}K4E|6qjSvbg7gGyb$MJM6j_e9Cm-33S0N*aOiRcE?r3D(rIxq76TEoDTtK*>-lC2x`>G$F7Aq(`3Jlno3pBS3DHBrC*GHZbt) zm^Lw_sIh7tU2aU4n-7*-mF{M>W2s^Yh&8>CBPU!}rT2c;S?T-Jdk;YEc@z<~-?rsi zNUV!24H*eNU)Bb&xA$7OpLAe9Vc}W~)(XdBtSX#~k*a45537D>qOw#L-FxRT;gyhh zz&q6&REDLRVSUWRsBS0~N!W+=;S-}W&fp=6CaH^A$*?|TVpKsWo+Nx@W1a8MksS|> zd_*jKILEgf^bxVREf2*tk&MH>T%>WMZ;B;ijeZsp3-AB+y5JTIcaOpyg|(X`XVkP` z&8X??VJM`>pG%&?`Vf&!VW)PikKnDxh6`#7>v##tLyf++%%VRh-GQk`;eq|d!yTpf zGyolVwt9c{-c_HYd{NJS^`f4^oe8gmgm)H==Pw#tAIbkE;*p(x|E8dqh{5#JOT^&N z>kGhtT0WAEa5(GFUar?)``-qG#2;E#l3Dl!BVc4ecoapa!4GNUQ4}o^ujA2J5gpQ% z<|Rm2nsY$T)ZR8b5h6>z(cS1rlA_XYmfC-vvwM^5- z+V*m_SeA$PXNZl8ve@8tRGaT95~Frti})S&jM?C->RQC#sD3Ttaa8Y8#6-QN6mhAt zTM-UawpPSMWwk|S)RZmqqMp6^qMk7uc5JLOwbhhisXl<}poB4krNTv}Ai*+6xXTb$ zSmul*2qvcqxJe%cYM2jT z79pU}!m?7E$ndWT7Dx^6M+ugx5-e3YNuLi?J~hl5Fw~IYmXe8K#D}R7AF7PX-pj}A zmFK+wXlRL84-Lh492}yFCMq75J}uNKtIM4P=~YmR<_ejmjf(aw8x`dd3CcT5MEJME zZ(3H8)t|!x!I5EpAVuQ>%?mS8G3-s`$2aFCo**djPxLNLzA*c|-lP=fUrp_@2Lwk`}4po~qvt=+! zgvzpn3DEGMX#obx)^U`qLGn29)Mh!Y&1sG_O*BpIONdFWqgN+dARUY>Wu#Cknv{&F z8BnVW>kD23^9nLI#}XAvo@}+wHr1o6h4vz@8mcxsXqO$NN~|ntUM374Fj{Z{WwXF9 zkF`0B%~!NkDn^z(FUuVeOdkxsmJ6*H(kK9|K!S-x6Rhp)i8kSOS!9AA6H6bPDIc3E z5MTg71{YE=QW_Wm)l#zjx$%Qz=f;jF0Lju`!L0_81tqAxKQ8}!trJ#UA=X9l6(Jy7 z7~euJeD?O94DKf}xCevTcE;nqN-*I`T)P&G&T=m2&$G@IK4W?dr!$`REc#z726c&W z!o7ui3!^j`(sEthqMEJp%Il~$4mH@RA89-4{)nMqIbb_drOnZ%$);)6_72UHTDQr} zmNwLj$@0fx>c^q#=*AI_7-zP$rRJ!mC>K#mP-#@GLsf=I;v_-LmN-x@yk(3VF>ruz z0|XMt6iz9gZd9<H*N0_Le2IL@pDlo`7_H4`%oJM$(fy~*gJUF`Km@79{aG)eda#lLW zmD`V#o0ZP7hv}u{$5M`^9Wsi!Y4q1|5&d^;qkr}e#r{ptH<{P3V|iGlzEH18MjQPk@6U_>8+bsE#f4sA z(DFSz($Ql{76xfqo`Y@x<^Txw-ccA&aA7#oWsNZ07ee3)_1<|#7_JU}Q@Il#Ktb`F z8o`M{2HppHzUR8(d$l1q2t5EL1(c_p-&Bc|kcY4ZpA@>GLo2a+?;haUH4o3@rvwL& zr078^a&Hd@_md9vZ~}zu^#b1D-nK3q+*_IJM{`kQYBY2O%3?rQj2RpQ%HUX0gcQ7J zwJNV73+O5`v3NEqyv|9w39nEjGq#RaXl47KPIj^{1};q6z@p zos3aC^s5-P0}Q;=0~Sb%m}PH<52T8uZlqpB$x$~^FzgfzTgAdMMd)tmaL`t8XG?ch zg`4FyH4;=*Okii!hgqoeWy}~w9aDy8L}W(Nqyyst(8J!x;A3yc>v7!#IJL3+Uje6{thrO)P4|3xTH$nb0P=~ zV)%S>mS3F3n5FJ; zqeV{-vs!7a#WXY`?-Y?@=_c<8Y`UJ74T~>pBR+B76%J?s9yq`Y-cSH1eG=$48EvhD zouUR+kwhb=JE|=j{Z2j0A2Rdh*!TIEZtNVpNu52ho0=VXL2qkCeHGj{Q`k*;JzaCZ zs^kN>0SpLVu(ex`i1k>WL>HFD6(MY%#)^!ZP7M(`-C%wx$xbmcJhTRjMpbOXHipSO z2?**6bE`Y(82g@gc6JK{(3%fZStN!r3-TagdIw^8x-x;=OybDS295gwTQ%hbMh^TF z)Q$N{CK?pc5fyC8v~sTJnDL8>B}&cr=455DO<b2<63o))XB^jcoxtB5Ad06{>$zaLW9vi)(6^&rxL{&#Wq=wcHcX50LV|K{0` zY$2f~h!@RFM*(#P$4H5_1^9{u4lX8Hg{HQm>89;0V+fj_%<|jS-3$AOnhvm!#6s+p znlVJ$shPcLgDMXyNpuJ}TNEO@ia%aZ6gef5S=C@aX-e?g5Y{Y;na(Q`blN@545j!) zcK?U^MhoGP6aocHxUA}w_dU^U-b_DUy@9pc+S*}{xMayCpd@>QRHp8S0YUiAc^(F* z4fE*9*F6u-;?4^ukrn_*+!J!`#xT60u$%sW-dZm`;OCnHs1X{$O_O_3)!yI0r;E;3 zK`^K~n;tdDeL>4?w$K0xx4q<@mU@+L(&bXPz6AjWwCNef(x{(lZ?-BfszaLb2D&*K zfI=Bc0t1C~oujCoov;27dKthe*+z9y0Tv)F12GGwL?UUHX|Cf_x~d|Ow&8OY2e!yL z)hoq%=p|5z^db72L@gdI)Mfn38A+s@YlZY5W27hF&Fy8RSMQQcsu=iAto6Bq1@jvA zbD^m)@z@iJ!ot|~O+yLyJo=3Y$w3`y*7=f@b2uGXeX#ncunq^{8>~VomJ$q+UsKtyf={bg;>K?~&Joc@c0+U(+$duGHK^%M2Zq?ig?cE! zOQ}JZ>$>#FUgES+6*X44I5vS{`nGHgteR7V=Cs2s5}Xvv(YIu;kVGXb?99$l%Z-9~ zGKCGzkVcD1dpy7-hvZu&kL1W=EzjjZt41>sl-w+{5{H7Z2XE|e4^NFcdDauE-7>$K zfLixGcgLe^5%JDeMCcoL(h#&aJ(4EsaWZCgKVmJWj9jd@)dXQg3id$Eg_Hm`B5o*b z(Pr3L?rcNh(G@K@lVe?|y)zALSso>_mQDix4T&d>syn_9BpZ3VU=kHLoJ}vW3VMr# z5+pm_LG(~p@8y$D@r(OLl5xwI30hO)TE3w8VM@{~SC)K;7s-cGkcQ^g<3<(%_$SR_ z)Iw~|G?!5cLj`8>X%QLNU~{FRKYIKc>&ca#V+)$N6adOt!)02|2-&lBjCX6}0qI8; zu{UexV{;egq|_llBgfR%kZ77kl6Q$Manjl!)A?Eaz;t`b*n)ll@UT< zV)0^p{s>8d!C8F}*ueekab)fjw15++}B(*;;_Pe_RZ4SUI`}CdzC1Yctx!WJTwGJ|Sp)wC1Xc;sn z(omj~=II48=7c^9h_h4d^y4tRS``1xgSyR3fObCiqHxbh0#NFJ@spSuJ&I~fKLp}} z$#}sugjLU_6SK0u!A{31CTSfc5*QJzTCRq!oVi582>)PMTYTHb)U+B2*E-;OprN>z zm#6P|oY+;*5hTcAv9{{j515GaONsRKsLkyQ96t9fNx!VpKwU zcyH*g0D6VP_VY43qjG2oDUdV4zevi2+UTuLZ$yq%k>sQz#F^*a<{mop9(gysa(uKaxtRjAYozCiM0(V)^!2>@vV0(M-xb8d;m5}N}-#zeY- z=?$CRf1`{6%o{05rayH{RF)tRE~T=H<>8fZuIsQD%^cxYvt+>XwO`kGg;8mQhW7?( zX})#Sm(#CSIe=!u?G?9l3h4^vjfT$9yEJ_9^`DSFUVRVi%V4irpVDem1||tP&v_f; zRM*n^=kaB)KHnZJJ(10Tqh|$duD4Mkx2_7=X3WxpMV~20H!xd+4wns+l4P?~!S_PI z7!hR{nW}Wfnpt9X%5w3y1t$5egok|8GSSXhDW&G|4w}w2v;3IZFQgw3fYnbL!<44v z!@rKlsI8wn4V7U8AVBjnfH(T-2_Srnu-bbb>m~EK5bG_?h+>mD*$TGMWUV^C0YUw$r3Vg$j=IT`I8?CGXSukBeUMnxCV}s z!>Fk;Ei&Q7-$lhbfQtMAmqFcDw95NX3W&-BaBS`N8ef5N4F$y0O+~aD!=7HJd7TE} z3rj;R70E1b0z~;csaGT^t!ZK-A-P5{sVom4yNI(P)={fli4;K@Q--yu$BoVDIv$cc zPIHWd9TnI7mYL+jAz~uHB2d$*(okDTiv1cLYx-U94^P6Da(I#H(BWBClqf}qVCuNP zl%)@>g@7YRtO4;MkX?A{QaRZ39dwtwaGm3&86l_1h#G+((@WLJnV_=v)XSl<-W4lj zc95y0I5>WHuoe}dm1=aq6RD?bN3wsli(IU;g3eCvyIfN;7y}r=qJ(f^lz>=6YHnQ6VvRUIczkruN^_Jg#7O`NevN)(HLN&#lp@8r)UmIrhd zF-nmJGGbUn+lpCr`MM^ayoXgu7T1(phWl^EYMu22z4_an(=1k>8obsApP|oPpG5rW zo#_#oBqc2PTO0bnBS0cb$U9-*j5<6M1_)UE**NzmYkT=~FVI}!tIr>Pn~*|;6w^2z zD5e<0Wa;&^sNc`Emc|QJ?_rrd5Bhgpo7##I2PMgSn!0`o${5`&k|Sk>`E~#`eLZ&I zu$oT;L&@#7|5{Qxc&MGPe%Z#$P*C#3c_Q@|pOnk-u*MKIOx{Cder}{mQr-NNLYc66 zxI@OB8~VlMp*CD$#1BrA_4f=Hm6Bq@3~ixxZ^*~B+G0Dbp&_h_%~=y z%?zIUDA&!U`;WPd%N|-=@k$fyCbo*kS1r_F%kK-s&1?tNf8|_tno|ijydRqH)#wkf znRDD*inzyHZE)_@#~;+u-PJhm)5`c*vFaVq2(NS#5FE_+ZOffwz=`0_njZLNNYTUZ zv)4XWL;^|w;lz9NFE`Gt7AwF#CDX5fe_vQV&j=+$qonC!mM0 zKL<`8A+BFl2TtT;fPpQd;{GNEoFrZmJ9c7|MkV(E6mnikcy#pQWdkau?*we|05=bq zr`#H%3!2gQtptuXpk_nla*vQC02c|H8>VVvU zo-~oF$O>ZMB26FBHc*0;GXr%Ttx^L`6~d$^3IGX1f20Aqm}|yWcbRl|@@6NiVU2*; zfNKPzRa#wMPZkRghUL%FkI6ll57J#2lIDFjK0U@pL7KMswgDhjaB524U0cwns*C4y zsrCOTlSp19zy?+bxgt(8P7}dxd6}4JJ2Pwf-OqLdDEA?3IzHB(q(PB$Ws8&Z%5y5*c%CJ?GI zc_4#{?yWg#Nqz+x5!{8SY;*qQpMyy!kGD>|S|(h) z0HE$d8a$xgQL@0)mvhF)WjDB#_DTZ4Nquo9aq4G!R?sXC0)xMHjpVc@<-o-kl?Vpr zznke~J37Xp@YsN+{Y4KYJ2_h+y!H{235DA{gJqJHAEAu`^h~hq%F%!e>~#fQq#F?V zi!qCXKo7q|MmvGvzkfr(Qn0QozIwpjrD-y9?=+Y$JQJLx6O*jNh=O9IT=OA4xU=x5 z=kg5HeZ|jCs)ArB!jNoUcIekgDqV;ypA5kc!XLvlU>R+le!Je-bKJUK64{@O+l2_P z*5w)+zNOse{pd?u`qTanSvyI05N#RAM%u4cDJ}I@bCusr%bp&YhbeJeqcS{~fnT?w zr^dW9FPjl;!NjV;tCQXeo`=3gMTT(QsxC#IlVQh;`RSMZ$SJkl;-qJ~^+`d~fr%H& z^WMShc9?^QSjR>}5s(|nPZ5O@w5RU>pd#3!hPP-EL{v?>0R zYHr?K5P~cmVj{o1im??=i!u0&<9G%`ZJRDEk9w0%xF10mYECBhe~^}NAmR#Ek?il$ zTmPeP91sCzKt^i!ODnd}F<>1aJdOk0i~N~V2v$5o{a(GyP{|_ zoGJ&{0&GZN8P!pPtLfU`d1+(vUv~T>pTUN3Q~+TZk&ddX+vv9clM)co&KLW>A5Fhp zX0e>&#GqUG$*QfxKnX_%FQ_QL_gj)m`MA!KCb)RzsAt$W;^~>0XPLJpWs$t9!T2>E z=wUkVG_^36{YE+Pb^tsy3b#USAkRhe*Okq_>!w!iQ_H=9-MFtQl_hMkCn@?sVNhhr zi}?#+{uMkfCl-ho^NN@J(o9Eph2qhou zc2^A6u!L*IUoqTvdC7Os=NtT4RtIW5L#K;JZNDuQFFUK$2$br5P~K-Er5Nh{(Su+v z{wYb|>~DOxok0iuVHhtYRnJHiw*krcV-v)8BV8O~y<|H@w0UoVmsM73Fo`_qT1PSP?LRg) zV*l(&DgjckP!VSVZ2@!v7Z|8e;VRyGgAg)$oq2CG8r`x=wW-DT0EXf%Rv1MUztDkJ za=LqhL^AP+G}^-;gp7W&Qm8#jzl*$hw@5011xZBY5%K7Xq3>{3G4yhd*4309lQmWD z$?gMjS2(w0zV+WHbxcQB*-_=5hq%)g-4+kMobyTVtV*Q`T=(BA7(Vdc8jEjR4Q~w- zOi7Hbx4sX=%>-f?*Xy_LURXX3Vi;c}@paQ%4pUN{w5_w{rsXacv+hNapvP4(oYvG# z{%&b}uV6S?8Z9oRRWN*Df@Fe35+NaJP#!7e?>$mS`J@az1BhP%EKGEf(3Qmezjje(Nc^aHz$VTByYo)NkEy-ES>D7?6z6Bs~vBS}eRw zQ*BKr)o)E<$SC`*ZEFfSz21Ewwh_Yp)|ZMI`mN*aw}u&Hzcq%G*RS81Mx(*j-_Ou> zK%>#hRy~ zM7aw5_=^?3Hw@#k@L@a_Hr0E>TlZU2JoMxaXA4tky6zq0v9K@ja)Fm-i+b-1sa;5m zxsa@US{qPB`neo$j{GyMVzXlC`S(kdD6e&8DaTUIq?A*!RV9>ruHUyt-pM+1|1R=k zVJlr%Mb0!xd*r`c3F8r!VZBxu-`GQ?>ky>OQzXt zj9~2-c@8(;+9KJ0Z~dldXlSU5*3}|SAgOf~RV2F#=3W}ThHs_sO?o#?{*G^@XQHec zt-HG$RiiNt7!H0WjyaZLJQh0g1HgFZd(uF%p)M2;{nkvn+v({rG3ppOY|^^NW0#U~0M8Hq6B2 z&O2$~%0q3xw|?sg=M<@u1Fr&!5_xaEH*zJpKvBU0Yx3O%1u9TD$IVYV-E-t5CmBf< zSHbXsB_pX4$yEXuBEXeeM@|i!+zAB1%lrG=TcizK>8jAV50}trG!R&qN@Aj=sCq~a z0da|mSjF5^ate?A_@r@l#e?{SSg0_qRIcX3F*En-zh|m7b|<;xPdU|Nl1{j1LxBU%S_uQn(j|x-oM4;Va{ER$fb?Zujf= zNOp4XmBPre<=FdF!EjnJLJNllj%isu>7$Cs_qmvQa-WOo-AaEKlVxk77GDQlxV9u9 z4^vW8P^Ddrf^Vgibw-&ekw)HlH$U7-MoU3U!M6v8x3}dUE3_1JICIe=t3}qlUPbC6 zB;^Q6UlB?cSu>_lgc4-Z2uYDzO!~Iwq|w6NNyZ6Q=5D?R7+4Cr%h=89xe3+_m&AZK zHnNV%v_~3#J$E|etG!hcb#69l0)bu<|fNDM*1Mzr38lt>ZUQHDNv4EtZ zAtLgCKty?|ikh67S7ZXAe8>bsc}5)5JSGsj)r2?(OA=+$O2rR;~0Es zzH*(p-*1?ld z2rv;afT6qx{_-+Io06LGuA@LJpE~P2>2-?Gfq&W?=5R`ap1Ff|nAlQ-m4!g6v>@R{ z6Zrl9QziX4tlbgyhhQG|p>0KlY_P$CWh|7t%mRl!x?hfh7^5g6!YFDv9E2l)2u82- zw{|^%`*Qr6KggUe0A#;Y*|_MFIrh;Zn5D%fnz# z5hFKb0PcP*=~2$m=$_0VgfP{X%cUBJp8q-Pb6PMGNq+y^fX#lWMS56-*FiDKa8JS! z0wTUmb4f?A87)JETf~1mwYIk0*tuvNJR{;OE5zR{z^BaaSPyoGWZp1wMTgWri!J2; zIb-R>IYZLqm!*gIa}({m2($9#+S~3{$v(8X_~jrCTB)`XzMPE%#xhC_G&X@kuTy;HLH`amo!LLF7EN&6<}RzJ}QMN#nfIv`cmfNTW>5a4Y@8IOgqkT)UZj46`e{9PpQuaVmQ zbnpnduiP7~uwjH%K_`QQV(#YYUeK!ER2o0ztW~O2%bJnc*;~;s?sK`2=y$2fr`^DzQ{=D;%Xnjva*20efRRDB6G$~6B7FnS;iW5Ys6cjN za(Xg<>U5LzlyvI{#qqS()59SJQWK^r<6&}0WHssnui8(ou{Okc*!N1TeW+D#7k)%0Y z8u4)La4z00zt1>nXM&8p1*QmED1lHA?>z2)8Wb5`uNHLNB|f^!F&EoUCudDXK-ut; zR{B3g5s;G&230@c>Cm69lDi&++_hxi_ev4asDFq=B6vIcrGp4C>1YBKSK8t2RK=&N;65FGs zH++{l_XrRn>0@;3Or&x%52(+5AQF0UYrKaAi2btY-Q|S`7P}XJ`$!#+ix7Tb9#0m+ ze}NR?&0ipS(vzq+Q}(%$;@PJ6Z8XN)gLr}NUn|^^)G>waYZEt^xMTUI>XQl(zQh_M zbrYjyOfj#V;dLCEg-|WdB&y2oAd01%x;x;p7f0`7P#Ne9s4OwUrp&WEZb{L0uu}|Y z>xaLjt4x;almK(?k+^Awkd1u4SU1R$IH2Wqv=Y`ALm{uJ0dg6(Xw3AZsE;>VRUFXV zIF+(_qzSJytucQu0o)lktPJ~LN;tzEhDT_>R@TGd4xcFu7ZY;pm}1(MsEK+F9Z(`D znbKo@DxWC4douAb$6ZEUL82N(CGeIYbY3}_<%JU|BBt3B^TB8){oSh!SQ+{mA5sUDI zzcs?exVz-dg64Tc?yW9E{rV0)bkC@xgY@%=RR{T)Zr0U$g%_4RibDre5votqB!m(6 zCR7l&k7b=WJs{m(i{jl6KWjQ`DFZR+B9-*NI-^MO7*+zrQFzCmoZFTe^zdTnS>FkX zK?!uq45YT9D9fD%w`~O%I-yEZTuY9G2R;lXjvGFFFRgK%9*6wrGG}v;#fS%R%MM9U zw-CKw$NI`&LC<3YM%{bR-8E#A7B%=f2m%W9{t(Z(yrLiQd=EmG6w8g2I~CSKiiZjU z-T)hvqJWHkZqtb+6>^X;;f?wh!Bz!B(Rm5Ch|NN=Pc`SU>V-!qzM^b7`?eXzK_rI+ zk7hCB3|KHb&P=Y~Z&RCtvLo{ZFCl!zpd4fPyQ6RB*|PFwL=UHmz~M1XYh@RiX@to3;I_An}tQcQRz9q`Za<`>WNcZh+u*4?g zokvmaB^26l!8#ZNe+sjx?tqd zOf)KSM<*&%vhPM`Fm1^w`1?0XDydBzY$%JGlF!5OG26tQ*?%7b=x9Q%DZtenW%jY; zhUWGPOgy-}Ul9@Hg6SKQU!neu3I(Y&^V=Qr%Vvb+v|+mh{8m(n+Z5BzFA*gy{!?qU z)l*OMY30lKTuf>9jh@fNu27Ui8KWH9*oCCLPH#Xe7p+Jq~6EKNF2 zhI#R5o!6|+@bc*>Ty+HIf*9^#UC0U_%)2uppyXKLoyQmbYuWJ;iq%oJM_XBj^P`&X zZd>^DY_BH9OS6LTE8!Wb(VbxtEH)s#)6tTy7_gpIFBCQdsW7kr3c##fzk1lRyATfs z>8nXr;pF*X1meL=#!AzpRalO+-37+bcYp!sjT6$2s?cpea*_T6RyKI?p>Nu5Gtzmg z^&NpNJ5`@!X7E{|Q8CpgaVoXc#`AdJl^XZZNKG#Op)O-84Aa1;&UmSm9-s`B&I0X~ z>X^7fLFEzMNU7{0a0|d_Qqd1UH|dZ`lJa`DwiK&HRjW-h{FEl-^<)TS`AI(s!~?>(cXs35oa4wFHs{`~T8f-bQk>6@!WJ zJ=w!Od^#F>=G^(oijh~Ire2$0pb5`G+_H)+7%ep!Fq~*ro;D_0-q@r>%P`0nht@{W z*5ewnN>0yc00Y--Wf>@eR3K3Z3!fD53?HgOlS5e*dzJ^4JGkT_*(TC1gc^EX`0?t74!&Yg|(%VK$GGhN#G}g()Pjmi?fuOx;a| zoGHwtL=2L$3=!d_<1klGu2s^4-*vliJ+P$Z)4PPcyQ+DJFc++MgC>3{Y25flJp~Cs zFChT@cySkVQJ{j0Fj0TGHL0U&=314ORr%RPguC8%7_eqd*WT+eR+e78n%Fk@cc?j@ zv>@i>fO1x!Q*dNadixG0A!z0>i-o4*-a2m+=n=c<8k>MK(x1eDs=mvI$y3X3*6TrY zwL{5uN3lPPZ02#<@4zmcHIwQnr74Cs{u=A>fxyaU@vJV65tVQTrh39cx2;pd`R;=S!`U*KRf6&EQ9S_yI+bOJ|z1-{iJsM^wbSHDv) zzRIj4X7IPH5W2SCG98E?G1B7L#HA-gay0m!W*+y7ro=(pa3NeC4c=7teAJqUq|ek^v}$C~5l z2W>S+vLbR_!M5Il*^&k9wY?_xco{fnADpmDAz?8TB*V^O94vopjO`h0Utn~JTy=wW zR-#ASzN{K44gfpM>j`n=kp-(r-J1J&d7_;VxZG?jc#J&GK*CwTdN?p#ZkT8Mt#S z*4DO6#zeXaETEu?*orK=U8Q!rbjg~{sCt0K#75iGVKWHP(-H0`~L<9h_osBDYP zI~-E!kIO!7d~(;rf6)awgv=6eTQ*AqE6rF%Bs)9ji(bDk60r9b?Ei$@)i75Wb0b~M zE)?J2mkWN-7(v?8Posgh+a?9*W+~u=3CvR-zd-Ek=&A{o?|Ixx1Px5H!GPWtc z`RW0+C|{DN<{0k1B`~I~da#En8zp1#Tc_8TO>AN8QMkyWTs4N23!xG6K~lJE_vV87 zVaU6%j8FhFW0#@MY_Y4Yyo;$iOI`2%%wBG`0t|x5qf#hgaK_+g8)AF&C-l)A+aY~x z8dold@K>eR*lC7%@Y_~MFjPvDqQ7q-D`+A%3fXQ!QIBD><10|WUY~_Pa;m61dRWT5 zdJ)KdIJo#Y6?Ted)nv8th@4^RrX}E#^ZnH!9Mf3j|IPeU_YDfrl}G&fIECRe+rtu# z-4G;ac7smQ($>J4*_`zqNDAD`tmX9qxfW>%k}_3S#}0CGcSL4L;aY?maEBkweociY zguM<&uzN@rUX5TV%(|dQ9N}nU7G|k10jYhNDizRVDCfy#1URvc4_1ma9t(v}@|%H^ z@(ZA(?Gz?P=JRuj9@$~$2A!43NE{j1U-qU2&%~KoJw_NR?xCXxhN9i#bxuM!hs(~~ z8T76jOzDrIPrcxe;_+-iVYvmmegBbo!yqCh`x<2iL}-#>lc;At^j(uYGd1?!_z2;U zV|hoH>EQu!OzV$X@i$E=S-W5SqY#7EpibuLvX$)9 zNG|>7s#PWeSvSOQoLU*lem3~V`)F#t&qXEY>xo@{hBlMXGjOfq#jp?sBb+Zl$M&H| z^?wCVcQSkLQtpvyw2Z-H6>Qj+cA)<-w+ths0TLWNJ20(^@-~1}BbThMhXW+}5d$*z z>C(0ytw9Ws%51_(bcfzc|J6uU5-!{N%iF@Biv`Qf?}bMc9uOr|d)@q}4}O!lSRAd_ z!N{n_lKOe;pL~3t4HRihQ6qXGURfSA( z7A)+T_^o5&GU>b{7l~-KGtB5-Xc{o{km`(>Q(T!7w*FfAEIrtKVMT9KsU6~p)0%T# zhWQ!D*evj@4#kSeTw9Q$b;+F^`FyW~3IxKdOVE}}~Lu1^J8iiR0;A0HnGwU z4j_Zb2CDY3hHC38dk3@z+8hF5z_a$p)umER|AO;LHN(RmJjJ<#acz^u%L4A%24Hdm|3RH?d3Gat!sY8xPlz8` zpM0VOTb#u#&orqZQ?eps$2m~iyXT3*h==LT98;?dyjbgR8~o0Z&pvONVZ~|_Ni#oj z!YoD~q)^BkOl_-JW$DUxKsvrh9frICh%w){niE`PX#k#hicmU=l7)j4?p;Fzz3E7v zyfr2u{Fy{*C_mu)(DGaSa!R`O4oU7#;aXqxL`U}FVgx?rK_I>si=%-~?aT`;nvj54uuWZ2aU zv)Sh4Qlvxhx>RNrAh7E(x4gP8{GO(f>vB{v!Cay#CI7ZHMNUqI#jCTey5PKw-n0^~ zAgL}<_ns4Vl^n^MXsGLFE$ODqZ_5%7OYO;Z-5R(L(bkAAF_8#w_(fuJKm>F~jwtI-g(*i$O9ED!RN8Cvp9MjC@A zR|lh!tR|NmO#(hUd+WgqS!-)$z36lmwT2EW;arlb!zh8m+GyWTdP1XX%>QUhQzkT)shPRmGJ&QUn(ZKNS-Uu}$xiDarQL?PFY z^m9$8%vS2|WfjmqRAdiKnJ+ABL!y$ntrX=Re(CHxZJ_Le-o(#8Szak)(5r(}rh2YU z9H7Oj?*YH`UPKHOeTg}KO+iacZ?@t)*I7N_ZXr>U$mM$nr8u~qhHh=SCP8;a;CRjr zx{ZjA$GTAPe@;ehVqSMkI5UmO9;-n^)Y4GW0U3Q`pimjr3Pf)uFlyUkF!7?bYS{xH zU&GMqLix3`wfY?jC|8^zz>Vr?=dOPbnD>SZ=^aeiTLO>cWIJ>+<-k zU-wJQG|J)l#sTM6_9-XlMTc1O%w46htl|K4(=uw>+0d(^HnD zG#v0P$nefnRh{RxQByGi;9If)G6YbLUua`>d*&Zl5M&|0lX zAJ@XHr9WUnBgh&@ZnUDJoq#Wge0kaI315ah%-qYBmIcvj(dWVeTjV~~#0OCBvLi|8 zn>J!KzdN7;+PHb@=Hixbk5p&X4T zm*7F)`VD#Mfr1SDCwOqJC>8@ohD4U2abc zLOCB3EsD#8IT6uIfcW9(xKaQsZz{MlV{wFR#+H#2D-4N5?yw{y>FB;V&f1f_TYcwV4t}AVG=~& z#QVnsAZ=nnFHqnTZ1a-0<^`k!-&SfKfB=JzTE{=5d^H2jmX;WEV%t-dnR{nTvoDwQnJR?Mpm$p zr12Jn#F)PyOhgMc0FG6*gQK^2M7{bKK<}iV_p%{BrAgDVo_8~GS|18f*E02E>% z6#fBU;j6>~kPZwc0~)PQ(_0`?71jfkOy(L;IRJ&I5+l9Rqkv)t3dl-qt*UNSMYCE% z)}UEAo%^d zdH-7&S7~Mh`K5lAo zW^8ao(9F~ni#0enI5mF;V*4#E5SRaJTeHuaY{~w6+oF9}$<{~l-PqR6S8u-RpEqB9 z@jJZ!1u*{t*T3N9U*P%~tbFfosb594m=1skVCD(Is!mV3l^C0Bf+zekh}Q><%m>d3 zS}5rub`0I{_|bu)YUYGT5*3UkiHiqNS#vpMWIk?Gc-X2yFi~7QY?e5e^9LEz0!$5< zshI2ZiKU3cbVuczmLd+<{mZ-`lp_!7M@z*-VlG7^ksO1+rgR6^Q8n)FH11y=bax^# ze;s-856UqI()TO3>8ITIi|sFY#l^O(OcfEP!9TYGKs|67^I87{Dq}wDwPHS7f~%m) zXpQL8cBC~T7`h|v8TutskoF9{Gbu=SLq8@3>265Zrof{_B(M|snIW7B4!#{4nIVX6 z{ppjnQKLC+>-RrwupjvWgxc$;Pa3?}N`3&L$Oh5(KX5Zo;)f5AkT^*^ z345JC$Q`+u8+hHf^V!2aeXDaqOeUKznu6kBEDov3p9t3z3P@#duODFNd zI(apnlqNPzreN2zKYsF6qvh7@G4bVz7f#?1xnjw#Qoj^$Vbq*n6V!8-s$;`J;5HJsyIl_EW7TGA8iosbWuM~aG7{D*-x|~1A zp#YT7!c&c>DO!w&m^y=B1CwkQ;!){7tbe(>L)peB;JP#Y{r-Ef-E*79)g8ZSDct@A z>*7xY7k5q0?fQx7-IG`-^^}s7id+P>cz2*6r6LzWLPafJd$2C%^-s#R)9k+PL7Q%G zb`P?9@JHY3jCPuw>I;zgqJ+EibOlwgFke)%aJJd~dQg<48G2L%6FGvgR z-xE*;?dbk}=kM(PWp?*|=Ib6G-g_JpPo{-G9H*mVo*n;c8FA0}$`wH2Ph!Qt#A=`e zD1MMX@hK`Sxh!f!^T~X)qI=}J$b7S6>yxphsE{DmGIISTmy%D3dCe{*-wPAZ0VS7` z_Yg2jVM?$fJHtw?3on#Ms1R0yi>t5ZO^DFqE&3F<=zS`s&YtIapSSwF&iQ&3QD0k3 zXIIfLvBi}4eZPfIL1%Q%`6bmCRP<-Rq{6^Jp@*F5 zkDQzCs(NCo%oj=^B+#q>!1{|-Tb0A2=dfTAF@sBWDFWl+Xuk1a6p$$x{J~n@*OzH_ z|LxGD(6s}1=rL-C9)rQ(P2)z_&a-KuT}vHrI?idFo?~L;F&_S6Y+6VU?O#mXp^db0 z#SR}n(N46_iQ`N5E2+5i=Vghyf2;6`Ohx&uxiP4l9%=bGWI%sR31dg9*qZ40Ek#d{!=1Eme#RF*C3%NorX{$>9fvB?wxb!09J&SUb384(=)k zhgE}f!LO3RHEmG1fdY;k&MB1K2?;4kEdoP3=)ZV8zsChDtAhC&P>*(m*h=2hpgPEBbni!~v#Y=3{xlJ^R=_g=4 zd;;P&edN8jF}AW_ak2BaS$!wEkx$8?B&ctl)3g|(&a?0HA*6Cr6(gUL9(j*ql398sNRB zp+M4L?>=8*e$}FZd!#Jj=K|}Yv}CfuG$n&&De>G+L?)cc75@Fkdl?Wi($6u3`gASo z^SQKz*P%tv^E+5niv|;4(!R?-la#dY@>;a-(tYsWpPY=8u&^$m&zfL$0lhgnpgBJ> zIvj$~UO#=#;GOpR{>RA7uRnZXr0Pa*lY|fi>F|kQ1c)oT3WsWh$#a7T$Bohkd-um} z=2tBnxJM1iRSgLi3`rBnG*N)b3^oSPUMc!alQ!84f{-!T34{#sHgh?0HQcCpWZ;?M zX4zw-XA1Ua5g~^e7}^l5@{#F2GOQX|b4GSGBg2A`x!_Z|$eK1x6l$O_#qu`a1-umS zngL}1j}4xw*js{;U@c7db;pCXP*wL;ra>FEx6ri*YvC;{-okVb)@SCr*$Ehz8T)B(Q*)?C)Ts6TOgCi%g6In^j*x=yMOnxDZCQIJoG zotqUM5HeZq;j%KJ_HbgNHn0T6K=g>#Ua$92Y+@Bh)p6b{S5?g01TmtsG05Jm5FZz> z?ca{333GsyIXN;W=pUi>z^NLc_7Gv#)F4xaxX`LuAth)wI;f09(P}#3|Pn7fL7d2Pb{O`4D{)ge$eSI)NzmVD^=%s1XQFMV?2~rER&~y+@jGRPo<3){WG@A4UR zZVRHO>;Q7mc~0k_vmQtp@>Q=D@>QEjtD2y`_e+raUhV;2cM`*cz~FX=(ZC11*H3}a zQ0YvZJvcqYN{EUD^IgVl#HMO(7Hn0~r-_`#nbfyLm61?cD5vpsUsYCB=OuJrd8p49 zpoj0$%BgUs%@X!k|FkJ#fAw0hzryhQWz{(R$yi;oUr}Y)d7WQclHMP=AtmYMK;U<9 z1mL}w;X!KaPoFf|URyr{PT(vbG*hUN!qki!Gp1%T9)9xAu%G@gVaKWisA?xWd2%{i z6+&gSM6!W2B?4#F))jr?-upMFg-q%mx3t`UPi5P9r#mbAU6qBDUOT(8&aJA>>(Z?9 zev6z{TVvLiwa@yeO&^^~P0b9A2%8WyA#6m<+|bm} zH<5O48XrnA6@$TFF!-Y}9R7+lJmDP$Btar3LBeIiXPJosRar8 zM)wggSx88gm3CO%J9KV5T8}I2@dWm%RubG}&NYK}2}%S+k}vEZr)7~pJ<`DrkmoMriU^) zZfqjXF|UL;HXh>`_>Slpll{so2?bvp{UBB7eUZ@*QiHyP(GOyYA;P4Sm-ndA5Bf!T zc8T%J1knVHTB7$}#}#>bD5GCwi7v4vdap{s{?Y4eQQuGMixE7cu6ea_eJ@|@bNN_b zQJq=hI=e)_)DqR%MNH=x(ewQL-QVli{#-xy=Y5*=w3wysx3t)nOwim>MTQ|kWod*L z6&M@_2^bs(4Be50+JooSQ!C&DSYSY9qQLr!Qr~!6Lz?+J%}?ilgwAR01Zn&!RK3J|RpQpT|4)##sQLmjuaBRT@rHusE>YqtUmydcamyha#Qd}(T zuTk}yw>k4(&AeDJZ&R-6FE(9-$-;!8q`g^j5mQbJYhOm?U2vDvLiqS)zk=3y9-IGd z{^DgWEd8238My#sTj-&WMgM*g0NkIuwi|$ zxqyb*1H)(9EOBn&;JDeGFd(4Bj9(q%7t6KeslhV^zs{eW!Ok1(11O2JIpq-@Mm$G*UH5t#aoIuDN&N+lcF>(GNTioUcnhvwnF@=^&%NFI#rGX8Em})?r z4yrpIbcqPD(!gI4$7|P1=C#Ki__*`;^@xQ3od*RZ4t-*}gcv9W5z|pWq!{=KI8+=X z&hh)F!h7$t8vUS@p%%Fjq3dNDsXp%&VB`Z}Qr6#Hse`yhLT;f zGAa|D?0SWl#&{K_%Ku(V#U^|2b-G%dVq$`#^!~{WDM~L_0^jpT;Cps#H?6Kxb<^rI zYRs52AsLh7L33i_+viWZuyp-_GlBJ7X#(pvnA3$mQJB~o%8BIoXwIS)U@mN>QN($%Igoq~6?pNl8S;81p*6z|YbHw`)Dcy9cm072(8 z#KRnDr1K=3@xusCB*f&Fv(;!bSq#Q9!z=CVajrH`Nw`;>3)nkg0ETALX2zzb4jOTA zYHC0}&}Un=fb@7;PMo(LEL5^TEAeE%$qW$ohm35={%f{%`zs0%lMkDG*gs7^9DBk@ zbf&tVDl#3WI$2)*r8;%6{;r;VruZ7@JPc_51+dtb`q>*aOw_!7W)>-uV4hJ;zTx1@ zI;yVo>Pl5Mug)R|iyBr?MJ=6_H`oc(>OeBxXhlLcqJ#_?Enh~XZrq-Cs*v{{$~L}S zsEun@c`DB~^fxH_&QJYQgmUMnUTf#4xdW*GN7a8`J+~t9>c$KhZ7`tPKS(dAS}piSR&{Sp_8y_mLfpZA`~$jW|2b-9YQ z7OoGUU0F3p_0`gO^{oVbQoO&^!>Ve{LOLOwvOi0oB{=5^E7_mIsz*{XviCj( zjtL#4qZ$!!Doh2mYD9`hqoO!g}<4s2$9?fiY|48N}GQ+?_Dntgs|i>>QMyL_%< zyOi?0k7bMNtQMEldG)=lK6PG;Z=233`q7$M3v~AL%12iq(;~sjM#X*;|NL39-^6RN z-$a+>y_aQ`m!BJvV)4hau&^uapDYaHet@VogUk;#9X`~sMrv6iHRlQ3R9S{l3e^yr ziowp7S+jGuRm-bJE@{A*6^EiyAf}JZLHyktU3#(+wvaBO*NbAWdG) zoEtefXq2^AmQCh^#7+kiJFKM~OEtL2;i85WR3C0UdQ22IP~hk(X$YWoIeSPTz?}Y_P0f zV~WKrGN91?{vU3a9Hj9Y-g8=LV%>*zZhXSoon+*dmGCwWLLX~94*rNkTZ$N*#a4Gm zs*`ppO@47^a}9=Uj=|v9om=l?*`jOyM=U$9zI8>Nb;252^{K^lR*zDi_n|ZDiv`i5 z4kzS*%xj@Q6=bqM#6N9I_J?>a_J`<3c<*IdLN+*tWdZ`S5GSZ#t@3dVH)t!CqJAeOj(8zn= zVr#LgplRVf(OSH@y-%f3pZ758^D|oXzDdZ>e$mVHK>bKd*?;-xk5cwuUQ700vaIq_ z0<43vW6Y}Z{1Bu(xd!-?5*QD1v&r@w&+Gdq&+Ml^aH?b$XVB`VD!5F+Mha6TvaFkn zZM>OO6;(!3Q*{2b_odLLzb>$RBkdp<-eiOOH4b?EPA-(ZTzKs3fqxb zq;GZqIgT5p=%yH_w)Ha)ZG$>cX(&I24z4|wU3-)97OH!X ziVT7T8-~i#2roTMJ}l;;CEXF2(=xQ9&1;aFAe)?s)y$Vg8PEJ386K zk>gDc*3{Gn1`7-y4^jM}xF+*ZrB;dN`f7Z$+3R4uiZU9(*4M^Xz7GTp)aQNgjG{i* zdlR0JpRK+V*gT?Q^{wkqVVgbA>wGSV;$P{JPeIiYbV>b7!ju;Bvm9DPXP^Hf_=8gQ z8B^|j9j~?XbxJ~DI28*AGog{lcwi(v^bU``}9#UNrC8JV0&Y?6t8wTzdYyOSSt z5#*5LH=WxoT$gtg)3|dxXlD~?pq-ER-pWXm{R+BVn`%3VK@6fU;KB`!jg1JKnHGrr zf4e0U^FMFv`~I!AHT&#rtK_dGfA!CjzqZdG^nVM4hxXNWEB+sd&clE<1c1?msG44p zW3ywydGA{sd>njP)nQ&8$Rt{pz)qQw1@U4DB1s7XM3b!Q@~bB?hfHwS9ZvtOXbA%Y zvlOfUMb%$k{f8+ei>iOc*IoaLs=SnCSp=S{^Im<03Wdy-k&*e|Ysvgy;)~S}Le)XM zI?gPT(eU2#bGT4ACg%U|wPH%<9I?tm)mgkelyX-q${OrVe#p=~O-lw-eGg(Is_REg zhTMqL#LQJ%g~E*zoD+9*&WHghX;YPx0NOHyG6e?osE!K=!Bk1fI&vXtWaa|^F4Sbs zpX3Vy{t{PfnKfNykI9?$h?!l*%&>H`<_OAq2nizAgt_T2<_o1QmQ{q5nKgMLItV5W zWymZ`2r0vwFE)L~vPfC;F<9}!ZJNekJ11^e>BH4h4lr(jkpkZhz7(v`aRLW&K9Exa z8Y?0VAaa1HK}VH>q$T+GFY}O4QyUnV8NO$gm0bIe3U+_VekJuO;5MO!_g<&iVn$nZ zDg7hX7lY3e8Racp#gDVHKJQ&vbZO~Q)9>fvf1)V0zVF{eMyw|2H$u)}-#%Yx-lc^C z_i%v#nk5URO-hd(su>Mf!89&=4nSM(v|voe-mEnV!{FXFI>a)HFOt~l@)rS7U64E|D9MpGF1l+^cq-iP?J{~&@KbSW*OM?!w4 zgZ0mzmd{s>>ta_D6-y~Oge18+Q>=IgLigfa8HoVapRr4cA+4h18JMxfxoD9 z2g-d~Hh;Bi?qX#mJ-3okWwb28<=8(U{))Z!4~VO~-K&PqKZyB*{tpU5`QTzvHA2-# zyt)!|u3>0d0y{};T4bD@i0Qtn>Fifh9;&LOvlo-6MCWpCBD%&Z>{+cCnqReaU>$U9 zS3y-XwkFSZ#j|0(*j(^qey}xZn84-EjU5~~oAc!Daa)GGPBjT))yPBuND6e1n|3n(hA~cr8q!C*4%)fAm>3za zgEB!{CP?4)ulN$j;KrYf{Zrw(JXr+VIP~#}X)}`9<3?r~ZuU5ll>|5c!<0WnnR)2p zuS3Y;F(Sf`6MEp(grE_D69T8k1@eDB+HQTH7XP{@guIJuduhrqt}RH1pA){3@=3KAYhI&$U+14i1bkvHjM zMT{ZpMidSq0%(>&<%tDWRFPCbi7H0UQjP`U%U6dCTP>?9yBwxuI%?2zo>TqGLp;7Z@P|;JC_Fn7yj}m;LrXA z;~WphBZhP_iq7%8rbTcv{^A+5ke=Nb_YW$zYdR`L9MjC~W@dIXGfXuzJDQo@P0dVA z9G8gWIG%|?*HR+HvdV6^+o`<7bZyr|+oo@&9id&4(5&R+y1W>1jMyg1h-11Ae%f!X zfRRs8XO~Z?g=-CZA4ASxVvFhgk6avd{!9PQD=pOGk_*KsU32jM0D4$oOUsa7^Uow@ z$gg>=kY6j=#g4L%s{6dUixp7SkeOTJU>;^*cei`ZRUoXGU_wiw2UAI{MK2`U_ek${?aEW}ygQn*Y5P&Ho;VZNxP} zYqT6!Zg-dLSJ-*Y*H24#Of>)q!q}m31ArimmE(UQifti1tYy@<`vkm=!I-Y;;S&&# zgSSwO|AKOiU5xk%X#BmzNkJ%3^m3QC0}aBc(_<I)tYGcXphEK^KY-u^miys@B4kan2pNO@>=}ctudNkK6C4d)pD;U! zPV^kjxx$QT&N)YaqJm+zEJtXxSBpMf*A73(6SryddiDpJq`{mqEsvoH1kvCwWj16} zF&|{?#lr+_DI!x&!4Wtc7eMuqdaKcsGr+kdG)z zLOgj0NySTS*EAmgpl~`L4vBc&JHlUSHiW{T_!Kr?`97bL{@LYupF6*1)aP?9zlhF0 z#rKPU{wzJLuveL}Z}ZP|8T&S`75lc5ox87ctg=3T#UW*VaxOP`mtGDH1?FKtd?0&$ zptQOQ2IwtMXaS$eoIPY)qn&bJzP7%8&|(wMK7R`sx;|L|HB;b_A_YSx@FCH0FsBZR zZUS>6-RLh=B`lqkx3xu|24JGaI4ZZY%iodcs2>sypPR;61npXg=W+}ZpK>~Cw`*l& zgf~%!{-Q zc#mb^zjXiduy$g?YaIU-b6V(PWhC4t0&AI{3>lQ$3S9i5Rf5u0aj`j4RgYg6;Fcp= z(6OO2C3~}I;LnL^-CtB*{p|e?s!Qs0Zcb`~=1s=m@TOzCjtchTn=ZySo&SPJK}bPE zML{lI2k~Dpj=_ySJD{7+k2#G(h5`~%$h^ch5m>ZK!d?6q#AR9-%Q6&Tuvum*ujb*9wBB#)QAvqgHzK2 z`9A;udCT@&*sT|t6Kmp-=q1=FVQLn0zK}d{a|BL5VdKP<4CdQsiT~Rzo&Vb{nE%_Y zp8wq~myfnvHvhkD**>iWqVm7UR_(VjAhWN|zUrT|ue#5WkA{5IKSMsMB28RPS$Pz| zk%gCwhZnr1{?&#RYA+Oyd2neKvRUb2{eqLoLMjeuuWr+U=b&xaiGY!5LxZja1z8D8 z=f)`8QI-8lng)s3A+_oc<;*{1v;0FtEs&J-)E*{Y9i?i8)lL=B22!gZ4u}we5UL~^ zR$$##kF*#lo61PLoW?_rC|uu%>iaOi&+_~H@3p>93kxwqSlHU@92$VTdY+b!=66~@ zFu*K8$8^%XaI@|3mGZ4=0<-0I)pEm1xn(82FcPvu3PTAn{FcHSLL4{|r)`FnS(C<$ zXIJqItAys;AGDcYdCefE7#=lB^gEz2$r3#c`Wuxu`)7ve9z8da(eK zo3mxwEJY}Tk}vR82@uYp|dQ`nc7m z2_onv>~PWnW0j+%HhnvMI($|oRWc(wt7MZdH3m;=Q%Hh7!U|nv2alBw+FQNvL4_yX zNd(=+tfSE3bE2Q%!>Bg%82Zu)oraZ3mCR3V#u!0=p`;7#7+Ud&=*Hxlbg41G)#hF6 z!X+JsPiNBc<5GQ?pf-z6dT`cTcBEM0NU2R3PI^#D7uwNdrE`MX1c0p%nRF6b=tLbr zO=|P^LZ7iih7K22BTV!cWt()VKG3Aq`ySBw)A0T&1E~K-&|j?3Q^a6~+LS6me;Pr5 zk)$7W1hwcWC}B=&lO91o#`+1Rn{=r@f@%{ef=*+H3>`aGIw-o1@|A2?U!ny=qO=$7 zSwquhs`KTh*HkAAOn+79%uJ7|PF_u)sZKdWx2aB;m=3GXmYFVFJ^YLz(OP=rLF~I? z-{qgN??Q86024_T0)RsyrT8hD(L|GxzTE0J9X+oul>sRm@D#0{LZ*M%K_t^%H@H%e z>UhC}4+~#bItm?mP`@Hi4$?u#S9bjhYO~HO@AlXAt6%7=LSI!~xWKiLv0$TCIaXmk zwn~NdZaBR6F8))z-o=9-oIu829*B^iMeTbnq5vSd`VUfggCixuapbxn8k75Xs>z%vE*>vjmoo;xaYQ?n(Hvj)E3+7=ZpV|= z45sd<{s~m-e(JS!KXu^(C&PoUTP*%krI*&{kH8`2@%eDWLGAImk+?za@xkeC@Hd|m zs-RZ}K?t!Cvi|Tvkg5m0S0Z#*Ck{YVgJ{YpdBO#lnxzQcRnXfMq2qC*ySmX~jXYpv z0xK#WuJzzOWM$DOoHVvjlVu&@!J0_~XAe8oq|Tm~^DD3LCxu$(7ZUoe$dUKn1x^;k1RY9&qNmjRaqT1& zi#^^=&WtBu&TBG0<+YF(MFf@{@nZ3ZaujO!;g81i-_WV$(YTQpMFff*X$))pN8?h& zpk23!ZR4Cf`nYzS9_Bf*$+h26LnkLb3KD!KmAj z#(zygn`pO|BiIW6z$&bL*WyT7exf-j>O|HsyCEkD2@Tv#h{fV5xcWHebY}; zG;G^=s~l0V@sEl)$7!LkZi?wAC6Z&*kIs|UIB5TXY)XzpAD3PKit+H5*TNbIcl_n` zZM5=u!kZX!q3OTAcDjHI6VqOR7A8pseolQi-9gUKBcoUHqMFfT%hjDP_ zm=q`Zq;QXT6EXea@uG;pn$WIeV2vA(WXB}eUc_B7kq*+hf3zLrG=4)BEXN-dzH|o; zWu!Y!*LgmJ&Ve=2w{iq>?ZMkM3mS>TpOB+cyGQ+QAw8t?c8?u}c1`03+O&$?F^S!W z?2i9}Ha-0PqJm70O7H&eIIzZrfKs z=QRCP#N(KoSd4==v4zW#zMEmEtVD02Y+|4LC-ZU;nyqk7SwAAgGvv}>sAn{fK53M^69BTI;*iN&1PTOq}3ozGt zv;r*#8(QIQKg4uoZEWcYNVH@8Cl$v8@A`@8RE!F8dB*!!Bo^uUrh~LpZqq#X3HYmv z#rO$`*F_rnO~-ak*JkF$MkCQRWjI+n(GmSZh&mMQrFcW5vD-vgz1bcq-5nj|BHJqX566(30OU$zQ zX%@~-z!^B_=GaWn!5P>zH_avH*!-Jg%uGx3(p+MO%_Ph)<`G=XE<~7L%r02WEauz{ zn_JAjnZ-QMN^{aYrgYprKi44g#8g=G^?6=$U)w z67xHg$eDBI-2CQ*m_y*qBIXb@^UcgWGv{V^2Q$OWx!HwrnO)@hWp-(qKg_xLHM7hs zvr5XGqMPG6#bh?Q8JS1s+}yS#xOppsn`N`e9GgRC*nFGe**3p(ZFXnc{F-NTJHPUu zV#e%w?e{tt2`NxK%K-s78yQG|Z8L)t!bSv*2oX0kEI?+y4Bq-Eekx*X_DBAVt(~tE z+Zq`e8SyYk0V@Hx2|6loeNMoia1 z*U9*c@jAC}+9|i`I7W#gO+)d-Q1>q8-Mk@`UB@xQ;m@_{SXke`pb)AkBl8l|Pr%>L zF1Ks?$aQ%!ezlC7&S{}dM;U3z;dV0iOfaTP==^7(9#+@CSmONFKa-R=zx7%;zbz@i zt4PyO>Mo<|GOzx`L_rw>JBg4mA)u{RL~yimoH8bFPIS0A!OAVLQn{Saz$Wu?1c4x? z$g>AH3klXt?ViG*fi+&c`+IbGBV=KndmslpjsX=FyD`C^Q&9vt*fQc7bkHt(iDSfw zV?fGahez8)q&VizW2gHUjAO)zW0xlb?MTEgaf}kdddCyTh<99`iUW``h;KT_jYoMd z?~Ig&Ufi1w#y|l@ki%^}#&nL?!%e>aim*w)2RsKe*ZnV29xuGNlAv#Fxu1M^#W-6ms zH+r$;_v$FaaR%cO_BwAeA1N*!aIPzy3+v@_lCbgG4Ix71C_2a0J(Y!!N{Dj2Pe2bJ z{V1wfsGcR#w8e&vs4NQ$^Yi5#R+yhRmjjynndKn);e#3MQM62I9;PLScv=rI16Wd= zDI_3=a{?(#xI0=b9{u zp#+DG<`hD*!1NQ8?h+?++GIXr7Z09NDgaU}xUGkuJ6zmMH_?e{7NYLCP3I|EnPx_Y zR}%xf@(_kFgdyNS=I=9%?GcG*)5o0~6C(yMziFsAMhrKvqv9DvsW?W78MTv>5$D+* zjdneZk|8sU5$8i1a(K#ZdWWusbpE^Zq}>xR{~ft5Z)X(av`shBLRuKdIUTfXA@98o zj4l%xtr{>&mAoIo{lZmb=T*sjq*X~>mDW{hEqb3+mDIFK$j?x;#>46Vdk^c1YK>r( zR5kk}{)w`j{SmJ<`y;v}l@TFSk;xr4p*ss(^lwy96XO`r-p;)5v{%U2Jig} zJctjLA3z9!{q%X8?IE!5e;nC#7!#OqxiaB`b)=ys#H0KG!e{Zr2RrNc&mH)uKbopy zUYx-y8p{&c$3$*UU5PUuapKq?E@ezf6Q<+}rUdK7tcgMeC|cmN0#`?g68%K71WA*J z}*&%%$^%PIB+(nsyqRPD`&${gs1|icl0OpoAxc0eJFqmdj1kq}97kTHfCV+=xO24rR=04xA}2e6gg_L{og zx!e&RAIo;`Lw5%j*ZVPOAI(i;FU#zNj7+beY;UgMB@;;Vjsd&5tuf5|E1F7-jzS$A z$;Qs*({EU7Kc?;?4lSn6Ojv?tew#^i$JN|KMsKW|70x(^P-qf5bxuFU;UCgj*=?PE z3df2Q*>n>}ZN&%GRnZH5-?v?Mu~8^DLWOqsXny}JblKvOOLNdoUeO~95!8W|oLI43 z@FJwsYY1gQqQHi2(OtvByi9<&C6&l)bu(FD)Q?nO*J9&J?$iq4Q9Z^BTPo$?+l=LT zF!j7D?}}J2@mW07#(@&`(V(pIcnT&lFeN5VL$xsYn;Q5L$!^}y9n&T!(S_5O@`u8k zqmMKB_gFeJ@oPTxHQ7e|haYNpXsjy;5!{BV0&4jV$^bBQ{DYmB8dJOjuk{I?<%VA{ zfJFT#Gup3ma|=dokwGARE(9X_Kr~4ITfHJ-XnPLEPf8L0QtvyT&3aHAE_u2Bm ziOM*ZYTrIv1BH85#73qnjQwT8qJrj&-Aju&FL^S4J!iYc`yOt1W9s))ao0+sC)Gj|ra_ z%7MjfDXQ@4U2(ehX|kx5St0|rXGehJv_K1d_)QLE9F!WAJKs0R67$KMV4bo$?!zqt zLpZmgj1k{O(nho=TtaGnH}oC+Z`D*NDJ=hM(^NXBIV9~f)Zi{dX=&cX zmtMwh`%%sbytZf48zLPD_98?;+T~`A?sh>+moybn^b4)sJA1tlb??hw%ieZ2G)42S zi`E7#5@dFp2PCR0Te+YZWF$4blCpctK+$aQ1;ZPzHw21g6=9n!3!@ytpKnw4f6bLF z0M>C|=AgbK!E7+Pg}Ls^mrc?VP|9$Z5V&z-sl7cA#VZzE_R#1XxDzB-z8(dK{(ok5 zw!YESmk2)HxmC$@M%9#f?xK^vOEF!p^hDBcnW+F)1pP8l=hz*17<>yd<`!DP235(rU2q+uymgjeIe;Ylztml29 z*}FfBEmn#sajq9CtIx}>r7;;a5Lt!_inL}r?=P-z+H+P#>_3$Xw@Z(=8F27wJl9vg z3g4a}l0=#d?#*U)9_bjI&8i>i@yWFE)^@vOEx*0dk@(9<$U>hRcTIN#-(PrN9zzIy z4}gFH>U>s|Qf8GhQO`=tDm)2GdY~OYouz|iXyly5Q9A39bruKhtfMCc>_?!zgDI*2 z=|YP;kC;{CNM-GKS+bWU&DeP1t$#onCLR0Q|jHHaRFmBE=&2t-|HD6D4`W z-vcCcsBAdvIP75np=c|1J~>q24;akc6zE20#KH_imW7^u ziX(HUgK8e31LDNjRm`Q0bPi~F4yPs*S|aJf?&Z+yE=S=pMdiHU|050p3M?}|FOoTP z!XCw9?0F}ZOVQ0$##$IdYPsABbk#@!UT;e?rd-Q-N7nLTGMG0k9*9HK_}kiTm$TU( zsA_Pu4u}>pCH#>i^MjsJBv>>0qxw1n;~$Yw3<{sVRFAD3-4#CbgGQio=SRkzarOzR zt*9@&QqaF{@VS2_k?iNP1*yaKKj&8DyKr9!`FbGbqM_npZOZhPQW0Rg3n-}b)&ow| zT%TLok=d2DYuhE${Cdb>sg$o9tz#iraj@;&;FA|(&oo>Ov)8$o97<#?EY!jkGtnj^ zL^xgHPc#GMlrq}TgiP&qrw+m{M4GzVxnN4r%#R9x}jG65(H+n^0v3a2J-i$j}m9RXHZKA3iUKr&yZEs1mroVs68LRdt$ z>fu9o2*!Eb;@+95Y5t9DErcP&EfST^rYM`xgND??;yn;XqX<*{yM3d0VnDD;_1VW= ziiA)Ejm$zAtqIoBmu?2|k5y0f@C~aJGRWBNnkSaUgHz=(zxwQei)idZ3Sy=1$vdbJ z?m=rDPF`3f9N_v z4{oTQ5!&I9$*UK9f{OG!D0xX0iGYEqhRVFlT40p<<+z_S=KI!62xpqsrQc~U-|}KJ zl8d>O258Pbn{4R`YTw9vZ&+^Gm=1u`6xQM0np0*TV|EvanBd@oj?1Ts_C`;Po-&0l z>AMUBI8M`#`6dPicmLN%a14uKk5_hnk{VI})b{)o_P1Z4|9Ac%{^AQZ^ zK~7_0(UeB!cT<%Jj}|<;6mRx3jJ$SCcfVsdTuxIL3NwU15dz!2 zc`zYy;X+VZz}2cS+!Eq&stWf*c`H)>9xX1(=~Cb3CYzRL=n)mZQc1&AhueSv1>MQi zv6rZ;Tq8R@$-`#6ON&JBcBgeVpmhzPG=Ee&2PUEw>&$-~TfHGCHWX!|KnD9bo}bV_ zg0px9DTN%%puVtp=rdub1--h@!|v94pLynX4J8o~CD&K3XmLx|1VX}Fiw2lB%c2G7 ze%(twT51}vDJ|2~GsXg)ZyKCDTA@qb1>BBKYQd-DLU$)S(z#?%2#v%(=SxHJ zlyrU2`uEuD&)y@VSl2l~x9}?)Ku+`;VA_rv@Q`r;dMbejFxIbDVHowTZ9w5}KHy$r z0B0M$-h*^`s>50Vw3!buEOw@RlqO4stJQSI5)T>1D zH}|@R*Rj6X=~+4u=ha&<)wtL|VgB5cWjv_VXyw!fKycP-0WQhbg;Wko*c|3Gbp1E= zu_)kO8!7Er(cC1MaJuiOj4*lJ89OOygh&t#-B*k8*SkK@mzisP(BNAJ!&Xsg5sTvT ze>pI52UKA)j=^9A-U)X0!oR|`d?`dn;0e1xkU9Of=2ZFvj7G!I4H7+!A*hKC zO<@!_pjan9j?$0Arh@7*Mc@*>ZL#GMzlp%c84>yYaega=E0h^;WVPc70a#84W=!s3 z8RD)g#dzYOHM7-$Jox~9BAJ|zYcbSg#%-1pW8ZLbBAHPkJ{c-cOh|Cxt3FWcgx>Uj zrpIWik^P){jwquHYd=TjkR-OdXCckupoI>ZWmac~QMc(s2(PegCWB zKmaUnOm7I1o|wn0aa3`f3d&Qu{Y_;tsMOR;YQW-z$dGDsJsc=*8l8SGHg{CSu$%1H zq??ijnqTZ1j{dQ0(1uBEn#X}#AqlaVDl3v4%n~5jqkai3Vi5%!fTvW0QlqZS^pevh zna&7&tmtG?Ceu2!t2oVc2HxzYY8?kyEn*s|QZaBhYG{WFzR(N;Sd{Qc?xm z@)rKc@U;%!YOE}#YCJwz`@vkG0DQ;#?B58V%$f_nRu!Cu+ifb7qwE7-RKwW;z=Rzd zU7S3HT#|rC9^H>g0@Eh6j59IZI!B4EJ1Oj{6Y`+;|4A`1NY=txMwLKrx|~4r_b}*i z4+7<-@{4r&=u~eTFwPg;%a2u)^pH~hz>o$Mw~eP?)u&ubk;v7RrT!z0IEYLaJu}m7b+>RJGn4q$}yB_dBW&B08l8pX$0>Y ziAHqNQS_=Wud|i~+cJ}+e|k(K+O$Xqd%`_{*i%j+BNik1SKJu4_&z{UCHFw%&Jt(Y z7XiaRlEow|sH`GB3DD}_X>#7Pt`O2nBU$St=( zrb;bBjWePa7i1UFBn%Sb<_9iI%5{W6^`MD@Tzb)M@Y+3IEk@<`VJ zEeiu8ATZa&4m|@M<#(+dkfjcY343x%eX!~?H6%oV{s9o1*qx-Yj0A*6+DZ#T#4X*u z2VmwjI3YRp;=jr`xUgG)lzu+awc)V(-kn=ZW}xbT{M=olRQJ>yjAaIRa-medd){bp zmtv~^>);`hu$#ym)axoB)WPcW2|LpA9QZi+TBEZ$RQ92zo06Z>H)}04_D0ubXUS`< zYIEjjgo>^beH&3c+?$ZXU;U#8B-@xK5*~b|sK@7(S44P9FDo1yPuc7`d&9yb8$C(h z?CVTRx?JCH)mKIr8O&Ed-%VBDZ_s8+{6UYn&)7Es={I+G+swcrdMeUeQFTYM84$*^ z3Ic6LUz6TU;s71AM$TLeKK7?c z61~dQRE}m5jG|K%VAfL%BfLM`sYG_tYErI$7_GKakb@#HqG$79tX!l-=(e6!ikP8t zQMrFZuI$3>p@af?^Vm=wMa~fSLx@vHrH10c&tBE=jPgTO_)P{C1k!orTdLmOeL z7>WS^;isj8DSJp(Tt~~vjWtsCb`{1S!A{1WwD#~z%`aTf+6fR`?TLG?IjJ~DLa<~x ziZE8w1pWo|vOIMXJV^ky{Q1)+yAu80Y<9e=B!mdBJ%_r#dfH+s-@^6P6%5c>@z%nu zj0>1MM>mN!bbNqtoZ%Hfb5fbKSK*@?Z}QW*rrb)Z(9OYnkrQ(1k=ng`I}gRZfE_=E zMXaU;!w>F-y{2Pdwb$&@V`H~Yp#ginAi^BQ!Y=jkFX6aJf%-%IiHDsYSlUyUMBhO> zEu^7-dGjEZJ*(iQXo9?n7rR|TL9&;Cg$Gf7%nWb4;1K?6LKI%0GeYjcX!`FKX|h*! z5)!Xver>J~vlZ`Z+s6sji2Dm^i>?ZY9>H&b8#d~IE5 z_Wc65=2k6{D}fjPzHS>JqZ?WY0j9PCaODAP^PIl=51oFW#|irlXF!WhS9uHF;-ml4d%}W+8^6hqz;N3R+hBfX5~+_Ng|yU>|#2` z9s@qHY!M$NJ1+luUMy!veN!JmzfG+m8Tz@(ARxN%MQ8<9z*$w1c*L*?7^6dCuS*sn z0d#&_NJjg{??d9I{KJLwaE>?pZg}#P`B2OiFr}cdFguli_dae?U5B_x3msPmlj7k5 zy~Q0f=MSaTW|3o`pyJ>WDdLnwVvz&@GSu6?5uzrj^@gY}LK#PKL2rx^_LXJ!twsC3qG3u_GCC2?IL)O%_#m#ZS+J z;&6IN#NzNHWbq^iHvvLv=b;TM4OA}P%c#7g$PT~F$N5|AqmRp?`*1Uft$CPbs#4*a zll9Wf>0}G>g3$=dHILHD-X<`~5j5c7y^VFtC721##|~); z={@^4vY`{(U)$ZE-xTeaZa7`CD~h_5J`oW=Yj^IE2M`O`Ll$TwnK z0nDinAhsBggqEX3$%mRgFZ0-Z9huDpw=z2tDV@wH;pq|CTe-x|9-1g?KkJd?yWx>@ zV??hdUIGWf(kh+tizA&O0hM zvA~D0FQDz}z;QID2_G5kltU6{d@E1Vf9E5M$E#g(9XPt;t4@*>B7Pr?iL>Q0aPS($ zS~k6MOe}<~4K(2_XG5gpxG)(cJVtEAEgGS&bi1AeO=`6*!=^UHv#Fu^-|sref!T3cpFKa`Aqtb4!T790Lel2Ed|O4i2%#=5y~`Wn|doPHR?SJdgm4e7ygoO~@jqT2ytpJ1+!gPm)Lq zjrOtqKE@j)NQyTx1WA>H2H;5w#n4R1k-xg}@0W~iqocl>JlI|}^7y5&%1MzX0}}MV(GT&=rUxl*a$yUPZk?W2!o^y^UB<^0ts0aCy&W z#+#ry)WQC-Oc$hp{6)+{J!R;|bqI87UJ}bC&%1(kSGnVVV@kjyYxfeovBeR>6W^** z%5=is1MX8|W>kq)pff?pp<*1}5ovwHZ}WFVPh+kLIBk$ww#AMCKqlrHf$l97RK6sm z`w=jeQ0@zYkgCvZpTvtm?&})#OW~pvpr9XBFeQ{nZdarOw<$O33ClAFA1A*dc2 zXh5Vq&xc;g$wam<0P-u5Y&P`0eAV$lplt0+cUN;fRRvmxVkf zdYhzvDjJ38aW4FM1$+ud?!fQ7csLL0NmYWr+V1KzW&@G9Duo4lsMt;nWCQa3ANtO# zT>xM}pT7YXtbmOo;Srf6to7H!AF*CI5@iw#lN{Qd*P zW+q^v=mW9So(MZ^A(>#P-zh;E>bN+&`cDzn9ucA01LMr^NhH2(cjT;u1w6%OFB`L( z6V_8Jeq&I!cZ_q}ZZM@<&Y5mNM(z)J`h?~Wa0Ubth?b!|3yIR%*(_ll=;t;g;$P?j z)SL2eok%2o1hG^FfWEkT@HjY&j3`e!Skc~vq*~HcYoRG7H8(mh&_r9Y{7CwAvD+RV zI;pnis0?Ojjo;!~3fs-y@eAwUA8_d=R$TGmz_a#F&Tf>&nzW19bL}j@4*DPfMq2*& z_UV1f!3+wS9WxrO3uh@pwG&B$SE4OQQY17jm6GnuAk*Kkg`@sfkYWE+b&zuuNTd#S z96NHx&cwdyHzgFQCRK)lmY>#i1~BUBSQwM7dD=R`mtXFKkoKl4twABeH4DzFg~&L; zPddfQhRhqD8#1g|5#Gc~2S7wD9;L54D}+T|Tc#J~E7RrX<(o>3urg!q{6W?THHb4{ zMZZwjz30Xa-wYDe19t2ZR>_!C(a_v_Gs^S=G`;I)Ka0N}8)p!lwUcb8r$aO^MjzP2 z2L5*s+jJ6drax-OQd^gTG59~Sqd~P(uzyjNlThV#9Ww3Y&*dpVbT<7;s6%R^(8_tq zeIdWAjCh;4W7@z}`z?2h#I~J!*M*E$66sbwHm79#AWkX;E@>6h_x;lbiBHD)xD+nYj z^*M5~p3LLXyGtNI!t&m$uAfyWh43k)f{pa;OMtK^MAhA~6h=3OO-bU0}l8M10XqSw6 z4?H4x+m%G5W=_=pF4SJeeA>$i&J*e7cJmN!2KW-eD(-Yl?1)_U`1$OBBl#b+Ftuqa z{}1_cEo04Nv80WtLJF~v0(EbM$PANjro`k5uWW@n(d-to<_`B}nzsZq;@pXT>t5^x zj44=1$+jNVgVdEWN*g&&rLGS()fF}NXmHs5G!<(|BfeF^T*>jI*3h8kRMnN}=vy(> zO4Hy(HnSIzH+IzqIoVyBtuyj6pF#-BgdtNUS|o2oWxo}2KiBF0LlY<9SM_Rb39>|r z_}SHg7@hz5@>E}^yBjfkjc1$gEK4*EZuU+8rhZJNy&U5kdSfzdZ0e%Ee30(DN13N! z$q=J2i4Ef93qClY!uU8@WVwkqe_RIU$)*o0o4;|oPIQg$mmK}=v%@GNpe4`}gbc0y z1PP2lW4p#fVQZ}P2{CgA3xd4B&BFE~JVXXNKh{wOvra;)3nwMj%Z;}@aRB{EAh^&1FD8n zcP}-z=@VM;lo@Ho87WC%yZ_i-s+uN#26Dqr2iboN3GovG-f0(+KK&z2ye7O(p!v#73e?k-{l*VRXbcC5?U13S~5{ zhr%QcpVX*gE*$3w1{}HwQdD>ZMKXxQd$PJ$S$UTTl;SU&SOY{)qV`wS9~ls5;X?V! zAz*kV6#(eSOTkquGesdV#d6F)W1IS7FGQKl!L~w>ThY_FvU<}`OIjs0Rjn0tk#S{) zNmDb0S#!TL1d9p@*v{vzj~Gu@KXh9rF|ol_qz}W>i<~Ld4F<6$dc7^>a!Ny!uusSV zzEme^rQQLU8dP1IyY{r74 ztQZ>PB$l?X1}CZfV=Zmnuo5jJB3NpSPr7Sf(t>-Zwr#Qx1Akaz!=n{F33}W#yD0Ny zQnnyTt*3S6&fzBK3*S$wf4YGT&K z%)$(SlX2yDW3(rAk@%nDVYW-B(G+{OO986h6)oB=KV9sjvLaUW=IXfKiEWUAN{&hO z{@@NZo$D@eL?46ww{_P4z8baCxOG6ND069KAHe1tkJF|g0G170rF*yR{-oZl>jbVA zhUW@DiWqZw{saFpxIzZgEB88KdsIXhM( zgbD?L>x#G^HD6D@{PuYaFo)`c`T8a?RP*S|YAqLlCvIJpb?W2hzFcq>23E*kBJm{+ z>OiT5DI>q1*#U1AaN)!cLih(a+=b>b^q-wksYI%TS;y?GA$xy?+135vnwG=KaE0R( zDi-mPURS&W^=X$*#PuCUAL}6}2gd{C;7ie^0(6~di*0t&loJH|9sFhTq%M=er~n7y zaO~h#ge*>i8uAWamgr=TG~ER8LG@b-T2S-x}U z=8=|e9T0iZGP%U^UV|=Yi{yGDx`@lLvTEgL{)OI!u2QHBw6+K+!@$`f=|?X}{|dLl zME27De6u{Y^d)sMaOSbfD!{`X0t%C7jFyKv{gA}XR_b1#Y*pL@Kz4HehhEg0YmMX% zO>l6a5IRzWcf8p58`Hjs5ES3mOC8Kz)fOi3)@(5)r4~=y6&Gz zZy#@WVfW?Hg$l4=D9PgXW#C5-Og}Kyo{T=F2o#~G2xCU+ ze)d@M>p(v&HC$+sfy2Fh03q>?>W&C0`u0^}B#3!e^y-kQj!=AnI-bWGzpLqESXOY4 zS)Ipc3PZu%wUKIDIVn7|$-~=gcL~)oP72D%nGG_|Hc13&c(OwAWbBu!z~{~rRK)<} z68KL52+3sKDamUU5cDBa9s4P)Amfp^hY>fF&y-__8Ay`}iEFm=xQ36YJ-~pxr5(L~ z<Tx+O(kOEZD1wc1DjQcy+(BRgy~ zqPf-xjAp`cw?X}Zdg_O_o;-gTI=B&CIJ$9F?3s#X@Y-+_Q#@h_M^euv??&Wz9;oCkZh3iMGQbm6WPCThWbl`V8BAFg5a~n47m0il3S5;Ab zJniW1tSK>9#zgl*3cf4XCDUWeL=3akYvhq||Dol&z_+aggb+-3pbBl!Jev%+b2(72IFdXW#Ies>N=ca?*EPYvFAA1FE=&w%ro&#QCefI_xcfp&VRCOOuFLe@2tYTyfGsGKZ^ zH`(!l=Qze_3)|9e)wluVt;Uv;07I;t1*e?VA7)X`rQB2!= z%RH^=3Mk_haWNd8_nRu9>8+cZjtWx3wT_W)TJNbm#*F-qlibiwVJ+uL zW}3E3<%nyrDJ}tmw2}`Uax3yYFBWlP_Y;UkQ3~-ClKWts*Ue!*6{;zS0bt`698*5t zB)@uyyqH7)|A_p<8?^g`BCw- z4Lu~U3~4~&Xj0Nz$NZJDlVvM|UqA~296nbD^Mj%h+z3?w?{SR_l|0c6wZ4O{$eLmL-X(H|hj5FiSX~6LEdjgXR!Pf)#qr2htB|4h z4)$MyPh*Qs_=IVA%^;)!TvBct=B~qMRp^MWVW>TRK7M6S5Q`K}8 zwSXqEJOce#Wgkf-XZh^v2+K|xX)m@Qt#lP$X}iR!>g`)dqAHd*g-RKk=?&qq zJn-F+Ig!mAZf*7JK#}`3W(y8X`JPtAx%N`O>xrbNI|UmQIo_BAbOSR8wlv2>!p;q; zymcG$@Eo_W37iklJ~H%nXn)1w$Y%f_0Q8Zuhec;L00@zvB2CN%l1mJ%uFL^@-%}Cu zN?381szZ5n!90C@1t!N+_4|G+{}c&VL{wuo_Xdj>AQGZ-4+ZM}1{8fG>EKwwc=iHX z7PQw6sz&(r`04_56g60Iu18$HiUWs=YanSv0MA11(Sb2cMS(yflx{aw+^$N!$s@I@ zOtrSk1D&1+%E_Cb8Ylx+hopv|4QK^pmJN7vAI8ss|91yg-{kq}zr(k3uRSUqFcrNR zU^EbTw;hm5)&NYAUfWZ}O_4z~bEkB`H1s?G$R0hQ6YN3&rhdR9rM<}jOO-!>K*E5> zA*0+3m|~LyNYBLl2AwC4Y-WJ8Om@~c9>9bT@xP_$dKwPE!yUE+b?I?<5C$O7_qMK4 zAy^T#Z2b~QwayerrY=4h(AtP;MAl&P_LlcWV^0pnGB77(@f!tJ^HyB&>Z~gi7vBTE z4grT?UBz)hV)K~*^~}gZ^$|)eo~+MaHL{~->KRw$(54J8%N^?na+vXlP|a#2R-x?P z1C#HNoxD+!ZBMXH4InA0w}?A5;3VNQ>Ht~I9u0&fNZOalYYUG0TK=k<87{sav|$&u zm|Bg8Ayo4mND9A+k8Md2>^P>A*rY59iLbmd(s`B;JtSV6NLl$M_H(wUqNl(c)HT(9 z`**@kujzf3;%@VWp=}9-aMv>Mp@MfDiZ(<2z8&& z=ZIj8XU!+3!XZLHwCw~#&AhP-)3LE<0AX;npD6l|FT{zGghQV+d5;|Q8lQb6hVxOP z*hB5a@cxCu+wtjYpdl6uiGooa{AUCyflkuvj0ngU13SYW&nO6|HeVIc$c2(PXT7>X zkwMCPh3D6>9QI2ggn3T=nlox}K^$eKEkjufG_9F8M_T!mw-s- z;{W(w!`atgXd@3GaN0t@2^gyZG9iAj)&SCvBJ}w&#kbBd@quRy01*b4D{!G~lArWH zMCZ80{}A6(w~fb5C-mZO7Bt<}KV)dh)8M9c{L>XTKvEk~MW$%=06mDl z!2V!XW&Ga@5D*8gcpNo%&pNSon2C&cSm9hG{#i>$3b@U1ph-A9X5+T}gg` zGAvCzessmejXSIYKQsw}29i@l__>l`{Wz5fXzt_aLy~rxS^Gd>Xj3998HHeT-qhPi zwUY2kQ#i$S{=Xw)bg(5m)#G8BlqyG^C4Wn$ z^S02tN*17XA$hljJRT}563{ICGp$G9z5V9=dyb$2oR;2oM?UFjvVZxGz& zEtg@x+HcBO_}=vC-9uM$K*hr1>AXp*&+r$TRA3YgPubD8hBo|YhB55LZ6=!L?us7` z@-*Ltq=vBXs3o|vcS-@v0=@#g0xxk@9aC|D3Ozy07~(^DcloxkfVF^CH~Rao2?JMB zHW`yhH4;}+N;1{UoC$YPBrcKaq+-701#2Gfy^qtiJqNVztrP5JVP~_XoZL!WwoEGpipsQ7 zoTn8xR^ZUljIh3a!?4fY|Gv8W-(SICuQ=CAbFoxYf)5VJVk33F%#lN8{_e6ZrA7DD z{qL)~|NT|HqFY-fK1>4JDv@*f;az+*8hGzz%oCXCVc|dNz&>mBd+cDQHMuF6%WA3d6CE?s@=mq9RZGtVWF)|VE<ekTV!&Xzd+)z%Z4Gd>PmchRJuEBuXZrV6SR{;Xb5xz0pq>3Pg=A0 zwcacX)&<{K?34a4U#0)cU(qlTx|PI^06;H7^%6HWk4HRLY6{gi4-kmQg(TMY3W=uD zNla?t!~lqr1x_x!)JLQfiNswjd1QoYJSQ6K7~~R9Dhj!9BoYK5#wJpZDzcw{Q|&N2GIUs9xem9Th!IL?$6#6Ol=%X1=I@Y9*&JiK1rWOguN%Mm$d{ zGV3Lt1Okv{Prl4|y#FnnaSMLkCVTsPm5~DJ=U-TA;Phx}W_mNdUQSL;$yMyXuPc?$ znk4x>-}QfztY5_=!9BfBYfa_*s4LMB0n#QR9()7<^z)V8QeU`0!%VgTtY zgyn-N>ZS!ewh}5W=8~x|Q1}^xi`>9NoKU|g4*I&lR4|*>R$x^tuqqbW z+pc7o>t&E>w`G;HO>;C0ABF6*)TJIPqK+FJJ0NlZL@O~;@_pY^o1$J1-Vr_sK~Nx~ zK0ADP_%QIn(H^#vVXT)yrdii2M^9m9Dm{m;qX|Wsjo@hivXWt~mqDi6*D7b5>gZ|B zEGk@$(W0j&cW2lx#f2T_6;&=@*m=wL~iroi;x z`|y%^nN&&^hw*CnZg@K$6hrLaV;jiQlZyBf35h6#=|v_kY$1g&l6D%^LY|Q@){S^V zo>7zV)mf_@e03quh}FIKw32VLb{Wfj&$myR@A;nZ*bS?ZOJK8CTryi{_rKwrC>Fc% zzx~aMp^&XVNLpUzPoT0p^HtfM`70VmHpgU`OSirtII&G#G;p+OOe0r2p~*P8xVX5k zJ$&~jV-jiT?n9Pq1OH4>T{IW9KW(+?!x%+Fp}4K57NIC^>#HbktFy!H-h0^`lV9)) z{K}Jvx=fjD$`n};Fv=8z^!lcAm;oUJgSa&Fp$?H7S#WlB%*+cB>A1v|jK+Mau0tS? zxe^fuJ*ntiA|Vmqo#pPx94{h?Mb?h|JJEo3Us~oZn*P2+A}*2YqaqVO;vFH6>X>GI z)N~}11oD{p3h(`m6UFu{v*y?FM6#yto?|kt`*8EUjk@vXtJpdOnYz*PyF>?^fw|tn$5AB`b$@ zeYkGi2?ER&&EcApMNz^f}ejk9}n+6OcC1DrQFv50&T%?a0rKn-r;=7HOR_ZK0=H=qk2^p8n^F2u#>8f#X&(ZJ#$cA~rWE zn_Jb(jY`fT>(Je%KTkT_RMv`DYWyBCf!<;xEl)^MJsFirPBvwM*usSuh7+6if+X$>#23?sri%1 z1{02(Z5llMNLeJ zNnwQi*hqPxfY7$3^4=-lTeY)E6_B9-125Ra)LrYdNob%{bkhKAQXL8?>Y`z-X$CRv zBDwB07}u7e^(M}gIT~TSbY-)d#9QJ@MkFyw24BLHicA?8G<~&uU_IDcgDOt=lU2#G zSS`!yiBx`g1Dg_<=y>$wNa1E#_E>sZ>ykm~cuV zBN;&(O{u*QK#@{OglVQyNo@E0?@VLP?#A4;v+UId?vzbIByKZfmiUYJK7@0%ea@~n z!6Ns+SY@kZhy1(}&0JPaPKtZtzU!;F?-HO?`jkpv-MV^y%LLZdlk!r6f8j}i-rUbp zV-3J~x}S7xdQ-8YK}x@Tjp`!vv}%XB$|kxpORXIF5IMS~Og>7)5ZQq;0gW*uMva&V zU~ItPsLiH2E<Xb|cXh1=M%9La%A<-n?Na<%CP*whDDRnCcYIAd5t z2Aldc(2UZlNp)&fHZ>}mniNf~%0mqW8PrRSiWsG%2G}qFhe>Tp6Hn2^RWms$i5Vm~ z%vCJ26w6%Y<1~|}K~5J5C*3 zQZ~^Ww1Z1ZzzhOv+%!74rvF5WVKP4sTeMP1+%5kO+b}OMd$sFTZ~gsv+w>y7WOwF$ z(?k57)`2gOIP2Oy!DN19_np6=IN=-aoM7!Ud$sYq_lA2bUO}Jd4FQYkD4 zcoYa60$To4uEfS|%JUg;l)Ziyvcxy5DI7mLiv@`jDV#AUiCWdb>1W&SSUH6pUvqln zNJR=^PVer?{tYyF^$pgs!L`%fBf6Y2jF!wgN64W$s|nv^^T=F^-Mf{Mv%sQGoUixz=nC~&S1 zri52O;yLiCOjvP!ATMvNgQcOt+v~d@2y@G}PedRfP}Qt_~y$+Yu;<%Ea3%|1xaYJ0(O~gg+3uNO;Ors>EvR;w)?}p;2+?I+_FV7L?FCZ zplQ}4Y`2aT0yKI%F}qCoEK2mti|W2_;VhB`=2{XJ%vU#TC=P>=i?xQ1JL&oe;OgPkQx)!dR;ytABsM@~R^0)xSa z)qu{2QV;{cZh-bbn@8nk#U3Lg71U#fPhwpZ;B-yLQ%@xEt~rAM$fOfd*btY%@KE5v z!X`=Az)v4KRKt?SP6uLF8_5#8fxY3Y#7O8x2WKgR)lsg z)gebkl8wTkOe?Z0F98~{v=*fR=+5swd=&yFxp_8fss*5hB>O zC*PGr)uTdBCL7{E_;pXIH5nvJ6x-#C&VJ(xTY0XCUrvk3pzKUtz9-V-PIvG8~9`jOUSaYm+cx4L6|ulcd?xdYDiNu3D6ca8UQ0`xpoLE4`-P zJf;!xUiX|KBPOX3?LKC;$S0WmA2@4Dwyj=*$mVQkdiZ!FBQ4C$D~bcn1=z6%iusx& zyfXBDy8(xa4SWuuZL%!#0=lvsIJ)(KnFigFMB21;N`Vu`_s8}%S}lO*ex2>Ud= z^vjY+C-@It)g;o{GEpaPm@4|HR4Ss*YD3&5dnk=dzS4R{;%!qQ!Sh3+uulpg9}o~1 z5|8{fV=egs84&nPY)L|}bD>jSAs(%HP$yu3sJ;hmI>26s|3c78Z3FJJL2AGu&VW&n z0l^TalF=-VQfxk)=n^)(b(;PofTQ|hNAEGc)i++M1M+n>eV~)*==X#>g?YHRA{p8g zmgEl-uiayyR{IUKlLfqLweYX+Q*g{H+i6*vDW1D&?m!e#!}Bjm-}z+F&aaqYeHQ~A$ljlHkExruLw^TMwe?pcqmS;vGk8i}runtMji-)1# zk*pE^P&o8HtcVRgw|H!g7y={3q03TcQO;5QtrW!Ktz>}{rpf#bPSY?rJ-1y;Kj_26 zDKpL%7LzLfP3TcY>dCN(kqIs3v@xh?V|>g0t^)8k+DXGiK21oVWJqY$7OFZOE%AQ> zQ`$DI9hNf@EUvMEFF&9CV~g*x-^z)wrNs*V#lzjh>7~s9+g~Uh7;Eusuvx36><^nU zuK5r8H0M{*a?tWKPgc0EiC;2^m?THp z6Jwo{hPFG{{26Qu-gC@D?AFr-i|kmdi15nU0+Pb0pR@89jhg}~XWe5;Wm{wMG$mJc zh_Gr33o&sPo6uHT?N3g70>D{LZnKuK!P$a6r`^6=B_~koPRn<>4eU*K0Grcb`Cc&r zxfFlJ@H}K?3%|O&j$P035cnyzA@Wno&5K z<3G!gLJ(M>EoS%128EJmBLyS-*r*I{NQ&s)tGK^2oNSgrOFaAaMOBZPW*M7#^@`UN z$_I84e=`Zbkkpi7LWoRKj9VF}GT^fiNi)k~LSwYp%}l!ODn*yUZ`T$x!QA>+G(F;^ z;ktNmKX%>#Eh_EN#?}4TKPJ@TzHkr0lycx*w&FW1nMKS;2IePT3FhlN@XdB&I;W{F zre9S6W3YvE8@2I}D5^xdW9*YsgLklBkPVSFjG>cFufT1 zJAy#k2GaZsaAVqsv?6h7_C*sH4h?sbs+HL)F_c$`?GqRwBN%Jv0mJMGj10K#9S@)s zXzM}NI$jYD;F%l%ZwA<)^!tb$BWu5HfWiA{fMse!sPA=vt<#5! z9!lC;QRNUKpo@!GFk?6|kiY;Tj%{@qr-?1222VkLh|VHv5T+MffJSvgVR-gF1M2I za3`A@dxj1GyOi}gn)C1kRG?*k1W-7DbD~#3w6N+zmag=c?h}@SPQ;dDp-U#kg6SG^ z!gPDLXxhyj<@0pe`s1gTp}K*SU-C%+CZ8fYkw@Ww@O<96`bp18rDQAng}@`W0jLkE zfdgRlF#x=206325i41x^Edm?>fHDk#Id|~5J^)}HOf-)|w;Vp<H1XOK9&x{*DlT#q=2A_eu>xY8=2bb)RZ3MKks0`#e4K zqPAn#|0#Y8|6bYM-QB%Y3RwzW3V8}MBAAI`r367%G`44v2}LzvF7FBnT6Um8V`xtp zs~Fl<3@xjsY1!gcmA>8|L{zvJ5EY56B}8>XYXMP}%swVWYyMC-;O|IAXm=Pv;kwKA zujt5x(oZPq$V8He6R!P;gpY_H2%@4R*`C5DNJ+$z5^Q(b-un?^=&db6rz(j$Rr%AY zN~BIzt}Z^mMaSymiH5ITzs)`GgRCXw);1ZD-Y?^haAhC&M@AhV0 z7F%Wq6LVbAfW|O zpue?ue47E;dv?2O%&So~8snh%p9Mi$SOwGlFn;QO7#Nf$DvM23ZidPpn6zTz6!UxbE_9jKYyh zFoHrD`Q$V_l3fdgSU=Psh^S!Bq4y-hq4%C;2S0Z(9y{B!$SD9O4Hh+dcf{T(4g92m z@u0CieKnLrmE6LMcL)7ObKqwVj7N^`iIeg?sgk+Ciq#7SepQk)==5>YoGMN!CJh!{ zTK*+0<3@|#-W~HBb*f5M-N4Tq7!N5X+q_kP28))wJ6>-zh-x=KQLuo*g(PQAQ;11( zc@yahQ!Oq}e}D0J{1{wfZ?@R>-V>iz`@`R(@}jyU>GJLfHALpYC_G8Gf=lHQkaeSt zJnFcyhN*EU9Y^9(K|P416Y9kmFmNM18O(8_dZ!1Mauf2-g$rb|=%UVpwF#diAtLWT z-L62H#@G06aTVxuyOze&x2NA02pgR~{(FU~6P|{)Qke8~Y=$Da`dR zY3z@NZ=-FGlFS7pOjHk`ErLz$W<@YM;NC9|Y4V{>#yR(-f|Rlonv+E=|4 zXV$*zr?sz^#Ne5+p9Iy)%vt8tpy#L$l?)6$I$hu^7dR^ghSoG;mQ2eR?P;l}Qa&jm zqnean^}n$Dd++Z}HM>!NT@uC6jV{;$~;I^?_PR6a*%4GT+W#Rb zOwG$o=h)?k$Tt^>klQSZt3{VoJd12tAY7nRIb^J$Sb%cD$yvPd8}Xw?2AUa`Zv>iD zMdX}Om{)k4EjM^>M6hysVYSR;Y{=kvVKUM1#r!C_MMY@Q+AO-{gpoaAWLYaRsNR%f zir5grF_T3_WYMM@E*Y*GFBr(r5{S7vOEhsMKwu#7ohc>Jnd=9Tji05Fj2#40N}@h6 zSxOIZ$Z`CD4@{2}Rlh1X@5s$rM-`lyBD2=Je3?OCuJm2D%$_wT$t?OVh0c{3G_KM7 zODaS2YYF>Gabi4dR=XHfSA+VhdSGJEmm0K$Gv_4+#ra{1TnAr}sDrWYKnPsue zpfl&&Y_ZXTgEot{%ib=^E?*fhw5*vFVo-=#fu%`8(;A{W!A0Yx3>Hz!x~1-LhNk zj@>Ykmv8Y$xLlL@C;#FfJ1bT~>eU*%ujSm2Ef*#>h)DbZm{PQhf*a*MI zAL6}Tplf_@C?lUz!>;NL91i)I-|haL1A@=_9ne_2{f?y(S?uE19-a8?Nnm1hVo>Da%7cq$#G zD7}~~!S>qaMrZ9F4$Zpva(wIF(^2=;c6XhUs`UH2%RAx_N9Mt!jtga8-INRlo9Y-{ zQ{5rQk;S{tai<{>Dj&WWO36qZi4T|RA`>+}461i4Jslx#G!BVFEA`AuPe*B`o;g~n zcRxF&Ebmr&I5aD~m0pf-g}2hvS>dg${Ji%+lCJ%M?dz98T9&Dq^I<>D`LNE4$^2rX z%KZG88CIDek=YD>_LCCR;d?$M21uE&Qs%4}?P(!Dn1$E?g96M7Dov#c${I8n4d$a) z@95Q8FBV=dz%0NneCNoEB?2=>0b-O8!`_wtr4fxKQ-Mtm*2<>2s?wwPekLy-1E7>A zb)~rF@2Y)7A}It+wtuUws^FY{zi<3^+v2Y%V%EOwr?oH3wA`S6S%}SgnV&*bLNQpZ z47?a0BS?_BqDLgm<{=W~H!8wHMPgAS5@2))5?*r%6C4W`^$ao}nka?u9;FwXjyX;Y zG9B9;#^0y@u6&Yrx7~55V3Z3ZX|tPUA&bFomc`3O-z*Ebm2Z|sTOxs-?gm{Lg(IDY zuj+)qsOuchhfYNiJYld%#P2%lsNUuH5#oCEppRl zvKZ_Ivv48d_nuk!lo>gLkc-$D8G|TVKNHqLtO7%F2P#RMnj4#&UTg@-Z+lzy7jfCL z`LWnm?6>sR>#wiBdg<%0F=OA;!Q_n!|Ja!f>FZ}rBiMUGuBH*K3kK{{yU{CLOK0Lg!rJdiG2Ib@qvAQn4e8f z6$YIq2^}R#B6C^xZf{MJuveybvU_m;(tjo@smLRN`XnKW;7RK$f#5I@Q*ji+b-e=p57 zasKyHoc~K)IORbi>zeq~VsK4-ZW9Qa?eGDQpxM=p%ax7;aBSe<@G*66*fG3_;3Wo` z^Ty>WXa>D;!OB_4U0`zBEMIgKh{{KXet31~L+Pm7`wLrtJSPPkQ7{}uBHqHUT_tpqI1=jH^W^2yL1?8!@cx|lDy2JbWCwX=^p z7$q{#gHZU=jdJx?ZQShntDpA#^=Ab895F+U<}0H)OK64$#dg5v8ax#bGDf4npwqoK z;CWGrM-6}QZq%ta5Rpvt-r6rO^={>fX{aYv@Ni>MPYSIlkwaWXk5aXLE{DxE|IyJ` zZ9KDy3gc6g>0n`eZXn>9clh9nmvU9K03sPgHGGyuG<0CV;Q$T{DotL>70(RirC^PK z!U!v@q(s*x%L|&LJ%vpW%-KupHU9vj(#g;n*t5ll7xC}w3qhT#)a_gZb@foaJK8u> z$6-1okJ@iyeL4BP9Wr}<>!&@xrExG(RoJJ>eC5(qnXg7@Zz4oR=5bRM8jFWHwDe(t zhQM+a0!x+-3l9;jZdjl`R>!IkEQD|Yh9x@V;xXgOYEjcz9+*SR4>@R0?y4u3m6PKU zwBnG1`mC6$c39YP;UyQ9aqWF37w@ zWoTG3v@D!C0x=5l3b+v_w7sH0R4+2r1@=VIoS84VhJWjHExQwgQmc@N51ZV* zwReEE&xe9m?$kaXidng1R09EJCC}e^h>m_m8Ljh*R=%i`+6yiOtaNn{Xt4E}1F8Yc#siXy!KD@m4`%I$CU zP0QUe*t=pw3lyXQ8;CZq`+7D#AC0f)Uqf&{JKGZZF1)4kzq=*#FmToA*kZZYZ1c=J zeB-t@iGTZ@Je)C6zf9H5X=~o-=nxf&6NL;M$&ViR*)#JjvNBQC13zonbBftAU08mQ zyjVg^7$m%2trjU*wOuDP`+@8Ijk#7PWWDXg`K*`LnmC{JQ=HHGAtiAPziyis*_vhX z`8gdciw~~V;aeYdCIdfZU{^U#AKWtl9(eiyuF9y{s!|s4YvWR}(xuzmue;mdFMw-I zk?;Dc$ahV6*k_xAhkbTf(v)Bcz-gzTQc|KT!L$peC&%pR8F6j-8PwF7F{{0B^!;6; z636iCwy@bp3Cu_C-I05of=e$fyTW)EMltDV z<%_1|y0dmp^|x?@by~uVcT7~FT<6Cq6Lkk0ml`6Og1Y0dhN<=7(h(=t$m6h<<(+Wm zMN_gyIwh5QQ40U5b;qO+FAp~5L;M3tm>k2e+p@R6t#_{0dM-p@p#n3AfelX2cf+CQ zVF<~`(Ob2@e&Ck-Vc@Oa|FJFDZ(+anQrK@{R3ft|jLJ5P%KUSr7KKsWX#NiksZCT) zQgA#OSz7yQZL9X*zhZ0lZQEAPhfB6G7Z11OylfWQ>-sYFo+nT6H>I4Sl@ z^7BV#SV?|xJ39Q15AH;V-|->w8L7p*Je`HqV!k$gxz;v%6x)wXII3}Td0EKI#lBl5 zoH!d0o$RYUpglZ9j@Nce=Df|KHuNsi-e~Cz8f3f6w^>^Ty?uAqZg243lOTrHb}4oI znAGXq8{&Pnf2uts?{ZwICtoZDYdp{K7d4*LfAy|rKmurR917)~Udz-kin&f#*D|$k zC+iucUTkW@b;u*W1XJgDu=W^8!uDV9H@DtytCIq^_&GsZ*CU-G|HMm$O_6`%ry~Ev zb$3=BVV@(j){e!}oj{Y$6ey%v;iuJs#X@19LMx;bg}p*yvu3SF5h6t8x?VV`3`bQ= z;HV1x6v0%%tjSRkI7)+~G90zx=nEY607qr9m!+`QuhN?MuTq|qX982snihmj6xXvj zBZet^X}xGU0)T;9l|AxINdd@NqCyZeW=WfYfS~G;dG^4bJ+Q177&JsPh6%C|6^KCQ zg3J4B>q8+_RJ*$wFn}K0^53C~O0XU@zPoDwcwqdTxG7lxAXegfk~j=D>GC2EQV$}J z_&rnd$V!|CH^TVMN)`b4upe-)vU56>S|WWlPQJ4CZsyYmxe_3PM7CFqFn26r1w4nNfF!UYBy|l&u-Z@x_0w? z!)^eUKxw~~#?vkEb-V7?7}I$7_H=uDx>Z@D(8ZE`Tsw_hAUhm(_DV{W=h;X zI6o!%I2#C`eNB}7j1)+es^#az9#QaD||N9{} zx`T=8gcEffnGc`pq_T$6gHce$;?GfiBnInzh}TicdZy?$p#%B^E#W6>d?+j;lRh)J zJY&1Sqk=~|eoQJ!1%)emF6tQB!ZdA_{9TmaUE}WW}cuqs!2rlYlGMEqXI>&Q-X)1}M&5*WR7K6Q9^eo3npVGdd z1|&IvqJqE!8Xt!nUysJugNJ+-v8|a(g|)HNB_%gw##(P8{UJUKZ~02LU|*)UWPe@P zm$c@qu`Qb4*0x^%wQb2h+#B*kd=cA_oe#rXJV7$qj1=~1@YW6sW=Q%8B?;X~N=ZjS zFFfGJ3>gTZ8i15h#Rwt}H7_PZa1Q&-nV&edCyP~36|s4OIirEN%m0BTN6yM~sf6ZT zsVn0T&HgG)=HPjGIyFsZQ5&m~Di2MPS@iYZpan-^6qC|1r(H;}Hg)fPP(S8ET@jfK z>S{{wy$5s~2?#_GO#Q^fQ{#FqgQM=)od?D$5Wd^{z2y-ZBA z*VX7~dO0uU@rV;~efUD>^XX`MIWVPaJ0gq8iF7{M3WYSiAv-1?R0E>^-kxe z)DZ@gCEDF)XS2)M<}O5ihtXKws@9cE^n{knBD?L2s40Q7Xrc z#dpt*QpH`Xlx?4TcOK|-?5&PajjL)hLN)%|=nV4TDb31LnuB2&?B$|sHfB?G<6I$& zKyH+By;z^GSyF*ol}%LkfTU7xl$vQ6lj>^w9Q$H>+96!2Ak-UuP3M+h=cB-0ktVwZQU2U6?`DV8M^g9ZUn zjo7>^{VHU>owCq9kRE7wsIensr$&lcj}x)06R|82F{qKqj#^A>h|0u_m`h6oH*%vC zjK}VDH3Ap{qQtgP4F3qa-1ueG#ZGfDnSZ&a=CGna=doRlSJNom1OIO z&6=;Oep>U@k`ijx$3A`YGpQ=Z3=z488Uf$_c8eJu$3@h;pbtuRcf-<nAVY0RP|YdqAr||kcdRiIU%cpsL%QLK`iLWs zx=>H)d*YgC?ZGB7U;p*f*MF0+l*9sr4fxFm^3;n_0m_pg$h)6AIRQxyYs*u9jlmAdQ@S8 zZ&=)3(C3+;?FG4N6R362>~Ha3r9&pzM54F&dm$w;4-;ZT5&JBfu|Tw3=}wTbA%la3 zIir0oL`CLPrXgn3m?k7q6&+9kg9;dutb8R{N*0tZ9Gx*J#H^5O<};AYCmByQW-lP? z(gyZdAv7mf$Ep@7tUzJK3b~UgjszqkAc=vN!=`D2c~{8L62%F$=7@;ajRyO{#B`S^ z@D+~A(*^`3tbA6#42V!wq6$d?S(OOR8U=jL6_sd>_OV05r-w=k5Fo0Y`THO^vA;@d zRzfHMT_}k~fhqvXkds5l z5WK5ljwumL#UMfs`U4<100IUdtCSti|BiEH`yQPf9M+EcdofpcoeoyIdx~GtZ30Lr zX-0(45m8anOi)o$(u`1&3Efc<&eehle-ayg3l(_VVFZ-G3GZDX-^iJPau1(@vrz@g zM(;u7#5W8ao`{V&?@3JQK;3)x1zXdC1+qYv_5R8GM)>|C-c@aq{gkIl%F>+v5MKnv z$mLb(uut3!HNn)8p;NFa=(;2#l49Qbgb>j@8K+x&qoH=irK@bQweVRI4 zj~%BC1WGi0%?=AF!MvFv!tyn1{iTQ*Hq05V)kBEtmZ2qJJ6>`P0#NaKHGS6A!Fh2g z??OODK%t-+A*2+)8KDtAM=_;qAGwtEjHP15w>X zfHald-G%Az!@uiErnR+Id*|;f{tgNCva{l)7S=O4j>Ly*q^`D6vc3k(3GZY4fBTda z7U=y6qg8FO0@Zq@#j2y;ERZ#>C#YJe#(&>IT9%{<`!p}5F=3zPr?5{esaa@F42mPN zC{x7EC3I{_<#t?daHNs{-FPDxH!i?kk)>iKQV z*2)h!JzvkmLwNtoTP|OPw_N^*w_5&phtPN%_{bh`U~(K>AlN5}8A>$UA)B>O<{c<= zmdRX0%+Y8JD%t4l&Znz8=s58ysLXwIT9%19{Y9U(#dz;sE_#+t)E7(D$_$Nh=$Fj1 zY4Uh37d^{y4Jx_Q5Z8-MFA<_j=vd_r=he3fpAqG1`&`>}`KqeY!aiGOD9h{#Z00a! zv(^muf(UpC9ji2PadA0mVPT&vGc*T-s=%;bvla=RiDp+bm{yJEqkYwvOGhP->xYpe@L_HS z@WG7=)W-lHj+q!@L=Y20t$CwzMKpVQDxZUz5(!uZBdCR1^-xIaj& znQ|CW!6+S128D75ry-FzT*8Mso-CmYs*gbV6H^jSd}@IP6*}+z29h#*mSgxueq!t% zVWl_lLJp2aB4}XKqq*t%d|vH=O+^(V^09o(S@k#5+8?R?5ihO%k=|F`Ef-afEG0hj zK2VepVRK_0HD>dchD2UIT;jo$v2wLnoX{^h!HN@_nK6N%2|A%6n=pe#Y3=tyP|VL0 zLs&sFS3xpaI~cI(E1S-`xxCIrDkJkEQ8R*O$z`jNfMznA_xGdv z?>}M*-MS;zJ0b2=I}M4!sC!5HU3ni_ca*$zmL*lz1MraS)*ElAT7Lt3HvI33i~QB*vmza%&>{7 z93H2FQSbh3c1R8j&0&t9%0a;bvdW?O@Uc)3%ONV?4J)bYWW>8u4((^Bz69^RkA}mK zD{UXeJUw-(`FSccpyU>f5dmZ~1`Qf4Iui52qk;%dofMGG8WnD|A3;l?WeW|fg_d=r z0Wn&R)VwQ=8S(8j1OrfT(HL3}p)-UNn3^Xvt(9rtua3s6Tx^|?y=Rv4CAL8uS*4^-jFC@F$Y2!iR0qxIY7okoeE$|+C1&w*(MZ?b-oAxTalRw0G;lx(l7RGR zXl!tBdNwsRy?Ti3yKGyq?^o$d>!L_wf0nmszB;n?eSZ8uY{C8sy*2ym?5|!r`>QX+ z5YqqdmWq!7ZHg+!iW2rqiiigix;H<0B3C?-EEP9sw3ZDaz*#IgPMR{!5qQT4oVBvj zn;5#A1p9#0D!ssSCE5P%-wp}Ilq5be9dRceuanA|9IttlyMMRslr_NXny4?(cWGE( zps&)fzCf_f4C@Ot%ghkN{LHr*yv&a+1Rd?|$A+TAdmm$iX{p4#EaV3}*K_B};=O!4 zgv0`bvbc4nJJ%KO<<}uBG**inAY_23@uH(ERy!Auoh!@9xi)oR9zj-i4I1PmVS@w? zA{#_Fes0Vr%r#_KD}H~}P{PrBKL;Si{W}s;LL%`qN)ifEdJTo|Q6CC%s9+Sl&-T%a zZ~u1rvF6qJu*SXjK*`t5xieRFU7w-?)ero!t3ovX8!xgc`z|jPGG*W8r?T(zLrQ3R zmh+ni5u-ytmS&HPo**id8Co)H$#{}IHK}`!8;dM#|Bmpaj^vq@L#}<&wrvV~v$s$29%l9p z`Pc`3V|@SbBFKMjnz29oY3$D_y!SLc%drYn1PdZu0CM7>fU`nPv*ahw-(KyVjns=x zQAHz|T6fg(`rAJD6QpHnXuj&DL`<5m`YFvf$enH^+ws1ZZ+?^H(n2wJw#o^*3gVW_#PaHB<6W~`Mt>!#^0?HLS$s7_{R z0HTU|lhROM!e7AVsbLeY7oS*`wRyZ9BNF-aDq*E+^LSE0AAi5c>-_E!ClR-M*Z)l^AW=lA(>>;gi^k4cuOCza%# z#a)NKih^|=I?0{D=Y(*v2{Kr8)_yMpW`4eiT)l{7t+-Khd7cm(V$zhDhfROr9zHZI zI0x&*$ys3r`@t@AGCWxc!Ja};-RWvh$(X7^gGJ$G4Grp}UHNEPGBl`K6{4AABL~Ng z7G0P23;Oc9dU<83yr4_XWRzXTP=E~d*rG&KGcn$kw#++P=B$_S-0s34u20Q6e$<0VPQxad?Y*B_?Rj7EcX)Tohw%Pi$k8<+lkE+= zTIY$VaUI8r^^Q{$)2>J<+q;+h+klZziSPJVmBObWxj*l17_IRv8{swnx#0MPjVDMg zu9ku|dY^$IAN#pRzsoWD@z|d)h0xFY*N?q(zi;$BpYyx*=TmSVxY9w|SjU9@6F-Ih z6F-EeXF0a*5hci8wmrLISjF`cn_V%@>Z6@_?`e9L;};5o!_y>12_{npL(rN(LDpLnP6o3?Lazc7Ep-hCaB!|%yM^VTC;tVjx z5Hcb%L^A*u0Ata|UGfjddH@V~4+w8JMcqkwpdMy`vUFioJ-GjFdRTK(x?_dSV9DH| zs%A$#wQB%bqwiFZcCze&P$J>C8JY~lwQopR;jMT1F8^=C3ss0bD1Jhn&Q60Z7OSDc zMnWAE&5{k+hnGIAq;(X%$Y{5}VxP>!Fx6~3>g;g^me?N>trXsy8!}Q=QU<#50X%-3 zNbC&c%IZPw4t#^=Q?Tvkw6-r=AgRFCSbr!(4{<_{uVPrNs((dkV6qOjMqPd*+(eiF z{iO1_lXDG|44qi;c?NDRI!2?!VeeHxIU{g=1QOn)@Ej({C9VT4$aYVc>IR6tv zvTQ;WyxRMURx$|^v203bTF=2%*pR5RwsDS=UExa6#JmKyjj+(zk>pWTc2gK{OWq^5@kN^W$x-3J zQ*bBwJrz5sa5o4Rt|3A3kOX`Ct@77IRydyNA*Uy(+B5aQeHn|6lDcuF57v8PKD7cO zRVJ+K)!B?<#K;ng=%8rJZ5aX1PHd;n~tg}0HYC*4H{bi_!=m2kVVY2RUp$sj?ivjE%hLA`y4(*5!SwHX#0R(p2!QhTmN-2C)9@o&*FI|N2(vN~And5?k`54-BsGYM zs5mCKNwbxAv$}dk3y`&@df>?OjZ^=*o7O$vunlxkTGH$_(9MKnpuK{hxHxRT+OVy4 z@N;6~nMC?CgWmIP(O9D5n#K8+ppCO(>$)GuxzFtq$CLnrs_ zmahmz3?k1>C3X-~Y+OWBSq5zRPE>_91Q3ZVMm(3EgH^^;_kGG>20LpGDoKnFEG=ja zx*qDA7?X3l!HCV*3akZoDmWHXv6Fj3vpUo+(Q{t&Copva+#p^(h=@KzJ0zX5IaNay z0F;^WTQv)D&?>Jy2VWWBm;_q4o?Q!t?j!4X66%EY)v!7cm1fiCy!bPD276C`x-BY& z^!{9rfQ4Ay+7lo8PGl~5nj+QV;IiHK z2*L`qL>6#fBA*Rx+tc7G10W%664{s>+tL>VZlN7xbrW1!WLBGSYvvnmGDq#EPE-y) z8?b_rrg)8H$I}L+b5DElJvj+?0Rk?pBCsSflU}n%l8{=5^N4^VFGBr!Fp}4X)BFy! zA4poCpoB=|$O<7TL9i{0=tKb^be{d-Ngt4^X1NkI+d*#Cr1DT6ftse|C@p)?YQ;@l3L{2W{#V=3z(E(VHBx1qj37xlYz4_0z`p+`aU zVSaXnqVozJT++oo>%Q~!4Ls+gbtbo2L0$`i_3{QRUjcpEVn+GKF`4O?TkE*stE3IM z#qR-^)Y9o+zG2#zllF!#M#lkka=pP3E?gN}f#u0O+;J8Ia1%QL6 zcvkWnoBgSK{oIC;YgoRZVB@rZDB!4p_HRvYVH*_RnnM6Z^U4Cy2|yfdl;ms~S=RtU z&H~ghl$?gpX_|W;AM*!>WE=widd;Ym`wT|&Z*H|tJJtb8?3{eWGDGHwP|Qx5v`0MLlBWpX*|ZUziQ)(U$CHXlF^fC9fh+5Y!F$s>$X)kX9nmNKM#a^@k5K2( z0I($r{dvG`xy1mi``D9m0jX70C)#OSYz$S&MJIDYLV~!tpoLsxf)gX6;F*y4XkH6n zP;Nw281?$YUs-M25K0lVbGSwF<%-FvW|hunb*A4pRV6^(VimZsrwM<;R=#iP(3J=A z_SEkgu6B2y!IQb`oHF_!Wzd@eY5L*|UffPx-iF+lSHR4PV?{en#c3%@Vy(R8WFLG| z;g%tpBG_YK2$1-=f(Xp_7S3&ry)PLSd);IN8ZWKk{MKy+XX!+6NK<0)Gi$HYGieXE zzBANC_>%4|s`0CUf}slW#9_q$rD=#9JGQXfJ8zJuig2Gx*4h%_Qz*WT|-jOpjfud)9NCGI*0U zvbdT&7Wz~Sq2;@AeU^)2G?=Uwxa`eGNy~k~I^@u&AqTv?0UEnm<^Gf0kr47xs%tnc zIHL2Pr8FUj5B4{Xo}u3Rt&_wCr_d>^7`YxwY5)BckBzPh#)zF9kwy+VoOP3Wz3U|nliRlV z=jQlLNhjDLi%qrweK>L!p02B;Clb9PDEyXCJpod%7%^_wmYQnAO`{zj~w={ zx~c!A%aVnX;0*HZW$tl+O{T55O;EvSQjQO!#IiDqMmp>;~IU;#@StW?;^$`?npB+Pnjezw??Ie2w>xY z_91=Tq>~jnkeF7GYgP{$Govo(;%-iYjdR$h3nYi?q`d^i+t7W-FCju~O{5yCzC#Tfv`ho_T!$ z`oFrmqYv0fT;X*lu%fpNV}ZRv?;DfsJAfb7aH?C+&X?&J%>+wn&dZ*|T^zjyw!_cY zivp&DHeRs8%fR$E0H!d2n`MB*7GZ$yE5IChqgpoL0(&mW8L%f?z%Fuu4GhgGxJV4m zdu1B{)*CA|0FXFfz4L_*4*$1Z=A#`!2d*7y$>;Ql} z0uLHOgYFzz^Z;st&0lfEAPw1u!fe*xGuD;YW+EDsRK+<$*{$7!v&RVN+hH}G3iFQG z#DI{ws8)2O?bK4~cN=6Ie{_1;G0ZUOWqKQgZz?TxP7;ggb`I{j29(2L){Q^5J31@g zfY~Ww`#mK)y-KM5XJwb#YuLHHDriNa{YmaX_E}`U+ie$UYEXp|9h3EEo`o8AT)c2z z+Ky#TYrj<3uovN?;8*8m{@-BrfvbT2z--S+qgwM#srhMlNg0F zPq@j}*9`A2ql8ifaRBJJg-NAiNI_5$cFw=NVt(zXqOt##1%g%QB?# z5j-D9Ev6}+-gWhZ<~B6KmN0*7bo?-Z>(bSNt7beY809`Au;;*0HcxrtWjK*pzjHS} zqwv%1NK#F4yLpMRryT-d$C(=RFIR2E?&TEAe;Og^Xp+Fn^g}JmCfJj-pYsO;^4%j2 zMIQjFD~bCH-hD;F2%I_RHJlDN?J3rrZ>alAQtZMmWGJWjW|1s75|n7gl+d`Osk`Qc ztjbmqUDS2C8&0ZDes}Qx8UDD0*)N#99wx=`E@InK-C07>0K^_H-Sr6bZiZ%lx{(*w z(!C}@xDZ@c!8I@e*Kxcmk1qyxps#JgT7wF&kr^HThZ)Wp?q|lhX_#F&CMsTyXIL&N zXTd1j&M@{`FTs~(JH{-lgOxM}98sDXvW#Y2-t52hh5OT+HLWIg_(*)=5I)mvuXO!6 z=*a{pmw8uOf@1Kgnj_bqfRg9bb)8u?=-1{kG{*_!zxt>g#1cky5C45yDT{usT-8b^ zi-hG|iFH0d{w_#?D-f$v0y3LGbR>p<<}mH?m?=0vxJ*B+Z7VE)gv=GYD0=P9k3lrZ zK>qk3&3Bp~&id{Z$UB_|st#^Pds4d^!{gvrR7q9ChU%Aoq{GlF*!jVNpvQ4@ZTVW9y`(Le!9~L2l70;jNcP+%kwGK`7`qbn5!8~tD-CPHTf|&=R4PbA7l2C@GfgzcWPN2 z2>lLKZ&?8;u%=#r6Rp(s=Xd2h`!>&5*%fepNls@~aTqmK$AlwY87#@c>kUU{1L;Fu z%k+E%IJ8}BeL)7S=~7NJ5TSluoHz;Ed{J{7Lo7_#BJB=Pa>JaWnUg$8RBsj`R~4$m zrVqlZLWwIyI|}KNpFZL%KDuH&L+U-SGKnx)LrWy+&2COzAjD zmEIzjxsJYIKkT--^j~WD>*iY6587*w8m4YTnTIHB_RUtd?zw{!TS?*L{wnM`GiTUc zY*Hpt*}9<0)V_}X7V^wy0>vw?i5>{|m-~%fD7YsVS1dBjTX}u|YT_p^Z0_i99^f@b z-!NC?y0enL@)nGfpxg2?2t!t}wuSB);vz>@41o>oGErwIIPtO_+=20}_IVVU6!0pi zHPceRZ-4z5nMrYr>!Z*-S@f^fcR0DlidcLf|13>{Ct7k1IvF}nDQwRauuLX%5t-81 zDv)wc9#EM)8Zlk0Sb+Zezgcgu;MkUWqpJqj;fTuk2*wUoQM#mjU6hc;_Ys>0%4{f~ zB5GXyL0V_zMGAt(kkUmGBhOcuI)qMR%Yp`cfsn3CqnpLzSptDnHdUkb4z9vJBcI_| z3t9_}=0^afbD&%hcT`Pk(>!jeocFz*B@GdKEi6~}=lmbDnBdVTVbJKH>-7{KRuAcD zQ*Z$jhAFt~%xSC8L*nhdZ<4F{JCTSY47_m?>V!Zx;0FahfFli5AFWS{4|6|PG2Fv&Eo zQs>XyxnR7P``WX1@ey&sH4(rh($q~Zc*q*jkVGd~!8qmTwpby|X%McPQVp@70tACY zsz5=3d4u%67Z!fd9u*?*8_kO`@5l}c{RZzO7_Ao%Bf%*$9E9Ky{PVJpr-qTUO zNH9?hn<|3aFOO{lIEt461aYYm&d{-VLJ&)yX-IO2G4>?zhzN|$cC3@uEev1QQ3JB( z88JF_CRJx(4uQDtA5h695uubYDCCL9klGkjpc)w(c+cA)7=!Tf&`g|v*!~togV(<; zV|ycx=FggNd&*Ba*<=nW9EOe1Qeg&}e2QxDi;qIB%0ua?!9D>qaV8O-|0@)ZkZffi zHy3P|;)tzYd77ZiOjz`r3A`q(yBUoUDU2KuRQKB&;++t*233prJAqii6|xv?BVna2 z*0##6H7;!+bj})W_Qms&5cE-~cz*k~o#84GmM&t4ItO3je`C?pvyA~93+lFGV7@oT zFqoz};VV6~`P(83LvO-5BLxrRro#-xJ!H7NYtv#Cf79k;Q8nM_gOJ~~+JQ}{wa~$w zV15-aHy40~gu>ZMvWG6Ah**f7uEFcU?Mx#Jh(frse3OQ~)P)!Ld{S#W7PAX;#g^3Eg`oy?TCn_=Bp0>7ePBH?(pMy-vVPTVZ05OFzwniICKua zW2Nv6ScrNMv5FEbu4ZZqB|q$NA`rN)LcfB$#jZQp?yZ>A zmFsyK65z0XYqD_#&W2RPp%!fsQ7iWH=o+cHBQI8kmg8OgWUEh=z+#xxf`ms#7acOO z(Hw*Or2Rgt1|Pu}kdUC;mBd?;(rqc=lWq-gl8DR?fH+6@R9LI!g+1zu02<|#Sk`lJR;P-!ao&#FE4^eVJ z34g$BDJ1M~fbMgz2)mVj-mI0R?DxnfD^e3%htpg{k&S4c3okYN@*{D|P#_)OO(z&a zszGHHj+2xjNzjP9<4Yt<_YLUT3hqHo!r`wkK+y4o*NnN33yQU zr|(yd+DFg>f1dA8d#{JqB~g-65Ef2q^?(_PvrJOhX}0P|7)L{*k0)RNzEi>C)oq-V zOuBueo{tWU!Wu18F!hCqa>;eqei#<3uilJ#epE~74MGNMhI;REGwOX!D2*gNu7Rvz z+rInk$RLpKFJ+xn<%-d>U+xblz@Mc z`cu&fSE#EbWB^;Fgh|nydb;`4DyNOx$*XmHaR<7G?T>;zV=!ejz@Ja_lM&U$m_egt zLsM|tVKK?AzAb_ z-yXnm*DrkjUQEshg7&7sV$VP`*8`&$XxA+}^J(|558 zj!2DU%$c#{ZV7~}#1vdN3l1Vmjt%`-%Rm_fo(4*YZwJ9Sk;Xcemk?%y&cEwPVD*gl ztZY%pXRC*5QwYva`nWwkb-krnpmq!r`0Rhb`Cvi(;W3zKsoYFvE=-7A8YX)>`KWUy zU5ON}uf#$#XL!72Fx@ge7H& zMLzfX?elEGo)3fkl4m1UL*QGE*nH6I^88e>nr7 zM?`>j7|1TJKysu&D}i2c3&lF|xmv4H(1hrWveP8@PJ~V+o()F6J8OEfm*Pa{_jw6tE<7egXF=g>l4c0#0LTQNr+E!6JQ2v6pb8!-LYKdA|1I(sgp6N*lEf7pMq1B!*0S z$Ekd~I~+vT4>lBx8l#ve$bFZL(i$q&E|K<_;l?;}bV?}bg+PPbr?jXX_u({C`*q8%#Ht5X?Uox+j{L1 zmqn>U26THoPYd&UqnxqQB*Iv_^X2fDiCGoq=+k^2X3r;L68Jvj_}p&mrQ2l~?19GC zEX}8+lXfwf5EjbLAws9!@wB(iE4%!>y$l!2EO#UX$uBEmuglrq%pnuzcXa2OJpVGS zD)2w*Y%2xsrPyd~6>Yps0 zmpxD%V6L)!k=U@^Y(VyU9xn_y-VyMg9hg2WB7T7vUcR&qh>P9_>>mh}X$cH&SRwF$ zD79HfXqkKW)7EJXuSx}FlZGx2h>{bJ{R?ksOx*6@^yV`Twmb}j8=`k(dYCV|hB-Iq z!sM=smUoUrS#M#&zts(=2a_PXxy(1)KN~wUb^pJ@(xJ*dXc&brW|eowM2ETReZpRS z;oZfo`w8(5-1|N}1a=I47yF~>Gt4&%>!~xkn8UM!wG6K-| z3R}kt1>pOdl)xG&yT9Lp(;*Vxk78uHFaxVxm~IrpnwCe@#;`j#kFl ze+^jiGoCV1jB*XQoyI60cxv5z5fc4cF6zS|w#Me1U$_l$>)^J;{Po!fWl2XFaLoMj zHO*_&-;b}V(-KCDod;oF^tcZ4L(r|fin1$ARxVJc+1-`*GVGA|3bh%*=sX!Bza zEhgw<-s?|Zg#$Vq_viElb*wT!*QCz_)GBI#a+wHl2)=TSkuRF};0ENZm#+N3%>4i# zRv5tUW0CenB38dR#An^```KhCG86Z+RcOi;Lk)+$kBT8!NGq=2c+cMj2nNPs@<5P) zBC@6`UJ{B%UCc|KjLMZ$*@Bam+dCgl_3gRh+kdIDSPDr)E7m40 zZ!{sb(iT^^gzKWY=*u(_-j<08Daf^FB}eV~M05KwTm99H&hlc{je^Tz1fyzX|C}Qn z5X!gN!76s$ouHZt;q^uGE=)LfP9-!XM)uT68dg{#$h-#d%f%g+-T*(0sYaC1*DhDI z;l;WdcFPbP!?}7}>@XcN3eHjdnaZ6Yx+L!^rcmv+y~;fg=189~XpOHy^g)!4Mol!E zIz<7qBn!^alRnv^OB+At!c}aOMD;lBbv!p=kBk9_FI*gZg7?FTVW+6 zk&8X~&<+eKCXj#BY?M_PQ$z~z54+VZR66Vi`~9Ls%9H$jBD;KBA%26^hD&q~9J2w6 zHb0{3a$de4r6H^4u{6t5j0KakRGz4^_6Zfut16U&_M5;H6>7WRBfN19Ni{Bcy28WK zvzV%Eh!7z?d72NZHyeM>zNycPHOelP>f_vdkHZH<_}$k}9_MPRR8#?hv7z>;qKk=0 z+K)}l^dA`y7-88{fPH&BuEBT`Cz2-}sGT||lHPZL&yW`nj=0A^1(!AzhJ>MUUYjTh z8$&*`je~3C7&m|7lK-dLB@HMvk0)84^Luyx_>A4U;IQpNV>tYS-uOZ*G^gvmUU~zn zRR@+;4TOG;zx@$Svh{Cpi!LYJ^oD}oIN)rJ2pK=_FgsgfIasH-zC3CUbtXxtV{o`3 z0jS&(`JE@8XV~aPAi81Dna!J^BsPAP!4TAR7$XGq?wyJmfRQD=2PoS8vp=6;JXaVk z2|UiazEdaf`Y1dqoG|=Mmj47UH^rAhmxNBZ!m|S8kC5>7wpsM>su;r1YarDiaQKz&gQ8_Em6XPd1M*j4sW&jf{$cH zbaF;YpWcsA>7E<{C2wdbNuf$hWE{GrD& zkmGj-%BGh)cAlklv{?%(F@bYse8Kp;s|x7`q%q(fD45rTs_`BD?f-GFD(;ZV+D{|8 z`N#y~iM_*%b0UrFGJ9rhimP>f3AV*xGRltV!_N_9CEmLNj-m@xZf938g7u^$u%|i+ zU#?ZJJ-1vKUF>gMDIg6BGZ%C-)hVoh8bi1x718WX3q*;CTC5MM+_HkDdV$9qgc&?H zd7bAH677+#5@~-ja*jzDMf^t5v?OAAEfjic8nhx0eF-E%qZdMzE+Q_vfC9i&qDuK< zE|Si&ly@iPQhj-4$pSyHu>f8_ly0TvjA?}|k3B_h3{VT24}Sb3ZzMY2r{*YQIO&U) zi+{;Vyh!@u68b2wB+klDsApp#o zqk|yf7Yl2JFVYa+zEhdiaTpR>fj~_Urj7#e8rlmSm&{qlR$HB5j+&k35+8Qn3JM#K zDy2l}suB-@H#CACnvjbZA^d*%xrPX-+TQIdgM($~^lAMgr0-$Mo6TV#`B2&)pIMk} z2a@Z6|7vrkmXnb{otV!v5U(v%Efx{QEN5V*j`0$u_vdjQ7tC`6<#!%a+vtZ7=TP8# z3dp`N3c(@;Iy2^FVL-AT>_@2e7#cUy(;1S-DAvhX&N!x!Uh-%{PJB2IJGL%v(yqOn zN&_-fmwdn=Sfe`3Lphyh@Gg8Hq|(SU)1O$}m&d8gP3#I6C+6*eC|uX26_>$Y@5UgJ zif1;VXDp6Ny^@WRKl*9fQu0j#y%Cwf0t#~A7cROe6EgLo0Jx~^?*orqU};|kh$U&a znuS44%TzkDKz>$P!!hu0Y2b&x5sL`WEt^K=F3mCm09=(h+?g(K`UR8hKlV2qu` zk0$DLQ>5_T>Ikx%iq=Z`8e|<|(A9z%mOySYTF*z!F|8OjN8C^w;r{xslXK85zX2xXw4@SczPtUtpnZaqxH5Nm zzG{VAmi9uQof;vr>n;#7oZ`N1DVzn34nRb>{j%&f9!huvm2AD3#>p9yA6B6|>&Pe# z2F%*w!mFdPbUD7Iu4>1#i$JdLGY0e`Aim}h?(SAGNVNAIe2IK^ccIIUksRoKh@9M` zCTqNyvb7aRm1X7ek%t;p71C#;i|_7MagxveDNv9urHJ@=wx1KUB3%d+0Hc-ku7+37 zu(Wy-0X%6Zag`1MfbsFc)&8n$cK667_br)1ODzMuM;PPv#gq1AZey?Pv4suYSKef@|bTabfLbmtr1$x-3U6 zRC@wh$+sii%`%>ud|FcUpK}lQL6V6uB-+as#$FLf{ z4-YwK^k|{4+6&P}fb~nE8UMWCjwzJyF~aXy7(|a<;K0vrdlvc~7YKpy$uD~|p@jL_ z!QLwkiH#vSyQe107iP0ix)|=t=xbHyf1$;Hh|G= z5^QZyCuG}gICocVuBTTVWRp5Ip&}mAYC7Z0F7rmVHNHi6uap_g6>P(8T!E7OS2hX9 zHC82RCeaQdgTA~irYkKM!V$lUD*?x-{TjH>h_k$3V%7uRXadm= z#{HOK!M|n2%1NAs;Rdj<2Lt+qg9peuJse}$bH#uC59HPl@WAJ600VLZ`kA%16ol6= z4*3{@kn2dYP7$|7WI8$n(H!AFrj1lB_7Gan+sp#P_G<{0ltjzRzy{@03sfkr{DRf% z@5w7E`Nkk3GAozqG5c&+T}N`}5}tR;@AHl?gb7nU$bw9gojG)v0$Q@_oLeAHF}w{i zPuZfxuBi`bAuRp{IllrEzXDlZO#lqfj3*sQ=JRXW z)#$(|GDxM17wc4VvCcjDzG%MKrDO966=PfZyTbETLh!EOcwj--2R!3}F@X0z$3uvh zpKp+Z9yth44hWNn&kmsmdO54gbar8e0hZn{?isdxwUMoKaBz9vSslA7 zoN`jY_p-)JYpKo!9Skz536+xqI<^(WO$%f+lgH(pyJ;zcn)4x#{pC$u28mQIK|t-s zV-HX6&pnAdojY;~rF`O%aB$wqdoQbF(C%xEhQ-5hDyG{(OysOYAXs5OU_ay6>E+V>lybU+)^%nw^6S4hI+9Rdb304HR%VqqJKGo6NVgFqGxCs@zp7 z_mv4Uba%Fvy9wC<0hq?35;^CZ$>S2n!a1Fr@UbW#`(u$*E*(uUVQD8}XMrcZ_rB)~ zUts7%86yR?(A!$+bQhRrYdeLmh_5T|{3I+zlJ_g+S#t^z&qBVPr3oS^dIsz=k; z)xzW)c4F+z2mvF+3|W2snM#r|@)|4m8o%m;LtjWts^ZlZg z(DhEOFLd48a_0hln;-8DP~H3S;y}OT$BP5e<{xhh=*K_a77z_+gYN;c$kb3n0g8h# zH24{agvtYdNNn_Q$N$yBWbDlYEcfPau!CAGgBH8iWGo%PGEVBMl#ScVhrr_DCHr?< zvmleRFWI+>0A;J6%_>NJr+56i+ThenMs4;hSUpp%dRC&PYwoKxL1HDsttvN!;E{t@ z<=|H~xK)N~rpckRud~_5v~iT{>Jcywjc>BlZ0&F)T?@a^&nQP-o>ephnfFv>0?LEeFoN;^eG1JZlOS0EkjJ$IZUN zSOtuj&@AwRqPu~5#V zl*y!-CdMX@P%oB*BGemVU%nvDvwX-2Lak|M1VryWEx+tK85OlRC67y}BC-b;O3h>* z_s4kY+JKj^j(`$h=y~t51PvFb)n~B_rGK z0Byb2^_q>Yby|OcMzb#KItWPK-zsuQuHMqW{*huGewQssu1?}Ezy4woiDz3QRa*Ng zUTcKbev04Leu_zu_g<&V;ch~hlva#=T_b+$hSxQM$MS=wN3>oVXnKUq7Xr@$^gy8fykE)3J(#zJESfNnyrXSq3Z)tfq zA8%i3RugJo1M)&j;zlg(iT(hsLK+}3J%%ta^XQ?P0cKZdL0}39lGbR~AKlq>0M#LL*dN=EF#jZLbN@9gL+5^?efgn*|^6N~E!%ThEoWXaqCZ$a@ns6*dZFSaZ10^bR zWqYQ8GHRkd7`+)VT8PZ<`x^v(RXzy_N=tU(!XbBE`NVKCv)V{|GtP@eh zOYe1nYOwB!uMXEn-WXtF$?{#kPoEpQN+>A2ui+?e@AEz3p71f#_eAk=6CNNGU zo{iA^+x+(awiqBn)Vu~V|0KO?DHk?|b*wX*^IgBq`7TYT%i(UcojU@gp7LrSu{1{p z9KGwT&r}H*>yQRW-eYKipb7+Oz~=(*1xnP_62X>c^0@pvT03T`sHpwz0nR7vIZ zUbP3%I96+TtTQk(GBGtWd$Q(6#>+)||KGB-w-R42@hgzYS-$_e+ngBMmfw*SILHZH zbpl`A;2|VQb+U--K46j4QgMhxipa-4QI+rdt;%5$j)}w#^ zYyN}Y@BQz!&1mm`zwP~B*Yqsoa7^&S_;_Q2Dzgti2YCY^iT|q|k#Q(NM()m%3~Sa9 zJu48sDiFOZd51iKPaW2(Y__+tNa7Y;(=+zl?g!2X z?nlftM6YUCJ5iyL|L=HNwrIgxFwx^;opygQkcXtd#Fu4X5kc^o0ghdYl@E$|{8aJ+&EW~u=TuyXKo!JsKZ`L)$cePu%J<*GU{bY{Hj zY!DjXIEl()S6+<+ufBp|=uRw1h*(vK*p*Do8e^-jP86kmwhQ&WWi#8 z@syp0zT~^~mhKuEd9TO1yQE*HA0PX_MkH>{QrV~dR`%%=w<-;lOKok*RdGkg<|MnA+brD?_2-y9-w3?dr)v){)4oU?O33e3Bfs4=)|4DPB0XH}tq+|-10 zhs(M=ayg2mv>YoKutJ6uI#S5cQ@I+^Fv4-OU88LDD1e0YOL4PoUI&?{yV^Q(!ze0? z#_H^cp1gtx)fmzk;^?tKdu3_p?7r&6(3w%AxgV8gERN&Fr59`bZ|*!c9gWSS3(Gtj znY;7o!WI!Fy;wT$K@WP+0~vVlVLKn@YGLS0-j@D_XhioL`Q0~){`i>7=#8Nc5xu|d zxA(Vw5gL!Td+ByA@JrwY)>Du3>{*@VAw{mH+*TPSz$jrx?7sd`lH&Y(660+4VXq}Z zXCL<4*@u0R5#ADqhbi1L-p#VOBx9Y)8g))6WR(kL2HIGWKLE&Ib+eI(Wxstqm+tXY3Ks|!E5WGBSTCP3NmJ#dHRAnTa^0G>)Rb#K~kP34Y=UsU(tDtD?bsehEitL`POqg+1!gGZH zz^rXapZQ#Rv93G?#ht%cTJA;~i%aEvY+lq`p>j0gdu{mxhF_?J!h0BMH-=xRghHmJ zF#JN7IMcrntrKY-mlcO!=n`kKASDzsO!OYa@C%htz{J+FMTD2c)>8Nqy`RCYd%wfb zU3wr)f7LcxpKY>BR3QN)aRl$u`v&}c;ZG;d5nBHyXJ1Rh~8w^8yhqXdWJoR zs6oOF6H=Q96+Rxb(gdlovHvF(k1mpETbvREVTWnwMwOYVGDoS*P%5(vl{u**lQLn# zrL`l*&L10N>0Cz%&x4m9snz;1v~FbGrN>Awv4!*!WBq-JFj2)P!m^e4#eg3llYbv( z-Tzz}BLAh+`R)C6AxJQVYFmgOVhg9ygfEs7;dZmtXf-=mWyXh}3C2GkekSO;_<(=G z8qNp&3*tK9XM!ILpobfW9Q?@DpSubNrD{gNY$FH+KpOx8K~ai>QuT9NaZs#mP^xTD zu5fhV$e@zqkyn8k*z-_*OF=*vgqQ?i-m~ZEf0*ADo(8iU7h1Ij%MCIZm>HTGnwyv>nj0B!7kT~H&U*b9mi78@D=X($ zFnbr|&?5VCm6P3D!(|{Td@pB-j(Ic^>zbbB*uGVg_MiLMb~2EoC@mukwCHEmWq{n=FhT~1+{48J zc?EI}q+DQWhN8M!1eD6O@nhZmb>%yVJG1E2jIqB~kQnqy0`d{zg>A*J%jaL z_S@Ho>zf_>4#iY5C6!M;ARiID_xRIQKywEi+k{vRG%zzTH#9UbO(!Rf49$%U&Qo{b zSM#um8|2{i9}%(W;Jg0JY$Z{^^kpAI*BCLl#sLq&(da7W;F5wuqBR7mGuq z{O!;EX`;zOsiY*qBlY?ZLwCsg7T)jti71iymw^5f-*tVFTV2z$eA3#vc1v4>`FLxy z@S%CGie=hzp#_63cWtTgp)E2ibLzqc7cjJt@@VjIjL5DZN_(jwXsWI;BJ(R)CNgib z{XFycIQT)_f3Z|}_*Skl4<#ziwCQ>iB~DRzLT+f(s4To|$;)blsO)vUp%GJ8cvoI+ zl@q<|tIJ*Ga$l+3R@xN5tMK~@hDyn3QL}T1-8y_|j%_7U&D881fS^3KmB<4`5FtNP zA9h!usTl0W;jVoQ>*t!xonC%y6KiBDqhOsnzu9(ZAHZ^>&+S2%!x5X3M;ZI$!phj+ z$L{2K^O&>K!%3uCnkrDF=$z!y+1!md)b4!1)DHNIabGT#&SnqCeYkTc!Q7k05=lfO z9x$~7zAq(@P27n8cBJvBWb))Rn|P9@e=LkXeCd5WUrE}?`w=+bC`lXXw9#xtyGC;g zhQ7q5%SJAvug57cXPwqxp%j?2F6(j22qu&Q)1|lK*o|kSSNA3@_=QjKQ{_-&+zge4sfxD~_%TM|UNo1M;bWI;#olOa0)&2N+CrSDl)b zPVK6u0@kX4wX2kxb%@3gjuBBB7Go=m)VN zH?gXl*wstS>SDuz1b6kB;*ki!u`EI*DVdVTq?u+i-KEd%0qJw4@|=&&nWgaDrAS2& zCN=cSac6T^!e3Y{eK^vG<4WdoNm&8}sg{l-nTz2Y!Vq4%f9dMp1cu?q$ACWukyu1D z_HDn7ecNRP)7fn8Tsin%K3+LUvcu;jwU>{vc(B1$+tbLM%hhQlBLonMWnUz{ec5YYKyP36+uN5%@S2MlhiP_f zN2AfUEXPZ_|Ys!45?r9*Bq+Ll|`-+naWNWJcl-{1dz*KdE{^+jlP z4pONk`lOD=-IzSm&7#qn+;gN;F{=42GKoEWNC=dJhMyB73f6;yH3-;+yyrlcP4XxT6!{ld__bjdhi?~?|%%>k(n%Z!(Mhx z&vG>uV}=iuSilN2O-#(pjZIBW%#Dnvi`0CV&NBHfo%P|nbk^;=GFi4?`y;Y+-{hBd z|Jv`rDOs<7uVj4?eyo=D`mvl<^Z!Ve?9*}<&i}h2u|I#vy1xy}ntj;Liv9QX+Sq?# zLf$ErmNliW(ybQc{0eee@UN>W@W2K#Faa0@&_ps7hh%_IZdP4Aq`rZH5~c-M;I$x9?^x?Oa*VhWCCZZ%No#8jGG`SC(vw zn+eHZ-@O4W25bgx_A*wCR}YEG026kX7{YMD7O21z^C!9IJF^E&dhbU(U0;6nmI(}h zeFjTyzvj2LU(0&$c|6?iCfd0-5iKo=sgfEh(j1K(j+*QmLxVDqpsWglOI5`SxM@+2 zJo=KaB@{pwy=70sK8v2gE-i>#wPs&s>?;a2a->ZgHv9ULuO}2B7?ruM!YSM9Dr3c9 z0!cNz1!bX?w?LOPcck&y!)ZR2g@q=?l0=c(p;%Z_q@sMT6qL!_m6A^oK%Y_(&x?eJ ze0p@y2mir{z@5zfh`A@3&!G}LbGb)}ar$l3F}iQB)n zLo$M(NN=wz&$il86#VL&p@(XPx>8%sOiSu(ix}+sLO;N)E;V9KqB2<4g$BWbyxX-685Ni-ak)5U&lb7#I-SH3%CtO{WXpCA;$qoNm< z&YMt6BaEF%$*9P8T7JEJ#fb`*6g`)w3Mm(s#{QZx_^;W-4DQTgk;un_MT8M*Nrwd& zv9vdJaOYFmblROwy6Jo>@n3T{;(2m6CZA7xQ4endnV6JGegw}sX-IVBJ&G35*NKkg zmXS+uE#G+A(!B|6X+#^vx;I&R7l`!r7!ZkDHNQoY-oIAUfji!AFlM)V=tfh3`{VDm zKl{Jh7rSagZGlzu+uFNXNqAvq+RT9>U9u4qo!!}(3dEEsC9#Fb7=U)dn*-pN!{GZd ziNC1eF?jL_)nmd)wTwteL`343M=|~V*K3QUzyJE}@4q8A>=SVK+jS0BpAAS;15T(* zF(BP|ry|s#NsDaM?ySoUK`IsqC{a;n*d<^G!-xwbFwCSpQ#`X3gt-cY`6_3&egMPa zHD`OKdT48qt_spuAZTmO9VMYg0Ad7SMhP%Vm@&I&rYEzili5{%Lnh75t|YaUi4=qk z7f@K8Z9!pV)*h;htja}pr6L0oGd+=6OK6nfu*t5qO9!8+VQmentAgsQp4!S2!y#t- z7FLxrxi2yIFeJ&jQ~40_(Ax1b$G#_3yX&xxVlqU?VMOeuvBY2ZfZ&l zCAO!vJlnro+{^Z|49CXGu(o#R@Z!QD#bpfvCi@na0GdfPJV>6gOFp*;I7j+iDHbIb zyoX8rd0~I~-qf^5;mKstxLiJ&K)ojYbH+dea|iN0Efsn2Bn@nOc!Q;=6KLCV&1!MeeV1%lbcG{y$mw$7@+Q--YvCUW@Zx z@l=s))oE2a20eq8h163qn2?^=L5Tv%$n3@Qh>0k~(h9;%oRnHV^z&+s$4y;Du>-{f zh$H0ch`Uf{$8@nVwS)6nR1ykjWF7v&KN^xq>~Bn}B0lW3Oi>XZ_FEAj zw&A_Uagvf*5(usW)m54v!Mgf_0xvY+)a9yvS5Mq9ysJ7Nx*s(rU5%V9OX9NHuO{TG ziMuMify7;%eat`L9w;b)LIcDSm(?*^f&fZLp>_!gz>|b!zI`V&zn7fE6K=Wk7>-*w zCsnPB==P8Jt{C2X949H6LG8{XjE#4U3~lH{gJ3{{W~?xhd4%&KVu=$)LOn@@pb(P~3lLh4a{lw= z4h2}IG@Q*a${wQEgw-O;W#EojgUEV3XbrYSP(JN-c81CHE{7ExQ&$pcOx2=>@{ zh?wE?$brC<`g6&`P$mp*Qb}OJMDz(2k>gWyUU*7BkYrydt%nl}!RQV}nqkI6gb2h? zouK}5w3xQMuuFL(*%#`xLNH2$U0*fen;H%`wJRRASF7sPu7YaBZXK?Mz+jhopODtbZYzxC4dJdls4~6J12Fzr=c3>tBe*VIWK|W&M3!+Lf2H{)K2+ zZxL;{mPGHFK2mGxiizI$4Br-Ug&5m#2l=L6R9;5y)cLEZqo_d72|Byn>QAwbA*{zIHw9=`7vturFa z@@=>y{32?2cZ9r>8hnY6gc^gz!-;l9=1sOY3o}tztH!d)t|4w!Ay%v)R;oa(TqzsC zL~YX1Btvm}a=Q-I5K6BlHEvQI4IGY{?COGALqjNsaEkA)HCPsgq9DTs&$hzSA&lV+ znVqfKIovkh=t~1on4%y93NkdOD8!iEI4ii(yAHzBU3;cRazbXP?7o_~tZbw-vq$VR z6%m7yB%}*rA+ek?)lnx?6FH$xQU#e_+=-J-hTT^v?6rr4zkJ4qthx6B;I4 zL>KYa)avam;?G5N`IsKVbUN03NU(17E_GPQHEQW!s5ZXvRt1$0_5QfmqDb$L`|bVl zlNBj?@ZNuT!5rC9VQ0qw(A`S~Cd}Q1JALfWUld=wPuJDG2t+=N@7xjQ4SXd6DxA96 zS*1{!>~bhUfY>X<4QD3XQDizv*ki13VQZ(Y)3rXY@FwyKmouJQPFL$@0+A1=F7=}5WBqm$EV=~-kdp&&LX zk;S8rkBF(rZ0g}Ga27Q3xD%gEWKjpwGz%6u(s|sSr10c!WHygUrM-zM#D7kwrDxMw zWEPiulLnzw#Brz{*G~kS%H^&frH)KYO5FLhv>S02O%}%Fv*)rqEj^1q0iL(qqqsIb zcEeuwirJ|nR6`AxNH!bb)WFQd)WFEx$k0VHO^rpFMf}vh78_hGKR>r2_Wigl zHn>`TWH%dP-_J|4VW$1WFdJguPdwW&!+vBqHO#c1e^bLu`;mV&#J(T6rG}aI!}8Vu z-+tM~-blld5y4sMXUzvbvY+u~KMRTTBHeF{`3p%!=h!wZ*cx*XyxVYMzJ1gNznOdtVV8 z+v>)p0&Mmluy!@i+wSZjGieDJC1%WCKBje}LR*#4c9mnZ5>k!OF+zr(DN+kaI7)0; z-Ai`0Dz;V1tSYm!F}4-VtO_Kl{&v<8E{uSoM+y{`*;(DTGBE>U;s?Zpjg=?_kRJ_4 zAIBd{Jy1lT^DyDDbb+1fGONm{Ptq4Iynvx8B@va`*&ZkfO$9pH3INcToc%zy!huys zpem&ScmZeH%mItO zKAFtjnGBN6$sKv;Hf%(1>_A6v?BKoc@x&kSl)|B&Qn)T84hiw06(5V|FjLG@D^k!f zJO#%GU}J|V8~DfKQ#N*(vay2-Hg@pd)6abGDdDsfZewZ(`j_L$W6yacVNWujPNviT zgufV*#pH2M?#3MeFUw`n08d%&5|<@l871PKcxmgN2QN`Ts_A&yG8+04&l>>J+^Ut> zx~=;X5)usIrCZX%LGnnk{>s3*w_l_8247P0-vPk3Igb}r_Dj5$CYAjXzm@$GU&KT= zqgOQLW${gEg^&V`hT+gInwUj6cER4Cdw>QgJlSj(jYS-XI?^tO8cJ&SnP)1%qI1e>E98yFcF8wE%CE-dSp`0g)}HTx(ZvSQ!9W!=6U%d-7A z_FCJIY3=X3tt=-cixek_c7XX^V^@vQnl0q@lyqa|Uk^|Eb|+M^xdXk>woba+(MtF( zy!TB#`$a)PL2*%~k?M?goh-7LNKH&kTEIU=nzMzxSk%qsbBR9gOeXJr@9BCY+Qo7F zyMBv*cLZq@rOv3$7IG6wvGR8;kuK~n$6GkS4m~`%OF8#u{Cbq{`t1i(l<#`&j1*<0 zX$SlUTO5GjAZ`J}8iyPV;p(ui($Q7%KzU}+KV%>B51FWru4-6Yb#zuax~d%A6;NmW zeW=y~kiMvdEtE7#%W@DgGc!Um+NcAQ6aX+76bc6eB9TZWD3nI-0~CN0YHC7yGNO%S z!9b3JF_7aZV+ccr5MzuPkRcjCSOJTCB;)U_+ua<3N5?E3UMLM_X1Ez=kh}C&3xgp(pzmwLe4IzIAN>c&&_!T(Q3s^z{5jMKGQD?zG^qO- zKH;ZPpWv(g43Jpj0Sfx0)N$>e4aE&0MS}nUMuZWXpV|g|>rGb@eSnO3fR0xI`&BBOCX+z^yd{CoC$4f!K>19i1wJ|5B zYYckG-zp|)Kadq1*{Gd*<&D~KlJQd*2*%O23r1!eOYW zP6b9spR2A-7h@QEUp6+=w-4lDL>>QaHA6)_nOg8|j2I&|fbY!nRjP*+lldn+*p^|%Gby-hXi=Y-% z=7m;=)+|-8n=-ETjYhA!Rm8u;ShTtoWHzy@SU|H7FY~FtEP6|+vg2}|#PSSHR*B&z z@=Srfl|i}4<^jl}+Y6OHa}mga@zm{t3qWm5T!?mkWKPcA06zoFH5p#G z_Ti?C2#k+S2Qyz%h)xD8#7(huy z8?vj;SlnjJM0-_C#V2|x8@8NO8b<}ThG+_`9!2w-^O(6)f151UCoKBsgf6us%}$=I zXOEHN%QURPvBg^xaUyu>i4pUO!Qut!>(QC0@K9TRHL3VebG3;)wRb0rc$vI@bOzIB zLVj|wg@ehMWDN*U@f$3aLK;z;hn0;%mYtirXNd|ye#5DC*@QGd$b(u1c2IdnWnLSnLQ9~KPNNK9zNFfIAB1rTTMYuv1qG^EPs_->i zaYfh~#tmXXX2-xkVc%H;hMi**T^czaD8!O^(2#sfsfk+kq`EXqi4)g>96q^Vu*N3I zlG=9X5D$ZL>>-rJo^Jm?^Z?gBa}6LGAWz$-v?wGK=I+4>VA&T&a069j)q z2Zp{vxIYt&@O$G>c!GjcZ@W5T@Z+iySm%*e<=?K2HkTEQBa0FRsD9R;w zC78nk9@#@;O~;}Ie`v99Mg(z?Rk@jZ7O&JRMx{8|t6QLXQg+?=4Ytc5U&?(m7Gkw3 zO&CyX$%x(@75467;$T4*FmcFl z-vwy5Sf38k2AO-!8n8%-MR(twYFu=fPP}QCrVMF)!@m>wEy zjcSlHr0K!$+^6A_Ro zzxtjvdh8Zm_IyP^$WR5UjV#cU2YV$Byp$AZyL-psi^_Wi~6rk9<5vvR#ccQH24 z)F|QxSQEx?-$^^HVF=CGuWn0m4iB$s48$ zBiLx2p=y9UrV(~D=tjWqp>SA@4kUkcbook2Kza+pB)zncbW^FAuDkr-GR7{S{AR(d zbt!Ww3Pg#{F~h=@+r|D+(j~YS;im=31XgN062BU=Jt35WA2|b_bIYpJE+>_nY6d!S zeX_=-lBykqUvVtqjxQAM@KLyL9Bgy7{EBsQ>=PWfYTRhPieU5jB09;FaY)vNojSG1 zKxV}0Cc7O2#&OD~#vu+6wR|w2RJemrhZmzcviEGsNJesJ&bR^e4`~yM*=7eTs4oB) zt~2l|-3`~VP!W4L(+@;M-Sq+3Rg57wya|6Q8N25tZvXM zhcRyv7@o9+1bCKnSS)dAOHW~@cA1qWrl9HMy1;T>vcIm(aRg~lbHZlRlv~_eBscCwgEkQ zzOMm?_W{449Zx#IO%;b@+}eJi!>evq6qU6VuG2y$@jwrGJ6kt~gRmm<-Wyx3NJ@zQ zKrnG1+FgJ*AfOm<#<>k2J#euNM04Y6HLgR0Ni7e909-;xB@@Do+aPv6&IYDAky8th z><>G8S}(a(G3kmQj-v{35-{6hnMjCeVj}<-_)ZUL+h@Nqx3fIAkxzMn&mtBqfD9pP zpYrm^3GJ-~N~{bT8L1ElU2dyX_HT=pg36M`^yk`}cAxn%CJ(%8u~E=R8Qt%=Ggu^6Gnh(G*TKiOb*y7CYY)T|qRqLg zu}b(#6r33*iEoW3%6G^0 z8Jcd;DAo~juev#wyH)|bAJ*?y*D@60g!Z<}uo(iqrU2X%brsrp;QM0lciWi2(AnC% z$qwXN9g!?98BdtJEtPiV#O!M-gg%n0QX`Q$NfW4|c8Itb`km>ea??a+q8uXM=>okK zSxvo9dK!hV$TcvQou*w{e-QLv;QqwBqL={TRmim_ngh{{xc-m6#L&XNDih$J7Bpt; zCy0WU;dzZ(h97@cr6L1ks({R>R+MozosB(`A{!0Kpy9+Qdf;amq2)7Vb7sGI~t?3;79TlbdcRqU?9-R&HP> zB3=lPnuSTY1@3a>a=J#Ojq%R+rI4sVr$QVF_TAT^rG5nW5(EH15f;)c7+SAJn6S08 zPV3%7jy2l*ha#VNIIgK<`> zb>iG3zDM`8ObypU2W}y3xg&cU)ZgOgcFel^D~>eI^K$J2pIZ8ewMuI6px-E%`O#oOjDIL&gM#ReEZ$ObEluzxmcfNWepua?V^i%0v>%b&0_h@s|FMuNxX7u!u zlz6}@;*O=A+X13#Z3lzU@=efvXx`_7;R5A~B^4IT8P;^yZC->TJ`z zjO(9-vd=bbLB8Rq-cHhGkE{n$GctGn3G8v<0buS29AZO1b{+$sG7*4DzRi%m!*Jz? z+ko}-$R7$oQf$DF8~|(EhirFdt?Yi=nN5tVfK*c0uPhb}S;rZfCo7mpe@(_TE9Rr% zfw800JTSOtaAGet7|7bJSTNM^k$FrN-B*%ui9K8;i4B%p(&U}zufGEMhu1no8F*z$ z{*(MrpY84o#J?2*6}<@Z1kSapfR@SOVk28nNL;-Bu_B#4^h3xwiAyXSV`3j1t}or8^>G{sYoCH?H0KSR zGt{Kc+kM-`uHg5o=1F{`SG7afZ3mt1#t!gpFS6`%ld~W|+4D9pfE^BT%@tv!2BcNs zdL1R4uz*&Y82Q%)s9D;7AZ6`G_|R)8zD1Db7}plv5mxpmR;`GGH4Lg(8o5?cFSX>o z$}P;l$9jZ4d#sgaqN0&TFm>AKWDsCkSD>~IvWyckKs%w5an zERn!t_*4i1n+%omZL|qp8ke+nKO37fOafOxhru-Q!=*sXxUw4Qnw%%Db?e!6%Q?j0 zE`IM$y@`J6xSn@?>Ba;u6W>t9zC*TFeiLV|=@cbD?RlJZ&2ni!3|q#zbUsfR^x2)iqyjnU zSzS|N9Scz&T4K}0$c}R92Dcq8N!6i6xjx%kFIwP$sw%yxKN6#!SuYAD znTI14Iy|;5w@RQE}(gEP6r~d3N zZwjQiZgt6Q?7inst-hdul{fB$MkqyqLZnwpBVMJptN0w(W-J)%+CFdty0GqKPVx7$ zw*1&xigqylPbojC;rb9Sww+px1f(mkAt^vF6=F8KucV;iR7<>lMypm{34CUFzoSxg zg_Re^;Iwhs6Q8gGH=(n|I|R|KuC2p7T2<80GWDddh8~pPO_;_YbLJaR_3Tf$=e)EC z=Rm`!W9kE-$sFx(@ADN@RfAW*Y_~NO5V@@DniZ*fju61k^wpa2(-pFnBSaSB3;yV1 zFYx|iQ1CA)lq`3UlKAcQ0Eu+-%k|qvIS)ij8p%kWk6J0T2DL7P@xwJauD8I3jLW@)L;ilb5q+udjYx38t3$bO+%yPkyslgo9YwcQb9iCzIxpFH$ z6dTiL4=W%uiWQMhKpDd2GYskL>h;bL-$tQwy92gEh9_QVz4z0@di_f&k2;8LJ8QqQOvp!PDAxf*TEp zBq%cCk{n*p;hx^Z{TuF;h6NXuQbw5MkxRWoUUj%DQXtQZe~NF^N+nRt-V z+3l_ZzXAP%Ct{$^y7r5n19F@)x6R-gphKx+d%!KtU!4=^Cd$4KfC+MO*4`QWGFDRW zaRvJgLM6^J)vjC5Kt2JTfnu~ zvSI(o$mgcLc$_)l%|{f!>|?7Z@l(X2DU)f@nbfMNmZo~u9FV@5$o_al03 zIchCvZDwTWu+YYd`kMaRU*7Kye@P5zLVgKgxFvOK3WE7`w51Kd`151h;DF>ah=mX0 z-$hhz{tNXkS~&y#7h=I}HF=>8DgruB@t+f6;u@}_OH_oLTZoAUf}dE3#&e^qP`&pG zeXYXCT3g=P`z#Ac@$d}b&gm5`#?cm4g=q-YI}4U#nKkIhK;MxnMu*;wc7RH3b00e- z43@7yG3QF$e_W3pVZqJdhf6*e?Hv3@sFqDjv~VPnKe2dU1m5>HM{`l5Vb4IkolUEC z`oCO_dx*LJ<9$ut&k4d7o*oL)zetD^zwMyw7+%j9Khe2b1^^(gKT)yk?-^?39zA5t z78?qtNttzB3iOrph$X#cu=OKXEFg9?Dd+QXp!OV%e@_OfL8Xf3V%&jEoApbPZ*uBd zQMXh)FP!fo#DqpxUKIIio!e&Mrd(h(8t+#%(kYl6jGdNnRoWR*Diq@AXahT{* zl&ayQ%@%x@mI$aqJyWDWI*jFo0n27~kcX$|sPT2K%PGUoRslv`dIgbNefR^rfPK0d zuo=XC0E|F$zvFGAb-qd@?jzZjOj5JBtBZ!`&nk?99h5_*-_TUVw=|Km57hhJJ`(<9 zp4`};YpT2$s5_yoOW`VMd;XeMv&<9<-kK8Ac5sx0=Boyzv(r+u#OK8aCm?#pXVRJh9puLz+ku} z%EeL@;@m~*1jeNYkYth7ibLSxB|#BNS;JJvf>$?r%@DgCL6SI3j;l93+*&ss6FNlv zq+`ZgS;hHYku@(4#g)p1ndo$NEt-E=3cEp=k$x-CBHFTi2e8TSvs&gyyri>mW;I<) z(8RL0y(rS7Gvt09-jaLy%`xCNawUI&tWp>X$%@~`{q(bbk}3jB6JnSq3PaZR5JCvo zDB=8F(Uwqj;4Ay!wbSBq0EfF2S^w>4?KXhB&O@EV>_1l<^`60e&~5`9D;f|iJ>Zzi zn>sgCxRMlaS(Jfg7t2tlmQm61Z5%BS7ZIAM@wDSQM5@oEU4!be+pwM1Db8k&pN688 z+_J7yj+cJBsijJ3^p{YV+hrbb>=yT}x(VhRMZR>LHBchaz=1>@DrSDUh|<#X2eh8I z69GA{w2xdO|Gas`+ee!0(U$_w0(CMK>;{2Xdz2{`)O?07vGolP2pR1wguRi8f*FfU>d@5)w(jf`1aYILD!l(__{Kn8ajEQ}_ttYK{3O_b24 zn;2bwC)-`jyof>kk8^mFhb6UkN?IgB#B`bvM%(a36W>?jFCBk80*QLfUEly$AjBh@ zUh?*5L=aY(L(-Q624St8a3XU3M}sY209q4HI7ABz@I_Vg;PWq{Z{P=(0fAC@3YHup zx}{uRqw5pwR;`3JyX*V}yS5zWljirfpPLcwzt#g_v-JGX{B!#_==De!N)wrOiK=yd zKVqbBI&a=jo8VPiGxFD?CFI{;Y~HO^mWdL~l>EjO z?wC5!R<+bW_!|VU?j;-Erua3P-#P5WsVA~kRxDo3mGhht(g?rF2UhHZ+ivy3l z_}V4q9(OqPVM0+fPOlxvvMjdC--RXSn0{lhEI}R_z{@2@Uvy;}8Wy~?I!b>shXUb) z0odaH2$%XVju(M2`*l}tgNc;#%MV%(LsI+Q?rb;ARJas3oSvbdMra=zdDQUm z(Dy^4#~YW~9Nv$u+f0*yfxOU8Z0i+9r3j5U08@{dEZ&aiHWul|JTI zFZ5Z~x)P-w2Q8aI69w_Fn&OeGE5M`3Gf)HK2=XU1dla7`E8xgB?!CWUa>uAt$0!)c z2SW?!he3oh&BwK!sGzzSd-E)V0l@v2Zl<)f{{zO?(FR^MD;@bz(KKSYH|{(Ny6REz zJ0wghZT8?R^I8!;t7cPyZcKT?icP_h4XB8$5Zx@<@vqQwVypB9t2Rm9-b3fJDzn3P zP_aS9{;@)0Mj`sjGxX%ZZ3?>mrWZpM?YOaSgk$-RqK%}nGQ~0i#}%`2Lg=w@qfIdY zQ3<*Jg>#Yeu3HD?yg21uR z@>ycycO(H@;Tk2}%j7tLMfFbXc zDQQJo40@&>m;mOQ>k29bo+E3pkM^^L#IX!4u-x6s$x2`uL{4Co-tf$k6`}`|H++k- ziRe67hZR@uKO+$C59*j%)NNV}`e9!T;aoiD1V1l*TF*^B(> zS6Y?Sk_uwjjENlxU`C`N@41FpPTD#+`e9n zbBu5OIr%7}*v6pE@0#=c-tC1I7|_<*hhmtS*WC zJpKCO3EOR_XXqaGlFP+f!XpgJ$hfFstpnyMED}#Xcv(j{BikUtk-jR7J`|L188+DA z4DL^s1?6|gxef0hCSOVxSqztXQJtxLz`&0)Q>meVia|hYZZbsl#JknKzC(L;sJv?v zkyeBw8Qfl`C8I!+JzD^8^A<(hH zOCEt9{l^zMaJN%>--`AbJKQRz`nwr{5jvMEmu48)=Yzx*-a90L$PJ=q#)}33d`8wv zNLKwiR!!kqmVP5_sIMWT{nv9zTzIU!^$uA;daE^UP&ef?K)WTE4WR41npLug7WSV9 z(R&{I*5ajH+&7 zXZ1j_e4*9t1aW|_-4pvuzbU>>_}wh^x2c}Z|1GdNM0E3m65o~ZkSuzR;S5+iEUbkI z-A3O+p0M^P#i|@QWyfBxTjAeJ%-owNKOKZyTQ$b6Y!v^+}x<=V_+-n``Ts%wcF~?pej*aXGfLJsxXaEmMLCkU!)y) z*8GMY@pvC!7+D+oCZ?!24zQ5$o1SA=Ydve;i+^A85+1Ovdi8}YsmLlXm$o^NumXS> zoL1jJ$Ut)ic?o#F!4=mg?(J&1>A(NbGMT(0SJ=Vg$dKHj8lBI}pOV2}?I?7@>R~nZSim;{8I5Jk@nk1@HW=@oN=gyF; zGT8iwd+=QkP`D4PwqWE`^B5({CrbW&obL9!`&(JP>_9`<`<31=`=K5Q`mEH0?3dv8 zj+RUYnXz`y0W;A%PS94B$qq5b(Kuqy$_a}q1ZbL{BAyOOo2er!51g1n4+fFbHnF8> zT*I0II83X6rKDq)=p98i-ql^8q75bRd=UgSbW5MnASAduzL4V8$4H6=xLi_9 zP95THox}AkIV*%$Ze7?{4B5N7d7?H$a~BRN-M9NZ<~9f3wIrluE;w$p+i~!h!3`O2 zpE4MRvg5s$yfDXd9c4m)g9lLs#PleRE2_;y=V`5@=`&6VY6!ZxaKqpOm)dvqoRsbA> z@f{!lYSsyN&PTQC&j+Z?SGZmf5F!h$m5>NkD_B-3?mHNsyTP|9>n z;7}1@eu~;B%U`)@bQ6_6k5`{}dfE-5+Sfw2EKNN}G+c-UW=OGp4Fk^tKrAO?iK+0R zm7Pv*SM>m&4y&DH0pNua7?EG%U6@&T8g35d;dPG#Ymum8w4+OlYBz$|GTp5?Ru~mM z$rA0ZRaOva;hu!6RG(i8ki1cOnQ-i)ZKBsIj}jMn??==HZrfWX*4wEM4mj!)GqRtp zdWDE1EKhH2I<1Jl6*P3g&3(UR<4pZrLAhR(CsG|sc`EBuUZx3OYLy8s3WIMLdIKajGH2?uXYul7I^dY+h8URL>j?wmY+khD8L39W34H=-_8Sp^M zJdm3eeF-%z?i*>$yy?F3xrtc{Uw__AT7~)s-`@`YyGw?isMzrHvtqc6gd__VXQ-I~TXsC-dpehfPy}ISBsJV4 zw^bZ$c{O)6;xeEEciq_&HSW_FBa(u&Zz?$$$CUQ;Tm?U^*klP?!NBsLr={FCNDfMSnmSYg3?Y3E+y8So6 zdrI^Tb)z+CX3YQB2uG0ReZlT}kMU!1#8`y?2AGijs0(KHN`_*!bq^{R=KAI z_saO69&;bWNpz;=wOPORizi+i>B=^wfZwXq~62iUQ;LO@;T8EP2ADt+-;boXZVG7HE?m(y>H3ssR9!ZlHpPi9n0bQ<_T>}lx!Vo zUifclvno;$pz)_YcIhepG@at4QA8e_++8J3xvMgC2BqCpxm~UCjG;4|ip5nu0sR1K z;-Z4Ft|ea8m)G<})XR(d^0M|=fLQ^C$L&RxyQ?6cuA3?*^f9449-Bztj3UuDJ6AC* zY=y$_H&FZ8XI-H2pJy1d{YI15YVx|;-m0y2y}i|&3+1h%%wjA?qwR#Iv&43tlw{m_ zi9;v3TwNpwcNa0asaBM1t^xyDsu@{T##TL&zC>$1YCdAnnvAOD zRSiacvuaRE!fkeaxvndCHK|9Ek0JCG0RyWty%3{E3?D-%&I&j@Xf$_`nOL=^f(4)* zu=@l{m#N5Hu5R7$FR^p>;m70PSQf5gnXL!Ee`x5#Tq!1P2$#>YV7HsCMsxM{!?MT5 zwu~on-B;Yc+FaKhi-^)w#=6R0SJ!==)mKc-kEFrV=bmV6}v2hwwf3b%aS@sh7XPndjlf$CgSoy#hF;_@}dlPw?FaYCu9OV1fk zSxu$ml%uO4Jd>U=n#uzw6}+lLZJ;)sYR74&CNhJY@<>a7)=?8UYBkl5Q|xusaSB0K zb@cRvR7X>Fw6%u=CIy%rFPbXoBr{i0qWMhCXKN0}NeU-9Tr*V*gk~GV)qB&@ z=!iC!X?2!$nVVaP;2dke;DQtvHDyTSW_ zxnPX~98iC=2UZ$bc_0P)L!k!i>JMdV28B9c88VD$ zn`>;_R@eSt0!b%|4dMB9zAWEvw$=~+KlryvZup4rt-3tzIQe8#G}4>QNbjxIo^+gg zv8kQZ7BfQgR+pYUaH6qIEu}e|5fmt}Y7{82TFkTUgqFWt?@gnTr!+Vb9ZIi3McRTB zsnMf`4<@vP8ztm`;YV^VqvLTX+hAbk)&=^5zt8^Q4?~gd%I46WjjhdO32P=@&$PT+ z!LbN@oyo{7GKWOD&6md^ktl2d%$ZALk;x^`Wia6=n?oJOC6|FL<~TXe$ds2@GDrB9 zxw6PC9`hwl_n5|_3uE!9gly)#A`UajsNC^+|9P4>S&Zpo{-ccsPTdS{km1D%AR8JP znGiHKHZmb>W_12%`CqPTc~!;yxK%~_@JSWz*SQk>->s_Ghvljt{NJu>wOYRemJTD4i#n$VF=~$PHr-%ma8Y3Ny~R%&KMHk{fKGl?#m>kyC<1R?mlBk zbW8Mr;hyKG3rd;=@4ao;^z1ungYPt0@A3>KWh;dxLcDeftkbQI4*fbhNWAwu5TIk3 zgj^;eZM&xD1KclATTOMVaKyB{>klO_`MTfz8Vd}G_g)1k7VzIvE(>Tc z3}^su1RzHXKk}bS+}};oj2cQ+@~a9CGA7dG&Hz4pWsOw32@9pK1 zmwCv`Y<-!nAZYKph|EZ1Jz_J&p7Qlb`Z5!FnXN5RohR|>o~d@6M%2|2vFb_;BVyE> z8#g)d`bf=7>IW%#V{XFIs@{OX_U__I$WTZu$raLtF%$7cO2ssK-1ZY>_K*?OsF?-b1A6nvTb zqDWXE$A+8?)I_rPy!DUbdbQ~gmz6)Pj{STJ%Ufc96vg05r-uXK^+n0 zj}p4-jBX12F{7i-=&UaeaEZYM0y9rpj}&z^XFCCp6A%uXO~sOpfWV>O&~vbAA7c|x zGn2&_?{4elz8Hy!-wo} z>#|<;pyRZorzAXQsLs=aC)j<2CHjlHqA{NN{G`XGFQ7&!5SsKfprts^^5 zBXHo{z{!Ev*B+EDYq2{4iRpMkR!(OE$j?NWpie+!89tHmYB`qysvZ+uI~Fg z&fh!uJKfWnN=O-kEEScILnSds7LSU#vUoIvQ9=%BxZoj|m@fGU%w^M(&xdowv>X9h zoPlim;RS(&=pIw&V}KJ1a;72?SyV0=nR-5DaoMEl-Uu_4OW(}Q%nS~5*|gMwvbby} zEl0?E!Mx{e4ujtNB{5Na^g;UyX{Gpu;iNm`ZCHkrf2eJV-j~Qt{^U<)e=;p9SNz*t zEG>4`k;0-_q?-~KD|VcDQv1W20_`k@o8k=I3fJB~Pz zo-!5{{NzT-E)1XWGYAHk6vYd06y+oUIBpkAT1QG)LRz|eth4qPS%>QR2+|zp-uo2I z8$TWeCi;C4115r#?wfRnKG1L5z~!|zC_;087KwC1+%Kh)eq zLa@B6LPlE5qd($2_CKV2ucQ4jp-SA{iwN#)ZEq&q`FtNx5IT)y;zn-r14nS zt$us;Ao{<$D|FkY+x)VhHVB3#z3|H*gy-wSKKuHxZPzsLohp9-->F_#TWV@cZB40( z$bjbRDiGS@d2{0?2hEcRG$2LI@UA+eRo&`-_xW(#C9e0vqxXiva=P~9J~=5Gl}&e- zO+r+2qGl78KJP1M2QKt?&;bqR10@(KH62~MQq$4s-HT+UrenP%2D=&QIC2rBs2Z7C zBU^iNb%qQA5P^v_F3t8@i=h>7P7eG%RtTB{f1k~Py$A79(_1U#A}VaAG&~e>SmGeV zq2))725LMYbA#(g2$>o}w)*61OoIu}5NLY~70-DET+mV8vhX`s#OP-MXFm)0=R$B% zS~9si4E25yauNLL7lprY_qct$Jo3~IZCYPoL{x{`^sw zN+W}d3lhkt(n#KE7$KWVdLQ~b6KPyBViuRhBh!f9XFaGmW8S|$ROEY6WIJE{eFi5K z=t~#b=}~NAAuRN-(A#n={9Xlm1G_$Z1G_^mC+%+m&N-=B1z5wRehZMErC=CvhR1IK z%5b3HL-gCK-_{&^NX~1D7_)Pw1^S`YI{+Qu!oFzO);Zb`dvi7tte30 zO$8Jbt9_^-CepZ6JCj2tjdx#~U%f}E=}3#PoqoTawf>E64K1XFv~U)-uDlY)@Q``L z2>A8c5ip3Cn%;U_R!-UkyiX{%>rI||pLRO9&MLf5w%CkG%j-`ulSSqmdD`{5^1EBno=ww8HG8LX;@q?i%zDFCW}@y zD`FMNWR?h4t*izStppv885x?InwS_Gq>*V@RMyzo*v#<$XI0_aE^GMC66P)#F<|@yQeaug~KDh25N(oU}dvr-NJ5bF1j!ReDxdK)g!N%7j#H{w~%B z2W@t(-CkbaMS_!yz@WvaTc(1abXSq1=*#ToW7vA0eG)o0yONm1t~^&&MhYXF!pN*G zvg*UJ74?wu8F6oPG`XaNg$f_Y))tZ#5 zE{Y`<4dL?y{NbBT^={Rj)Yrr3!{r4e=ne*?9U*9Xs0E`etSJl2nzO1kc3MPsRHk{W zI)lnOWFE2BH3x+VQIrfyRhHi5>P@!Rp7ISjhb>=&P9SF07EvaMc?x2-zUJ!(;e-ul zA|>6Mnw1{oIHX|&!!e9<)?+M<#CK z#btAu6g!Ja8_ai44rwgk9wueGzjPT+`c_MX^a0-ETexh)62i9~3*EMzWjN_!d?1xr z=)#}pogsdn6U5KsCR>=B#NF)Bj}+2aH1}g3`#y6&=9ReQayFc+2+7!ld|wy21n`rP za!UYVa4C-iZQx5qJq^O42H{v$Zpe|RIe4}G!IH*Wo=)Ndx_|3~++vwzlt!^_)>n3T zgjxvozyJwhQt$+UDTI1phJsG0SOLScB7`9r9v3HsU_2NCt{CBL;q)GoY{7@{kN%dwIg?9x>+ z*rlsd;2&4*;6JPS!K+&p4c9H!6?+#64cBcFSL|gh&07uO$*1P6+ITwQRAVv)i^@$- zAc8ErVt5M{KqrVzOUxqEaydLA)j_zOJ>`n#HZ`}cDQ=`Vsi$lmcJQui!4!E)1~eyA zb0+eX3u@o6NwnztT_H9UjqN~VF%l@YhqFHkIC0}dj+T90v8QCBI-9Dqt+{%NrGv%8 zuCFk&y=CLVPVP@ygI$kRiPm7(XKS!40M;Vx8i>*y9CgMHnSC{3tC~QmvMRvJz#C-(g*tN@`p;<9vfIuLrM6T5-P zE}hs;%IiTDs1mXLhp$8Pu@5+~&ELhQNWrEggG!64(z4229!_EddUidQZnC5y*Ra=a)6%kX7A-M{D`qE6 z_q&-ka_SoOA zVQ_V;>isJkH*6;~6iQeg3B5ExvzB-Ii?+Pe8w9+bHBY!&4?IpMcJ*dcnV^s_lSAZp ziCX;W@>`#E`RxH#9vj=MJ4ubK`b#0=0h9+d=tyfZ)F1$h<}xERu<8s7ks3W}_*g=7 zFe5Xus!UF#1*)ZnUCc<#tLn=USwXF8aHUE;lzbpTaW5l4TeX7~A}MT0kpZ135n>Pl z0zy$0n-LSVYKa{Y7#m#zSB%m&-w@3A$WHu(Fj4gMtD_Eu7^`&}!H<2Zg_`g=Mwwv~fDR_WQ) z(d1#P`4~&f-P*^!xhuCW*3O22-8Oqe9kZc<+0Dq{WO@({i1ugK1fI!auoufon<5u^ zq2mRQ7Ecr>`_jk=+4O4lKSK9I2Y-*nr8@ZgtPcKO$!#W!!H7jB)5vtx6zbuv_piuB zb~1G*TXAx=xx?obgIE8jP=T_*-)C9i@8i9n$zrgV3w5G1!-C8JIzht^^k)zGy{2?&ELwwk`pAsyd#nuZ zdn|B=jy*)@HAVbUW1IeVQy*xH9xZ&ZfI+6^F4A&aaW<|P)NeKP_mv*h&P6sQkw&t2 zqQ#NP%#lW>yv8LjI#J|uzWBAXh2uhxf(t#$cjp<~u4USu-#V7vc)nRMi{-N{m<6+$ z?IY|MBxha%jw=NXjSa8Hjs?yhlBww|DvL_WBGXu;k&iIk;e>)*NvY{rRLY4;BaNNB z$BeOLE{!=&_i|(LD2d5qDh)T#Y$7&=<``4vnz4o6UKQ1JJj<&X7RJoqyMG85vzbJm zfRBdxP?=Qgen$&mw9r*V7kQM{4kUkN6#$dwrv#9y_ui#%7Kon=O^l4ujLnSCmB{{G ztCIaWu5$Sl=zIxcyb&A-6WL0aVI{GQ9Z{TNJPwVL#bONl)q{G5dO5PlY$A<7c~MzZ zE)|)U$RZJG%$JasMrMqmas^HIV2(p3Wl;$!Ly#j0&G>~^JJ0*mK6`(9w>oKMz#o%x zWk3@Q_z*NS*4M%i^!B>G&@gyU@>-`5uXVzD&k+6hUb5(nu@vS#J((+Mm>h;iGG7*r zN0)qrmX z&0ib%Ybnn= zcj@q9d|Z|eE~^Kgb@(tov8#hS_psoExU8OkBE)6&*tn1n;}g$9T%3nxmxIgdscSj- z+RmTl;AK5Knjl`*BO(Nl7S`j^0Et~ae+Ebk>v?-XysT$d1M*>fZX~39ZD+Pq+kt^V zi~Zr#pFI8HLtJpG3vNxpsVTTM*77$7G~h3J7B?yxn%}PQHPB&1Y%D-0%7o@TSAM&K zPO|eW0O5PEJqcLUI!HxX4de9K`0f4b@hcI~PW z@Y|}&;rREYkifz=SBGz{$2y;69Ww28wXQAPe_>_#Y-2reid*jrDa52dRI5s}R9 zCqh@?)@N7X7Q{dn-pFDWhRsd(>}|CX$73LFCX~c;l+Kvy~BU`Wd&Bc>g#4F}`xD~+cwnE9|MH1-A+C>y#TDZIcz!Fy{qfi}IsBJT?90Ke z*F8lgoPr2As7u%Y% zsX5!)vaheX^+{+gPbaZK2pmBa8@6V>cd%6aM#x?pI~$L)Xo!22YV$f7kJLb%&0LltILe)GPCArUS45iXw(># znvavPTwoa6qfvV_t4^cpG;5Bg0+tKxVtHDI@U&QaEmX4EZn2*L0w^MzjF3$wOk=2Y z_u^+7hcw>3$YjnstR2~8>4ZiD*^=#k1HguLGfE5Ujz!zbHWcWF^}_s zo?EvDI`%a9GL>OoVNa9$*ed9YxjTrbl*cB04A2mIozo;b*ETl21P8vgZPnn?Ef!|jjn@G>eG=>8(W{P3~7dE)az)%$I0y3mOX;8`p zRdGYblZhPdlgwR}X;o1u0ELeos7#C6x~WodGJnI)&^#?$V_Sc-sT;*2{YmL44Y~s5 zd1?XGq6QW;^sLas1GvKo!!WALU3c~r5sI^^INQ4O1OjFSVXH2qxE?qkINSAwJ>_CH z4M8{Uv8Q-uQy30F5YrA|sx@q=k%46tiU^>nfrkb?>cXB1VKxP!#S{b&Hy^ke0Syl= z4<7ucJ~dUG25=sT{SdlmUGNdw|D0;}WmM*oHhY`XZ%2zM|3W|b?d@pR8 zxk{erC(O;fsCD}O{-UVJ_o4`Q8YyMV6rabh{EimJez4jLgsa7iiDUrbDlC~5zD4#v z5oSnFL<^Cg$QCw#K2HrNSvZi3t7QcZr*NHq4ewn5&_eG+l&AQU6><%JU#=UV&*M?P zP}#yvB(v?`C=K6ymWFSzquuwNv_nxpnfgF7baVBP^Y;Qu z6Em1iO-C>rqH3VP1fmg8#zMh=194eFTu;jPfUqv*d%##HCF{WWL`v3yG%w|QKpn}F zAZ*AF)gX-eL!knnKprp|P*}^$NnSH7puC~H_brBWC_)oZ%A(RR$!kLLc=Qq(ympC= zA+d=G?>yehN%L3q9_z4><#(T`KB09l8{k4ny|$o9P3pA;YaH+)fVkLxUmtJ~!>`B2 z_6ukAm=XL5&%xsPKGkLqC)s(@V<(917e=DTrdndumYCH89UkzQ$$SfF24$=3kjCLL z$>Xh@L}XE^c;p?8!z3P0c0ND}>#+W|K_)*+_l&GV<8S+H{OuROu)9}jM9Lr&1po)ENOcw} z6Lio-YqY=udP2LJ(0)ItjmATR1}Nbh<^jC9^%OiiDu7^sLn?$ZXA`e=B8dwSK>@Gs zcbUHj&H4^M9tXc4=(o5qUg1-2?V9GZ&L{lSXd#HPH15zREe~7;3b0lV-g_RzSPwc5 zmGmBCnL`J-&SwR2aUGlwi1!6?C(u}29?t^hA|1e{R(`~yBH*W2)a=616NaDU7KTq* z>-s*`Kmdzptb_{RB*or0(1LPl^vZ+E6r3q2!WKg+ltkk2-IYf?*XgR@>Ony`Nn^XO zl0P0L9^wfhPWs*la+80hMZ$AX6ziuj^IC77c${WvQ$x}d8kF}wg%obQ&T%xH^&lmY zID~il1hW_<%R-sCTD?%L+Q=+rMNL^*Z8DLTCoW%YxDl7t5SNveWkqFKS#vTGm?th@ zeVkZW)fL#Z33a0`o-Y z2IcU1!ogKV;HV;S))^emMGqG~QgBxkHXo`X2rc=(!<0@4EjL|GM3Mk z;H~%eVHfkTtR5>ui>A<#g2zqfZLH68h62w?b|_bslDvjTnGyu>>#uhy~B$G?8mP*q&!)xYYaCMLxvxf6fN4}ih}TT65v1W)s==DDS-eoPkeO+I@vhr8*~m^ zzTy})lS)QLulwg%%#F<9>qIsci9=<)6PbqYJ~R@IGm!Bf@4kyG{eFuJ3Efz%+pcH& zN6udtxYvr}smBRtGjZYK#%mZs%%+!(>-=H^0K(1GY*H%0K}!<16L;pE z@~XvJnGl6AjF%;kfC@tde+>{fM6hlF;#-2Q3J~8Cd~-l%m0%5P0hwV~(6xcgEkW5v z3Wfz=8magPO=?mwELigZ3=8gf_?94y0N(o#a3xDpWZ%W+wLL}leG--vKhLl`o3FF- z3og`VN8}ep!T~iNz(U%Lvh1i*WNwSqdDbmf$LZvDga)45MlnXp%j=CyMW)`e$g~m6 zk;WW}S{KNhs>rSrXo-XKVAlS(N|15*Ukh+x0A;wDk&zUj^M4h+hTY z5>oTK3d*_^Oap9Nr2HyKpGd(pK$i=^G{9R6eiek7sOjz^JC?td*lcfScP#HJvDJ*| z?jbvtUnvn0qlz%8hA^RqFtNs%5E)8&CRG_JQWU2Ljt>-*YYc2E24)>$tC3(B)!kFN z+v7eVN6&?Rs_6x-EYOx8>&HX|WSf;O}B{@{k;UMJXYL5^}h3Diqjlu`fz9 zhDO9;(@|;0kT`5QGK)&I`-Oh!g?UX;{_C?S|Mf~@EY@x3FgK_7em-eyos`!BT@)#= z1HLw?83kCgq`VGDyHYR<*lWS-fUpz1_q`Xs!&2^Q%56=#tvZ%>mGEFy7#ookscx(0 zdgOf6UYPZm`EWsNFe(<&lzS@Ww)UQ`JBao7ueZQ@3xa$|P0FOtcDn8NC~ebCx?lxE zVITHc*oSq)rel)a3Vs}j+X~hni2uQsgp~h58(7N!V2KmF_c&H<#FnqO4RG7j+0P^M zaMna?hzVMi<_8XqibJ#hXw?}Sb%thzp%F=u>a?ni=q}pBr+TSWV@+@p=W*=5qi0Z0>(pEajadK~w5!N^LcPgG34>a=>aT@6v{6&@i!u2h=xfl@B7$N8Dp4J1{WSR>7$@2J73jVP72e1%NXcBfv%HGo*R5kE zI{3#`Ebz~(STO5%P^IG}*SQ-kY-?XGACXh2wCxl`ISXP;F=R%B2xd3|!xR8OAP@+K zLqdT-Bp{Hrbq^E(6<=mPbT~$s31g8!jK@Hb!x&_kF@_jp2mu)~8b~7mV~5_53qU&D z0dL;{=>sxdHa-y5e-~2kF_71CG`|9x)AAHH##3Z1Ded6hvct8MA&)lH`oMppzw9}R zYz;{>R0>2RECpJKvJ{R~294&@y_O)t*?nY?g!Cy#ky*tUhd>^%*FvM0!`^0vi$uDDEXE7j33N zr0qHSN)c7zb807FG{U{GI#We8|efA?q1t z`1b0caz%z7MYn(7&8DPeTP?SJnH!kDx`3kD7;!Aa(xhqq>b@_DHm~~Le5_5R9=zW* z^T8d6^+~C{@Lga#;`@{&Ku73RqLWr*kie;AIN1UxA9$QPzA$z1*TH%z6v_`}^?}}a z_JD+^zMnU-FG8P7ok}e;fQrewo()DE8U3@2L$!WsmVu}$1{CxQGSxvV*$sn|Dm0G< zpX4g000rt6^HmYNPo#+H1jB3I-UhR1`H&2exiXZ#PJc|qKZukbEFuzVe+=n(|Fkf( z+N+L6MPf0p>&Q+Nupo7b$eX5DU}$6I(n+Mvmn)WY_pGHP$bUD7{7(k?{~b^UsYtwB zz|zU~S(c`J9VTB>0Nu=+KO*(t5oe6U7pGUdtOt-J<045C0Q92BHjx3+KqUxN>2vX%kehX4v!IEI{ARwj+>XX$d z-i~`>9ezqomc1B?j&V0$og9?1G}S2uq6PaLO!+iZBFhhW|vJCn<}5L|%~R)Mv? z`777k3cBs7JBt-K5-8vp7RvPioSmFywLe>h9o?wTyjKH{(dp|!IX~~>>gi-n6q!hE zq|08TF-7uMHHpsbiQm(P3-PUBh1lI&I%!SAB+CrD2BqPZ)cV0%IU9^D`F*-ne+~IB zE{7r(2oGpc7$4XVvhl+WHrD>Z?S_Pxd@#bBofFxhxx4zp>7?n(0#tlqaGv&bcEjOv zos~3CmVr2``RloM7gJyYJS^;`m48g6Ozj7o1n~J1*FnG>nz^`DZ7p`rB$tLeaFTs=+V$Wz!Mn`eg**?=LF&^;nx1fVtj- z8xH6}?b@3{sXM;^4UAGe_5~~KgI2lM@sx#H(OVEb`^^z^XZNE^=pmeUbZ@MTR zN8B8A1~>GujcV_7McFAtiO_Vn_sAc9j8~-R1E8twl?l%q~<7Ez&AAm0iWe=ewN! z!3jfeOy+Gz@}00Y)G(mGr;GaYBuom>7u0LCVF0}oeVksqa?SbQl?0dx2#Apt6W*3u ziDYZEVM8oJ4|j@o-N`v=TH4ZSyi7{%ngUqZmQ@U}(Ks9RVpy6B!ucsPF^tW#Swh{p ze8VV-pnuN;;_@x_f@ESdQB`f6PUN6%reb@ZpqvfePGo3t@O34G)sPGoO|1Ntn*c{Z zxWA(}oeN7QG9@EE!Dd|nXf)?Ou*}PZY)LGyW!V@63be4x%w>v%=;`bbVq+&Fk)A$;MJ>;zwlgJSRL6n8x}GM162UJB`TKW3uR*=?j1Nr8}G1{yn8Z7 z+yG{7oqLGL0Bdw;Eoq>M3fqPvVIo7LWGGG(O!Tq|VK5F_#M`w14TeC;Q7F6?nF1}6 z6(7ao0$LfJNM$B6m%R zO!4GiQbLwPI0h*Y^ov*?nRl!oE*27-@};{X0kxU|l>JBPd33jSL>45XtltFz9XI`D zAXLQHs6GD!PA)RT%~7x`6Q_;)i^9b50d=6@sfN%fQ}c>oEV>T85w^mLCf(vifPOgFSu+(LH%KIg1Q|R*OG$&r>y%LQEQ0K886iF6SC70oGf@D!cQ_*u7+-tbtZ9Hkbh7tMWi}!R(20QYz~Hw{WcW?7(%I=7k2E|}CW>K~2) zvt^TgZ9dJ;l~<8nsTA@VxeWmmAlPnUn&ENfAo?TcC501%7}fHb{892i}|dZUEH#d6opv%~L7+aE=|$=~o0jCN;H`UzxlJ@_`1g zJ?rC?%w#H&7x2B|=^1B#$}HMSWxxj)8Eq=!>cgUT%JrrMNLyYQv_0|?(w0jn8TsRT zU~sT@j(+>dzlF_@WSS*s{IY5E5vP#zsaP&@{Wf{ zS$7g{2q3xACU}32OH?Y~%YdM)WP_Su)U!)(fQFBBr}kH(4H}I6#PsY&Ci|V=Eu~BP ziNUMiL%K;M$>sFSxy6Vn6P7N6l%aqKU}Ua_cr@)mpHYapus>85pT@lLk?B+ zOfuk^x zN_wqUuOJK2*)|ff+KyUK?>Tnr%f~*;Hr)PE@2q>b6YFA0_BiA9>HpP3&-6nPReF?8@`gF4Tq%T=`UGY8n1rb8R1 z3%^ZY&CT3~x(${b{W-*iiKm9^y*_@N<9I**?%K)$eD}3sj{BtW(VNq#=W)?7UIH@A@&vspXXoQ9GP_59z$v*(tZ=*H~Skv}^Neq(# z+~ymQ$`cW&Gz}1rZ)e#6XGBj2FlZwOm|`*k!D+Sm`HaTj+6E*=f7asvQ~Ut`Isk|y z=cD)lbKgfst$|iJZrm=aVxu9Z>T8#6ipifVs@Y_1*N zexQ&2mtKVkO=ze$fCtOKP1uc}d@O7Zzjk{8Wfpx}k_z8+Yw?wHiS5dz53c+IebY@m z#Aodg7+w}pbFAe%;|P~uR?i!l5{plPL<6#`c>ZcF62~QEIAfA4wgi})p zLrcuGD5sO*vNd&*Hx*l`L2yUPt8{%s+Qo5|>3F;S9@DFDS#>q7aGA%eY& zhKO)6u;~~r7qjm$OMpDGg6i>yj{E~$!E2p?x>?V{TQL(l+OfLYkJ=g8`VYc$V^_+| zd$|-7UakopNZr`Y{)gnJLvbQk6ywOe{|KwGvCNANSZJcKiP-HYBx02|X1u=c`BszA z7rn;-qB{sEOPl#>S7N6*Tek_LA0Zbv>UjXkn8>5Hr#{Zv)DsQ|%q7;a9^ij@5y@lG zV{SK`c5}1>mK!|W_B)2c=uB+x&ZVN5|1BOf(4@`U25h01Ko)7C0Kz0N2B<7mP9l;k zDs#PGa%$5XBZ}JVgkFocKjFWj_Ptb6+{?Gw6O~mf*zp5S>9j= zlpSV}H*zp^zY*T(ywXJQ9FQtiz~6frpTzx=+mj5;P#1MWfrKc*=M^yO*~Y7?g1}Ew zeK?Iz8dY*jsQPI#(6{ew^QA(IJTSlr1nfi{0kH=4BXZ3w*_BGhg({$d+z}F14?A6f zDyTN16W?MF`&6TtfT}6(seT2}Q}y)*nDa!0Tu5*?=BN4u#x#FW%Ix$w*o0^O^KQ}= zJf8{_$GMaGvO8Z@w41`;3?$HT`_@PSR=|m#6rZ)!YW!9+c;2`)-{m#X=V1+re_`pq z9!JB%^u`FeG`21Q`!$k)CG8~iuwS_iUd+<~{MHbqiz9e^XFv?%O3p1Bua7p)`MJHR zXL~hzOP93v^aHC!BxA9c(K12lxjYEa`A=2FajhC^B>n3fQv!R3#2;t5^zyzU&INT9b=aq6E*$|vFT z9c7IH{mr*k_U_I%o{{SpwvQ%3(vxuIGR#;MMq`2~sI)+|Ae=WMPIv0Uk_F z73}2{GXMV9)F9s!rDE*Sc3SXPhquF^puf15K!781oLyK}5Y>jDFE;jJ88jYPes?SZ z_-KGG*%D9|ES|Ylevd)SYQyglvYjiv0x^A)E6$l2#n9)m*&!7)azuee4R z5Zm~R6lxRA{Ogg{|=f`Mn$X8YAx;R9*v&h~L|%tMOET)U;R z?5>%CO}?;E0NqVpsv;B}V<%macjEU+3oZ6090dpXfPPE^3Gp==LD%hS zC^)lxtSg%6df5lLXz@_p0_(}kA!I%WBE1X#J>R0C=Nv%&j!fLTGw2EbB`JZqagJj! zd_ItKY zNsIEqKPspFt3;#`eP`C^fSYILAS&)bP_*K5(kMFZ_c!#DTP11b1>$7Jxul{Tn(P1T z>_KivuS&~V`&EMCYFy832+s2i~U$-VyYeK!?m6a^lrN=NEe`{4^RA(Y?WF^xHj9LZidTGN^f4 zlpFZ6r99HIUh5KQF@h%4j-DhR9=6{s1LX~Nt6BZTO?py-dfE!UVJj+*LbJUx!U$lK z#G#`gEta@P89`?X5rMBmM);nySJ?jmv?HOewH+ZEB=~h=roZ#C<*Vh8pd=w;v-y>qIf5!H3SF zOk6NHzY3)6=EMm8>Z|K{F}M}7Wk;W}O&D>Q$P$5unm&=X98cUf$XO_Rpiw8{MiV@n zmI%6;5@oQD%)>#^jR2GDg~28`@}6$XNO1ZBtiCXGBHRIl1@Y>E2Nl{X@EN1|xn=b{ zI#_jSgBo{auYBF>Y_7e(>3N{pkt!6Y8sCtq=bA;Chq>_q(J(g6BG>V*OAhqIhhu|T zvn-t2HmV52>1>tq3ejVHeiB9~;@cwsx^_&!E)5$KE|zK{o2q?^4h(YAT0%$W6<@7f z_D=_zzNjfB>Z2CJQ|AZ8H|Fb75I!lKC-Q9eO#;J|c0=1pP*uTfZ%#MG84&&r*>DFR zkk<2?)997wbo~}f$V+nbsbpWA$OO-fHm3p{hLcwKcsj(HDAJa>b`m&Oqsg3Dvp=K5 z5)|EzR7WS8*{oarTM3#5@NzMrHlVRe9muHILHA0R*{4lbeO{T>&RaKku}Ev?L<`xR z<}7ry`D4*XGbd$N7s#@a&x(RBfPNiU)pISYbDC_K*-Jm|ZxKuCK{0eXtI?yxqd1km zu!Niz6el(VM39qt8TNs1Lhz2$Gv1LCO-(G0HJ(9>oOtt!oaQ7^=ZNHp%SsYJjw?>* z$f<|7lRSox)ABh^hl2oOjktM1j^r-8EPKhk+=s#lXe;*ErP)07V^(CYr155Oje zoVWpV@mm6C^R!j|Ad8VF|Jc2ICB#3t+mRD+MuUzk*6a=rY%^qK)Gh?B_I|TX*R2zw zcPKMg=u$iS0zgkp#sgD`a{4dOYmvv_0Z!5J|D$?FryxVXg~H&r4X|8)Bc=fZ^8uGt zBN(t94SNn4amLm=vhGp%^E;nDz>@`LAzFywP5 z+Db4>j07x5Ur>Cu?Jn>jxnNEF8jspt8b8_Y==iByJn`;pj91jm$sPjAsP>Z4LzsrJ zMA-a%&OV};wGraCc9OUcgBQRovpEX^#$N=J7^TVH7oj!XcMSb(v;aZyvPLQ&B8 z`_z)YXjl7{wWq0Iu>f%-dK1|nv;j*Z!NUiq+0@SF=-%OIjF>(mwK-1YnO-dG$!O@! z_*ZlUCzOf7U@G0VIlqa5D(_mEf{mHg`lcCQ@zPTh9P?2~RGNORUMtOG0wZ&6G^P`F zijtkE6Y&OWSXOz+Gh2U9rsp=iyDNC8cE*Cz;ImPnK-XPqcEi`G3zLHZ*>wT!V43ff z>6o{HHp1b)6 zk++Nh94m*SsT!}LlX`)L{3y>MZ&JTQe z63Fo8(5LjuKq^%N2i9pk^8=Gr)s!}RYG@YFi}+J>*Q8AE^uY#RMN)ywO;8fCpuKUS z*IChpSg^!r!1nv88?@?XtuijNTHG#lJb@n=z2hV3_2H1?6t13rww6FcjOC!FPfr+ z^u>$E(>!WkUR#wHj5 zUUwbTjk9=QTU0HkFr2@%&?2~jUd%swMB=e-}v z0nixO0CqZnxQ8D%I!b@~r!WdlW(3Yxzg1i?LV~+n=^9b510WM#K0nUEisSsA1zfZH z%^(W`oMq9LX{dkp=$km3^|I-9e~wBz-(XJM0FmqQ1qutF4ua_p^3Wl4TR)pZUj^IC zxWrlM4SQ_dZtSnjq*}a<3TfoOs=<{?-?U0SB%Q`cXgF{orT9^>$1{C^Jh&`el{|B^ zZA6e_ew&yK$x^Ql8O)I87cH!D3~xMMC18P8RxqgaTJ*UvtYB}bBhb6wT3>x|53Aa} z#w{d^==KqBYFb%k?DfgNEJBV31uc3?xHlVj`=b%#zD7#V+LXJ zQw$(!J=U{OnX@`lc(9GW7Ci3bw#{4h@bR%EVh6=4Dil4VkPRHn2)F0FUvQHOGecnz zE!79vFIyYGd4U_Tr`9{-@uRfE^!U`51uC9uWw(yj3ct_>x%i2Rj16!~NDriD6TQ0G zCc!M=1VA25%UTAB2DW=7xy*tu6AouHyFzMj82>?dH=Iex`pTOQK@J~*OW*rY8xk2O zgQS=<$)uG25DTk?R7hI*nnf`n;fSDszkJb#G#mSt4=@q{ji6?4Q_vx5fP`7P4;^fL zW>7=$XGEgW^i6^v1r5W{MUWt|qLNUN&unSXy8%^5Uo_d14py=@&e&ps#hq+fXkl9x zegJYzCgnO%76{lUU7`*CfYyDmxCQBn&9>PU{s^lbTfkMc&&f=WQj$v}r>{CEgzmIY ztWYQPW<(_#g4+7`i*17nI?c*SCwYL8+9-!)g5H+eNy@L6eVu6j^S9%V+>Wf>PR6SS zheS&Y0MS_cv%>lmSdTp`zL1^1T~fY@8xV`;*F9qcTb}mF{=Y!z6s6%}w+jV#&nj&k zb|5DFUqORltyE{#FRAa6Sj78alD%qsz#^sZ5j8Mkh?4$Iso+_7VM|tA3Se|g@UqFT zB5X_((Rbb$CU|h`zl*qfFl!32N0&gh-DypwtrZq66N;3C6u6TSsJSbYcUMfEdH5>5 z+)Vr$1+g1FHv{XBGju_k(A?4l+z3P6Lcz>m4gO)t>%cZ37gSyzZ)J6H;TX&z%UP9~ z9WLDq)Aq2##U^ji_XqNhnz8pk;9 zXc`FiJT(G0g*{rGBCGKziCmcGuA2&_EeD+8A>k*?Mz=7zFBh($Igr#PM~a}Yo+;kH zXg)aOU9F96P?s!JB?hq2XSEiW97q674pa)unSW?IOf3WA2I@@C;v3Ga)BK-Y|8kPR z-lp!%@#RkkMJ+Cwe{;nf4`WB0sx`d^gA%go{5l*-6_nZR<8$>J$))HS(pDFJ=hNEAG|%eKZsXCX>MwPX33XAGO0&ee9DpeE>o(QLlUZK zg=r^7Rmguz3RbqsDxN=H%Z*&g?f4xiz)~(OIY0I1T8Y6vDAAk@1 z2TAP1U>2@Z%;*2`8X;J(+3NB5s z;ZF|+EF|ht2>k{FsWOXm#gRdt5*StX!D^Jeh06iweWSv!pxh3fV)d*IqE=3eZzgT3 z;&4G&E(<5zKDj4KfRG=hzsx5#N7%$=!_@WK%jO9|vk{*a!br3ud51PHN4W4ZR6^nf z!znF;ecH;f^)&P9+SRgqfXg)E1&Qv56RkycjHzzmxJbNgb`B7KHhYuU*tq(FjfiAf z+<|=^Hn=O8F$-gS7EP`aecpc%2Ru`gUOxj=tTO19fQvEQ%V-O_z^f9fIV`v@!Iqug zogd5eLFuZq*dOkNzqH*f$l0F36_I_o((OT#i4FC`j!ZWI&1ho=`OOv2`axy#c7ju&n{=#=m$BzR7iFL^?{EsE>_b8{`kF3bI8LqI3OT zIGW&6a@>7Cnwvb)SSDv%0@bMZzzjjLF^36i^Dkgz`ZDU3SYF&}HYq#U9Qr{K$9G{_ z`5*%?HPoLWUDOw2V~c#jn>Wmr!05FYyNJ)?_EqGct%Uh}8)a}T{e^OaguMuXRM8_{ zII_b~K8_q-J3mD!fDZGt0_6P=YM2?IT};o=+(G;YNxb^p{xSC7Kh6D%y1D)lN&0`i zK44FtX~&2`B{)jiYUNeyF1&WT^TgG+V-F&P}5Kfqex`YDLM( zUPJzwD59MlJCixoz%h$+_wEujD9_%yfIT5cJkw21C)EVCM+o3fk346BBWcoks6?!a z?1mM9R}<}b_-pZ>I3x8|Ie+Zy5NJbJQ$y@~+}tF@yLiL$p9zM(Py1_{u;a~-H{4?1 z5Q-(GiwXHxzFS_F(_iidG#`wt;rb*1L!sI|HNPKE)7&*L><~!{Ur4h!mW-_-x{~5} zJ!V;1d>$~+`(%`zITD?`54x^mg;8Pc`&I<-MV(j4yzR2+BHvHklaA#@zZl%Sr05nD zbxt~Nc?%#C4gZ$Ly2C5ic*|}dp;YdAsZE6h3*aa+mEnq}U zSAOvgE*}_4-N*c;YVtZ}W6MAO1}7K2JHKaKTeDt5NX=UT!G#$?O&@|tMpZwY#es=B z`YOa~8_ax352StUyC-%3*@yC&2(oRmfa)HuaEE%6de=E0!DDV>m5c(bjChD!+E+%g z5x~k)D!tXmYg_-094-qz+AtDaEHvc%V^Tis0)c{_n(7bZ*7_sj`71A_NVH}#)uN+G zwmWR_Hvr$orLlIi3JYHlolTDww--%Q?X{cDg;#N*d!| z56NIq_9E$mQ7f#n$`ab%4sb)>=yvjn`@>3FvdqrLHGd$Ll|wU3lA?q^U*d}fXd_EY z`7OpIiAZ`TGdpClfuXOBiu7&EvW9}{Q3vg6);WohJHrwaT_0k0^zDMYgyW7xD8}uL%fS!2T+agWjuw80=PaBT{LseOA0YZ8 z&T(y(C~25dJKsafAo^p0@>FzcC(+NWh5Vl|M}FLQVa?S_oWwk8C*T@co;lJ|cj{&K z9^BS3yUsm!p9r3s@FJzr*TlgT1YTrBP4{*lPA&mCsXz@XTn~D?r-or~I$kUWgZ34g zUJFWE6nDl{AU+$?ogz{-6c^UtbE5^VFqoq?rB~j1BFjZJG&eK6bREd$3Ep_qU}Ld? zk&?R6s658NHvpd&ecE?HDLk`ixNl>=#U;=%Z2043OEaQ0 zxcrrtMgl^{In5o>gxNNF?Cu1yIlZSA)0D#3`iKtMR?`H|Cpz9n-Yu}jaA1$XS{QSp!#oG>OWcLc@XEQEwTLCG+AO@Q1N zu*TP$3V|~U0Tik3#BA3Z2)AxjQI~C$pxX|3aJpI($SQpQVTfGonw$t}k0`1B#jwr& z;-G!XJpQ6^=x9Hc$>PNdwPCiq?K!xq$_yFh2A)P{VwCiv-&Wv-E$?42J>ZV);HO;ITV5lJotJIldF3}>2}JjDv?Qe>fY!ZmSKEf#$rl8T!@4i3;@l zeBiMIiTg71U93U=avu(iV3p*K|#aa}zDbLo|q;x#3L2N zcA0I3+DujtS4S@Jw2`Z03V4AZnYIAyAUUZ3YPx)^O*v(cVWUQRA)gakh|?YfIhv?G zWeVfO4FtWcsRZ=w0J`CvGgVNOoo%r};DkdWJSL7Y*~GL%qANr%jE+v)hRG~uX48>JK{ z13Uv71HRU&SiXkSy0mhDo&{4Y2l(cgR2k@TOsWigYfNhn$l7s}^cv8Hr1xF|Z_)yL zb&4f6ZGnBSvZZvCDkNsKDkECNbd`#R(pe=e1;iK>vBinl5>=z+iJjpm6E!ZmaNq(0 zjRP78C?3!*pdCTUKqV$^fz{R24$_J&hXI}|Qmu8$fr%Ob$*gp|;CST2NcA{jmox^$ zfrSJtZd^?_GpCzb6mJ&&JxwhAhWv`8%%naX@B!qgfg>PoOB#dW@WX*0Ko)3Vfg0}G zxZ{L{$WL(^dDq4r>@-&@?{$itb7js>Sv`=m(K^MIs~m4l^Et;?ymqJVJM}W&!>*CZ zBYcOE5{Xn2!KS_ovC2IMA^>OxkT-!Nwi3IIUG^?xUEYX3J@#7m1CBQvjCz3T^GPW2 zy{rBPm3dj8N_on>tW(Ln38C4_4J`UA(R4%7Gb-ImH<;567NwIwm!u+yilkT2 ziQO>v%YZmniC4A+u5YAmfSN;m$ z+!ea{iSQ(EZ0IJacxXDpVmd=0lm z=4C5OV!mbjRElSDaJyvnstzVCrsOf-aDi)CwDpD(PZ|!;0OGDZ<;}9JWSYM75F7EkSDU4`mB zVf7+h7S>y}f)@4JqB2NkDBT#jIpTxXXi_~|bZAM1N;E|cstGLtZBda@kx7q&93{j)@TClqUx zjfJv-B3twh;GkQ5+`6jhMYN`Hu~P+$eK zlg^sTtxqa!Q6JEn3{HpypQ^h`0Xbt7s1_qFM^;dJa9N*ZIP<5B24nzfS8320^G0(F z$x&qZOnIe(&KE+4&y@EE6K#`;S9Dd1c}eL=pp%pnBqs^>_OOF8hugQ6Srbqu*Sj|M zNU|}R4Iv3Pk&o`&Hll5zqI_NoeL4YUGO5gW0BE}8e)GhY*oiA*%<6!3MO>HbOiRep zR-v%Rs4_40063f^6)Cy$<(ArGO7X&F?rjtdqog?42UDIEAyYB9$WZPmf-5VA6IbxfVN!2^-iArN0orOz9Tse3Ol%Gi#YK`13*NAj z^cqm+p!dGhgPVGA6wRx@m^6V^8PS8CXkJ}qVq*dsic2a0k{ zDzKDA`}EQ_4Urg?q%g~_rc$g=L!}#N(_N(}+9wumjq3>r$!4jA0tEp|LUBo5UK?EA zs24$AB+Xta%&swb+D5TjzR%Pd6aMj2${h@{QThQPnpHsX5ZoT&CH8+05d;w z{!RDznycJzE)|x>vuvlL@$Br^dw*Z|q;*+!pns7>?ZB6xq<@~73M~&>;4Z5&f;HtL z)>#@_w78Vwg(lU#Pr5g1wp0LA_DWSG9as+XYlHko@leq|y+}4n!B^|!#KF-Muni~R zs213T5EfT|RhhtYkLVsWzRIrLPEV$+=_}+~T7w09OGupHfSr2vi z(Pj?O$K(J)KZJ1Dl>w`+2C94N{Hjyw{Ho>7@xQvD3{Z++C>MaDd|tY$+g!5#3o4cI z&B<@N?xCZxBgFlm0bu-_HrB`^OgOTh?V%*IXh;j)8RXpK#?7>6W?3m8Ac7{DJKuDg?Q|7XUHzq9F@&S#^GBr@8}oDo^`%()S&Zjc2T>iA zyOlpKCXL4Vrg3Jh1DE4B&1~NOmt{IB^LO)SF&B%o&My{cogc?vN5*K+pU2~^`27~& zSTkFE9*@`iL^TpQ+jUSJ$Fi|Le!Kql@#k=M{p0xOTpZuY#`=icE$?G}{C53~_3__I zBAianb`JM-K5ne(wG1VUFUd>)WPZepgYG= zp!jzEZ$>0t6a5^WtaGE$5?3zoV;sjdG&MFfHas1fFUNRH7 za`bvTvdr7{K8+)ZYMiaa{obzkU3@w2#s5}*IgWo0r&JDo7rHOwaC~0s;6I1UD|CJ? z&gowGuP(d+H(aO3#vd^e8cpO2yzhW(6oiwcvEL{g2My}Q_qEGbvXjW8Y4Cm$L5N)($n;hvSj~(0O zXfJnbljD-dZf&wm4((PKQg#iO?>UQL<=3X$+vLJUZ&GQr=Ub<1o4t3~s>HK|_noSQ z_cI4vN2u-TYNfZ?6AiU_zFIs4oxQi|r~}^;?g0#4;rVcfbh?wKPWLv~m2gFL3H?mn zc_O=V{VV~YH6Jb3UwOQnM16VS92k1e4*P?l=j^Obz_9*ySRaP*H2^qd`( z33~53e5S%g9Pwlm~auegU;)I!)=no@5jwoP6 zfg?Xg`hvS!6@y7-o+fBGxHDB%@}q(UhAdFTM(ZKyd?BR8)!nKGVD60}0X;id)~^RM zf=dW4B`6>>ZI^!PW#P*0sqxbTl;fXH&NY5crW2_18n`->)ty9Om(+f z4Pik5R33HdUDz5F$HScp$HP5jIAJcX8*=3bMkN{+^gx488l`5;l>$0ZAjRGua-5Ly zV?!c$)G3ed%XU)54~mhR)fUNRj!)X#+ zXE~}C8hvZY(>NKP6~j~&cRIx(3ZN%mlpLVW8OJKnJ7(IxM;r@pA{_o#A~I#Rrt2NZ zVQt?yW5&dSm*~I?AXvkSb|Gx*Eb^kD@=M|H^;RDfH4rnfaZD)KAxLX7`C&|Ex8XLIf&?DDlTrViHis zf)00y9oB3?(7B`m4?9$Y!w!T;x_mkW7n`=*9mLHgZNK1h&8m`%!2PqP%hwT}=2fR% z!)mK|&wvLi`jXdyekLHjOGr@~kU-Bm*REr0D#CWjJEB25F{QY&oyamp9()Tj+reT` zCDsEtSTb!?=!pik0@Em}DDsX@5$kL;-ho}bAhwkPQ{8E^&+4_!L*gRt8WyS}x%w%w zWUfvIpVd9}@dMBtkOVLb`Msw`}( zTA7s>z9$InyUc)%Ct%UH4-C%OJDuG%D zxyYn=o{MFP3nE~Ofh8Yl51Hg45H1s5#AV47jhKg0@Q)*+q}7Yu7%AbFBh7@zCI%@- z&L3vReIGi~7ES>ZVb|nNa;{`#^>2rYC=b(dfU|Bqlg^rKWN#Wd*3t&eFR&~qo1e%sb#J5U&!tY2z*6I4Fa|l$MTw@B*Rhrff=&30E zD?LtNtLmngDXC)}@0>=U+AST^FT=`Z)?I=k>gt$k_+6E_oHT+j`GnxW-dvKimmd(F z*5R4>KoL3mO(tUB5 z#B36wezkQVmle1<_C|?5`fy{iC?djM2VqHuAO@>04lzW2=fCoD@Ot8k=x*pGsIgILtuU-I^81~vDD8`m1G`O z81^meglsxD7Oi!@k?0dj$`$A2SXkyeM%iE`y%cVVCB!W%R&fDo(g64v@UByl``e-23H;!_rm`f=Ro;O`PRV9g3GnE=<|G0>)IEOO4!oUjfe@!{EE39aCtv)>!ULqNIYw=3S1r{{%o8#~snS4AkscD6 zsdEFtk~TNzxQu8ZU`S;4JedBT_C+|=bKa(t@zJNF7g!P__jjIN%jrHOu4>#}OHBu# zb*%CY79|`Rv(Fu=hjVq_13^GAr!p~2ZfHqWAY*2$zklCHDdt52TJfAMH}9H}&}2fa zMi>hag4&jl2vD*P>mUpr1Q4okrk4RKP29mK3lo-7D$CMrJgqW<8q=qWjv%!r2zt!# z%@L}LOUOXM_c_Z-an6eY85k#w-4)67#W1n2%7WZ3&#Ed!ENJ^H0`1TtXSd)?+63E%*H=EiaqZq?e{?yj_vC|x` zHae|Lu0-$@0`IvwYFqH`fn9vKhed6AwS>tUWlpiLMxY1k7fM|Z_z@*tO-G@!Cj~Ay zJvII(7v59Y!}N|HnUp)ffVA777&B#&_wWJ`z$zR?cwg9t+WJ!1=3`M30)YTbVD6=f zf?A9s12fnu9I@Ojas>>4KE~ftLbNP2fCFr18sX74{=ll$g+BeU#1x0mZvaAu*UxWf z#;R%^{cm+Ti4j{cKbbbmaN&P70GkEY#{4^^cAJlYxg(fqXMM#d? zS9*M)2ADRqN6tt#KAaywfA3%FS4NIi<>CV!9VsUc;AYXX@!fU@?DU`6)d8?H)fZU^ zd;P3fUu~zEJ=1)DxsCBxMaI2SnX)`vBzPlE~H1d2P0VRMw zz%%KPvr-I=yOG+YY%Sb6cY+M59nd9@r+VRfY-iRU38_MQB-hN|Kd}uJdWNP#DzG6l z9=RG)kI;&=;uj*u#56TS3it-azp7Pw7@EY7qcUnxa0BFYKv*xJIoko?sZb~-2BBOF zod-NbLI${7c>wsHh^Y=<96H`M04&kijQSq{S1@3r>t`0tQoqH~lHgn?V7?z{Jf0mf zvlshBgJu``zt=HL+mccR6&Z1$6Em&olo}Uv1wv@W1LYA`gO6du@wmgC>Dqp~Bv&Yl zWg<{Ei~5%YG?7uL!fN9q{_y=7M6=Z2sUSL_ItnEsOOu0}8^McOLkl&sscNtc&sP0` zF9#U-pEoejY2njHZ}`F;I-mr^O&93SDd;d2s0+g&X1z>3@D1s&ehAPl)>@66@P}oC zUV^q>uS)aTxw6ih?g;|<3F}i$lV9HD6mFS8)r5gaHpDWlEqizNfevCnhh2ynIdVx zIzybZfpXmW6Oe{p3sa(eA%HU=;w@n?q2`hgWO-8iEzeZEdq~a8`c7|C;UIs=Q3}_@ zCVX1jP%sXGLj~DXaxp2YIh`uVyLseMi2#{te_xFuW5acp$r+k@-O+bcaxq^FZ`d&8 zg2m(F2~Ca-506=}b*68SWzqUcHk*2g%d^)CAs80$nb<1H5;DX=81u|(%7j}@!5|VG zC9=?2@NlG!L@zWACp8n#xPApS-aTfhtx8o>5<*P&?tpU+13?*dqnq&IgxanGyMRPA zc_IrCiQL)HKiPrm>NDB~BT3Qs;rdWY*8oO9xxa%7Si8xleCjz7#)k%1J1gd2Kd2lM zIpPEQS)-v12Qzb*J|n36(=v$S`=Qr)$P41mHMG8hnZE+Qeq#+(s}AAJSl`W5@xYiq zi!iXz0bIgpDNNkxg2*yum*SEkcY?kPaE5i{q8Nkc(1la6kQu1CfYOlde*znZ8gzHu z^FVoM1Pda?5Jc}7PVk;DZL6ST^Bsb6ktF7-f`50e4UhyFRfy&Zsf!h|GQoV-17^M@ z$bX}j)vG=<^U|&;Zr2Lhu0*c3#>qd>ng1SIrrwXM%03ytP!KlK4Xmu#fNM*=6d~e7 z{@?o;pLiP706__b0#M7&0oB+!VkBn>6x%byjnUXbZ`X7%Bj6nE8Zs7TG8zard_Awc z2&wt&ng)=nZ*|RM;*on~twlk{uu|ybb+bHq42}{_Z!{=>?Cj!;gE;r@4Y8yRj`sby z4XzR9nNoV)xp`7PHSV1c;@Wr?U133^v6x(3kSTx_Q;dfmhTEiKcgq4Y`i3r1lEmth zkqBj%7+NQ}MsZG)_TWXxd9cP4?1T}7t_SNDf%+xh`!mp>hnwLSgobKA_7AO}e~a!Nixc>H2Yv}rbHZLQt(x1h9@mY!gFp2L~d`;ESMdz;V-P{veWz`F*q1Z_q zEjA3Sse+-z?VN^!oOsNM{6UR8CW>9HF}ZtvA#kftEXy7@fD&66`VyNXVtmK?_zoVL zqz>U@A@ZOB>b7Rx5*ci8XKu8!HSpG3Q6~ zd0?1bX=c*i&XDVky-OpQ(AY))zi%*8Ty+cLPB}Jlhf}~D=JT53OQZ8q4pd%mpn!|I`EL@M231eA@G!ZXjA<~C@97u73QPj0Y<0=0gH4JE> z_KBtK*PX6k1Qb31)<=DKYIz)tUX zgns}+#}XFLo z*y4u|W+Wy{w>*p^`hsHffV;T{v|71ws7qMSc)D7^Vpx7z{ zpgPaSbib+sZ1({g{1h!d0Evd11Dx*C|8y9V{{No@KwlM&mad7~_eGW@DCngE~#QA6w7q(v z$PDIil`4Zp@lNKxqfBb;g%X>lx^T`&-{++8$~on=kQqbC=ZCRHF@DlGsYSZ?yNCAr zSvPxC27Aw7sK*L16oc1xR+_<3kO_j^^&P+C{YfvKmAj6fbu;T`_!gN#@q1MUd#%ab zEW!>M(*gQs@RW+Kzz}hI!SUx^lWlzfJbJ$oHhIzM@0x4fL$sjg@0E?rOgXbTvl}@# zG84&4EzL$|-jwce{!Yo6g`^0r*|ofB-K>GDq^Q-fI3l!Gy|583GbsN1%~&Jhz_18M zbdhm+mBDTX9p1nlEGDnR%{W4LpVAhSw;PRyMRSJ1-5rrYAQ;vi;iWIE)zSNfwJLnw zWo*Wfxn=tA6~5k!ux(aqnWX{wOAu9Zm3WG91E?w8K52`f?uW0vSGMRSc+PCiFbV{a zCUZz$b+*YOnMV zPJeG1EW$8|VX#^p34&qJ)<`V|P}E+Du_*NMg$rID^j*VDFGU533H{8)Ug*1~EDZz- z87xhVaKVDLqBvU%(8@CQ&!z9W@0x zCA(Gs@y^y>`VRy$kw_#F$fXdY;C0t6ijU&6t?iWfmoqE#r4W$L_2 zugD!7szfFVUQJ3|=_#CVq)Lh9rI4cUx~F#vUKPkh!HP6R{u~@?Ra&^Zkb)_ANld|u zf>(Y}ZQn6ybK2TiZYUHT<%PxIo)qBjULc7;NR5Xpn4i30h1|hgmC}^4tYCf>3e;ki zQ^xX2ll~n;WqAGn|BR^>Wos|8;?b4pyv?NvHoh~*2hTIJ2Ma*>ravi(h%h@jAY zN^|~>GiV$S4p86^q|yv6#{rHiEsLu?;12^w0Tvh{2@xzZ<|`9$OqhtE@JHh6LM)g} zCJ_aa1c8-{nL)UIj~>U1a$L;pVrG+ew_TxUmL zt~*d9FQ?}Pt=TH={+qC1Swv8Yoe)?!fU31Fln?6g>_>61>t~ zce`4yG}_8kneN(74PP?T2xb`V-L=!ou!9-K3Qrlv4(FB1r=2z>IKAbdm0h+N-J!aOe6{* zB9Vv#M^#=)@4W>d{FxMTu6v4%O)M2o_QVfHW{#ip;QBv~uwVEoQrpmjP7rm-Z zL+Dew?;ajFf7et=uSVaCv|(7ptCo_WKuq}W@`6uZa3WTS|2|Z`kyGh#rj}3vLu3&_LdE;DvLyC1OhLJXF4=`Y*R@p!t1=kY5F7*Q1LZVE-g_UK%R zGfpRCi{bQqOc`6uq>l&Xt{)FdH+t=~nC@k;X5GxX*{}Vzw{A9JM4DYPJV6~! zi=xz^p)n$rnMtwA%{a>FQXeDI?o+C~o^BD=@2+7K-pB(gmDT|dhXWDVJK@hCVN~>v zQ4^Dj?w}?wi)oeG4c#JaTLHuZ$+fK&_rB`}VukBn_bDwvgc$->W$lCut?4M5;bn6c z(P&(lh~QdUt|hj?H1@(;^}cD7s8fkdlhfX*NYmu> z>^}frflN~}D|FI}zKfPdC{ch!0#(jw|K2S@qDcDFz!-8fit-Zt37+6LqaaW4)}Vp< zq?DpGqq^IIS5`+iL@ny7Ju&F+i6j!4$P3n2`YSV(wc6hsP-IyTM3AAZb+HYmJ^Ira z06&dJ_ku_y0+BMgX5_neWQj8}2Cr_FdT&OC7R9jzr6r@S5LGIb3lkP3EGR5Yx0r~K zsEEigeYyn(3kyV0I0nphbKy_--(9Dw(Bf2Rwf0JRI%js%C@dyAlLDwzmz8PIvKr9< z06_qw5P&c+C?b!?BO;+trspWw4-^0cRa!)VUQ{|4N~BSg<0y)PlrhE_LkKa(7^4(X z0Xk&^WaN@WHozE%1$RdstZVlM6zJ1M2r7DAdigwXTsxO6z2IHM;0An7$#*sX$V)OK zNZiV7yWv)ZbvU7cJk4f6?EfZZ+DC)J3x`ufI1Cwg<^K8x*b3J6w z%#k^z#{jNENV@?T?lUfc>?b_-*&>md0u27)^o$~=hpiBCr?o;rlB}J87%bI@XKTsv z{{SKt?!a^ z$Ay4z7W0l(LWFB;FCHzgJAiUn$`Fd2lWz9!#*Uf|o$9JU3W;>8;uz>@z|qLDZU7kf z)qtVNT}y8XjgjF&vqPe^D;Q5KjM`v|q>;RM=K@jDOW~wCOOsi?U@XE8%IM*Bfc9o~ z0m{H(0ivjK%>>5E2mxUvsY}~eaU|L2p|zUQVrhcJ#$)QU{!kj=9xgo!zkJ3aDIHEB zh2E1vNZTNyqu(TkM-&U<4i!Bh4)ZS)H?Ql5XcXxyQUU`IoQ^$(eiS7#4<*w|(~ zf0JVctS&7eU&20gtU+mfy17voiAi+xM1;anV8GxU$25(AE96yL-V(ISqB&SN#Z4Vy z57Pwx2T1=Rq>#vQnYygSM~Yy!GjT|1$qBVP6s7$1~3(H+d$Te1Tj&H&7!$kP-z1^wua)+Za)aTN4 z=g|SaebJKuZWkiM2hc+NC-BhJ=1M*dFA1P__Re;QBux$d5+;MD;T+i^W3qdKOv9xh z@P@cBy2$MsCh~#^ZA;>cCpH{on>j&greDJnyx8eq|N!_4Y|D)56K2sJKE1UbA;OT z=d>{gG#FYq8%DHZ_H@U~0eQ*~1oA#Xv3)?8eIxfbCkDj-V+^|b372fm#h4WJp%5FV znB{~N)TtbEWa75FOkxoL$kkUCg^q-rZ-?pMge_e&q+f3giAvJ=?Un6UIueaO$hSIa z(H>*s(e=FG8ZbbXW`9J3lY?AL^h}xu)vrGbHFeeyh2@wYLwW1l#f8=R`42Xti`%%< zhE%VD`+9$Ky;4yx`YgO<2V#(X6`fdQ;_3)2azsm_sxIdh>F)tUT#<@ZHLb|7vn8y^ zDYVl4H!Mtxeb)AAnW}?aMA>wO>1ZPZDy@**;sBh)NX~M0NT^ieSbUtv^hzp_IDC_E z$G(k_`&?}DVZ%Qq%hygKON5Lt}S zd1DPKvdM+2BekZN_cSI41E7LVUcK|zDU+Yj-GUKYF6W0(`rgm zYxl-BT+&-iC32!}MUvjq2P%*yv!*6AL?jln;N}gkE=&?^tuE|_byCU10Byf%njay_ z1d=Vh{7Dd)W`7=;A#|2w3<5J%rS$LFyJt6Wpks&7rQ%AU2!{;egvAe{r3gG86T2LN zj1|bMQ(8&u$Bk0f(^8Erk>6n`IRx;Y6Z0LZ#J0Lh;Kw(NS{bY$BYDC}tj3K$(0?tK zrCs6HM2M!V^2D`+p@bH}xs|gg(Rl1p5pdCv_-61H4Mlq-YpRRkq$K;=(N7u_&F*Dv zG?HbkOBW;wumnF`L6SOsWpEDf)`sMIHMj$0+=}SP#V`6sT6m1KLGl6S2tqGgeEGSd zG1s{Pn&dXc>H?tT=uD`~D2nJVymwyKfPX4R9PFIq=*% zh&u!}fW&?ZJP%pkJP@ZrW`Mi=5iBR2IZ+%AfYEQ{8;81g$B5$88N>A|S$`hHW}>(* z&P02&372TtnAnf^g!z#RCefXNxFn*|m+qFHWoi=v_|#D9Lr z>2~7toQ!JPT14+f-`WL9FuV0dwUgmXUvzl?-#$p?Se#FSTB^H$hFQ^3H8xL7#gaK0 z6^2U5XelApM2sYij6xe$X-!hN#ELJ68;Y~I6R)M zIK&3SF*&1I9CA7Kqb{pHF$V~f7>!S4=(Xz8(j~?$6vr-gOq0nzaNBmH)*rFbByetv z#sfo(@3X+sC^)V*w)*8VKkQA}$gAAwyMO^$m~-2!5lW+;s*ilffU6KNTww_+^juj2 z3$DGRY-|~4naqFJ#vyODNekdJ_iD8E1Z48P5yLhb_rOIoNC2NyR2>u@%CtWACqp*?^GX=(Au zoarM0w=z=Ywj5Trl@W*KhZQt3G^dVkVT8N-J%WvdWJtpy7 z#_9(H$@P@r1>68YX{v)-uNXR+RZ@=)66(q=?CP>v|3%aU{3zOjz{nsU%4gS(Q3Ph7 zIHvzf+FR(XW#pPLAY&y8b>JlHPUO3RHyRaKkc-3N{qK`3oLKG~*ZnS8`}X*pLdRlY zr*4E;UCCD}v>V8&>mfg=?{m2#!*a7LUcA-+1*rYb~*G0@U$}6Krw&2ZK&>5D#k=+C3zhbtJ zp?V?6NGH=eD(CYx^B$8A%)taXjxlani-eJw=;B=)_ayr=`jwxM!zUehJ#L0+je<-f@nd1+XQvY4Llgv(FaYn@u34Q{a@H0jG$FS)Ubq7O#1YsW*<6RTxl*qxl z4%c)WOKiBw>EFxY34RlXoC+>Yc8?nqA}a|6Ya--$Tl~CQF;yD=53_0bh=S3V9+Ip= z7mI~e=x8dH7_pYz=IQ-70nzCJ7S609Z-4lwng-baM7$B`-kO>5LZn&?T#(sG);ikCr4W%ZYr7~T(dwv zz%Y5*^7(f1jF6LkD>Ii`0nqtd`HKors3#7(3;DkDDcLyqasHO2$RI}a+5as&C_=1( z3VoIhcoy0Zc)?TyT9g|Y-mh6r56Hx+KDViDV3}kb^XVkI`g3+=e!maQ=|Q!4K^+ zB$05dRDA$UpciEEj5cjB6ohTC09=s@M#c5SUIsEh61c@mwnzk_v}X()IgQb*=n47PRy_HR4Q-`mMN?y2*3NPT+0R%4r*b_Y4a1dq1v8idc=?$wB1 zQ62c$3EZ(jLAmesTVU)NS_xlX2yzYrM-oohiKM2HW^|vt~Zdq66%3;D8sQ-0VYsytJ>leVcl+0 z$L#0|7a5sDn1Uy?b=VVtj4{=tEk13VOw=BYRP73adj$hsq)o9bJraLz4;=M2e!WVu5bs%Vv!H?qIxJ4}mK|LF{ed z6o(og-RACIGaDVL!U0?q2LQegc1s^X*pE7r>=x`UFL`;0L(NEBg5)Y}p(TIC0|t=; zBeb(435B_=pRy=a~DJK>rIovxf5L<8bFWe zXfVuz-h^JW>zOSH|E=33#97G8W%7St)d}0)`0|aC#IQa`Va$FwvhY z7|i~&;|ox5@re>bQCB)c8v<(0k=xJGZM*m#T>TeUPpKU_iB)m^nvhXw9D{3t% zFA0N%A*hM+z_|5jNpMh{DZSX)zH3a)26#(v1MrEL0VL59pzVFTn|J`(eGE%%ik=hU z##^NnhQ+0p{YA7y_072@e{Q$3o^P|Y63>YN`{r!(zIJ`1sLWtAvtAymdrRcWUd}y= z6GEN0-Fo2l#sa#fU$e^=DG^tA9xl#DfnlDwVcXGJFF?d|eY2t%(EZ!mo2+2{c-1|iE&CLRDbNS-t)A2^=hiuF40G+owb zndoC9%N*nuO7(~>tOKjvSm?H=bsgt_;6d9pv#ySvLyxwJorm?uhtloWxs03YTy&o{ zY^dGbI%gSSX`LtbkGY}`*0~U?@>`uj1plm6=aDERKcIEi?D)=_;~NW%>6p;(Bi|0r z!dbXw5TJ=pn=fOZJ0L7CU!@&2yV~MRV`4hH(3uusIEVp&EmoA9PtI)Psk`SK|7$!_ z&pA#oWaOaYi5O)H&{vibaZ=pw$Dq#bPp{NW7It$}vn6>;r(GVqh>Wk`nR9mxZtwug z4_;Xz4CtYj*Z6$e=s#pKbu0xKaB{BxsfB(PT`**;@I;{|cGF`W5+^-RX%#dlkG6f> zvQB{kkmyeS=Zby`772ta%oM~iuF_S2PBffT z)40_Sv{!7hNv|D0mSJ=mju_+dfC;JmY}V3JRj7+m$OM9O_e%*}VB7_%7o#MzwbucFa{{@bXs~!yk}%o0&S}bi}p+y_Dq&Sd>hzdF{$Yoswrjeo9_UM!$&ay z`S*LqfaRaH)PXA;|IBmWq&EC>wlNXo6p&~=a=w`vEQb94e3If=c7G0|9|5qaW0a_L zazHI1_Dq)j#1&Yq9rq5LRD`RbfcHfTlNVc*R)%2i3=u4k9|#IGv}>H3@Su%f%o!T* zSS8UUs3Q=(I&~=b_M5X#;paar$HO;EhqO-$Mv+XM0}5a(;5(vkPV=;$wJLVmid;OMLTc0nN1Zc^ZmW zN+%+B!lPK!gP04J1p*LQ&^aaEVnpMb z9!P@>d#0>2OxjdUF29Gj!?h0Oe08|#!NDX>V+!37d5zRlaF`|?EATURFz0O5G_o*J zTkoH*7P`--&k)H0Zf^NDTl>AG3j)e@MD!mtRMVuW!6yqV7zp*M$cb(0 z&%)&@E>Q51-S?^@VQMD6^Z?KHHc*FeWE{zC=3C!_e6%Gmz$@GZVce@y@lE_)fnc`a zL4fHBLWTt?_#vGf*`cU#IlUG2;V-qEGu6OP(Krs-Bf;x6ngX&QDPkDTyiZ=%vZ?@S zZ;t(GmS*i!a6#7`I`lHew5-tRfF3#p-l>Du#Y0c*tJ@ZLfW-~0+l?Ff?C=w1;svS% zSY}|>9$N`*TAO1JaOSH%Owvuz*o^ZLN3L-Lw8qIu7VJul9|d02%`_&6#+rGkmN-JR zfm6e|IHt=A+c661&BukESX_B!HdkZ)U9+uSUs0ehll6pvKk|Sd7d%51T`11>G}G~h zt{sHXHAzhHYnGofHkzMkxI1f7vCGLyf{7pA)hJ?M6rKNjLthH>N?LUJ@gTrZL_Njy zUkI3TkhTZA)?kiyan@+jPV5CI<$ba@+)+2zCu$kz-u$GhyL1xpZJ6}X$tE(@O+%V5 zhO}*m4aBq5!*rCWVLfe)IGxU;Up`0dV}XqESD{E&Tx0>}j^5?<4e0IY6(X(Hjt(dq zKW9fjO%kH*gh*HZ`4kwvCXUBS4n6Q<*%^+iiso3W1emR^VmA{5ZKj37RpmoL%s(NF zh9934NFflQkxP!vll-D!5exve==UsYsN+0=`8=h?93P@MAJB%gS^9mCd;Wk?Ej02= zcKj%t|NQQ9uHqu1LmT{B=g@8_Gu-Cv&F4c>D-B=Q?ObV^nT7NbFk_{;fgo1L#k!a) zDkQLRdf5dX?n1p|)-8>!8Wo^O`cvpKB(YuW&QpkAOmtL&i%KPXUIh2?Jl<6AQ3M(Q!6CCUgf+Khsby#Q)y-dA``HDr8Rg8_%FOcG2hj0$6FoQ9}TJx+aVrCvmK zQHPK&H!zEoZp!`j8tDvso`nN~iHbMd*NfM)TAiiC)?IOB0%>^4 zW){I|RNBOf7=X8LLc{X89{UV5Z*ZZ<75n|Q^m0C>VGujoKVMP(Hc8~PPye&KpRTD9 z%jp@OO_qR*I_pL+2q_~&ny`S_}+nYRk_LUA(j#bdRAo~i(FM1j*zz@&rlsm0}Cu4PMVQyh#jkzyKrB$|} zNVg~J$6qZoknrKs0JpZi_#GqDP#SAl@E`zm1jyG5FvK7T7`Qc|;xM(#48rJyfyeJA zTE2X!?Q66Y?azMHGib37BXbd)8Z-dQwg%zs$8-O!hT;zMaMl2X$WinTK8;J2#Q|!d ziqS=)F}20ae%w;C2^b|v-8&J-qPw0R!49Mye+shF7jNQ$PbZTW^HQQ~>#(xtfTAW5 z-quRmZ#tf~lR*q<<;>LWlM%3qyKL2ugFy684~wco=mjpVMa5p2-#gt{h=7U!i}!bF zfMwy&?b45ramR(cJo-52I+g~Ax3_Lzoy}5L@|Ydz(9h+cDf$M`oc2JQy+8}ofiBW9$(cE$OFmEqr{268 z@)cSvbT(|;^tCsRIGse4+{ZTJ-WxqX!?p40zP-ZXrK~Y;6R+i; z!qLg0L-`uPq;x37$erfKSopWLW3f*(gv=G@4qftVlDApqR@0G*0@U-aZ-9T>p6>{? za}l(jSiqup0qM97fOj6OvvKX=4F=B}YbD0DEB*LkuQ~7Eng|K7VsR3!xF(DXua&~o z%}hx2oVZ?BU4(IfrzKVTo}7s5S`tmDGYhJf&=6BCj2v?}z_l;oa4q?ZnC`S@ofX2f z3osn3gV2J1a^u+oA6;*z0DP%Uo0_qvqw#jr&*P4plMu&;R<4HQ z)$UajKA*)Ft%V;|bmRYtD@R+9qi>u*_rSBVW8r2M#~{{^?M~A_xK0kU!K-Z=vB>Xn zTS!{KBc@8ml$IjVk;}=`2e*I`j2@1}P|G1Nb|&dkElmPA59F)#AX)$J9;AE1VK)G$ zi-%DkA|>IGD7S;%(mB2ej@1@LCP|iFG>@cnj}qZnZ8b0;%M>hxO}83ZRRHLx^OKoJ z5{X5LumT&f0Np0XvP|<xg-jBWhfbLk8Qx3a+n^Fz`{9QVwC(~#Fh8w&K+1(kd>nM3{^I9Uuh)0rU08}%Zv z+j(n!Ns)2}U;@Bj(h^6W9TiF)J`1mUNrzAMluRAI!_{Wt_;oGmhew-tN|D#u@%mBN z?Db$kdmIMnOd|k1dxRU=0K@~y{Dy*Gb=Hi5&6C#*O)sQb5rZz9ZY;u4{9OTKB%7#G zQT8Ol98t`0pJ7;B0IF6)+DYyQt=Khfa3S@>NZX{(zd2_5X;$w|ZE zaxMDL3z5-bh7b-zE`h_Uy)v@EJ4zm(zppTYPe%z;g=u6BhQ8(4Tm@wVV%K*7Nya~b z<-h>#`^p240bC{Fn|8{OmQM^%_sI>%9PgnHr^}GF{>`@;Fb?hRw3B0o_-p(G^#r-< z-vqt#Wa9+IE0R^)+e?gMteRY!BQIhSQe8~TNy)HHf0oh16;-|l2xJ_PC&!3jhAhSI zW^N?fb7%;B>(h;U21o&Ijdz{~ifYQAgEhr}AXSmqKmX(*XVO3sij$RZSOToONyqK; zx(a*+Vf1nDMvW-}@dj#nmc`cN%K(s2cB@#4D$gUCG`p?aDa1}M=7?8}p4*)ipLGUx zUyaEWW26P3L8uW5yvU~GijjFnb@aq}mW-y3t~8JjEzjJs4hd&0tr%?F9Aw{>Ry;ON z>Q)7i9~N_%=CQZ)HAf3xYt3~#FEx-|I;^-m#aDVqG6iHQFT=UbM-om&dkH@~(xoLBGqx=RM2?$Eu$M zr*={fE-vqBl+G?nGoxoJ8ZHXyK~AA$Ew5d8&l9CFQ+F-^!PrB$G3t@kTj^Rz4;as%T^14)l?_%RAB)pT|Ur)rvZ;l$m4KI z#xB=}eKm?+dyoegZTNAvgFLCH%vK~eJvu=?ykRBC7xirO@j!~>!KmRvv6(Q_N{$Gc&;9A~==A27&h8;)4v0A94%lgvNUWjZf;5 z+!Mff2|eShAEuni*c|_)sgBGb8QVxjyS|)-54iL1<7c44j-4KtkRj1n!kjhQZ5&o| zBNM&RRVTi~^JY2_I%+O0vThw1KP!MdZ)u+r;T!q-rw|AWACm+qA?5~n^iRI<14Kj` z-`kcAxRzWP(EIEMP`^IkfcGMN@E$6M1#tcShi`rR`3l^3g++t6e&B|2 z6G`00NJtz$DP9gVGipg3eR{+y{?&thZ9;o;41!W@P%y)xt==1keyp|LV^smp0T+jvyfKG9C z6nSH_J<|q$&?c`tN*0Ncce~>|O;qB03^`enZO)Ys5k77; zpx%c(X-)Zvh}FicR)G_6G+$D*SXmB&ciB21)gc6Lq>*%V_)Q0%uv^R5O!RMPhfTl-NysyIR zJ1Xu7Zs~U*DjdOz)aebyVhTXb!V%IyC=!2M)$zFU9LC7-yK5w6fCL;!Ac8OY8%u?HRW{`ob?MCOS3n85vC*KkTFv2sk zT_^XI2HltRyTxX69-#-L{S7wNg!=tUEWo8y<)U2aj3dQb@Us5iz?jz$mR)~JTG&L> zcXdg6W^T+MJ8kL zF4`I*%q831w0BZad!<)B@l#;yj6-|9pwNINngOGQlXsQhNkU07{?Jlr$x90W5@2VI3z0-uZC62o`@4>lzd6Or?t_l||%*b@WQP z9Il~$^N53H3ni556G1TcyXlo8%p*n$SH6LCUH&9iwhgOIc;Y|a+_5T%8exNV*c&`R z$-4YwXREjOjR!GY8J^Dty!Uh$L#N|$4a=s}`_>d}Qjr&KK`tL3;X>xzX$%9hTYe~HsG1*ZM> zA0#kq(BhqATd%7qF!-dQcs9-Oqf#L}u+N~p;RYUxa=Uz0Io+z$WB}J33W~WYG{HfF zUNXQ$Vd1A!e3{%-U$ud+E{6LKIe;j*+Cg%h#?64|e^;1!N$I_&T?=JE?YhVJvaSjt zO+PFO!U}d02dG5@I<3GoQD7$gNiI6d|98v1lJnP_Po%QQXSaAuwEQPpzl&(&gi;Kt zvNUK(Y?Ht=q{KEEGom4B;xu)a(CZ-Xip=?3e{2>Z1(gBj;=XJ&L8;jR9-B1e?Csa& z%Qc48x^40K=O(}o9uM*ZfT;ICZ&fw`XLWUep_&gMjAo!-_W_tm7|=ZyP_%VFzj4ZE z*mK4~L)NA*=raq>jII@W5FWk$xlc``a(hd!haZ$7( z1}Rcid$Bgp$)3*=U?b^ux(n#b&Df5;6j^C1wD91U2}p9KH-#M%~v(GDFmFq1`|6kpQH zaeet#j4s*8kBc{>xXbRbcz37Vz}UcWpdApE68dv#4|fPH$vW1HMU6Fp5D3aL)JMTD z!i|y4LM#0B8Uc+DLn#x%0eWjQgF;4fEtPC=%ol<4kHAJ+3CYrVY+4zS(g1(t>;+Jo z^)N7WW%vvr!y~SOxUtfO&J(ur*+Bx;9utcv&H!;pD9d(YyvBASTz6?w`J_Q@4!{`I zVOhGMuc#MPzS+?umE|u}Xs5B<1+H(t%l+YW-zPvo4{Zb)IjFwN21qKM0}P9nWK2#2 z#yy}U2FRP%&j|>9fN2~&n4>7IrpJ7OO`^eZ%nQ^KRL-Pu7F|=lwyO;yS;_z3ALFVE zK+11&Qb^Tp)O#|}3Tuur10gOrkMJTRu5=PSf53%NA1}7FB{$k0lrR}2<9eVApW5*4 zBtRsW%vVnSG$}XVX}|z;Y8v%8ydD(p*25H7nVJ0#93m?b4bJC2h!bgaWt-mDdI>8u zefphK z@re^~-mn}q0ApM62R!wuiOF;HcQ_5!CYt&P2ZBG<$a3uonsF(B3##j-Kku-bPzq=< zv;HbBPlTu=V@$&VC~yU$)0c2A1_;yeQhK+^6YaUQ+t#_^-d^NfKsWQb!yCBkT-cX} z!G#nzdf-89zDgEeX7g~FzcdzrhwDK8>kb7xNB|d|!!S$Dg|I9>hf|o7lpbVJrHJag zlX+3z#xA=;FiPECNOt0@6M~rj2lBqd{XI9vR3Vcr)c72^!)Ic? zsvh*9VW~TLdnpyrmG^oRTcg+|Z>vi;&=T6@OPf4x&k_R$<&%1_ikbOyv_po1c`u|% z4NhyY0zCrImhY{dh{Z{t{=fq=gNTRR0wXt!}Y9$Ce81AL_6<|mROS0ObPL1D9>R$ z&Q(N5Co+(YuCeF{)}uf|W({jX=Eq&`&wz~ZVWJdpz9@8#?wuUcX209i9QH}*5t+p5 z!V)=L>1swSEpEoV611*{;gk?9u=yEIuQx&X_&y@U@gUt3HOTE7$3ze|2DOvyNZN}L)L#xMZliz-%f$m^1slqP zD+jJoOLz3XB!oP;5FEobH0Y75AmJWWCf}D0x=QxlkSN*bDndu7fWEV)EC&Wdjxo^2 zNQIUpZ&>jh=47%(o{^xC_IhmCFgNd~CY-~}(-Ex_`8wKaY;~T5n0;S56fszE6zghb zMRb>F-$^*IB0O6K5Kp6{=1x6bmhFswG7Gxkm!iVyvvz!Jn%`+-JurrMj@h;Tlv|CwGr5MzqKEt?Hs6gLOdHKGhaz8-*@ z_hWz;19lCS4U@(uMs;M4*xr)%%1K{1LYd>5^^dJq?)lV1_9GvoPPTD~HtWiXV$R@4 z<|F_e!-m~bw{%QFtT3_VN?I)JPYpGrnyIeN(d0^pLhGPFZ!t<`5s9$(r=0>f+vxU$ zxPdQR5=aW&A7LKmXdF&ZEVo3m5KqK$;UbEVgbal;DJ4JM75-~)kphjcE@ zjMecVdw0HDL+Q={Sc?-HSqSu=S)%E=|E*J`NX=#c6zo0zwat0+XA=$tmY*<0@`x}C zkaj?unYZ~S=3S5h$@--;D<{-HTQpU|bdvYdi-<+DAk+_c?L(1?or;+FllX~8HIb6l zU=6Zrhmn|C`Ib2duISkL;(8Bga#w%Ka-9IJdAs?Q8&R*lebQQ963zmBMyX1 zNa*lB*JlH&lnw4Wv5b(WHvL7xlH*(oF)mKUwS>#k+}d3XU)PklV9=~;2l{DgjvSY{ z3b1lE^ZnvjWV@~6n9B4vMaxyZZ6pkmh97*b;{ttJV2skOVX0L2xJ*}7-_u7ET_8V6 z06jp$zZt<7w1$~+iRK1(Y5>YnS&5rd7ThMp$=+*nd-E&e^lY)}Q_>94nRPGB#K+?4`G0qyQIa49nEoSC2*scA!ZB2CZ;hsBH|ITg~l(44Y*k z)Dqvf%Di5$R*wx{B3iMFjJ$U<$x~>a>OS9>(RRaaJKzz^FMe(unF2BF;VOU+kh3W) zJxl_|DHHbMx6CVYKu(NWLi_Xe;?A0wfVoInOai)I@Yv9$2ggA159x+gojMj7BEgh$ zpkVW&0^O78vMB{Us(fB3`eg%FfG%jkMDD3oG9cF=LIr2{N)$NSormj-z;ScNe2yaH zMZht$!n^>7jZ|@T25=lyT0;cjSO=c~bd)sS{Ed>tgaxHR4gW?057-(D?AeY0ha`1r z26d`&JVM3grlcK>gKD3OP?vffx!$a30EC8v$~Ws`@9Mx`sl{!7T^!|NaV5ds;;_DE z38UC;uEzpQD5xY?5oS~z3Bn&k5q)-|e**5|*kUy^v{BhK&Eb)AepcTsEZaya2<8?S{aL1S?k}JMi<=$$rjHsFX)gDoM^j&L}k|fx>oP2u_Y&Th(!97^u)`va{ykn+faUC)ja) zq9d#10yN42%CVpzLm+FmZ2$}aCIBFbTo8mi1R-3I-A~|KudMEFujzj46uOA~w9LR- zT#V-P_$Y#f9?RR}z_9NoqbHq)aWSm`L#Q|r{{i;_@B!}EMU>@uL$F-l>PE!&R9sxR z{#Y+SI;BSIN7G4&@w;fSN4Hp}gmC%u%bE9x! zS<#WssLU?39MI0|3DQuePRE1;C_2*H* zj!Lbe3FSDT22s%jQ4?y?(B-NdO+y)W_3N0{X4vH(12J|*yIB+)?H28(S#H*8TMRQxv+9!Wpimv!VF_W7fe?d4aLf^>+#D~V}@P!KR)hthF$LYk`cP&Oy(cM zin|%VjcdA!(A7T1GavI2;xd*IB$N3H3>jV75Xe(zUA?WBhK;7_%H6wiul&NwhKi=H zoSJmy-caSP>B^_B9-kFc=~y76BEtns4i)%X-p#5k`xp9AzIGqepW#JLyH!}g$Y}bN zDDM`bJ>njTR2z+Bn|!O)R2z)}8^>NhUcY{5Z|UyNx7vmvXSwkE2g$oVc{hhiEbk`o zHc>1K_Bhp`Q{F8y$-B9?Tv*=C8*=OzqZk+~8pa+I(xiRXKFi9&5&9vZpMM?Ar|y-z z=Di2Nh3{i1lgUJ4EtD`}CP}q_GyZG&!$9v<}t zKnLuIU}R7@3uM`-_A&awcswMDe*K^%6g5%EVBs!rmfi_!dW~eF8WI;=@PS8BySgiQ zAT1Hm5Cs?XCW_jnaqL!X^R#0U4WZov3_kFVATJFRT1!DmCP@Mz96u{Cy1U~IRyrH| z*sot?-)EQ~^q_}H%650fUd5>N7~fuJA>NQQc}KdP*^q-#DIt-VNG2pmcCqdbsk`^< z4gYlr&p^B(tGl})XJO68s9l(p^y{cyQ+An3zb^Wlqxmv|#!$ZH!kKaTswb!K&LWLV3zJIKJ@Im@+KHcw|%V(#(4o9Tu;imKjlfN-7ePRUd@&=_`B7qDCs zgX54Avb!F-&cCe&o(!VAk7*~dT(sdgkJR6!9=6GHPL^{Ll?h_ROv9N_S^EA$i?Z?d1hIE{@A(^M{VVJCNRlFN~{+>Hb zR@@D`{P;afE8g%pY(GA1KW^~d4L<9=XUWjM=r$0xA01X$cI95VR~}`=_HOX890v}Y z$ZMSaxr*JnOr_C=1BZ`ygA=p?*J?0BNRml}08;}J6huc^^b(TEgoH6l!5DQ3sJnZE zxV&3Kp6OOIkD>8L`Wkrf71&fh6sbjPupRpsXaAz5oD@)H8+gDXdJH$_ zq{u+ez>6{I?(Q&Y;qK9Wd@FYG)wsLWDw*97xw|Ay+Ql3up-5|(mg}TguTK1OojBL& zyd$o1@5;R^cbB7%ymt2oi!d9q5w1V4j2jK*TBFBhm~mt$)j(WSCvN&68=N(cWzb-u zoo)?YPMsL5PUkvt$F2Fwbz*m)CTc>$b^Ln;00XG7z<6X-(hD;kWTwvf9qsf3-;eMu z{oAax6Mz7ZNIwQ~*3JVH>7P|%+UdTK@yHdU@n_6x8+Z^ui_rKt%Vdjxai)J=6zx1@ zzMmy1K?z1o{E=Z66v+6;Bhr%lQDS7Ip2|qmDkFU#Q!P`Tuz%*thTD-WG*C=4q5|FO#b%h)V9N}f%BB({Z-m~Fc5f%+qQZrlc%h{shum^^ ziZWGF$6s#s%DE`RIvv6x+0l~za2nN@kcd+ z*R5Kgiu1%deN}o{)cWMtqT=X)u8N?;o7cVnTIz>^O!zm3s6y>+uD%%f)g#%$MicAm zl8mB3W#xgcTc6xG5IU%pq5|5t%9)EP4`@^?R8N`YYcb*d@&KNE^*jfe8f~y-)eU`u zE;+;7#iq%2|zp-U!tR*gRur`4(9S-C^%jZEDtk4ai_yjOm{pyv}*2ov^L6d(9 z0V?=%s74E$_?K;QFTiOC7ja~R(TkrF1jpI-A;5&tC=#Gx{s^sDl82laHdXS!L=euHMf7rw#89$ z6ULTCn+2*M7UkJs)?pwl2cZ&ULv2com*0jrB#NLwKS7A=ybD?Ev|#Urks600C0^d{ z(#M#0)71>#uB4(b6Nq=E^ir3z@!KemVXES(i2v$ol9UpM!UrS*4)3rMOtj90SiKB} zu?CtjHSlgg2yPF(l3hXrx88pJK9C>q1@3?zzxU*8_GAYs`z+WQnIeoRP-^TMk}pQcVk!{&*(>`L1+jDGPI3{NTOnj# zCCVD3w8%>y8&r=byt`*{~40K)1GKALrZb6lYOtqP84_Gn$*KY3a8QP$b@}jj2rrFlrz-ENb3q z-2mAHG^fAh^86=j6x0K7&O^U=$tIgi4=kbYOH7mY>KN&31slwO=z$!6a-QoH|;S&K^edm2@GRWg4i1@Td+YTt=4ht zr?W$?DrRNx1)sObtZCSn4Dvgf_4M79(2+svU9K%>eAIkb8!}ehhj+8$5;YxxK0$X`SE{3W_?vLpqRi&-H+n)Fz-%m)xtp#9LpQu*@8Cv<0j`jR?F=>-hP+|+ z<2&$B1rVlBFay)M5&TMZ2&Ss>JDVkv!ONoaJSOTu?aK_n>^-@KH@!=v;fCBax?eEE zLu5WxdJ~>0F=g84bw^x2@t+}2;2#a59P!|o=ly+EMPy&bL@Q^N-+%EL-ex6?LZOt* zD~~hl{$&COO;CLj3kYOHlpvHMj*OK zyl_e}ahi%p3wzJWYCRz3T z_N0((vGwW(1$@B{nK-2NH1q=-4OJcCah368VHbWN{>ep6@mJ`c4^;wp(LvJE?r3`_(m-ynAnuS!4^}Z9X$`n?##l>;|2X z!ISHlBFN661UYXRe%lhzT9P_89^+c)?J3!0^E5n)*!kvS!KUOM%~S5p$U{PqiFE56 zs73IcFX~Mfg|$vVC@MZk0Q5}tYtKkF2n?S`A#?3-^bwdX)!9IBrNnTJ|1WzcrRw8? zo%zrbNIeS)9;kC*s6QaZhn?x0MRoY3ZA0T`612v7233nmvQP9=UEbTyhb@(x&F^oq zl9g3Y6nJf(6m9+P2vcoURsEpL1gDnHBgi>bshf6Ut#r5L**c~3l*Ysw0!9xO#?W0T zB760ZG9OLcn3eL(k+5Bl?ylPAh4fg_sZ|x?3u72{C-f2^$<>-i9GAx`&u7acLG(F0 zU#qO=tYe6i<)@pTQ+6_YqlW5ah6{$o&3DxHWK*PYQlTrpc~tLyuK+Zb~JB(wOt`T#9c+?;c&b4WKnwG<3T~ zm0eD|FhSMW1O5;*#T##&B$%b+^~aedNww;_goxxEJ)-^FtkOafdY=L>UPrKJ@j?>r zL-vf$(l(^$Ev&lFM7sr%DpB3gFbho=+yf9JpXM&cuGhLy*ICZ>LJHA0=uyax9{pq> z^B%~fhWwKVz9Odb?6j#+f7{$zqK8bNy92{2w|s|~-HF?-#EIM$ z8iD*6!Ca@*2K?7*t>$ z6~Q0s0b9b^T=H6210l}(tlh{?qS&+f5*xNuc2FWa&5U$!1l5RZm)2fSfH3C>{ zfVp_L`ujqHlxP|Du&vMfc0d3N8=}4pxF{ITY}S^CuQ_M~MoAMfZ@;_%0hKYJBGz{2 zhBZ;v;ik0S{*t`tVkc4{01ncw^XCGhNEk%?-fpRmk-;C#`#Z0W)#%PB~9T1)f2c*~T`n89HR%j_^ zY06}K#OnMT(SD(4wG7Q=K+HKLYKsq8Q)b5aHakW22t(7iW-21H~k@&C6H z`qK0K!#UZ|Ll_z?3>NuIi_G8mT_zH)#c%<3Cey~@a`j1o<)Te{A{^kXxn+^?CM|l1 zTNV+H{e17$SGLcDyDP`w;J8gvQR^;daMS#k^(lms%T&e|6eHMLuj9l(FY9Obt9614 zticsg<1;n{FrQK;7ntE#Whg-ykkme7YJJPB$jRdpspBA$E8sms#)J=gJ&bQ7WLV_l zh1`^t5G~>->!M1CY`)t@Ajon2>J`jkW)^ns)-o1R`rC%+9*A=u<*^TXl7_7+^L#Lh zJp~JSNV(Ub{0I`6KkbC%ak_mxHJieuVE6pguW_IdJ2*YMAIB&3Nm;hDW(HR_&^Ie( z(hSZdGKd651BVXPJo=4YTgbY>+Te(50#_$ zOv-O0a`obXpd1j9^P$~QS2G)xKf5EeEZDEdup+6_(sPFiRNd}6*ea%&STKDNy(>Wv zd`EBBGd!+L5bn=63H9)VMXQ}u^TFqUE)wV4Id)S|nemk1_n)KpL?{PbD@S;73N}G# zK5-t8_Y`jx0Bn-p-y^wFv@u1=^+k~G;hlg;sdMzasmYVieWC%E0kwsn;l#UQ+pC*+ zXN7!#cXMzo0B)=ggqnR#DKze%HmEecY5`Gc1$X2%Ij!V^C@>tw^xk?#HxWolyG1CA z*c7GNVjQD6%#y-hz!V@WefA68N4App`^n_pK0=h!CwBm;YH8IsME}KAY>_@iH|;|Q za{8o*S*{Y`<8^G6)8`_-hDVu;v7C5Of{#4i^x^ocjVu(Jn~t|y-;C<~eZYs_>iD)+ z$~QzN9N%dDJ$M3Zx-t{_b}Wte&D-fcR?|Cp{tYDvjo%i9g2EoX?LBF&s!q4G%WTC?kEe=f3sAx5b^+~@Kyb?BE6^z5BKK% zM^XN61!P9Mg{J*oMyh^qwot0k2l?vd*QnJ1e+4^@Y00D&Uo(Noq;e(-?@LG6Dz`d; z7yoW*QnHPmk^yx?1+`#(eroGl)I~Y9b~kK`YETlEajjmNSq_Uu?h&pt*%YWcm!7gW z7aqfi$lA(W+6O}R5Y>z0UzDs_PaQHWYDYBm_4mSHoxOGXL(&r(bHsLKI}ilY|(y*G#4x zSdsLL?hNu5+T6lRz-N$8Yq6v5j$08ea7|?p2$sJ|zM-!!@73`S=%< zyYGplNImdLOtB@8z(eBoQ>_5|`vTEK)DM&qS|MNZuQQ`M2=vqk?rzpS2bh^)K)}C1 zZ};Ez(}4Uk`EHd*dN7*p87$>w_*NVi<3I-juw9`1#_HkceA743dqV*8$$Em>@`u9T zQsf05?H;p3$3b2H^|5B-0w*CaI-Z`!B8GCvz)tHsGp^~r7241{yicZ0u?yW85tdB< zkmtpixA}CdnZ)%{(BF@rg`OC+Otk+gcU9v}1qjF$sCze>by8okavJ6oPP2@SCa^Et)*t55PkCLoR{`37)l!&b#1e^tL90pfwQ>_Qt! zw5~z!NQCaoLAODnr)&U{>bw^ZDEc34LEHsxWIz;YrY?p(1U$hc02*iHr*qz)JoQd` z(Bii%PbX`X$wqxj!U4!l4Hxa*cbM{`A4rTI3 z&BbGMx0S-XmXV;_T~=KJ*U0pOp(Ggkp~{>-hcP(A7{+5Dm)mRbjv^m@JBQHQJ}AT_ zo`t~^DaATw->cdyuH@00q#@OnnCMl^OLA#hfr$qU|CHiUU5NRQ2lPvls&u52SAzjO@*vvgs5MgQyI7Cy~DrtoOpA*m<`&0p_L}v)#hu!lR3=lV; z*WuXcEV>`094Pq%neB@};R~>B^VZI#ZH~85YLbS+;i8_MV!@L7sT>k1F%_60*`;bL zqq@wBzy2whabobk(r3)b?iVpx2~6KJuz4UI39UOELaGr=OY1}@-Eb5=4U~ei%>!jh zooyXJA!zm}bcRH;LF|z7Z`4V{9))Z5YK#s%9VCCbj0ftJsn~Rm)d+WqI-Lr(hW~JB zlSX*diy%r`$gbL>o?w^W83G3z*Q8xpD1x~QGYaS-&X9)!XIMUg5G}G5s}-QKu^ zXSO#}U&AN?L@*BSUyKT58U5vN;MvZ~Z%GGCL)s-bHvSJ$iU|?xhxBW3AwSfFNBMJS znxNX^_1va~8nOC2ddbz1JM(Q?7krdsk&%KH=JFiE<1S6sxA#AkWgIT=&9hMKJ-?!FDsT;@&7axgxZw6q| zmKuOsR1oQ1pSrl1AF1C%#&9Xpe;Wny29R=R^ey|NRnyByamQU~gzl;G(j z6nKDw=qW_9iZb9spjGLu=K}5E!Y2vhrh=1}_kelpb;qG8yxwn+eFcAm=vP4hPnM48 zcIme5ykYl@`wa@>P9il|6m+p24y{1{MFEYU@|H6@5kk(Jr#-epLmIOs*>;X)bgPK~ z?2FEChSbfLfmkKa3^1&&@9o1g&?7f1mKVL!`;OjQpMVFm|B!? z^$~76R$^;Z_iobFn{I~X_*!@0mk1llUrsaouVI_LC|MQ6r<4b}*Dz`JQuyZi6z>>E z8lBKekh1p#3vmT&P==rx!_?42lTgv__?HtJdxZWVwX>w(vLB?P9>ISTo%1@$`?Mf1 zOCVW8IBtG(ty0lMBL(8nhD+sIzmA|*Qu0CO@yv*RK(T%+J&RCljB&Tb9qv0 z@lx6xn+g+BC=Hz}q=iH~irBY8rlhq(;9M!~tPll8r5L)->v5I5PE%Z`p_u$K-}d=T zji$Mp+&V&NvgRPx_=Apr6#kzLRp&}%&p!tIbZiFXBm_Jpkwxp10}~r_aVrULECp!U zYG{2FK*#qzlm96wa_6+l!1yg|L7xY*?;PfhgJT8R>(2u*YqOkhMqD1xfSCe^497>{ zyDP&n0Yj7vRH@Ej={x8@q%7GaUd{{^l?N+NbbX%*%y3VP?&%V*m_Divv$pT||41T8 zp#(%46UZ^+jUlnboLyQcTW`Eg;baxo_E(t$ZE{A&svdV#A#g_V$79 zSs)K}ho&QWT}|an!E?^#r@416O$t^TfRQFMVqvY3bzKkQm{ANQW(Y|gIMOnJEqPD3IgIG6-oVup(7qjg%)TI zPyK0dHK*WpkT2YwNAgn%Ao$0{ErR)Hg(R?8@1|ix@md6mKQ_1g#3Lj89$TsR4Dg7= zkceWZlz;5;a4F4j2|it)64+gpl$6qiYh}PfNmD3Y49jh8OrboA4}I3uZ=}unT6g|s z!@xS{6x6o8fx_^z$_i9Xu|;#w+R{F{R(enf`(m}nbT;Rcgk3tH=AiE{v_0l6=^DGf zCNsbR+&FW^Q;o>RP>7`io|7&_JE&@lfoL`Q7shI)4mdCx%g1HYbRgDY)xjd5J1)^! zgd>QS*j|`)Kc`HvzkLp|1#;lILquR6{+u31nC2*zZ3yF&Uxd!M`qvdStzE)P1NgkO z>{^09Y9T7)Lp8CHx|dIL!|GPCUzlyqNQ>=1?lj&V;fpz-?G=rHN&8hk`B2$`qfHt1 z4G9>L20b9MY42AxIr;N_Im+$nk_k)wxv@pHt_Z9%A_s2hF68lSM0 zPzRmGOJk#XKk})EbGB0+sC%}td#t9Oar20#xrw7z&&3Jh<%C>8`U5DC`TeYLLQB7I`7h$d)eVWZC1%nhqbO)CT}bWHciEphej;YPXL&c z#w(S6KM`opxSc?7!dd8^fy{S1lq+Z*0~wtT&ueRjCHV-ahbrIcuIvP4w`~Mur&_n< zApj`6s6q{+!<+IO6ecP^42taPgqvl(ta-0g;2dM=pq`&S|d#hPZvsb53`hr^q( z*O3BQQE+SsQC{gz&53>aSaleqS9Xb83WMztg1uK!;oU!{(uzR}O^ypqUjT(>ru0IZ z6k~IGZc@YL$3oUSQ0w(aS&oL82@?X~zYVL&-N9jKK5fNi4>A?VN}4sBj6)_>c09v& zLu($BXmzo=YRnMoRuvXPLmRMqPkF(Bgg*fypc>8KJ|O6##;vYzW5J%mTw0|^$4d&c zp~rjt5-5Venyph${}04{O>rgAo|_;aIBV$j7$fds2zFaS0|S6X!qwAMAT`t;m=Xp| z(jFDXNzevFaqw8oVXtqhdu)x^#j)s~Fu^01j`i=0$Q8|%UJm>mALT)UjDNY1G7%Pn z{t^*H`8qKd#@M{IB4A(h-A5}vGEsV~~u372tL z@mK8-f(`XZ)6AKK-_(KBBEOGCbli%jhZ|!M_yDyTXd%i>ihgPNt$sxje#p52VrTjtX$v~{2)0^;;l!t?A?e#=2R?Myj`NS)YB=R5M7*9vh&|xJXaO7 z4+?#OZ5IW`G4)y{M}e$ZUp^=|!^!5=!xxDr8SOgAQc}4LNEToHZnxihDqLFij~E%j z1Tg=^RD#EiN`KOUE)^vGb4wox01xiK%lMLr%BQYV!mokBVvK!JuaVu&>_zx*d(}O< z$}gQM8(msoVQpQ&_vY+?v0@(%e9VuZ<^uAm@8MjE*K5icGaHd&)=<{+a0T1^l?Ubw zWLdHso=V+`A6#)?RD+Qvz;E^>;J@@Ggd`#q(}J)XjNYnVR(wzc)a zW##WERA`l1@Xt_eh;iD0xzsE31G?=rFdg^d^qK)fV%?EV)30{6M%67jsre&2pAsfX zoU%^|cJizwmU+_7+a@pjCw)St=dh^bRpF`)uLG8h^RnB_$L}39YK>%?I}oTc<_V0L z8ovLjIVpkM-=b(WwNJjQX90YItXnk)#lF%hf?3zym2vM4w1s6BaV0=DAB{V2|vF=co&S8-ff_J{u`Vh~Cp4rn7G9{V>%s%o&bOZo$5>gHomAcmw&NJeoF&?vE`b1F(2Q}3 zz94~*c=%Z?1cj=}^Ajq2`k2-e${XM9wP=D=XGTw+=rSA|6X6|TU->3qO%(Q4Ybo!B ze&wqgq(olbwwib};0?PCjRa!QdT@}83n^fauVR~)b0qe_mUP#zcECOq#oh;75f*J? z(MsAmh0Brg6xA2EM2$)buoZ62Rj<-kDzK)i_bN(cMk}bOgOFC0djr;n;>|)Fj($1N zu;(=N(GEHRICM{o6`HMY{nXD0Yd9Y{yzvx~gSJETH&&*jqqPxA6VcY`!2N~V7O520`v>`M#N^amW3{embzxr5p0 zAm#{oO(WA`o5mGV0=awcT$6r;jL*S^Riz}WR7yI&bjAOT+vJ~z_`@Ded-gPqfx8<@ z%q;?tZy?zf2J=i{FsEZu$LPJ1y7#%do0Rowk;9;ak{x@3Pe{Si#-vEF6!lXq_1s8o zdmk$5lt!MS*{({J94W&u5w-(IidERf!1T$d3Dj^uS%nT%)38nR{ptq(5j1Vy>MXK0 zM61Su*{bAL%SGH6cX#P|lQM%_qmug64w3}!6-d>eQdW0u0utj7)eT62@r#9Msi%fF zC8F`U6d1CO247XR*bR0vWjb3-@%MbA?G4lJq>N|40M}m7?N`@E>uL{MF7Es*$tIrQ za-i;p821(cbW20h;s;)O2A3k{aXe8D_SLeEW(ijab@qfn%!*ZR6mdbpkSR4FO~DW<$I zI@LoYLQNckefn*!hS-wCa8dZ2(8+EbqoSSg&@alzW-4m85Ss)v-9QRVYESos9`f-%$?7fHmk+L7PEm3oTV) z^bn|w8ve=LW6QQyBa2*#joJsKCftsqB5FQ!f_|uRN3Q`EK1~}ee*>(;&+l!&b~S9> zc}rE5*->f!YMk-{m|hv-=BudvRFcvCfuW}5x`ZlXqEG5k6_yRgL5iFqqiF3)F$Yu? zUzm+P18Di0ebQj}7@$l8SE(yuJP0C(KMd?bo@ri&_s7gG(X~#*^bhP@No?8{U1>Dz zyE6k&E_6503o0TfUbV%?9_KkKBAj!QY+WDPN)AdjOjadRQp#8DLZ@+h%R8~{QC>P3 zF?@919KF9Z@88k?OZNb0eSmUY8xi}!nlN=zr@XPmiMc{uEGT~m0TcSF#R<{F77>R< zZ8$v*1<&YhP>xR2>)!HY7DF3{MLRW32?p5s>qRYXZ52iyMlp|}2XLUZDd?M&+;stDZKF2j`oX5DNm zJHMrasvXk;Zh^qKQG_H~l(aho5s8>1)QR?;X~p=P(q)UewpT4&i5XXUV#qLM@Ce*9 z7Dts<-8p-x(!Im_9(G$D9NRRey<_1$GL--%=JYQe9ME@k|_@A<$TsQdd|2$+c5h zKZkmM*xiCCWTL2xX((c8AXKwT4tD)B)VdiwJ$U*&Nruso)G;!b6Umssg&A5&x0qjD z`i7yz877B?_{)mB1G*vkdSk0ib|n(5B^f2W7234h2%V6sO+JNV_YDrq`_LQ=M2V^*TOs7Ua`S+7w%ekunmd}RRlhN>7pk>&PEY1T7fWDAeJR&0>#jJ%)#VT!e}IcECO0E75!>0n^hrdOl((& zk24L?p;5gmL$rH~#?aVCxe%)`OO+oWpfMPPc+~ADVf_Pj50UE2+|mi-$N&JLNZNo# z+7Ec_LI#}h{qUOtV>Sj{u?`}=E5UAaEF}dQ?yR5)G($ug#c*iD3IjufelZ~~Sy4*m zc|Sk7Zr)y{p=qa_lLV}oi>bcz-vXdP!URW^$9V%YiB5_TN*XuasX+Svwj) zg-q0~lwi({HN7+`^s%X?q`+X{hD2I}MZ_%H>$%aY$+J?0z4e&xPy7W=O(lzXSNH7DT- zj_AF+kAs7h?h+NRj!)CO=;4<0(Q?NBHn9(1P*={6tfreD-Y{zXlbkcr+2*FN(b(uFDv< z`HgF?%N|)_L3=W;WTY9nJZ2^Hy&PmMzX4f;g1ifb-ljLG(zK;ELS^7M1{FM~*#pef z2=;=T`HY-9e9MGxsXJPG>>$cJ+N8wJNuZ;#f>z5zKKi(<*yWq9Vt^%8i!#kZgl_K| zQN+2hmGzwEwpQKU9!jj2T!tV`o}m4qq&&)70%j5wgAAz;7?=iJ=%>x_oj6?3B3D?y zRok?aMZAhJYCMAFw9pUaAy{xsO$5GKN&g7ZXPxy!&|EM29TeWcEe;_^KvxM5V&X-B zrzBz+VOfnXft=VG!MFf0=Xy5Q4aTC5_W>3pYSq*eW!OSjZV?&%Zjqu(x)Q}L3D$t6 z65WghL(rH;+zypgX=aLA+54D(<*esGu3PJiqHz66Xyk0%4Csci$`_R9j?_W=*C4B0 z^)7!$+4A(efX8}|#|3ok5L4RrU6!@mKUSbb7xP5Uzc|^LbpD}Y$k@|t2nNyt@lpS& z89WYHrU|ZV!dE@sZv9)NqA23|O08x>dvZ)dFU-aHgNY8PfQOX+Jra1T{n1dAjGLObpr<^uiPgIje-L=a>m+YX+vsQmH=w^#F1yM?04Md* zuXETj#KP)%!NaA)~Kpw@RN`l?F5lNDmp`Hjl1FHQ|UXu!G3t`BaHn)RM z5teJZN#2#XJ>yG#D=-~*>m*wsP@sB+X<#2H#df9V+dp{3_M4>V43)Wm)%JQ`3@IX z59fe54%i{b6NJ{2^m%M$yBu@kCz_eXMPWzO9NUE=2P|bh+cZ0y*av0wkJEL4J|?s# zng|4`n8+GfOecdlS_@6V+CYc6Hou`}jw6b5fr=5Jy%Yf8OZ$f&6adR;xLui$1{}t> z9ZAyLE^y>cbd7sxt!>BuHK#VbY`em}2ynR9c7zCuwIr?k{4Y7Em&heGyqFsy0Y^(| z&WNqiO`!*tsF_)@C#@~B(axV$7)G28Rs$T$6Iz8-{p%RR#uP3~dV?oVMuTOcjBHUm z;(#ypIS!1}0Ls0bD^yUFooxW<5|$Og%)1)%yH!11H{U%Xne^uOq`9YlK1qgJkFu&K zv-w7}DFxf3HxkcF9C?+-6uQA%E_frN16J78@NmWI&C)eEJw1Kt)ljF%ol=(qr2?b^ zNJ*g}AXoV@I}R6OVb1s~K%eJi{Mz_=?&tere4g|9|Es|GJlFG6UVNSt;P-}Tp3ieT zA4TW$oQsd5#pgK|e-7aD+=_n!z~?y?-z4Yr9M0$6e4b13cQ&8rYQB!<^IY-Ie3?)D z#Ls)VA>7SY3$}b3lQpD2if%R}V|F8s=H6^I7)|D)n4P$?7!3Bp$vhY4(z&qM=E7i{ z3wv!Xn#+QDnftqu%dDYpY(F2?aFfWs8#PeSZ#HFc&V#|aWv=~}It%TxJiXh{vOIH2 z4LoHBWk}KF>VjdeB*wwBBuFAIIjRj5h!7hXA53i3+pM~)Rd=;&P-g$2G|(B8=Be}S zJ%3QUT9p7HEa&Sawwx)_9|j!6M0;#dLwhKYu$(V89$su!Gc-n2jGZ7UG>2JfJg?qb zbzW5$%6j5nRh=Lv(V7U9gdjb6I3&NLrDOp`v68vWDf|5b$|zwk@TK4UhWqV$87f^M z&yj}?CDNtX6e@)0+Iy@31jZ%N@4AH?_kyw(79%8_u>h2+HI)G+V+s|J7LJS`3J;1NZi7 zR-~4;tW;leHVzbSLA!-;ljVL-G<2Zl(|MxQGpz@LmKx5?Lx&RS-aK?HA(cj~`zTqI zgrsbhkeEF$AXkyUXS#Z4dAHWO?hafUA&>5Dggmyl>)svs=^%Y0tZC+6L%7d#HyQ;6 zb3S37yfEpJdtC92X?s!Yfj=kWC^RPR#oN>RoQU;=&yhn`r2!ByY(Oj$XJU#lvkwcP zBa0Y3%avHO0Ay1VE(8Q9xaEK_Br`*dseviR$jsE##LU3J#1QFi%SyAei#)2DhlE#m zZk3-rDc+EiU3E8iZk3^UQM?Rnv+69?U26~{NKZ^kuNnp34crSP=BPTNDN!aAEeIy1 zVEy!hP90bDn~s;zH#8m7H#8l)nlv59D{xIC3}j`v5G&M$TB3JjuQ=#)If(sS?q6mF zznuAu{fl2?|1!ckXA0IWv(gFclexdb$VOgt_KfHg)QuvcF3+0{oSrhYOjU7FRccd} zT2;(dMR}*blq!tf2P+`FoRl7D98$K#&t+CdHh>U4fCyHwPLUNuJI1@*ssfGiF2BZj zH^pB1_0&#Se5@9)kt69$%5)~B$_MSe)81RflMSH*@9t@*UPV59Aa%%Ymo|V9+vKk^ zQL7B}+Ff`S#STlf%Y(_fTVVi)KzP6ND142$oJ?I#s#Ltx9GlIQ*3>r5%9LiM>IljN zMcmy*Ol1o+iV!4-CB2L_y=UIQcAD2n@ukYnq^J?phD|bR1hoOb)!<8&oi)%JM|NbY zkMo5Zo!yHenV@EYZ&A?%X}Mfs2IUS4lt;;?CZxZQlES7YB&+!@G0x4G{9#10>&HYf z{AomR8xytKuku?W57&5@Uu!(v0_U75ShvhdCM`KTn2e++ zQ;Sa)R7^%flh*1dH>ssSiv=EAO#_tgeUxNjpXFP>{VlvR{(bp%#=lHImcDz9xX$B^ z9W=vE7D*`*08Ul+Qe7u)A|zp18zGrRKa7f&N=VG4d>bK|uCevm7e4Ht4}Tj&2YT%^ ze2XI?ShvhSmRR!$Y3a1&RP?%$a?^(oWKn!O^&3V&P7)@mdW&PD2_17)qq0B^oxuA&id^^0>1!LLPsbL*NprnN0fl(KgKk0@4?h=?hAg z3Q9G07H?EV9hK8mQ8(4yhVafV^)8Q)eEe`(DVIe@OUVM_`+}ZVM+M951hHu0m7JFC z4QO7fe8Pbb7O%-*W#-&cm@FINt%&#T1 z!lJQYtWc||CBkgAACL3FA$Yi~3-NHge3Sh94@`97;?HH(BGv+p zwB8<>lXEVE zlGil8F_!$sZ`-ykEA(EOKzL8MZ~_bM*7CI0k*P>TY|@vVt&@?Qywelcd5l$GZd+|z z!37$5R_s7Jz}8%`g!Drw6y~6tU8wUTXzV?|#@^SIaL$>6b<3>e1ER5Od79#>+3 z-0FhPUdFGpm-UFCE#aJ>z~UpyJqRFEeMOE^nVmh8;(+$>pvMS0pf|G3D>5*>Re(3~ zgX;BG0a}t=XVI>1orSxC_4nmgSbuvU!iH1Xa36ZmsYvQ5IE=-m684f~EtYTHF|RTO zXw~zr8znHmo4sdU;rPy4VJzNS;lx(k)$Cu`RqVa*YW6Y|SFwM|S25mwudFIqytV2s zJj*7@@l>Ly4QhjmS^T{YJ zpg4jr6_95Q7U=2|B#Ga#0$5p=)nRv7o!jht6G)Tk#JtUIUt(Ii>DpxHAw$u3^rV|f z+jAhTK%8(CoP(%{=mum3sig4!dn~;J&9L%#K1n?d>CwCcp}6#w+NUDJdSb%6k;iid zur{5YM>hAq({*k;H%CRz4bIIi=V4-ChA+e0uKfXjD=aIwT-&3~I4#?`JDqIqxE$^_ zyD`{J=eDe}xx3za%dy=0*7^>&dTV{hRno#e;@Z^Uoa4YzBhXpOo+H#8q(nY=x>7>k zpdc5nJI0A6XnYwq+@Kzi1^z(GMrdL@(8Q{8q;iJP zaWf%cFmJ6ZFV-)8q~N#thrWpW66UL7%#VLkt<)XND>+;*&y88b@_gIum9k z^my8U+7Y!Q1Edffjknelnh`E!dr2fm3PakH?xHMHEGG0y*n!GV-Uc*cXAdGmQ8p;l zVun+wGsczHLB&KwL`0J$X$+$kKu|as3Wh|2aV*C`3|#jO69C4ECZc&vn<}f+$ES z#t@%0}{_%*((_bkQv=I zMT3PA#;pygtnBxAZLqKRU~U6&tS_+uk0GJiBX}>*oCU= zO8(SR1VLz#$S*z}QV1fv2pt5V$Y{Vyw|hhr{n)F~5!TWFe^)%`*r_!DAiRE+M9Pmy=8e`A zW_zIj-m=nBu^V!bkPhY}O2Y)9hDYbba@8=8b&@?ffzjw_HC5Mhex}BT;>Jbr=Jd+JzTq~12?0*~EQ>T35{cf*r|Zjwzu}i9b%i4LeTIK4zcT{-9g|5&Nq-GUfP7`FCgofImCzCC>tRT zZGN9$l6S5>R3d@&uwL@x0Ds+hymm1rxSb7;GQz3jyUv4i!seY*$n}*7R6+h$Jd2_F zozRMN{9xylO&T#B1=9WuIhpzN%X#(PNk+ffpMElC`uizG$m1D+kRtzFMT#A+yjX>X zV=Og^D$S{xNwVXIKvwF;1^!^~_O?wj2II`3rkr94H*6=}5{#V>_ zLSF*YfRKiF*D~*@fsm38mXj(685P5fwoc7ZhLV!PkOrxi!&I`(RXr9CRym8G6#s0? zYbmxtFe8=;qadV2-b2H;6z4bgN@zMis+M5*Pgm>w4A#W2a|JPD9W3K?h ze$d&L30E1~0dgX%D9S!xLugB)Bq2d#hv8kIPlv$W?4hXlDyB z5Gg7N@c1f}0J`+LA`)Joe&6f;lSOJY!{(sj{LN@Md}$0uqm(3?R!q3FcPNSNVXdPh zl2|B&@wE#3qsW>1CAC@00*jknSqTx}Ql%q5Ok1YCeJN@1Fc~I=Gwi{EqP?z%H3_^G zH$f}?#6A9)?Tp7NcyL{p(nudgK1dA8d8>tBb?F6yMEh}NU*aEZ-BAJwwJgKkr{fqPmq z&VDrE(!CKuvUqL!i(e};lQT_Co9N=5(Cz+SlH06f*uWF~@+M|SfnEmA>xoxJ%*n-9 zy{ay4h{(vkMJ}svi70(He(R&>@=w`*Fek&dW2bKFFsIfa_Y0Tl03) z2yf0AA8@iOqjbZ=mmQUVb?3#88seTa{m5unS0@@JJrMvX|I!Cxq(NhyhDhv=C|rUw zuyHBpE1ZZKoHD10ZTIRYPJnh*QX$L-{LU+(I%j~IO9p_Qd*A4^FK?|fieOS$D@ec) z-sj>6KoKHzn!SGKsQA#1hnXq=n$BNgN@o4mKABkGCOGjPQDT` z=Qgsq%6^etE5~1@q`V_u0uaEi%_o8slf< z9X&*}^sooosOoMEFQ`=_wWMk1*NZVbqZO;l)5C_5lK6K0scug~yBi}LR~~hUrgh%y z7gNX0D!;aj){f*GkxhmFJTju-qMtQjNAx&}!b3o$bVn<(l0oh3V#A7d*AfihW9jeY ziyW1#NsnKv7_9+jV8i7vMyZT0+}l$sHSodA%{+T8Wzqz>}7DY5Qj--m6jucYr z5-f1D`+&LqBb7;b%c#&2i(2in5rr`7Jcyn|VbrW0I8BW_x6|?liiX<;`8FfW&#Z07 z0=0yS{J^N~6{KL)msEz-?Rn)9)HnZ*hH<+(18Md$EnB_~ZIbka3SUMk-Humm!0RIH z|7LCk2X5>@MD`?9*PeIm-EW%Of~+kyyl+4=75>Rc$H_ksRYu)JUlf`-Xy17ELPY52 z7-MP4+nBJm@~a4J28Oggz3)X)_iQ`*cg6A|YlC$=f=^YKXshD&fhsO!*xxH62w2h! z;zh|mtp{Q;YQp#_|C5~!{@jWG5*A*RD94L(Y!*O6UF;^78qZ_p=%Q^?UxCp4CN(ec zgu~qOlGlSaT@)Md9&fiVCm?kpI7}8o{2okMx<&a5%~6!q&v=$VgMzV*vA^gvVGP4? zx_r-={3OiTF;l>dY{kzwB8E9{Ae83G{DVKG20`DoXQR{8;h>*J8W)WRO-FjdTW1U? zEgjAD2)w%&<5WYTCYD(A;Xz&&#f@&!aITM#l@(6Y_z)`-PC?L2uU0g^HD-j5<%t#A zH(><`$^JZ5M6bRnE?v3hI%9%t`$>i`iVvXF=(7Jy)6bMTTe*RPlh73VH%N$~%(D8+ zR?|-sqDS&l+s02`&hPT+G6}d+kE9*1xp2my!!ux%`hOn=sV%RiUK6l?zG@(lhL1vS zmSCa~Bw7*3)70hGQnUS9a6LL(W(PV-L9j+X(2|vp4Bo6KzuuC4kJtzL+AC0J$$O&ugGOtYsENkC+1x=4n(1dKOs{(18`ezzvt`Xx zZREpC@cvmNw;t@x{kM{seR1+YnXCa5!`@-J!Ue{u(n!p30G~`no1XYANj`oEAndkg zonJ0^O+_XKP>}TGvaW9S8_kAy{b-gH*=Epv@QK~Q;erodu)74-2r1J}hr+fxkjLuA z-wVMKkxFrm!Od}p#c+*6`bj%!1vm^fNdzf2hKI65U+<;|%5`rhnv3r#z=?M5@B z@J%^v9#3F0yENP7{%kN;XFO?o9v*F zHWN&8jWY2vz8<6qYXZ5KFQ`)9X$evQ^lC|?|CFN-d1{_}{N8gTfqssByW_s!&1 z-RQ_&jorG~{=BvZ(z`dVdizUDrT`c_$<8qdaP6vy0K=tel}j)e3HSpV!voA^JwknD zDQ2eur4sl8iUDAq46DtE4Y1X~o5?BUzpg=))3MH`XH$Edp=j)|=&Ur_7R8i+eRlqB z2n^OY+7$o1{BiIW1)~VFqAlz8B3lx>1#K2dQK5dgTS<(x+0?uB&c$i7G3aQOMCSYA z8F&MDHgFc;LiK-ip%$c@dB9<;o{lplNd$DEi;uO9qYT9eXIv(=0M=v&d(h={IC+Yj zb-g1Y$vNn@cMG_0k^hP?jn&bHO{YC347^d)f!0Uc0j(!tJL7*DqOUnHU%~yaIs6|? z)&b*E#W-M3ZK^h*T;qZO`vJ&_EmIzJ9CVot@K2}81I&!;0jj?PVxDz~E~F`af%xA7 zKV}2x;9n?ly+0R;EJ-?Pu%n73CbiR!p+I((OIhP0oxI8btmXhhkD~@}7>mCM_RuYs z4S4i|`@tJy@fAUPXn?A0fn@_qv;%4evjGxLFiF({V9+1H$bi==_W=-)Dg%^zhJmk1 z4UktEaWUY!GGV|RTTu|{frz#?M={Rg00xFDXBXXJZ91H?1m+PdI-P;;r0t+Q!N*=o-#d;er+EOyMh|RLlrX)hdf98#O z&m*13;ELa4<^N`8W_x9S_uU|8pee>FII{Qv=m6yaZ#n-qt9^s^Mf{R*K_owSJq$fl z!!;8`KBU}WESrrD3r2&vSkd#V<67O<#+r;+PMSuWk;%wtL*lX<+2B@3*_|az3e71y zdnAQA81!2L$Lc1|=*r?|;;wx}UHjQ^>!z=cyCqrEC*yIX)omhm8W2`n6j&~uoV2=U zCv`YWbvPSVY0|2QRz*f58=NC?q%f=_Vc0=(OA@6InhPewUHj=wor|sRd|U3AV54u# z)dU-vNL{D^8nsMn)Lc46jsj)fkrXgAD(V1-<+174JUd zO{*eqeKH;;DTBFeFeExDB}WEBHq5XQhhPnaA7CR6`L=Dx%tPVzdV`=_8g(1-m(A^l zv}bZl3}x<@7ONEsukme(%pwTiw{rJE@>`RKLT2Xi;Q{ebcwzdz8oJ98B2xU~y+sGP zKFEJPOaW)NDDT>N@typ__QPF%wz{*a;nd{0YR}osIpbRl+k#={*^*k}8aa=xAlL9~ z7fn1M5O+0^;V=6z3KA)&UI@(+jGye>5@ZP z_y~b;pcNg=(1yNu-TEiL@7)wApXRB^Pg6>z^h!m_i&Alt;+SI-{fsvmq3`D?Ys8|H zysx!bv-z7+Q&=MZ%RKygPpj5_4ZB4r&E0z`P%J4$+-InXlN4&9PCwJ~5iOTb&QwG< z{`6l=sXNV8)B3ZAbBHN*s;>^2)M-da3}ur#%_gNHDVicF(q082_>1o64=uRJ-7$Bo zNJ>r?uXERG55KhP7Dq86#kc+7alZ$vYHLqb$=~`rrzoAgqk)Q(loBsj2JRR~SN)#V z-h!`rXL73IB!#*LtE{T3nmd2gU)MEJDI<6WA4I|h5zqO>zi&rM<+XIb$Jq?mXMa%Y z*LHFRMXi1?hM8py-W!$-N5;azfyo3yBq#vGK!F7WUJRfr8~_0TKmZ2f017|_fdCeA zg$5TfE{tqgE|-jy*H)0|Swtj|fPi?m*8+vonB(}9U;Fafx7^fkxd>T?$=m{^>u~8? z%8QzOlgVf<8_v>5Xv) zNAEuAw)!kWN1W^90O$o4Duyhl^kHe7N`$W2RL-s$_7-g4#*8q@q__^WduJfjXLkP zXu694u09XPZU?i*Tl(rR-3|l9rqHxc=&e6gWbH&BXy%+Hy&K?Rx)P%{UdU`t3_fnX;FGDAzY!K!XPL^y~xW2(7;RR19-^J}3 z)j)>1ORfde@H|B2RpuAI5o~S~Y}F5~0wxayfN!(n@c}H;-kt%z6bV-tfWr&|MvVaS z>7ES$M}_MffXV8Y);6hFd-{M%TyK-qY`IMoNv;faqn(^6E(o?^Qx!oSrzo@iLZ62p zeh3l&B6BVddQcQT7-)1ueercyV&%5*&U0`lpnZWXybTip>o31#IbuH+f2i&XG8r)j zgOeWG9vd8(r-2kJh=mJXkboAhAqR5E9}0h&+MOAC`<6`=SO_NY2J?*P_s&cHz?E6G z&|qx!tvGzUPZntNcBbQC5K8GCI9EFb@~%k8%&jXb!(C-}dMTmAS`bH^OErGS0}ODE z)u*Ir|E!9Yqit~4`jL=p*`|4cZ{QfxQMRl{U0X9;m{(5p+&@*VUSGf}4upEAGXz(8 zb4=Q3L$NIymb)E|{I@~>Qj7_b7PbNBA)l*dHUGfb57#lCoERAJV$ei%WqmtH>=Bs0d5nz82@t*7eA55tiA4 zE4bN);8yQq)=(u1CuPGv7uN4;QTwM1U5!d2!DbxPdz4A6R1aKNM|swoK*Cz=grYg12+1q znJMzj-zcJ?A#AP>?d9CwNL=SMRkac=crHRf%3~)ZpSjerhd(({cj5v?kvpZckH7h| zFdC%cq&;}__aX<9gV%l?S zDLV+UI>8>*=&6KQrud+01Pf9n-CLrht?do-=HRBrwkFol>Y^Mr5Sv=!o^lvh4AiYk zge$LbZLSM?}90{#0|VDPftfLeXMKaXIE2eHsK2bwuw4>zp(8 zAPEy`HpirEIJjBt5c0{cvx(t@rruEN09BTkr;*|8OrOXY<6wfs+Fj;8- zL)&mpT0+41C;>4_Z*glBvVDgc%)zf)al`}Q}Q`|WY&6{`;sLE$Dte-8EFe59RF&&p!9-g-1_=c4~6 z4VkcUYN)7WU$*>h+PTSy9+7f$5NmVKSOV%h-DW-STz0b2qwQk0cAn}+&zEh^DcLuB zj|Kvg94Q)(!rH8PY;BXw2qzt$t(k=n1%7Agl@2MttxYtRpY{|?B9Kjuk#=ISU$5x# zgEHe#6KomG&lX6~fW}v}|4#B+GlaQ3Btjmmw^Laq`tOQ$94amvhtRGgcbAry*gj78KhAB>Zw$VkH9`p zwHhNVbbX{x^FX!fWpHRI+M7+IA5+SV2PMoK?zr7AqILsg4YvXKFV7jhdz%U2H^;1l zE=P6UC_SRT4R2;;Aw&`GR53jTjtDqCXyv@LT=n2{VYLzd~hnecEA z3vI|HH2jw2EZoSN5uc$K(AfXrR74e95k?i!`3D3kgR(2UR7G@2Kqk96Nz9s-o36!= zpI|UIAXf4_XNvy|Ue}laETXea3{*N0xcsuLIfn7c7_r<$6!8Gc>6q=Qh_aJWN3@E_ zG#gq-kPV3_OwNm2`6w~tA1Isr&K5WVZ8nM+5`Dvc5XcqaK{CT=`jsPBS%m5^dL$WS zD#ixc#xq1l_lc`j2X#1|&%IV#-X6&pE*e*6C`w&@5IJS0zR6^vPDI5qOkmxev>xl%x?SPcf-cQ4J%-T4LWw^%RYW}EVyh8WnUPL`b zm0H1o2dz0Y?rqRAA_>>4s9~Q4zH`aAHV^GGFpf#`J3x-f*V}1~4 zR*vHYg)y0gIpijy=@_p>m^23g-+&!iLcXT^_R>sXD~KTI3Mleq;o&SE=zREXI>p-?F0*c(9MRYt*b(fo%V@kKoX|NvC0`lqRUB6 z5g9@%BGNJ#IGy{j0?ZkZxw{Z;br3Vu%Lu6IsJdDPhm*~w#Ft`rd<)q0;73v?8|;uX zGcr@(X!%ejbThj5@Y1wuFIFB)S&k&=gova>XVW100TIj3<2KWnx)I55x}ZZ>?Z_kFTE*Rt;7NK~O2A!1 zaj%HW|7=hu=w9hWk?IsZ)s`q-bt?#N`b%JtNFD@PkaZ_U-G)K|41S9oj@{J8siRO( zF+@84$ICJ89bSd`k6b%}s>5d&ciJL`v3kK*9R)y=$ZNs==b69Tb%SGbW~#w3kq=SO z!mPseQxw*!a|T)Zq4FZ6j!IrIE38D|et}YM8!>)64*>HjXnsw)6FoZwPj1$e0tck! zex^bdifd=cEy<0IR7t_cbYFRM(VSkCR_q3)EEIkH;^=~ld>BrY;8B^9`FtKA<;Le0 zK`bqcxCh%#grz(u$kmlbDR z#Ut{WLitE#MRzqC8-4fq*177M`kF-@ndeo(Pfb&^+Q|WCYs-|X-%+(^8}Xo8xN>6| zFd1Tq7kBKG*u=r%0n3PSS#rSjk&pa%%q22@qUvBppFc8t23I&dEVsu@xl^NEp&P12khzs>i%-A zTOd*u+6Ivvm23uxDv{`%iLMgdj0nu!8S7dzcTE*VwF;p-e;OLowk>Wm&&eQ*+l<(z_H$GF?_qHI?C-W$cJGt|tqRHt%?e85@t9PS zt+dFn*S!V4(#oVheA&dt+k z@>CgGtshZ?j0ylw*>8Q8?6-ahuQs!PO}dcG^xnfvz%F^kO%|t0RwN48=b9bL8#rxUJvAqEU9yK}PZ4SJ+#-e9dbSWT#wO*H71{OY8b9m)0fm!CIRAyf;Xa z^W=q|yv%623|m)eBftCAXX#Dbi^`WVQBU3=L(P}e3-{*G*>Q98M(ruz3-!gk(RIq@ zg}S^~vsd~?+1R4RDHa<x(ZhV&eRAE7y*Y4$3w~+ z&2g3>!2%`0ay4XO_yVwnKjUmW)V$*M8v{NP@uf~g4sD%iBr21*(nJPvs3UFtb{gt- zF0#j5J8my6Ese2LQrDA7G-Xnc4fQ4aSn~||41a1BE#TcQCQ~Shc#C(pi%F-Ul88S& z*>{X#yT5y$D;4qh5Cv->;j7}c>S&`PlBkqnGr9IpACvC*{u1+iyFNkSOSLR0n}TKQErT zwiAcCo#$SAOdIiJzh)JsAR^~5zoIjmHrx=I9Lg;)-3P{B*i*BLJ(Q?MTGdZOaE`R9 z&l+h}mXjI{nN;RB{_ZlS@mL(bE3VJ$#y=XlqBj?jHg4-oO1IJcLlWoStS6JTU7V4` z8EFU2PWL<`Z@Y-Jjra`%MjZe06rKq{?XaUCnOgYqG~+f?UmDH0U4%jRV9ff`vYvm& z+XjI5|F-nIR`|BxW|ggQ`5mjQwLogMC13Z-id`R1OTG@Cs?hv9E%`dvwB+mB=?tbr z$=9t@5m{RDbv~=)>--QMTL`2WqyPk<#*BBO&E(o+W=!1L$0XwMNc4I4PtQo=jN5v? zbh7Vb3ev`zh)X)244MGc4zbUSw2Qco+ea8kW=tbvkhaY;BkiQHXSQ)-QW0rC;dTt% zF4}hs?|*Ksmt*l@YGBu*HgH{Y?2hVSnbQc@y?$J`46|x0`LneS5ykhV|`vDe>XHdSI~+ z_to=`z?bmE2$k&c0~1gnQo$y}+PSnqgZXyLhat9V=v5_D0)=Hb5rL-_c%D4Y6FOXc zV5J6{DOd)f5-Yd^L$9j4#wU`!J7uGOGAkd66$}X!hUCgc0xUY1CopZ;VxTmsbY3JT zk1uWX=@D$+=Zm0FlY%oB-oJu00eF9t@saV$*;S1CM8EY}pGf2eGf{!WP8}3%FDzNR zgLqfmJ$H9`|1^FVeMFsD^a=flpc`o>(#CC~BOyJ%mrcnfU(ZMd@Wioc_Ss5EB2Khp zuNc-{YSG8}7j_h0T7?O{N|$AHDxAjQW3#Ya`7vyZYkahf%cL(N~QUI*CEASQr{ zncuxM{%HHXH2!WQY#-YAy%d0r%l6wi0@u+eVyh8E@L~$)R&|U?S~9)+2smjuG|SAS z<J2XxpfotM>36Q%@*8>bJHirC7CM0Dq+s&}ipolMOb zD%fHy7g$cIB(5Ps#4Z{02QyV##btY8t?p3meAQ2Tx~iS8`mCL=`XMDcT}}jNi_!9! zS*=~m%0pyYRvqFM;Dh%*2ZQBYhQoM>@&+lY@e?&vVuj)ZC}{X8K3PhzROBd6#(^ha z(PXUM3x^R>BW%cC(h|5mS$uukrBambF52kxKzd;dR^3@r!OppBS)a9Q*%4h%1b4H^ zOjJ1qHtP2xjn&|dNMAKdM5Hc6$bpKGb9EvIdLrjai39EwNo|K}X=#V9J%@opJVimU zefH8u?Owcxv0bIrnHF;kzw`C`g)Ed(R607n+xtJl()O8fNS>ENiisfMbQ{u5|HoQw z)Bmwn*L0*^48Qc((ChL$9@0wN8@_mu05(TQGXnwy4G5*OW+n$jjqHOPvB7)q<}fc0 z@1O@b|GPtCP%AX(3hiq50HX7RCUrr*#Ly9PggAlM#F4_W@tq^&c!{lEQ2Z=Q{jS$K zM#)eriu;=OSSuD5SYF^gN%VO1V&F~na8)6qVm1dD)+&gFpciu zNDQKX%^=kbr3-(voU;PyJR~6p_&-dV0Kx1NthW`N7 z>mK|#8xtT#EePqzn>$iFHfk>{-3+^FI3vkziR~_cPz+u43|+f$^+m1Covuxs|9uwD z|GS&)0tStMe0+F@L?@1d1Oir-qV)L8BI5n zlDjt%gH1%8-RqW-l#b%}Sd~xR7$iacLN$FRVeq8~dBu)}iB#LVIG=-#uY{cZ7 zoWmM5A_fFG1rr0ZPO-#*5ILJrxKFrk)M7eXz;EOYAHV=CO#m7tyfA0HXcfnQJt3y+ z14TPy*Wg+QSD_N90zrP(wyn>yaoRLVR4}tNHRyKUq5}^CZB1W;_x|;Y;g@N}eUYn_ zp0=kIaNRZzot}oSS*Vku#cUO_(T{#qv-$c&I##V$Ai|D(mu9pSALI`6M?;aJ{%Q z+qNh_uUHAx*w6YZfg1Z^*%G{7KQ9qK+8m#c^P|o2!J+(6V?Xcghf9P{t%MC~>}Q0< z1{KAJHlpVRJow<`#tDvH&C5erY)}^4)qL}{EgxEDey4SEV^{O*ke3QH=S<3bo~Uqw zLxl#j>a^sA=FJ!bbfQ#Hq()NAsGg|ZiFEgh)=pW4)#eE>vovjX$SDG@jYHGGB#9k>aVpZg{Rk*!U7o63O^GF5TtHYA()4m^U=AHX~ zz>~R-OfJDc=*Z*}yZe5KclZZ*kKxzl*0qe%bid1Ix!+an`62jNeNPF`hv)IA(6nl5 z0Aj0pFDsTO?*-+(u1a23C8SE&2q6{122B)+m?%$=A|6FC{CqyS$~Gm^0*g}zlRoLT z(Ku}5SqFFB6c?50T|8RT=Xf&5mp&wf3P7QAgn*#0Y>8~IR}8;66Ez0(VJ2|JfL?@9vT2EC7QgRZg-wo31xg8a-T^M1#Zpm=joDWKK5 zXlL)^d{*z{Y;btgMGhA`fI3)8V&0W?QuD1AZx*B_N6QW#ZYBkyM1d$*5m4Hpv;)Yq ziYiG!X$P|gPgDs6MsAGYsL`Z187T!F42uU3<%9Pg0Vd|T_}5~Qxb8EM#MRE%J*H=V zH$SL6cmxH%%gO=j5mq%RRW&JBKNfy3;J6@wC(Tm;RP_ibkXQ9<(v3r?zvo}50>9FG zliCKib>z`3GxNLgHR;A#X69$(yPbr-9i~fr6))t0f{DWKRG>#%)%MrlMir_+HR-(! z*9$>@=6?^Z)`fl6h_m-$7nldi3xuip@&b7gshVG_-EJ8F+!}Ls!nSiwVO}6GB2~K= z$lZLQ0G|NckY6m`Yc<2Fc%MNsJFC08nM`w4ju#qqC~%$G!@ZH(#OJSTJH{gavb z(YT4RDPhRxh-U^C@yvA3XW#MtH0v1c7QUmcR3?9B+@5V@GRPO>vq2FQnsu6z$s+In zkfqFtNeRh_E~oQq5!`Jii^1eq48Qt!`qh5MrL%R7YVv>-#Gr$h8Yek3Ha0mRYKFL^ z*i^JH|D-AXfAs&&ws1XUzx_i~m(TNCxf?kgwx-MHxm!LDgP-rg02Gi#0LBX$$(~h; zt;IOX9p^ym$O);fGTk|LEgwFRkd!$lIQql!!{)7JC0pmaFi$ zP;0cbFRxi9Vx+V$uhU(Lbfuzw*-u}bv@iQC+LvFF%4HH4%55bZU-5e?^4yqq3z+sQ zEZcjFb`eR`qT4n$MAx>lkl)j;H$rY&o!`L_8JO9God$`30fWU16B2M@l;ZWYj8TG1 z_ue2kFxb1(7*^M<6CtW{YWS&v?!h8MX`Vu7(V2`Ifq_QlqopiwH03BqCW|xh6jr)~ z6D(~MoSpEJ{(Daa9XqJPwPsVSHR^hUr`y(N1$)qK>!&~}8up}Vo>52M>yy0V>L4Ts zaq$5cGz5DXWgK?$RZhkdib^4sB1eiHvx#)&g@aB4@TAl5wYNvO1y2R{UVXAb;IdS@ zw)Inzr><>%mac6{V6dO(4WfIiOw2${%v`Zlx)-X0Nx|qhiUWjP;<6pN#D^?xXG973 zOB;PUXnnmntiPw&{5?JGDiQ$i{gR1#vr552iWh#KDquC(C|Lq2Iv#j$wCF|=jvo+j zG{z$q13Tqn=8ei&kiF2t?uG7N-e?YvN>$1WO~J`Uff2tI{x z^oS)Ybeg~+sq5|#nH;`D*Y1ShOT*XR?rV-VD%wp-_fg5UACI?^-I2+n zJ!9I)WRcyEa-zW%RVs3zDsrxzWN@0HNadk?Fe<@9BLT;U>b}zNzN$bSBl1=vDs$Ne z-vdVg-i!eBz!&%DovQ{SJ@5;4Kvu0i5XcIFtIz{gq2I|bfGv=iq6Mq#O{l!;E0YyNvcKF|5bx*X6dldi1JH3Ge+0sMt$N!bV+OtaOBp z3ZN;%LPgjs7c2FFQi-ES&X^NoY`|h3a*WC2Fg|PPg#8mg4Qj&v$%#z6x3pA_4VoG* zRIqXCa8e(rUkv1g3oNFBp1c?c^8x=Eqxr%Bn&vE)@}hX4gbFYsW<|>|B8J5Sb@E!- zXs-?|WD`!yyL9mH?!K<{l8Wnx5N2Rw3_$}WZv@mOi!lTm?0^FkpBNX6 zA*>*WDQiMN1jZjj(;Mi?aOladfX@_}74Fn&E+0VSWYMH)<~3MM(K3vP^kcDd5@#A6n%rIv`Scck=r^9c@fE%r2zZ3AP{#Do zm^7Si!))2e<-)2i{1A}IH^w$s=vB{8d7qROdQsviyd%Qa=N zpH1JkRkOSTNJx5Am>7Z?4s+vhDDOdFO%yaVI;!EN3^kFjX0lWSW`jxkO?-+0fW1-4m-;Qke(f%8 z#NHJ7lU2Fa?623=11sBOZe4!*!a27tpEb8GlB5_@DTym*7Uz9<&to$H{MTts-fi%I zZTYZ!gP^=om71Kx&IvwIMN%P@Sh+nwlt4k0T=7bzHh5L1BE*<|gN)xw#?5 z2U5`R)1v@lK%Kuxb+d!s?5=bj?~%@gkm@2xRI4DWSIlHPOV{;(;m&S7Zg^F@qhInC z5`t%Tg@b5nCRk%ZI1(lBBz{(0Y_P$rYwClV3!2=Q4}auNIjZ9&_sh$4-&Ht zUi}tRinirL%^RfXx=}MUE0L)l4cen!-Dp^Dl#gaba)ShiNX^UB6E%LKNQ_t!O~H&n zVMeY{a-i%m8Nsr_tvYR^>KTx_23pr#0o6c(YOYi`ATxu?%&t^sP?g!0ZN>x68@(Z^ zvO%e;*;&Co&Xy)oR4jp@(?rJuk(3Bs)en^jT~&z+jA(&3A{CMSJESJ(z#~UZpj0h2 zzl*+1;<&x;zIUkK#HV=IW!T7gC+fr>*={Ru<#4FmM%n&kBiB9NfrM(H6vMg(`UwK% z9cTs7is4IkJK5}hqk|BHAOyh%Iyz3&XQz&n6{Gx~iSsi8J9zKsB}}&=v`V%1Cw}K@ zgKn@@7QL%eeNe4Mf-dFkDuYNbtg$nBSL5Zit=aXJVpV<>U#R|S+^=DU%BI!%DeSaq zbw10c)$v52#6Eb%WqGSuWL38(Q;La$+hZ!V7H3OKP7JF*PJxY@iII}LQ5umHC{?A~ zeWg`?4Na^1ESgre(a2&j*(j!gJzIy?k$2BVCW~~hXCv=p9RFbS+sUr!wwgAR#dP)f zW#SsHVLH-FciTGFRT-GASDSD1BCRVq(E2IZDw7hVV5>^h*g3&r=j6%=h8H}Zjr z#g5&{%%l^Q!=bEV1yDVd_ZnzL5zx>u1yB&^0yvBvjk3Pc|01#wG3Jix+LP`gA$DKs z9Cb4v6I1w-?joVt>7MPlh~G~-FybHY{2XmZ=nZ|25!;!=#3VYu$HhMBYpH zkOmu(H}cc4V3C+S;&xHbU6u}dda~F3XWz(KXUd7Vl1#ib|Mc80;>)Kwj#=VC+%C>f zPd1EM&pq&?$b0W&=~7>Hr~cUb*Pf@=Z2Lm}I<(pshx1vpkBe`P$z&3d4@FRj&U1F+4!u}6UV=$C2^i- zr0wK%BWIjQOdjp}%a$$s!UBW=5MT!$Ml7HP1PB|OnWrcF<|YS34apFcAEyY3{n$+i z%oVai(JoIiwfSl+cmZFWklO%M& zU1=j2lgB@a9q>Mwx*kWBZy z!iT)=7`KZ6n13`9Y3nv}X~_dWY~y?cWHjG)QZGi{DR_V=YN8hNkLZH9hFPJjep9eQ zw_b&;aGoA1BF7`cnbuRB6VSeuu=GYF_Y4LY{KuS`Du>HiN)Rw<5=`H=|&iF{G(wP z?|hD#W9FC=kjYyo;z=d@XU>7V|0heB_A0E5TeB}ORO=VU%Q{hh?6XjQe1jKER1iKl z*#;JbPj1EsTjO(ceXun?xeNHndk=n(>|ZdGxVWt*{AszQSbIde^!l)gb- zo+p~MR|g@eBLj|_wV3n=Dk%U}IZ{Ws=uwWyq{XDH2PhU_pGPT{3A(4vc;bMxXlaAal(gU8ln>LwuvtlTlnA&d9j7aGoL{oE4 z3f}Cs?55kEi$x4 zzEUBj!iG;&%u0JOXb*NZlp5LqM!i@mOAN{qyGlwWZ74;-T3?&0zx7(`Z`C0y-@8Iw zJk$jST7d}?jNk$XjMOds9wg(d1t_A=EE&x!pne_ZAiFCS|^4Zs<^z(ySE$e@#rasT#^Z)wg zo|y&uE+$xeKra##tT~`BVuCdX^ny*`Nq|Iz@WYb;;piWp1c=AuvcbbZKOW}~y4{u! zCDG(ApKcUQ z146G+NCt>#&SEYwAh+`c3L`KqSPRF~AUO(zQgtFjPvonbOhbx>4V+BsM)_z!d?Hmg znv~~_nt-uVqSArk<%PbyS12$LlvvdZmipf48^uoDoNCk#W_1F?g0-(Z{efzES(!L> zFDxG@lh}%+Nqc#DtEOvlcReqzyNyxZ-TlU>!n6u7+Q?_)${%fpu5_~J8OI~BH?QPP zMEgni;h1@@^oud$Uw%B^${2geWamjHr8}l)nYlMN;l!fMoV%!jgg|p{$GGvIIR4Er z^Nb^HgCZz&JT8}5z2!Hld;4_3j`>YWD%(KCY`}Ta74ei@gLi;C!nUgRSil7S9q9X^L5uPYpdRRkqWFCUw>68udz%;C!qF1m(h5*RMqM#r(51%=FMZ4BN#rjI|ef>ph zu=SYg&zrNCgAYsqvRdz5cXw}hcDIj8e5rKz_U5;KY)zT6@=ax$vh`O!Z|c-6;Q>15 zh|bSBn7^AApD#U+;xhA3ruXxSg{K!_dfvo^{v{+c%paMS;DlXhd8Y8ZZg%-1IS~|> zkDI!18dqSS^UmK;eBV}^dw)iooz#3;ET#A5DJ$P#@=5RSdjFlW@(q4n?ymRQDJ$Pv z9d;~VowD+6W#yZnny~WCYq9d}!@y}{Q zXJm5Dk@u5+&pEzKB9lyWZ)Vcx8A&tJj&b9~`zfDeVsgCa!)@hkGX>`t85l5&_?+vO z?4B;e*6o~_#9P|!Y9yhcbOKRo#0v%BKiZ~AH~OB6#5uQ%H{P3VnwBIZ-$<7e!QEUo zxaOj@Gg%BKzhd}xxh=b@s?8MpbAGz=WPgtRIiJP;9B&5q(^?$N77Ple`j;&l{d;t; z#ieDYKg)E6)8)d-Gll0@X5Uwt(r~_9=si<-K4tFxnK$oV{%+djoUww)lQ(;mUCTE) zF(D}-Ig$AzQx{L83+zB_*$)C=uJdN3b19eKLA@bg?z@wb$kZ=2Ha zs(vn@c&@1UT1v~OO^KW>R?wY06hAxZec2R+)5QugoI4aBJL!EHChvOxHNBlrE8L#E z*~dGT4^NBEl-a+#-gl|Y-rcc$c1n|Rtqvx?=c`k=NA3eT8b*i+k+U;J#DgdXnHZQ^ zN|SNxbY81m&Y%L+Vlkk}I6q})-%n{W&TG+RoH~RD>eWKb01}ZHs7}Y@aw%4Rmd6c> zpwJLlR7A95_zDgQ5fu$zdr*A@9HOXseunYxqT?O)hovRn4@*Vn5=v4LCn}-1D3f@x zNr^}^msFHgeps6AI|j;#r8!CFWu|9lZ&sP}3^vT(``A3s@vn#^GR-rRX8kVOvz|oG z$)EWrbAuu%^djTEua|zGX|LjCqn(UWaw40QkjNu3otzWF$+y@h*Jv|Y7K6zzRrrly z;Wv6+YyDHGbGVsYuX}3dwF+AEL+>HqaHjeQmF^lGf3J0jr6cClU3z#9U%-N+y$RJ&8;*Z)l2n@;AqnP$rdgOc5t4{pNUX0h6ww z`OU=O(AbzE>=1ScTgAmBE)kcAODZmAPBEvLQ&e#=Z|ocU##V9R#3XoU_nT>GOlo)i zNE{lI+})U@?k`Qx(EMg%W^QUkZfI_3ZfJaL5E?6lc;?QN7zi>-B?@M-e4u5NNCt4h0IYva-BwtKwyI?MVCf2;gMeo3osvu-0Jx}1m)u4bZ+V1w+O zSn2evm{Tqc0MKE=LQUAKSnJ)b7))KaepaAAD_1@(jOfVCq&PAZN4}EDSST+XYA>lP z3Z`a7*PwLm3g!k34qOcjj$*l4sc3)-9Jw2o&V|Fw#X|}3Q2D4Rk(_b@aDIsrJb5}qq0qr_%T0Sog{wDXOZ|Z6*KH6m&c0xTO7=E=m0?l zc(wjz2rJ|0>3@AcjiF-Ir%XQ!%}+%*g>ne(*qKO8RKA}UoxGiN*ZY2YJD+kq?MT^4 zceDRahoIcq>b2aFwbeSI_27=IK5K^53>&wZcc(p2D6kcbB@-K$)E#NNaZATE;++^1 zvkYZT(bl06@6b3jznM7(Pacy#L?-2tILlDd==M{xNMy==GFWDC;+-8C%A6YH?c}>j zST6nIn8v&#Ln%Uph*>9J-bEtrD`Adfkhe0QV`Lj&Bl=~C_5@>!tbT}jogrOVXr=seP@09iaM7r#9TJ*|3wYY9-pSwUIuTyJrzy#{dxH z@Z;Kzl&I#3nYji7PfjH^g~3t|c1{1c#rpC%*?&BQ1mxkJNM|!Z#s|Mxw9v8-LZK}( zs*^{Xts$XA;E!}-%sd25j%&+y&#lq5?L)-TCRGqJg}E)xO&IV_SuC`>CPDX#0MdQT zQ`QtaA(Mh8!6;Hlp-gI+PUyhk0cQj~=%0lw5xFy08jfAL6D+FLHUegx3|4KYvlt+JT)rq6WnYZEa;cJDXtK#H$s!mK zOVH~AILLwaQsIH8T<>*^B&pk+i3&ZcHVm=X1UnG_v6Sp5uH-PAw7g$?5VHD*fo3N- zO#A7hDK~bgYl=a!r_qz*$p?tq|be z4NdQ8peh(vBu=Z94rkYzNK?(javy8sBIp?EV za+P9_D&p2eBkneG)?zyJZyy2hr*q9`%qguG2)KKI<62&@Tsg5#riC4<+@k${V{P%9 zP*?ept8@CceXf+YuVTs4JgrpR!7!((!RM7%My~$VKB4-z>YxwobOBZ}%M{#X)}!7C zEIxXG%TP=YF`NlJN;*B6NO5eny?VfJaT?TyT~*>yntP(YPR$Kf)lRVN_ejHO{%u{W z}tx*N_%c5|9f0-5=P>&)N1^yvo@JLM5;~Gr(=rbXaT{%j2rz(Q5mpin^ zwr=kNc9ryPssbSmYpQPmE?7 zp&(=gHp;%tC>;-!N|kc`gka?NvjI&Rc#k~99#<$ZuKGT6$O>Exi1Odg7VIJPg3&4r zj-5;fGcF=FnY@o}HF)n0&=K&|R{^h&Ks8fMN^gfrH|EN)J^7^YZS1PkR=c%HiQ8vV zKW>HB!d7sg?>MOT*+bz4Bg|~AeJAnfSEJjxtFq`6rkgasS<~@4y4F;{j+e7lZ2(4l zw?QbZR{t~KW3r0@;@NyE+o%|ovNjfc(wjEC<5j)&htyXgK0kkcAPzE;F* zcNd{YNB$UI2u_d_1vrx}BP<32>s=dIlkzSkKA42(3<6=_y zHns=a2n>PNR!j+aLZW~bN6JGm5y;R36D5>am!f2#Uq($uyvcph=y^F z8JfNjo#q)VIq*8c0o~8AVvJ=_p57XPLp-N|3F#GbRNCLej`8XiEh5x2Y%O%)4%38; z^?^H!DaL$(~fthRxXE&nn@ysc1Bi0Jq-NL|Nsk^#!rJt9yybk!AUP>SC4POmi~ zKrM%Fxlz)A)@Mh;AX`O^501xS_&uYXb3yN>gkGN_);yC)Xf+`(efz2bPKVC{O^2QV z4u{?WkB8m^PKO->Oo!hC7>Av?41{Hp`aCC1f%7tdfnQwhfwmL7JOLoJ#$AYDFyiLM z!d8WZ#pU1+a06%ltZPG=+0(kj=OMbeEpC#qY-4#Srz-on5I5S8)yJZFrzQ)BjPY-N z7;s$C(;V@wY^4bWeXt~#m8{Xc=?>C)#RjCb^CK5Wp*>;2wH*G1a z<_iRLMrDwYFaeeb`w#jG;i5#%mJOCJ>TYDZ#c7P+}12zvuPJr;3_KBQDg9zv}LS`a1c?4fLwe1bDAp_ik9$C11ka z;e(y6jSMhLoO~yUo|`(#veno}<-7z1m8Y6!nw0MhBrQA;cLBu`^Ot+uu|Vc{hf?pyvpoh6djwZ9Wxx%>W)=HH4=BK~8b9{p}Z1i2Fs^OYsSSTfP5aXN4}jz zAdrANZ;eb2%nf6b)MD~Q-uIOB6$HVVo(1+&BvjpZ^o#j%DU+Of$Z!_fl zC5D1GZG=G_XPd@ChH0`C1RrASZVe!&oiTNZC|JOb2RF*;sr|fo1Am&Dm;!Z zn{vUvR1p8HWNe|Di9aD=*_NAdX2yF>a`Ul_LtBOIfu%s z#nR#5a-8k!x+M3R@U||6nWoz1Rd&t!3a;}DiI*y#dgKhL`l)(V+#kT8MVa~4`GY1` zVJ+psOpO<&JT`3$P#CDO5-Q&%0#Nw;th!DYdeM|)5BI2Jw@M}e&FW=caK23Z1%OOn z9Y|OA9RLaMn$@J*05&@E?_ZeSyR5A#mw*MBJpJCo0<=gA1L~8}Wx`akrSN-|);7KA-@rHeqt=)`oS5H@=uf2{?9?0Yg z7h>bj*cEM2$R)u1jSMZ5<`|Y|@m=2(jTI@oJnRrIYX2Wx} zTe44i^3j~qIy+2`_x&5tf2@qdw-?*#33F-S`9^r55`9nZeR|iYW>YL9M~F>#VlW3r%cgx@7f6O{e_D`**0XiwcF$CZvzxNX2n&fHA5ifS?PpyR+Y!CP&C%km9pB4P+jxnpKi)=q>h9;PH7@kB`syhq+_b!E6HYHVEi^0ciy|LcDo|Zpi z9kKl}?m-<93Sm&D8@-E>bNN*u(`Og1Q;&Q{d*OV0&8o#D`N^eej#narg=z%k;I}PT zi6ja<)^RgVQWI&o5#b0n0alkjBQtBtm=h~A39*nxlbMnc#fzEa#54t}#>JxiI8?eK z7m(hFfw=XbUiYm8e@Y}Z`9)od0}VI91L3jm`qHsuK$3HC`b(`frlc=frCFHmI#a`I z?`)rz0yi}|aZ+CSl>zl3P2P5oOW}gdU~MroOX`yyK6KoaZfZG+OaPU70hD@Ydt|b% zYuGr9b+6nJMI6M?qf){KE-X)mS)&pNi%6wFTnZi&K{9KbBnj4A{8+;R#!qH2dQ13;LH4#p$k4*+DZlK z1Jgr%K2Be?jiwf7ReGqxF2^1}e1T@;s+6w#48k`_!D`W&-IPol4!1ABat4p=)X&ezG*syYV#h;Yf5J>toCrSuh-b~_FddDUQGb1QFq5Z8|%&~5w zZjmnh?b61nfqw?48qnM^u=ieAdE_uMjE@ZzYsi?bMf;@|EyNz$CRvZ>FH9_ultQzM zI8b$gwB@Q$L5vV!ib|dCq-|%tg9Foz>k4xFx;q)2`7%8ZErRGNGjmSk*Z}?PvDcPW z>vGG-T>EO!7_@n{r&eO~N~lii+ERrxgNHKX)^A)DQK<84=6Na%U2WS2OGaIFz@p(F z(sh@3?B}_`hIjEM@q84j{kPjI5b+USz7VUj?u?&6bJ0P!>*v>b$Z5*Z0W7n|Ee+>3A~nWtP!Bf1jeg-eS@sTo34Fm!*)C#&Y{_ds*2 z5~?A8JUD7#bx<%8(9P&sA5YAR0*r9eWnhkaBCh?lvO#A0MSW7(znCtOP0Av)S=B{( z&N(Rv>IjYO6e2YEKIXaIstP8OS2;1C&boDqisen4S48tTovFH!Y`m$F2dII}ZxKVT zF4-%;IGr?B#TRRDTlO086_>!!;Wpi>c1ZseK=$iyo8Z0qPe77nz#CiPHDxP=p5BOJ z*?-$*2l+ee-n6%;iXT|e6_%J2x+LL6Qjp@|NO3DKU?T$*2g-17S*=iP@~_23MK$!@ zh5+9*db}hzZME)r$OI2yndLAjVnBNkGkO*{=Xg3oh%#?Yh&488g{)qUCu$dQ>eszK z^~aJ(WHtrTMMOB6^T<=vC=k*T@`(Xq(5waS2u?YKS#h8LZqc9>mYfF?v3vF=bx;3P z30Yh%&|;!8;pVsa@nXr_Ra;~ZVQUv?FzqaWlON*pgD;>qfqunoFpC*9p%$Mjm^b>T z3!Qbw7impYYlm6IkF*FE#EIP)?JmbigslX?#Yj#euoCEnHiYYfF)TzW8r%3BRk1o? zvJTy4>;DLN-no9`gLl#th*p|5PRLqBkjY)$4RGs0&I8qf9-A(+`Yc38QCLmcxS#A6 z7JQFe7u-t0%;+9iCX*4Dv?DHb?C~5mfnfz?TF-xo9*e(NYE6a9lbLHAfqBV%^9{#H zPL@DpP&tIga+nYp|mUhhW+4FljY{X2Qj2 zhCQ5dh%=BwN;^Y96^+E9k_ISkiau&V#}GkX6GHgTq8i#$hm1a^4Gsi`V)b<&a1m`L zNFE^iLfu0!*#HK27F!R{nU~psr+xc?8XVjJ+kHA9pD{n(4$f$AxcdJ_mjhCNfKjBG z;#0{=*M-(vhB`jL<2Zbvr|#Rl!%(3?55g)b%e7m?N}uewg?;zqy8qYOA!dB` zPN&d44@0)JFh<33oMtpERwb|zz_bL(3+xk<^d+HYs-d&`|1f>yBbWDWI({%RdW{5u z8OZ3T^+cb6Vj77t&%V|i1S^vJD^TC+0 zn%qfC!ssgCmYNn*xGdEjp<`8rNsQ629e!n`)Cl~!%97a7=V*GqEe)x;RO&3{$^c{h zjTNv$mhTyW-IUPZKNDzq}7$=O$~i3@?;QBS$Rt zQ1=hFl~(D{i|kz%MzT04JUrfh|4&OVm#y#f!E5SrV|-kAdBNsnSEvkF$bw{&1dH-; zgdPUL9pD8hVL3};3rm$19#yRfZ){Ezo=&nLNh8RN0{Mqo^sN*=+q@}lCfv?B?`r%+ z-rO(OX2QYn6?J&7bObxMX?P~XWVWg?f6&tOF5kChEF##~zF=USdD&@=WaZr2dUv49 zqXo9c0veAUdMs-Nfn*bFjZuMZ;*y|B7-s*>3f8AC3vB`Lhi+dnCKd*L)jaxXHTjP{)9#E}A5ogcWa<~7zqm|-Wv z!TAkYBH;mnIaHi^2DGF88ghWqGA~_N(z&0WOjKx-Nm0Xlu< zx?LFlu+natts(6p9zbZzU<42T9Ylw0C5vdbaAKkDpqJCgV?7XTtgEfmti%5lJ^3XM zx=uAa!Mz|0iF9W-^y)-)_B#lt`i%80oe)tu=uJ34yVQVFaa0*1=ScIep!TP7Im}1+ zHBJ6V%fqF$4-)-YfOQJ@rn1hnVF4$8=1QjZa*$_Reju55xxmmyNza5+oh|xcK~v`R z;Yzta^Nq0p{{;LXt&yPvAB(+BCcp3#zIaR1+ueP4=3p51pT+ayOa$ualii)B;)i8cw(NL+TFzYN2A* zg?cwkI4X!{SC;NL;&?`L?M=Mn8#;#1)pS!nG7tO%mcr#kJZ4v}^mcSq&O8NP)rVGC zTslKu8jlte7#lf6;+E2!^PK4+IQ?(P3U;xrS>#NPzV?avR=g_H9CM@mk?uXCQdE3g z8d8V5luZg>Sn2Maj-v$`me~4HITod5#!D6xfs}_=-51qA6V7&`?oiv_|KQ=bn)`b_ z1X@#57oFljABHcEvlXGYwf%JI*xi)h3e)6b0Oindc;9Ehp_5-27E;vdOW-8)ZI0G|m$2spqInV@4C_R{`Stl>?=0Xcle13yz}M-%#j zipQC(TxFyRVIYs>n$s|Zxd}gjLl|c$rFR|662JuXP+5XPdt{*sXla#`8}3K@uX-6q zXq6Cc_!pRPL0FLE8SCc$^6|_Nq8&+d$~3zOKur4j;CFgkH_9^^t#_~{{G+gxpYzZ-)y+8g5)(ZTWH zG}<=&um3hN2aH{$sR11VmJ&)lG2IV#tX5dueDkA42x5Pj!Eh@c@D@-AJHWw9T_ILMK9LER2rXDQ+556s(TD%JwbfcqZ`LO40h zn6}6S9pJW*%8>(?q|+8d!{^oOU`LSTn;xUEcZ>pw=~au8N?sqJm-N%d59w~$8B;z= zZf$IUMR=l4?)##j-2nSvW?$bT1>dRXjSaPp_5_I!R&A8qbSjQxBx`+~&8ZcvG9zNR zX$xNs>s$>Ru#cerHL_*G+hR!fAiW$+76h0sBsUc9l^Co|{#VoEgFw}l3pgc5OLeFq zyzof0e7f|b;((bVei z=*FoUcbi$wmBt`xdLZ|YL15(@apu&fpV=big4yF59GD%wj(bnSjB?M_P{#xhPhDCK zBi1$+HP@OVY3P{kfB3h+in9(YPPjaa>ws2Mt7*^koNYRIM<$~U%4JU!(yjgOTAqy) zxps(T{XWm$3`s~hw6bfSmJ$xS8PI>uGBD<1(q@Zi5MW?b)gMu9E>fV)_J{sG(1SRq z3zjjFTFv4Z-U}7+V9Q&&GwI0ViL5Y%tRSi0wv0R=Z)jhp3XKDz2oLZU$U(!Y2u?qT zA$HaYo2gz`uAai&2)4woh*H!8mT{SyqNX?uM2%J<1Pz&0T1=UCR*ko0Eh-4x3Xn+3 znWP3>F2e$Y(o9s5ERuPEXomMbO5pRcu1t=>_K6P%w{n#snfuGE=LA48Mw-c z-rpP#yo@Ky#9R|i3q*$NTu_J{x<7MKGTR)WF%A`Ebv`472BDjc;0}ciNn!&AM*@Xi z&&+Jy=f|!Ln-n9|IKHgJePf&#-TrCxHy4gi@E%a@JazgizhZo-~$Yy@i2UT(t9_`xgV2S-F_#Ga7bY^f`+ne|>1WbE%-sx3bO zi0s`t<5Lp87mGbncr9Y?D7$R7VsQTD=qyfSi-9Tmd7Gn7VSkU$5Y>|Q$^NIbL5X_5 zYgB~^FwLw~jCxjxQU~sUF|`#RC@0Xv zPQ?h%&&-iU?nj;5D>6oa1rY~#0pxax07)ueH{=8*aaK%m&cR_OQ^7foELElKU^MLs z)=6tpqoYsRQ;on3xgF-V>>H^U4fJLf9EJu*#}=yGBM4M^DD4}bhmP=An_Kt_tj9mN zpkG;9WIt*a&cys3`r;0e2uW8$3|*AZvb1y=`)Rz=oV@Fr|3g^*qDLz}L9H|MujU5kxX2Yp~_Da@Mpi&ECOL zGXJa7_#Xsaw-dN#-?QC|NcJkCTknOjG@dE0>hZ~tPl;aEUXTzaT6-fUR2!DS*wSE1 zQ%*w7hdUJ}vSyV8 za9V^`Zj{VgtjXOZ{)XpHZBr$;upnUu&LZO6)KHk_GdP0R&@!qe;AJn)*Kk5Bc^AQ6M6 z1)aLZ_0(D&pbJyf?C=09MgK-FkN%}=0;SfdbgczM zAoB_GA>@!K7#Vd$2oilf!XS5)&jw?(lL9SAtqcob^3c_KK;XL0gJ!#n~w{hmMG7lr2(Xqaiz2Kb;W>{ z;)4t==I;$qO~FZqrV`R6&ZvhZR{Qk)E9j>*+Qju~fHZ1gb+L)$+PonBPB6IozN1hLb(kp{gw?V@qLf=*8?h98o)b6LBi-S$FTYN z)5*)350_xuU;NT#ZeWIXij)YlzEHmUk&@$-a#zj163*4kq$rak^UFlA;Wn$=M9Kj&c3DN%=%@RGHk|L|(!(7yxVGSfUG%^*8;7ROil(!51hj$2uUNBmIq;L1yK4e(yl(* zePsifZc2pG`;ow-87GDA@}iw%E?fuC-16BLPg;(+c#Des+)Be8wpb^^*Q$*+e_A;! zTb3ESrbs~4hdy(VHWro4ig(--{z zNV^PRjq(~T^Wt3TKhm$CfM>EMZS%AmbIF8UC?Ttf-5ct9LP$I>Sntas)U%W;bQ{67 zJw@P{n*Ky_lfPM;fre*jBu6CadzMAm2EqGSHuyy7F;r*M5u&O`S6y8PPPz+mFCc!! zgewHm&x?Odr;$t$c1(a1h>tQ-tNyO^bfD8w3!bhblX@9Fo%g+;;u2Uq zrA-XnqH++*>Lg4a(r$=rgyYCn6#Gp{dg@_Z-BMU|ju!r^XMRp_y=n^20t@`xD+Ez1 z&;MJU1X;!U=aV6g!OwVgS=v+^61DL)$VM_o^TPi9CvPqo4Yn*ll~;*kTVbD5TJxmL zV1W1lKf^>cqp_gDB>6I_t|?4F6}zg>lKS@K5Ulf@h%yWUzY|@6nH1$CM zUgZr)?EhlQJ2$TFxv$6SXAetweq+kQKDSBoK|1X9DYja1g#X82hz*Shb9)t*_#APG z&7nZx;F$NVFVg=eGx9*Wn&uMS_7oXJVBAc8HZ^y*wyAE7=jqcasf7%SV=)=1v6q;- zU-*^#;*I~`#Y8=M1+)q8BFLj9v+H26~1akp|UI+m{q~`f0p)79y6=N+gJB>GuA8QjFqWE z)v1E-TM!aW9!PJHqIu>Y;~I@@Hg=ho`q@cdt?ZiSumpus7_7BipEF4Hvr3+yM_QZ8 z`WekW`6n}Vie0%^ZE&+iRkB$I$UJ_#U`oc!acpB}rO3>JmWhbu1k?e^66~5|O1ef2 zzK~v`2?PdIk|ZBsO=Uj)CIGc8tYteC6v=1*cjS`NWMs7-4aG_Jw*`h6$jn7*+b@(Y zL3(*497_zS7B&?s$|gNxwQs_}u`fU5#7IFzDZEdpwlKSfYy_S;5=jWE& z;1L@06}3Ss=@ajLSjS}{rhqHFmgE|rgs#&{xQ6rWO;v60JCL8Xom*aUbJ94?&5a36 zYlGOls$^>|C-}Ta+K91VQGLU^m&`0NHac$pQ5G4%fY|qoh0lnO#Lzq<{%|Q&^|qR{ zf^zpQiub`98$VId8;26xhB(6@$E(2>v?@2AX4G!el#RUO{QW#8aam9@Q6^|r5C%;A zag3WfzRwiEMQpb+gdD6iTCft#Cw%9N#;-y->*yZoOgQjzBlQ++jt^GhUsVV~CeV}6 zq}^;onMk_LIB@NqAw{MtDcJ*&Mx^6P8^jThB|KN;NhJ|c-$h2ag&s~={aY;*2ex>9w|j2JW!+Yv(_JMDFDdmX$8@>VBdLg@ zHpxj5TS6(a0x_Wq%cQOl+_j9H-~+sxCnwFw-e_GAM~3Wx2_{Y#93LuO%=^AS&~XSA z>1-o04dT+pL4T`uSc^wY@b+gBmJ`uz9k%Ig+4y8G^0bbJjnM^L+!GU{`23>(K(amInAF7OayceB`Gvgc=O-&#=V!FMhmbE z$rToW+z}ldoIB&)i50wpJnarop7$d^0FMYoAHOEt*}tgZ2uIL@Ogq|mAFFHt_*?Sw z>=DpttFRcN{cr;o^hT4PKufd%AK8GaqBogGH{Rbq`pJAe`EQvtMi!T!#qj~l|GL-z=}e9S{et(E$ln2949A< zG{^Tlw-vA&o9~5V(0G7A%7lFhyFfy8Qwq%I*q_TAs|PgVRWElv{4V4y>_`; z%TJ3(<-8_B%2Skgi3J^9JiIYK#xId7)x4&4dcyZ?)Ci#~{fMY7L%lB#7tWScBF=E! zn=D_|Oa3^2Tb}(dZR;7U39HgmveCt4oo{{qEKkfQ;10AMlt zz2^G^H-JH+8<0}77?=bYbayoNZ#E$RjS~I_jyeMr!vXIS7T`=wO8?0@9qY*KG5l88 zx(}syxgn=VX*Df(ZqH5niABe)WaOQEN&bcw&W1W)biT0Hxj**T9X%Gm5Z=J6+NXPDCimuRxBvu#_A=CPx%}W#4Gq|9OCZ|SdxNVNvn)4Sev}z=pi6@L9@|eFKdJ4 zoIsoz*PxKjalY)@iv7X`{67TItIbp0vSp^BqTgb_twH-OTwX+7;8b#%+Nir%Kny0N zL!Lpg?eF|VKuYhPF+1xpgwvlKcos}c!wyXdd_%B3rsEa|36=%XW`jfn*RlLI1$L*1 zLW%L?@&6_00{M{?ng1xuU?5s|tPPgK$ItlNZOmd}DNv*Xwh6B{+a>eYGn_~dsRtS+ z%Bux%;pp^hV|QM6gaNRr58c;2I0dSYt~8ae!?jSmFtJ~V2I34kq#Xn-R-EhR$VDy( z4S_!KD|r8GhT#sYEdx){>?N>Y+ziBzhpt*wWJ4vJct<-Vr#2y)VIHOnSCE+>P?f8A z{r)ifn)*|XCd;N=l51`xQ6pwU2$%i{yG?f#`as*Io*z;?(_p$e&^vxO`K0mzsX9GI zV&2c@FzOlKvhMB*NYCog=q6CkIe*5(=5C*Qu>XJ63eJ*b9e^1320N8y13J?CLcjn) zjR8b14J0gr8cf>&)MGZ_%FUtse;0=V0q+5iLKaXFJ2renhqhG#mJid(_icJlYX|~q zEatdAy#26XZMMdJPTESv;BobU#Lf!%&yo`6I8qy*ArL3Z6mY64-#I#~UOGo`pG0`W zHq&xGY+x?e>nNC;t6)oZ!YzT@uruSw5pbH?{fQ3U_ zoD})L!rskAYTtvPt>aHepgd4W(XaIokiwIBH%;TV&|5R({1gd%O-znTJlTgJ5| zm-4V+?q>y!G6drO0f+R7Lw_{)_rI&;&C&ETrTFFI0N5Q6EMS(e<)WDi2rb}bJyHoC zobVeYx*dL8CwFUKB!TF9iBoz9w0w|yzjy~nGzUUy7~h}8CZS!a0pdNluVg-!ZC6HG zH#dO9q}aa}>2uPzRL5pU`94tRt_ld6-tsRQ~#<#i}PO}usZ*arc< zQM3VhC4`@%lK^)>h`))0EJlA0wsE5yraU87^?yV9hWBu602%?AeVmTPz#W(J^!*(Q|Um35YZMek|BgHjrXXuJTC zJk*EPF_3Igtv{VEAH-(KTj%mYFZMl2e%{j4H*GUWX$ET26=m%r$i~U*mvQHAAulF% zQP{+4Mix-EtYR}U2DAO-$$}lW;6nq!QZ*E{Iy~i>M2KR{-l7d>PkpM%yyR)#4JJ$o z0NOwTf9ZA9gxT!+aasku|Uq69vvwZ&`^=Z zXQTHLP?CcQG;v9Od2~z+FQ9F3O(gUIY@k zx<&PX2+5N2mLWRAz9y)^=v$$03KE6Ji^|n`mlpOWzIr2I)V~<`@U&_2&uF^Fp6?Vx zAEqDtapH@|DQE)F4w8kFSiHc)e{U$TS9M`p6N-dWVAAtjMV;J-J6Kk4N>yknGxRwI z`}&OQWIz;YdqkgOvG8S1;e?R$RbYkCR3&MP4CPHSrawk!z(<1n#Lm6Rf|0@{k~#cj z%V6Y!*)j^FI16O+lY6mT@z}346KlWxhjYgrsjcet_-t`w9FT=)&;byZk^m578~jZi z%#*uy0^&z@y-39Gzib&7!z*nT{t}@B;-4GuiD7v@Jq&K;d4Ey;%<$=8=dLWDupiTU z2~Pm!sQ-aQ>iC9V!*DtIKdaSz2h`laeaiD%eNMJ8z$W(5q76Y8xxcmn_q+f?x;lUy z2lQ`&F6Q?Z3uxdu|A{1_k(KP^J`d2~#xcpKvpRqV29aXq-7kp*i2y2oh#gCCR@w%E zHbIW$>a-WkkyyD{cw9!mMi)xlDAGf@?10qZK&TkAEi~T-6DBMH92aUt)UqsEzJI7X z({dG62v5oEnQ)7SQwnvXv>IN&nNj$kV&d3Tq(38&^D4>D3gd>FGga&M#+mrDx{g>kWq3 z21BiaAy&aq3&Rk8tZ-@s^NU$s>6z)cd;zvMu6X*>17Z}r;3dRy;VWr|Fo#e_lGP~o z)6UPnjm*_AeRJdw2))y=k{+#!vYPBXe-vLDS2W{fqT=VK$Lj)3@l&P|!4H@&cpo$H_3?uDJ@`syikW=O2;ysF={>e3g~| zsFYP#%4Rj`N=BBdhXQhmaHfM4S%y~9SMW0v7A@9ABtD9|xT*8M8t{vzH5RT1F z0h=3~8kzdjHKxYK1Z%y6yh{xfHoDpUO6<){imSDjHQbmN~Cn6{X&4m};PEp!RfIq3MPa8>%cRLk@gB z=3`@yUk5T`fsEQHvgXg%cW&&?t+incKX$0|^&FV8+*U4k;%4F&#-j3m5%^(HHsL*5 z+p(NavF%P4SxUMr47J_8m#}E>9hDcQPAD#`*yLs5ZvXOe z@0)=1@&Byh-a|n0I36=0)sRU@HM~|tL5^)m2NYn2kUaj8I+_`z&{4+ar^}BDA~(nl z3ck2cfK;HV2MK<6a&T%`X#Px00OvFey1=|N!O!0U?-Rg+6D3qR(X(saY^%j@t;|;Q zE3@UIKeOecZ-Rni^L9%m5^rvKX$Bay#5`P}6z@yoBn9W;vm;IjP)rcIMs<|x{z44& zvNt{KO;3xJ>1l(#Y}2b*W$A6D0Ct{-T{K_*ohB!-y~i0u`SnM1SEBs-Got(kkQ%3= zLb|L4v|hWB!EE?-A~2$`BZHgRZ7lYU5b!0Sin24CD)OGp3NA8I$t85YkI>o<-~I)L zUu5mZPj^TD-=Ep?_f1exY~HSJd0W1ocjdNn)Em^A%OQ&`Wy7x%)zmr^nS8Z<*{XJJ z2K;)E%hz$N1L$lsM3cIsiG1~vd~nGK5SpBz%K0uTp>$bQ%qUwH17lQ*S%fN|y!XK? z8jf>TVH5_0Lie||=?gDA^EFciz$4P~^_&1-H_!!7d1!qHh}{8d zXT)a4URo?eEU7!1h|br2E_^aj$b>0n^cGD8e%-zLKGyg0_1*RNvA^Hfl~$VmTiO)3 z%lf{2Rg@i(`|CYmJwPQv6<<3GuJ{!tcdb=|sj;)yTqO#(z*>j0=Dcz`)Que%J2Gy_ zbW8x%`VULihQp>K)&bKAvx%v78ey#`-E|XW@}==N?D@?aj7qIv&+iz7Z01i<;4sLD z$++~B0bT)zzn$4=l^1(n=6S9F1OjbpYz!Qrxj9hdx*I%`;0_zyY3(ke>u#Va3Hl z08R+Y4am(7qE44`IbYBiw?!7C;E37|=SbHG=^|e|fAKS>q@n|5J{k5$CR}|o?9Y5M z`~rfvt97*0i3E^!%ffO#CF322=JLy2bXX{>))qYdp|cBU>;hV=qYI(~%=WnGk#R#d z6dR7cTz$GAKw48RsutDOZGpHl1the*w5lvATLd9=w{1Kn>at5$A*)qaPiL0%D!@CH zRTR`I&5%$1-6tAVM)PHDr`Ui6RDkaQ2B9@x1tWJqo64=rSmf9&i;ojRr%`&AybTq zC=Aqyf$!NdFR`A7{?Y8xaNnb-WB1p6N{g16cHnvUX#6&i231i8axt3>o9Gpw93%I; z`?sk=+x8vC#+ZQugjb=2KA={?jS=<) zM=Wl~8(U;5?ePgjTo%HS$$&LC7QfRD=$PSn66n%PS<@$WYzBpm<6rFgMJ%YxNCL5< z>F8x0&#IgIsmN1$=;NdR_r#%+aJYv+gJ9RUbn%;TKYFZ?{XB4TPhQv#90Cp;E($!@ z;5dA*494}Vp{SOO2Xru`MxT%)Mg$iaGNH;~Ejweho>Sd6HjhiJ81R80k|Asw-$do8 zT$D@Qm3pS~$ysmMJJR5WzG>)7$lFUU#ji{eI`mLo`Qj(ffkWUtu%bs{%#z7Sf z8Pg6647mYDlPN2`2cu8@BaAIOOF@r&`w{UN22~M%?6?KdXc!H;g@vLnHf2PJH!IbJ zSgPojDot`N_@zk7p2=U9*wLcD34zce#z{uF&B=ry`+lXId$f`uGQ{z7f`0b(7(OI4 ze=%C)QWYlmI6$iha=g(pI#PtW9p?%2zSDv|Ca>12S`5+H-70soQN<78$le-?cS_5_ zuY%5}g2TK4k;(FylkD(1(S4-_BXh`HzT%M5tUwbai1aoSLpFwO3tK|uBhj=y?6`z; zqI#C4w~_WhWCy8{G{iYq%TXh%2EOhx^u4Ci(|kLAALLekxED);DS!@nrIr3HX4U)U z8|0ZPR3d3_iRd5v+u=wNqu*j(?)l2iDZwY_kFGcg&UwKa)-Q7yh9Q46+xdp`69BTp`MxYakC9vWPh}q3G9cA<=kdNcycnF9$U%P;keNkAp3rm zExDhC^v8X$ambaHpRGd@Mv)&tOhkx&52gmYN7>?PclB)6nhY|lpZLDyy5dulf+MZ< zC_H_6=>NtNhDgv66B!|xMCb(oqR33*4@r|TV#stRhib?|355G=W`5UcB93;T|Bp&@ z1xyf-U}@r-A#n5-4~C5>ZeAqYd+W)J9CB%ORA&IH5ph4{YmX@{>p5Km%NuyF0*thU z$?kFJeJG@kDvqlSOw28)1|zy#qquG>iyiujwr!k5+MqjCnguOj^>k{15-n{}yFwI2 zVX;+U#luOkaJYenpICy|i&l-DkSMC2ho6ft93Z;$iUKz)>>qWzObVopA@D4{)RJn7IxB=K4JE!L^AvQ` zkiWKJI(ajIjb_2nE|c8XaN&m*3zaO8(X32lV4@V8d4&3B>F;Q2*_vGJQUN^xs%YkX z{CnH>hFvKUOqGp(`dghOpnsVUf4Za^FUjXLpPtxPZmZ2mlcb=;Sl8RfeydkdFoF@b zMU3i(FBP=Ky331JK%hZ@2-g+~(RC$IA}9-@A2~~1yo=eB2huBcqK}9o+fEBdPC#t7 z9L=SO$S=qWOQH?2fs2$=tdynQ@Kadl8nBo@^IVT5qViJ37Knzn4!~2qKb74ytO#jL zS&xePP)@}sf)m(nMp@zZI|FKph@?TEmQ!4XOX_xO1_JTrWdU0!^KdSha=m7Nb5{r# z4Cs}c^}1@)hSOD&_#MujZSne;15FllkT~!N) z{$N^gJ`EjoAO}oI4g8w4B21G=JAi4_vuly4iTwY1M1vKTtvgE$oc>;a8Le5FMmy3t zGJ(6Kfd=2#CcL5D1V2oyd`E_lT<=_wlZUn8O-=o*1EV+_N3 zMebz3@pR=wCOFDcrABe3sIXza86(iz)5>7rXUi)yA3@9S-Cq!;{h%cUC10||;akTO z)a5M(_=bS{--BDRGIY#-Mv%*Z4sH5r_9ER7$9?&$IPe8heJK9{%Wi8*jG@9#9y?&M z6Oy5*O;$r5YS>nYAz)B)yUt|GEJjhS%g}2&S|K76py}YW(kD)j`H`OmH{{gDZaIAz ze{93o7#ZvtS*NaZ&WRikZ>8xsk?O$r2)!wGtFEG!=1Aq;-_cU{DS91%D)0NwY8kTw4j|r|0|4to0BBCk zkyYbb$~`O>0ZN(397pi1wr$M7Gw@0fFB69ep2Iws%(m0b8HWA$THM_(iBO{yx)?k` z^~Pr?s$NhGk9m2qM=|j2e>c@urg&1QWdhuTO%+{X0kQ$S0k;7zYubz=)Hh~CCeE~0 zlg_^K+?cQw)zyTP&PgA~3SP*6m~=v)bY?aTGaJT{$i$h}$|d(sA`~K~U0% zH-1KS^;Px|gp~>e??c&lz&~$K&9M}VrSD(2YM|~*#mL445Cq}orv{pO^HXypon&Pa1cGoQGrR%_%9@!bi_V_f-5)2lbg^vFX+ta) z7bq%n{*v5XcQ-UOyr@Vyzwto_D?eLN8_KkCl3fIR&>T_2qX8Ru%uf{9-PHz#jHa_|JM6b^3cCN<92U49esZeC7rl8 zk(NAG%sNb7W*gBsi?Z+BbiE!ec_g3d(`vQ)A>;ck;gIWPW@a*vXpHT1X!_xKBAoGm zSSFNqccT~x-=m)M&<7o?z=)7y1-5Q9(Hf*iQP9r=>F3NAvrB2j+!LL^h>&81b3f7U z#0jWBW}e2n@%i~Nf&)Ye5gtmOlGw@%#-f zc_0c}sp&TQnzhln7bTB#4HQTkDDbYfkFTtX5&EGePZTV>=}Y;(t%iEeHwYu`7^Wf4Dozs zS@q++3yBU1jcr9t6Dy)MIyOW=`jmc@7|P^hNYXR&1>1PD?1HMs+rQ zYo5^g&#{`TDOIYiAe)Fye74;RA|J(_AT_Gfl2_H7`m;@A-B=}c=<$r!wdf}42qV|} z4SjQU=O%inag_0Djde31`Ip8xW|S3k=XER^E1J<-Y=8cf2Vc7l4WkdJaR>^#XHnx#?CT5> z=S@12ucd~Yzcu6Qe3Ga935KKAe?gvB)DiZQo z?hNfjNU>t_(2)TGM$huRL&3KDuv@|Ijh)Q>iG0sH-ie_8*LR~ZYP`kCCh{^wuL0$F z95_f!9>waL*f-hU#KY?j4vh~`Q*WZZ3yF@FGD1y$YqOKa5HEY{Fa49xGLe4Yt)pSI zsWzxR5+q2F(BaXTNdV?r0K>p#fr|o`0GI?W2wV;@pt=}2@zl@U?@b8 zCjfwv@BjoEf{on3MqXe;fdHxlJg@)+3=EkA9AKzeSYTjaTwt(;wkCS&o^)$);LzC6 zp(W4rz$ly0)bt<5`YL5?hv6wN9PdR%3dft@eY|Zne)gWVZQq99@ODprEWXzgj8?0N zw{z>ptWtSc?)E4t{8#oCx!C(?3Qg;@{^IQ%!!gSkH~BU{+qnp)Y6c?TYN?uKEIPGH zRhy>JAr!Tn^|dm0y~o>Ja;RX1n6opX-iDc@`6#o624xY6#c9_Lwa_iXl8hPU$({#w1f9Ry#i*Q?&n z9eV4r=te8P?=g9+Za^*RhI(oYgb*XM`BwCB)N0)QOrymdW+VPg(#>udRj1r)CSutG z9nseUjC#^`gXU+MjI8QxRr-1U#YN?!a$)fhhhPkrio*nn*r+sihh>v8MKb~bfB>Tq zU_dZ17Ltbp@?anmhJ#e^0Th4&U}jQ?R#G;V36mttVH~9#B8C_MfB^$UfM$vS9T9DF zH}@E#L^u_GgtqcwJk*@ZO$^ESwcXi~ACv+3r1X`jt;qiY;uKNE|ET?6)z7}Rj48nV z@kr1YZNKlE_U1>p?)?Z9X8SnyN2u=h+i3#_Y{!)x_)Whb@WWq>lHD6YiiO@bpa}0g z;Kd{`;H$p{fAEkwkpW~dnU7uqIl&nWV8A6~*(G;+S*ucXK%jnsP*kt{JVk#VPzmo4-jZE*OMr(s& zVFz)CpCb=QU&Tg;*&SKP?_-8Rv+ zB=BI299cixOj&(n23l&>Bz#n@wkpGNV%a1iFu;8gC92isALDw9Sn5GI82x>Ekm?T_ zPR~3$4m3T_?kT=^a`(7Dc;^5ebJ2~1hVN_yRQ_x?VTWyI99%KbfHf$einP|pVirag z89rz)QPP0a}XDB!9_N^FT&Se`yFFqo*MVur_MUeQ+7`W2e~*RW>}ds)1?4I7&4FS z9-k$2VC*fNIq*y@l?e)!a0V!#1B60D{0RtK9K`|4-jyHdw??;9?st#MLiG^2Ad!OL zRXOF)UM_4X8}dn&vVcEmP`d36qhic`b9voFmrOQmO2ax>U4%akR^tLw^b>O?ebKJ- zK5qK=r6q(E!-jZnbn3aucC+!f=7wAKmWLSDA@P~d@yK;h0PT&tNl5lR${-|*$$4*ls!~vc2SxFlSG%FMr~_8XFw4)9g=>^K}LN?d0%~V{SaS@PC<%o z`1t?ut~0pGx^|+@O0N}I!r~d{{4uaxRsfgUpF0o0jGCy99AWU{q9XFL&Nsd~2h&pB z1F!&Kj9Btdi)aF1GggMOv7e;1ab}+I-Zg~TMxwL&OU0oCmv+{X?nPwt_8SU_6lWry z|CbEWI!C8}xqcLoaQP{gzGB-wlqN*7V)7i;f{J>5C^ebf{2w#&Vy~bU2%?(Ie(Ul3 zlM|&*n+S>WZJmtKQW1(gA1RJXo{!*C5=Me(C6a5~M`JETw|zubaBX0Bkt1Vm>-s2v z6xj-n@epBZ9}R4wqH4b;!SsnT>s?4LF08{>M|EoKyuI%vCN-+LiBlN!eL)ZIj5MnN zIHtMhM0)rU!I){1Jn7zT8BOKaEAn<7dPm7^jJJ*6a!RGq%XqO)WRfLF#tzk`3~>v? zr7hsj&;K8)qEB{~2WwDZo3Dk*aDbebiQr4H1&SBv*%vnVG{{4F?^J-K2A9^d+do=n zUQ}mP$3x{Q5T8%sD*w)BSo-}r^FwemSqBZ{q$%oK1na2mH-*SX+d_@`Fer{~_5b}q zd0$~fxQ01iVoCbwb;GIfb)m{IDirD#jCpbfz-0Cl3L>~9oqKiwrcgH*L9k(5m9aP| z)s!kGSyH>ElQ?UtaOF)#NkdBYo|*-uAH3Lm6AG;N4|kyUHT<%y$>{K}9btrAfr+f= zL{eDT4AlL$6DjUwv(z>-EQiGQyH7*a@g=CX3HLYo#&w8W9h^9( zh)WKuXo>O8ylWXBr#L}kM75!7IbW^d$XH<#)xC2PRfZAGDaNuyMmTt34Cmzg^c|Nc z-T3&dX{e&A^DV6EI0P2VsiKBnhoycgpMWl5PJLABQhBtMzr zsS84H7cferNX0p}-J0X0^wV^h4i`eM>9G*12;T5``t8m37?=KvvjDiV6{)QGYvkR zE51oy43sn?Lm4hOsiI~n&$MRH&aKA7ki#+gRg1}#gv_`m7cgZ_hp3N^{28tT2oD)Y zO%e=;jEMQ0YY*3r66rxPs^@<{YfPETC60wVoMS{Fyk4$FgpP@Qqz(=Rw}_dhFHZ1Q_H&dM^#Ikda&fR$A!w6Ce5+A zm>%U(y?0!18;K8!Z0VN!RBgtJ=K0Yo6L0Z}gAVY)JKX(?f!R|TkMzf_=tz^Mb3(5K zA+{85Ni2$`z@gd^mm433{vk6}h%ZbykTCd(*sN59TZEs6RV6doXDuVV>u1pL#ieQw zBneXp`fvR)d@2lf2t06NeWrqbMbT?RE%GE3y>Et@WZ zrUt;Ak1n{yI#JNn1HuAPK1>-;(hs6X=0VOHXz3&e^UlwD{IWn2Fx6(?tPM#KbII1` z{y~OhO%1VigUV@3T~W4yzOGcpFH1&ML7S=vgP&#*!%ri#xt;}NFa8khVqZpUSfZ0S z8x&~+Mz9XA10@Cwj9s!C>J9;tK$Jx%G+w1~A-^Unq_t$Y#$!$~RFHwqp$7`$AfYP{ zxU|FW7ZV3@$Zs}O3OWZWi=Y)aaX{__$<(zXk=PRMcx+`Jowlo(HB|4Wg2+JRW#?ep zqPNjdd~`$g+JJC3A`{^QumUJN%8p<5GTqOD@<|_ErGmUjjLD|N$W_qTkWnud*nRf> zf$4i?gePb+lgS>m1sWe`;S;qz>-hqW#kdB~#Eh?1KwsOe>+KtEs2#shtr(kbB~(u0 zCmxvP@KdkL~3Q~?vt`b&S4H9nP`aCv?}p=Qapx<*GZoyQ?+{UWQNx$B>g>P_|vd?+_XBtT&N zB{xw?rfYSLBG%gWw?BeVgd!A)>3)9*a2U1K*$RUO=XDs0zN~&TB}{nLEfik`@48y&pNSZU3{1DlixAm(FNvs?{i2})Q z3^vz9U~zeb1eapng$IQElQj!Z%|nL+*D)n$ZsH+rE``6;li5K4d++1Pw)(#kq>7}~ z?%;LAlS==?2>~9@mAB!UJQdZGlV7W;G6)V_@(tcuWh5&3?z%}I8|^IZ&VYl7_2N^V z_W2fSef=X`z|_M`uJ&m#5dtPtDmxuyS%Y4M83wj(0EVj3vH?0@&%m!_4RHRl-}m`@ z!pFTrKqg2@x_sR2n2PRFUc z1|2@k%DG#x{hFBNd*@r|Vic%d?q7Aw*2-!Gm9pF@)CnBj%+j79uRwE+qGbR`+q4Ahgdm!aa)0fAxt+~ z9~ma?XPu+^IIHbZr|TsE&P5oVWacMODu9na>C`6|uw`~9*U*Bq=9PIxJeyRIj0DuU zML#KM4WvH+!PwOlPMtx51g{8GWInM1L}`F@GM8|$7yOKoOox|>0o?1r>=7O7Aaf)d zK=6~<>NLPVeWYc^oMdE!w4{quo(V|^N@S!U9E^;4mnW6NK6-3G-BiW&Uk~%ETfjdl z%JDiDsq8z^nukP5@Zb}<89_l2ko`w(B_+yaaGaYIiGwzLXOXt4?2SD|>%&t#6DSsy z7`K$>SrhavA3<8H%jhkY9c>MKiaq#ayQQui0jJN~ANsepo`Jpn$Ysff4Q8 z8F~Bbpjd3dQ83RB>@~??1m7S`8vWdrT7duAyO**5ASr$#`q$woz(bXe3(*l$j@;6X z`BqBv@-*?11&{3T}s&ygG{p-iB*=2 zvBciGm7Y$E6Gg8Ez&L5^m9(@7`A+jv!~({__U(3P2%DVN1pm;2mE|=9s>c`%T61JX zoNB4LmpdyIob{JryG~4o^e~kC;ll?$%adn!O{{1^?#-SV@)ka>FGK_<4cotZt>l%m zif*>Tm?n_v)%fVmcWb++OpdS3qF-CA3o4Yo#}3&Tob z7g{?_GJ&mC&bf*LWbayF1aCTy4Io~>)+P_vS(@Olv(nBV+{-|v`_1gRSKyam$SJiY zVO5ulI7WflB~;lPbKua}_z%OPa3{ojn?v|1!5W$ji^4mUb`>EHf)ZK5pv!%aY8y5x zDSObp{I#&4Ggj_9wtIkr%YDlHse`to>BdQ%riyB3)kI0+!&@&-=QHE5ju0gy zQMX?(E1)1|DR}^4+K1onch-&Vn0QVHh%P&mb}F0nG4J7)wd+&Kn!D@_SP`zpb&j`Q z+LD&35;~M2%I(uJsrahE12ft8A%z(N%Ru~3OO^`S18o2(Y`|Sq%i=l*ay5VoQ?Q?u z^DFQ>dEBKBtshiADy%%rXoj&j4@~`(RUyUQGp5nOd%j4>Harma9WJ$|zG1=5>FLw8M4RKDhs_TWZk$1V#b?sN1Lq{sAagp;!2xB-J`F&| z^LvLlxx~;^5=O1cfH$X24<F(+8`_mhJvEl(bvWv-7H;c4)^5cRq2r??BuSid`8YQph(>?Oq0~eFk9aK zAfpkRy02_iLM_#BGPs8hIlXM6MRP&oGzr=S+}Koz0Fal!l>$$J6v*h>*uMoXe0Ji4 zB4I`kJN5tOu%(T%=EnQcK5;p+zZhlv+u$Zx;x1Iiio&Ps*MbRb0jRRmYoW1ZYz}0x z8@^w4v2Z?HI_9@fM-u$|;JJ)|sKPf-k-(8YUrBxAfCq|#FdRT79a42@8pjJ6fB3p8 zO1K-KVlc~DXc4X)(8W(LO3L-V5pjcwBhU(lyjKXo!e5CfWrU47A#MX?JeIIxBcFW& zeJ}ZoorV7{c3)2|{=Dcj7!f!2pi#29kt*yON@v11Mo+&#X&Z>JQU7MRJdmTJuG%C5 zLZ9K{{O%>~g2Qd45g9yWZ3kfWqiTvc)a&b&I(h}nD`di=)yd=T0;C2aB}4Yb5E%`J zQlZ$gtIxyAmN#A-+GH~rIG>+XBbN?!DNRL7(V^M{UeZ7$;sWaiai34_=3u9QZztvn zJ!63$jW^Mz@lh;^kCHipuL0TL}|6zdV7h)9~0?unKtq}RBRkp1a$Cd~OT-ygCcue!SI6cYA2-t}fQEDCTJWl4=g$^j&*j20AOy1Rk;W{?M(1Z{7@L)5A_evdCP>#zu7GnR(eWbCd1 z8VEwI-RC0_4InYm<#@@QoT(aDN@P!DP1Yh(2fb}h)`XE6ZiE$O6myq^S_DS{?}B~s z(&}4=&agKsfzszujk)lWVsXr(!S$AoLt|(NMqQ;PCfkfL{fK9uZYm&1v%kf5G7jLt{ z;|{lb3mLbc9j%^22(|%K8-T4Q3XxBUm}2P0?G4&`54a{AgkJ&E8u|lx(B!~jz0Gz> zW{V>QHHVV9cTNU536P*bptP$eIuE#x$&-i|NmlN0dYMEN8dIGDAPEPdb6zvG7mBbt z8WAcNM{!DvdpNKk6GR5)3sQuJ{&Ow5z(%<7OFNpO+a2~;eH(F0^i(wB8CT#Ewm>JTj~%W!4{NX7Y1n~};UT*@ zD7&-wadlBMx6ZyXJo{ElUmXUl&F>;m$*}_#7fa1YaqcfhNvy;q)-iH9^kdzT9s`nA zHUJNGJK)R|>2U#($U7h?pWJFX>D9}8uDW)wS*GQ^0U$`NCNjE+zk?T zN`qpTvDhv3My&W;1nde#;R5{7J~&=TNokY}Q|NoybO8Wppik^pQp{c&zRE?>0_brT zs}YEzS@u!(Q6k`8^a$@ypDfY8rZ4Ori0M~wrJ{Ecb~r;AUgj2Kv5a3lu1R$11sJ26~JwZ{AvA}e2)f$n(Y{CX|J%R8ptaA%X zahQv&rL4z{pjLSJgu5^RY~788345;Zakxr3vVJM7n912Kw$U+w0Ctle)K*I7O*5o7 z#H6WfBgmRYt>pKIUkrq-7?!9M3#Rr@;FCdS4C`+|8m{7iD17~oRlsEkIY19|w+sB_ z0@~6pqg)^o8QKPc@H)RC3jlx+rE|o1mK^PGqe^E^PoMLZ2!0WRZP%wV4Nh5k)dYa5 zScm~K0w6aPBpD+NI_s~b8(4XCvRLd7n@m92v0aY~h+@uJ_`4+_@^_|`DFr18B?n3c+#(!~$pb9=pWgfbg{dM2i#&0!S?wvB1;K))){@Jz|o`w|p8B<=WC% z7nLz-F> zre@u9>JK`5fs&q*?%hPgWPD;G-XQ)ci$of+HwMG;Gc?45o)It?5WM+N6RhF1t(gAZ z8*XOp4Lv>sA8up^0LWVD{p>7O!JNg86pdii<82;Tx`QIsVUdbpsS?3*Wr770DZq4y z*)xhpFnWF7QeAZXlSoZgFlnLXX#^ywnG}`chYJfepud%=Pg7_|y#Np8<*;I|8hE_D z150&Kq$66SwJ8`;FIITb!VEo;0t^gqg8)lE;6-1wh_=v)ve=Op?Nk-*T)hCnk`xGE z^uvlYxKb5dxq4wi#%bgOh-nkEMS#j;Hmn%IMe~G?6C}8P>f-GdnRKZE!xVCyA-oMj zCjF2i4W?8DNqDbz9xyjNHz8e@+|c>YSUxCd#gco*V7WqQYnJ>GQNbm7F!4KhB%; zW}CXM8hn_(CWs3lqimvLlNKPMFb)_#ea~VEH_n-C>cf=vKv9ARXNhLa#acpg!98~B zcW=hJ>Qh&cth&Q=ycn4WN^AqwK2|^{&_W~f1eUD z$+|sHj0*bbFBTm>Rp99FNuz`pr?3h0QbSUa$-Q0TA`s?J(L~QLzlT-kaF;nPtGT(k z>0Ks+-x%!W_GUV72c1Paicq|*T=6yyCyA#b2?4WNq}ZsIXUrppf?-%Cm@F`x_@v=M z0|mpdR@|tOO-W7c?PJ-?6(5Dj8~z^hWByy|b!}H)O9Y+GddsP-bJkm4o%L2GdvDY- zyli8Zme(J^@?uU-gLS{N)ZFaW{m!*;F)SRUIh?tf5up15-S;c?MG|cwdDwOnDv3_O zq+*lpX5$*52Vl)#_94Cd5l^+4|5na>wJG>1 z3?0C5qoaluvu zuzC9UsEly7kIyQGRB(m2R2eR%X}Zjr9gm@|-Nv(=nlE$Z*=khRfAOr= z$@)s&kzvOKA&U6CyjC{LMb=j;4-7mp;7sMPL`|MB-V?kmXlW2yiRN&Akizv_p4Hx3 z7gYuZnHXB8aC*@hDle!IDUs@s5~ZE=3RGP?56{|h)_8Pf;ps}$-g~3=4_@ofqTy)| zrs~00C-5|c4$=CF)LFb%Hmk2tkwL`-B}IB(q9{)oGcqvI0yuL*z*7}4sE8qGN`y<) z=zNj#R$c$|toHX^)NUdOo{Eq`gb8kn_^?Eeob*r%cuD{m2oM8~sh!s~(tD%t=Oz-H8&*UDxEWqqaU$e?2a5Ji0A=!-5Y0tcQV z!GIJ42vZ|+UdSO5l(N}HgPbm48DJB*itrdIYQCSNR9zqDS*;G%Q9m$2^?`}202B$D zi55MNNbtbZ7!0u({uBrz74Xyv2B#R9rgVPM*As*mE8yuX!)t=J2oFm%04ZLb%{l>3 zhh!Ksfl!ejUlfP)gXZhPpv$k$X0d>$G+v-VgeY~xi!wQ1G+)=qiRSycTXlVwXSFw$ zrS_j!XR}Da(;gUlV#t}wd5OwALukIP5uoP#xvZM&Yp$=Hn(HgA`s@GKKd1is*QxTY zweS2^3ZKD_zre?5;CT#4)YP~DVKfh&l;~ve_rSQlZ4ief5P>U(E&sobuG@`OfaIL7 z0NKXwD|YYL?f%%Qu9yCHZ`d-2jg8RXF%2aXd&`#X{d>!Q!|pdeDvj*j-tJ9w{oUTL z*md8yL1cf-IX}YgWPAVk-~V>+8%s~@tuz*oPfGqq5|u_a{x@t1yIpsCyIyhZb}eCd z>&CTZZ0X*v``@tJjsNZ5OGfs7-R*r;F0r>T$w`>}u~1eB#T4uivO)# zD$a24p?8Y+`TNt*q-L-^T3i-mciafBapPrR%nI-OHrQy>rXnW*}YrFmbs;S+mY=U;PO4n zp>k9t6Ujq3bVn-W6f%k%yQ34b30WuzXrtr^NG>7&JmHRfWD;@@k22+GN7j)?BvFo# z@KFu<$Od@i9o5JkWMDlzqLF8086(S4j2z1_PA5k%vWwip0&KD!{p1zdN8@+YlXsth{mGOsi$nJXY^tkrfjNfBN$`t0!UOo5%p$}#9^_iHxu`E zV0uk6gg25;J~Sd~BnzdI@CXxkSOL?ki^C_MryBgmlrSZ&sRJuOVF2|qMoR`f1cNp= zAwt*$J=pN-mJW{0$_f?Jni&|i|Ma!pqTw&V;WfPQ8qjbl4UNlSnRR@*UIZu09-nwC zm8_i)!)&d58`oO-wh&JcGAiwVo<;lLZ_#)=2Aab;`y5A&`7YOr`L5r>@ivSo8Ix#< zrz-JRwa;PHl+W_4l+WsHy?hj|_3}|pwt}W^Kju>X*3a83u=M2_!-oo{3(GX;Z4pc& zsCfH7b)HHGf}Jvred2(E_Z^LElR#Z#pW)odFLJuOr{x)XFka5bzFZ|e*(JKXFUP@NNWg5+*k zKTP1uMgbifK^_|6rYWRDh=kTpNxbc2Zj85S%=}aZQ^#g2rRJy=5*t>Fw{z?{>gkz- zM-C|446DW4EI29_+!cx+D1wwI5wmMWLhz_U;JnQ`Rp?Mb0`f)5BDG{8-f`mXE)=sp z6v^RHZ>5cgL!s*`;#slsnHsSYbs-h}NnJ$ctiYK# zlz@Z^t5A>mZw+#J=FHW{Y&Q+UmR?Yagz-D&QteF!eIv!B=b&%&RA%Rj4~#LLZv zKv*~3+%etdAN;hZJI&H7&2?Pvwb1Ku&KbaOoI4-p)twK2fOB3VM}oUCZz3pw=p=EC zxVZKYC-o`vbCY^lv4f&w7JhD2Hkl0~Tswy+b&;~+QQ7dW2q1(2DkM`NdGVI2v+-X& z@miHAVsk~O%2Mv18PY|R)AOZrB7E(kzh@fomv_zA~CjH=r4~Vpo!Wn|& zf=!f*jmpJ#Wn#nfY36D!Xq&g%6vnl0a4%k)UWw{lKxtn43F`7vxuAIM6F;d55YYli zefUv*_+9a=z|%sIg_3uQo7HgQ+Ck3Pj+n_y0LUg{GN}R*77vLp4x-WD?R!5dlf@w> zV-cyGheo0j2eGZi{RQTL{*v>sBK0}JCkPD=&>$j|@;zCEcvzwOxkWjX+ki-YMEXF| zW6mN)?h%=xXH*324RaWYNlMA$3?mOqA=#b%k?UHS=cK}@6R{rn`)xyZrEN4CLYxNl zKURLtE|H0KtvhH}zRkv|G$zrNZ{yXKZ`-wSgj;@>39xHq$VcuqlEQh54Y{_8oTyL{ zqary|DTKrp@JX>maV8EA=-n=tA2vt}Xq9@fagmVw#5oV*x$~bpy~>`bdkA>i|Gdfq zrTv~X+DVBV3GOCill6S;TEUt(PS_3@l+2yZe84+ksF^$Ux%P`|llUfV2TdY%VHA~f zQJIHI=DA`5L7c0Kb5~42LWl~%g%Df-(oAiy5!$SXBpQvyW3s(-*eD{8{!T(#?^o=) zagU9JihTTg;hH&*=gxor)tG#D`7z(+RhDN=%hJ5Ie7}BlFrO zM?}0W$Z3h@-syQgZa?nM`I=QWH|_W*V-s+o()JLP8?@4U3zQPwYJuBK?^TlE$`ddu-&ohg`BH z5WIA8-U}8B63O!{w;yQVpK*pG=9=%%H#zIk1J@ucuR&PQW$D+;`A zG(I5?lDqMj7OZ)r@t4Yb?cj6fwPkixGDSOzpk}rUDDVVe5E2{KELH>Cgpt`TP$wQv zP&F9!3e{`Q;aGzOj#AWc~dYsWkNBosn zNBk8Rqva;C$xwh?w@?mqdk=BlIj}>F_-#YuTEu*Q{HD)$lj#*KT=a~qVPqd<=}&u_gyy;znZ$)hpU@_>j^?_S8_jY% z(wbZ!?9nZ~qj5CXt=MRv$!t6J<_l9H8~b8fTMXtUQY?kf`DqG$vqWiy42 z6eVExS_Fg&6$q89T1c&eQVK6TUZ7+#4$}f+wy$?->26aJ?!9xXLCh9rzm^Yf`XZYG z{R{+9!%-)(_Yf&m^8Z*XS)W)OTNVv5sx5)Zr=HBRHuiKqM&W|S!!owkUKuZYTmh3g z@XOrS>Gjz54wUsdXSo!v*En^?B#P@bUd8p=u7`7OyY0X^Z{xfX5^uj~2EFY8mJ&fj zJdb?XcpC_XQQIx8-r{$^1;p&BaF=R=+0NLnl>#?4kxf@*RV%V9B5x0^)cll)xdH_r z&Sb`2Efm*QI&&^o)h>2bi*YL+nV5J7Yn zjl!N2QJKt%6ixEo0(A3j+gV+d)}}D2q-p45pVP$~~a4Xd@098uEd`fHcXUT#f{HQ`%}T zS2PE}sSHaxe3ou(Y&_KTqODnH=|4Brx(e_HTF13FsPGxAtQf&7gYj;9Kh}G=2L`V7kbpxSI@%Gj-4d z&Q}Q}Ip;hQOG_?Gob%iQAP__WI%psnpsBXuy8pk2{Vv*aNI7ua5DK!93I;A3IPvCoMMROdMWls%@a$5m*2M6wt;f$T(^id3JW^eq_76 z&NFlyBd86~X=C#O@8zy)kqNZd2+eDskbo~L2s(VGyj3YgJ6xPW%@ip7X9*xGF{|!; zW7FP5d8I`=ii}k5I?2f;c2^wpaS`u4c5d65{xeJBuSQ(PYV~)xsNIlrx1#{(+yo@> z#?a?oD#gCTJ(ilknNl(p;o2`!IE2`^pyE=g_^7xxlhfwOK5I`JD9lgaQwd0KzPwTv zDA02Sj}=h07ppg6Ug-_g=!x=Hp}beM7Yp98)cj49l4$^j!n*0?MgVe%6h1NOu+eW= zEC84r7YViaKpL^_+=)Kx&g*OL^t`%e-eo^?nKNBFoOfV*^HljRrv{xW-{n=6?>=DV zj+Kl#lKxgIy6z$&qh4J^Tb{(l_6L*EB{rDW&E}WR$vlX~ihqEZ6agE=-6y_VezfhzXv;d8AdYS<`>O*B{|kcQuhwG z*(Gv~ZnI0TvP)}^uA}T$w?#`eIG439IL*`i_i4_PVKGyu+ark(C%g%5!kUmKoC#Us z>SBp;BFYxINd=+Qq_bMcEW#`zF?`z7S10-J))Q|VT` z4Z=5*@@7>$5Z=Z~bts|gP;v!RLIqQD#ikH^kx;deT!HWdgdj5zp0`P!C7!Beg$7?k zqSwCBptC}ygMtSNsM%{n38qY}vsy9Cv_+AdRFA6h1o8kv!7v+2Fk4{ul8_}8l1?yZ zW>XW)7}>NhZ{8)&xd=rXMr9HbQTgQLiZB$i_bdJ--kWHJk5$K@G^5tQe5P6ax7e&Nm-#zVVe_ZLQ3}vu%%hEK+sIBzdWh_7jm%(Ms+_%T;_@Tt ztg~?HkaN~qcy-oU_|^#wzcCo_?fDcVyxzzMZ{P526wVboR*>+1-lO_0ASN&62#dE( zJa?XI=dWIV`r>U6M#yXtzo-a2M1ZQvYzSh4cx?i3W)7Tl6M7^_km&jNSX4IEwGtL% z0hl42iG$GEZ7V;y@t?(et!5~|!*dAp+Or1yKx02m6PpZduMw2jW}ihqlu9&zY_^X} zl;#AQAS5=d+^hyyGsY%`u~os?u5>mVj^u2dt*$VkhJcjYy_wA9jT-51WpYirZe+jP z_(qLHLWW2D%P^xm%+Wl6(Z?sCYXy=fSW zNe}GR#+YSYG+Xu|a9n%ET$#sQ83S1zhvQXR$MLNjti$$reG(=mpL}3MWpaiR3nDP~ zkSvJcMvu5uBsK;7#yM|$2n&1|!kIWUz(oS;%c(un6EZ3YC&b-*rr`#^N#Q-mdr+FBRDlvbbmh%oy~T zj76hSsPx48bTjAGJYQF*%zw%J7q80vm$TK{d99qf^IF#}7#>4m-PBuIo!4pv?ZpCW zy4<8Jwtk5VsOQ46QF+pqzUS4Q?|Im2w4b$a_2^z8*P<&2gUOaGCj@v+)sGDDRaE&9*UA>#SA^o)!H9O{9fX(2yV( zXlAB4a76Qi=L!T6co;Yn2N8^BIv;kQ0D-Sa5Dc#&%w2TqcM{Tiv$!53 zi$WIuE&}FAs+G#(36~{|?LpqVaBJ`;+u9n9hNuT$6A}=4Snk0X-Ge4Rcjn<(nmxiZ zE}4bVC`S?^FS9Zb>OntNmd)_okL%8B_DC1MkxKb3oC?fEQ+^Atru>$j3@lkDZ4igP z=U)?uNN%LoW`j6HV*2aa*BB&DlF#xg$!A|~wi?Z4<~C`wsf*it?Gn!wJ61r!?^G76 z;rH4uo+)^wfSSEQMrG2?+miZCY-6SGrBv;=oO%MS{r0!KTKjF#?8X%*mDoE@_HbZw zOCVF-1f&N_=%FaRmt40a)8JfYc_WI6l1&MZlaD(WNx+^?M8CFf86u6WsIFPwG%_N z6La+v#bPryt@kUHJ8obdk&2=X-%tUs@=ojrO zS%WrOMsr>)vHVh3twSO=;HjtST?J)zP{jFxf_Yj4)vUhJ=hEos6$gcBsvw9 zFg>GCY3|MRI`o>9N-#Yh^(4E+U~UO~4_-1l0D~$GM0oHJtg-n}BVxAq`rcq(PZQm^(o)_4jVS))5K>k+_Z12?Q zjm{}i8&3b!$)>-Ss@@UAvPaUkZCk}T@4$|qlDSET(3V?Oiwack7LYL>9v&620JSD2 zCMG5(ylU$vB4*&6+a7@IHY)79I1XUn^=|ci&vDT6y-SYDC3gh_+r{UrrBp%*Bzi7O zQ8kjTAx%SGZ7c`2_ehuF&Z%7v(K)r1!b#Q0rY!H&_D*L!W>7O1ONIl!yT*_rps<%T zKFK}cFj+VCh)KPsrWwj)uc^tW2aK`o)kh7Sr z%{=Y%8*0ig;ae%c#BT+88>nZu9Z=xeu!NW=B}Y)a9iuT*!Q8RgIyMZAkU~ca5?p@- zN8@bO#I>5)CXU8&S)ADe&Up+kJp}++01C;SObls=j46?y1U3Q4SzM$-!r?5G*UH6f z8||bt4Ng-pqI7$l?KRSSZI{YxGnr@xR3IX-Nh zv)GZIYpd|Nb_hg>$*SMd+oc-i=LPpvl&|C0Hfe?B^hN_5{V?eNW)Y)DYwHln$aMpe+i8*?(eixN2 zUoKb5?9Q-x=b~L~YxL=M4SxEwwD(SqU=Q%cWo^0k!SBGJppD6lJyYh9QOoVdJ^`Y zh{m1~07PRw1VFDyO!`qa5uKcb$=(bA0Khpf!If#+ zHqSF`=DgSZs$sCYe?I>{b0$pq61pfk{RXbWTK-aV`b~_ggE@?v({El>AC9}!oPOh? zmdJQZ&FMFvdICF=({ETRFE-8TH@uqDZ-9(1FR&b9M>23tw*kX!$a$$a=qbMUTQ1(N zf7C&WKZ-$=fWUIk8E@CXb_bOx3?49`Xs`|OYv-7~HUu$qUK{5n8f9wH-hYOB(_$2^ z6|4zVB*aY$!bNIa!Po*33IYgElLv|iswRSW(@-il?R@-%WPgxGMeN*%?CgOXuL41- zFr3w%;(HaO-wn$l32B9UzJ%-ULzF@yvczl&Od^A1dN*pBbKdh5i}`Q;u2270MoEr- z`IAnlRIRdBK@5^kAwx4lGn)|O696zE90~|TV!>c6E|OKbd=vl?V{|}rJVc$zBRPy> zDINwDGDI0*$c)H<3=jlh1ppx7gCBXU-(x#QKHw7YZgQ}4lh}R^5DBv8O5QjKLlH6T z9wF?_O0#l)igIUe=?iKAew3=r%q6hY@P0)hoNI)3Z|D^RNgJo#SQD8LZ-a9fecI9K3hNygwO%TWFk8sL}838 zjdGSsLnAI`>3o#bN>rC5u$T^gF7&~!y9&UOt> zcew_w+}p6{+NCS3kFRdxS$1je_oAIK*xrg;dLEQm5ohR3kaDKpk38KxtG|ObgByN-%_O6!5ty%5 zmDv`~+~!&ax!J{{jl-D65cA47sT}e#4bq$|CQ+Zy3Oqq?af5Z_jtKI)U$nM{L%mi_ z76)^TFx3ZmmUBjH@(8Sf&Kx0gpH*zNcHS&mQRv(e2+gIFhya~4$I=0U9q=230tS&* zx$YIdo-kJ4+tvYaVvdbLI&q%ORqc;KOaQ!POiGKOl)w@rd8z|6Q~n|xnIQ8>0F%^O z-;>A!@6-wC93^YlnSe15Twt(L{tdhaCs0o%Mq$+6!Y;3u=>SRgP9m!Ad8{rKI(J4s z6YAh-y+%M&G$rg9(~@W~ESs=3hqdvWM(amj-&g}H5^pkQgO z_3LDK;ejic@POFo4Vsm>|0iCqu_!2d)};+@S~fNcuacO1(XOl{UBi-!qZE8uwIwK}-)}+;Vd>aK`3~GUR*qc6xG{GB->?;a4ItMUHJ5Rs$3f zH#YatmM|_E_?#FuYSK?KGE)Hw=}9^>I+^(o8DO*j9=J zt_DD1G7pa>X23a>XBLQinWo~Lj~0&@vlq$%(Re~D*ai47`i)>DIAQ4;_+G$cY#q8S zoI)HUR-+F$OIKDABsuC0e z`WXec>x5EF%J99pe3H6qRvNE9Kua}ZL1hPrt{^I{bol)%jWNAwZ|q$<_b?Od_38*3nf@eoQ~WX?Jd?L--c+I zsmJDFs)1(zdNH9Szfz;UD{i#xTFc0^AI9HD3qp*_@c}G8fY$|g*gKe{&ksqOS@x`7;Sj_W1Y4Zrj=Gpge#)fm9}Pn&OorxStCN4;bt`x zeWSNfw_U96z(w3#o?bUjgb`ES!7od8y`2Q;=b+@-m!8O>1iyD>v72fx^*C{jYVK>t z?DTRwSWZb`DOAd9hLc-&mXt#=drDb02O{nbp3pocX6|I2YB%`{+a|e`hFqexWHorT z3e_#+j+Js=l7&=RwWdPp=HSz;r{E(t{SOa<_vFqR$9f#br$P(&y!$--y`Nf?h6R!u zn;oWED=|ru(CSRlImnj5k7cpiynn=1STTRkwE z!J=uzS3mtZZA`~Cm*|UGvV)mX+TX9{9-OgeL2^e`k08pSrU~yjccp8LA$*%rSed-# z93m)I1_ipXN`$swB{YVk;Y<$7WrnxaVR{jfII&0NW(~(oCMwj}Go7zw-=hQmrkF-Y zy4vkZ@uhyjf+gLzFA-Btb&X+v8Iqql(0qj_snYwPzzx0_1!&NH8#q(|+<=Z?J*jo9 zZ>`9vD%jH7fLQ+PE(P8@>j72zJ9EGq#7Bhv|{79gvh6qq&rdZp3p{bbIF z1>$WIeo-M9jY$Ohrb3}E38+ba}+>1u&ab^f^GPCA%s zT7sm*tvdgnE4^ouA^ZcM48yWB3yr3A06=nB#?@@fFwB)MnGG-AkqtCHHlGkfXM;!{ zLe0uH2H_x3ceF_Hr)|Qc0KT%8dWBUBZ?j0BUx@W>23Cc0_d!UK!U$Wmv)_#7-8KRs zRs9>#cw~J=*L2zwmpFSOtY8Ig;&SVibn!&@AXc1zZr!bh$`V?&F%q(MaC%FPk8Www zYB*QFarq&nZ2-*WjS*yUO*_h@1k^5lH*7a$Zm`8#6d(`E=^#vh!$o>kL{ABuEwy6p zX4()ckjAusg5g0~U7Qm`(Y0LUO9U|}OdVn2;(znVBC2rIG43fK zG7Kq^)VV=v{<+kv<{4x4nvM!nvzeO*H)zCY-(FDU2?x{!Ju7GEkn{YT0Q&&zjKpjq zbxuR0KmsB6LwCdV#f6c<+oD7oGP!R2C8u1pDxhz~6LhtlVe^l#DMf~jWCvDxirU3aP0C(+0 z*@|B)4Z$7p0x8B#D8v~{%t)E=HTlxvGPoNa14!ADk^ z-@4;y#oFEh_+o-->sW%cwA)ReOe(z1)f!?2^E2Rz>Yg=1WV4wC^;-;~Iatj(=s;pv za2dRT_#sgsL)pWq$DR1}u(97R@&WK{g#+hZ+2RdTMdSFf1xv`@8YVCq9`#~=c0Wd1 z**v*>F7&Aat_8wmOeI%idk^Fr4(5Csya^+)LBKaD!Q z{S2u16N4BiykyP<3b3xAXR4tTFL1k%V!6bOu`#aIKkldg!T;?;&5eG{ z1Rz0KB8il3NboL2`#;*v3Mtxky39JHz2?T?ga72^&z2o%t}Y@6hsz~=`S=3VN3?1{ zfXjND8mtTdF4;^#R*rz|TFV!zdTJme@O~m9Gfbw#J^P?ovmaB0l(XS3Zqp3=&3HXE z3gdoDduNSBlE?A$AP_P5@NE9oYct<*FnJB1B6@`MgWo_5Obt9!&wp+J{^Cj9Ch;QGjy@;5IZT|BoM$v{7|o)J#|ftmmfq9oG$>sYtI^}>9t))l{I{sf zO1X-9=v|K6XxNEw1NsX+^-{a%YhZEW*@c}x%FaWe*{PPX*xMs^ThYl&6jmSIZ_Qef z_Y4q{v~KphtI$0L4*DQDkhi{3LM&h-s4KF>vkU8nyvgZMxr_0NsIC^R82SsX1m-{I zA`M^E>)6n~MlF29bIg}7RAoU)j;uM4Ryi49c%UVp{t(p`io%!Z`|t2G4S3VE z-%_YPAJbm9@x&P0|3vh~<|{7HKqJH0UKi8SLT50G84U~+0u8mkGKmd%3Q>3hS{!Lo zzd(8eLGE#TLqSgGC0llVoQevk8%hhup)j04NY1wl>r2H=?kG)kTWW#focGgR_fNPL zgig`S(y4 zWf!2Bk7^Qqfam=s9|}#NoV9O7b+2JVUa4)B-F~{0iuHa~(3OBMXQy%TVDmXx!GdL) z+|tZVlb;xDVe4jj1@A&~2|9+=My;{go`$^iR{2AajJ8`DJ(taJbOxD4CFxQOZCu;c zP+gRr^@9iW!qcwM)~_1A6tF<}O_rCgGan8S9RU(AzG6Pz=D1GfnL+sJi=|Q(v@-S_ z>Lr1qaO`Lja4m-Pl0H>;!;tk(A#IR!$V$}0=%pY3*FCO;BHcIWrY#oj zRfV{1EFeOZ`*2h4N9{r6_fahZR^W7njkH{E9dMoABldX;O>ekWavd0~^;XfpBxL-< z+vo6}S7dmR)FFA1$3|waT7wvHK^)gSOaqSGDHp!7ozup}(E=byy-4j%wRrlHFuhQg zXiSD|Tn#M|3BE8S&4ZxG4ope>dVtO5!ehM|Qi7<(2c>Iha&VtVO`k>KksepzLKau2 zfa_}JD&r*4$C1c!h>sjFu$3lg==2}ll2h)a)s7;vGJjhxB;ZQUE*d+mIWxAR1n>Jl zbxy6ZH~SGpTHAJftu`jk0c%S=fqOLRXnQ>E`bQvF>a%Ir(*}K1e+9fK4~b_Ikq}cH z2J~g!BC5%f5mH_8Z1gxrBGno+jsjsNHJ^__y<%Nr9`(}crS=k~7EQt7^b)yotYzDH zwMQZOkR#mM3RKNGCr<(x?36lTgAH}H**@`3D;w|>79C;hva>H(K{rHLS0WK&)F>_G z_3^p=4TvR&m2;vcz2?^?30e6(W`#3;>FK910>jEjDmpwGl(3}fM+yc7pnpMz`uQnP z+BTDc4XvY42b|swmritHOuv$To zr^?+I-hkeDLqgzk+g(P!@-hB41P$F~dv*?L8vu|V4fqCw9Uy0B0J6mFs?X=4Z=czK z;Om*Y|Kq+Ku!Zj!kk>hVx)E|D&)d7g?}_+VUZEfG|G;PfcSC|C9DbAqluum!bGVTa z@%Zq(eJ$||H~WultBqEqckx&a87kcK3EZ3%J=lEVQC7`k>`payDOMrPMk!()(IvzK zc*y8ntTy17F2`P-%5)z54uyGe-IYH&t-hq8_&3o4QA6|!h+623mdIJf7Nu`FxQ)VV ztZj7aFaR9`QT53i1+}||;V(vl`7r#3k($g&9?Z2g>cwBT`CL#e zs$q6}f$_1cF?L?z zYQ4`4Qm6{aWPq5w)fhPVYDI)2-kp!Tn;}M=0jQfrD~jU!nx&U{bc~Jy$0Y1*5cAhJS2}W>$??dLr>z_}|Ht@P=dSg9#KVywqzXqdD$iLVk52KfkzC->XmTYT^6@?O_^!j@9rj<&K!K`k zuxqtad=wl73|oFtTYyF8aF5x(haVjR4eZLph>t9cOm*?DG&5ekUJ=N6g)O4(j1*T_ zhYY$a3hi)CH;6#z&Mt3!DdizS@<&Gi%@_@bd@G(y(ff2aHBw~q^3EZ#8OkQmY5-Y< z=`|RuT`q2jdKFM-1>)?a-*LM`fzkEx z6KIY>BmD_S?S;1q|3ENX9k?Z+8C1`ltdyaKl^H25nh#@(0HG%Mi;~0g%_$E7b5Xzj zoH@`%#T^cG7C44#Enu&hBld$l6LLvkM4!^3Vd`Q`HfO{f9a>k?JGHXkNofN!X=&%b z-k4z)3CP^8GKz1${f*ew7yFx}(~Nxn$h;L6^tqk0y`p`<^olSi{(0!rZJzGAi~$X; z@31uCTL0Ttp^g((Rjat!sNO!NW)8i7deNu~=6xnX$t63N36U;IIe&$ysZUxsap+_A z&zdhnX9mzLL(3H8}&@OB0T)%+bt?K?6;c4D$s~!vuQ94K0ZO;80#*BH44m0|b z+`diAON2!$@3lTGZ70ZD0xWY;cRx$JMv*1#3(A*`C0;_hoc)`hV-Q6poDASgX`)er z#>0$aRrPS)N_p(SVQJel>)M|zjqbXc5nZsQ&nWFd*z}sQIj3$)oUYEFqDGwv&MEo4 zle#&zdiOtxuP?lI9N2X25DUn;d%HnC8~rFzf@#IaPt$W!ZE$kpZt7=WSKdf7<@flO zbj!EE9n}mFM4J)NElSp7=U1)o*F}?)r~nEFNc*ywX$&s+R%|qNU}B`pF);7y&CQF;gC!t!MM7w%r3*H=E?0}xo6p33I_D_A|phg!j-hW!4{Q-=i$F# zo%ofk(3QTp99^_-fS{2xwzHUo>!{I^v)2hW{A7i;er|A^8DO}abZ8=|?~Q8DLhnaY zuzKuy`6RF}^}vveCgYP)Mke9$r~*Aek|;9}??ug|vRf2QIM1YmT)=?spHElB#H7T3 zy!#kiw54VyG6Yks{!BX~ zfWf%~(CFZI5;_t{_-Q0ZUN0__yu-_V44!`{ah$O*vApldtJc(1%j^=0izP!D8x;*S zsg9#a_Aph<*oC#w)FK8{2GCy}7y5Ax;*rz@+K3s(q_O$psgS%2_!(e)NEGr7HA&gW zCU#^ni`cO%HomXjWcAbry>x!~M)%2(SW*!*yJJYEP63~%f&ui0=)3hWpQJxfLe4$N zxSvtyi?~)^$S(a>m`R$f#G4Sy>?RmnBc;r~tiX9ReWPKXlTMmN6s!tJt&s-M$bxpN z&^xOLlvj28|71%_ATXQ0{RH&GI9~C-lOy5IE5TGGlh-)Y5YG<3yu!HzP5>|T#uoYx1on9g6Qg}r_M4?-l%ND zD{hSz6a&4TM3#`~6R_~0Tja`fQ+VApNzq0ziZfLHzYTmIVD2GK>0CQKKb;b?17Qf$ zo}h14+iQqz50@1nY9}{;tTiV{$1oqtaQx?~JrfV~kw)?KC^ASW{}}L<^Mg~%Gy#lF z_IAfq$deh0Jw@P#L%O}!x0MtI&4tYvku&&of?|=mt8Ua>q7B^itLFrqbojsPmU^v~ z<^wiYSfbP6QYs4xjm?%r>3hi)rRlUp7zHHwDZ(7G)qc_dd8Ub{PCVp4GuP%Xjl10v z&x9N0s3)A(mI0Mz=B@qDJVl$Y7Gzh7*@e_;9#Tmh#(X*TPNsUxj%OZAAr^m9CIWgt zmMk3d_uLiwV}V6^ZTBn*Tj<=xVmya~mTR+fM@gy< z9>iLAVH6%m=!H;X7&z!itj{8kRukyxMFBQutUH_;r@mg!PGk~(L>DCnU^psLR7RTj z-SLaPvYf=0Qyk@MT<`XV=Vh^4?A;p*m0Z%V1Rde#vSB(HXt(GEbW82mfcGD}O;m75 zvZ@i8p@0=uG3#^h5N-koeY0@EF~M#pfytO4VDaIF-43sT<=wQwWUK_-9R$3W0mVrC zkyku_l3MiWUqpx)i+i{>nsmLXb~)^g?U}`kjvu%Ko9=rcft}(zd61W-;DAoq-EvDOk zo7m*c4oz^)_7?F|a|)ELIm+Meu1-_)64-CTO+Sj-jQY;uKOsz<7?+6s*LTBLv^?dV%i(63U$Lv8J565 z8k=90QgKX^^}*XP(a+bj??(tw%qYrpDu}R~)dIBAg?NdvyalLrzu(GxW%_BHN7eUd zSr&;v&M^B}oSeJ=qOH@DD<{#0?$lhd5%ul}M*~zxnToR3_w-qAcZpAkBLPAUnG-gjfswxNf!R`2__8ax*uOQ27pZ=<7M7 zA?R_(f@el;%C}=I+6gc(#o0O#fFwm_RPR@FT(&>3lM*>v8Eq11b|;(% zaT|--!wj8VT?v}^d<#l>4CG)h&=i1rEQ?i65BEvj*PTWdk|PfEY?xiM%2NP=|FUlNeRX*wD2Z zr6GX<%(zKc0N69$aVt7to*p*!iv5q46Bjqen`Cunq!U(dmP#giZs*PkZxgYl%=kM1 zWsot_2U~0DA?&T}CMwLUgW!5Ay9}u7id-U^88f}zOLtrYG_`opjD|Og`GglXM#KI)IQ1dL?T6x zpoE9*cUzMHz~=9{=Wtt{*=>^sU%I(onO7M;XM})GgmuW(l3b7hixP2{nw%-tr*!OM>`t74DscjrV zXn)~krBR2SMY>L)Y)VZM(Im*W6?M??1}<58z>OkP^>?d}&cuADf#?XSE;7U~W{QD7 z!w`7FpB8fmT4}o61z$7ipn5mo1S->Qr_kmzZQQY6yDXBdEOLm|RIlv7NI3BFIDbYQ z35SZ?2GDY*Pb@qXsXh$L1W>Y%aMkfLIbyB=`C&-^-v$W5!M`Gn3qdyENww_!i=uk~ zbU=&0xX)uXd^^MG&oqQ|`AT%*nQWYXe3TN!gzi?sqp>AIce<6E9Q*6yNC`ldMdR@K z+ulnp6DUc+It;gmgpCjQPOX#g`7I(0*rYoJlR{>@4sGK*vj6E4{z`Qj+i&xQ**8`_ zvHz%Fduq5TA+x(dl=Z4_pZI?+^L=~2-8O#ZmT0L|0)1&)=INCoQlM<;5vgYF@+5$_ z6jv3c5vSIv8BgJ&o*QF`wq|DRs%1JgG(hQ=R5yvsqQ=Wj-FAeqgk~uLU|98H-M1Q| z=c%74?S;8S;WN$NOeb6^(JEa2mzmth**cgL3TfIzp@{C!hnqKqL1BZF%X4+P?Ul|k z1uY(%aEL5-2wmQ-xn+n_K`sUoXof?H^M-F$*RUILgshOJ1<&A4m`Kk`CpC? z)b%?iB7F$kr~BTLw&lnJe&>WURZ4o~?dsA&!a)~Vu5|ii z@+?x#eYWw)Jt!kv;txj1#MrOFFmbS!qdwsU z(+|<~jNLw31mbQ6DI}pOr)U2-Oij$b{$917r&B# z^to_e)@+SmN-~FNDSkkSU~QC7ucwtM2GGO`yQ=|_QshK&1ZnLxkI#D|e`l`&f8jsn zD5xgQVZHTN=0;Hf?W|YEm6F3?7t{<{$~L9~0_OR)(+JU6`YkVv)G_7jRQ*e5ALB^_ zyEHm78xF74pZ$L(SbtNu$++y@iB6?dbD^g+LXI(EmLn0;76r%=8+0i4CbPp==gv;p zs@WI>D3wxo{z4t`cPKUU2bzHQBW8Zshf{ z#+W+w;?C^fF?r=K!?3;bdx{x#F~j^}`nCW#|Mp8pxB3}F_>{&ldJZd0>A*2Yww-3Q8S1PBYA zoTg%|0x!CQmpiXt0RQ6zpv+1@tq>zUt|%ut+hf7idq|_ScA`Tvyv2*4wEqtnIG1Bf0S5BSZZys!MT_eoJX3@M zasO3_IZFh@&7B>fXf4uS$D7;1mYoDKeG!530{rMZa)ZA-ap0oS#b|ZOI4Kfy?{1FfIk#8dy5BaeyIv28t1DP zpE$}=y_X@M9IfqK{w&?A_?h~ilvRWAS(U{MVm^9#_&hh!Pe6m^{==gi;w@!VEIa+R zK&1a$H-HUz>brEN1gW_y`X_U$$8Xc`tyC=P1fhM5FC>ma%UnyE?KqVJGVw4fIt(}b z%Q;qld)v~7a9#lbclxwwPBi3|{Cwly5LB{bDBvW(+sORu%wKYn6 z6YU;#5b;|jtp`*X)S6ZDbGT}05$xxY>a!>RQXo&~C>AT+u8n#(NE@z0@ z*6f4&!n`mEQ`>O!z^&-152B{0ZPJN((o5}RbkR7y!(rYO?$^evAH7|VJ#K@-x7O=~Pl zMwdqfF1d*L{fRiKc|A8qr8Q6w0u?l6(2*2Zf6P+?TO;EJ^Hk}V8}{li_b(ddO2D>u zwO0MG3IrEDBVFJ&>xO`Gt=unwv;>g1MV#_fYy}tbeVP|A<8XeDcS+Z7=vO+`bW;GZ zGh#Eb676u$r5ZdO|C}B%KpuwGBEkm*W1 z&9k)pM?@r|ENr7*V_5HU&I$p$Cl*~Un^^pzq$fx_s6j^(ZS%XEG=Jb9W>9Llgs z_$KDXguQhAQspt2vvhEppC4mgX>5j?m(|!iDwPR0m&q*HW-mYenvrUpV<;v|+KDRM z0Z%23RWbKfza`*ZjD3c%5-^zMPR+EJmAJ?nA4i-44femO7r)g)bq8xV;81u zkAm^oSVbc%j50L@5jF4|9m_kvup;y;Wv?7+yrd9O-+YAuHJ(j>U)hwhRch@;NdxbF zzH}^T#yv3T^Lyc%Kn3~!|7L*Yma_qU_09l64a_}(XcW|l$S{LIZ% z01TpkIJB-=?61XU0UyZDziRS-_y%zfhDa~s2yj$8yKH?iX8gA}snI$JdC}yX6$yxz zoTh&+(XR<}* zsYgtJ7(N2N(^KG^>SdnYkBqXG#^TC4%5P${rXyLhOy1{seheo1m}H%zv$1o|30><6 zEd8&4;^IFWgRnMv_MvLj_ttZDtCybMvV4CU#Hp_-g#4GTwTRVSfyDSGc1?6e%dRIc z>CQd~N{oNChRyify60$wxubd}7XN|Xh#d2?DdFM8$!CS*V``5-6a9jVW^8Mo+V)Zt z16HJDfSTnGZ{Uyre_^7kI8T!aa{%TEf&gLFJQDJS^Fbv}1M)CS>*q99}6Ig;M z&I-pc;p~}E;NW9Q1BZtR$G=(7(xgeT*nuO29w+f%LZufzN!FPaWo@?-QYx$F8i$x zP39TToMb}Kb=S8$bs$)kZmv}&U=o9G8BGjU-?mwy3#%b!-cyZ-WOI z7P1ZrXfEfH?uTc0e5e{C#r8L=`6nQ-?@4Wx0M9vPXUt3X4eocg+o3j_C4%t+Z1|of zqTnC#?M8IYfV75W|F~l0Bxu6kSm5(3+~~@`cV+A~AV%avFtA5(hDEjV z|3gK>WEmnL95afxA9>`ZaAoYtnR`wU>5Z|fl#6Fum}O*qM|yZjo;!p<3j{a=NQVSP zFzJm7x>$@Gy09|0CH5;n1>VqXNoL@G+9sUkjs83CvuYnWb z%OS%=pU%~y4SXVR(l;a*lERoMDra=+xHkgU%i>~|u|Z5d1!&@)u9f20tPgp2gCMGK6X7S}bB@TZ z;IJohJUk2hGSgs-62q+5^RY?z=0*Q6rRcXH9dU2SF5KC?;5tB_dCmazw=i zoHio*+bw@0L1$mKHfIctNBaQ_MacAHJ{ z@N|YEOecm`JhZKVgIFBRmdrnk-sW__Y6ti>Heq8utlDo4LZ7L$)nW~Lq@YSpc?H^xVio@o`SR^Lcz zSem#Q!?tt7gmFrpCkPDp;9UX_ku=}2yS$9pHVE!F)uCp6E*B0l3BMX0ifQ-K`#|11 z6RY~uqI6{DsBYL~CNSlH?bp~muJQ9zT9^I#XAlJ-smcKO`37FtK{m@>XR72d1P_CB zsXB}s8ZhzVjsQ>j9Y_nPja!S_c&bbZvr|l`B?i9OHN|*)Nb!4EPJE~(cQc0l>3^|; zqcquuBy#aG<|4I(qKP2#Q9JrVviJoX{^CtAJ@%$Vlr>&zYb3?m*iYRBCEo@Y&NnyA z4O7C%@EtuCXL{6SeBf^Rkel4p>>h+L=I@?@A!7C4yB?04ku3JN%D9l z0J&W9leIlzimdX_64HO_@f2TXXj3S2p|l#>+%k??NQ}Ina-~^fyF#vjnHa#8#N9Yq zMUdwa|FHH}%LUHI8Z@l%nEh=y)qgI)6~iTPFl*ixa2^=s%rx16`*4O(Zf1$!c-a%eHK*EhZlq8qlw0$ws6wIG0ReJ87F+elbCt&KUe7KhXQo&E z4m<>c9qXP~3CB+OVQn56Y$JL0XBkQWedk)<+QxuTmUn-R9+jiZ$Q z{1P4nfEUkGA1OZ;E6xZe1fqUxr}7q}_Ax91u#{(t(P#n?Sz*je2pRn*3SL2lwz!6vbCMu`$R%gcy^MfZbm9$st2_xN&Q;5c894ebl|q*5&?O8M;ci zlz(v8z(<7qGD!z9>yO(2PZa@}kp{PUf_a-$e^14cjzz~Uwqi?f-;c=`H`AJq*J#n1?s$q{>S zqq~9e_M?-Y8J#M;XesQ%0U3MB!(IMIbmEIEZ;R6LKJkEuBLrGaKY6rHpIL0hz!D zq$MBgNIs2`HXcB;?ffP2TuXmC2pIhP7J+5FA~^i_*{HAu7R4u?18YY`rH}|A&Zm=h zBdClPMiY*(l8O}389x&J%^2y&e@iFjO>%gyp^xVw8}fX<(LNs8Vw<9{&ESkPxH~^T z4@b{#lBK_6r&>##`~kd4oWJ%baRDgEHS8Rd0ZjECc6;6A&-8Yj{s$iZS^uA?(C6Ff zW@F)w7{$Wx0|F$40J?V68%uE7*#=<`?gB%Nulg_xdM0o&Y9E07bx3ej7!sJYOPF@I z>EHxcoS|l~SidGpM0))0vZ7Wg<~q0kwivv<3@Dl!0;d)YTV(f60jvR=0j~i+;vTi1 z%O?;&;{F3NrJkVBr>GoKEq2sW>S+m`GC-o4{JlI=x9$rlswIC1+RUz#ze5*;BSLr# zU;?EI0fGcN#o~_6{lXVwUEt?J;Byf`O3CGS6Im?R66JoF>yayvEok{G!3_7F>mPQ{ zwcIVr{m!q?K%L>{%=Hh;nQK~i!7P97f?2-1c9-tHNL}B2^TGJ$8@qdVadYfrebVe*dct)F+n6WMM-RArA z$0v4@@%CX26`iZF25YqT*g!x7X9JYt_`y8M3Q9dz9f>iRsySFWSu{EAPR9YdH_-DM z_PU0>Y|)Jy2TcOHBrPEYM1axRbjDk!rEEu%DPiZ0au^FBgXLTJ^`7g`MN;p%{$Tf9 z55RM{8|M~$ZHw`>y5{Ry^6SA9NLtndV8>{lo|&?hFI=af2hWS}aHEB2wVw(HH zOZ@!Td$Vwh$|{`S&$X!v=l6pN=U4w>LOEldhbLe1VhMu+3M z_XV;OpHoc}2&iU0d}jYPI=D#lGyiuuT$JIGvSwVI@is5>e|NJb zNC8mIW{9a~1^`sQCK66ndyVkS!^K%Lwv!dUfVfWXK^r|l%-7=ER(yFG98f2 zV08x9!qlOz$FCeD&Pj<>qY zzlNqkI13B`*)xhc4jYPLZRF7Igyq(aF zL#3YNtrMp-f{iw1P5BySaymT+p7;AkV_p4Qu|h-=v1>XO8%b|jY%bm9>FP7@d(mg! zd(`#K!T-fK2V-5~{N`P|D|d;cHoDe`1%;`QbV&C;)*ju}qq8&GDAxiQQ|ABQWd{&( z=%Z4}a9}iGH)l2d;-;$@p!*@sHKemP`m0BU)F(sUdE_{@%C~U*iX*8wP9Vo~xB<6d ztbTE$tJJJ;6f_kU00(o_+zfxP+zg{okoy3>{Ak&`_HC}MCeL97aPJ3mR5GN49n#6} zjO>!&wuU}0p_cH6Zq#KM(*r%=JE2 zAq-)bF82jtip=#yP>7-kJ>ePQCBbd^;9z}lvP>B9auOAn6hc5CmZu z27)LIVxSttvjY-ci?lJEVg$GI5^$D05PL-IeG}JXX8x5Z zbZI^1k`04ye6-A4^cf;rR(f@i9k zpIa@-a0v%DURaXI3hSV#A&RQi45Q+&5FtpFZK-3^y*CE0N>*ZvVMVjemc+9Xltj`v zH!Q`f2h9gn*94FezSeJ!r7E*ph=CiwoArXb`=r#33`}6Cls_KvS@~kAPIKm$6Meu+ z;9~3^#ZJRG!nwo|rf0Y?jL`m3Y^hjR=!1-8FZaP)Z?;v`nM5)ThMZG&Dow*`RcmLIk0|R5P#@D+9 zPk*K*J4St8g^&JI05dRHtEHRdIi+?*po+dY&4O&9H$V`&7lS&#D%`KA;)+c+H=h{(4}gfsb&fb>PrdmEOYA>(*5jeRiX2f+p15!K7>!LA7-nreA0l ztFB6v{dJh~Mu62J;rgmYyc+$9IOB9T?1cjmL z-cir4(JiL`=bZSlNmFk^dfEGv;oz+_{(x6*LAhyZts>G+<=ZT=x7AQ8FyReairMeR zHiFbUh)#cl)y_2v8-KV-w$e+>FyU{Bp z%!JI<5P;$iJbI!VJsG_fKtdYf@^(bO zsCno7SRA~=o3ufRvv{U%f#J8(-oPnNDD>%`jOWX*faf5iDRIT6ArOCJ>nXN_=sa~U zBIQ7zKPd~RJIfZ&`7gg}o z$bwR3mG6o+RYGJIZH5m^R7TGkND(JK#$H=x`B`j0Ud14XYbA=T%|&ed%)!Gv4| z)OPI-p}?%~blyy{iCs<92P!m~ffXaT8LZNDi=Fw79Zd%ha3RYO-SQb@cha^BU`hnx zi4$Y+33IFy4p9m)Fs{)#)NBwMbIG+@ZPFfx{La9ggij8U&;$!v$_`cno1m-I>=%4+ z(c1(@W3^b~{8YPJ4tMO&&orN}71W zVtM(ZH|~~UgBc^h_v1_5vB4Pvp_&-^A2++)uE3C8B!}&ZuXIebn{U4*ginLg)0J*K zKuv`uMC`F+8p@cKdaD~$af_-+rWDmi7x$1EtgSza>6q%l;b{5X&Q@h z0SSFj2UX}vid~!i%Ie2MqdV^VAvNG$v*rLR&h3Gwps13BvAxwq1 zvVDof)$8u4s>3}n5rGqxJ@$(e1oTvoA;J4O;u!WP7|Jr#zBXDpX(iGUNT|DQvIooO zm%l!Fh1;%}y_-FS6XU&iNrj{Y&;Fo8ZvMC6Iz=K}04+3~ocdUi{2@)r@Gk_D@lyZ0 z1D0&M(4i-NB7{4s2;t15M&iHUhz10MoXdH4k}sAD3qAQZvy z=H}4-o0|%3V#6t1R&XGkm=l3J2H3sF14852edOXUO{aY`Rt_4F2<y;xR7bTjgm$=}Fl;RjB>;T2203a>rniuLJU6mR*4v zLjMjB!oAVoF8d!L`o2DD7oETy+&Ct?5_ag|y=)NpAT@vmI}J|9RL%tL28xGf`B|x?3wGZQ7oL>728Rr;74QsW z!G`e^O*3;$yt59HdPsg7FxA8Z?+Skn=zv}j1BaM?RT~_OPOsqrxeuCHWJNVSo8$W; z;{Zjcy{xS>{>NeMuTk;8ZS!wIGh6iFN^q8Tg*Mr18CylIxm3$GCHJNeJ72t6n{5ls zO5uYAx{pSRg~VLZmy!qp@@z$kJF$ZMKdIPCI_;&Fl(-sc4}nIqW#6JiHE2VC)jp=X zdJJUO9pnr~k~Tu#9!p+w?vP#T+t2TObk{+6IBB*lq(LE)a+SZ#)NiVi>!-EYW=KHN90knUk5W znDEF4I_Jlup$WGmV}gtTcx&PqaHc>b{5X&wmLAj|8T?S&{eQNo<-w$?M&aL4ixwxn zvU{f#$_V2K<_JwjC*ukqq9P&}hH>YGk@(ZPa==@k&Ucxi_36|lGx!o6lN5Ah$ZYFO z{ys~@_AXgGD2aJAIcphpR4sdzED<^8vqa=r97LyyVpEG8Ff?PRh6v}(whBSRU?#il z728CqskPe5Gkvgw5Fu(ffeBzJKrSJZNv#!2N}-B3=}V&d80%HHZPxdR{r2i+>SxL+ z>XS)bW>l706$=2OE;4D$jM|%3^=4Jv%&G{gC@MxA*Wnr(%#cNkC=d#ZwB@2!nS&?$>qdG?MJsLr11 z>}u_;+195MVw^T?1u9ph(8Z7_WNNu!IzGMEWKt80<9k)XUX?JENkqakr$7mo z$a~+j0Z~rMzy24R3jbyH(?Sga0wL5e;oRALn?>6=X#xFdnX1&jzhXg1Ey=`QS)AzW z@G3SGC8V$^SPTwbTUQG(XAlDocgWKn0}Xe6LI(&NUoCJu(Zs}0w-e1B0}Xdlg^aIV zo+!Z+FLg^6f0wrkW4qLG5)RQuq%t||fv?J>5y-v9mPQ|NzYDqP-sGu7h#rEx9gjsM zZ{k$KL+*2%?sX!RSb=0a+Qhj1<#e1rqWc?%+?(X&e+Z{$#LC>v>LHt&oF@N6Gp$i= zrhDI1;+MyUYORb+?rECd%Ud)(CZuVm)0k#TtPD)ljRv>_53XhgFT#UT0S@(tZ}>s){p}SVSK%;gug>BD&6VObVY8Ko zX(C73n|bs`s6J5K295*e*`Yb}QUeb4_gFqmq7Ge6y13mr95FFxzZ z%ePs$T@^sgwg#b0Ifyzq)Zb?jT@XQs2rtO^tUXK-Q#MdFwjGvgu~7@~Trp!s4cyJc zS1&5K*l@wI>%A>|WfMznVAj`6^}(bs*AE{JNRri*giJ;!_Zn0Ro7&>j9b2+ElMwk7 zvOGTBN@4O=8i`y>7LP$J8t)`A_*4u!)t0U$k#NOQQob$~V(4%6&U1qFO`<$uUX&f{FfJ+{a9$P)SA%3CAvSA>kbFA<_~ zPqZhz%^u(U4}GKW=X|DRU96&~%~qq$VmVfbNprp4zT$;=7}hT45q!ox|8vJVL(M20 z>YYhy76qonJC3E^lTZ^+6f#gm*|n8U^9JfSXQ-7Mj~ev8S;cWI$AlXhH&L(IDn$09 z$4hjyOVOyJ z7}Q&eJcWJ=*H7_Nz`9Q%>u;OljrEtT%dHm-_y~a?Z@=$Md+Sko;7NI&SFICo3Lylf zGFe;7Vt8@QmF6^coUqwPNWAB{c-K8$HEK}>C3FW}x-omEFab#hQj}d=-C9+1%STZO zF((NUNGt$%K+=gqV~B*I1-x(2zKbHC@>9mq#!lj0wp%?)+&#$oqlYO(LW+z^d;WK6 zh*S!ll3FMzC@OPOiO9P6R~n-kmth2pLnmZ#C%zJoLF9|x2qX-zfswz;_>xLbE}yb^ z;wA=fmitELxF|efRdwO$MVL4zdv@3{^AtDM!|3Go~q8UXsT-?n24xBAEm$KTQjd#c&o^F7aR( zcU*3y@MgJj-VNl!{&q(#tHUmQzphR);>IEDt!AfyQ?M`E9RWm&6!^UTo4=YlZ~x}8 zZvW93%xS4vwsdL6=D;=h6LJtC7-fPmot|Wg8H#)f(GQn4;e|_0Q|4lH5Fz$D`+vc8b zYpf2}yw9gYikPqa`tNz3C+^SwD#eNWv&X{y*#{Z1EeVtEOT?9aX!gjApsPmU=%{R_ zmZo&3M$lC#GpYV$dg{zBv{EfRs{v>{#i)R5u$`e$dwubpq0#-XMr{n|zJ6`q{OhfZ zr|UNV`poJ%R*%2Vw(@^;mz*OX_E$^Ikq>*Ukq?iADE`}hU$-bE5XHR1Sqn@4xH`ClYeADOqCs!sfD^&0{&6+j)&P3zBIi z4Ww-S5ts&+8yhPDXg~;46H{X|6EoujMR}6z%HlFRBEI(6cJQ$5*pZ?7VDQDDld3@` z;j0U5zl{q6-zJ53qsC;!qI_DWG^!6R)Vc#fFa*WGBlV?tTxii$Lh)|XpP0qz9zo&a zKAx-vvMtEB>HqwfmISD5fCuBz1J%K(9v+|q1J0|d{nr*xDh1mstAhwc38kSbEFC{+ zVn2K1^Z)*g?C|mJ=$WAs!9Wy)j+8LU15L3JWWM;<;kG;Q2jS|#2jRkkqneNbd-*nq zjXiBiwb;@WD#W5Q6)*vffu6Xbd_e5k!by8Tj8hh~#yDqYu;8XRwFeY8EUEZpM84OL z6a5+(Agi-82x#T5>k^2)-XZRG3FBXd$(GRz$rJ?xI#FG;rvgE!4wNYZ9~ykZoc{HD z-RpXd+rhRYMGHtekX|qmZK1*$G9449imQXgKkj!eke%^qU0EGO_rKcm(RG}UZmY}K zpO?*K`phT94fG~RGF=t|!`h!s_plgDkJ)Vb|9M%6CGvGGw{`pIeak-KZ5t3~1f%6V z8_=5oZNQdUYy-;j|MMM?Y{ncUoH|z1zi!vMH_=};AQDYCi9yoq7jHhwN7s=)&V*PZ zSfP3N;qcjcazp+yQY_h~-iIw7C%c8!Xu2CM;fpP~kwF{RLeG-G^he?TBmuK~SD z?d9pHb!>krVBN%zw}CaI_q|6G7JhS}OSi1f9>0vIfj;-N|FQIjKl{W;uSRfU#r_X6 zv2EC^el2I~iM#7+s!6@G#5{U2YVt`!BfjD41HG&pxb7th+Fw&bYWa}O{%ZS z7JRL75it)}vqm*Do9fY-*>uUKZJNBOwWkPBB1F+%G0eNA#a%8=^rL(-w(j5Yx_|k< zuX`Qb{tmx|HDhzJVXvB=g-mzSNJL8ti^F@RC`1PD7+Azai06pJCcZZ}e~q=)yG%6M z?8{{$PJx7k#Qu+y) z9~un_R$`t+2R)Fvx;gPLSg)wvOT5ET6`EA`uB#lm^+9$fK6LaBeCFtBu7r#ltbxRh zYFxpU8m>U%w%cS@vbhILyua9}wPo4XRBF5$%IgiqAej+Eg{ui zh!p^wZ<8dR+UR!J=&CKcA4Mgc@L_(ScMmJlp%5rUVUos?MiueZhM(BJ#lo?9^hmjM zxL#;sz>TihRV#KCXItrDQXQ-+tI+doZ|Gn=bey+Mea(2)h}l*TL}Bt)aT+?gFW!1_ zBK-*dKG(>naF1Wgosr6sTaT3^^I|saHH&ev@MBDj6!|YWa;YxW_BV6fB zlfz0Chm|Um)Cf67VBraR0!K*w+O+G$w?HTf{X)s-7iyhfNGP_$w)H##cV}5#@%b&X^mTjarc?%&y(g>(Q>j( zUYyr~$%~T(alNq5l4EcUByM^73XZ{Z){7`U1K;(C(bf(rQV~)HfX*%aL|YeRC=-z$ zc5DrTk>=uQ52eT3)Y(*(r#wThL9>0lP`SMWSp$fa=>eiM(VeIgT2q2wgAA3dm`eClr)Rej^rLNXqJX<<^wm3a(AZ4nC19DX5ZNk`8m>D%i2h40b zv#sb>4)h%wqiIq&G#2%N0-++HaPUKoprt}m zzo`=)_*1zQP!tDkqbU_I0X=~ZIaM@gFlzv!EU~pkqsppEM6SkMp3Zv7t{&K@J1c%)pVf-J{sy?_m9J=G>CBA!!%<^!RWZ1#&5f2=$}*$kvnre=miA^;-mHo@ ztJ=_r$yStDx{FbDu_|4xdZPi8tv6BJCVgoLksuTUJ#nWeZZ)R;++8L&mC03Ia#NRF zmFEV|o#H&0z(EFwDDT~@U)M;E$fovMg;aV{zziAER1po=6H zO7PW16N_V<#NrqyF&&@cYx3#1=z44kyzNKRCpD&dmcDMd6gOjFQ1V)K5qpDqkEqRJt;r6c&dfc#F@pxDk&7e<}rr znxQgDCHSxH?tdMA+qb8pzpSo;a>ACxBrqv(Eue_4HjBMruM|iz`+r{lIqMQ(p97`6 zBmRnmllG2yEZRFl;(obR2g%*6%BdG9yr_|M;+*WVR4+%5g_INP%v2K!O*NLBG`ihI zbR@?J3n)4(Zw9YMO=h)e>n*ktfft2Gqwr`8Yi6g z+~aKm*;Eles*i8#=M_wSZ(xbeqwzAyl%?YqW_mf3tJA{mFW) zv#hbzGS062HL^chcXcDs!*dKH#YXRwjl6f+#IpZ)l7VPH?h2c3AI@S_Q4A8geMup4 zhzu&RSQ-*pw5(VP7Vj2iaOov^JnFc(2$4o5(B*j4aWVpV?|(eewgz4n))YXsfG1}r z21dpP#^xsz6N9w>u6IJ?Z2(c!xKF!xYf!q4^JASs+sDU;?T2xAD7=|)lHEbF(`?M zwL{CaLre81>X|&~F;!aR$74@SbGvJOpqD7-;cC)q=rE(U%&J;u)f*ILQqd^CjF3go zUsUvdn+ohz(>XSI-%LW~aA`<1CW}uuizOp*N2Q2-QkukYzw%mTyz*9e zYdYt(K1bPoue)F4&gXJiXXz9&mTsZRb!Xz;ZU+PLU!LmM1#Dz^L8_2K12Sd?My965 zrUu3ah6O;7*#AL41Q7d^z(8C~mipRas>~kNfs2Ph&%>a_*We@s8MMH}Xwfbn!vBB= zS+9>6JFfk+xVs2+SevL#Y?$D&N;*;#d%SIQpzYT%6m=VV^Z=@V-YEp_OH52XG)1F=ZA@^+baGd;NdkD}UJIoT9nv<(? za@Cf9(9N3dU01X0`YhMXZDrwZRv!9Vqpdv=@%|&*eOBhGmARTSSEbC=8SPzJ9%xqF z%j%gy@SaTo3`iJ8r8Wj6jkg$(X<<__h)i0c9KJ|;AuPVGFN`_PjLUX79)oy1>b%zx zG>@R16sRZtff_+o_z0qT0Oh1MDikW680kusCQ6ji)_U&oHhWQd1`0_Jw)s+1UqETK z)rxGoBCC?jJbmRhUG^(y&1~z;w#MA3FgU6YH}&DFIM0!rl}nAk2Mz9CA^nRWi7Kbi~LLvAXTgoLE8r`>1NwV}c6^BKk3!xJ- zc(g(^FHL5kj}Od&B!^Rk(0$0U9BOFtn2{Kne~f>#?z+YqI2&a-oX31EaW4kOD6JZn zyMQfV%ZT!xyl?#iUs`qnWgJrqIgf#`(c3QLtHdJ#HMv3WVIRQcq_O`?l(>~3lVj6fI!xVYaBDH}69>hH&sa zaq|>6S94AX^6g6yIO%YDA;xE=arYS;b;O01*FGYH4SV7kjKtP@riTgBY=Yzb5+JK>KBFcqN>1v^V)wVyyhN1%n(dip&@&D zIBO){SDZ4G9QBfYeC3kcH|vLvK?!YzLBXP6u@7Cm18v#6+|~^>?E|O&{stD@egcYf z1E>CMmh5Fk)uSyV)0UB{&xQ}j?uJM_6_JtcNYEA(T58l}Ry(mOxalJUE~6?zS3Gx6 z)XXfMnX4@`wKq?Da}{Nl z&QB2=A~O9~iFL!@g&wyrRkhDrBI6eio{3ii6}GfUv*iZy2FjGg)1$zN$&Fr7zbSW0TTcYs*SZ3UjR_wUmB+ zd^O{zGgkhEj;~C4&E7TyAHjUh^9jBCJcX3X`!;fM5nv;iZENrl{Xvr=A|W>$?Jc^w zI|?>Lklg#hXXV}xAwgb2+#o^B6q|bV%-L2y?(SnnBZUyMhZ9VYA;f?W@@t%B{AGlP ziD=7+R0zdU!N$jAz|Y4&3OiCI5)*>~Zo8Ddt?X@OZ!3FSk0pEC8Ih2i?MI_qW-lF( z)@uQb1GZi#fN?+<2owAx4xXoj9{dwE|9x)g0WRht;`Bj6?@vYaPPD7OQu0#>N+=4K z?KHNTkF$ItzJ(=1vjL~=Eubp2sfvlHEXtcz@nW3c>CSKo;nB;{W8nm<3;E>%qbY z(;O7kfS_gsL8=@S=sDD!6y&%-rUsmqDb7lDgd8KV&|9(Mup%Xa0*M+on$?Jokrzf@ zkVR4E9Yv=NlucAUTZQ6%#Ej}g6$B#7Tjyr~gwr{Vn|iFXwt@opQUcM`W@>kyO<+7mK)sCcSQ)X3n;|nFaPtSr zew!siMv7vpY0ATl6-0|PC03OZtI}dWO!k>DT()>wg5OGO-7=89scXTHo-|v ztt;^hz$ivB7+;&3{xt-537vxVm8m#n;ikM=IYz;Wh-~&>!wyveVDL;Hv{?FAw*|5` zA+R+Xm1Ik$(Wpc(>!t7*!^pk5t{~7+g~%Zs|JBxWYiP@_<1W517FpazqLQW11&bT~ zKZND-*UNs{GM%RXpFI*{>)xjb=`x_#^}vp?BM*`-k{{Wp5G8^DJK=2* z9YS)xj||B4eQYvd%}55MywrjnH2J}psyaFR5R65fKd;Cf!)+n$MOZ4Xb)D^gH=UfRx;WKmY8p^WV8)Edj|*=mNu3+kk8+Zp3MX3 zjM>(VDdvGBtvDX&P(voqbpX2c<2~A-J-usj%B0ZJ^-6N~VxF+vq zHzci*PthKkuu!OSq+)E=*X4tFrVC9b#;D|ETzaqa_dA%$zP&mrv7VSo zje4XvH3+GaQ?=9xDANWvJCvX)GwKZLVTOkqHclQCFKX8;x^N^F%)rDT?f;$NAT}NbJP}e& z!6VKZ#VU+X-25Ok*eQFRpo|=&1$56 z*{c*!A?|yHn`9a$*CnJj2KeGDH(Ja)uNAElQc$)?z)cRih&dv1?lwns1ZozC$?0cfRx8|=_ zY*O7@^H{pKh6`c1;^lVfCJknVoq*yagyvhU9nHg~!qW4#!PC6WUT_lcMl2rhEubp# zR3)y~+|!!78gox$?rOV&Njx<%fC&VUL4YV3%<65<*!xYiBq<5@!bnuH0*1*%YPy~i zdVwO|kNkN1>)zz2aKT>$FxC;z2N!AWrTx_oT6<}aReNb4WJeOhN(=tDcuzV-h{Su3 zlMcd$FcvKx?M5Y}Ca40Cm#Cz! z5oJkDI))UQkr|Q%p#b9(06-`h3Wq}iVK^Wpln*G`0~CM_T4FkQKt?{2M*?9mB8HBP0}+L6++TXH`G|-J~2AG zwz5Q&Z!jt4Zr1~js{w^zVDo#p7_AEgLVAQr|CRFe-23K>DAcom_CjTqQrvHI%l zsOZ~Y+L9$S)0LX!T6zA83CpoSnFI=FLT;?%2p%@Ffr7>*CiA0Ckvq)6XOSMj6D~_~ z7&q!1sah6nJMMeh)`aJ>91;Yj3ohIzwLwOOsIKq*LE;z{Bgbi)Frur-rPH&ib?$--rM2di?PMM%JnP*UeQP~D0V0JE{ie! zA?}W;P#t5!z+{AYhu?rHtj4IuG=RX<>#?EZEMlJFbgbIX>?Ld_rM{ittbbzUzJ8H< z5r-htr8=dQf)4jw0iRzAow8?rE39ShFAYFME>O!XXQYo{vy>x9FKs3Lc3y)9W^kX} zG@F=qv~Xm@sNCgBa#Eq8e^_D}gEb~pxDUxhZ1pGGn1(iXgccl)_D|Vfg`{`6MNti*m^Phx@PV-ukG5i?3c~F8e;S zO=iyw-T#MSf#9WvA^>TUsAeN?4At~n$5 z2>TWsW<7)e$H{%U4y($eC3E;qDHcmREs_&3a2m{^2iZQP{_|&X@V0B^36=(J&a zkxXj(k)nryUfK-dL42^)7?6~`CA2;l3&M%#+&h^~L%U_sojt_w(}~ZTmd}m}y3yP3 zW)pjX8;liS>-SVn3^u$!fMyP)Ln9W#xpxibX|t+$|`jT{X5Q2+fn34$w-33u!BTOV1SkxX;iy z%=K46ZN&|y=}_tRPr5wVpu1*zbB-I%87kzkTi~@7;?nDK14VvcJ71k1W z4hR}u9`UML@ zjSfbLGg|Or+mO&;=4jF2dSx0;@2TO;2u&ddkOY{>qp)6je|Y)4`w?zU|0dbZU znF~4@qR3Y#(x9f794uWH*Ft?muv@PC)vh;gggPQzqv=^+*(O&?JilBmA>4;Yw3v`2 z%nP4-*ABYoOIE8S$wqFvVR7fk#~8s|YC~Es$3&XWfRfa7p8`YVjhmgW!*$!=l3pui zsl-eIAr6{Zy0T?fOuAyWB6J-umhE^oo75tUc8N&JJlt^!n|0uV*_GS6DVkiLoyP#o z4H!~$x0;W&{U(K0s62Y1q=g`u_vQO2|110FTLp*^zOKox7uZ!%>6Kf#;$*c`?2u-{ z3%>KG>pT>>#QXRmIz#&^(r;9Svp};E;L=wnM9V?xVMR|aN~`-7P9yPz=t5+zJe`7{ zlzEPq1MJCzr-3aqT;!!B6P&uR4z?g;#{YJ8q{^%TBT;;LSjvO!x$$8jeazxy|NW4Y z_sP0e-^-z|r4))v$y-+i#AgnTv6m^Ra*@rUjZ{-CHL44SK}D7Ctfq6T@61C|h2!w( zgTb9K2d-XM@kxUu=?EQf!W4YA#E0B#A;>NHIvs;*TULv6AgcIK2qmyvL*6umyvY^z zz}=%KKzjM4?Yg+NdDqLE4}O4J+gzc}d=Rt^;`UNf_);Ny^#V@xY8z@ePw^@uqYWwD ztN9C|0(S%I31P_00oN{IpOoyj0hhOD1Hx$j02RamRLlbuVk$;#2@L}NwG9Z5{($Q1 zDL+`&G@u18?n(~*{hyqZ;&vGQ%cS{O4yiDChCVw1#=%brFG|ML(26Gp_Jtw+1H}w86KWHtk2F>kN(FeL24WB$c z$v<6!1wMX7qKVxk|F*mZnls=$a&Q=mWCYWTHar)BcnvPn+UCz@&q4$;8O1o{B|&IUOVGg`h%(gX={TJW)X|Cp2I zC4hQ=O-cgn0)yEj69U+bqXRoOx+&KM9(Zu6$tix5t(7!fY`NR^Fa=>qfo`Z_PMNd9 zr#>vC;P^NLq8{B-vyPttnot$Y(7_zr{+!mq-WAQngukXh$u#+&Kt zEoP;D`D148P0=Jk6fCkOh<*Z|#9;T#_Vy}w<10tx%VGAx*N@7Av{&8 zbooR``|z{6SldrOHQp8d%<)*bC#h+xNsr?o2r? znP}`B!3K{Nzm%^6(U&4+44q5;Y9sGp(S#~5XBn$Dr?Au@W~r0@$*e}=bJ=+UDS~w& z^nYoykrZd=6!k5i8I*CiM6gn9Cu{60YVrrgp{L4x#3Ue6u9uu5A&4VtsGTS52M~ew zz7k4&*yyN9{|W~dI5t=^t~%+pf`bF_p4DAXLuMTfHMq@mm2nrB@YCSfWTsj zNVH%qbZV%OV>};-d=wqhaEAAXI8+|@MkA%Nc|s`5g2D!`+d|MU(?m4L+M?aL|3FgW zN3P+vf!&}Scj7cR)254jbC{?cEmy{-hFcp?T9=szS!%!ZU<82d5ewses9}^rl71Gr z9Ku~T1%TAqRs}O%eG#nGc`N&{!TAp29Z;+iIu1>zL^r+hP5}Rd!rEW>U1#(YFEXv8 zY}EHThg zrbhkW`n zpw|e&QGbR)q*}i32u&0>Pchy8tsFoSO!YGw{`O&xvA7!1r0ChrAvGV}1m9fq_)!0o z8l_rPl2k`eA8y>td`%=@mnl+pp&d+BNR(+t+PQ6NKeh*kLkC?rpLI&&qO(jlD6wYP z+=;6AtMFt~0yY`1s>FJlJY4aQGj)nzay^w*yg7fEYEGgpB~mT}(jVQpQt)z~ zlRQw2iSyosK729G80)KOYZRF+U;J<5UGL8x#hYwcW5+nkJPxnSEuRMhsu!{cRVc~7 z95v7?4i2!yN0m0LRN>J%U|mF+4Jxhq+>DRsC)>^ziy9q-#)!zBJV*_QoF38X4~er1 z>n9S3RnC(eTtt?iNF72bcz1<2HK(`1QYMfJtqVjVXPoz5h;@BRL&cF; zL^||npV0(=l9$`p)|xzHe{z>vqLW6d(dCWNMO145O-r}XJac23@9{>yr)BQX?-s0& z@bBZ^HTi>4%C7`0EN1BeAqioJHH6JQY1@0$mH(WP#u}hnGZPSa(1{8guCJo{gZCsj zWXoZ+A8G!Q&utQU$lQ|XpKBQurLbZ9h}&#dsesdJAYQvHf}L~q8#?i%)K1sR%}l<2 znKK6H4MGH=o765TZEG1<@cOFce@`iIQzF)@j6qR6Q3`Ts_XsTuI$%A`0#`NsIgOee zR^#RW_y?Y`PJ;NmjN4th6h={e_CC!jQM#E`BSG&cQh8?ct% z{}lqog$F?O;@=G@y8(2#xo!n6C5Cknvn$IKxa=7+3O!np9ilW^E}+3K&`l`@CASFR zNk}?jZ@IJ?fF1Fr!*=eAr`4QkNNWXphQyU_5^LuNC1PiiBzpx9L(C215aKThxr*L^ zhew1|ak&MDg>IzLy`jp|_Ine-*S1or3DEH6rILr|wA|=tToLlh@rey~tx$loU-mg1 z`-9y}GoZ`6Io{QB@7IDXrP7jb{1L7q9dA5VXox_7ki}n!LQ?RP`UwiT5Ghk7LWE6Q zT;qjdwrt7P24NzX>2^%c&vOI+8pPn`i&N<64;`GL!(*iDO>Nu5SX@$w5?qy_S^U8t z)w}$E{PysCA1@9UFBCT62+7RhDI%pz860IeQ%4gWa9rrwIDKsVS#AoF+W_U4V7**_ z_Gn3`JHfK5qJ?_ajZg^6IZ`Q3cf&&dDdNzG;wi5KuHHM+w>a409Bkb8m1;SHowauH z%)0C8KUBZBDQEV`=gLTzm~B#n)Iq5q_(!$5S6+@-_1NBI1#B`~)ZPM8{gpesZQ*vQ z8NZWsqYKI{Uk0e?-UXB+8RxTPmTB5jCM$XC@1krY^wt1+eq0dDr_5SD%gocSJ$N>& z?qKLbv$*bIXp1>w&$QnoG^L1Z0E%O0B(=`FE;=mJxz_|-VJRG&NW1}%{lg^6A!_1G zdr&ut`AfN7`2)k0QVRpMr2A4Y*I5By5Ukf|OH5ack!|byr+F!CYTsEXPpkF5SQCoBX zVJ6Dx&J0{Htoo9W^Zv*mQLuM%-+rIKU!;Pb^db$9O5z`d=DQ)4y7Q zG7bbXYaC7MT9id9Zdy+$-E?x3B$QxytwtgK&7OCjnGD6l)&v*|bI#5|8KX}cWfz76Q6RMu`r|58nel2l{ zcO_PbKFPJ8-Ld_^0;gENe-3cbj#JZNPW{mteYsZGqP1z|j;j!X`|Ft^y(bc39OMxrsqx; zEm8f-&1`u21Wp+mNxs!Va5p;}G^dqWLA0NiRI5HUI@`%s6(X!}k!IGub-h)qJSEdU z7y>Qof;dz{t$hqhIQ}ycD|fkFFYUH8M8Sjo2?D=;^}z#fDA%w zjO}{b#*18VR}#z{7Q)R>nE~`WfkZLpO zfCFcRPaZ!(O@#l;R9@5=aXG|!m$MwHYghX zo8SawrjHD^*F?F2(Ek>Ri>2{J_kqev%dY|-NkAr`*FLVLChs`)TYc}`3Opvc>QT~k zH`_~y+)gJPU(7PzGWBxLYS9$@9MuP?k$d-3{-&ssvpn+Hu42JtWxce8_)2%Y;8i6t&B}VK&V)dhh(Qda~EvMsw8}wFZyM zwA@4B#GE!E&wvBdRj>23TYXK~d7%HvCwragcM$VE8;R8*n@4)k<398g{m{k4g`UgG z)U%qyt!W0ATb*^>A5gs`+|rzpzCmDuZ+AZrX|FjTtb`Urr1Q%M)k$SVenIo6iaR9Y z*o7G0=<^c$b5X#oJ#x>7?Mx4kYcx2gsVSfLop$TYF2Pn z3&gNoyl>Zz5$AGXQwdJLJr6gLvkW9rEg~LC5funH4#su2S&a>-L-m69)c&;}2eb-* z<2LDqer)N5o`SYufE@#9!THo0O@_tHuX@|AO&U|ChCyRX3`AN|n-{jnq{EkA`84pRo?AH@JKo!HE{1bEZqVpf$((>Sgv zz+)+sg>V>E9-Ee+j6^zrsJcS*87T>}n?+#+YiNY_C=@-lmQq9Wjl%GB=)s#wwe#n% zalT0B5h2>T4=iC=Y78Q@EEsqwR285sfH8iaK9LLw4C`j3@D{V|<=049i-HesF~qw%@}b&r&A*=zFYHfWAv|2O@{QMfEu|WtVA`=m*yvTlBOrz=e+5q{ z3A=#}JPGd*zn%}x#+l1kh%c+^jDwPcLm^Q7b@wb(+G4_70XLPVoK%*_*-*Bi`z@I|rDQ1M^j~kq+LxaW4=zTHqpt=~tv31{QBd{v0 zIcNSYcV%Y?1RvOq;=0ioI-!+J=xI226IGPhu}h298XzM*sUqBUYrBd_MsC#CqBLlN zI&^6qRb}ABWc@PQZJUHP*xcf)5ZaI~QI_+Dx=W;a)YJ zbB@9|j{LLa^_anWq>}hs-OuWQaTXM#u7C4PBiR8Y7gR1o)sNoYLQ)IP;4xSIL%tcS=qHOZ>3-m(^>i-8TW2@sS9A@dxr zk-}?ia!O!n^xIWBO?=B!CC}FajIh=p>yV%n+^)#Fx!kj17EOg9x zZAY#YZ;;kKf`U4O7Jb(sn#MNCW5oy-!5{Bf6;$-`U(#||s;Kl4e%`ekW@mU5=m3A& zYRY-+<=Qc^o}g~rm3;EgnVV#K2&Ym!q|wEDB!}R!J{lqplh~u+9BPL1ptc3no3Ssz zTf8KeG=(mIJh)2X`S<4*A$xHP`3Y`5k!Drk7#}WzeUj4X`ZBqj-M?Mw62#WSTF;_h zei7P8j~%63jUKS8(@CLcg-$xKG~#<)LSGOua4W~MPMT9UF@<(2SD0H@N5TOv0*l%~ zmio8QcFTd=A;}>vg2PUns4jq>mR1l*H>wmgqoF!bNn$IY%UmEzC6&Sp>GwBbgo!R7 z%aXCB3P3bh_zYuheFxa?R16Tn)d*_v4n3gxTQ=Y}y{}6F#^wPzDUU-ifQ<~mc$*%3 z*9pRuiDaibBdU3+cX(p_7})CXAV$|F_|B&1Oba?nmS?T%?IoZkC+Jv{`#Hkaq+0gp-V8f1TBpr zDD`Mhz~5H42S=*i_I?obTauX-bn+_X)ORl*_)?PY&ZjZ0W3iLh(C=m!aMuG5_ z0>Pt+=!PCaL5(4Y0b$-tpL%_xi_a#|ZLmeNPf{t{t#9HD;bJ(FG$-(MOvl=AiC2-ovn#Qz+Hv@7TfSFtVG{H_+x|f`xT)kg@dYqoP!)k+y|2f|WLqQN0xK=QQFSP0TFfKp`t{IUvg@e)~(_y)ry8 zxd_qEZHCLC27?anLhdqlrK2#_UAH$fKBY4_*eWaqMp!BEHFIf1f z1N`73b}t@g?~c=uxJ4(_=DX;p1ad!wTDw>`;8wJu(qE9s6;kcU834IQw|9=ig{&q6 z*TP<}?n6|kMHfIJ=yU_h6X4X_^i4d(|8s$0MS#nD(FJ%sOP*#OFV?W+Q~i35g>Z{U*w80#<%9NUlR z2f@#@p;zFEV5~$=!LNLTHy8)`SfrWN;XNx@V#qiGLl9yi<#)MOR`ja{9{C~zmCr#q z2UjKq?t6*anyNxF8a4^bGKoP0F}gu6iFmTt(f(mQvQ5+iNOa7KkjVDZR6OMqh3kBx zjtn8HbOSHKEs1tvRu=F-_cs8#M4uLeJ5iuWpnuD1IS!vOB3CQ1Z&aU`<0u3|pZ1-!cWtm&KL~%pN2Wi5wRTk?G;X?P3%;K+#fr>n{fZCI1PFRuKwG>(I zx+-MYAj4k>&b42CLJMd(M)CAY8*Cf3Ox3zWR*6qV!$?EbyNaov@7g)Zy))GZ8qjm) zDWqC>*|NObmRrTMzVmjp%pQ)E%&|tyyDN;mPgGGI6s*Ox1u{4NFdS@ar?~z)b>Ta- zEB=rqMAVB0BybwT8mofZs|=O)#FV(UK|5z#n8$Lf?~|ZwkC&|oN*r2Ms_JGGWH6hN zi0aV3YYpi~IN1$N+AMOrbM3N-+Es1U^o17b`I8n0K(Q^vglC$cl^(ohl3HY7Ls6|h z6q9ur=nV=1@kO!nW2bKa(ext@&&4#eOOv2*Ulh=SQ-jnemQL&8@R}#8R6Sv}AfAic zkNVI9x>A4X#0za5x^55hFrEEV{~wg@QwifX6B`R|)1`6zSHiY!J7?q0@W8yYOHnN^ zSq@Ojg;WEVtMT%=>L?*hMRO3dtOQh84waPW1lf>_1E+B(ien|UrE@tV5vC#S>+Mp+ zztRPpS!wO-xCPH!tN|t|3x{bUf!aK^%q&OLD*2gUUB_&^&%GLg_JGk};^y-_6+-mO zi&%?+o0k(A&eiD1a9y!K-%mpI+3x;ADm&N%iT+SZ@i7#$wu(8ppe6}!=Cca#k zADW%?0{4nmzQ=c^h=nbg_U0_^GqU2J%(`!kAcb>Nj~r^xu=G+>0DA{KNlm=)aRcH% zWpuva_QwU@A<)~U25 z8_x-sb9l^aPLnfGyY>{d>OoYBSK9=ZON9jJhDUsj#(mt4-Q_SPWLEHNxD=LUS3TYu zthB34a_u7!6U};%{eGv|*ef?ECltsAZ zx^0CBs)0Ga5zAR&rxWlZJUv$a%$O^zxz*}&ej`hm%yk{ROvF`)71nD5)>ZPnM^Du| z{1L?5C9l?+Z+jQcMj=OGp)($M2GGhu!q$%5p z!P#OM`aYLmwwC#;%o4K#?V84-Xklw!6QsmyX1iN91C!zRW5brUP*#u{ZYHk-fvO9I zk$a({MIrCd-n`ZSzzSQvSr~S7>8*tC`29aAV)p}ujiJ-8du&*YhqiTe_=xLZ^jRNb&7}4@PCFV2-E-%28gCO^DbJ<`opO#SvLnk6Q!6)S-37?uPq8E{pl3fI1a1}RuPsJEdRn!FyRgt$+ z!Y{~sUwAx?>kVwR=le2$8mqKs`?ojPktemPs}?~w1%ez{$XXYZappl2Ul%z5 z^{0iOY9wixk~cx;9D%cu%Qaff=YmNkV5aFjJZ;h9{BTw?Z;;B24b6oMiILJ3wwOAh zhjGXg`hqe3rTFy9$dPKbVl7uVrGhAIZW^=>HV#I?v>bV>(_9en900eW1ddiA3bGtz z+n0k`Ry*t}ROMly=}-f7f%p=eeutqjnHh6K3`J&BNr!9ClKnl<$UQ?}hZH@qKtqNa z4-(3+Jlu;^ofm}0pxc1cfIiq%6mQMr;S+tf=C4jSmwS4Z7-SJaV9}vRu%E2n7H+lk z>axYfaFMb=k#)2QpoEb;a2SjIK!%+!<>mx0xH)*ODzf}k3S+CO3o%@Lj#OEhgWH+5 z468m;%W6V9>tkyZLgmrgYZYo0Q0qFSq0J~c7%Ny1oZ=w$2t?dt$bVAdCYd}WwuM?e zDcUreiJ=%Ta6iI8f2as_C_#Agc5A*sO>>BvB=pJo9%E?~3_xxTqeU;)EeCa*+2 zjW%)z(B+iq@QAKkJ?PNTdFPep4tw6U+7bw0H_i>0IVC`#N>avjoU#Mdd@x2syk{`D zQ{sswn}MQ4yXT*U`A15}GWYr_b9DY;JdX*fjHxl~ic(;eEe3+6_m(`sK=p#-=C1ro zJr;9p*rk?5@2wf37X?(aVafqM2N4`RV2p)|xV21o{Z;rg1Q3KYPw2p8L^;>mtH4l2 z`DKwsbMbG7LFAKT_?taWh?V%<^Gc2r6(AHg2tA|6bt-#jJ;SA*503IlXoh1lAXV%q zjJ7l4dE~L!7vR;`q`VQCWtSrp`x7S6=2`G`o*4^-8VA`6 z*c8xoe|zOGAr}tPe!Y}=acT^B506F)?D3&`czpbm|DcPCjuWXuWb;qU#V_cNfq+yY zFU0Jh_G6H!2dH^29(bOV-IiW_5ANZ27`=@vmB>JJ-9>ZQM#7~>Xb8ZJfye8T50VsY z!wH9jbSZD34ZUwb&6fp=4{3eew!T}}c++|M(T@M z&KAQsi?Jo0uzAOSq%{6g5|@GqREoC<;9Xt%{+Qjiz*D=vW095S*HWIQg+Mt{ciZhe z+)ZPS|7KxM@_2Xp?c@FiG!wHWa?ALDF}a~9l1Hzht;M7?Ix8)#N0tlLNtQT?k}$(I zV=-1y08#y`?Q8HXQo5Vsqn7Nc#j%M$c&-j=fs0jebn<>eXi1?5ZFjWBi+srgr9`;= znf-7SU;Qh$Nm9dsBG-|^42~>n3XZv?RCES<#_nR>p@R|K#vB;qdYRaHjr2dqrF`5Y zQd8Q5?#^+^pXw9*XP2<|SDOtykY0}K zg2gkz?=O9FLxHouVq3Lqwx$1x94?ZPpr04YQxnd$LzZ|q>MOM5P(Up({1i+m%moC+ z-bqi%V%#{nXtt6)WyO*W6f!IsI|fllb}`9j8QlePrX;;2mh}9d3>+X#NL)rbCX3=nmzN5fgNR z0%}Q$gsGI7o(#w2_p^7)WP}?v%0fdn)Nl8-|e{BtLH?WeRj5b&PU zQq^fy;+7|(61U)~p`{Q^Jyar9*y3d^JVgTUF@%<2XnT!m4kPfM)RFfPJ*x`_*9IZ6#uUg}~g75+2N6fSrtF~b53aTR8pfao8xoU7nut!fy6otvy5|&CR zKnVq4t*|4OfOYj`ni8v~#I8FWI~}tacDMvH?=L0QmX~$q^|bO+V6)lvlF4NPN9HgC zN#l(A_TKk^sB!lUUEQIQV8WSLXsH(q_C#JR7@k@|V}b+~0F37RuC5qJP+u8xeo!=~ zlOI-w-ZL9ero0&zhOWS@H_YjVT}fbA89k&1(|O0%3_LCqU_s-EoB?NBcL12S?zDM; zRFph*UM|<~aC7-rdla%$TW?J^>P!9fA_D_>m5qTpE5fk!#e)6z#e!!rpvex&xA*xM zE~kKu< zc8aOVSP`+R3Q7Zp7TMDd(~U!k2hWs8r2&J8m{o^n1tqf6jOYt`^RP0MCZs3CX11eH zVP;P-vnvP|hB-H=phNr;4=2c(MRissf4Q;~V9xki*;wReS$7h~+bqfs!LG@w#l>m4 z_%7j>=nJ_nx#;5V@&`T96}o)|5cx4}suH*SRwZtoz=T|Ng_w}5HV)8?DnWTxuE`iu znv7)NS%G?~Fia>WHwd}2$Gm~jUqXEzV$yf$J!%=N zTkW~1YlZ#6yHi>MJCzLKo#$qKQst%{mFkYl^=2voM+rKbEZATtTM)LdOl_oK?)6awXv-<_7jbLrCVU-U;*bH`D)S7y9rYkmsQ2}q~f~f zTu~*KT59V`{j^eFN$M!Sgahxt47D0lh7l{vlwtH{pU$G!-6H~W#(CjaR(GqApH}jP!Z;0Kt~ct-?dj!qwZXy*h1U(4jGdZfmriAoQdiAOr*f5r=@f2pMZDs# zSTWU^MedzHiz&|=1cS(vDT;6e2Ryjns?i`?GAWJ3c?5SgYs=XB?;HtA2t z``-WdzSaMMVd&}zOfpP7Oe-%eN>Gpsb#Z`Z)aC+_vMVWYQ;l3`PDk+IO@~sPEd*Zn z>6EigmWv52jKQC^b*H_1>fnHmL2Pc;7t{~8rw=Er#XNCKNf`U$sD9@n!*lZ&e5O!g)*a#2>jTr2dd7%yR=3#aiJU$yjEU+FN5 zqA$95bkQZlbg^W&(!DjUdk@dh(ec~Z*YQ(+De3pLa}4uD<`@P~-K4gt4LV%1VJ9A+ zbqA*U;**hg;fOT{B=>@hDQ8Dc51NtHJ|bEp05uB0T48zc-a`W3N6@T4F;yR7^(UqJ zlX6vpq4yV)YETM6EHo@`#M1q;ro5g~URT{Ks>bp&cp1HLIj*wKrke#Z%VW0U6r!O_Kw%*`H|q~oczY7wt~OHWy(XhsaF`eD z2L|GOFVUN*X#Al+#B%-i53yXFhQ$3UAAe(1Mfer2@E3=1RnhxssKI5pzg&8a@CjlV z3Ws-3yIw04^~J8=`eN6`$1HMHHAp6rOE@LPX4=7 zgN1SN<8blCb1qHh!mHQJdf_6h<3`vR$NrtBe4F2d zEHE%CfPk|dLhyEBY34JKP8iM@F_21|=`+v4+nID4# zWeM+HU95)9EO-w<3tmRR!t8|)Gcqv34>Q7Ov?n5S(S8c3#yXUNp$_;oy)*!Q0Dk~| z{-x!)Vy^?w75ga?K1lpfIq|c02o1_rfo!TKZk$ZqTu+*as2@hEG9=ay(;i9GZ99?X zY*9W?l3EL7^pGFD=eC&U^0L+k`l`LL)J+bHV#EGq)}HLCr;~S?j01_xzuO%R1v8_u zJJ69B3TApV(klC+Xtj?ZHbLWvbhC5Ts6@&}^8;dcoQ=h-9c(nL53Snc+u5)wG;9eC z3(Eo1fs1)s%AN8{J(!>+%k&fnbESS%B9n;h;8E9+cu%`$^+aOIKoNVsGyekbOl(*C zua*QiWLNuMie{v$)tsXmE)UNir+7swUo2-6xm)tvsN&GLm zOt?pS5{v!zBo?1w=pB+lW1l%5%kDYb52lV!!3NVqFM?*&{N4 zZMevg_Z|giuo)wFBJE>t^0b>s8V5k|sx_;YKRM&FD~y*vS*vuje1q03t6HqqD_iwQ zeX)z*`eGLtNZel{L)l~;dbC%{o1wrj@j!thX06wJz4wv@Y!bhID>F4QnNeW&Q=y@3 z+UkS?yOMwc2{&Z0s2OM8EPD0a+nG1@7fU3PYbn#qKmM0Q)0O!3+M;wNe*JbOekI_& zXTSn#cC7#I(4)TKYzRUChn4wNM=*K7bTpl%DybmL^+bX!*PZf9HE_UubkqWl2#|2^ zA^S!tgBU+;)w6}k8SSAXon&<+wsELcPPz9Z7 znu%ktZBl1`Ofxy@%nZ;0z8=q>fn+Y>Z1C7b=E)aEWhx`&`BEW8qC!#ok}O!Wn1T#4lvwC9fh&hlMgchRb*?%R(8Aw{Tq! zM|^cy_`+2A09~d%cR@g!n4HbQzWbkOoWLrk%sr)=j-uo5*MzTXjxr0AVLO2=s@1&qd}&+ zw47X8*OZo3I8BMQu)`%CA38s7eBAu_FpZh3G4s`Ab~2f-uo>%u)?h*=6eW|$qawQZ z-l}K(3@T&fM*t)_jY|?-0DuHzNrLyjhWLrAfAsNKwSAjhOtmxV2l{c0vaESMolPe4 zC_WlhS=CRq0`EFJc=P^2s*-{V3Dyzw6*{)?!iQhtA?bWkG_v%<$aQU*>og{hMts~Q zvWU03X)y}>ajZ+K8TrYy!1~9wI2o!n{K@-`BJV5Gme;Oqz{`Z>flL8;Ble0ak@pfS z%%nXqv;=G)AxMtTDJO>)3atN21f_OD**S6AIk~!M!B;!^>T5|I+Y)GZszm#cy`SQiEFd2Fcqgt97-J`uwc*oiS$n4gi}8j!9Aybe6#Ld zEbasii-=D26v_8{dmrs_maE$4l+Y7F7|s!_XVU1cfx6Kmw1#<060p^ z*cAFHWmf$lDwOJMSeE*U=M0zveDBbCzNKl~}ox!jzJWi%RDUJ%iD6D6x z#sINs-O2%JsDfRb_~x^;lhIya^XP&x$pq(X6~%P!+XlN_vyT||8w6QCL*yn ziqbyIYmK0^&+=Q^XAONlo(=1GR5mrhIv&wtlPF)Fj;1ZFiE88U=bVJI*I zFZB}$K0x^KQdeKJx+qmi?{vpRD>?VR7Ut4y@{=et=bF zN~$C^c4W2R(bo#AB3D&l)#LOBtB#QBYHV(`r$n1%ij>AYX;jo&{_N3@?-;P`dpDKD z7Wnzz@)N#?e(ERfITOWmutrj;EA!iCiNOTgN*VK=BQbL!n+dd)a;1@RX$P6PQ;BS0 zVagbq=b)YA1QxWwFhVAWExGc9-ORoBPcmLD;y>}?|AdR^gYfc&aqnrMw+HsX*KZKW z9l~#(C7)Tue}X3Zpu8fgh%4L;zC@R}3PB{UnPyU*PX`2N6Pa|?;7HCMkwr%NvblOy zPv|LoOSQq`R@-eb)Xgd zhQ(%?gUdVza!G0$nM8Ju6Ij5^n;zIVcMDU7(Pmj5*(^YNo`cKf&eNGIn@(h&RJ>y_ z?WXQ*$!F*>fOAsQ$aFTFNO_MPbmmBN@C7nxgw4#C$R8W0C50UWIR|f`GH2#3N~F=5 zBahzy9b}-asNBF5l#`z`#)Z%63VY7jF(ym%wgQ_Ae3Du1PG=I z1ao~F!dwGBcBCacA5Ve!U1fewGQaEn!=Ys7Unw2m)#pUxy8?Y!-qa;!i0%i!L|jr< zu&b?H?Qiy!qj{B)PY(2CsS?s7!;LD(f(!P4y)!LsE>qozuh#3k~s z$qU0xIq`1iw>k0dhv5Do848G3H4#(QnF>MR1Jh3nGx;XdqL_Ht7KMiLO?5O@NQGjQ z1PUaAGOJTsB3pqEG=%H>A|Y(w6A6*~p2)VyLX5`Z>LDYu8+xEaD4dj_Acn?jtc&GU z+7x-OO>No9miL`JWCKgCrV_bq(pe>)>AlxL4oGp<`(J{YG&=Qvg6X=AnBM;r=IS1* zk<5>bP5YSVFLq$|KqDqa(fL;j1c?(Z0}F{03(0jyg5@#M01_hi@mmL`_nrpK713OT zm;d&OxR2;!#Uj#0Z&Cr3Ckmn!k*jXMP(1rU-g^eLXkoLYJZ<T$sM;3k_o>0O2dX-`TurzSDYV5%|QsIU{2(f$*)JDmR~ zwkGsdh(k)fE8*1p4NCP^V^>icoplA%h^bE(g;Vc0CRQBP##DpH3&)EWd*7)U7pzf` zm=*ui+G4dQVvE)96wM2^86;)}ivrt~q$l)l!+>`xb0gc0sQL7b25TFFw`$9-s@$RM zfh46#XEnA2XbCq;xM71~hfpF}wMbJX)>u2xaq2+ls??;SL53=%Rby&a77*48FOzWE z0!IrSCABCYs0bG;2pB-jKtpZ1Ac9sELA$QdoK|Sp5gK*~1s+LYOG7W2TPoSC0FDQWDg@AE*vf$vKl*6?;m(oyLPhctE|Sl(O_*yntqVu z10A?p6dav#^mbSq_C$kWeV{QkD~^d4W!JlmG^pNzfKbH`^{$+r)J`Wb_0ICa`a!F) zD)l}yg`+#ST?9)t>Wva@Rw5xS48)pbDvb`w!L%2oHF|1fGHB}a;m znk28>NAs3b(t0@8W` zW&**ZCtA~37*iNOVEi~vd98g$3IYWXGPDgcv}%hf=K|1Ht!eCy3OmtQ3kySmVAW(( z3hI#yuBl11Wrd_@O_qZSx)QTtw}Of&Efy6+e|7cdp_Afj99B@A+4n?nY~K^v)?`sK zG-tMh06GDsxmnaZ1KNZs1|Ug)mCc!Y3^@Cu=<}G~|B)9mn(u0*tg&Pslins}emo+R zR9U5xp42QcxxM#UChL_|B4gwoL!m;-s z7=C8hp$hFMp?y8EqgHmWtL2fN@Je(b0tcUtU0m5A4`QH*idAAv)kla4p_LgVCddy0 z0)|}E``nD`SX-=qwJkj?0I0TD?X|b5Emr%j zEmr>=OtuC$FoDQlM&X##&|l2Y6+XKPyxIYZ>IXG^_(f94G{(3rh3CPx7;DUv!#E3c zv&V!4GvJqauz_#(jDGf|y+@_7#!?@~Sn5VCPUJC{J-zoT=5O0bN~DsW)Rf6!+Vs<& z!2|ZtkD)o>$J_+a0|^Qw6_C_njQVc=m9oh>v2a0`!;UZ|0R+e9f}jh;dmqL03F6ri zDzOtZ^dT0}Ud8KxNh%glnSn>mp#|mNHF6k5PNv>m-rlFnQK()saKL zH7;;CZt23s7y2$<7PsCFzwwA~j7{`3jGGWc{27kVY`ASn!%J%Tj<|wF3KTQEw){!j z@+Vzk2|n@)U-%4%9dSex7DW^@e72O~F%&WzQHJ9(n2qS^8n5vZu_X+*!SH$+0>yh{ zgW`pI6C!WsLoP(I!d)czE2JR>jOd!VaF>i2BYIv54NUJ{rP%=ajL%DawZp$q#$h9C{_ha!(NIS3j7B^tR++kD zYQuv>D{}=jFEER#`ny21BEr8?6|q$mvB^~AS~E*t*=#BBuEaV4??XYSA)5~|BBt8q zWvQPf)$5I=fG+H>mSKxhD7sO*qDUaZwvSLyP{52To`9+9@gXYuj2PiTk>$$LJ()F# zBdFsf8l#7hw04^)tcCSUJaC{SyKLr68VtQismIwX(|Zragm2!P`+b>e!0|?>Da9=B}nlS&+oA*MTI4RZ}3i!?`o8D-K+EfxwmNN(@F z4n9Mpuog!I3>? z^5MN#xoQ8$y@4z`(dkud zO*B4hjVi-qW(ml~`}2YPqUQlWkT2`7UgYt6Snu}tu-+Xt+DxWH;!0qN1u|aT^GM!h zorGh#h~qQ-g^oqn8*_4EnAaYoIWf#{b7Gk82FeW+YrLUxzEsp@V~HlQ|c*tj8sVTttK!H5+_jrD^XJ`h1M2u%ach!f4u?7vdB zCY`&gb6-h-ki5WhW0QO&{V*CrZ?h;7$QsO|CQ{Cpu)%!Y#rhtaLGXpFYB+&O0H*tA zG>i}=iG{*V`&s4BzBD*r>TvdTd!LK=eTsCEcJY=yyTX$ctRJDGO!T}}QMIb6YPzF~ z$IG|Ib5yxFjGkXt13%x#*YYc6ZD~PBP~ zvB;J6SsIE25E3sau|T9BeOdkJF4dF(9+Ax^oR#^ha9B7j*K37Q9G2_1I4qYa##CC` zu#3Y*9a7JI6%cQo)Faa@mc3Sw@`}fNhOrrEo%s;*2$=#_181&$X$0_8v|L$)r25t3 zcc?e3N!(lZypf@7W>yml?11cEu$B6X=KA6Mm~e!mI5;>p+Dujx8JTvonJ055QpwJf zoIbf{Y;&QxK!PYwlIO}zd=*&$RqvOH`T6Q1t^Z^X#v`f_UHWRJw@5~av;Jb~by7|W zWq5rf%6gr2g)tn4`&*3|(|xcAE7FK{(VM8$#kyGQC@{U2T^MmzCuO+5xroJCkyk zm3&8CM>=^rjcDX8Nuv@8sq*yR1I57i=G+(;9tU#aaaZ_PtCimh|EdbNmL{I`U{F&U5evB7a(tDf|vHWHU=vK`+}v$p z2m@13eV`xLMPNVv6@h&it_bb3R7F@zFB(fP@xZE6lbC;LSxc@<)za3eO1RjT+?T4Q zm2rTiI@n2fPi@%AhOaUho0HCBFKci74LJUWpHD%Fcfpta6O>G#G5jdo9v`A&i69@M z5+qWzc2wCAn)$MZGxu`QEfxz6-evF#RNy^C9lu_OI)34)5AZJ2+e2t`;+-dw6Yo@} zroK=-qo;Q9`}Rx`nZ-YZf1L8gE}n=lcA=-{sg?Mxi_FBa*I_1(;VB+%y-9N}uac_4 zdkPavD8Jg!k_ynPv?(mCfGDw}M311QoE30#;OMZ;qJWl!WV^B{#tOX8t`&G!(K)H; zuJ?|+hmS`NcFKi_?Kvpb5=$TI-G&zPDycu@E4hHlgaaAF21+cESXtW2C4%s?FungO zLqaBG{M+`6q2Rs$|A&)zn>+-zV$6$PU&sNN@6zj{uIn6WUGoC)x-s%orEtLmAm0QO zB-53-%HNBW_KRTI;HejRFPYMUz6!qq?>~;|{l782|2C%gpNQ%GFEZnM$ooHI#;u+| zgCK78v~oV+<_zPS55ymaU*-exhoS4_18&aH-FZM&GgRF?psE>`$kd>!8T#lN+?*jS z*WjKEOCsQ9!x{m+_e39wpQU)v{a{}wrT)so1;Gr{&^-%Iguo|dw~js%C@os>xN za(=gcsi9mvoArPqnCV>DG6aYXN z6bc0dBC%K?5*w?_*aH-R5?^9Gay~?!$>VT1jHhT4r4%v<8DfkvNM;5|1E7Ea^&R=q zHo(n2P)q@s76$;FIiS@6SP6i}Zlx5|>~4=8gW7O5-hkP5B#c}Qd7U<}zNM>#x0wIwj_gfVqA3Z5}QG2vtH<;n~&Os3;X6<}Cl z7Uqh^WKp_va`@EO$0 zX~4f20UDcTqfV|)y=V?S4KToOS<OF{P=(hQ66qdLh?YvAPuQS~a)oaY0#`%wK zShQtSnM1vYpb^CyPzLYTj{XhqtnvBP9M0ZD)tS%iF(U~vykr1xhn@L#ionxQ8Gp7u zE^bDP#Akw&-Y}FK^CJ@$$x15o&k5M z73#csjZ3}H0b})}DxV2yE(XK*)!dMu(ef0hHu8#s;mp+RKYtE6Z@Gbkacc0BQ|JO4 zb^`+8EclK|f&O{z1Hs)thI9DTjAYeU;}=qCdUC0=^W8Gn80+nN0gsWH%{hlS{lc4E zMUSb`$*omB#cK^9aAute1_a$Nklaz_Gk24YXeDz|m`5!gaxI$fnNKgz*A%O+EbU8F zLArz2cB0*^g1lE>ax*>pE!9VHP51(dRx|1-FdKTu6R`0nki&df)0*|AiZo!RU`jdR z3$?vByWfO1tKOCTGDRY1n6R+N2TwDL$fHv;iQpN zvWGvHTBUS7Us`vad_Ug9_d1iS{JO4s}cgu*>w)oLfjv}RDM0xU`&b- z%0mC801^6Ppe~g&S)GpJi4oKOmWeJ7%l%3c_A%>&OZO34>p&9IlE;aKk#b@Vi6}*b`_eS> z*1yM*uwP(rDU7L7vfaE&|{U@{ajxLx`6BnutUK#+J<#v7zgRGEV}LE7y1E# zSNAXg($W>0^1v7Rf2jT<3;mJk3)hU}-4)1o>y@3q$$)`@7hZkMJofIkQHMqmLAA39 zG=XA)#o+vf84?a2oKPS+ll!FC2HqZ(9i_kPG+2D(U39&DltB11#Nq2eCE5@%LQC#o z%^m9=r4@L=7=4DdtC=@<-+0xK)NCY_9=e+sp^%2}n-Ln6PcRiLc$x^D+#jrT2<+X9 z+z%1F4gB|o+H>56fffMvvrv9h5kxT!opxXC&6OyX*~%G+^Pn>$ZAG~Hr$$4&oOAJ; z80{UYerRrNH>k88-hZOush&AclfAkzdeee%K)Jh2_U5h>eT7Z*?FK^end5yke2DH{ zVs_h)(Fqd|a+SAl%H1|3uk2n|2wQb3nc7UXW6?-ZYbA!mYpARs$>Ea>f{q>XXEw|HR^i{MLy4vU8nEF;W~^- zfQ|vGrNoL;hV#=N5x2?nGNw3%nlkP{c<}a znZUeqR@_$xt?|tRc8yXaXc?!QPFim^BFV8%3lazRMaznku`0%GDt$t(M3*VngCxOL zg7-sZB^v_{G{Sc1EIQX(Ihz1m2+zOlJ>~6C&?U#$st(OjqykURuwdH;=dmXcBv%U{ zYkoRvg$18}{H+46a^>VrgzxTo62gi4b5bLbfI1Oj*cUAC;{z)C6b8Eaoo- zJSpvSd7fc@Y#K`GE3`g;_?C*P5R@+Vj2)Di0~Jn|_VpRZ!uBL^+?pq_qNJLqA6||L z)r!*q0E+&9dw)+tv}K~)DOTipz6V7OxYEaA(*l`;s6LSR$!Mzw%Y{cKSXTM44dce(3?jGE@!WUwRB`WCj^3gl@tDRn^#K|37b znOS!yYplJ8UglYrgM#Igt}))h48Y>d!nnk40Smw=h(Z6T3pt!S(u)$y`eK3e2aHP* zx84MI=gNKbJR^@WTQDiCy;vB8XFT(0`^B(NMYWC&sySg_QgVLP5iGV@F06X`W>O%! zmSY|_-A*BA4WTbFx3kB`GwFgk@*%F90Hsa%EeId%3PTM(#DHYWMh7N?e$wYUs`dg~ zz2LV-msJqaP0&4rOs+k}d+@xXq_ss#dpFw9;MxnFU-16CRBXXv&9;!!dS1ch#bkdW zVyQ?_=V+iR+#M67E+eh}^8%FazGXL@wdu?F5}L)ij}1GN+^Z0v2}_VjHzNbe0g7GV zLQ0@LtF}5iv81_4qa|y8x&Hfv(NfCbz2F1GB&VJ41wiF8GxPg4v)UK76zdzn3+(lJ zebg=tg0eeaFLvSHTRXG_N+ z%t_Pwk#*azaa@*b$sC3&(dAG~0C^|{6$&{!dC1zbcBI;@_FPy?h$Nno@l$!7O!42* zMg3B&e-&|XjWq6f<5PWsU8msa+Xxh!O;)yTny!VNDJ&e?u?IC^nmLa;cM>aJJi-;F zj}~2nHs8XD#-J05IL5@aQ8=`M9rkQ{6+w=VS)CR|T^(A*m(%qY2D~pnZ_yM2eKi(L z@8v;C&B_IHhc;Rm;20kV6Q%bW#)#`_31r$-WG7L&#tlb5?k{MRZlyqlF!WQlFFj(J zrC@43(>YSwE%RZ+Hu5-*^10hYpppiBh);oPm{3L{d_$ld^YfsR2`n$#K&dU)(|vT; zPSq(=PQ7?2>CESxl2A35BRCWlx#L}X=b)DG%(+Rc3NZblRrKUu(!W93QXeJPljU4H z`Xr47QD-5Qk{1i~F-4w&GM__@SRkBPc!8?i&9}@l?RhF|?RV$@{6G4;#}u+B;>z(- zVCmd6wwYJGgNNB`ug)1;xBzf>bVjaJlC&F5GtFWM@ zRklc;ZR>0{A+eqTld50jmNw0kgfH13@ z2<`Zeh|8ibqr7r{WWghOBO??HnuH`s(=-SouZX(I%Y`%&v`djo^!fX^Kusj#ZgR&J z!{~3*^dw-BJu&tNQB3kcE0YKpB%=(5mkU-)r|<~zB*F%#iMm`ColH$CcWrlQEa?6y zV(*hLLGB@5tkXD|#4elfFE9$!Sj9?adCtdhxjc*}I!xU5r_mI@nJK>YG0ZHV;S?O3 zY64IeE4(}*;rwJ!_>oC%{zmsu0Bbam#dD!qi(nLre{R5X60G5@EQ^f#U?d?V0(=VV zhWYd2_dF!lR0e*SNcR#$o!lE^@7SsM7kDtTzXM}-mqJNmIM@1v?Jb}&C|*~f_f#9p ztcDOs`H>jN3nwA&QiDHrRR|s(`-kXZZxAtG)5Q3rGf4B2r=)sB%(9=}y3)n`B@uxp za1@fJyQ2{iIezEK3!+bMBk`tBoX4GbJRFZiX+|wYk>u_23N-fLcEORBViPa>pxz_>W?N&`NtKK+x2D?_d<( zsOnzl;IH#>Aoxa4Ow)X!^4X-JPS%gvogV{sZazYx@g`vDMJXb;-JGh zOwU(GCN#BUAN%gcW|Rz|=i*dSz#lD>OxRlFyVV+==ztWoy}^vb=O$PbkaOh~t4W>h zv5M{s7VIPx?J)pclyDAg6viGP;!o2sMg_OU%`oG(50!qVD-x*E2aQWuQfu_Xs8Gtn z*d#=-aBrx-8YwW~F4`UbHhk}_Zzx!i#u+S2OXagHS<~{klCfE|mL6wBulBS+mc~B3 zKvTH@LC4rXX8km@ue%pmcd^L7ej`YT`%q(sb8)DFFlgdDtbLQ6PL%AI(Yt1^Bcp&G zpcCJZHb&s;|LNRo;iNb{uOZ#CI2s%(k2j2LLN1^7z9LAjq>MFwyaLIN{HEXwG8o{O zOcKhY(qz$UK41~AiT)vIsDCL;lw6tI@X3T``a}ruWJiWuCJ?~u$3`GSqERoWa3LSI znHAjeCGPk=35Ws_5*90!d=$R>0Q1vK`*}%7B^;?0_mLMMAW_#zo#M;_v+hI|)d855 z_gdtHv=-&M8>9klY1;%3p5+!E!%wHcRv| z57LIRPv)^qKzOMMyHEg8`fg{%z_cCw*WqO!qvf(PRx>pGOnQ9ApZhHI%Y+v&>k2c3 zNq0b^JI9jTDOS89hHR*vToULX@5uMrLD*(~4kq;487=f=Vs(u*C$7w-2pRE-g0$EI zsI_QMR0LF((g{wZ(SONcr%)Jp>93Q_i4vjy9%L4xRF&91>F_amq7f6_i;h*}C_;_~ z682hWJ$BnJH5dkh;99d{05v#aah!Tozpwt|^vy^BU;HSadZlq8J^v)f!Y(p)hGGCIJz0*8YrKo^BpPR|&n z>d1;*Cq_w6gt^#{k_28b%>`)b>dI3%_9``2VGE0zE~Pd$$W0J~lMKc??XW=Vp^-WR)hfDa4tijdi@yTUiCSnh2#QBaa;g`v7KJ@Re}fn8F4naiCs9KaMMPJ zWA03GQ{HQ(io%eIhHF0}P$x6k{_V?f5l2ImEi^ZvLX4gQQ`3FqoT@rRE~u_mehy_3 z7NHAR;I3-myBf#8!i{7V&9+GvfhRSZQg1;TMjpWTO`{tjxs3I1;88m=``1 z&~H7X25eZ_F118v+1ujsolQ2Pej~3xFAynnWRWxlQ)?N5^BLDC`nUC1ky27#OUFe) z#XH(>4)`*&ooO0?-k7F3j-NdZXH37j^%R2f9@*u`Do(d&DnDyg4430rn3XVm*>|wP zT*+*h0F(vVU*Kf*FkP&aa9l(!p<9GuJBWI<W#>#%B~?0JibK)UuZd2}CFh9#)mwC!tl~Vd z_meB2^biahM_bWNYNRHZ0IR@$eWqx#p}0KoTqZ z$FKJM`2B-#1h68q*8SQ^?gz3GPVE-8q~SH7V~!{6HXw0NPnFx**SvDpbSgVteFWZwA7U6QQtA>4Vbvh)if>2<#M-Il3>90GvRYg$Po~^$toSxNno%G7y{`D zelXC4KQ9dIY59=m(u+Q~3PX=kv0KkL&zL@3Tfhj%1iL*|X>u2%?*1!ETZVA+A$TbF z3DZ^sL5dP8d$ws4x+Z1uDzeRWA|tAW1wH)X(nzszQ%$%8olmpPs$bUB+hPuuf4~_9h)X zG-pboxIjc~b{H~^;`IspEZ$Yfu(Se0)YZt}c0@VXDT)1#ka!DW`Yv;PHEd04{e{9% zSMaW!u?Z;Dj&>AbBx=c9@%J&Y)sFA>K^^{$s%#NR0q&a*US%3xWg5{7d1sreop_CZ=i{{H-WQ!OeSt0DfHUIS0aIZiCIZ(O+I zHp=~$4+ks+<~wC}w@@V*#y2fhXU#OVUEYQC0tjL;0ILTNF}xeVM;%_`y61Q=-HTU> zxyQO%t>WmqkrLFrBo++jGYMF@R>QK# z?sxCwAQSBm=EnDwB%F2F-gbCq#zn3Op&LN%j;HvoxbSUVlGHnz`i#+UrB*ntE|!(;%0Clj+0zO+~Bbm0hGh7x}q&gFeXj+jyX@C_%Wl~$P) zu#~_ugrN$oC0a{wOrJeSd()+)HEJG_?*BNx1h`3_$#dx?`w?o%whY}|5lsYxJ7Bu;8nsX!abas4 zKFmm`hH7;f**zDAkh*keQ%OT&{+1Qv>{wzi41g4p@1531wzTH`OP(D+g(!U-EE}^{ z{MS}bZ1#s5`7Wi1Ul{ooc=OVDk4M`okKmKuj?jR15oD`@SxrDguIRav3YNVted1JZ zq)W`&KI_1!|_x1BHb(p15xHb^*`;_1_dka5MqO zu|lKp4ldL5nZE!1ao{Vg*YxqEhVp2#-l6t!22$j%PNd076huvq&-w(D1p z4d4}n0Qi<-UYbF*unyHd=T#f`vo^qMxnp0L^FY}fN)IHtM&Ti*WhuTtVR0xoV(A$> z{3uxk^BeoCSb84DVNxo9T+$}5t{zaH6`x}K#Fem*P1X0M0;@1*Adx{fS?&e3>7y+r zyZHRPh6RQHtPG|>602PNc@BG#*$xi8BMVO1BBnMaf=_*9I7^VAUxvCigol*sv#?he zbrQ)OBZfa@i4GR9&SXE!RJci%Tp%yNSI=Y=OFKC z>oZT_VY@wp zLh{4->fsb6#0WZOhd7k?h?VOl2p<_Ie&N zcnKRhL55fISM|^qOv3{tI*);(*f_CN;M$KlpF`NUq-Yp0-Pe5RZ3f*41zk7i1F|RL z8(~7a3M%4G)MH;}01u8QdQgo$04T?(r_2CM&%Bs!1?@RB&m=45PKblXhtX?+L2M1| z&|G{TbP|rDY?n7HcARuhWBeqx4f(cSy%2jD?+Uc4P@(}4ZYc7Za;gd|vhsjMKG4X- z|1#Z=nK7G=CN%j~jPXCbjK|!Z66HjM>n(i~5fGKG@Kw(q?~fd8cXGdBnL>*Bl_wGD zTcn#DUS>2OcxYp@7tDjrgtk9&6+!7BbT70oLu!I$*fTW|^#qIfdWoCR4`s3p2S0QsS`I=(6I zNpEs^8b);#GVH%B_h~&27>va{FBoD1d#S z_9Dq@SWmQBo73RENO}-%J|nB_a9MN%daLUwe6b@+pu9jXG~y)cnTAF<^|tamBt4rrHk7C<$VKoog+pX_b@jHaNj;%jI1LQ@<>!xRzr7i zla{NAU-30R=xCG{7V0>*uBMT%A+ddIYOVI4g03Uc;uN*r;?{plo$wy(NF&`$4sLh< z7;ULmw;;hAxxh|ILnMjA>Vl(ftMI%f3XheQ9f-qfp-? zTn@jAe!{Q9Gy3mT=DdeWxD&NVvl2HH-#UIO;r~&sZG?4(HiMo_Asbw5e(FaRW;FfJ zLK6}0p@?xBK`ZV5z@+F4{lqf>nuM!KbsWQcEEMI*kp;eUfwDcrWS*&>Fe(TIyI*?h zPzC2HgC9cJkAs}R$SI3qXVIu$FCi>Vn`#+qG94!0jskYqw_ufL#tz@4xhQS8gOiLK zDO#<6W+PeG?&$2ist*6Yd1+>!%zEZBV?VRLxTH**Ikn{T^45ml(M`PFFZ)JN3Z6vq ze1K`V0TG;eqyLNKTMZz2b4zu?8Hn-PbU%Rhy9Ve(<^ku!&!`+}82aBf0ByZf;sFj5 z2i$HzXGjHlA>%a5P-( z*{D|KjL0T3#cf<@tTa@HPp+c2{ssWqJO@}Q zdivB$G}SMXBgb!+{bHP^_Ur1+OIRZv*u13 z>7uirtzX&^05m{)dxT3E6qQ8h@;0C&9Ba*oQ!;Ze!M%~7m5!}7j}RBfRPplm)Z{!w zd*bnABCIf*_E}F(@tNJu!uU&$xEoj)YVV(0=5e6`UCNJ8XuCqedR9%oQ z0DiHEak(GFKKSz>8; zhWyD>%HjnA?uXgHEx=qskZVl4d%1tVi4uPpxpBIuWrp5DaVYC%xcnymC(2M(a}!^a zeLR9LR-EQQs?k$U`Li_fQtEoIDyiv;d7!~c8HJLZnx^#%>Nv;-GSm%1Mgm<2vFYNr z1!Fb>W?bxz7?Z$=sJKbp?u|TmFw$iUCnxmTNVz14aq)jtJvRu{b3vChBVP1(vW8GW zOePkVb(DGX21!u&S#%Fq^~^3MCl1NW*9z9QIB&P0&0a!|_rdj&g$=w3>d|pd@l#NL z(Xz(_0;o3<+U4GP(fK_XYf&&DU~nUbr*L~j;DV`>I!@*T;uve` z5{KCmBPEUe_ba%o-uCK>jXLyH^nzR052xQ|5iT-CLb?|4U#33rH;k3#2q?v7`j@ddYEHLQ%4Uh{*#(BVzE9s4%4O z6l24+2%`qS!LwD~tpP)c7ckzN<23k?lWC4wVEu<&lpjgKDl#jxRa{4)1$V(%{F6J( zJlo_~a7WrCr79|#hEtdT=-(E3WHwjkeDB`J>rd6KTuT)0z|$%Dy!6&<*XOO7@BJnt zMHZz@GkvV0jo7t8f>;vkbe>sPT(au!&?m+vEp=4x1a3R7f4?>pprqo_E2Xoh0Nn1d zjXWO3C*TtDjC;A!L=4cep!mQ!-H!h7{ig66E92G~8APXXEtz?9J>ma8$o37B3+>mG zctYVG4c8RP{NCVgs#!$K{uqjdr_O`Z&ti;w@P1cktkre)Y zU=M~nRFioNQN*iF1Vrd`s5~qv@&sX_eS-LtI)KayHN2XiJ2KZV`M&phz zg;?7x1dxKL?&a^;V~*C(uXD!$Q6!{o4it z@rBV-8ndX}0+G(J|9sx8ry0r#n<830juEyb8uci3Y?Pj6+704XWJ%u%jsJ>+-Ufhx z{tOH8XTLikUCuG`bng4$lb;2R@|#GZ;Jvi~*SknGoc_27@r;T=y~0qtOuoQCzM9u` zm^)%nEd2VH5D2hn8H|tan=xaBw# zxtx5!pz1WS!#_~}_Tmk&u8UKqJHYZXXL>uC4(uiNXa>W*3k;D_+m*5S5Jk|aj-EkY z^U1Eu96}z0p$Fl({g&ZmATeN&oXq`>%mPffMtT$WhSE}le#;?Z)y;8%ug*#*3;T`4 zGhviyFiHdOb{iN)03mMba&jiYq|Oy&kXJxFaiWvDkqSKFY@}B5cG}ETYAm`g%@MEUPliYTPzjbj7Q(FYRgd4VDw)n$H4^+j_N zZH!6>yG62*DBSuZfk^gag7zX)29?N150Zo8`Q!vM zKEr!}0u;ml`=C_b}G2Oo&gX35=EN#-9F3AJ|=G6>hWs0jf`yDYeI@ z0D3@$zYte!}oh7v2i8Br2E`<_A`InyL*+{Tg?{J)re zR=>x?g$mcec%ouNoj3FBRPF#L3DmN5P>GxcG*w^RgeYMOUHznY52e@^V@aq%<=d=# zqtsdrqhQ!+Swci+A6)ONPI8b@!#i)tHO2#k$gSnLQ%W(A2zq}Ur$4cjmFQ>FtMDjs z10{BERmK}ZhRzr)r9y>m^Ll$gvS0WIyykN7l<$=36fRV~xu`UMIJ1BhuNP;?WhaZ7S{gUCLsYQn$im zNwN`YBs=Of_TL!RW!ouI)G)ls9|xSG=y}^0vd63`KcCgTF&&c_!833AcHja>7K5|P z{Od_8>Tf+Je`&>EJ?3{44YTVPlWKOk$S%5pe^Oe^YXjOACola~Y7^TF4TG959f|{G>&HcSMBMjY0G! z2L-Y5QirZdk|#()h!c-Wn>fYF1#ekEt1%=My_yhW9GPaLiPE}|X@j`4BP#RPR8eGw zGTk7Gk_{kTWo7kRfEK7gBQ@Eu4zjgcM|xJTgR$H*LG?!j+jEjPZ*imn08r`};ZzT$S*bBrDU0{wSRztl=f3@~z(#QMScoCp12fJ(XC-Eu>GG3>^v9f+W& z!;W3Y+X4XKF&)}4-p$dp00>sQYH42KSO(n_KKKf0wXz|4BIL%H5%eCN$lL3DR>~~v z*ujiJ)F-GcTkpTkQ##v$1M57`tH`P^Y6ih8oeH9lBU^so)5p z$h6e5l|N0k-$Y0*LIk%QZ5~5p4g7OGimANG)!`MOwlv7ifH4a82+W1;DqSg3n+{D& z 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鲟k9Z=k={d9$W03Q*CZvLpB0Iq7e==u-?T ze~zi%g%qxXlM?j%FZIpIAwJifoYyUR0i{o_YYz?&inyO1=t%7F z1KWW}IBa+L*|fOH!3n1N6jDf`xbHqZCXN!hAKf0QCzs0PF68#V7B6aE@Vv-(f^nY$ zkg@HLN}zVdMn*ryL!jQefWS8C`%#oir6RDvJ{##?FBiNzY0AES+5j11C~I&z2QP8NmAB=Np| zxYDQx(X<2^iAWndi(5z}ijzRV6!!=KD2>ZqCsHPLVlm0Wf+s~UnQTGf;@7)%ek^Ml z!xnitNq&;sNR+6SlDw7Ixh@>uSY!*Z8O zD|cI3d@MS5TUhd0zLLvwl{}WG}JpVp9R>dk*D+p{DEC@;( zj95h=^AZE&0Vx#vFi-Rvb2&nQiR{AtY30sfP49~{_dL{O947N>y}+PMAO;v_CVqxb%lCY|e}RBiB53$+2wdD2wv zH$qdj|0$@3AR9t_D00udhn>t1EuaVeVez0vaB$3E)Dk&pnc9OfV$>Hd^qee?Ar+Mc^rLiDDC5K9pD`rh*GZa+t^CN8gBC4;k zBS!!f_bXgastn*li63hy+>1Hf_1~(B0Wud3EgeG7_j#hDvXFy?q8m7F6uELPUc=fN zwkAPnDw?YOO*n~^rfPqcrfPLFSec+$k}0Ol)H%*fk&IDAnhV` zRPg|%fISoyERT7`xcr~;rH*z=5AMm`B{x>BO}##jX0fnJm4D#;`#^bj6;ZM`pZE3u z*ZH>8YHDN^K-sLX3R;wn?UI7;Qd4_4!T-N@b+S~|L$><1QxIOfFf764NysG?86oq@ zqZFVca*QZq$?FX@M=wT7s4s`lb@E!%6I;j%&zNW3VyfRZ*1<`-oDV{LNIyp&W>c7% z63lEV)gu;TQE70y{fwcoHKB4qvUcGlsPHmwrZ*SVSWt@reZx-$HY!RsDoY?R@S8qx zGzzG<$8{z#DbG5Sn2a$|{fxIT>ygQMoV&nOPJ?^co@Ni+!vfhOTgQG?MJA=!SS$uw z8aKVXt$Snz%;(gmCy*c(l1nYgrM4u~LjW%b-~b<-af+%UPjR99*c)?U4BRDt-cfv&oi=+0)2b-?IlvZF%GG?%0D$s{oJZ;I*Qbfm0I%qVD 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 z z%ELSJ_$9$n#V&YNII36(-bptI-bpmw3!keqLIcRdv4D{}NJ8D=@xq+pg+mHstj`lv z(dkypCMbnV_%Kw4Im60@1-SD}9w@As#kat>T5J@rKHVrVa&`eZNihoKC>21)7fuxp zmgu_0m!u|l=vM_vI;$jg2XYd7P5P3qb(@rf;RJVhCfsiieS zgvb)l?sgu95uCX`#=;X?Km#^(tIxp`Qn^0&P#_8Kg><^Z-w~dnutH*$sl;?UtdO|7 zsgouOjw<$1MizhTS&S@6_t(tOl5|sg^Cur!reuxl~%A z6O3+=SvV95&_D(W5Z3$79foH_;cG}pNH_x&u;IcT7K^Vh2;m^Y0i;3Xw>l2s$tqBt z5e*=ZM^nLAERo1Uv06RoSZS&_l-R)%W%nH-9dVNdsylM0GnTUT4b(Pc0r5YWiDfji*qGGwYCXtB+-o6T=RDYHyP#+=HEhv`lb zLsjI-ot`B`zyWY+%5FjiVtT>ACtlN&j{Mf{HeD<6VDclkc4YRv~X zGErqk726)U+)>4m3}s2k1bihX4}NT**D__lpKUJKmi*Zx*g5K zu^4I*zaVGTg%;UCk?f%MC?%jUK>`RI^jjlC5@AlaNAl5cjmlc*XcB87Oqs80APvdU z#{_%;p%s$WI!Hnw5QkIEeLYiVnWxO{$<@`u+pKjIT4t@o0D*)CGPBm*vDS4rucm=`s7sZAN;~Q7ZtW^6|0vJ$RCB#; z;z_I)1LMv_4#sqhz}cVy9be###<9YWQxw#xb}Wft_^NaVMFA@lhSZH=R}i3rg82Xr z@nT3G5y_EOqb3!U_4Zup<4f|0Na{=Ch)9xpI|Z!Z>D32*1pp%nGk)9|6_iC;7d630 zdsk1#GyJAO(`R!(zcM7x)!L)J5=B$q)`4IB*al5y1o!-cd%m;H%qH zHB=2x)u3pGVmhwpRcW2lucvFIlGz%meU#a?x18;C!k1csF7@Wx{dIG88AfmK?zK=~ zsx4@XoGOA;VkqAu0s``A&<`q=J(#b{f5>3YG8VIxou&oX@Z6+QrfHfUKpuq_PZP!! z@{n{#a&Am#QJ>CYaj;rca6p2w06@ArF$)y1xgZ-$4C62%qpR?!=Qy-H~nLN$E;g zx|d2zrTwjER9eU0VN9jPRNBTLooYK;y9vRXp{@N}PvxToQcSC_K8t&l3^YzF z1;Q5YaIs%cV$3q67pv-XGNd>|Dx2HE=-uUo2RzG)Cr>6$(Tpr1DVG$Lro3nBbWz9)kxjOen;Bu2O8GKJ!xV+nxIr)uJ;F!xO@Egy^MGZjD$@>fV<#Q6s?X9{dWAqM}^R zGru6HoysShtM`2t@fz%%;Y?W|lHIaumO+YgUT6}@hf*FQ^OydkE z0ba4D`n6aWN0PCS-WR7@<83B;iwiE$K#QN|d9j4{R- zV}^_f+W@d7gOUG-83C^6+X-Tm)^z)iA9J*@7~IgF`$CKEJPySW8rVNjaeh(kSxs9k zroO%`4OiXO&WsO@O-)Djc6x4gIbS~YRx$@I_&_}VGa|$)8BWLl2vhyi>q=uP&It_D zmvjp?WgvZP�Th%IuB7W|QOUzgOGU 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