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.
This commit is contained in:
Jatin K Malik
2026-11-02 18:46:31 -08:00
committed by GitHub
parent c461cf5147
commit 61bfb1e58e
3 changed files with 38 additions and 3 deletions
+1 -1
View File
@@ -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).
+5 -2
View File
@@ -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"<i>Selected: {device_label}</i>")
# Device names are free text ("Mic & Line", "<A2DP>"); escape them for Pango.
self.audio_test_status.set_markup(
f"<i>Selected: {GLib.markup_escape_text(device_label or '')}</i>"
)
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."""
"<small>Check if microphone is muted or try a different device</small>"
)
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"<span foreground='#c01c28'>✗ Test failed:</span> {error_msg}"
)
+32
View File
@@ -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 <A2DP>"
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(
"<i>Selected: Mic &amp; Line &lt;A2DP&gt;</i>"
)
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 <hw:0,6> & busy"}
)
dialog.audio_test_status.set_markup.assert_called_with(
"<span foreground='#c01c28'>✗ Test failed:</span> "
"Invalid device &lt;hw:0,6&gt; &amp; busy"
)
def _load_settings_dialog() -> ModuleType:
"""Import settings_dialog with real GTK base classes so its methods are callable.