From e37a10f817ba07b13b52b01f9fe933c25761cb14 Mon Sep 17 00:00:00 2001 From: "Claire (qwery)" Date: Mon, 5 Oct 2026 12:32:16 -0700 Subject: [PATCH 01/14] Restructure modules Profiles and other generic data code is being moved to its own module that can then be subclassed by the themes, rather than using it directly. --- src/apps/firefox/firefox_details.py | 2 +- src/apps/firefox/firefox_install.py | 2 +- src/apps/firefox/firefox_paths.py | 28 ++++++++++++++-------------- src/backend.py | 2 +- src/components/install.py | 2 +- src/gui/profile_selector.py | 2 +- src/meson.build | 4 ++-- src/model/__init__.py | 2 ++ src/{ => model}/profile.py | 5 ++--- 9 files changed, 25 insertions(+), 24 deletions(-) create mode 100644 src/model/__init__.py rename src/{ => model}/profile.py (96%) diff --git a/src/apps/firefox/firefox_details.py b/src/apps/firefox/firefox_details.py index 3685ea4..a293372 100644 --- a/src/apps/firefox/firefox_details.py +++ b/src/apps/firefox/firefox_details.py @@ -24,7 +24,7 @@ from pathlib import Path from addwater import info -from addwater.profile import AppProfile +from addwater.model import AppProfile from addwater.utils import paths from gi.repository import Gio from packaging.version import Version diff --git a/src/apps/firefox/firefox_install.py b/src/apps/firefox/firefox_install.py index 7ef116d..8b09c20 100644 --- a/src/apps/firefox/firefox_install.py +++ b/src/apps/firefox/firefox_install.py @@ -25,7 +25,7 @@ from os.path import exists, join from addwater.components.install import InstallException -from addwater.profile import AppProfile +from addwater.model import AppProfile log = logging.getLogger("firefox_install") diff --git a/src/apps/firefox/firefox_paths.py b/src/apps/firefox/firefox_paths.py index 6784d35..beca54c 100644 --- a/src/apps/firefox/firefox_paths.py +++ b/src/apps/firefox/firefox_paths.py @@ -35,21 +35,21 @@ # or must I make a wrapper? class FirefoxPack(Enum): # TODO rename these to just use the name instead of base later (Firefox / Firefox Flatpak / Librewolf / etc) - BASE = ("Base", BASE) - BASE_ALT = ("Base (Alt)", BASE_LEGACY) - FLATPAK = ("Flatpak", FLATPAK) - FLATPAK_ALT = ("Flatpak (Alt)", FLATPAK_LEGACY) - SNAP = ("Snap", SNAP) - SNAP_ALT = ("Snap (Alt)", SNAP_LEGACY) - LIBREWOLF = ("Librewolf", LIBREWOLF_BASE) - LIBREWOLF_ALT = ("Librewolf (Alt)", LIBREWOLF_BASE_LEGACY) - LIBREWOLF_FLATPAK = ("Librewolf Flatpak", LIBREWOLF_FLATPAK) + BASE = ("Base", BASE) + BASE_ALT = ("Base (Alt)", BASE_LEGACY) + FLATPAK = ("Flatpak", FLATPAK) + FLATPAK_ALT = ("Flatpak (Alt)", FLATPAK_LEGACY) + SNAP = ("Snap", SNAP) + SNAP_ALT = ("Snap (Alt)", SNAP_LEGACY) + LIBREWOLF = ("Librewolf", LIBREWOLF_BASE) + LIBREWOLF_ALT = ("Librewolf (Alt)", LIBREWOLF_BASE_LEGACY) + LIBREWOLF_FLATPAK = ("Librewolf Flatpak", LIBREWOLF_FLATPAK) LIBREWOLF_FLATPAK_ALT = ("Librewolf Flatpak (Alt)", LIBREWOLF_FLATPAK_LEGACY) - FLOORP = ("Floorp", FLOORP_BASE) - FLOORP_FLATPAK = ("Floorp Flatpak", FLOORP_FLATPAK) - WATERFOX = ("Waterfox", WATERFOX_BASE) - WATERFOX_FLATPAK = ("Waterfox Flatpak", WATERFOX_FLATPAK) - CACHY = ("CachyOS Browser", CACHY_BASE) + FLOORP = ("Floorp", FLOORP_BASE) + FLOORP_FLATPAK = ("Floorp Flatpak", FLOORP_FLATPAK) + WATERFOX = ("Waterfox", WATERFOX_BASE) + WATERFOX_FLATPAK = ("Waterfox Flatpak", WATERFOX_FLATPAK) + CACHY = ("CachyOS Browser", CACHY_BASE) def __init__(self, name: str, path: PathLike) -> None: self.pack_name = name diff --git a/src/backend.py b/src/backend.py index 668f6fd..1f5b4b0 100644 --- a/src/backend.py +++ b/src/backend.py @@ -25,7 +25,7 @@ from addwater import info from addwater.apps.firefox import FirefoxAppDetails, FirefoxPack from addwater.components import InstallManager, InstallStatus, OnlineManager -from addwater.profile import AppProfile +from addwater.model import AppProfile from addwater.utils.mocks import mock_online from gi.repository import Gio from packaging.version import Version diff --git a/src/components/install.py b/src/components/install.py index cea0812..ac5b63f 100644 --- a/src/components/install.py +++ b/src/components/install.py @@ -24,7 +24,7 @@ from os import PathLike from os.path import join -from addwater.profile import AppProfile +from addwater.model import AppProfile log = logging.getLogger("install_manager") diff --git a/src/gui/profile_selector.py b/src/gui/profile_selector.py index 913ec70..fb6f4b7 100644 --- a/src/gui/profile_selector.py +++ b/src/gui/profile_selector.py @@ -22,7 +22,7 @@ from addwater import info from addwater.apps.firefox import FirefoxPack -from addwater.profile import AppProfile +from addwater.model import AppProfile from gi.repository import Adw, Gio, GObject, Gtk log = logging.getLogger("profile_selector") diff --git a/src/meson.build b/src/meson.build index 031c4e0..0c5bd20 100644 --- a/src/meson.build +++ b/src/meson.build @@ -27,6 +27,7 @@ install_subdir('apps', install_dir: moduledir) install_subdir('components', install_dir: moduledir) install_subdir('utils', install_dir: moduledir) install_subdir('gui', install_dir: moduledir) +install_subdir('model', install_dir: moduledir) addwater_sources = [ '__init__.py', @@ -34,8 +35,7 @@ addwater_sources = [ 'window.py', 'page.py', 'backend.py', - 'profile.py', configure_file(input: 'info.py.in', output: 'info.py', configuration: conf), ] -install_data(addwater_sources, install_dir: moduledir) \ No newline at end of file +install_data(addwater_sources, install_dir: moduledir) diff --git a/src/model/__init__.py b/src/model/__init__.py new file mode 100644 index 0000000..9153053 --- /dev/null +++ b/src/model/__init__.py @@ -0,0 +1,2 @@ +from .package import AppPack as AppPack +from .profile import AppProfile as AppProfile diff --git a/src/profile.py b/src/model/profile.py similarity index 96% rename from src/profile.py rename to src/model/profile.py index 620c194..a740c24 100644 --- a/src/profile.py +++ b/src/model/profile.py @@ -17,13 +17,12 @@ # # SPDX-License-Identifier: GPL-3.0-or-later +from . import AppPack from pathlib import Path -from addwater.apps.firefox import FirefoxPack from gi.repository import GObject from gi.repository.GObject import ParamFlags - class AppProfile(GObject.Object): __gtype_name__ = "WaterProfile" @@ -49,7 +48,7 @@ def __init__( id: str, path: str | Path, favorite: bool, - package: FirefoxPack, + package: AppPack, ) -> None: super().__init__(name=name, id=id, favorite=favorite) self.path = Path(path) From adc22ede8dea3314977d684a3ded47c97aeb9aa9 Mon Sep 17 00:00:00 2001 From: "Claire (qwery)" Date: Mon, 5 Oct 2026 12:33:46 -0700 Subject: [PATCH 02/14] fixup! foundry: Add .foundry dir --- .foundry/.gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.foundry/.gitignore b/.foundry/.gitignore index dcb888d..a30dfbf 100644 --- a/.foundry/.gitignore +++ b/.foundry/.gitignore @@ -1,3 +1,3 @@ -cache/ tmp/ user/ +cache/ From d06b6c2a9f1da2b4453a8e6df557256f20e682f3 Mon Sep 17 00:00:00 2001 From: "Claire (qwery)" Date: Mon, 5 Oct 2026 13:05:42 -0700 Subject: [PATCH 03/14] fix: log.exception() missing message --- src/components/install.py | 12 ++++++------ src/components/online.py | 14 +++++++------- src/gui/pack_selector.py | 4 ++-- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/components/install.py b/src/components/install.py index ac5b63f..4dee6a3 100644 --- a/src/components/install.py +++ b/src/components/install.py @@ -89,8 +89,8 @@ def combined_install( def uninstall(self, profile: AppProfile, folder_name: str) -> None: try: self._uninstaller(profile, folder_name) - except InstallException: - log.exception() + except InstallException as err: + log.exception(err) return InstallStatus.FAILURE return InstallStatus.SUCCESS @@ -177,8 +177,8 @@ def _do_uninstall_theme(profile: AppProfile, theme_folder: str) -> None: lines[i] = line.replace("true", "false") file.writelines(lines) - except OSError: - log.exception() + except OSError as err: + log.exception(err) raise InstallException("uninstall failed") log.info("Done.") @@ -202,8 +202,8 @@ def _remove_css_imports(chrome_path: PathLike) -> None: lines.remove(line) file.writelines(lines) - except FileNotFoundError: - log.exception() + except FileNotFoundError as err: + log.exception(err) class InstallStatus(Enum): SUCCESS = 0 diff --git a/src/components/online.py b/src/components/online.py index 868fa94..549b8dc 100644 --- a/src/components/online.py +++ b/src/components/online.py @@ -104,8 +104,8 @@ def _begin_download(self, path_info: str, tarball_url: str) -> Enum: ) except NetworkException: return OnlineStatus.DISCONNECTED - except ExtractionException: - log.exception() + except ExtractionException as err: + log.exception(err) return OnlineStatus.OTHER_ERROR # TODO handle this error better else: return OnlineStatus.UPDATED @@ -129,7 +129,7 @@ def _get_release(self, base_path: str, final_name: str, tarball_url: str) -> Non try: self._download_tarball(tarball_url, zipfile) except (requests.RequestException, requests.ConnectionError) as err: - log.exception() + log.exception(err) raise NetworkException(err) try: @@ -227,11 +227,11 @@ def _get_release_info(gh_url: str) -> dict: } try: response = requests.get(gh_url, headers=headers, timeout=10) - except requests.RequestException: + except requests.RequestException as err: # TODO use specific exceptions to handle being disconnected. It'll # be more helpful if the issue isn't just being offline but an API or # programmer error - log.exception() + log.exception(err) raise NetworkException("failed to connect to Github") api_calls_left = int(response.headers["x-ratelimit-remaining"]) @@ -239,8 +239,8 @@ def _get_release_info(gh_url: str) -> dict: latest_release = response.json()[0] version = Version(latest_release["tag_name"]) tarball_url = latest_release["tarball_url"] - except requests.JSONDecodeError: - log.exception() + except requests.JSONDecodeError as err: + log.exception(err) version = None tarball_url = None diff --git a/src/gui/pack_selector.py b/src/gui/pack_selector.py index e698e4d..64cda70 100644 --- a/src/gui/pack_selector.py +++ b/src/gui/pack_selector.py @@ -86,8 +86,8 @@ def set_package(self) -> None: if new_pack: try: self.backend.set_package(new_pack) - except InterfaceMisuseError: - log.exception() + except InterfaceMisuseError as err: + log.exception(err) self.valid_path = False else: self.valid_path = True From c31e89535b6948e45362a9d2e2ec9319ee09eb04 Mon Sep 17 00:00:00 2001 From: "Claire (qwery)" Date: Mon, 5 Oct 2026 13:06:16 -0700 Subject: [PATCH 04/14] model: Add AppPack abc Now FirefoxPack subclasses AppPack. Functionality is identical at the moment --- .gitignore | 2 +- src/apps/firefox/firefox_details.py | 2 +- src/apps/firefox/firefox_paths.py | 13 +++++------- src/model/package.py | 33 +++++++++++++++++++++++++++++ src/model/profile.py | 4 +++- 5 files changed, 43 insertions(+), 11 deletions(-) create mode 100644 src/model/package.py diff --git a/.gitignore b/.gitignore index 590d901..3798f94 100644 --- a/.gitignore +++ b/.gitignore @@ -9,5 +9,5 @@ dev.qwery.AddWater*.json~ firefox-gnome-theme pylint-results.txt subprojects/libportal -tests/__pycache__ +__pycache__ diff --git a/src/apps/firefox/firefox_details.py b/src/apps/firefox/firefox_details.py index a293372..e6a5d62 100644 --- a/src/apps/firefox/firefox_details.py +++ b/src/apps/firefox/firefox_details.py @@ -29,9 +29,9 @@ from gi.repository import Gio from packaging.version import Version -from . import FirefoxPack from .firefox_install import install_for_firefox from .firefox_options import FIREFOX_OPTIONS +from .firefox_paths import FirefoxPack log = logging.getLogger("firefox_details") diff --git a/src/apps/firefox/firefox_paths.py b/src/apps/firefox/firefox_paths.py index beca54c..170665d 100644 --- a/src/apps/firefox/firefox_paths.py +++ b/src/apps/firefox/firefox_paths.py @@ -1,8 +1,9 @@ -from enum import Enum from os import PathLike, environ from os.path import exists, expanduser, join from pathlib import Path +from addwater.model import AppPack + config_dir = environ.get("HOST_XDG_CONFIG_HOME", expanduser("~/.config")) # FIREFOX PATHS @@ -33,7 +34,7 @@ # TODO can this be a GObject so I can use it in ListModels? # or must I make a wrapper? -class FirefoxPack(Enum): +class FirefoxPack(AppPack): # TODO rename these to just use the name instead of base later (Firefox / Firefox Flatpak / Librewolf / etc) BASE = ("Base", BASE) BASE_ALT = ("Base (Alt)", BASE_LEGACY) @@ -51,10 +52,6 @@ class FirefoxPack(Enum): WATERFOX_FLATPAK = ("Waterfox Flatpak", WATERFOX_FLATPAK) CACHY = ("CachyOS Browser", CACHY_BASE) - def __init__(self, name: str, path: PathLike) -> None: - self.pack_name = name - self.path = Path(path) - def get_profile_ini(self) -> Path: prof_ini = join(self.path, "profiles.ini") if not exists(prof_ini): @@ -64,7 +61,7 @@ def get_profile_ini(self) -> Path: @staticmethod - def new_from_path(app_path: Path | PathLike) -> Enum | None: + def new_from_path(app_path: Path | PathLike) -> AppPack | None: app_path = Path(app_path) for pack in FirefoxPack: if app_path == pack.path: @@ -73,7 +70,7 @@ def new_from_path(app_path: Path | PathLike) -> Enum | None: return None @staticmethod - def new_from_name(name: str) -> Enum | None: + def new_from_name(name: str) -> AppPack | None: for pack in FirefoxPack: if name == pack.pack_name: return pack diff --git a/src/model/package.py b/src/model/package.py new file mode 100644 index 0000000..9b08083 --- /dev/null +++ b/src/model/package.py @@ -0,0 +1,33 @@ +from enum import Enum +from os import PathLike +from pathlib import Path +from typing import abstractmethod + + +class AppPack(Enum): + _name: str + _path: Path + + def __init__(self, name: str, path: PathLike | Path) -> None: + self._name = name + self._path = Path(path) + + @staticmethod + @abstractmethod + def new_from_name(name: str) -> None: + raise NotImplementedError + + @staticmethod + @abstractmethod + def new_from_path(path: Path | PathLike) -> None: + raise NotImplementedError + + + @property + def pack_name(self) -> str: + return self._name + + @property + def path(self) -> Path: + return self._path + diff --git a/src/model/profile.py b/src/model/profile.py index a740c24..a3d971f 100644 --- a/src/model/profile.py +++ b/src/model/profile.py @@ -17,12 +17,14 @@ # # SPDX-License-Identifier: GPL-3.0-or-later -from . import AppPack from pathlib import Path from gi.repository import GObject from gi.repository.GObject import ParamFlags +from . import AppPack + + class AppProfile(GObject.Object): __gtype_name__ = "WaterProfile" From 0d7feebe95883b9b0b9fe5dfe0c9c8eb9dd13151 Mon Sep 17 00:00:00 2001 From: "Claire (qwery)" Date: Mon, 5 Oct 2026 13:08:56 -0700 Subject: [PATCH 05/14] FirefoxDetails: Fix circular FirefoxPack import --- src/apps/firefox/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/apps/firefox/__init__.py b/src/apps/firefox/__init__.py index ba770f1..3a43dd8 100644 --- a/src/apps/firefox/__init__.py +++ b/src/apps/firefox/__init__.py @@ -1,3 +1,2 @@ -# FIXME circular import when organized -from .firefox_paths import FirefoxPack as FirefoxPack from .firefox_details import FirefoxAppDetails as FirefoxAppDetails +from .firefox_paths import FirefoxPack as FirefoxPack From e29f94582e8d3e566858378212701f7550073751 Mon Sep 17 00:00:00 2001 From: "Claire (qwery)" Date: Sat, 10 Oct 2026 12:11:33 -0700 Subject: [PATCH 06/14] fix(meson): libportal subproject not being used --- meson.build | 23 ++++++++++++++++++----- subprojects/libportal.wrap | 7 ++++++- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/meson.build b/meson.build index fece547..e3d8db9 100644 --- a/meson.build +++ b/meson.build @@ -1,13 +1,18 @@ project('addwater', version: '1.3', meson_version: '>= 1.4.0', - default_options: [ 'warning_level=2', 'werror=false', ], + default_options: ['warning_level=2', 'werror=true', + 'buildtype=debugoptimized', 'python.install_env=auto'], ) i18n = import('i18n') gnome = import('gnome') python = import('python').find_installation('python3', - modules: ['packaging', 'requests',]) + modules: [ + 'packaging', + 'requests', + ] +) pkgdatadir = get_option('prefix') / get_option('datadir') / meson.project_name() @@ -36,9 +41,17 @@ conf = configuration_data({ conf.set('MOCK_API', mock_api) conf.set('FORCE_BG', force_bg) -dependency('gtk4') -dependency('libadwaita-1', version: '>=1.9.0') -dependency('libportal', version: '>=0.9.1') +dependency('gtk4', version: '>=4.22') +dependency('libadwaita-1', version: '>=1.9.0') +dependency('libportal', version: '>=0.9.1', + default_options: ['backend-gtk4=enabled', + 'backend-gtk3=disabled', + 'backend-qt5=disabled', + 'backend-qt6=disabled', + 'vapi=false', + 'introspection=true', + 'werror=false']) +dependency('libportal-gtk4', version: '>=0.9.1') subdir('data') subdir('po') diff --git a/subprojects/libportal.wrap b/subprojects/libportal.wrap index a9c4e19..3e4150c 100644 --- a/subprojects/libportal.wrap +++ b/subprojects/libportal.wrap @@ -2,4 +2,9 @@ directory = libportal url = https://github.com/flatpak/libportal.git revision = 8f5dc8d192f6e31dafe69e35219e3b707bde71ce -depth = 1 \ No newline at end of file +depth = 1 + +[provide] +dependency_names = libportal, libportal-gtk4 +libportal = libportal_dep +libportal-gtk4 = libportal_gtk4_dep From 949297015c47c9999d8a1d69a36da490c8eb0078 Mon Sep 17 00:00:00 2001 From: "Claire (qwery)" Date: Sat, 10 Oct 2026 12:12:57 -0700 Subject: [PATCH 07/14] Add Python requirements files --- requirements.dev.txt | 7 +++++++ requirements.txt | 8 ++++++++ 2 files changed, 15 insertions(+) create mode 100644 requirements.dev.txt create mode 100644 requirements.txt diff --git a/requirements.dev.txt b/requirements.dev.txt new file mode 100644 index 0000000..a896a08 --- /dev/null +++ b/requirements.dev.txt @@ -0,0 +1,7 @@ +iniconfig==2.3.1 +packaging==26.3 +pluggy==1.6.0 +Pygments==2.21.0 +pyproject-metadata==0.12.1 +pytest==9.1.1 +pytest-mock==3.16.0 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..18e5399 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,8 @@ +certifi==2026.7.22 +charset-normalizer==3.5.2 +idna==3.20 +packaging==26.3 +pycairo==1.29.2 +PyGObject==3.58.0 +requests==2.34.2 +urllib3==2.8.0 From bee69450c714f9fa7ae05ce806925195c66f7344 Mon Sep 17 00:00:00 2001 From: "Claire (qwery)" Date: Sat, 10 Oct 2026 12:16:27 -0700 Subject: [PATCH 08/14] tests: Move out of AW module --- meson.build | 1 + src/meson.build | 11 ----------- {src/tests => tests}/__init__.py | 0 tests/meson.build | 10 ++++++++++ 4 files changed, 11 insertions(+), 11 deletions(-) rename {src/tests => tests}/__init__.py (100%) create mode 100644 tests/meson.build diff --git a/meson.build b/meson.build index e3d8db9..c9a1e53 100644 --- a/meson.build +++ b/meson.build @@ -59,6 +59,7 @@ subdir('src') if get_option('tests') subdir('build-aux') + subdir('tests') endif gnome.post_install( diff --git a/src/meson.build b/src/meson.build index 0c5bd20..145406d 100644 --- a/src/meson.build +++ b/src/meson.build @@ -11,17 +11,6 @@ configure_file( install_mode: 'r-xr-xr-x' ) -# TODO setup / teardown data to simulate profiles -# TODO test: background updater -# TODO test: installer -# TODO test: find old profiles -# TODO test: finding new profiles -# TODO test: translations with podebug -if get_option('tests') - install_subdir('tests', install_dir: moduledir) - test('pytest', find_program('pytest'), - args: [moduledir / 'tests', '--quiet']) -endif install_subdir('apps', install_dir: moduledir) install_subdir('components', install_dir: moduledir) diff --git a/src/tests/__init__.py b/tests/__init__.py similarity index 100% rename from src/tests/__init__.py rename to tests/__init__.py diff --git a/tests/meson.build b/tests/meson.build new file mode 100644 index 0000000..6e486f0 --- /dev/null +++ b/tests/meson.build @@ -0,0 +1,10 @@ +# TODO setup / teardown data to simulate profiles +# TODO test: background updater +# TODO test: installer +# TODO test: find old profiles +# TODO test: finding new profiles +# TODO test: translations with podebug +if get_option('tests') + test('pytest', find_program('pytest'), + args: ['--quiet']) +endif From fc62acdda9c75021a7b468e462f43771dc2b66af Mon Sep 17 00:00:00 2001 From: "Claire (qwery)" Date: Thu, 15 Oct 2026 11:22:41 -0700 Subject: [PATCH 09/14] model: New paths module Cleaner way to get user paths, and includes transparent handling of sandboxed Flatpak paths --- src/model/paths.py | 84 +++++++++++++++++++++++++++++++++ tests/meson.build | 22 +++++++-- tests/suite.py.in | 13 +++++ tests/test_paths.py | 112 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 227 insertions(+), 4 deletions(-) create mode 100644 src/model/paths.py create mode 100644 tests/suite.py.in create mode 100644 tests/test_paths.py diff --git a/src/model/paths.py b/src/model/paths.py new file mode 100644 index 0000000..18fcc96 --- /dev/null +++ b/src/model/paths.py @@ -0,0 +1,84 @@ +from os import PathLike, environ +from pathlib import Path + +# Flatpak overrides XDG_DATA_HOME so we need to also check HOST_XDG_DATA_HOME +_user_data_home: Path = None # Real host data dir +_flatpak_data_home: Path | None = None # Container data dir if exists +_user_config: Path = None + +# These should always prefer container path if in one +_aw_data_dir: Path = None +_aw_config_dir: Path = None +_aw_cache_dir: Path = None + +def user_home() -> Path: + """User's HOME dir""" + if not (path := environ.get("HOME")): + raise OSError("env HOME is not set. bail out!") + + return ensure_exists(path) + +def user_data_home() -> Path: + """User's true XDG_DATA_HOME dir. To use the Flatpak sandboxed dir, use + `flatpak_data_home()` instead""" + key = "XDG_DATA_HOME" + if in_flatpak(): + key = "HOST_" + key + + if not (path := environ.get(key)): + path = "~/.local/share" + + return ensure_exists(path) + +def user_config() -> Path: + """User's true XDG_CONFIG_HOME dir""" + key = "XDG_CONFIG_HOME" + if in_flatpak(): + key = "HOST_" + key + + if not (path := environ.get(key)): + path = "~/.config" + + return ensure_exists(path) + +def aw_data_dir() -> Path: + """Add Water data dir. Always prefer sandbox dir if in one""" + if in_flatpak(): + path = flatpak_data_home() / 'addwater' + else: + path = user_data_home() / 'addwater' + + return ensure_exists(path) + +def aw_config_dir() -> Path: + """Add Water config dir. Always prefer sandbox dir if in one""" + if not (path := environ.get("XDG_CONFIG_HOME")): + path = "~/.config" + + return ensure_exists(Path(path) / 'addwater') + +def aw_cache_dir() -> Path: + """Add Water cache dir. Always prefer sandbox dir if in one""" + if not (path := environ.get("XDG_CACHE_HOME")): + path = "~/.cache" + + return ensure_exists(Path(path) / 'addwater') + +def ensure_exists(path: Path | PathLike) -> Path: + """Ensure a Path already exists in the filesystem""" + new = Path(path).expanduser() + new.mkdir(exist_ok=True, parents=True) + return new + +def flatpak_data_home() -> Path | None: + """Returns the sandboxed XDG_DATA_HOME dir. If not inside Flatpak, + returns None""" + if not in_flatpak(): + return None + + return ensure_exists(environ.get("XDG_DATA_HOME")) + +def in_flatpak() -> bool: + """Check for signs that we're inside a Flatpak sandbox""" + return environ.get("container") == "flatpak" + diff --git a/tests/meson.build b/tests/meson.build index 6e486f0..e67e709 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -4,7 +4,21 @@ # TODO test: find old profiles # TODO test: finding new profiles # TODO test: translations with podebug -if get_option('tests') - test('pytest', find_program('pytest'), - args: ['--quiet']) -endif + +configure_file( + input: 'suite.py.in', + output: 'suite.py', + configuration: conf, +) + +test_src = [ + '__init__.py', + 'test_paths.py', +] + +foreach testfile: test_src + configure_file(input: testfile, output: testfile, copy: true) +endforeach + +test('pytest', find_program('pytest'), + args: [meson.current_build_dir(), '--quiet']) diff --git a/tests/suite.py.in b/tests/suite.py.in new file mode 100644 index 0000000..8c4ba7b --- /dev/null +++ b/tests/suite.py.in @@ -0,0 +1,13 @@ +import gettext +import locale +import signal +import sys + +pkgdatadir = '@pkgdatadir@' +localedir = '@localedir@' + +sys.path.insert(1, pkgdatadir) +signal.signal(signal.SIGINT, signal.SIG_DFL) +locale.bindtextdomain('addwater', localedir) +locale.textdomain('addwater') +gettext.install('addwater', localedir) diff --git a/tests/test_paths.py b/tests/test_paths.py new file mode 100644 index 0000000..76d6ff5 --- /dev/null +++ b/tests/test_paths.py @@ -0,0 +1,112 @@ +import os +import pytest +import shutil +import sys + +from os.path import exists +from pathlib import Path + +from . import suite +sys.path.insert(1, suite.pkgdatadir) + +from addwater.model.paths import * + +TEST_ROOT = Path("/tmp/addwater/test") +TEST_HOME = TEST_ROOT / "home" + +@pytest.fixture +def fake_env(monkeypatch) -> None: + TEST_HOME.mkdir(exist_ok=False, parents=True) + + monkeypatch.setenv("HOME", str(TEST_HOME)) + monkeypatch.delenv("container") + + monkeypatch.setenv("XDG_CACHE_HOME", str(TEST_HOME / ".thecachedir")) + monkeypatch.setenv("XDG_CONFIG_HOME", str(TEST_HOME / ".theconfigurationdir")) + monkeypatch.setenv("XDG_DATA_HOME", str(TEST_HOME / ".thedatadir")) + + yield None + + shutil.rmtree(TEST_ROOT) + +@pytest.fixture +def fake_env_flatpak(monkeypatch) -> None: + TEST_HOME.mkdir(exist_ok=False, parents=True) + + SANDBOX_HOME = TEST_HOME / ".var/app/dev.qwery.AddWater" + + monkeypatch.setenv("HOME", str(TEST_HOME)) + monkeypatch.setenv("container", "flatpak") + + monkeypatch.setenv("XDG_CACHE_HOME", str(SANDBOX_HOME / ".thecachedir")) + monkeypatch.setenv("XDG_CONFIG_HOME", str(SANDBOX_HOME / ".theconfigurationdir")) + monkeypatch.setenv("XDG_DATA_HOME", str(SANDBOX_HOME / "thedatadir")) + + monkeypatch.setenv("HOST_XDG_CACHE_HOME", str(TEST_HOME / ".thecachedir")) + monkeypatch.setenv("HOST_XDG_CONFIG_HOME", str(TEST_HOME / ".theconfigurationdir")) + monkeypatch.setenv("HOST_XDG_DATA_HOME", str(TEST_HOME / ".thedatadir")) + + yield None + + shutil.rmtree(TEST_ROOT) + +@pytest.fixture +def fake_env_empty(monkeypatch) -> None: + TEST_HOME.mkdir(exist_ok=False, parents=True) + + monkeypatch.setenv("HOME", str(TEST_HOME)) + monkeypatch.delenv("container", raising=False) + + monkeypatch.delenv("XDG_CACHE_HOME", raising=False) + monkeypatch.delenv("XDG_CONFIG_HOME", raising=False) + monkeypatch.delenv("XDG_DATA_HOME", raising=False) + + monkeypatch.delenv("HOST_XDG_CACHE_HOME", raising=False) + monkeypatch.delenv("HOST_XDG_CONFIG_HOME", raising=False) + monkeypatch.delenv("HOST_XDG_DATA_HOME", raising=False) + + yield None + + shutil.rmtree(TEST_ROOT) + +def test_paths(fake_env) -> None: + USER_CACHE = Path(environ["XDG_CACHE_HOME"]) + USER_CONFIG = Path(environ["XDG_CONFIG_HOME"]) + USER_DATA = Path(environ["XDG_DATA_HOME"]) + + assert not in_flatpak() + assert flatpak_data_home() is None + assert user_config() == USER_CONFIG + assert user_data_home() == USER_DATA + assert aw_cache_dir() == USER_CACHE / "addwater" + assert aw_config_dir() == USER_CONFIG / "addwater" + assert aw_data_dir() == USER_DATA / "addwater" + +def test_paths_in_flatpak(fake_env_flatpak) -> None: + USER_CONFIG = Path(environ["HOST_XDG_CONFIG_HOME"]) + USER_DATA = Path(environ["HOST_XDG_DATA_HOME"]) + FLATPAK_CACHE = Path(environ["XDG_CACHE_HOME"]) + FLATPAK_CONFIG = Path(environ["XDG_CONFIG_HOME"]) + FLATPAK_DATA = Path(environ["XDG_DATA_HOME"]) + + assert in_flatpak() + assert flatpak_data_home() == FLATPAK_DATA + assert user_data_home() == USER_DATA + assert user_config() == USER_CONFIG + assert aw_cache_dir() == FLATPAK_CACHE / "addwater" + assert aw_config_dir() == FLATPAK_CONFIG / "addwater" + assert aw_data_dir() == FLATPAK_DATA / "addwater" + +def test_paths_xdg_default(fake_env_empty) -> None: + HOME = Path(environ["HOME"]) + USER_CACHE = HOME / ".cache" + USER_CONFIG = HOME / ".config" + USER_DATA = HOME / ".local/share" + + assert not in_flatpak() + assert flatpak_data_home() is None + assert user_data_home() == USER_DATA + assert user_config() == USER_CONFIG + assert aw_cache_dir() == USER_CACHE / "addwater" + assert aw_data_dir() == USER_DATA / "addwater" + From acc32ba68941cf9986742aacdc38d6dcf84d850b Mon Sep 17 00:00:00 2001 From: "Claire (qwery)" Date: Thu, 15 Oct 2026 11:41:39 -0700 Subject: [PATCH 10/14] FFDetails: Replace old paths module --- src/apps/firefox/firefox_details.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/apps/firefox/firefox_details.py b/src/apps/firefox/firefox_details.py index e6a5d62..195d760 100644 --- a/src/apps/firefox/firefox_details.py +++ b/src/apps/firefox/firefox_details.py @@ -25,7 +25,7 @@ from addwater import info from addwater.model import AppProfile -from addwater.utils import paths +from addwater.model.paths import aw_cache_dir from gi.repository import Gio from packaging.version import Version @@ -57,12 +57,14 @@ class FirefoxAppDetails: ) # TODO just save the unzipped download in /tmp/ and then unzip into addwater/cache - save_to = paths.DOWNLOAD_DIR + # TODO make these into Paths, not PathLike strs + save_to = aw_cache_dir() / "themes" app_folder = "firefox" theme_folder = "firefox-gnome-theme" full_path = join(save_to, app_folder, theme_folder) def __init__(self) -> None: + self.save_to.mkdir(exist_ok=True, parents=False) self.settings = self.get_new_gsettings() version = Version(self.settings.get_string("installed-version")) From 928f0f218e1282e51cefe3cadd73b0de8034956f Mon Sep 17 00:00:00 2001 From: "Claire (qwery)" Date: Thu, 15 Oct 2026 11:55:20 -0700 Subject: [PATCH 11/14] logs: Replace old paths module --- src/utils/logs.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/utils/logs.py b/src/utils/logs.py index 92f3343..b8f4621 100644 --- a/src/utils/logs.py +++ b/src/utils/logs.py @@ -21,27 +21,30 @@ import logging import os -import os.path import sys + from datetime import datetime, timezone +from os.path import join from addwater.info import PROFILE, VERSION +from addwater.model.paths import aw_cache_dir from gi.repository import Adw, Gtk -from . import paths - def init_logs() -> None: """Set up logging system""" - LOG_DIR = paths.LOG_DIR + LOG_DIR = aw_cache_dir() / "logs" + LOG_DIR.mkdir(exist_ok=True, parents=False) + try: now = datetime.now(timezone.utc).strftime("%Y-%m-%d") - CURRENT_LOGFILE = os.path.join(LOG_DIR, f"addwater_{now}.log") + CURRENT_LOGFILE = str(join(LOG_DIR, f"addwater_{now}.log")) file_handler = logging.FileHandler(CURRENT_LOGFILE) console_handler = logging.StreamHandler(sys.stdout) file_handler.setLevel(logging.DEBUG) + # TODO tie this an env var instead if PROFILE == "development": console_handler.setLevel(logging.DEBUG) elif PROFILE == "default": @@ -67,7 +70,7 @@ def init_logs() -> None: difference = datetime.now(timezone.utc) - time if difference.days > 7: - os.remove(os.path.join(LOG_DIR, each.name)) + os.remove(join(LOG_DIR, each.name)) info = f""" ------------------------------------------------------------------------ From 36a6148b0b7933d01c347eed762edfce1431c9c2 Mon Sep 17 00:00:00 2001 From: "Claire (qwery)" Date: Thu, 15 Oct 2026 12:11:12 -0700 Subject: [PATCH 12/14] App: Replace old paths --- src/main.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/main.py b/src/main.py index 082750a..a13fd16 100644 --- a/src/main.py +++ b/src/main.py @@ -31,7 +31,6 @@ from addwater.backend import Backend from gi.repository import Adw, Gio, GLib -from .utils import paths from .utils.background import BackgroundUpdater from .utils.logs import init_logs from .window import Window @@ -50,7 +49,6 @@ def __init__(self) -> None: resource_base_path=info.PREFIX, ) - paths.init_paths() init_logs() self.init_actions() @@ -127,15 +125,13 @@ def on_reset_app_action(self, *_args) -> None: settings = Gio.Settings(info.APP_ID) settings.reset("background-update") + # TODO delete theme files. Maybe add a method to tell + # AppTheme to clean up its data? Or make its dir + # public + for each in self.backends: each.reset_app() - try: - shutil.rmtree(paths.DOWNLOAD_DIR) - except FileNotFoundError: - pass - log.info("deleted download folder") - log.info("app has been reset and will now exit") self.quit() From c7bf1c6f906fdec60a55ed6b4c6739a1e62c17c4 Mon Sep 17 00:00:00 2001 From: "Claire (qwery)" Date: Thu, 15 Oct 2026 12:14:35 -0700 Subject: [PATCH 13/14] Delete old paths module --- src/model/profile.py | 2 ++ src/utils/paths.py | 53 -------------------------------------------- 2 files changed, 2 insertions(+), 53 deletions(-) delete mode 100644 src/utils/paths.py diff --git a/src/model/profile.py b/src/model/profile.py index a3d971f..1d1b1d2 100644 --- a/src/model/profile.py +++ b/src/model/profile.py @@ -25,6 +25,8 @@ from . import AppPack +# FIXME also check for profiles in new FF profile system. +# that's the default now class AppProfile(GObject.Object): __gtype_name__ = "WaterProfile" diff --git a/src/utils/paths.py b/src/utils/paths.py deleted file mode 100644 index 1078659..0000000 --- a/src/utils/paths.py +++ /dev/null @@ -1,53 +0,0 @@ -# paths.py -# -# Copyright 2025 Qwery -# -# 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 . -# -# SPDX-License-Identifier: GPL-3.0-or-later - -# This module owns all app paths and must be used when referring to general paths like config or firefox paths - -import os -from os.path import join - -from gi.repository import GLib - -# APP PATHS -XDG_CACHE_DIR = GLib.get_user_cache_dir() -XDG_DATA_DIR = GLib.get_user_data_dir() -XDG_CONFIG_DIR = GLib.get_user_config_dir() - -APP_CACHE = join(XDG_CACHE_DIR, "add-water") -APP_CONFIG = join(XDG_CONFIG_DIR, "add-water") -APP_DATA = join(XDG_DATA_DIR, "add-water") - -DOWNLOAD_DIR = join(APP_CACHE, "downloads") -LOG_DIR = join(APP_CACHE, "logs") - - -def init_paths() -> None: - paths = [APP_CACHE, DOWNLOAD_DIR, LOG_DIR] - for each in paths: - try: - os.mkdir(path=each) - print(f"{each} directory created.") - except FileExistsError: - print(f"{each} already exists. Skipped.") - except FileNotFoundError as err: - print("Couldn't find parent dir when initializing dirs ::", err) - return - - print("All paths initialized.") - print("-------------------------") From 93793c65a15eac29c572be90578597d4ba156415 Mon Sep 17 00:00:00 2001 From: "Claire (qwery)" Date: Thu, 15 Oct 2026 15:45:02 -0700 Subject: [PATCH 14/14] cleanup(FFPaths): Use new path module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Checking for paths should be more reliable now — whether sandboxed or not --- src/apps/firefox/firefox_paths.py | 51 +++++++++++++++---------------- 1 file changed, 24 insertions(+), 27 deletions(-) diff --git a/src/apps/firefox/firefox_paths.py b/src/apps/firefox/firefox_paths.py index 170665d..3ffceda 100644 --- a/src/apps/firefox/firefox_paths.py +++ b/src/apps/firefox/firefox_paths.py @@ -1,36 +1,33 @@ -from os import PathLike, environ -from os.path import exists, expanduser, join +from os import PathLike +from os.path import exists, join from pathlib import Path from addwater.model import AppPack +from addwater.model.paths import user_config, user_home -config_dir = environ.get("HOST_XDG_CONFIG_HOME", expanduser("~/.config")) +# TODO use new paths mod # FIREFOX PATHS -# TODO replace fstring with join() -BASE_LEGACY = expanduser("~/.mozilla/firefox/") -BASE = expanduser(f"{config_dir}/mozilla/firefox/") -FLATPAK_LEGACY = expanduser("~/.var/app/org.mozilla.firefox/.mozilla/firefox/") -FLATPAK = expanduser("~/.var/app/org.mozilla.firefox/config/mozilla/firefox/") -SNAP_LEGACY = expanduser("~/snap/firefox/common/.mozilla/firefox/") -SNAP = expanduser("~/snap/firefox/common/.config/mozilla/firefox/") - -LIBREWOLF_BASE_LEGACY = expanduser("~/.librewolf/") -LIBREWOLF_BASE = expanduser(f"{config_dir}/librewolf/librewolf") -LIBREWOLF_FLATPAK_LEGACY = expanduser( - "~/.var/app/io.gitlab.librewolf-community/.librewolf/" -) -LIBREWOLF_FLATPAK = expanduser( - "~/.var/app/io.gitlab.librewolf-community/config/librewolf/librewolf/" -) - -FLOORP_BASE = expanduser("~/.floorp") -FLOORP_FLATPAK = expanduser("~/.var/app/one.ablaze.floorp/.floorp") - -WATERFOX_BASE = expanduser("~/.waterfox") -WATERFOX_FLATPAK = expanduser("~/.var/app/net.waterfox.waterfox/.waterfox") - -CACHY_BASE = expanduser("~/.cachy") +BASE_LEGACY = user_home() / ".mozilla/firefox/" +BASE = user_config() / "mozilla/firefox/" +FLATPAK_LEGACY = user_home() / ".var/app/org.mozilla.firefox/.mozilla/firefox/" +FLATPAK = user_home() / ".var/app/org.mozilla.firefox/config/mozilla/firefox/" + +SNAP_LEGACY = user_home() / "snap/firefox/common/.mozilla/firefox/" +SNAP = user_home() / "snap/firefox/common/.config/mozilla/firefox/" + +LIBREWOLF_BASE_LEGACY = user_home() / ".librewolf/" +LIBREWOLF_BASE = user_config() / "librewolf/librewolf" +LIBREWOLF_FLATPAK_LEGACY = user_home() / ".var/app/io.gitlab.librewolf-community/.librewolf/" +LIBREWOLF_FLATPAK = user_home() / ".var/app/io.gitlab.librewolf-community/config/librewolf/librewolf/" + +FLOORP_BASE = user_home() / ".floorp" +FLOORP_FLATPAK = user_home() / ".var/app/one.ablaze.floorp/.floorp" + +WATERFOX_BASE = user_home() / ".waterfox" +WATERFOX_FLATPAK = user_home() / ".var/app/net.waterfox.waterfox/.waterfox" + +CACHY_BASE = user_home() / ".cachy" # TODO can this be a GObject so I can use it in ListModels? # or must I make a wrapper?