diff --git a/hermes_cli/browser_connect.py b/hermes_cli/browser_connect.py index 12b4565d1d..0d0b786ad6 100644 --- a/hermes_cli/browser_connect.py +++ b/hermes_cli/browser_connect.py @@ -104,14 +104,30 @@ _WINDOWS_PROGID_MAP = ( ) # Linux xdg default-web-browser .desktop name fragments → canonical key. +# Includes the Flatpak application ids (``com.google.Chrome.desktop`` etc.), +# which share none of the native package name fragments. _LINUX_DESKTOP_MAP = ( ("google-chrome", "chrome"), + ("com.google.chrome", "chrome"), ("chromium", "chromium"), ("brave", "brave"), ("microsoft-edge", "edge"), + ("com.microsoft.edge", "edge"), ("msedge", "edge"), ) +# Where sandboxed Linux packages keep the profile instead of $XDG_CONFIG_HOME. +_LINUX_FLATPAK_IDS = { + "chrome": "com.google.Chrome", + "chromium": "org.chromium.Chromium", + "brave": "com.brave.Browser", + "edge": "com.microsoft.Edge", +} +_LINUX_SNAP_PROFILE_PARTS = { + "chromium": ("snap", "chromium", "common", "chromium"), + "brave": ("snap", "brave", "current", ".config", "BraveSoftware", "Brave-Browser"), +} + # macOS LaunchServices bundle-id fragments → canonical key. _DARWIN_BUNDLE_MAP = ( ("com.google.chrome", "chrome"), @@ -150,10 +166,12 @@ def _real_profile_relparts(browser: str) -> tuple: def real_profile_data_dir(browser: str, system: str | None = None) -> str | None: """Return the default user-data-dir for a Chromium ``browser`` on ``system``. - Returns None for unknown browsers. Does not check existence — callers that - need that should stat the result. Paths are built with the TARGET system's - separator (posix for Darwin/Linux, backslash for Windows) so an explicit - ``system`` argument resolves correctly regardless of the host OS. + Returns None for unknown browsers. On Linux the native ($XDG_CONFIG_HOME), + snap and Flatpak locations are tried and the first existing one wins; the + native path is returned when none exists so the caller's error names it. + Darwin/Windows paths are not stat'ed. Paths are built with the TARGET + system's separator (posix for Darwin/Linux, backslash for Windows) so an + explicit ``system`` argument resolves correctly regardless of the host OS. """ if browser not in _CHROMIUM_BROWSERS: return None @@ -167,7 +185,19 @@ def real_profile_data_dir(browser: str, system: str | None = None) -> str | None return ntpath.join(local, *win_parts) # Linux / other POSIX config = os.environ.get("XDG_CONFIG_HOME") or posixpath.join(home, ".config") - return posixpath.join(config, *linux_name.split("/")) + candidates = [posixpath.join(config, *linux_name.split("/"))] + snap_parts = _LINUX_SNAP_PROFILE_PARTS.get(browser) + if snap_parts: + candidates.append(posixpath.join(home, *snap_parts)) + flatpak_id = _LINUX_FLATPAK_IDS.get(browser) + if flatpak_id: + candidates.append( + posixpath.join(home, ".var", "app", flatpak_id, "config", *linux_name.split("/")) + ) + for candidate in candidates: + if os.path.isdir(candidate): + return candidate + return candidates[0] def chromium_executable(browser: str, system: str | None = None) -> str | None: diff --git a/tests/hermes_cli/test_browser_connect_default_chromium.py b/tests/hermes_cli/test_browser_connect_default_chromium.py index a47f248b31..4a654d8e66 100644 --- a/tests/hermes_cli/test_browser_connect_default_chromium.py +++ b/tests/hermes_cli/test_browser_connect_default_chromium.py @@ -104,3 +104,69 @@ class TestDetectDefaultDarwin: def test_bundle_map(self, bundle, expected): with self._run_with(_ls_dump(_handler("https", bundle))): assert bc._detect_default_darwin() == expected + + +class TestDetectDefaultLinux: + def _run_with(self, output: str): + class _Proc: + stdout = output + + return patch.object(bc.subprocess, "run", return_value=_Proc()) + + @pytest.mark.parametrize( + "desktop,expected", + [ + ("google-chrome.desktop", "chrome"), + ("com.google.Chrome.desktop", "chrome"), + ("chromium_chromium.desktop", "chromium"), + ("org.chromium.Chromium.desktop", "chromium"), + ("brave-browser.desktop", "brave"), + ("com.brave.Browser.desktop", "brave"), + ("microsoft-edge.desktop", "edge"), + ("com.microsoft.Edge.desktop", "edge"), + ("firefox.desktop", None), + ("org.mozilla.firefox.desktop", None), + ("", None), + ], + ) + def test_xdg_desktop_names(self, desktop, expected): + with self._run_with(desktop + "\n"): + assert bc._detect_default_linux() == expected + + def test_missing_xdg_settings_fails_closed(self): + with patch.object(bc.subprocess, "run", side_effect=FileNotFoundError("xdg-settings")): + assert bc._detect_default_linux() is None + + +class TestLinuxProfileDir: + def _env(self, monkeypatch, home): + monkeypatch.setenv("HOME", str(home)) + monkeypatch.delenv("XDG_CONFIG_HOME", raising=False) + + def test_native_path_when_nothing_exists(self, tmp_path, monkeypatch): + self._env(monkeypatch, tmp_path) + assert bc.real_profile_data_dir("chromium", "Linux") == str(tmp_path / ".config" / "chromium") + + def test_snap_chromium_profile_is_found(self, tmp_path, monkeypatch): + self._env(monkeypatch, tmp_path) + snap = tmp_path / "snap" / "chromium" / "common" / "chromium" + snap.mkdir(parents=True) + assert bc.real_profile_data_dir("chromium", "Linux") == str(snap) + + def test_flatpak_chrome_profile_is_found(self, tmp_path, monkeypatch): + self._env(monkeypatch, tmp_path) + flatpak = tmp_path / ".var" / "app" / "com.google.Chrome" / "config" / "google-chrome" + flatpak.mkdir(parents=True) + assert bc.real_profile_data_dir("chrome", "Linux") == str(flatpak) + + def test_native_profile_wins_when_present(self, tmp_path, monkeypatch): + self._env(monkeypatch, tmp_path) + native = tmp_path / ".config" / "BraveSoftware" / "Brave-Browser" + native.mkdir(parents=True) + (tmp_path / ".var" / "app" / "com.brave.Browser" / "config" / "BraveSoftware" / "Brave-Browser").mkdir(parents=True) + assert bc.real_profile_data_dir("brave", "Linux") == str(native) + + def test_xdg_config_home_is_honoured(self, tmp_path, monkeypatch): + monkeypatch.setenv("HOME", str(tmp_path)) + monkeypatch.setenv("XDG_CONFIG_HOME", "/home/t/.config") + assert bc.real_profile_data_dir("edge", "Linux") == "/home/t/.config/microsoft-edge"