Override do_destroy() to clear _active_instance when IBus destroys the
engine on keyboard layout switch (Super+Space). Add retry logic in
inject_text() — on NO_ENGINE, re-activates via switch_engine() and
retries once to recover cleanly.
Co-authored-by: Sbenazar <127324689+Sbenazar@users.noreply.github.com>
Replace hard-coded stop tail trim with configurable stop_sound_guard_ms
(default 200ms) so quick key release no longer clips the final spoken
word in push-to-talk mode.
Co-authored-by: Fakhareddine <fe.sabbari@claranet.com>
- Bump version from 0.10.1-beta to 0.10.2-beta in version.py
- Update README.md What's New section with v0.10.2 highlights and bug fixes
- Update SECURITY.md supported versions table
- Update docs/UPDATE.md with v0.10.2 release notes
- Update docs/RELEASE_PROCESS.md version history table
- Update web/src/app/page.tsx softwareVersion and version badge
- Update web/src/app/changelog/page.tsx with v0.10.2 release entry
- Update web/package.json and web/package-lock.json to 0.10.2
Key changes in this release:
- Non-ASCII text injection via ydotool clipboard fallback (#376)
- IBus detection on Wayland without legacy env vars (#381)
- IBus engine process startup fix (#361)
- Missing deps for Pop!_OS and Ubuntu 24.04+ (#379)
- Systematic code quality refactor (#377)
- Redesigned website OG image (#392)
* fix(text-injection): handle non-ASCII characters with ydotool via clipboard paste (fixes#362)
ydotool simulates evdev key events which only map to US ASCII keycodes.
Non-ASCII characters (accented letters like á, é, ú, ñ, ü, etc.) get
silently dropped during injection, so 'Esdrújula' becomes 'Esdrjula'.
When ydotool is the active Wayland injection tool and the text contains
non-ASCII characters, the new code:
1. Copies text to clipboard via wl-copy (or xclip/xsel fallback)
2. Simulates Ctrl+V via ydotool key to paste it
3. Falls back to regular ydotool type if clipboard tools unavailable
This preserves all Unicode characters perfectly. ASCII-only text still
uses ydotool type directly for lower latency.
* style: fix black formatting in text_injector.py
* test: add coverage for clipboard paste paths (xclip/xsel/wtype fallbacks)
Covers the uncovered paths flagged by Codecov:
- xclip fallback when wl-copy unavailable
- xsel fallback when wl-copy/xclip unavailable
- wtype Ctrl+V paste simulation
- Paste failure (Ctrl+V fails after clipboard copy)
- No clipboard tools available
* refactor(text-injection): eliminate duplication and dead code in clipboard paste path
- _inject_via_clipboard_paste now delegates to _copy_to_clipboard instead
of reimplementing wl-copy/xclip/xsel selection (removes ~30 lines of
duplication identified in code review)
- Remove unreachable wtype branch from _inject_via_clipboard_paste; wtype
handles Unicode natively via Wayland virtual-keyboard protocol so it
never calls this method
- Drop unnecessary time.sleep(0.05) after clipboard write; subprocess.run
for wl-copy is synchronous so the sleep was a no-op
- Add debug log noting the clipboard will be temporarily overwritten so
operators can observe the side-effect without diving into the code
test improvements:
- Fix fragile string-repr assertion in test_ydotool_ascii_uses_type_directly;
now compares actual list slices and verifies wl-copy is NOT called
- Replace test_clipboard_paste_with_wtype with test_wtype_injects_unicode_directly
which actively asserts wtype calls the tool directly without clipboard
- Add test_inject_text_ydotool_non_ascii_end_to_end covering the full
inject_text -> _inject_with_wayland_tool -> _inject_via_clipboard_paste
stack for accented text on Wayland+ydotool (fixes#362)
- Remove now-unnecessary @patch('time.sleep') decorators from three tests
* fix(ibus): start engine process before checking registration
_setup_engine() gated on is_engine_registered() which queries
`ibus list-engine`. On systems where the IBus daemon doesn't scan
user-level component directories (~/.local/share/ibus/component/),
the engine never appeared in the list, blocking start_engine_process()
from ever being called — even though its register_component() D-Bus
call is the correct registration path for standalone-launched engines.
Reorder _setup_engine() to start the engine process first, relying on
register_component() for runtime registration. Remove the XML
install step from the setup path since install_ibus_component()
triggers `ibus restart` which kills the engine process.
Follow-up to PR #304 which added register_component() support.
* fix(ibus): remove dead code and add engine readiness check
Address review feedback:
- Remove install_ibus_component(), is_engine_registered(),
is_component_up_to_date(), _get_expected_component_xml()
and their tests — no longer called after setup order fix
- Add SOCKET_PATH poll after start_engine_process() to verify
engine is fully initialized before activation
- Rebase onto current main
Fix two related IBus issues on KDE Plasma Wayland (and similar DEs):
1. IBus not detected without legacy environment variables:
On Wayland, IBus is configured via the DE's Virtual Keyboard setting
and IBus itself recommends unsetting GTK_IM_MODULE / QT_IM_MODULE.
The detection logic only checked these env vars, so it never found
IBus on a properly configured Wayland session.
Fix: When no IM env vars are set, check if ibus-daemon is running
and has an active engine. If another IM (e.g. Fcitx) is explicitly
configured via env vars, respect that and don't override.
2. IBus text injection silently fails on Wayland:
After switching to the Vocalinux IBus engine, the code called
setxkbmap to capture/restore the keyboard layout. On Wayland,
setxkbmap does not reflect the compositor's state and returns
incorrect results (e.g. 'us' when the user has 'de'). Calling
setxkbmap to restore this wrong layout interfered with IBus's
ability to commit text, causing silent injection failures.
Fix: Skip setxkbmap-based XKB layout management entirely on
Wayland sessions. The Wayland compositor manages keyboard layouts
natively, and setxkbmap can only cause harm there.
Fixes#380
* fix(suspend): delay keyboard restart to 6s for USB re-enumeration
The 2s delay is enough for the speech engine, but USB devices
re-enumerate slowly after resume. At T+2s, stale /dev/input nodes
still exist and evdev opens them, but the kernel tears them down
at ~T+4s, causing another round of disconnections.
Split the reinit into two separate timers:
- 2s: speech engine reinit (unchanged)
- 6s: keyboard backend restart (after USB re-enumeration completes)
* fix(suspend): use inotify to detect device settle before keyboard restart
Replace hard-coded 6s keyboard restart delay with event-driven
Gio.File.monitor_directory() on /dev/input. After resume, a 2s settle
timer resets on each inotify event; keyboard shortcuts restart only once
devices stop changing. A 10s safety cap and a 6s fallback timer guard
against environments where inotify is unavailable.
After suspend/resume, evdev devices disconnect and the monitor thread
exits when all file descriptors close. The resume handler only
reinitialized the speech engine, leaving keyboard shortcuts dead.
Restart _setup_keyboard_shortcuts() in _reinit_after_resume() to
re-scan /dev/input and open fresh device file descriptors.
Copy-to-clipboard is now opt-in. Users can enable it in Settings under Text Injection. This avoids unexpected clipboard overwrites on fresh installs and reduces background wl-copy/xclip timeout warnings on Wayland where clipboard access from daemon threads can be unreliable.
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
* feat(suspend): add D-Bus based suspend/resume handler
Monitors org.freedesktop.login1.Manager.PrepareForSleep via Gio.DBusProxy to detect system suspend/resume. On resume, invokes a callback to reinitialize the audio pipeline and speech model so dictation works immediately without manual intervention.
Fixes#367
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
* feat(suspend): wire suspend handler into tray indicator and recognition manager
TrayIndicator now creates a SuspendHandler on startup that listens for D-Bus PrepareForSleep signals. On suspend, active recognition is stopped. On resume, a 2-second delay allows audio hardware to stabilize before reinitializing the speech engine and resetting audio reconnection state.
Fixes#367
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
---------
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
* fix(ptt): prevent premature transcription during push-to-talk silence
In push-to-talk mode, a brief silence while holding the hotkey would
trigger transcription mid-hold, injecting text before key release.
Add recognition_mode parameter to start_recognition() so the silence
timeout in _record_audio() defers enqueuing audio in PTT mode. Audio
accumulates in the buffer until key release calls stop_recognition(),
which already handles final buffer enqueue. Toggle mode is unchanged.
* fix(clipboard): run clipboard copy in background thread to prevent blocking
The _copy_to_clipboard calls (wl-copy, xclip, xsel) have 5s timeouts
each and run synchronously on the recognition thread. When they hang,
they block stop_recognition() from completing, delaying the state
transition to IDLE and preventing the next push-to-talk press.
Move clipboard copy to a daemon thread on the two success paths so it
never blocks transcription or state transitions. The failure-fallback
path stays synchronous since it needs the return value.
* fix(keyboard): preserve XKB layout when activating Vocalinux IBus engine
When Vocalinux starts, it may start or restart the IBus daemon, which
can override the user's system-level keyboard layout with IBus's
configured layout (often defaulting to US).
This fix captures the current XKB layout before activating the
Vocalinux IBus engine and restores it after activation, ensuring
users with non-US keyboard layouts don't lose their settings.
Fixes#292
* fix(keyboard): restore XKB layout after engine activation, not just on shutdown (#292)
The draft PR only restored the user's XKB layout when Vocalinux shut
down. The French AZERTY reporter confirmed the real problem: switching
to the Vocalinux IBus engine overrides the system keyboard layout
immediately, so typing 'a' produces 'q' (AZERTY mapped through US
QWERTY). The same applies to Spanish and other non-US layouts.
Changes:
- Restore XKB layout immediately after IBus engine activation in
_setup_engine(), not just in stop(). This preserves the user's
keyboard layout while Vocalinux is actively running.
- Remove the 'layout != us' guard in stop() -- always restore
whatever layout was captured, including US with custom options.
- Fix 4 broken tests: wrong expected options values, missing
TimeoutExpired argument, incorrect call count assertion.
- Add test cases for Spanish and French AZERTY layouts.
* test(keyboard): add coverage for XKB layout capture/restore in IBusTextInjector
Addresses Codecov report on PR #343 - adds tests covering:
- stop() restores captured XKB layout (Spanish with variant/options)
- stop() skips restore when no layout was captured
- _setup_engine() captures XKB layout and restores it after engine activation (French AZERTY)
* fix(test): mock start_engine_process in setup_engine XKB test
The test was missing a mock for start_engine_process, causing it to
fail on CI where the IBus engine PID file path doesn't exist.
Some window managers (on both X11 and Wayland) hide the title-bar close
button for Gtk.Dialog windows that have no action buttons. The previous
fix (PR #333) added set_decorated(True) which was not sufficient.
This adds an explicit Close action button to the dialog so users always
have a visible way to dismiss it, regardless of WM behavior. The
instant-apply pattern is preserved — settings still apply immediately
on change.
When switching from whisper.cpp to VOSK via the settings dialog, the
_auto_apply_settings path called reconfigure() without first stopping
active recognition. This allowed background audio/transcription threads
to continue using the old engine's native C resources (whisper.cpp model)
while they were being freed by Python's GC, causing a segmentation fault.
Changes:
- Add stop_recognition() guard in reconfigure() itself when the engine
or model changes and recognition is active — this makes the API safe
regardless of which caller triggers the reconfiguration
- Add defense-in-depth stop_recognition() call in _auto_apply_settings
to match the existing pattern in _apply_settings_internal
* fix: bundle resources in package to fix missing icons in system tray (fixes#349)
The resources directory (icons and sounds) was not included in pip-installed
packages because it lived outside the src/ package directory. This caused
the resource_manager to find an empty icon directory at runtime, resulting in
the ellipsis (three dots) icon in the GNOME system tray.
Changes:
- Copy resources into src/vocalinux/resources/ so setuptools bundles them
- Add 'vocalinux.resources/**/*' to package-data in pyproject.toml
- Add MANIFEST.in for sdist builds
- Update resource_manager to check package-bundled resources first
(module_dir.parent / 'resources') before checking external paths
- Add install_resources_to_venv() to install.sh for curl-based installs,
copying resources to /share/vocalinux/resources
This fixes both pip installs (resources bundled in the wheel) and curl-based
installs (resources copied to venv share directory).
* fix: ensure bundled resources are included in sdist and pip installs
- Add 'vocalinux.resources' to setuptools packages list so the
subpackage (and its __init__.py) is recognized by setuptools
- Add __init__.py to src/vocalinux/resources/ to make it a proper
Python subpackage for reliable discovery
- Add 'recursive-include src/vocalinux/resources *' to MANIFEST.in
so sdist archives include the bundled icons and sounds
* chore(release): prepare v0.10.0-beta
Bump versions and release references across app, docs, installer, and website for the next beta release. Add v0.10.0-beta release notes entries covering reliability fixes and CI/test improvements since v0.9.0-beta.
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
* docs(release): align Python baseline references to 3.9+
Synchronize stale Python 3.8 references in agent guidance and distro dependency docs/scripts with the current 3.9+ minimum used by project metadata and release docs.
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
---------
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
* fix(keyboard): support AltGr (ISO_Level3_Shift) for Right Alt on int'l keyboards
On UK/International keyboards, Right Alt (AltGr) sends ISO_Level3_Shift
(keysym 0xfe03) instead of Alt_R (keysym 0xffea). The pynput backend
was only checking for keyboard.Key.alt_r, missing the alt_gr key that
pynput uses for ISO_Level3_Shift.
Changes:
- Add keyboard.Key.alt_gr to MODIFIER_NORMALIZE_MAP (normalizes to alt)
- Include alt_gr in the "alt" and "right_alt" key variants
- Update side-specific matching to check key variants instead of
exact key comparison
Fixes#322
* fix(keyboard): generalize pynput modifier alias matching
* test(keyboard): make variant tests independent of pynput install
When the kernel's evdev buffer overflows it sends SYN_DROPPED to signal
lost events. Without handling this, a missed key-release can leave
key_pressed_devices in a stale state, causing stuck push-to-talk.
Discard all events between SYN_DROPPED and the next SYN_REPORT, then
reset modifier key state for the affected device.
Refs: https://www.kernel.org/doc/html/latest/input/event-codes.html
* fix(tray): use icon names for AppIndicator updates
AppIndicator expects icon names resolved via theme path; passing absolute paths can result in blank or stuck status icons on some desktops. Use icon names for runtime updates and tighten tests to lock behavior.
* fix(resources): prefer populated asset directories
Selecting the first existing resources path could choose an empty install location and break tray icons and sounds. Score candidates by expected assets so runtime picks a usable resources directory.
* 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
* 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).
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)
* 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.
* 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
* 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.
* 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>
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>