* fix(audio): prepend silent preroll so sink-wake does not clip cues
Short catalog tones start in a fresh paplay process while PipeWire is
still waking a suspended sink, so the first tens of milliseconds of the
cue are missing. Prepend 100ms of silence in the same WAV, cache the
result by source path, mtime, and preroll length, and fall back to the
original file if the preroll cannot be built. CI mock players keep the
raw path.
Fixes#800
* fix(audio): cache sink-wake preroll without tempfile.mkstemp
Some tests replace sys.modules["tempfile"] with a MagicMock, so
mkstemp unpacking fails and playback falls back to the raw cue.
Name the cache file from a short sha256 of the source path, mtime,
and preroll length, write a sibling .partial, then os.replace it
into place. Reload real tempfile and wave in the audio feedback
tests so they still pass after those mocks leak.
Fixes#800
* fix(tests): pin real wave for preroll without reloading tempfile
Popping tempfile out of sys.modules in the audio feedback fixture
made preroll tests pass, but later whisper tests then bound
TemporaryDirectory from a MagicMock. Import real tempfile and wave
at module top, pin only wave while audio_feedback is reimported,
and bind TemporaryDirectory from the real stdlib if a sibling test
already replaced tempfile.
Fixes#800
* fix(audio): catch specific preroll errors and type test helpers
The new preroll path swallowed every exception, which hid bugs and
disagreed with AGENTS.md. Catch OSError, ValueError, and wave.Error
on the inner partial cleanup and the outer fallback so a bad WAV or
cache write still falls back to the original cue, and other errors
still propagate. Annotate the two test helpers the same review
flagged.
* fix(audio): fall back when truncated cues raise EOFError
wave.open can raise EOFError on a truncated RIFF before playback.
Catch it with the other preroll failures so start/stop still play the
original file.
Fixes#800
* feat(settings): even out dropdowns and tidy the About page
Pin combos to a shared right-hand column so long labels cannot stretch a
row. Fold the dictation-tone preview into a two-stage Start/Stop button,
quiet the model status card, shorten Performance copy, and rebuild About
with combined links, channel+Check, family platform tiles, and Talk to us
moved up next to the license.
* fix(settings): keep tone preview on start if the cue fails
Do not flip Start/Stop when preview_tone_cue returns false, so a missing
file or silent player cannot skip the start cue. Also annotate
PreferenceRow.set_title / set_subtitle.
* fix(settings): paint About icons for dark dialogs
Dark ink on Adwaita/Yaru-dark buttons made the family tiles and Talk to us
marks disappear. Pick a light foreground on dark themes (and keep the
original ink on light ones) so the marks match the labels.
* fix(settings): show Android and iOS marks on VocaPhone
VocaPhone covers both, so the family tile now uses the Android robot and
the Apple mark instead of a single phone icon.
* fix(settings): cut repeated About copy
Name the AGPL once on the License row. The VocaHQ blurb is just the
private-dictation line. VocaPhone says TestFlight, not source-only, and
Gateway is self-hosted and headless.
* fix(settings): space About icons from their labels
Talk-to-us buttons and the family tiles were packing the mark against the
text. Give the image an end margin and a wider gap in the family tile.
* fix(config): share one ConfigManager across the process
main(), the tray, and the sound-effects check each constructed their own
ConfigManager. Every instance loads the whole config into memory at
construction time and save_config() writes that whole cache back, so any
save from a stale instance silently reverts what another instance saved
in the meantime — no race needed, the second save just wins with old
values. The disk-read workarounds in main (_should_append_trailing_space)
already existed to dodge exactly this.
Add get_shared_config_manager() and route the three call sites through
it. It keeps ConfigManager constructible for tests, and the tray/dialog
pair already shared one instance, so this extends that arrangement to
the rest of the process.
Tests: a regression test runs the two-instance scenario through the
accessor, and a source guard fails if application code constructs
ConfigManager directly again. conftest drops the shared instance
between tests, since it would otherwise be created under whichever
mocks the first caller had active and leak them into later tests.
The two single-instance tests now patch acquire_lock itself: they used
patch.dict(sys.modules, ...), which main()'s "from . import" ignores
once an earlier test has imported the real module — they only passed
through an unrelated mock leaking into the tray's shortcut setup, which
this change stops.
* fix(config): lock the shared accessor and prove the revert it prevents
Review follow-ups on #691:
- guard the lazy create with a lock, so two threads racing the first
call cannot each build a ConfigManager and reintroduce #689
- take both handles before the first save in the accessor test, and
assert identity there — the old order let a fresh ConfigManager pass
- add a characterization test: two direct ConfigManager instances still
revert each other, which is the footgun the accessor exists to hide
- patch get_shared_config_manager in test_main and test_audio_feedback
instead of the class, so a leftover singleton cannot skip the mock
- close the ExitStack in _boot_under_patches when main() raises; the
caller only closes a stack it received, so a failure there leaked
those patches into every later test
* fix(audio): route the tone lookup through the shared ConfigManager
The tone picker merged on main (#707) constructs its own ConfigManager,
which is exactly the stale-cache overwrite this branch removes; the new
source guard in test_shared_config_manager.py flags it.
Settings → Audio can pick start/stop cues from the shared catalog
(lift, flick, ember, step, voca, soft, chirp, scale, drop, glass, off).
New installs and any missing or unknown sound_effects.tone value use
voca. A saved id, including off, is left alone.
The old start_recording/stop_recording files stay on disk. They are
no longer the implicit default. Error cues still follow the existing
enable switch.
* chore: bump minimum Python from 3.8 to 3.9 and adopt PEP 585 builtins
- Update requires-python, classifiers, mypy target, and black target in pyproject.toml
- Replace Python 3.8 with 3.9 in CI matrix and nightly workflow
- Replace typing.List/Dict/Tuple/Set with builtin list/dict/tuple/set (PEP 585)
- Update all 3.8 version references in README, docs, website, and install.sh
- Fix test_wayland_no_tools to mock IBus availability
- Remove List from common_types re-exports and update test
* style: reformat test files for Black 26.x and pin version in CI
Black 26.x introduced parenthesized context manager formatting.
Reformat 7 test files and pin Black>=26,<27 in CI to prevent
future drift from unpinned version upgrades.
Add a single toggle switch to enable/disable sound effects in the Audio
settings. This provides a simpler alternative to full sound customization
while covering the most common use case.
- Add sound_effects.enabled config option (default: true)
- Add toggle switch in Audio settings tab
- Add helper methods to ConfigManager
- Update play_*_sound() to check enabled state
- Add tests for the new functionality
Closes#283
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
* test: Improve test coverage to 80%
Increased test coverage from ~33% to 80.15% statement coverage.
New test files added:
- test_action_handler.py (100% coverage)
- test_version.py (100% coverage)
- test_logging_manager.py (100% coverage)
- test_common_types.py
- test_speech_recognition_init.py (100% coverage)
- test_resource_manager.py (93% coverage)
Significantly expanded existing tests:
- test_tray_indicator.py: 76% -> 98%
- test_main.py: 50% -> 92%
- test_text_injector.py: 60% -> 94%
- test_recognition_manager.py: 30% -> 48%
- test_command_processor.py: 85% -> 98%
- test_keyboard_shortcuts.py: 88% -> 95%
- test_config_manager.py: improved to 98%
- test_audio_feedback.py: improved to 92%
Configuration changes:
- Updated pyproject.toml to exclude untestable GTK dialog files
(settings_dialog.py, logging_dialog.py) from coverage metrics
Key testing improvements:
- Added comprehensive GTK mocking for UI components
- Added edge case tests for text injection on X11/Wayland
- Added keyboard shortcut injection tests
- Added error handling and callback tests
- Added configuration management tests
* fix: Use runtime CI mode detection to fix Python 3.8/3.10 tests
Change CI_MODE from module-level constant to _is_ci_mode() function
that is evaluated at runtime. This ensures pytest detection works
correctly on all Python versions, since pytest modules may not be
loaded at import time.
* fix: Add PYTEST_RUNNING env var for reliable pytest detection in CI
The previous approach to detect pytest at runtime wasn't working
reliably on Python 3.8/3.10 in CI environments. This adds a
PYTEST_RUNNING=1 environment variable set early in conftest.py
that _is_ci_mode() can check to reliably determine if tests are
running.
* fix: Use patch.object for reliable mocking in audio_feedback tests
The previous approach using patch() with module paths didn't work
reliably because the module reimport caused the patched objects
to not be the same as the ones the functions referenced.
Using patch.object() on the imported module's attributes ensures
the patches are applied to the actual objects the functions use.
* fix: Use patch.object for reliable mocking in test_settings_callback
The test was failing on Python 3.8/3.10 because the module-level patch
for SettingsDialog wasn't being applied reliably across all Python
versions due to import timing differences.
Using patch.object() on the imported module ensures the mock is applied
to the actual reference that _on_settings_clicked() uses, regardless of
Python version or module caching behavior.
* feat: prepare for v0.2.0-alpha release
* feat: prepare for v0.2.0-alpha release
* chore: remove CHANGELOG.md, use GitHub Releases instead
* refactor: use existing About dialog instead of Settings section
* style: enhance About dialog UI readability
* style: fix formatting with black and isort
* feat: show VAD and silence timeout settings for Whisper engine
- Renamed 'VOSK Settings' to 'Recognition Settings'
- VAD Sensitivity and Silence Timeout now apply to both engines
- Added tooltips explaining each setting
- VOSK-specific model info only shown when VOSK selected
* fix: resolve overlapping UI elements in settings dialog
* fix: save VAD and silence timeout settings for both engines
Previously these settings were only saved when VOSK was selected.
Now they persist correctly for Whisper as well.
* fix: show download progress when selecting undownloaded model
Previously when auto-applying settings, models would download silently
in the background without showing the progress dialog. Now:
- Download progress dialog appears for undownloaded models
- Model icons update correctly after download completes
* feat: add download progress, speed, ETA and cancel button
- Show real download progress percentage for VOSK models
- Display download speed (MB/s) and estimated time remaining
- Add Cancel button to abort downloads
- Update status during extraction phase
- Whisper downloads still pulse (uses internal downloader)
* fix: defer model download until needed, don't block startup
- App now starts instantly even if models aren't downloaded
- Models are downloaded on-demand when selected in Settings
- If user tries to dictate without a model, plays error sound
- Added model_ready property to check if engine is usable
* fix: improve first-run UX without downloaded models
- Settings dialog now defaults to smallest available model, not saved config
- Only show saved model if it's actually downloaded
- Add desktop notification when trying to dictate without a model
- Prevents auto-downloading large models on first launch
* feat: add download progress tracking for Whisper models
- Implement custom Whisper model download with progress/speed/ETA
- Uses OpenAI's CDN URLs directly instead of whisper.load_model()
- Progress callback works the same as VOSK downloads
- Supports cancellation during download
* fix: enable download progress callback for Whisper models
Progress callback was only being set for VOSK downloads, not Whisper.
Now both engines show real-time progress, speed, and ETA.
* fix: update tests for defer_download and config manager mocking
- test_main: Mock ConfigManager to prevent saved config from overriding test values
- test_settings_dialog: Mock ModelDownloadDialog to prevent GTK init errors
- test_speech_recognition: Use proper MockVoskModule class instead of MagicMock, pass defer_download=False for download test
- test_whisper_support: Pass defer_download=False to trigger model loading in test
* fix: resolve test failures for CI
- test_main: Fix ConfigManager patch path to use source module
- test_settings_dialog: Mock _is_vosk_model_downloaded and _is_whisper_model_downloaded to skip download flow, add missing dialog attributes
- test_speech_recognition: Replace recognizer with properly configured mock that returns valid JSON, fixing test isolation issues when running all tests together
* fix: mock _download_whisper_model in whisper test to avoid requests exception handling issue
* fix: add gir1.2-girepository-2.0 dep for PyGObject on Ubuntu 24.04
- Add missing girepository-2.0 package for newer Ubuntu runners
- Use --no-build-isolation for PyGObject to use system packages
* fix: skip PyGObject pip install, use system python3-gi package
- Use --no-deps for editable install to skip PyGObject
- Install other deps explicitly without PyGObject
- Add python3-gi-cairo for complete GTK support
- Removes problematic girepository-2.0 build requirement
* fix: install PyGObject via pip with proper build deps
- Remove python3-gi/python3-gi-cairo (doesn't work with setup-python)
- Install PyGObject from pip after installing build dependencies
- Simplify venv setup without --system-site-packages hack
* Fix GObject
* fix: set GI_TYPELIB_PATH for pip-installed PyGObject to find AppIndicator3
* Implement custom icon for Vocalinux (Closes#10)
* Fix system tray icon display issue
* Temp commit before purging
* refactor: restructure project into proper vocalinux package
* test: fix and improve audio_feedback and tray_indicator tests with proper mocking and isolation
* test: update import paths in test files to use the new package structure
* Remove icon background
* fix: implement robust resource path discovery in audio_feedback module
* fix: improve resource discovery across application for consistent icon and sound loading
* chore: ensure uninstall script is compatible with new resource paths
* Fix black formatting
* Improved test coverage for audio_feedback, main, and tray_indicator modules
* Fix test suite and improve code stability
* Black formatter changes
* Fixing import sorting