From 30fa5a9d7bd5d2a91c9fd6a85f9506a6613acbec Mon Sep 17 00:00:00 2001 From: Eli Date: Wed, 8 Jul 2026 04:10:55 +0200 Subject: [PATCH 1/3] fix(webdav): avoid malformed subdirectory URLs --- src/portkeydrop/protocols.py | 1 + tests/test_protocols.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/portkeydrop/protocols.py b/src/portkeydrop/protocols.py index 5eee2b9..8e3d263 100644 --- a/src/portkeydrop/protocols.py +++ b/src/portkeydrop/protocols.py @@ -449,6 +449,7 @@ def connect(self) -> None: self._client = Client( { "webdav_hostname": self._build_hostname(), + "webdav_root": "", "webdav_login": self._info.username, "webdav_password": self._info.password, "webdav_timeout": self._info.timeout, diff --git a/tests/test_protocols.py b/tests/test_protocols.py index a34249b..c08f290 100644 --- a/tests/test_protocols.py +++ b/tests/test_protocols.py @@ -276,6 +276,7 @@ def test_connect_builds_webdavclient_options(self, monkeypatch): client_class.assert_called_once_with( { "webdav_hostname": "https://dav.example.com", + "webdav_root": "", "webdav_login": "alice", "webdav_password": "secret", "webdav_timeout": 12, @@ -318,6 +319,7 @@ def test_connect_preserves_explicit_webdav_url_and_port(self, monkeypatch): assert ( client_class.call_args.args[0]["webdav_hostname"] == "http://dav.example.com:8080/root" ) + assert client_class.call_args.args[0]["webdav_root"] == "" def test_list_dir_maps_webdav_items(self, monkeypatch): webdav = MagicMock() From 0f8e060ac1e3ef435ffb45dfa0d4e842f77af2a9 Mon Sep 17 00:00:00 2001 From: Eli Date: Thu, 9 Jul 2026 04:36:51 +0200 Subject: [PATCH 2/3] fix: defer startup speech notice until UI exists --- CHANGELOG.md | 1 + src/portkeydrop/app.py | 31 ++++++++++++++++--------------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6aabb35..b44dd22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file. ### Fixed - Pressing Delete or F2 while editing a text field no longer triggers file delete or rename — those keys now act only inside the file panes, and file operations tell you to focus a pane first instead of guessing (and sometimes targeting a remote file you never touched). +- WebDAV connections now force an empty library root so opening subfolders keeps valid URLs instead of accidentally generating malformed nested paths. - Background transfer completions no longer jump your position in the file lists back to the top; deletes and renames keep you on the nearest neighbouring file, and sorting or toggling hidden files keeps your selection. - The "Announce file count" setting now actually speaks the count when entering a directory; empty folders and empty filter results are announced instead of silent. - The Speech settings tab (rate, volume, verbosity) now controls the built-in speech output instead of doing nothing; verbosity also controls transfer progress announcements. diff --git a/src/portkeydrop/app.py b/src/portkeydrop/app.py index 004553f..2931e87 100644 --- a/src/portkeydrop/app.py +++ b/src/portkeydrop/app.py @@ -184,14 +184,6 @@ def __init__(self) -> None: self._exit_sound_played = False self._announcer = ScreenReaderAnnouncer() self._apply_speech_settings() - if not self._announcer.is_available(): - # Deferred so the activity log exists when this runs. - wx.CallAfter( - self.log_event, - "Speech output is unavailable; announcements will appear in the " - "status bar and this log only. Install the 'prismatoid' package " - "to enable speech.", - ) self._soundpacks_dir = ensure_default_soundpack(get_soundpacks_dir()) audio_settings = getattr(self._settings, "audio", None) self._sound_player = SoundPlayer( @@ -208,6 +200,13 @@ def __init__(self) -> None: self._build_toolbar() self._build_dual_pane() self._build_status_bar() + if not self._announcer.is_available(): + wx.CallAfter( + self.log_event, + "Speech output is unavailable; announcements will appear in the " + "status bar and this log only. Install the 'prismatoid' package " + "to enable speech.", + ) self._bind_events() self._update_title() self._refresh_local_files() @@ -2662,13 +2661,15 @@ def log_event(self, message: str) -> None: """Append a timestamped entry to the activity log and announce it.""" timestamp = datetime.datetime.now().strftime("%H:%M:%S") entry = f"[{timestamp}] {message}\n" - if self.FindFocus() is self.activity_log: - # Don't yank the review cursor while the user is reading the log. - position = self.activity_log.GetInsertionPoint() - self.activity_log.AppendText(entry) - self.activity_log.SetInsertionPoint(position) - else: - self.activity_log.AppendText(entry) + activity_log = getattr(self, "activity_log", None) + if activity_log is not None: + if self.FindFocus() is activity_log: + # Don't yank the review cursor while the user is reading the log. + position = activity_log.GetInsertionPoint() + activity_log.AppendText(entry) + activity_log.SetInsertionPoint(position) + else: + activity_log.AppendText(entry) self._announce(message) def _announce(self, message: str) -> None: From c0e6ac188c52fbf6d9e1aaeee8cdf260d2f32da4 Mon Sep 17 00:00:00 2001 From: Eli Date: Thu, 9 Jul 2026 04:39:08 +0200 Subject: [PATCH 3/3] test: cover focused activity log updates --- tests/test_app.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/test_app.py b/tests/test_app.py index 28ab0bc..4be0620 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -149,6 +149,19 @@ def test_main_frame_init_sets_transfer_state(tmp_path, app_module): transfer_service_cls.assert_called_once_with(notify_window=frame, max_workers=2) +def test_log_event_preserves_review_cursor_when_activity_log_has_focus(app_module): + app, _ = app_module + frame = _hydrate_frame(app_module) + frame.FindFocus = MagicMock(return_value=frame.activity_log) + frame.activity_log.GetInsertionPoint.return_value = 7 + + frame.log_event("Speech output is unavailable") + + frame.activity_log.AppendText.assert_called_once() + frame.activity_log.SetInsertionPoint.assert_called_once_with(7) + frame._announce.assert_called_once_with("Speech output is unavailable") + + def test_bind_events_hooks_transfer_update(app_module): app, _ = app_module frame = object.__new__(app.MainFrame)