From 485679223b4338b9713e304ebb2f763e65a779f9 Mon Sep 17 00:00:00 2001 From: Will Miao Date: Sun, 27 Sep 2026 10:05:05 +0800 Subject: [PATCH] fix: return clipboard mode from _open_path on headless Linux Addresses PR review: on a native Linux/SSH session with neither DISPLAY nor WAYLAND_DISPLAY (and not Docker/WSL), _open_path unconditionally launched xdg-open and reported success even though no file manager can open. Mirror open_settings_location: hand the path to the browser for copying instead. Fixes open_backup_location, open_wildcards_location and open_sidecar_location together. --- py/routes/handlers/misc_handlers.py | 12 +++++++++++ tests/routes/test_misc_routes.py | 32 +++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/py/routes/handlers/misc_handlers.py b/py/routes/handlers/misc_handlers.py index 190c3124..4ad1bc56 100644 --- a/py/routes/handlers/misc_handlers.py +++ b/py/routes/handlers/misc_handlers.py @@ -3378,6 +3378,18 @@ class FileSystemHandler: elif sys.platform == "darwin": subprocess.Popen(["open", path]) else: + if not _has_gui_display(): + # Headless/SSH session: xdg-open cannot open a file + # manager, so hand the path to the browser for copying + # instead of reporting a success that never happened. + return web.json_response( + { + "success": True, + "message": "Headless session: path available for copying", + "path": path, + "mode": "clipboard", + } + ) subprocess.Popen(["xdg-open", path]) return web.json_response( diff --git a/tests/routes/test_misc_routes.py b/tests/routes/test_misc_routes.py index d11bf226..e8d5b459 100644 --- a/tests/routes/test_misc_routes.py +++ b/tests/routes/test_misc_routes.py @@ -523,6 +523,7 @@ async def test_open_backup_location_uses_settings_directory(tmp_path, monkeypatc monkeypatch.setattr(subprocess, "Popen", fake_popen) monkeypatch.setattr("py.routes.handlers.misc_handlers._is_docker", lambda: False) monkeypatch.setattr("py.routes.handlers.misc_handlers._is_wsl", lambda: False) + monkeypatch.setenv("DISPLAY", ":0") response = await handler.open_backup_location(FakeRequest()) # pyright: ignore[reportArgumentType] payload = _json_payload(response) @@ -551,6 +552,7 @@ async def test_open_sidecar_location_opens_configured_root(tmp_path, monkeypatch monkeypatch.setattr(subprocess, "Popen", fake_popen) monkeypatch.setattr("py.routes.handlers.misc_handlers._is_docker", lambda: False) monkeypatch.setattr("py.routes.handlers.misc_handlers._is_wsl", lambda: False) + monkeypatch.setenv("DISPLAY", ":0") response = await handler.open_sidecar_location(FakeRequest()) # pyright: ignore[reportArgumentType] payload = _json_payload(response) @@ -563,6 +565,35 @@ async def test_open_sidecar_location_opens_configured_root(tmp_path, monkeypatch assert calls == [["xdg-open", str(root)]] +@pytest.mark.asyncio +async def test_open_sidecar_location_headless_returns_clipboard_mode(tmp_path, monkeypatch): + """Without a GUI session xdg-open cannot work; the handler must hand the + path to the browser instead of reporting a success that never happened.""" + from py.services.settings_manager import get_settings_manager + + root = tmp_path / "sidecars" + get_settings_manager().set("sidecar_storage_path", str(root)) + + handler = FileSystemHandler(settings_service=SimpleNamespace()) + + monkeypatch.delenv("DISPLAY", raising=False) + monkeypatch.delenv("WAYLAND_DISPLAY", raising=False) + monkeypatch.setattr("py.routes.handlers.misc_handlers._is_docker", lambda: False) + monkeypatch.setattr("py.routes.handlers.misc_handlers._is_wsl", lambda: False) + + popen_calls = [] + monkeypatch.setattr(subprocess, "Popen", lambda *args, **kwargs: popen_calls.append(args)) + + response = await handler.open_sidecar_location(FakeRequest()) # pyright: ignore[reportArgumentType] + payload = _json_payload(response) + + assert response.status == 200 + assert payload["success"] is True + assert payload["mode"] == "clipboard" + assert payload["path"] == str(root) + assert popen_calls == [] + + @pytest.mark.asyncio async def test_get_settings_includes_resolved_sidecar_root(tmp_path): from py.services.settings_manager import get_settings_manager @@ -656,6 +687,7 @@ async def test_open_wildcards_location_creates_and_opens_directory(tmp_path, mon monkeypatch.setattr(subprocess, "Popen", fake_popen) monkeypatch.setattr("py.routes.handlers.misc_handlers._is_docker", lambda: False) monkeypatch.setattr("py.routes.handlers.misc_handlers._is_wsl", lambda: False) + monkeypatch.setenv("DISPLAY", ":0") monkeypatch.setattr( "py.services.wildcard_service.get_wildcards_dir", lambda create=False: str(wildcards_dir.mkdir(parents=True, exist_ok=True) or wildcards_dir)