From 61bfb1e58e5c2228049d7c9472d607647785939e Mon Sep 17 00:00:00 2001 From: Jatin K Malik Date: Mon, 2 Nov 2026 18:46:31 -0800 Subject: [PATCH] fix(ui): escape device names and test errors in the audio status label (#1043) The Settings audio status label is set with Pango markup. A microphone named with '&' or '<' (or an audio test error carrying them) made the markup invalid, so GTK dropped the text. Escape both with GLib.markup_escape_text. Also correct the v0.17.0 notes in docs/UPDATE.md: the Snap never bundled VOSK; it installs the project without extras. --- docs/UPDATE.md | 2 +- src/vocalinux/ui/settings_dialog.py | 7 +++++-- tests/test_pulse_sources.py | 32 +++++++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/docs/UPDATE.md b/docs/UPDATE.md index d76ccb7..0109824 100644 --- a/docs/UPDATE.md +++ b/docs/UPDATE.md @@ -324,7 +324,7 @@ See the [full changelog](https://github.com/VocaHQ/vocalinux/releases/tag/v0.18. - Distro matrix runs `install.sh` (#810) - `install.sh` Faster Whisper box is CPU CTranslate2 / INT8, not NVIDIA CUDA. The whisper.cpp GPU step does not install the CUDA toolkit; Vulkan is first, CUDA only if a toolkit is already on the machine (#826) -AppImage and Flatpak remain whisper.cpp only. Snap ships whisper.cpp plus VOSK. Faster Whisper and Parakeet are optional extras on `install.sh` / source installs. +AppImage, Flatpak and Snap remain whisper.cpp only. Faster Whisper and Parakeet are optional extras on `install.sh` / source installs. See the [full changelog](https://github.com/VocaHQ/vocalinux/releases/tag/v0.17.0). diff --git a/src/vocalinux/ui/settings_dialog.py b/src/vocalinux/ui/settings_dialog.py index a97f32b..37c6d27 100644 --- a/src/vocalinux/ui/settings_dialog.py +++ b/src/vocalinux/ui/settings_dialog.py @@ -9977,7 +9977,10 @@ For now, the engine has been reverted to VOSK.""" self.speech_engine.set_audio_device(device_index, device_name) logger.info(f"Audio device changed to: [{device_index}] {device_name}") - self.audio_test_status.set_markup(f"Selected: {device_label}") + # Device names are free text ("Mic & Line", ""); escape them for Pango. + self.audio_test_status.set_markup( + f"Selected: {GLib.markup_escape_text(device_label or '')}" + ) def _cancel_audio_test(self, *_args: object) -> None: """Cancel capture and invalidate any queued result when Settings closes.""" @@ -10048,7 +10051,7 @@ For now, the engine has been reverted to VOSK.""" "Check if microphone is muted or try a different device" ) else: - error_msg = result.get("error", "Unknown error") + error_msg = GLib.markup_escape_text(str(result.get("error", "Unknown error"))) self.audio_test_status.set_markup( f"✗ Test failed: {error_msg}" ) diff --git a/tests/test_pulse_sources.py b/tests/test_pulse_sources.py index 055e37b..98197a0 100644 --- a/tests/test_pulse_sources.py +++ b/tests/test_pulse_sources.py @@ -679,6 +679,38 @@ class TestAudioDeviceTooltip: dialog.audio_device_combo.set_tooltip_text.assert_called_with(self._SOF) +class TestAudioStatusMarkup: + """Device names and errors are free text; the status label parses Pango markup.""" + + def _glib(self) -> MagicMock: + from xml.sax.saxutils import escape + + return MagicMock(markup_escape_text=MagicMock(side_effect=escape)) + + def test_selected_device_name_is_escaped(self) -> None: + settings_dialog = _load_settings_dialog() + dialog = MagicMock(_initializing=False, _pulse_source_names={}) + dialog.audio_device_combo.get_active_id.return_value = "2" + dialog.audio_device_combo.get_active_text.return_value = "Mic & Line " + with patch.object(settings_dialog, "GLib", self._glib()): + settings_dialog.SettingsDialog._on_audio_device_changed(dialog, None) + dialog.audio_test_status.set_markup.assert_called_with( + "Selected: Mic & Line <A2DP>" + ) + + def test_test_failure_message_is_escaped(self) -> None: + settings_dialog = _load_settings_dialog() + dialog = MagicMock() + with patch.object(settings_dialog, "GLib", self._glib()): + settings_dialog.SettingsDialog._handle_audio_test_result( + dialog, {"success": False, "error": "Invalid device & busy"} + ) + dialog.audio_test_status.set_markup.assert_called_with( + "✗ Test failed: " + "Invalid device <hw:0,6> & busy" + ) + + def _load_settings_dialog() -> ModuleType: """Import settings_dialog with real GTK base classes so its methods are callable.