* 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.
Fix missing close button in settings window title bar on Ubuntu 22.04
(GNOME 42). The dialog was created with flags=0 and no transient parent,
which causes GTK to not apply decorations on certain window managers.
Explicitly call set_decorated(True) to ensure the close button is always
visible regardless of WM hints.
Fixes#323
* test(coverage): improve test coverage from 57% to 80%+
Add 30 new test files with 861 additional test cases to improve
overall test coverage from 57% to 80%+ (statement coverage).
Key modules covered:
- recognition_manager.py: 43% → 69% (audio device detection, transcription,
model download, engine initialization, buffer management)
- text_injector.py: improved coverage for X11/Wayland text injection,
environment detection, clipboard fallback, shortcut injection
- ibus_engine.py: 22% → 73% (engine lifecycle, text injection, socket server)
- evdev_backend.py: 11% → 78% (device detection, key event handling)
- pynput_backend.py: 43% → 76% (start/stop, key handling, callbacks)
- keyboard_backends/__init__.py: 54% → improved (backend creation, env detection)
- whispercpp_model_info.py: 32% → improved (GPU detection, model recommendations)
- about_dialog.py, first_run_dialog.py, main.py, single_instance.py,
common_types.py, config_manager.py, autostart_manager.py: various improvements
All 1433 tests pass with proper test isolation to prevent mock pollution
between test modules.
* test(quality): audit and fix test quality issues
Code review audit identified and fixed the following issues:
- Remove bare try/except blocks that swallow all errors (7 tests fixed
in recognition_manager_deep, replaced with proper assertions or deleted)
- Delete source code inspection tests that read .py files as strings
instead of testing behavior (deleted test_about_dialog_coverage.py and
test_first_run_dialog_coverage.py entirely - ~35 useless tests removed)
- Add meaningful assertions replacing weak assertIsNotNone/assertIsInstance
checks (common_types_coverage, ibus_engine_coverage, keyboard_backends_extra)
- Fix tests that mock the system under test instead of its dependencies
(text_injector_extra now mocks subprocess.run instead of injection methods)
- Add missing assertions to tests with no verification
(ibus_engine focus_out, log window info, copy to clipboard)
- Verify download tests check requests.get was called with correct URLs
(coverage_boost download tests)
- Fix pytest warning by renaming test_audio_input import to avoid
pytest collecting it as a test function
All 1356 tests pass with 0 failures and 0 warnings.
Statement coverage: 80.3% (3257/4054 lines covered).
* style(tests): fix black formatting and isort import ordering
Auto-format all new test files with black and isort to pass CI lint checks.
* fix(tests): resolve CI hangs and rename test files for clarity
- Fix CI hangs by properly mocking gi/GTK/IBus in all test files
- Fix sys.modules pollution in recognition manager tests by using tmp_path
- Add pytest-timeout to prevent future hangs
- Rename 28 test files to use descriptive, consistent names mapping to source modules
* fix(tests): resolve Python 3.13 CI failures
- Fix _load_stdlib_module() in test_recognition_manager_downloads.py to
handle stdlib modules that are packages (directories with __init__.py)
rather than single .py files. In Python 3.13, zipfile became a package,
causing FileNotFoundError during test collection (exit code 2).
- Fix invalid escape sequence '$' in test_cross_distro_compatibility.py
by using a raw string. Python 3.13 reports this as SyntaxWarning and
Python 3.14 will make it a SyntaxError.
* fix(tests): use direct import for zipfile instead of _load_stdlib_module
The _load_stdlib_module approach fails on Python 3.13 where zipfile is a
package (zipfile/__init__.py). Loading it via spec_from_file_location with
a custom name breaks relative imports inside the package (e.g.
'from ._path import ...' resolves to '_stdlib_zipfile._path' which doesn't
exist).
Simply import zipfile at module level before any test mocks can pollute
sys.modules — this is safe because the module-level code runs during test
collection, before any test fixtures or patches are applied.
* fix(tests): mock gi/GTK in conftest to prevent CI hangs
On CI, real PyGObject is installed and module-level imports in vocalinux
source files (ibus_engine, tray_indicator, settings_dialog, etc.) trigger
GTK initialization or IBus daemon connections that hang indefinitely in
headless environments. pytest-timeout's signal-based timeout cannot
interrupt these C-level blocking calls.
Fix by mocking gi and gi.repository in conftest.py (which runs before any
test module is collected), ensuring all test files are protected regardless
of whether they remember to mock individually.
Also add timeout-minutes: 10 to the CI test job as a safety net so that
any remaining hangs fail fast instead of running indefinitely.
* fix(tests): unconditionally mock gi in conftest for CI
The previous 'if gi not in sys.modules' guard was ineffective on CI
because PyGObject is installed via pip, so the real gi module is already
loaded. This caused GTK initialization and IBus daemon connections that
hang indefinitely in headless CI environments.
Now unconditionally replace gi and gi.repository with mocks during test
collection, regardless of whether the real package is installed.
* fix(tests): reduce timeout to 10s and use thread-based timeout
Use timeout_method=thread instead of signal (default) so that
pytest-timeout can interrupt C-level blocking calls (GTK operations,
socket accepts, subprocess waits) that the signal-based method cannot
handle. Also reduce per-test timeout from 30s to 10s since no legitimate
unit test should take longer, and hanging tests were consuming the entire
CI job timeout budget.
* fix(tests): set global socket timeout to prevent blocking on CI
Tests in test_ibus_engine.py create real Unix sockets with daemon threads
that block on socket.accept(). When tests fail or timeout, these threads
remain blocked indefinitely, preventing pytest from completing. The
thread-based timeout catches the hang but cannot terminate the blocking
accept() call.
Set socket.setdefaulttimeout(5) in conftest.py so all socket operations
(accept, connect, recv) will raise socket.timeout after 5 seconds instead
of blocking forever.
* fix(tests): add cleanup fixture for IBus socket server threads
Add autouse fixture that stops any leftover VocalinuxEngine socket server
threads after each test. Tests that trigger _start_socket_server() spawn
daemon threads blocking on socket.accept() that persist after the test
completes, eventually causing pytest-timeout to kill the process.
The cleanup fixture sets _server_running=False and closes the server
socket, which unblocks the accept() call and allows the daemon thread
to terminate.
* fix(tests): reduce socket timeout to 0.1s to avoid slowing tests
The 5-second socket timeout caused socket operations in daemon threads
to block for 5 seconds per iteration before timing out, significantly
slowing the test suite on CI. Reduce to 0.1s so blocking socket calls
fail fast without noticeably impacting test execution time.
* fix(tests): use 2s socket timeout and increase CI timeout to 15min
The 0.1s socket timeout was too aggressive and caused legitimate test
socket operations to fail. Use 2s which is long enough for test sockets
to work but short enough to unblock daemon threads.
Increase CI job timeout to 15 minutes to accommodate the socket timeout
overhead from daemon threads that retry every 2 seconds.
* fix(tests): block known-blocking daemon threads to prevent CI hangs
Instead of using socket timeouts (which either break tests or are too
slow), prevent specific known-blocking daemon threads from starting:
- ibus_engine.server_thread: blocks on socket.accept()
- evdev_backend._monitor_devices: blocks on select.select()
These threads are identified by their target function name and skipped
during Thread.start(). The thread is marked as started/stopped so
join() calls in cleanup code don't raise RuntimeError.
Test-created daemon threads (e.g. mock socket servers) are unaffected
since they use different target function names.
* fix(tests): patch Thread.join for Python 3.13 compatibility
Python 3.13 changed threading internals — join() now uses _handle.join()
which raises 'thread not started' for threads that were never actually
started. Instead of trying to fake internal state, patch join() to be a
no-op for threads that were skipped by our _safe_thread_start guard.
This fixes the 2 remaining test_evdev_backend_ext failures on Python 3.13.
- Remove setup.py: redundant with pyproject.toml, had drifted deps
(missing pywhispercpp, psutil, evdev; no platform markers; stale
Python 3.12 max). pip has supported pyproject.toml since v21.3 (2021).
- Remove build-website.sh: broken script referencing non-existent
fix-paths.sh. Web deploy is handled by npm scripts and GitHub Actions.
- Clean up CI triggers: remove setup.py and setup.cfg references from
distro-test-matrix.yml and unified-pipeline.yml path filters.
* docs: improve project description with narrative intro
- Replace the one-liner README intro with a more compelling narrative
that highlights key differentiators: offline, open source, multi-engine,
GPU-accelerated, and privacy-first
- Update version.py __description__ to a more descriptive one-liner
* docs: replace all em-dashes with hyphens/commas/colons
Remove all em-dash (—) characters from README.md, replacing them
with contextually appropriate alternatives (hyphens, commas, colons).
- unified-pipeline.yml: replace blanket .github/workflows/** glob with
specific .github/workflows/unified-pipeline.yml in both web and python
filters, so editing nightly.yml or release.yml no longer triggers the
full lint/test/build matrix
- distro-test-matrix.yml: add paths filter at workflow trigger level so
the 4-container distro matrix only runs when installer, scripts,
packaging, or version files change (previously ran on every push/PR
including doc-only and web-only changes)
Saves CI minutes on documentation, website, and unrelated workflow edits.
* feat(docs): add Voca Ecosystem section to README and website
Add cross-project ecosystem shoutouts linking VocaLinux, VocaMac, and
VocaWin. README gets a table with platform, project, website, GitHub,
and status columns. Website gets animated cards with platform logos,
tech badges, status indicators, and links.
Uses simpleicons CDN for Apple logo and inline SVG for Windows logo
(simpleicons CDN does not serve a Windows icon).
* feat(web): UI refinements — terminal install boxes, contrast fixes, wider card grids
- Replace CodeBlock components with terminal-style command boxes (traffic-light dots, copy button, dark bg)
- Fix dark mode contrast: add dark:text-black to badges and buttons on green backgrounds
- Header: replace GitHub icon with filled yellow Star icon next to star count
- Hero buttons: rename and reorder (Install Now, We're Open Source, Use Windows or Mac?)
- Engine section: widen container from max-w-5xl to max-w-7xl to match features
- Ecosystem section: add VocaLinux 'You are here' card, widen from max-w-4xl to max-w-7xl
- Fix Windows icon color to official Microsoft Fluent blue (#0078D4)
- Remove unused CodeBlock import
* feat(web): link sub-pages, reorder cards, restyle hero, update FAQ, remove em-dashes
- Add href prop to FeatureCard: 6 tiles link to /offline/, /use-cases/,
/gpu-acceleration/, /shortcuts/, /wayland/, /for-developers/
- Engine section: reorder to Whisper > whisper.cpp (center) > VOSK,
move 'Compare all engines' to centered link below cards
- whisper.cpp card keeps 'GPU acceleration' link
- FAQ answers: convert to ReactNode with inline links to sub-pages,
fix 6 stale answers (distros, requirements, languages, shortcuts,
voice commands, engine switching)
- Hero buttons: GitHub-dark 'We're Open Source', ghost 'Use Windows
or Mac?'
- Ecosystem: reorder to VocaMac > VocaLinux (center) > VocaWin,
change badge from 'You are here' to 'Beta'
- Remove all em-dashes (9 occurrences)
The install.sh on main now dynamically resolves the latest release tag
via GitHub API (merged in PR #313), so pinning to a specific release tag
in install commands is no longer needed — and was causing users to run
the old hardcoded installer.
Update all curl install command URLs across docs, code, and website from
versioned tags (v0.9.0-beta, v0.8.0-beta) to main/install.sh:
- README.md (4 URLs + note text)
- docs/INSTALL.md (2 URLs + note text)
- docs/UPDATE.md (2 URLs)
- docs/DISTRO_COMPATIBILITY.md (1 URL)
- tests/distro-testing-checklist.md (1 URL)
- src/vocalinux/main.py (1 URL)
- web/src/app/page.tsx (remove dynamic release fetch, hardcode main)
When running via `curl | bash`, stdin (fd 0) is the pipe — not the
terminal. The script was setting INTERACTIVE_MODE='ask' and then trying
to `read` from the pipe, which returned EOF/garbage, defaulting to
interactive mode, which then immediately errored because there was no TTY.
Fix: if stdin is a pipe but /dev/tty is accessible, do `exec < /dev/tty`
to redirect stdin to the real terminal so user prompts work correctly.
If there is truly no terminal (headless/CI), fall back to automatic mode
without erroring.
Also fix the misleading error message which suggested running the exact
same curl|bash command that just failed.
* chore(release): prepare v0.9.0-beta
- Bump version from 0.8.0-beta to 0.9.0-beta
- Update README What's New section with new features and bug fixes
- Update all install script URLs to v0.9.0-beta
- Add v0.9.0-beta What's New section to docs/UPDATE.md
- Update SECURITY.md supported versions (0.9.x supported, 0.8.x dropped)
- Update website schema softwareVersion and nav badge to 0.9.0
- Update website featureList with new sound effects and clipboard features
- Add v0.9.0-beta entry to changelog page
- Update web/package.json version to 0.9.0
- Add v0.9.0-beta row to RELEASE_PROCESS.md version history
* docs(readme): add contributors avatar grid via contrib.rocks
* feat(about): add Changelog link to About dialog
* Revert "feat(about): add Changelog link to About dialog"
This reverts commit 15e0984f516caccb14a0609e96d5fb6870ea247e.
The IBus injection path had an early return that skipped the clipboard
copy logic. Store the result, copy to clipboard if enabled, then return.
Also add xclip and wl-clipboard to install.sh for all supported distros
so clipboard tools are available out of the box.
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>
* fix: improve Debian/pipx installation experience
- Enhanced error messages in check_dependencies() to provide distro-specific
installation commands (Ubuntu/Debian, Fedora, Arch Linux)
- Added explicit pipx guidance: install system packages BEFORE pipx install
- Updated error message to use gir1.2-ayatanaappindicator3-0.1 for Debian
- Added comprehensive pipx installation section to DISTRO_COMPATIBILITY.md
with step-by-step instructions for all major distributions
* fix: add display availability check to prevent GTK crash on Debian
The original PR only added documentation and improved error messages.
This commit adds the actual fix for issue #288.
Root cause: On some systems (especially Debian), GTK widgets can be
created before the display connection is properly established, causing
the crash: gtk_widget_get_scale_factor: assertion 'GTK_IS_WIDGET (widget)' failed
Fix: Add check_display_available() function that verifies Gdk.Display
is available before any GTK widgets are created. This provides a clear
error message instead of a cryptic GTK crash.
Fixes#288
* fix: add clipboard fallback for Wayland compositors without virtual keyboard support
Fixes#297
On KDE Plasma Wayland, the compositor doesn't support the virtual keyboard
protocol that wtype requires. When both wtype and xdotool fallback fail,
text injection was completely broken.
Changes:
- Add _fallback_to_clipboard() method that tries wl-copy, xclip, and xsel
- When all injection methods fail, copy text to clipboard so user can paste
- Play success sound when clipboard fallback succeeds (text is recoverable)
- Improve error messages with KDE-specific guidance and ydotool instructions
For KDE Plasma Wayland users, the recommended solution is:
1. Install ydotool: sudo apt install ydotool
2. Enable daemon: sudo systemctl enable --now ydotoold
Alternatively, clipboard fallback will copy text for manual pasting.
* style: fix black formatting
* fix: wrap clipboard fallback in try/except to handle test mocks
* feat(clipboard): add configurable always-copy-to-clipboard feature
Enhanced the clipboard functionality beyond just a fallback mechanism:
- Renamed _fallback_to_clipboard to _copy_to_clipboard (more general)
- Added _should_copy_to_clipboard() to read user preference from config
- After successful text injection, copies to clipboard if setting enabled
- Still falls back to clipboard on injection failure regardless of setting
- Added 'copy_to_clipboard' setting to config (default: true)
- Added toggle switch in Settings > General for user control
This provides better UX by:
1. Allowing users to reference pasted text later via clipboard history
2. Providing a backup when injection partially works
3. Enabling easy pasting into apps where injection might not work
Fixes#297
* feat(clipboard): show notification when text copied as fallback
When text injection fails but clipboard succeeds, show a desktop
notification telling the user to paste with Ctrl+V. This ensures
users know their text was saved and can be recovered.
Reset last_injected_text when recognition transitions to LISTENING state,
ensuring the first dictation segment in each session is not prefixed with
a space. Also simplify the space-prepending logic: always add a separator
space between consecutive segments (not just after punctuation), and strip
incoming text as a safety net against whisper tokenizer artifacts.
Allow users to bind voice commands to specific modifier key sides (e.g.,
Right Ctrl only), preventing conflicts with common left-side shortcuts
like Alt+Tab or Ctrl+C.
- Expand SUPPORTED_SHORTCUTS from 3 to 9 entries (either/left/right)
- Add SHORTCUT_GROUPS for grouped display in settings UI
- Implement side-aware key matching in pynput and evdev backends
- Update settings dialog with grouped shortcut selector
- Add comprehensive tests for side-specific shortcuts
Closes#306
On app restart, the shortcut mode (toggle vs push-to-talk) was not being
passed to KeyboardShortcutManager during initialization in TrayIndicator.
The backend always defaulted to 'toggle' mode regardless of user config,
causing push-to-talk shortcuts to not bind correctly on startup.
Fixes#296
* feat(shortcuts): add push-to-talk mode as alternative to toggle
This commit implements push-to-talk functionality (Issue #252) as an
alternative to the existing double-tap toggle mode:
- Add keyboard shortcut mode setting (toggle vs push_to_talk)
- In push-to-talk mode: hold key to speak, release to stop
- Update both pynput and evdev backends to support press/release callbacks
- Add UI controls in settings dialog for mode selection
- Maintain backward compatibility with existing toggle mode as default
Closes#252
Related: Closes#185 (superseded by push-to-talk approach)
* fix(shortcuts): remove duplicate code blocks that broke settings dialog
The previous push-to-talk implementation had duplicate code blocks
in tray_indicator.py and settings_dialog.py that caused the settings
dialog to fail silently. This commit cleans up the duplicate code
while preserving the push-to-talk functionality.
Fixes issue where settings dialog would not open.
* fix(ui): add set_subtitle method to PreferenceRow class
The PreferenceRow class was missing a set_subtitle method, which is called by
_update_shortcut_ui_for_mode when switching between toggle and push-to-talk
modes. This caused an AttributeError that silently crashed the settings dialog
during initialization.
Also stores subtitle_label as instance variable for later updates.
* fix(keyboard): remove duplicate code and fix mode handling
Critical fixes for keyboard backends:
- evdev_backend.py: Removed duplicate __init__ and _handle_key_event methods
- pynput_backend.py: Removed duplicate __init__, _on_press, _on_release methods
- Both backends now properly handle toggle and push-to-talk modes
Fixed first_run_dialog.py:
- Added do_response() override to properly close dialog
- Added explicit destroy() call in show_first_run_dialog()
Fixed tray_indicator.py:
- Added _unregister_keyboard_callbacks() method
- Updated _setup_keyboard_shortcuts() to properly clean up old callbacks
- Updated update_shortcut() to properly handle mode switching
Fixed settings_dialog.py:
- Added set_subtitle() method to PreferenceRow class
- Updated UI text for push-to-talk mode
- Fixed _on_shortcut_changed to properly pass mode to callback
All 547 tests pass locally.
* feat(shortcuts): improve mode switching and add mode-aware display names
Key improvements:
- Add SHORTCUT_MODE_DISPLAY_NAMES for mode-specific shortcut names
- Add get_shortcut_display_name() function for mode-aware display
- Fix restart_with_shortcut() to properly save and restore all callbacks
- Fix update_shortcut() to properly handle mode changes
- Fix _setup_keyboard_shortcuts() to clear old callbacks before registering new ones
- Ensure only one type of callback is active at a time
This fixes issues where:
- Old callbacks remained registered after mode switch
- Single-tap triggered action after switching from push-to-talk to toggle
- Display names weren't appropriate for the selected mode
* fix(shortcuts): stabilize live mode switching callback lifecycle
* test(shortcuts): cover callback reset on mode transitions
* fix(shortcuts): remove super preset and migrate legacy config
* test(shortcuts): update coverage for super preset removal
* fix(text-injection): detect if IBus is the active input method before using it
This commit fixes issue #249 where Vocalinux incorrectly tries to use
IBus for text injection when IBus is installed but not configured as
the active input method (e.g., user uses ydotool on Arch + KDE Plasma 6).
Changes:
- Add is_ibus_active_input_method() function that checks GTK_IM_MODULE,
QT_IM_MODULE, and XMODIFIERS environment variables to determine if
IBus is the active input method
- Update _check_dependencies() to skip IBus when it's not the active
input method, falling back to ydotool/wtype/xdotool instead
- Add comprehensive unit tests for the new functionality
The fix ensures Vocalinux will use the appropriate text injection
method based on the user's actual input method configuration,
rather than just checking if IBus is installed.
Fixes#249
* test: fix existing tests to mock is_ibus_active_input_method
* test: add comprehensive test coverage for is_ibus_active_input_method
Add additional tests to ensure complete coverage of the new functionality:
- Edge cases for empty string env vars
- Case-insensitive detection for all three env vars
- Priority ordering (GTK > Qt > XMODIFIERS)
- X11 fallback when IBus is not active input method
- Exception handling during IBus setup falls back correctly
Total tests: 62 (was 54), all passing.
Remove MagicMock/ directory that was accidentally committed during
previous PR. This was a test artifact that shouldn't be in the repo.
Also add MagicMock/ to .gitignore to prevent future occurrences.
* fix(audio): detect and use device-supported channel count
This fixes the "Invalid number of channels" (PyAudio error -9998) that
occurs with certain audio devices (e.g., HD-Audio Generic interfaces) that
require stereo (2 channel) input instead of mono (1 channel).
Changes:
- Add _get_supported_channels() helper function to detect supported channel
configuration by testing mono first, then falling back to stereo
- Update test_audio_input() to detect channels before opening stream
- Update _record_audio() to detect channels and convert stereo to mono
- Update _attempt_audio_reconnection() to use detected channel count
- Add stereo-to-mono conversion in audio processing loop to ensure speech
recognition engines receive mono audio as expected
Fixes issues introduced by voice commands feature where audio settings test
would fail with "no speech detected" because the audio stream couldn't be
opened due to channel mismatch.
Closes audio device compatibility issues
* fix(settings): delay callback restoration to prevent race condition in test
The recognition test was restoring text callbacks immediately after
stop_recognition(), but the recognition thread has a 1-second timeout.
If whisper.cpp transcription took longer than 1 second, the callbacks
would be restored before transcription completed, causing the text to
be sent to the wrong callback (the main app callback instead of the
test callback).
Changes:
- Add 500ms delay before restoring callbacks to allow transcription to complete
- Add new _restore_callbacks_and_check_result() helper method
- Increase result check delay from 200ms to 300ms for more reliable detection
This fixes the "No speech detected during test" issue where audio was
being recorded and transcribed, but the result wasn't being captured by
the test callback.
* debug: add diagnostic logging to trace recognition test flow
* fix(recognition): prevent race condition between audio enqueue and recognition thread
- Increase audio thread join timeout from 0.1s to 2.0s to ensure audio thread fully exits
- Remove redundant second audio thread join
- Add comprehensive debug logging to trace recognition thread lifecycle
- Add logging for final buffer enqueue to diagnose race conditions
The race condition occurred because:
1. should_record=False was set
2. Recognition thread could check queue.empty() before audio was enqueued
3. Recognition thread would exit before processing the final audio segment
This fix ensures the audio thread completes before we process the final buffer.
* fix(recognition): use poison pill pattern to prevent premature thread exit
The recognition thread was exiting immediately when should_record=False
and the queue was empty, even if audio was about to be enqueued.
Changes:
- Recognition loop now runs until it receives None signal (poison pill)
- When queue times out and should_record=False, waits 0.5s for final items
- When None is received, drains all remaining queue items before exiting
- Increased recognition thread join timeout from 1s to 5s for transcription
This ensures the recognition thread stays alive long enough to process
the final audio buffer that is enqueued after should_record is set to False.
* feat(commands): make voice commands optional with auto-enable for VOSK
This implements a new voice commands feature that:
- Adds 'voice_commands_enabled' config option (None=auto, True, False)
- Auto-enables voice commands for VOSK engine (which needs them for punctuation)
- Auto-disables for Whisper engines (which handle punctuation natively)
- Adds a toggle in Settings > Recognition tab to override the default
The toggle shows the current auto-detected state and allows users to
enable/disable voice commands regardless of engine choice.
This addresses issue #147 discussion about voice commands utility with
modern Whisper models that handle punctuation well.
Closes#270 (by providing an alternative solution - disabling commands)
* fix(tests): add voice_commands_enabled parameter to test expectations
Update test assertions to include the new voice_commands_enabled
parameter that was added to SpeechRecognitionManager.
* style(tests): fix black formatting in test_speech_recognition.py
* fix(commands): keep voice command mode in sync on reconfigure
The first-run dialog was crashing on Fedora/Wayland when clicking any
button with the error: gtk_widget_get_scale_factor: assertion
'GTK_IS_WIDGET (widget)' failed.
Root cause: Direct destroy() calls from button click handlers caused
race conditions where GTK tried to access widget properties after the
widget was marked for destruction but before event processing completed.
Fix: Use GTK's proper dialog response signal pattern instead of direct
destroy() calls. Buttons are now added with response IDs, and the
response signal handler sets the result. This lets GTK manage the
dialog lifecycle properly and prevents the race condition.
Fixes#267
* fix(text_injection): re-check for better tools after xdotool fallback
When wtype fails due to compositor not supporting virtual keyboard protocol,
the code switches to xdotool permanently. This prevents using ydotool
even if its daemon is started later.
This fix adds _try_recover_from_fallback() method that checks for better
tools (ydotool with running daemon, or wtype with compositor support)
on each text injection attempt when in fallback mode.
Fixes issue #34 comment 3 (logs show xdotool but ydotool works)
* feat(main): auto-start IBus daemon on Vocalinux launch
This resolves the IBus autostart issue mentioned in issue #34.
When Vocalinux starts, it now automatically starts the IBus daemon if:
- IBus is installed on the system
- IBus daemon is not already running
This removes the need for users to manually configure IBus autostart
on desktop environments where it doesn't start automatically (e.g.,
some KDE Plasma installations or minimal window managers).
Changes:
- Add start_ibus_daemon() function in ibus_engine.py
- Call start_ibus_daemon() in main() after logging is initialized
- Export start_ibus_daemon in text_injection/__init__.py
- only transition to LISTening if still recording and currently in processing state
- Cleaner state management simplifies thread safety logic
- Fixes#258: high CPU behavior during prolonged sessions
* fix: auto-detect audio sample rate for device compatibility (#262)
- Add _get_supported_sample_rate() helper function
- Tests device default rate first, then common rates (48k, 44.1k, 32k, 22k, 16k, 8k)
- Fixes 'Invalid sample rate' error for devices like Vocaster One (48kHz only)
- Update test_audio_input(), _record_audio(), and _attempt_audio_reconnection()
- Return detected sample_rate in test_audio_input() result dict
* fix: import pyaudio inside _get_supported_sample_rate
* fix: resample audio to 16kHz for Vosk/Whisper compatibility
- Store detected sample rate in self._capture_sample_rate
- Resample audio chunks to 16kHz before appending to buffer (linear interpolation)
- Initialize _capture_sample_rate to 16000 in __init__
- Update both _record_audio() and _attempt_audio_reconnection()
This ensures devices like Vocaster One (48kHz only) can capture audio
and speech recognition engines (Vosk/Whisper) receive 16kHz as expected.
* style: fix black formatting
* feat(ui): show sample rate info in audio test results
- Display detected sample rate in test results (e.g., '48kHz → 16kHz auto')
- Remove redundant numpy import in _record_audio function
- Helps users understand that non-16kHz devices are supported
* feat(web): add 8 new SEO-optimized pages for organic traffic
## New Pages Added
| Page | Purpose | SEO Value |
|------|---------|-----------|
| `/languages/` | Multilingual support (9+ languages) | International SEO, language-specific keywords |
| `/wayland/` | Wayland support guide | Technical SEO, Linux display server queries |
| `/rsi-prevention/` | Health & accessibility focus | High-intent health keywords |
| `/gpu-acceleration/` | GPU/Vulkan support | Performance-focused technical SEO |
| `/open-source/` | FOSS & privacy positioning | Open source community keywords |
| `/autostart/` | v0.7.0 feature highlight | Linux desktop integration keywords |
| `/for-developers/` | Developer use cases | Developer productivity keywords |
| `/offline/` | Privacy-first positioning | Privacy-conscious user keywords |
## Navigation Improvements
- Redesigned header with hamburger menu for mobile
- Categorized navigation with 5 sections: Get Started, Features, Use Cases, Privacy & Ethics, Support
- Added breadcrumbs for better UX and SEO
- Footer with complete site map organized by category
## Technical Updates
- Updated sitemap.xml with all 21 pages
- All pages include JSON-LD structured data (Article schema)
- Full SEO metadata with targeted keywords per page
## Build Verification
- All 21 pages generated successfully (including dynamic install/[distro] pages)
- Build passes without errors
* feat(web): add dropdown navigation to SEO subpages
- Desktop: 5 dropdown menus (Get Started, Features, Use Cases, Privacy & Ethics, Support)
- Mobile: Hamburger menu with categorized pages
- All pages now accessible from any SEO subpage
- Click outside to close dropdowns
- Breadcrumbs and footer links remain unchanged
* fix(web): make SEO header dropdowns open on hover and focus
* fix(web): remove dead zone between dropdown trigger and menu
* chore(release): prepare v0.7.0-beta
## Version Bump (v0.6.3-beta → v0.7.0-beta)
This is a major beta release with significant new features including autostart support, tabbed settings, and improved GPU detection.
### Core Version Files
- Updated src/vocalinux/version.py to 0.7.0-beta
- Updated pyproject.toml classifiers (already correct - Beta)
### Documentation
- Updated README.md with new features, bug fixes, and version references
- Updated docs/INSTALL.md with v0.7.0-beta install commands
- Updated docs/UPDATE.md with comprehensive 'What's New' section
- Updated SECURITY.md supported versions table (0.7.x now supported)
### Website
- Updated web/src/app/page.tsx version badge (v0.7.0 Beta)
- Updated web/src/app/page.tsx softwareVersion schema
- Updated web/package.json version to 0.7.0
- Updated web/src/app/changelog/page.tsx with v0.7.0-beta release entry
### Other Updates
- Updated install.sh default tag to v0.7.0-beta
- Updated src/vocalinux/main.py error message install command
- Updated AGENTS.md branch naming example
## Changes Since v0.6.3-beta
### New Features (13 commits)
- Autostart on login support via XDG autostart
- Tabbed settings dialog with 5 organized tabs
- Intel GPU compatibility detection with automatic CPU fallback
- Single instance prevention
- Evdev device management to prevent CPU spin
- Improved GPU detection avoiding false positives
- IBus fallback when daemon not running
- Fedora dnf check-update fix
- Web SEO - 7 new optimized pages
### Contributors
- Jatin K Malik (maintainer)
- Antoine Monnet (contributor)
- jatinkrmalik (contributor)
### Bug Fixes
- #254: Improved GPU detection to avoid false positives
- #251: Skip IBus setup when daemon not running
- #243: Prevent dnf check-update from exiting script on Fedora
- #240: Raise exception on IBus setup failure
- #234: Removed leading space from first speech transcription
- #242/#253: Removed disconnected evdev devices to prevent CPU spin
## Verification
- [x] Lint passed
- [x] Black formatting passed
- [x] isort check passed
- [x] Version imports correctly as 0.7.0-beta
* docs: update version history in RELEASE_PROCESS.md
Add entries for 0.6.0-beta through 0.7.0-beta to the version history table.
* fix(installer): improve GPU detection to avoid false positives
- Filter out software renderers (llvmpipe, swiftshader, lavapipe, etc.)
when checking Vulkan compatibility - these are CPU implementations
and should not affect hardware GPU detection
- Skip Vulkan compatibility check for NVIDIA GPUs since they use CUDA
instead of Vulkan for acceleration
- Reorder backend priority: CUDA > Vulkan > CPU
- Only show incompatible GPU warning for actual Vulkan-based GPUs
(AMD/Intel) that lack VK_KHR_16bit_storage support
Fixes issue where RTX 5080 + llvmpipe combination was incorrectly
flagged as incompatible due to llvmpipe lacking 16-bit storage support.
* fix(installer): recommend GPU even without dev libraries installed
The installer installs dev libraries (libvulkan-dev, glslc) in
install_system_dependencies(), which runs AFTER the backend selection.
So we should recommend GPU if there's a compatible GPU regardless
of whether dev libraries are currently installed.
Changes:
- NVIDIA GPU detected → recommend CUDA (even without nvcc)
- Vulkan-compatible GPU → recommend Vulkan (even without libvulkan-dev)
- Only fall back to CPU if no compatible GPU detected or GPU is
genuinely incompatible (old Intel Gen7)
* feat: detect incompatible Intel GPUs and fallback to CPU
Implements two-layer protection for users with older Intel GPUs that lack
VK_KHR_16bit_storage support required by whisper.cpp Vulkan backend.
Affected GPUs: Intel Gen7 and older (Ivy Bridge, Haswell, Sandy Bridge)
- Intel HD Graphics 2500/4000 (Ivy Bridge)
- Intel HD Graphics 4400/4600 (Haswell)
Layer 1: Install-Time Detection (Prevention)
- New check_vulkan_gpu_compatibility() function in install.sh
- Parses GPU name from vulkaninfo output
- Checks against known incompatible patterns
- Shows clear warning and recommends CPU backend
- Updates detect_whispercpp_backends() to skip Vulkan for incompatible GPUs
Layer 2: Runtime Fallback (Safety Net)
- Added error handling in recognition_manager.py _init_whispercpp()
- Catches RuntimeError with "16-bit storage" or "Unsupported device"
- Automatically disables Vulkan/CUDA and retries with CPU
- Shows desktop notification explaining the fallback
Fixes#238
* fix(gpu): improve compatibility detection and add tests
When an input device is disconnected, select() reports the fd as
readable but read() fails with ENODEV. Previously, the code caught
the error but kept the dead fd in the list, causing an infinite
busy loop that consumed 100% CPU.
Now disconnected devices are removed from both self.devices and
self.device_fds when read() fails, preventing the busy loop.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Antoine Monnet <39551484+AntoineMontane@users.noreply.github.com>
Co-authored-by: AntoineMontane <monnet.antoine@gmail.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add device.close() before removing from device list
- Add threading.Lock for thread-safe device list modifications
- Log device name when disconnected for troubleshooting
Addresses review comments from PR #242
When an input device is disconnected, select() reports the fd as
readable but read() fails with ENODEV. Previously, the code caught
the error but kept the dead fd in the list, causing an infinite
busy loop that consumed 100% CPU.
Now disconnected devices are removed from both self.devices and
self.device_fds when read() fails, preventing the busy loop.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: AntoineMontane <monnet.antoine@gmail.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
On desktop environments like KDE Plasma, the IBus daemon is not started
by default. Previously, Vocalinux would attempt to register its IBus
engine anyway, which would fail and (before #240) not properly fall back
to alternatives.
This change:
- Adds is_ibus_daemon_running() to check if ibus-daemon is running
- Skips IBus setup entirely when daemon isn't running
- Logs an informative message explaining why IBus is skipped
- Falls back to ydotool/xdotool immediately (no wasted time)
Fixes#250🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: AntoineMontane <monnet.antoine@gmail.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* refactor(ui): convert settings dialog to tabbed layout
Convert the settings dialog from a long scrolling list to a tab-based UI
using Gtk.Notebook. This follows GNOME HIG best practices for settings
panels with 3-5 sections.
Changes:
- Add notebook CSS styling for modern tab appearance
- Create 4 tab content boxes (Recognition, Audio, Shortcuts, General)
- Replace ScrolledWindow with Gtk.Notebook
- Reorganize settings sections into appropriate tabs:
- Recognition: Engine, Model, Language, VAD, Silence, Test
- Audio: Device selection, Audio level test
- Shortcuts: Keyboard shortcuts
- General: Autostart, Start minimized
- Widen dialog to 700px to accommodate tab content
This eliminates vertical scrolling and provides a cleaner UX with
logical grouping of related settings.
* refactor(settings): split recognition tab into Speech Engine and Recognition tabs
- Reordered tabs by importance: Speech Engine, Recognition, Audio, Shortcuts, General
- Split the Recognition tab into two separate tabs:
- Speech Engine tab: Engine, Model Size, Language, Model Info
- Recognition tab: VAD Sensitivity, Silence Timeout, Test Recognition, Recognition Status
- This reduces vertical height of each tab and improves usability
* fix(ui): reduce settings dialog height to 50% of screen
Previously set to 75% which made the dialog unnecessarily tall.
* fix(ui): remove redundant close button from settings dialog
Settings auto-apply, so the bottom close button is unnecessary.
Users can close using the title bar X button.
* test(settings): update test for no-action-buttons pattern
The settings dialog now uses title bar close instead of a bottom
Close button, so update the test accordingly.
* fix: prevent multiple instances from running simultaneously
Add single-instance enforcement using file locking to prevent duplicate
instances when auto-start is enabled on boot (issue #244).
Changes:
- Add single_instance.py module with fcntl-based file locking
- Check for existing instance early in main() before any initialization
- Register atexit handler to release lock on normal exit
- Lock file located at ~/.local/share/vocalinux/instance.lock
If another instance is already running, the app exits gracefully with
a helpful error message telling the user how to resolve the situation.
* fix: add GTK notification when duplicate instance detected
Show a system notification to the user when another instance is already
running, in addition to the existing stderr/log output.
Changes:
- Import Notify from gi.repository when duplicate instance detected
- Show notification with error icon and message
- Give notification time to display before exiting (0.5s sleep)
- Fallback to silent exit if notification fails (e.g., headless)
* feat(ui): add auto-start on boot support
- Add --start-minimized CLI flag for autostart entry
- Create autostart_manager.py to handle XDG autostart desktop entry
- Add autostart config option in config_manager.py
- Add Start on Login toggle in Settings dialog
- Add Start Minimized toggle in Settings dialog
- Add Start on Login checkbox in system tray menu
- Add first-run dialog to ask about autostart preference
Closes#184
* fix(test): add first_run: False to mock config settings
The tests were failing with StopIteration because main() now checks
for first_run and shows a dialog if True. By setting first_run: False
in the mock config, we skip the first-run dialog code path.
* fix(ui): harden autostart behavior across Linux sessions
Use XDG-compliant autostart paths and robust Exec command resolution, keep first-run prompt semantics correct, and fix GTK toggle handling so state changes are applied reliably.
* docs(autostart): document XDG startup model and limits
Clarify that Vocalinux uses desktop-session XDG autostart, not systemd services, and explain compatibility expectations for mainstream DEs versus minimal/custom WM sessions.
* feat(ui): add first-run quick-start guidance text
Expand the welcome dialog with concise first-time usage guidance and clear direction to the tray menu Settings entry while preserving the existing autostart decision flow.
* fix(uninstall): remove XDG autostart desktop entry
Ensure uninstall removes ~/.config autostart registration and reports leftover autostart entries during verification.
* fix(install): ignore defunct processes in running-check
Filter installer process detection to exclude defunct entries and installer/uninstaller shells so stale zombie rows do not block reinstall.
* fix(uninstall): ignore defunct processes in running-check
Use filtered process detection so zombie entries and installer shells do not trigger false running-process warnings during uninstall.
dnf check-update returns exit code 100 when system updates are
available, which causes the script to exit immediately due to set -e.
Added '|| true' to prevent this non-error condition from stopping
the installer.
Fixes#34 (Fedora installation failure)
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
* fix(ibus): raise exception on setup failure to enable fallback
- Add IBusSetupError exception class for detailed error context
- Modify _setup_engine() to raise exception instead of silent return
- Ensures text_injector.py fallback mechanism works when IBus fails
- Add unit tests for IBusSetupError and setup failure scenarios
Fixes silent IBus text injection failures on Wayland/Fedora reported in #34
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
* test(text-injector): add tests for IBus setup failure fallback
- Test that TextInjector falls back to xdotool when IBus fails
- Test fallback on both X11 and Wayland environments
- Test that text injection still works after IBus failure
Related to #34
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
---------
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
- Remove fixed min-width constraints from comparison tables
- compare.tsx: remove min-w-[860px]
- alternatives.tsx: remove min-w-[600px]
- privacy.tsx: remove min-w-[500px]
- Tables now use native overflow-x-auto for horizontal scroll
- Fix subpage navigation overflow on mobile:
- Show only icons on mobile (no text labels)
- Add horizontal scroll support
- Add proper aria-labels for accessibility
- Add hover states for better touch feedback
Fixes responsive design issues identified during mobile audit.